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/sdk1.20 or later, below 2, is a peer dependency. Install it yourself.@subactid/serveris a dependency and is installed with it.- Not on npm yet.
Protect an MCP server walks through it.
SubactIdGuard
Section titled “SubactIdGuard”One guard per server.
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).
Methods
Section titled “Methods”| 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:
clientIdis the token’sclient_id, or elseact.sub;scopesandexpiresAtcome from the token;extra.subactidholds the verified claims as aTaskToken.
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.
Introspection
Section titled “Introspection”- A token carrying
introspect_requiredis introspected at the transport. That answer covers the first tool call of that authentication only. Every later call is introspected again. - A tool marked
highRiskis 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.
The log line
Section titled “The log line”{ "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.
Refusals
Section titled “Refusals”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 |
© 2026 Nikola Živković PR Agencija za programerske usluge Novi Sad. Subact ID is its product.