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, getEngine, getSink } from "checkrd";
import { wrapMcpClient } from "checkrd/mcp";

await initAsync({
  agentId: "github-agent",
  apiKey: process.env.CHECKRD_API_KEY!,
});
// initAsync fetches the agent's published policy from the dashboard
// and installs it before resolving — server is the source of truth.

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: getEngine(),
  enforce: true,
  agentId: "github-agent",
  sink: getSink(),
  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 { initAsync, getEngine, getSink } from "checkrd";
import { wrapMcpServer } from "checkrd/mcp";

// Initialize first so getEngine() / getSink() can hand back the
// runtime objects the wrapper attaches to.
await initAsync({
  agentId: "my-server",
  apiKey: process.env.CHECKRD_API_KEY!,
});

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

const server = wrapMcpServer(rawServer, {
  engine: getEngine(),
  enforce: true,
  agentId: "my-server",
  sink: getSink(),
  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
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.