@bolyra/mcp
Gate MCP tool calls so only authorized agents can call sensitive tools.
Quick Start (60 seconds)
npm install @bolyra/mcp @bolyra/sdk @modelcontextprotocol/sdk
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { withBolyraAuthStdio } from '@bolyra/mcp';
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
server.tool('read_file', { path: { type: 'string' } }, async (args) => ({
content: [{ type: 'text', text: `Reading ${args.path}` }],
}));
withBolyraAuthStdio(server.server, {
devMode: true,
toolPolicy: {
read_file: 1n,
},
});
That's it. Every tools/call now requires a valid Bolyra proof bundle.
Dev mode (full example)
Dev mode uses mock proofs β no circuit artifacts, no trusted setup, instant startup. Use it to build and test your server before wiring real ZKP verification.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { withBolyraAuthStdio } from '@bolyra/mcp';
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
server.tool('read_file', { path: { type: 'string' } }, async (args) => ({
content: [{ type: 'text', text: 'file contents...' }],
}));
withBolyraAuthStdio(server.server, {
devMode: true,
toolPolicy: {
read_file: 1n,
write_file: 2n,
},
});
await server.connect(new StdioServerTransport());
Client side:
import { createDevIdentities, attachBolyraProof } from '@bolyra/sdk';
const { human, agent } = await createDevIdentities();
const auth = await attachBolyraProof(human, agent, { devMode: true });
await client.callTool({ name: 'read_file', arguments: { path: '/tmp/x' }, _meta: auth.meta });
await fetch('/mcp', { headers: { ...auth.headers }, ... });
How it works
Every tools/call request must carry a proof bundle: a pair of Groth16 proofs (one from the human's circuit, one from the agent's) bound to a shared session nonce.
The server side:
- Extracts the bundle from
params._meta.bolyra (stdio) or the Authorization: Bolyra <base64> header (HTTP).
- Verifies the handshake β both proofs, nonce freshness, score floor.
- Checks the tool's permission policy against the agent's effective bitmask.
- Attaches a
BolyraAuthContext to the request for downstream handlers.
- Rejects with an MCP error if any step fails.
Discovery calls (initialize, tools/list) pass through unauthenticated.
Delegation chains (v=2 bundles) narrow scope from root credential to leaf agent across multiple hops. The effective bitmask seen by tool policies is always the leaf's β the most-restricted scope.
API reference
Server β stdio
withBolyraAuthStdio(server: McpServer, config: BolyraMcpConfig): void
Wraps an McpServer instance. Must be called before server.connect(transport).
Server β HTTP
bolyraAuthMiddleware(config: BolyraMcpHttpConfig): express.RequestHandler
Express middleware. Mount before your MCP HTTP handler. Rejects unauthenticated tools/call requests with HTTP 401.
Client helpers
attachBolyraProof(
human: HumanIdentity,
agent: AgentCredential,
options?: AttachProofOptions,
): Promise<BolyraClientAuth>
Runs a handshake and returns { headers, meta, bundle }. Pass options.devMode = true to skip real proving and emit a mock bundle.
attachDelegatedBolyraProof(
human: HumanIdentity,
rootCred: AgentCredential,
hops: DelegationHopSpec[],
options?: AttachProofOptions,
): Promise<BolyraClientAuth>
Like attachBolyraProof but walks a delegation chain and returns a v=2 bundle.
Verification
verifyBundle(bundle: BolyraProofBundle, config: BolyraMcpConfig): Promise<BolyraAuthContext>
Verify a bundle directly β useful for custom transports or offline verification.
checkToolPolicy(
toolName: string,
ctx: BolyraAuthContext,
policy: ToolPermissionPolicy,
): { allowed: boolean; reason?: string }
Check a BolyraAuthContext against a tool's required bitmask.
Dev identities (re-exported from SDK)
createDevIdentities(options?: DevIdentityOptions): Promise<DevIdentities>
Returns fixed-seed { human, agent, operatorKey } β deterministic, no circuit artifacts required. Logs a warning on first call. Never use in production.
Production configuration
Swap devMode: true for a real resolveCredential resolver and point the SDK at your circuit artifacts:
withBolyraAuthStdio(server, {
resolveCredential: async (commitment) => myRegistry.get(commitment),
toolPolicy: {
read_file: 1n,
write_file: 2n,
},
sdkConfig: {
circuitDir: '/path/to/circuits/build',
rpcUrl: 'https://sepolia.base.org',
registryAddress: '0x2781dF8b6381462d881C833Fb703d68c661c9577',
},
});
Full config interface:
interface BolyraMcpConfig {
network?: string;
minScore?: number;
maxProofAge?: number;
toolPolicy?: ToolPermissionPolicy;
devMode?: boolean;
resolveCredential?: (commitment: string) => Promise<AgentCredential | null>;
sdkConfig?: BolyraConfig;
}
The HTTP variant adds authScheme?: string (default "Bolyra").
Transport guide
| Transport | Bundle location | Notes |
|---|
| stdio (Claude Desktop, Cursor, Cline) | params._meta.bolyra | MCP spec defines no stdio auth surface; _meta is the only protocol-level field |
| HTTP / SSE / Streamable-HTTP | Authorization: Bolyra <base64-bundle> | Custom auth scheme per RFC 7235; aligns with OAuth 2.1 resource-server pattern |
Both produce the same BolyraAuthContext on the server side. Tool handlers don't need to know which transport was used.
Example
See examples/protected-file-server/ for a complete stdio server + client pair using dev mode. Run it with:
cd integrations/mcp
npm run example:protected-file-server