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.

@subactid/mcp

@subactid/mcp is @subactid/server adapted to the MCP SDK. It verifies the task token at the transport, decides each tool call from the token’s scopes and the tool’s policy, and logs one line per call with the human and the agent.

  • ESM only, Node 20 or later.
  • @modelcontextprotocol/sdk 1.20 or later, below 2, is a peer dependency. Install it yourself.
  • @subactid/server is a dependency and is installed with it.
  • Not on npm yet.

Protect an MCP server walks through it.

One guard per server.

examples/mcp-jira/index.ts, lines 20–24
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 } },
});
Option Type Default What it is
issuer string — The control plane’s issuer URL
audience string — What this server is
tools Record<string, ToolPolicy> — Every tool and what it requires
requireActor boolean true Refuse tokens with no act
maxDelegationDepth number 1 Longest act chain accepted
clockSkewSeconds number 60 Skew tolerated
timeoutMs number 10000 Longest a call to the control plane may take
realm string the audience Named in WWW-Authenticate
log (event: CallEvent) => void one JSON line on stderr Where every call is logged
fetch, now — the globals For tests

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

A ToolPolicy is { scope, highRisk? }. scope is a string or an array, and the token must carry every one. The constructor throws if a tool lists no scope. A tool that is not listed is refused with unknown_tool (403).

Method Returns What it does
protect(server) the server Puts the guard in front of every tool call the server dispatches
decide(tool, authInfo) Promise<SubactIdAuthError | undefined> Decides one call and logs it. undefined means allowed
verifyAccessToken(token) Promise<AuthInfo> The MCP SDK’s OAuthTokenVerifier, for its bearer middleware
verify(token) Promise<AuthInfo> The same check, refusing with an SubactIdAuthError
authenticate(authorization) Promise<AuthInfo> The same, from an Authorization header
reject(response, error) undefined Answers a refused request on a Node ServerResponse

protect throws if it cannot read the server’s request handlers, or if a tool is already registered. Call it before registering tools.

verify applies the actor rules at the transport, so a token with no act, or a chain that is too deep, never reaches a tool. In the AuthInfo it returns:

  • clientId is the token’s client_id, or else act.sub;
  • scopes and expiresAt come from the token;
  • extra.subactid holds the verified claims as a TaskToken.

reject answers 500 with no detail for an error that is not an SubactIdAuthError. If headers were already sent, it closes the connection.

The package also re-exports SubactIdAuthError, SubactIdToolServer, verifyTaskToken, JwksCache and their types from @subactid/server. SubactIdMcpError is another name for SubactIdAuthError.

  • A token carrying introspect_required is introspected at the transport. That answer covers the first tool call of that authentication only. Every later call is introspected again.
  • A tool marked highRisk is introspected on every call. When the transport already introspected for this call, that answer is used.
  • Otherwise, a call makes no request to the control plane beyond fetching keys.
{
"event": "tool.call",
"at": "2026-09-12T14:31:05.412Z",
"tool": "comment",
"decision": "deny",
"reason": "not_active",
"sub": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"act": "agent:jira-triage",
"depth": 1,
"task_id": "task_01HQZX9K4M",
"jti": "tok_01HQZX9K5P"
}

One CallEvent per call, allowed or refused. instance is added when the token has act.instance. A line never contains a token.

The control plane does not record tool calls. tool.called is reserved in the spec and not produced, so this log is the record of what an agent did with a token. See the audit ledger.

The same SubactIdAuthError reasons as @subactid/server, plus unknown_tool.

Path How a refusal arrives
verifyAccessToken As the MCP SDK’s own error: 401 is InvalidTokenError, 403 is InsufficientScopeError, 503 is ServerError (answered 500)
verify, authenticate As an SubactIdAuthError
A tool call As a tool result with isError: true and text naming the reason. A task-augmented call gets a protocol error instead
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