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/sdk 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, the open standard (originally introduced by Anthropic) 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 is fetched via
tools/listand then sits in the model's context on 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 for transient failures, and per-step credentials and permissions — already is most of the production substrate an MCP server needs. It has the retry loop, the run history, and survival across a restart mid-run because its run state is persisted, not held in a process. 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. An excerpt — the rest of the graph, ending in a vertex tagged platform:output that holds the tool's result, is omitted:
{
"executionMode": "mcp",
"vertices": [
{
"id": "find_stock",
"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 workflow'splatform:output. The execution model is unchanged — still asynchronous, still durably persisted — 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: transient HTTP and overload failures inside the workflow are retried by the platform with backoff; any other failure ends the run, and the agent gets back one error tied to a recorded run rather than a half-parsed response. (Steps that completed before the failure have still happened, so design multi-step tools with that in mind.) 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. Idempotency, honestly: the engine dedupes its own internal redelivery, but a tools/call the host retries starts a new run, so a tool that creates or mutates still needs a natural idempotency key (an order number, a request id) checked at the destination. 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. One caveat: on a workflow tool, you declare the tier with toolEffect, and it defaults to OBSERVE when unset. A workflow that writes to a real system should say so honestly, because the tier is only as truthful as its label. We derive our own platform 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), both the in-product assistant and the MCP endpoint read that one catalogue, and a parity test fails the build if an advertised tool has no handler or a handler has no advertised tool. The result is about three dozen tools over a platform with several hundred routes — roughly 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?
Mostly 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 transient failures, dedupes its own internal redelivery, and records every run. Serve the MCP server as a workflow and tools/call inherits the retries and the run history instead of re-implementing them per server. The one piece you still own is idempotency across caller retries: a retried tools/call is a new run, so a mutating tool needs a natural idempotency key.
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 largely 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.