Skip to content

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 builder
const server = options.wrap ? options.wrap(new McpServer(info, opts)) : new McpServer(info, opts);
// your connector
createServer: (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.

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'],
);

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 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.

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.