Servers you did not write
For an MCP server you already have, from another package or one you are not ready to change, whose tools were never declared with a permission.
// one hook in their builderconst server = options.wrap ? options.wrap(new McpServer(info, opts)) : new McpServer(info, opts);// your connectorcreateServer: (principal) => buildServer(config, { wrap: (server) => gate(server, principal, { get_case: 'testrail:read', delete_run: 'testrail:delete', }), }),The reader never sees delete_run in tools/list, from a builder that knows
nothing about any of this. Their calls land in your onAudit log, which is the
only record tying a person to an action when the downstream API sees one service
account.
Where the principal comes from
Section titled “Where the principal comes from”The connector above has one in scope because a verifier produced it. When the
caller was authenticated some other way — a session your gateway already
validated, or a test — build the Identity yourself. Every field the verifier
would have proved is required, emailVerified included, so an unproven email
cannot quietly satisfy a policy rule that matches on one.
import { createPrincipal } from 'mcp-authz';
const principal = createPrincipal( { issuer: 'https://login.acme.com', sub: 'user_123', email: 'dana@acme.com', emailVerified: true, claims: {}, }, ['reader'], ['testrail:read'],);Where the permission map comes from
Section titled “Where the permission map comes from”Reading it off the server beats typing it: recordCapabilities
lists what your server registers, renders the starting map, and records each
capability’s definition so one that changes under an unchanged name fails a snapshot.
A tool with no price throws
Section titled “A tool with no price throws”A registration with no entry in the map throws, naming the tool. Dropping it either way is how a tool nobody priced ends up reachable by everyone, or by nobody, with no error to read.
Why the hook is required
Section titled “Why the hook is required”The SDK keeps a built server’s tool list private, so nothing can filter what it never saw. That hook is two lines, and it is worth asking an upstream maintainer for.
Pass the same map as permissions to createMcpFetch and the
boot-time check still covers the roles
side.