MCP servers
Governing MCP servers in TypeScript via opt-in, per-instance autopatch.
Unlike LangChain.js / LangGraph.js / Mastra — auto-installed process-wide by raxit run's
NODE_OPTIONS preload — MCP has no framework-wide dispatch point to patch at import time.
There is no single base class every MCP server's tool dispatch runs through, so governance here
is opt-in and per-instance: you call installMcpAutoPatch(server) yourself, once, on your
own @modelcontextprotocol/sdk Server instance. It wraps server.setRequestHandler, so once
a tools/call handler is registered it is checked against policy — permit / deny, with
MODIFY support in the wrapper — before the handler runs. TypeScript only.
The ordering contract (load-bearing)
installMcpAutoPatch(server) only governs handlers registered after the call — it cannot
retroactively wrap a tools/call handler already registered before it runs.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { installMcpAutoPatch } from "raxit/autopatch";
const server = new Server({ name: "my-server", version: "1.0.0" }, { capabilities: { tools: {} } });
installMcpAutoPatch(server); // <-- MUST come first
server.setRequestHandler(CallToolRequestSchema, async (request) => {
// ... your tool dispatch — now governed permit/deny before it runs
});Get this backwards (register the handler, then call installMcpAutoPatch(server)) and the
tools/call handler runs completely ungoverned, silently. installMcpAutoPatch best-effort
warns to stderr if it detects an already-registered tools/call handler at patch time — but
don't rely on the warning catching every case; get the order right.
Run it yourself
npm install @raxitlabs/raxitBecause governance is wired explicitly in-source (not process-start injection), the governed
server just runs under plain node — no raxit run wrapper needed for the server process
itself.
How it works
The policy is default-deny with two allows:
apiVersion: raxit.security/v1
agent: mcp-support-bot
default: block
rules:
- effect: allow
resource: search_docs
- effect: allow
resource: list_open_tickets
# shell.exec is intentionally UNLISTED -> default-deny (deny.default_or_forbid).A denied call throws inside the wrapped handler; the MCP SDK's Protocol._onrequest turns that
rejection into a JSON-RPC error response, which client.callTool() surfaces as a rejected
McpError carrying the reason code in its message — the handler body never runs. Real captured
output from a run:
raxit MCP opt-in autopatch full demo · daemon=http://127.0.0.1:8426
| raw MCP support-bot server · installMcpAutoPatch(server) installed before tools/call handler
|
| [search_docs] -> search_docs OK — top hit for "refund policy": 'Refunds within 30 days are issued at face value.' (stub)
| [list_open_tickets] -> list_open_tickets OK — 2 open tickets for "cus_1" (stub)
| [shell.exec] BLOCKED -> McpError: MCP error -32603: [raxit BLOCKED] 'shell.exec' denied (reason: deny.default_or_forbid). The operation was not executed. Details: raxit explain <id>
PASS installMcpAutoPatch(server) engaged before tools/call was registered
PASS search_docs -> permit (ran)
PASS list_open_tickets -> permit (ran)
shell.exec reason=deny.default_or_forbid
PASS deny.default_or_forbid: shell.exec -> deny (BLOCKED, never ran) — governance LIVE, not just installed
audit.verify -> ok=true signatures_verified=true signed_count=3 count=3
PASS audit.verify: signed Decision Provenance chain verified green
ALL PASSThe shell.exec deny is asserted live — proof that governance actually engaged on this run,
not merely that the patch function was called without error.
MODIFY is supported by the wrapper, not demonstrated here
installMcpAutoPatch's wrapper substitutes a MODIFY verdict's rewritten arguments into the
request before the handler runs — the mechanism is the same shared engine every other
interception point uses. This example's policy only exercises permit/deny; it doesn't include a modify:
rule. See Mastra for a TS worked example that does exercise MODIFY
on the autopatch path.
What got denied and why
deny.default_or_forbid—shell.execappears nowhere in the policy, so it falls todefault: block. The handler body never ran, confirmed by the demo's own call log.
All three decisions land in one hash-chained, Ed25519-signed Decision Provenance log, verified
green with client.audit.verify({ signatures: true, pubkeys: [...] }). See
Audit and Decisions for the full
permit / defer / deny vocabulary and reason-code catalog.
Related
- LangChain.js / LangGraph.js — process-wide, auto-run-at-import autopatch — the contrast case for this page's opt-in, per-instance model.
- Mastra — another process-wide autopatch hook that additionally supports MODIFY on the autopatch path.

