Shipping an MCP Server Without Shipping Your Logic
The Model Context Protocol makes your tools callable from any compatible client. It also makes your tool surface a published document. Here is the boundary that keeps the interface open and the implementation closed.
What MCP publishes for you
An MCP server responds to tools/list with a complete description of everything it offers: names, human-readable descriptions, JSON Schema for inputs and outputs, and behavioural annotations. That is the point — a model cannot use a tool it cannot discover.
It also means your tool surface is a public document. Anyone connecting can enumerate every capability, every parameter, and every field you return. If your competitive position depends on nobody knowing what your system does, MCP is the wrong protocol.
If it depends on nobody being able to reproduce what your system does, MCP is fine — provided the boundary is drawn correctly.
The boundary that matters
The rule is simple to state: the MCP layer is a translator, never an implementation. It receives a JSON-RPC call, validates arguments against the declared schema, calls an internal service, and shapes the response. Nothing else.
tools/call -> validate against inputSchema
-> internal service call (authenticated, separate process)
-> shape response to outputSchema
-> returnEverything that constitutes actual work — routing logic, scoring, model selection, pricing — lives behind that internal call. The MCP surface then leaks the shape of your capability, which you wanted published anyway, and none of its substance.
The failure mode to avoid is convenience: implementing a tool inline because it is only twenty lines. Those twenty lines are now in the layer with the widest exposure and the loosest authentication assumptions.
Three tiers of tool, not two
Most guidance treats tools as public or authenticated. Three tiers models reality better:
- Keyless. Read-only, no account, no spend. Discovery and verification belong here. They are your funnel — a developer should be able to evaluate you without signing up.
- Direct-key. Authenticated with an API key passed in configuration. Suits server-side and scripted callers where an OAuth dance is friction with no benefit.
- OAuth. Anything spending a balance from an interactive client, where the user must be prompted and can revoke.
Splitting direct-key from OAuth matters because they have genuinely different threat models and different ergonomics. Collapsing them forces one group through the wrong flow.
Schema as an enforcement boundary
Declaring outputSchema is usually treated as documentation. Treat it as a filter instead: build the response by projecting only the declared fields, rather than returning your internal object and hoping it matches.
Internal objects accumulate fields — debug data, cost breakdowns, upstream identifiers, provider names. Return the object directly and every one of those ships the moment someone adds it, with no review. Projecting through the schema means a new internal field is invisible until deliberately declared.
The same discipline applies to errors. {"error": "insufficient_balance"} is a contract. An unhandled exception message is a description of your internals.
What to verify before shipping
Four checks worth running against your own server: call tools/list unauthenticated and read it as an attacker would; confirm every keyless tool is genuinely read-only and cannot spend; force an error in each tool and inspect exactly what the client receives; and grep the MCP layer for business logic that should live behind the internal call.
The last one is the one that decays. Boundaries are clean when drawn and porous a year later, because every violation was individually reasonable.
Deploy on ForceDream today
Free account. 78% developer earnings enforced at L828. WORM-sealed from call one. 200 markets.