Skip to main content

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 fieldSource in MCP
subject.idReturned by subjectResolver(ctx)
resource.typeFixed string "mcp_tool"
resource.nameThe registered tool name
resource.propertiesThe tool arguments
action.nameFixed 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 agentUrl to the agent's /v1/authorize. Typical sidecar: http://localhost:8181/v1/authorize.
  • Cloud — set cloudUrl alone 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 denyMessage is set, it's used verbatim; otherwise the adapter surfaces the reason returned 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.