Enforcer v3 docs / MCP Server Authorization

05MCP
05

For AI agents

MCP server authorization

An MCP server hands an agent a set of tools. That makes every tool call an authorization question: this caller, this tool, these arguments, right now. Enforcer answers it.

Connect Enforcer over MCP

Enforcer is itself an MCP server, listed in the official registry as dev.instruxi.enforcer/v3.

url        https://api.instruxi.dev/mcp
transport  Streamable HTTP
header     X-API-Key: <an Enforcer API key>

That is the whole connection. If your client takes a JSON config:

{ "mcpServers": { "enforcer": {
    "type": "http",
    "url": "https://api.instruxi.dev/mcp",
    "headers": { "X-API-Key": "$ENFORCER_API_KEY" } } } }

If it is Claude Code specifically, claude mcp add --transport http enforcer https://api.instruxi.dev/mcp --header "X-API-Key: $ENFORCER_API_KEY" does the same thing. If your client has no MCP support, skip it: the REST API below does everything the tools do.

Check a tool call before you run it

In your own MCP server, treat the tool name as the action and its target as the resource, and make the check before the handler does any work.

// in your tool handler, before doing the work
const d = await fetch(`${BASE}/authz/check`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${callerToken}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ action: toolName, resource: { type: 'document', id: args.id }, contexts: args })
}).then(r => r.json());

if (!d.allow) return { isError: true, content: [{ type: 'text', text: d.reason }] };

Returning reason to the agent matters: it can then tell the user what would make the answer yes, instead of retrying the same refused call.

Arguments are part of the question

Allowing a tool is not the same as allowing every call to it. Pass the arguments in contexts so a rule can care about the amount, the recipient, or the destination, not just the tool name. contexts only reaches a policy you name with policy_id; the built-in baseline does not read it, so a check without a policy_id silently ignores the arguments you sent.