Enforcer
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. The Model Context Protocol itself does not answer it. The spec has no mechanism for per-tool authorization, argument constraints, delegation or audit, which is why the question keeps coming up in the spec's own discussions.
The obvious first move is to allow some tools and block others. It fails for a specific reason worth understanding: one allowed call can change the next one. An allowed read returns untrusted text, that text changes the arguments of an allowed write, and both calls were on the allowlist. Server and tool allowlists answer identity questions. They do not preserve authorization intent after an allowed read returns something that changes what happens next.
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 }] };
Passing the arguments in contexts is the part people skip. Allowing a tool is not the same as allowing every call to it, and a rule usually needs to care about the amount, the recipient or the destination rather than just the name.
If a sensitive action needs a person to approve it, bind the approval to the exact request: the actor, the resource, the amount, and an expiry. Otherwise the agent can resubmit the same action with a fresh timestamp and reuse an approval that was never given for it. Approval-per-call also stops scaling once you have more than a handful of tools, so reserve it for actions a human would have to unwind.
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>
Any MCP client connects from those three lines. Claude Code has a shortcut (claude mcp add --transport http enforcer https://api.instruxi.dev/mcp --header "X-API-Key: $ENFORCER_API_KEY"), and a client with no MCP support can skip it entirely: the REST API does everything the tools do.