MCP Governance: Why You Need to Intercept the Protocol, Not the Tool
Most MCP governance approaches wrap individual tools one at a time. obsvr governs the MCP Client itself with a non-mutating proxy, so every server, transport, and tool gets covered by the same interception point - no monkey-patching.
The Model Context Protocol is quickly becoming the standard way to give agents access to external systems - filesystems, databases, APIs, browsers. If you're building agents right now, you're either already using MCP servers or you're about to be.
The security posture most teams apply to MCP amounts to "be careful which servers you connect to." That's table stakes. It isn't governance.
The problem with wrapping individual tools
The obvious first approach to MCP governance is wrapping tools one at a time. You have a filesystem server exposing twelve tools, so you write a wrapper for each that checks arguments before calling through to the real thing.
It works, sort of, but it breaks down in three ways. It doesn't scale - a real deployment might run five servers with fifty-plus tools between them, and every wrapper is ongoing maintenance that grows with each new server you add. It misses tools added later, since MCP servers can expose new capabilities dynamically and your hand-written wrappers won't know they exist. And it's targeting the wrong layer entirely - the client is the single chokepoint every tool call already passes through, which is where the governance actually belongs.
Governing the client without patching it
obsvr's MCP integration wraps the MCP client with a non-mutating construct-trap proxy - so every tool call, on any server or transport, is governed without ever modifying the client's prototype. callTool and listTools are the two methods every tool call and every discovery run through, and the proxy intercepts both.
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { obsvrGovernMCP, getConfig } from '@obsvr/sdk';
// Returns a governed client class - the real prototype is never touched.
const GovernedClient = obsvrGovernMCP(Client, getConfig());
const client = new GovernedClient({ name: 'app', version: '1.0.0' }, { capabilities: {} });
Use the governed client and connect as many MCP servers as you need. Every callTool invocation gets policy-checked, PII-scanned, and logged before it's allowed to run; every tools/list discovery is scanned for poisoning and recorded. Because nothing is monkey-patched, it coexists with your APM and tracing.
What happens at interception
Three checks run in order. First, the policy check - is this tool on a denylist, or missing from an allowlist? If so, it's blocked and a blocked_call event goes into the log. Second, a PII scan on the tool arguments, which depending on your configured mode flags, redacts, or blocks the call. Third, your pre-call hook, if you've wired one up - execution pauses there until the hook resolves.
The tool only actually runs if it clears all three.
Building your own approval path
onPreCall is the extension point, not a finished product. obsvr doesn't ship a dashboard UI for humans to click approve or deny - what it gives you is the guarantee that the call pauses exactly where you tell it to, and that whatever your hook decides gets enforced and logged either way. Plenty of teams wire this into a Slack approval bot, an internal ticketing flow, or a lightweight review queue they already have. What lands in the audit chain either way is the decision, who or what made it, and how long it took.
Why this matters as MCP scales
MCP adoption keeps accelerating - Claude Desktop, Cursor, and most major agent frameworks support it natively now, and the surface area an agent can reach grows every week. Governing that tool by tool doesn't hold up. You need a governance point at the protocol layer that catches everything on a server, including the tools it hasn't exposed yet.
What the protocol gives you to govern with
Interception is only useful if the protocol carries enough structure to make decisions from. MCP does, and the fields that matter are worth naming.
tools/list returns each tool's name, description, and input schema. The description is the part that reaches the model and shapes its behavior, which makes it both the most useful thing to inspect and the most dangerous thing to trust. tools/call carries the tool name and the arguments the model chose. Between them you can answer the two questions that matter: is this tool permitted at all, and are these arguments within bounds.
That second question is the one tool allowlists alone miss. A read_file tool on the allowlist is fine until the argument is ../../.env. Governing at the protocol layer means the argument is available to inspect at the moment of the call, in a structured form, before execution. Path traversal, destructive SQL, an endpoint outside your allowed hosts: these are argument-level decisions, and they need argument-level enforcement.
The pinning problem
There is a subtler issue that protocol-level governance is uniquely placed to solve.
A server can change a tool's description between the moment you reviewed it and any later call. Nothing in the protocol prevents that, and nothing surfaces it. You approved a tool whose description said one thing; the model is now reading a description that says something else. Reviewing servers at integration time gives you a snapshot, and the snapshot goes stale silently.
The defensible answer is to hash what the agent was actually shown and record it with the decision. If a tool's content hash changes, that is a fact you can detect and alert on rather than a change that happens invisibly. It also means the audit record answers a question that would otherwise be unanswerable months later: not just which tool ran, but which version of its description the model was reading when it decided to run it.
Where this sits relative to the server's own permissions
Worth being clear that protocol-level governance is not a replacement for server-side access control, and treating it as one would be a mistake.
The MCP server still enforces its own permissions, and it should. What client-side governance adds is a control point you own, applied uniformly across every server you connect, that does not depend on each server implementing its policy correctly or consistently. Server permissions answer "is this client allowed to call this tool." Client-side governance answers "should this agent, in this context, be doing this right now" - and only one of those is a question your compliance team can configure.
The two compose, and the practical value of the client-side layer is that it holds even when a server is compromised, misconfigured, or simply written by someone whose threat model was not yours.