SMCP Tool Governance: Attach, Detach, and Help as a Protocol

The Model Context Protocol made it easy to give an agent everything. That is also the problem. The first time you wire a serious product API into an MCP server

The Model Context Protocol made it easy to give an agent everything. That is also the problem. The first time you wire a serious product API into an MCP server and watch a chatter agent stare at thirty-five tool definitions before the user has finished a sentence, the question changes from β€œcan the agent do this?” to β€œdoes the agent even see the right tool for what they’re trying to do?”

This post is the architectural write-up behind a phase of work we are starting on SMCP (the Sanctum implementation of MCP, sanctumos/smcp): treat tool attachment as a first-class concern of the protocol layer, not as a side effect of whatever IDE or agent harness happens to be in front of it.


How we got here

We had a perfectly reasonable design.

The Sanctum Tasks SMCP plugin (smcp_plugin/tasks/cli.py) mirrors the Python SDK one-for-one: 35 commands, ~152 parameter slots, full API parity. That was the right Phase 1 trade. We wanted nothing blocked while the in-app Q Vernal agent was rehearsing on lettatest. We wanted Otto's local Cursor MCP and Q's lettatest Letta agent to share one source of truth. We did not want to argue about scope.

So we shipped everything.

Then we watched it run.

update-task has 17 parameters. list-tasks has 16. create-task has 13. The plugin exposes user CRUD, API key management, audit log reads, organization listings, bulk-create. Q is an in-app support chatter for an admin UI. She does not need create-api-key. She does not need reset-user-password. She does not need list-audit-logs. She needs maybe twelve verbs, and the rest is noise that competes for context with her persona, her job rules, and the chat history we worked hard to surface.

When that bloat hit a real incident β€” a user asking the agent to "save this architecture for the wizard" and the agent picking the wrong directory project because the catalog was too wide and the operator rules were too generic β€” the diagnosis got concrete. The plugin was not wasteful on disk. It was wasteful in the agent's head.


Why "consolidate the tools" is not the fix

The obvious reaction to "too many tools" is "fewer tools, more arguments." Make tasks__invoke with action="create-task". Collapse the catalog. One door, many rooms.

That trade is almost never as clean as it sounds.

A model picking the wrong tool name out of 35 is a recognizable failure: it shows up as a wrong tool call. A model picking the wrong action enum out of one mega-tool, with thirty subtly different parameter shapes hanging off it, is a quieter failure: bad JSON, malformed payloads, partial success that looks like real success. From years of watching tool-calling models in production, the empirical sweet spot is somewhere between ten and fifteen well-named verbs, not three Swiss-army nouns with sprawling argument trees.

Compressing 35 verbs into 3 mega-tools moves the confusion from the verb layer into the argument layer. It does not remove it.

The actual fix is older than MCP: separate what a server can do from what an agent is allowed to use.


What the major harnesses do today

Before designing anything, we checked what the field looks like in mid-2026. Three patterns, three different philosophies.

Letta treats this as a first-class concern. You register an MCP server, list the tools it exposes, and then attach a chosen subset to an agent via PATCH /v1/agents/{id}/tools/attach/{tool_id}. The catalog on the server is intentionally larger than the catalog on the agent. Q's existing attach script on lettatest already uses this β€” we just attach everything today, which is the issue.

Cursor lets you toggle individual MCP tools in Settings β†’ Tools & MCP, and the third-party guidance is consistent: above roughly forty active tools across all servers, quality degrades and the IDE may silently hide tools. There is also ~/.cursor/permissions.json with an mcpAllowlist for auto-run without approval. There is no public HTTP API for one process to remotely flip the toggles in another developer's Cursor session.

Claude Code approaches this through permissions (allow / deny / ask rules using mcp__server__tool patterns) and through Tool Search, an opt-out feature that defers loading tool definitions into context until they look relevant. Different lever, same underlying admission that loading every tool every turn is not free.

The takeaway is twofold. First: every serious agent harness already has the concept of "exposed by the server" versus "active for the agent." They just spell it differently. Second: only Letta gives plugin code a clean API to manipulate that set. Cursor and Claude live closer to the user β€” they expect a human in front of a settings panel.

That second gap is what SMCP can close.


Two planes, then a third

The architectural move that unlocks the rest of this is small but it has to be said clearly. There are two planes of "what tools exist," and right now we treat them as one.

The catalog plane is what each plugin can do. A tasks plugin declares its full surface via --describe. An invoicing plugin does the same. SMCP discovers them and knows the universe.

