Skip to content

Pre-release. v0.1 is not out yet, so there is nothing to install and no public source to clone — the quickstart builds from a checkout.

Protect an MCP server

@subactid/mcp applies the tool server rules to an MCP server:

  • every call carries a task token;
  • every tool names the scope it needs;
  • every call is logged with the human and the agent.

@subactid/mcp lists every option and method.

Lines 20–24 of the SDK’s examples/mcp-jira/index.ts:

examples/mcp-jira/index.ts
const guard = new SubactIdGuard({
issuer: process.env['SUBACTID_ISSUER'] ?? 'http://127.0.0.1:5100',
audience: 'https://jira.internal',
tools: { search: { scope: 'jira:read' }, comment: { scope: 'jira:comment', highRisk: true } },
});

tools is an allowlist. A tool not listed is refused with unknown_tool. A new tool cannot be called until you add it here with its scope.

Lines 25–29 of the same file, below its imports:

examples/mcp-jira/index.ts
createServer((req, res) => serve(req, res).catch((e) => guard.reject(res, e))).listen(loopback());
async function serve(req: IncomingMessage, res: ServerResponse): Promise<void> {
const auth = await guard.authenticate(req.headers.authorization);
await (await jira()).handleRequest(Object.assign(req, { auth }), res);
}
  • guard.authenticate verifies the token at the transport, before any MCP code runs.
  • guard.reject answers a failure with the right status and WWW-Authenticate header.
  • On Express with the MCP SDK’s middleware, requireBearerAuth({ verifier: guard }) does the same job.

@modelcontextprotocol/sdk is a peer dependency. Install it beside @subactid/mcp.

Then wrap the MCP server, so each tool call is checked against its policy. Lines 33–40 of the SDK’s examples/mcp-jira/index.ts:

examples/mcp-jira/index.ts
const server = guard.protect(new McpServer({ name: 'jira', version: '0.1.0' }));
server.registerTool(
'search',
{ description: 'Find issues by text.', inputSchema: { query: z.string() } },
async ({ query }) => ({
content: [{ type: 'text', text: `PROJ-1: "${query}" reported by a user` }],
}),
);

protect returns the same server. Register tools as usual. Call protect before registering any tool: it throws if a tool is already registered.

highRisk: true makes the guard introspect at the control plane on every call to that tool. A revoked token is then refused at once, not when it expires. Mark tools that write or change something, such as comment.

A token for an audience in the agent’s high_risk_audiences carries introspect_required, and the guard introspects it on every call without any tool being marked. The introspection done at the transport counts for the first tool call. Each later call on the same authentication is introspected again.

Option Default What it does
issuer — The control plane’s issuer URL
audience — What this server is: the aud a token must carry
tools — Every tool and the scope it requires. Others are refused
requireActor true Refuse tokens with no act claim, so only agents get in
maxDelegationDepth 1 Longest act chain accepted
clockSkewSeconds 60 Tolerance on exp, nbf and iat
timeoutMs 10000 Longest a call to the control plane may take
realm the audience Named in WWW-Authenticate
log one JSON line on stderr Where every call is logged

The default log goes to stderr because an MCP server on a stdio transport uses stdout for the protocol.

{
"event": "tool.call",
"at": "2026-09-09T14:04:07.221Z",
"tool": "comment",
"decision": "allow",
"sub": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"act": "agent:jira-triage",
"depth": 1,
"task_id": "task_01HQZX9K4M",
"jti": "tok_01HQZX9K5P"
}

sub is the human the agent acted for. Log it even on a server whose tools only read.

Subact ID decides by scope, audience, lifetime and delegation depth. Whether this user may touch a particular issue is your application’s decision. The claims are available in the handler to make it.

Subact ID Pre-release. v0.1 is not out yet.

© 2026 Nikola Živković PR Agencija za programerske usluge Novi Sad. Subact ID is its product.

LegalTermsPrivacy