BLOG ENTRY
MCPifying the World: What It Takes to Turn Existing APIs into MCP Servers
I spent a few weekends looking at what it actually takes to turn a normal REST API into something an MCP-aware agent can use. There are real tools now — but they all assume you already have an OpenAPI spec, and most of the APIs I've worked on in the last decade don't.
01 —
The Landscape at a Glance
Before diving in, here's the shape of the problem. OpenAPI specs and (to a lesser extent) GraphQL schemas are well-served by existing tools. Postman collections, SOAP/WSDL, and doc-only APIs are where the real work remains.
02 —
1. API Discovery and Parsing
The first step is understanding what the API does. If you're lucky, there's an OpenAPI/Swagger spec, a RAML file, or a GraphQL schema. More often — especially with older systems — documentation lives in PDFs, Confluence wikis, Postman collections, or nowhere at all.
This is the first and most underappreciated challenge. Everything downstream depends on getting it right.
03 —
2. MCP Server Scaffolding
Once you understand the API surface, the work shifts to set-up: an MCP server (TypeScript or Python SDK), each endpoint mapped to an MCP tool with a proper JSON Schema for inputs, authentication handled (API keys, OAuth, Basic Auth, session cookies, proprietary signing), and errors / pagination / rate limits / retries normalized.
Here's what a minimal hand-written MCP tool looks like in Python with FastMCP. Notice how much of the work is in the docstring, not the code — that description is what the LLM uses to decide when to call this tool versus another.
from fastmcp import FastMCP
import httpx
mcp = FastMCP("legacy-orders-api")
@mcp.tool()
async def get_order(order_id: str) -> dict:
"""
Retrieve a single order by its ID.
Returns order metadata including status, line items, and customer info.
Use this when you need full details about one specific order. For listing
multiple orders by date range or status, use list_orders instead.
"""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.example.com/v1/orders/{order_id}",
headers={"Authorization": f"Bearer {API_TOKEN}"},
)
resp.raise_for_status()
return resp.json()04 —
3. Tool Design — The Hard Part
This is where most auto-generation falls short. Tool descriptions are not just documentation — they're instructions to the LLM about when and why to invoke a tool. A description like "Updates a user record" is technically accurate but nearly useless for an agent trying to decide which of 40 tools to call.
Good tool design also involves granularity decisions: should you expose one tool per endpoint, or create higher-level composite tools that encapsulate a full workflow?
05 —
4. Testing and Iteration
Generated MCP servers need to be validated against real agent workflows — not just unit tests. Tool selection failures, ambiguous parameter names, and over-loaded tool sets are common issues that only surface when an LLM actually tries to use the server.
06 —
From curl to MCP Tool — A Worked Example
Say you have a legacy API with no OpenAPI spec. All you have is a working curl command from a teammate:
- Mechanical for one tool. Doing it for 200 endpoints — with no spec, and getting the descriptions right enough that an LLM uses the tools correctly — is not.
curl -X POST https://api.legacy-corp.com/v2/inventory/check \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{
"sku": "WIDGET-42",
"warehouse_id": "WH-NYC-01",
"include_reserved": false
}'{
"name": "check_inventory",
"description": "Check the available stock level for a product at a specific warehouse. Returns current quantity, reserved quantity, and reorder threshold. Use this before committing to fulfillment promises.",
"inputSchema": {
"type": "object",
"properties": {
"sku": {
"type": "string",
"description": "Product SKU identifier (e.g., 'WIDGET-42')"
},
"warehouse_id": {
"type": "string",
"description": "Warehouse code (e.g., 'WH-NYC-01')"
},
"include_reserved": {
"type": "boolean",
"description": "If true, includes reserved stock in the count",
"default": false
}
},
"required": ["sku", "warehouse_id"]
}
}07 —
Naive Mapping vs. Workflow Design
Here's where most auto-generators stop. Given an OpenAPI spec for an e-commerce API, the naive 1:1 mapping produces tools like:
- One tool. One LLM decision. Four API calls hidden behind it. This is the actual hard problem in MCP design — and no current generator solves it automatically.
# Auto-generated, 1:1 mapping from OpenAPI
@mcp.tool()
async def post_carts(body: dict) -> dict:
"""Creates a new cart. See POST /carts."""
...
@mcp.tool()
async def post_carts_items(cart_id: str, body: dict) -> dict:
"""Adds an item to a cart. See POST /carts/{cart_id}/items."""
...
@mcp.tool()
async def patch_carts_items(cart_id: str, item_id: str, body: dict) -> dict:
"""Updates a cart item. See PATCH /carts/{cart_id}/items/{item_id}."""
...
@mcp.tool()
async def post_carts_checkout(cart_id: str, body: dict) -> dict:
"""Checks out a cart. See POST /carts/{cart_id}/checkout."""
...@mcp.tool()
async def place_order(
items: list[dict],
customer_id: str,
shipping_address: dict,
payment_method_id: str,
) -> dict:
"""
Place a complete order in one operation.
This handles the full purchase flow: creates a cart, adds all items,
applies the customer's shipping and payment info, and finalizes checkout.
Returns the order confirmation with order_id and estimated delivery date.
Use this for any 'I want to buy X' or 'place an order for Y' request.
For partial cart manipulation (adding items without checkout), use
manage_cart instead.
Args:
items: List of {sku, quantity} objects to purchase
customer_id: Existing customer ID
shipping_address: Full address dict with street, city, state, zip
payment_method_id: Pre-saved payment method identifier
"""
cart = await _create_cart(customer_id)
for item in items:
await _add_to_cart(cart["id"], item["sku"], item["quantity"])
return await _checkout(cart["id"], shipping_address, payment_method_id)08 —
The Existing Tool Landscape — Open-Source CLIs
The OpenAPI → MCP conversion space has gotten crowded fast. openapi-mcp-generator is a TypeScript CLI that converts OpenAPI specs into standalone MCP servers, supporting stdio, SSE, and StreamableHTTP transports, endpoint filtering, and programmatic use via npm.
mcpify takes a single-command approach — point it at an OpenAPI spec URL and get a running MCP server immediately. It also generates a one-tool MCP server from a raw curl command, which is useful for APIs with no formal spec at all.
openapi-mcp-codegen (by CNOE) goes further — alongside the MCP server it can generate a LangGraph ReAct agent, an evaluation suite, and an LLM-optimized system prompt from the same spec.
AWS Labs awslabs.openapi-mcp-server is a Python package with production-grade features: Cognito auth support, Prometheus metrics, caching, and a reported 70–75% reduction in token usage through concise tool descriptions.
openapi-mcp-generator \
--input ./petstore.json \
--output ./mcp-server \
--transport=streamable-http \
--port=300009 —
Managed Platforms and Frameworks
Gram (by Speakeasy) is a fully managed platform — upload your OpenAPI spec, get a hosted MCP server with built-in toolset curation. RapidMCP takes a proxy approach — it sits in front of your existing API and translates MCP tool calls into HTTP requests, adding analytics and rate limiting along the way.
Stainless generates both a TypeScript SDK and an MCP server from your OpenAPI spec simultaneously. It handles client-specific constraints (Cursor's 40-tool cap, OpenAI's no-root-anyOf requirement) and supports OAuth-based remote MCP servers deployable to Cloudflare Workers.
FastMCP (Python) provides a from_openapi() method. It uses a RouteMap system for fine-grained control over how endpoints map to tools, resources, or are excluded entirely. Importantly, FastMCP's own documentation warns that auto-converted servers perform significantly worse than hand-crafted ones for complex APIs.
from fastmcp import FastMCP
import httpx
client = httpx.AsyncClient(base_url="https://api.example.com")
mcp = FastMCP.from_openapi(
openapi_spec=open("api.yaml").read(),
client=client,
)
mcp.run()10 —
The Real Gap: APIs Without Specs
Every tool listed above shares one assumption: you already have an OpenAPI spec. For the modern APIs I've built in the last two years, that's true. For most of the APIs I've worked on across the last decade — security operations tools, internal RPC services, prediction APIs we extended on top of older systems — it's not even close.
Here's where I think the actually-hard problems still live:
- Spec inference from examples — give me a Postman collection or captured HTTP traffic and produce a sane OpenAPI + MCP server. I haven't found a tool that does this well; the existing attempts produce specs you wouldn't trust in production.
- Document-to-spec generation — point at an HTML API reference (or worse, a PDF) and produce a machine-readable spec. Tractable with LLMs now, but I haven't seen it productized.
- SOAP/WSDL → MCP — most of the legacy enterprise systems I've touched still speak SOAP. The tooling for this is essentially absent, and the demand from anyone trying to put an agent on top of a 15-year-old back-office system is real.
- Intelligent tool consolidation — even with a clean spec, 1:1 mapping is a trap once your API gets non-trivial. I've watched LLMs flounder against a 40-tool surface that should've been 6 workflow tools.
11 —
Would I Build Another "MCPify"?
If I were starting a side project in this space today, I wouldn't aim at generic OpenAPI → MCP. That's done. One of the tools is literally named mcpify and the rest are good enough for the easy case.
What I'd actually build, in priority order:
- A spec-less ingester — feed it a Postman collection, a folder of curl commands, or even an HAR file, and out comes both a credible OpenAPI spec and a usable MCP server. This is where the real backlog of legacy APIs lives.
- A SOAP/WSDL adapter that takes the protocol seriously. Enterprise pays for this and nobody's serving them.
- An LLM-driven tool consolidator. Take a 600-endpoint spec, group endpoints by workflow, generate higher-level tools with descriptions that an LLM can actually navigate. This is the piece that turns 'technically working' MCP servers into ones agents perform well against.
- A continuous-sync pipeline — as the underlying API evolves, the MCP server should track it without a human in the loop.
12 —
Where I've Landed
After a few weekends in this space: the easy part — REST endpoints to MCP tools — is solved well enough that I'd never build it again. The part that still hurts is everything around it. An undocumented internal API I want to put behind an agent. A SOAP service nobody's owned in three years. An OpenAPI spec that's technically correct and produces a tool surface no LLM can navigate without help.
If you're a backend engineer and you've been waiting for agent-era work to land in your lap — it's here, and it's mostly cleanup. Not new architecture, not greenfield. Cleanup of the API surfaces a decade of shipping fast has left behind. I don't say that as a complaint; that's where the leverage is right now, and it's the kind of work I find interesting.
If you've shipped an MCP server for a real production API — particularly one without a spec — I want to compare notes. The failure modes are where the interesting design problems live. Reach me at gajurelkushal1994@gmail.com or through the contact page.
PUBLISHED
READ TIME
AUTHOR
TAGS
- ↳ MCP
- ↳ Agent Infrastructure
- ↳ API Design
- ↳ Backend
KEY TAKEAWAYS
- OpenAPI → MCP is solved. Half a dozen tools — pick one.
- The unsolved part is everything without a spec: Postman collections, SOAP services, doc-only APIs.
- 1:1 endpoint-to-tool mapping is the obvious thing to ship first and the wrong thing to ship period.
- Workflow-level tool design is where agent performance comes from. Most generators don't do it.