Ali Rathore.

July 2026

One capability, four front doors

A single operation now has to be an HTTP endpoint, an MCP tool, a chat tool, and a sandboxed code-mode capability at once. Generate all four from one declaration or watch them drift, quietly, into a security bug.

An MCP client asks the server to create a budget, and the model has done everything right: the operation exists, the name is spelled correctly, the arguments look sane. It sends amount as a string, because the MCP schema it read said amount was a string. The call fails deep in the executor, which expected a number, because that is what the HTTP endpoint has always expected. No compile error stood between those two facts. Two copies of one schema disagreed about a single field, the model faithfully obeyed the wrong one, and the failure surfaced three layers away from the edit that caused it.

Pull that thread and you find the shape of the whole problem. amount is a string in one place and a number in another because the operation lives in two places, and nothing forces the two to say the same thing. That is the failure mode you inherit the moment a single capability has to appear in more than one place, and modern app surfaces have made “more than one place” the default. The same underlying operation, say “create a budget,” now wants to be four things at once. It’s an HTTP endpoint for the web frontend. It’s an MCP tool so an external model can call it. It’s a chat tool inside your own agent loop. And, increasingly, it’s a capability the model can invoke from inside a sandboxed code runtime, where it writes a few lines of JavaScript instead of emitting a tool call. Four front doors onto one room.

The naive way to build four doors is to build four doors. Four schemas, four dispatch tables, four places that must agree on the name of the operation, the shape of its arguments, and what it does when you call it. They agree on the day you write them. They agree in the demo. They drift the first time someone touches one and not the others, and drift here is not cosmetic: you get a model constructing a valid-looking call against a schema that no longer matches the executor, which is exactly the string-versus-number failure I opened with, generalized.

The observation underneath the fix is old enough to be boring: this is a single-source-of-truth problem wearing a distribution costume. You already believe the principle. The interesting part is where you draw the source, because “declare it once” only helps if the one declaration is the thing every surface is generated from, with no hand-written intermediate that can quietly disagree.

So the declaration lives in the OpenAPI spec, which the operation needs anyway to exist as an HTTP endpoint. The only addition is one annotation on the ops that should reach the agent surfaces: x-mcp: {expose: true}. That flag is a marker, not a second definition: it says “this op also goes through the door,” and nothing about the shape, which is already fully described where the endpoint is. A generator, tools/mcpgen, reads the spec, collects every op wearing that flag, and emits exactly one artifact, a []curated.Tool. One list. Not four.

A single element of that list is worth looking at, because the whole argument rests on what it does and does not contain:

curated.Tool{
	Name:   "create_budget",
	Schema: createBudgetSchema, // the one schema, generated from the OpenAPI op
	Invoke: func(ctx context.Context, raw json.RawMessage, scope curated.Scope) (any, error) {
		var args CreateBudgetRequest
		if err := json.Unmarshal(raw, &args); err != nil {
			return nil, err
		}
		return strictHandler.CreateBudget(ctx, args) // the real HTTP handler
	},
}

There is no second definition of create_budget anywhere. The schema is the op’s schema. The Invoke binds the arguments and calls the same strict handler the web frontend hits. Nothing about this element is specific to MCP, or chat, or code-mode. It is the capability, once.

Everything downstream adapts that one list. platform/ai/curated takes the []curated.Tool and projects it onto the four surfaces: the HTTP handler, AgentTools for the chat loop, CodeRegistry for the sandboxed code runtime, and MountMCP for the MCP server. Each projection serializes the schema for its own surface, which want different formats, but each serializes from the same in-memory tool, and all four delegate execution to one Run. The chat-loop adapter is the shape of every one of them:

for _, t := range tools {
	handlers[t.Name] = func(ctx context.Context, raw json.RawMessage) (any, error) {
		return Run(ctx, t, raw, scope)
	}
}

The handler holds no logic. It closes over the tool and calls Run. The claim the code makes about itself is narrow and worth quoting exactly: “a tool, a capability, and an MCP tool of the same name can never drift in name, schema, or behaviour.” Not “shouldn’t.” Can’t. The string-versus-number bug isn’t caught by a test here; it’s structurally unavailable, because there is no second copy of the schema to fall out of sync. Forgetting a surface would mean forgetting to regenerate, and regeneration produces all four or none.

This is ports-and-adapters done in the one direction that actually pays. The usual instinct is to hand-write the port and then hand-write each adapter behind it, which leaves you maintaining the adapters by discipline. Here you declare the port and generate the adapters. Discipline is a bad load-bearing material; a generator is a fine one.

The story that made me believe this was a forgotten door, not a mismatched field. Someone added an operation and shipped it, and for a week the chat agent could do something the MCP server had never heard of: the HTTP handler worked, the tests passed, the endpoint was live, and the op simply existed in three of the four places it was supposed to, because the fourth was a hand-maintained list in another file that whoever added the op did not know to touch. The only thing that surfaced the gap was a user asking the MCP client to do the obvious thing and getting a shrug. One list and one Run is what makes that week impossible: there is no fourth file to forget.

The fourth door is where the design stops being a tidiness argument and starts being a security one. Code-mode, letting the model write and run code rather than emit discrete tool calls, is the current fashion, and the honest reason it’s tempting is that it’s more expressive: loops, intermediate variables, composition, all the things a flat sequence of tool calls makes awkward. The usual reason it’s terrifying is that “run the model’s code” tends to mean handing it a real runtime, which means fetch, require, process, the filesystem, the network: a general-purpose escape hatch shaped exactly like the thing you spent all that effort locking down at the tool boundary.

That’s not what coderun is. The agent’s JavaScript runs in a pure-Go Goja VM with no fetch, no require, no process, no ambient I/O of any kind. The only surface that crosses out of the VM to touch the host is the same generated registry, the identical []curated.Tool the chat loop and the MCP server see. Every host-crossing call from inside the sandbox goes through the same Kill -> Budget -> Guard gate and the same audit sink as a call made any other way. Code execution isn’t a wider set of powers reachable through a different path; it’s the same capability set, behind a VM with the doors welded shut. The model gets the expressiveness of writing code and exactly zero authority it wouldn’t have had by calling the tools directly.

That constancy is the property generation buys you that a code review never can. Because there is one list and one Run, the reasoning you do about a capability, who can call it and what the budget gate does when it’s exhausted, is reasoning you do once, and it holds on all four doors at once. You are not auditing four attack surfaces that happen to look similar. You are auditing one, projected.

Which is exactly why the failure mode inverts at the top. Generation makes the four surfaces consistent with each other, not correct. If the single declared operation is wrong, the wrong authz on the op, a schema that permits a value the executor mishandles, a Guard that waves through what it should stop, generation faithfully propagates that error to all four doors at once, with the same structural certainty it propagates the good version. The blast radius of a mistake in the source is now maximal by construction. And it does nothing for a capability that never enters the spec: anything a surface exposes through some other path is the hand-maintained second list this was meant to kill, and the generator can’t police a door it was never told about. Single source of truth is a guarantee about agreement. It buys you nothing about whether the one thing everyone now agrees on is the right thing. That part is still yours.