How to Turn Any REST API Into an MCP Server in 5 Minutes
SchemaBridge Team · 2026-08-22 · MCP, AI Agents, API Design, Security
Wrapping a REST API as an MCP server takes 5 minutes — the real work is retries, idempotency, audit and scoped tokens. The fix: serve it as a workflow.
The Five Minutes Are Real — and They Are the Trap
You can wrap an API you already own as an MCP server in minutes. Install an SDK — mcp[cli] for Python, @modelcontextprotocol/server for TypeScript — pick a transport (stdio to debug locally, streamable HTTP to deploy), wrap a few endpoints as tools, and any compliant host can drive it. The Model Context Protocol, Anthropic's open standard for how a model discovers and invokes tools, exists to collapse an N × M problem — N models and agent frameworks, M APIs, a bespoke adapter for every pair — down to N + M. You implement one server; every host talks to it.
Five minutes in, it works. That is the trap, because five minutes is where most teams stop — and a server that technically works is not one an agent can use.
An Endpoint Is Not a Tool
Point an OpenAPI generator at a mature spec and it emits a tool per operation — 400 of them. The server starts, lists everything, and looks like a success. Then an agent connects and three things break at once:
- Context is eaten before the user types. Every tool's name, description and JSON Schema rides in
tools/liston every turn. Four hundred modest tools is tens of thousands of tokens spent on a catalogue before a single question is asked. - Selection collapses.
getOrder,getOrderV2,getOrderDetail,fetchOrderById— a model picks by name similarity and chooses wrong some fraction of the time. A wrong read is bad; a wrong write is an incident. - One tool returns everything.
listTransactionswith no arguments returns 40,000 rows, because the generator made every optional parameter optional. The response blows the window.
None of these is a protocol failure — the server is spec-compliant. The failure is that an endpoint is not a tool. An endpoint is a resource-shaped primitive for a programmer with your docs open, composing several calls. A tool is a task-shaped primitive for a consumer with no docs and no memory that wants one intent done in one call. The mapping was never one-to-one.
The remedy is unglamorous, and it is the actual work:
- Curate to ten to thirty tools. If your API has 400 operations, the right catalogue is the fifteen or so tasks people actually ask for. A tool you removed costs nothing; a tool nobody needs costs tokens on every turn.
- Compose at the tool boundary, not in the model. If "is this order late?" takes three API calls, write one tool that makes three calls.
- Make bounds mandatory. Where your API defaults to unbounded, your tool defaults to a page of twenty-five with a cursor.
- Return prose, not the vendor's raw JSON. "SKU BRK-1180-XL exists but is not stocked anywhere" is a fact the model can act on;
{"locations": []}is a puzzle it will sometimes solve wrong. - Write descriptions as a contract — the only specification the model ever reads. State units, formats, exact enum values, and when not to use the tool.
The Hard Part Isn't the Protocol — It's Durable Execution
Curate the catalogue and four more properties decide whether the server survives real usage. Read what kind of problem each one is:
- Actionable errors are a retry-loop contract. The model is an unbounded retry loop with no backoff; a
500with an HTML page teaches it nothing."Rate limited by the upstream API. Retry after 30 seconds."turns a dead end into a corrected retry — the same error-recovery discipline any retrying caller needs. - Idempotency is at-least-once delivery hygiene. Hosts retry on timeout, users re-run turns, and a model that does not see a result tries again — so anything that creates takes an idempotency key and honours it.
- Side-effect classification is delegated authority. A headless agent has no confirmation surface, so ship read-only first, and enforce policy at invocation, not merely by omitting a tool from the listing — a listing is a hint, a permission check is a decision.
- Scoped, short-lived credentials are the real security story. Prompt injection against an MCP server is a confused-deputy problem, not a content-filtering one: the credential is the escalation. Attenuate scope below the authorising human, bound lifetime, and keep destructive verbs out of reach.
Every one of those is a distributed-systems property, and none of them ships in the MCP SDK — the SDK gives you tools/list and tools/call, not a place to stand when a tool is halfway through three API calls and the process dies. So the honest shape of "build a production MCP server" is: five minutes of protocol, then a slow re-implementation of a slice of a workflow engine around it — retry policy, idempotency store, audit log, confirmation path, concurrency bound — discovered one incident at a time, in production.
Which invites a better question than "how do I build an MCP server?" — what if the thing behind the protocol already had all of that?
The MCP Server You Actually Want Is a Workflow
This is the reframe, and it is the bet we made in SchemaBridge. A durable workflow engine — a DAG of steps with named entrypoints, a declared output, run history, retries, and per-step credentials and permissions — already is the production substrate an MCP server needs. It has the retry loop, the audit trail, and survival across a restart mid-run because it is event-sourced. The one thing it lacked was a protocol adapter that says "this workflow is a tool."
So we didn't add a new object to configure. An MCP server is not something you build — it is a workflow you author. A workflow already has entrypoints, an output, history and permissions, so nothing is left over to build a "server" from. It carries a third execution mode beside async and sync — executionMode: mcp — a serving concern, not an execution one. Set it, tag the entrypoints, and the workflow is reachable as an MCP server scoped to its owner:
{
"executionMode": "mcp",
"vertices": [
{
"type": "ENTRY_POINT",
"tags": ["platform:tool"],
"config": {
"toolName": "find_stock",
"toolDescription": "Current stock for one SKU. To search by product name, use catalogue_search first.",
"toolEffect": "OBSERVE",
"toolInputSchema": {
"type": "object",
"properties": {"sku": {"type": "string"}},
"required": ["sku"]
}
}
}
]
}
Three mappings are the entire protocol surface:
tools/listis the workflow's tagged entrypoints. EveryENTRY_POINTtaggedplatform:toolis one tool; its model-facing contract rides on the vertex. There is no second catalogue to keep in sync, because the graph is the catalogue.tools/callruns the workflow and returns its output. The dispatcher triggers the entrypoint, blocks until the run is terminal, and returns the declared output. The execution model is unchanged — still asynchronous, still event-sourced — the request only waits at the boundary.- Deploy-time validation refuses the silent failures. A
platform:toolvertex that is not an entrypoint, a missing tool name, an effect tier that is not an exact known value, or a duplicated tool name — each is a deploy error, not a runtime surprise.
Now count what you did not build. No retry loop: a failed step retries under the workflow's policy, and the agent sees one clean result or one actionable error, never a partial. No idempotency store and no audit log: every tools/call is a run — its arguments, intermediate steps and output recorded and queryable, so "what did the agent do?" has an answer, and it is a run id. No concurrency bound of your own: the call blocks with guardrails — a hard per-call timeout and a process-global semaphore that sheds load rather than saturating the connection pool. (That last point has an honest consequence: a slow workflow is a poor MCP tool by design, and the sync ceiling is a real product limit.)
The credential and confirmation story is not hand-assembled either. The same short-lived OAuth 2.1 token, scope re-intersection on refresh, and three effect tiers apply unchanged: OBSERVE runs autonomously, DRAFT mutates draft state only, and PRODUCTION — deploy, execute, delete — is withheld from a headless caller entirely. We derive our own catalogue this way: each route carries an @AgentTool annotation, the annotation is an allowlist (a new endpoint is invisible to every agent until someone opts it in), and a build-time parity test fails if the in-product assistant and the MCP endpoint diverge. The result is 34 tools over a platform with several hundred routes — what "ten to thirty" looks like when you actually do it.
The payoff, stated plainly: the fifteen tasks you curated down become fifteen workflows — each one something you can run, monitor, version and expose as an agent tool, with the production hardening supplied by the engine rather than hand-rolled around a wrapper. "Write one tool that makes three calls" stops being extra server code and becomes three vertices on a canvas. The tool is the workflow. And because it is a conforming MCP server, the same workflow is available to every agent runtime you adopt, not just one chat window.
Be honest about maturity: this is newly built — proven end to end through the real execution engine, but not yet exercised against a live external MCP host — so treat it as an architecture bet, not a battle-scars claim. But the bet is what the protocol was for: you write one conforming server, and it inherits durability you would otherwise have written five times.
Frequently Asked Questions
Do I need to change my REST API to support MCP?
No. An MCP server is a wrapper over your existing API. But do not map endpoints to tools one-to-one — an endpoint is resource-shaped, a tool is task-shaped.
Is it safe to expose a write API through an MCP server?
Not by default. A headless agent has no confirmation surface, so ship read-only first and refuse destructive tools at invocation time, not merely by omitting them from the listing.
Where should a production server's retries, idempotency and audit trail come from?
Not from code you write around the SDK. Those are durable-execution properties, and the cleanest source is the execution substrate itself — a workflow engine that already retries steps, dedupes delivery, and records every run. Serve the MCP server as a workflow and tools/call inherits all three instead of re-implementing them per server.
Conclusion: Make Less Possible, Describe It Better — Then Let the Engine Run It
The protocol work is five minutes and the SDKs earn their keep. Two harder problems remain, and neither is a protocol question. The first is deciding what a model should be allowed to do with the system you spent years building — the same question you answer when you design any API, except this caller reads your descriptions literally, has no memory of yesterday, and will do exactly what you made possible. The second is making it survive real use: retries, idempotency, an audit trail, scoped credentials, a concurrency bound. That second problem is already solved by any durable workflow engine — which is why the strongest version of "build an MCP server" is "author a workflow and serve it as one."
Make less possible. Describe it better. Then let the execution engine — not a hand-rolled wrapper — run it.