The session plane is what is active for a particular MCP connection right now. This is per-process, per-client, per-policy. A Cursor session for an admin might want everything. A Letta agent representing an in-app chatter wants twelve verbs.

There is a third plane that some harnesses also expose β€” Letta's agent tool_ids, Cursor's UI toggles β€” but the third plane is optional sync, not the source of truth. If we make SMCP own the session plane, the entire stack works even when the third plane is missing or read-only.

The current SMCP server already builds an all_tools list inside register_plugin_tools() and serves it from @server.list_tools(). The smallest possible change is to make list_tools() return the attached subset plus one governor tool, and to make call_tool() return a structured error if a detached tool is called. Nothing about the plugin contract changes. Nothing about how Cursor or Claude or Letta connect changes. The model's view of the world simply matches policy.


The governor tool

Once SMCP owns attach state, you need one entry point for everything that asks about, manipulates, or explains that state. Five separate meta-tools would re-create the bloat problem we are trying to solve. One governor tool with an action parameter is the right shape, because the universe of governance actions is small and stable. (This is the case where the action-enum trade does pay off β€” six fixed actions, not thirty.)

A working sketch of that tool β€” call it sanctum__tools for now β€” looks like this:

action = "help"            β†’ intent string in, recommended tools + arg sketch out
action = "list-available"  β†’ full catalog known to this server
action = "list-attached"   β†’ what this session currently exposes
action = "attach"          β†’ add one or more tool names to active set
action = "detach"          β†’ remove tool names from active set
action = "attach-profile"  β†’ apply a named preset (chatter, admin, full)

action = "help" β†’ intent string in, recommended tools + arg sketch out action = "list-available" β†’ full catalog known to this server action = "list-attached" β†’ what this session currently exposes action = "attach" β†’ add one or more tool names to active set action = "detach" β†’ remove tool names from active set action = "attach-profile" β†’ apply a named preset (chatter, admin, full)

action = "help"            β†’ intent string in, recommended tools + arg sketch out
action = "list-available"  β†’ full catalog known to this server
action = "list-attached"   β†’ what this session currently exposes
action = "attach"          β†’ add one or more tool names to active set
action = "detach"          β†’ remove tool names from active set
action = "attach-profile"  β†’ apply a named preset (chatter, admin, full)

The governor stays always-attached. It is small enough that its schema cost is negligible β€” name, description, six actions, a string or list of tool names. It is the one tool that needs to be present in every Sanctum SMCP deployment.

The help action is the bit that connects this back to the original pain. Today, an agent looking for the right tool either trusts its catalog inspection (which is what wastes context) or asks the user a clarifying question (which is what wastes turns). With a real help channel, the agent can describe its intent in natural language, get back a small, ranked recommendation grounded in the actual plugin catalog, and then either call the recommended tool or attach it first if it's not in scope.

This is also the place to encode the operator rules we wrote as prose in Q's job_rules. "If the user asks for a document, use tasks__create-document, never tasks__create-task. The project gate is project_id, not list_id." Today that lives in a Letta core memory block. Tomorrow it can live as routing logic the governor itself can apply, queried via help, callable by any harness β€” even ones that have never heard of Letta.


Plugin developers don't reimplement this

Critically, product plugin authors do not write attach logic. That is the whole point of putting it in the protocol layer.

A plugin author writing the next host product β€” Invoicing, CRM, presale, whatever β€” declares their verbs the same way the Tasks plugin does today. Optionally, on server start, they call into the internal smcp.governor API to apply a sensible default profile:

from smcp.governor import attach_profile
attach_profile("chatter")

from smcp.governor import attach_profile attach_profile("chatter")

action = "help"            β†’ intent string in, recommended tools + arg sketch out
action = "list-available"  β†’ full catalog known to this server
action = "list-attached"   β†’ what this session currently exposes
action = "attach"          β†’ add one or more tool names to active set
action = "detach"          β†’ remove tool names from active set
action = "attach-profile"  β†’ apply a named preset (chatter, admin, full)

Everything else β€” discovery, listing, attach/detach calls, the governor schema, the call-time policy gate β€” is shipped once in sanctumos/smcp core. Every Sanctum SMCP deployment inherits it. No code duplication across products, no per-product divergence in the governance vocabulary.

