MCP Adapter
@authzx/mcp-adapter wraps every tool registered on a Model Context Protocol server with an AuthzX authorization check. Before a tool runs, the adapter asks AuthzX "is this caller allowed to invoke this tool with these arguments?" and short-circuits the call on a deny.
One line of integration. Tool arguments flow into resource.properties so you can write ABAC conditions like "support_rep can invoke issue_refund only if amount < 100" without touching adapter code.
Install
npm install @authzx/mcp-adapter @modelcontextprotocol/sdk
Requires Node 18+.
Quick start
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { AuthzXAdapter } from "@authzx/mcp-adapter";
import { z } from "zod";
const server = new McpServer({ name: "acme-support", version: "1.0.0" });
server.tool(
"issue_refund",
{ customer_id: z.string(), amount: z.number() },
async ({ customer_id, amount }) => {
return await payments.refund(customer_id, amount);
},
);
const authzx = new AuthzXAdapter({
apiKey: process.env.AUTHZX_API_KEY,
agentUrl: "http://localhost:8181/v1/authorize",
subjectResolver: (ctx) => (ctx.mcpContext as any)?.session?.userId,
});
authzx.wrapAllTools(server); // wraps every registered tool in place
server.start();
After wrapAllTools, every tool invocation is intercepted. If AuthzX denies, the original handler does not run — the MCP client gets an error back with a deny reason.
How it maps to AuthzX
For each tool call, the adapter POSTs:
{
"subject": { "id": "<from subjectResolver>", "type": "user" },
"resource": {
"type": "mcp_tool",
"name": "<tool name>",
"properties": <tool arguments>
},
"action": { "name": "invoke" }
}
| AuthzX field | Source in MCP |
|---|---|
subject.id | Returned by subjectResolver(ctx) |
resource.type | Fixed string "mcp_tool" |
resource.name | The registered tool name |
resource.properties | The tool arguments |
action.name | Fixed string "invoke" |
Resources are matched by type + name, so you don't have to pre-register every tool as an AuthzX resource UUID. Write policies against the tool name and you're done.
Configuration
new AuthzXAdapter({
// Auth — pick one
apiKey: "azx_...",
// or OAuth Client Credentials:
clientId: "client_...",
clientSecret: "azx_cs_...",
// Endpoints — pick one or both.
// If both are set, the agent is tried first and the cloud is the fallback.
agentUrl: "http://localhost:8181/v1/authorize",
cloudUrl: "https://api.authzx.com/v1/authorize", // default
// Resolve the subject id from the MCP request context.
subjectResolver: (ctx) => ctx.mcpContext.session.userId,
// Optional. Suppresses the AuthzX reason string in the deny response
// returned to the MCP client.
denyMessage: "forbidden",
timeoutMs: 10_000,
});
Subject resolution
There is no user identity in the MCP tool-call payload by default — the protocol carries only the tool name and arguments. You decide what identity becomes the AuthzX subject.
// End-user identity (default pattern for v1).
// Alice signs in to the MCP server; her id travels in session/headers.
subjectResolver: (ctx) => ctx.mcpContext.session.userId;
// Agent-as-subject. The agent itself has an AuthzX identity.
// Useful for autonomous / scheduled agents.
subjectResolver: () => "agent_abc";
Delegation patterns (Alice asked Claude, which is invoking issue_refund — both must be allowed) are on the roadmap. The resolver is shaped to support them forward-compatibly.
Endpoints
- Local agent — fastest. Set
agentUrlto the agent's/v1/authorize. Typical sidecar:http://localhost:8181/v1/authorize. - Cloud — set
cloudUrlalone if you don't run an agent, or leave both set for automatic fallback when the agent is unreachable.
See AuthzX Agent for running the agent locally.
Usage patterns
Wrap all tools after registration
server.tool("lookup_customer", schema, handler);
server.tool("issue_refund", schema, handler);
server.tool("delete_account", schema, handler);
authzx.wrapAllTools(server);
Recommended default — covers every tool including ones added by third-party extensions.
Wrap per tool at registration
const authzx = new AuthzXAdapter({ /* ... */ });
server.tool(
"issue_refund",
schema,
authzx.wrapTool(
async ({ customer_id, amount }) => payments.refund(customer_id, amount),
"issue_refund",
),
);
Use when you want some tools gated and others not.
Manual check
const result = await authzx.check(
{ toolName: "issue_refund", args, mcpContext: extra },
"issue_refund",
args,
);
if (!result.decision) throw new Error(result.context.reason);
Use inside custom handlers that need the decision result before deciding how to respond.
Writing policies against tool calls
The adapter's mapping makes tool-call authorization work like any other AuthzX policy. With the application and mcp_tool resource type in place, everything else is a standard RBAC/ABAC policy.
Example — support_rep can refund up to $100; support_manager bypasses the limit:
resource "authzx_resource_type" "mcp_tool" {
application_id = authzx_application.support.id
name = "mcp_tool"
actions = ["invoke"]
}
resource "authzx_resource" "issue_refund" {
application_id = authzx_application.support.id
name = "issue_refund"
type = authzx_resource_type.mcp_tool.id
}
resource "authzx_policy" "rep_small_refunds" {
application_id = authzx_application.support.id
name = "rep-can-refund-under-100"
effect = "ALLOW"
priority = 50
resources = [{
resource_id = authzx_resource.issue_refund.id
actions = ["invoke"]
}]
condition = "input.resource.properties.amount < 100"
}
resource "authzx_policy_assignment" "rep_refund_policy" {
policy_id = authzx_policy.rep_small_refunds.id
entity_type = "role"
entity_id = authzx_role.support_rep.id
}
Because tool arguments arrive in resource.properties, your policy conditions reference them directly — no adapter-side filtering, no wrapping per tool.
Deny behavior
The adapter fails closed. A deny means:
- The original tool handler does not run.
- The MCP client receives an error response. If
denyMessageis set, it's used verbatim; otherwise the adapter surfaces thereasonreturned by AuthzX.
Network errors, 401s, and 5xx responses from AuthzX are treated as denies. When both agentUrl and cloudUrl are configured, a network or 5xx failure against the agent triggers a fallback to cloud before the adapter denies.
Reference demo
authzx-ai-agent-demo is a runnable end-to-end example — MCP server, three tools, two users, RBAC + ABAC policies seeded via Terraform. Clone, terraform apply, npm run demo, read the output.
Source
github.com/authzx/authzx-mcp — MIT.