checkrd

Model Context Protocol (TypeScript)

Wrap MCP clients and servers with Checkrd policy enforcement.

Model Context Protocol — TypeScript

The MCP TypeScript SDK has no app-level middleware chain. Checkrd ships two adapters that target the canonical interception points:

  • wrapMcpClient(client, options) — Proxy-wraps an MCP client so callTool, readResource, and getPrompt calls are policy-evaluated before they reach the server.
  • wrapMcpServer(server, options) — Proxy-wraps an MCP server's request-handler registration so every tool / resource / prompt request is policy-evaluated before the user's handler runs.

Install

bash
npm install checkrd @modelcontextprotocol/sdk

Client side

typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { initAsync } from "checkrd";
import { wrapMcpClient } from "checkrd/integrations/mcp";

const checkrd = await initAsync({
  policy: "policy.yaml",
  agentId: "github-agent",
});

const transport = new StdioClientTransport({
  /* ... */
});
const rawClient = new Client(
  { name: "github-agent", version: "1.0.0" },
  { capabilities: {} },
);
await rawClient.connect(transport);

const client = wrapMcpClient(rawClient, {
  engine: checkrd.engine,
  enforce: true,
  agentId: "github-agent",
  sink: checkrd.sink,
  serverName: "github-mcp",
});

// Each call is policy-evaluated before reaching the server.
await client.callTool({ name: "create_issue", arguments: { title: "..." } });

Server side

typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { wrapMcpServer } from "checkrd/integrations/mcp";

const rawServer = new Server(
  { name: "my-server", version: "1.0.0" },
  { capabilities: { tools: {} } },
);

const server = wrapMcpServer(rawServer, {
  engine: checkrd.engine,
  enforce: true,
  agentId: "my-server",
  sink: checkrd.sink,
  serverName: "my-server",
});

// Every handler the user registers is wrapped automatically.
server.setRequestHandler(callToolRequestSchema, async (request) => {
  // your handler runs only if the policy allowed the call
});

What gets enforced

MethodSynthetic URL
callTool({name, arguments})https://{serverName}/tools/{name}
readResource({uri})https://{serverName}/resources?uri={uri}
getPrompt({name, arguments})https://{serverName}/prompts/{name}
listTools() etc.https://{serverName}/tools (kind=list)
yaml
agent: github-agent
default: deny

rules:
  - name: allow-issue-create-and-list
    allow:
      url: "github-mcp/tools/create_issue|github-mcp/tools/list_issues"

  - name: allow-listing
    allow:
      url: "github-mcp/tools|github-mcp/resources|github-mcp/prompts"

Why MCP

As of April 2026 the MCP SDK sees ~97M monthly downloads across Python and TypeScript. The Linux Foundation hosts the spec under the Agentic AI Foundation. 30+ CVEs have been disclosed in the server ecosystem in its first year. An audited, policy-enforcing middleware is the missing piece nobody else has shipped at this layer.

Caveats

  • Duck-typed. The adapters target the structural shape of @modelcontextprotocol/sdk rather than importing concrete classes. MCP's surface is iterating quickly; this lets a minor SDK bump not force a Checkrd release.
  • List operations are evaluated as kind=list. Default-allow policies should explicitly allow listing.
  • No app middleware chain. Tool-call gating is per-handler; transport-level concerns (auth headers, IP allowlist) belong in your HTTP framework's middleware.