That is also where the convention story matters. If every Sanctum SMCP server, in every product, exposes the same governor tool with the same semantics, an operator (human or agent) learns the vocabulary once. The Tasks SMCP server and the Invoicing SMCP server respond to the same attach-profile chatter call. The Cursor user driving Otto across multiple products does not have to remember which IDE pane to open for which plugin. The vocabulary is in-band, in the protocol, in every connection.


What this means for each harness

For Letta, the SMCP-native attach is bonus on top of what already works. Q's lettatest agent today gets all 35 q_vernal_tasks__* tools because the attach script grabs everything. After this change, the server will only advertise the attached subset to begin with, and the attach script can either trust that subset or mirror it explicitly into agent.tool_ids for double safety. Letta agents get the in-band governance vocabulary on top of their native one.

For Cursor, this is the more interesting case. Cursor cannot be remote-controlled by an MCP server β€” its tool toggles live in a Settings panel a human operates. But SMCP-native governance does not need to control Cursor; it controls what Cursor sees. When the SMCP server reports a smaller tools/list and rejects calls to detached tools, Cursor's agent simply has a smaller, sharper toolkit. The settings panel still works on top; the two layers compose. We get governance behavior in a harness that doesn't have a governance API.

For Claude Code, similar story. Claude's permissions system continues to do what it does β€” allow, deny, ask, with rule patterns. SMCP-native governance complements it: instead of relying on the user maintaining a deny list for every tool they don't want the model picking, the server itself stops advertising the tools that aren't in scope. Tool Search is a different lever solving a related problem and can stay on.

The honest framing is this: other MCP servers expose a flat list and let the harness sort it out. Sanctum SMCP exposes a governed list that respects the same architectural insight every harness has been moving toward β€” the agent's tool surface should be smaller than the server's catalog β€” and does it at the one layer that every harness already talks to.


What this is not

It is not a way to remote-control IDEs we don't own. If a Cursor client caches an old tool list and never refreshes, the model may still believe a tool exists; the call gate will refuse it, but the catalog drift is real and the error text needs to be clear ("not attached β€” call sanctum__tools with action=attach").

It is not a god-tool replacement for product verbs. Nothing about this collapses create-task and create-document into one fuzzy create. The governance layer is precisely not in the business of routing actual product calls; it routes which product calls are visible. Product verbs stay sharp.

It is not a security boundary. Attach/detach is capability shaping for clarity, not authorization. Authorization still lives where it lived β€” API keys, ACLs, the per-user key injection on the Tasks bridge for Q. The governor refuses calls to detached tools at the protocol layer; it does not pretend to be an access control system.

And it is not free. The governor tool is one more thing every server advertises. The whole design only earns its keep if the governor is small (one tool, six actions) and if the default profiles trim enough to be worth the meta-cost. Done badly, this is just one more tool to load.


Where we go from here

The implementation order in our internal spec is straightforward.

First, the attach registry and the gated list_tools / call_tool paths in sanctumos/smcp itself. This is the smallest change with the biggest payoff: the server starts respecting attach state for every harness, immediately.

Second, the sanctum__tools governor β€” one tool, the six actions above, with a small Python API (smcp.governor.attach, detach, attach_profile) that plugin authors can call from their startup hooks.

Third, the Letta adapter β€” optional, opt-in, mirrors the SMCP attach set into the Letta agent's tool_ids for double-safety when LETTA_AGENT_ID is in the environment.

Fourth, the chatter profile for the Tasks plugin β€” twelve to fifteen verbs, the actual day-to-day surface for an in-app support agent. Q starts respecting it. Otto's Cursor MCP can stay on full or admin as needed.

Fifth, the same pattern in every other Sanctum SMCP deployment we ship. Invoicing, CRM, presale β€” different plugins, same governor, same vocabulary.

The first one is the only architectural decision. Everything after is mechanical.


The bigger bet

Most MCP work right now is at the level of "what tool should I add next." That's the easy part. The harder, less-glamorous part is figuring out what to do when you have too many β€” and you will, fast, the moment you start wrapping real product APIs.

Treating the server's view of attachment as the authoritative one, and shipping a uniform governance vocabulary in every Sanctum SMCP deployment, is the kind of convention that quietly compounds. Every new plugin author inherits it for free. Every harness benefits from it without the harness vendor having to ship anything. Every agent β€” whether it lives in Letta, in Cursor, in Claude Code, or in something nobody has built yet β€” gets a smaller, sharper, more honest toolkit by default.

That feels like the right place to put the next bit of effort.