import { JsonLd } from '@/components/json-ld'; import { StaticCodeBlock } from '@/components/static-code-block'; import { articleJsonLd, breadcrumbJsonLd, pageMetadata } from '@/lib/seo'; import Link from 'next/link'; import { ArticleShell, H2, Note, P, Strong, Table, UL } from '../article-shell'; const PATH = '/guides/rest-api-to-mcp-server'; const TITLE = 'Wrap any REST API as an MCP server'; const DESCRIPTION = 'How to turn a REST API into an MCP server your AI clients can use: designing the tool surface (fewer, better tools beat 1:1 endpoint mapping), handling auth headers with encrypted secrets, and respecting upstream rate limits.'; export const metadata = pageMetadata({ title: TITLE, description: DESCRIPTION, path: PATH }); const BAD_PROMPT = `Create an MCP server for our API. Endpoints: GET /users, GET /users/:id, POST /users, PUT /users/:id, DELETE /users/:id, GET /orders, GET /orders/:id, POST /orders, GET /invoices, GET /invoices/:id, POST /invoices/:id/send ...`; const GOOD_PROMPT = `Create an MCP server for our billing API (https://api.example.com). Tools: - find_customer: search customers by name or email, return id, plan, status - get_open_invoices: list unpaid invoices for a customer id, with amounts - send_invoice_reminder: send the dunning email for one invoice id Auth: BILLING_API_KEY sent as "Authorization: Bearer" header. Read-heavy; send_invoice_reminder is the only write action.`; export default function Page() { return ( <>

The core mistake: 1:1 endpoint mapping

The obvious approach — one MCP tool per REST endpoint — produces a bad server. An AI assistant choosing between 30 near-identical tools burns context on the choice, chains three calls where one would do, and picks wrong often enough to erode trust. The assistant is not a REST client; it does not want your resource model, it wants{' '} tasks.

Design the tool surface the way you would design CLI commands for a colleague: task-shaped, few, with the joins already done.

  • 3–7 tools is the sweet spot for a single-purpose server. More than ten and selection quality drops.
  • Fold lookups into the tool. If sending a reminder needs a customer id, let find_customer exist — but do not expose the four intermediate endpoints the API needs internally.
  • Return shaped results, not raw payloads. “id, plan, status” beats the full 80-field customer object; the model reads every byte you return.
  • Separate reads from writes and say so in the description — clients like ChatGPT restrict write tools on individual plans ( details here ), and a clean read/write split keeps the read tools usable everywhere.

Auth: the API key never goes in the prompt

The wrapper needs your API's credential, and there is exactly one right place for it: an{' '} encrypted secret, referenced by name. In the prompt above,{' '} BILLING_API_KEY is a name, not a value. You provide the value separately in the dashboard; it is encrypted with AES-256-GCM at rest and injected into the server's container as an environment variable at runtime — never logged, never echoed back, never part of the generated source.

The second auth layer is between the AI client and your MCP server: every deployed server sits behind OAuth 2.1 (PKCE, Dynamic Client Registration, Resource Indicators), so your wrapped API is not one guessable URL away from the public internet. How that handshake works:{' '} OAuth docs .

Start read-only. A read-only wrapper cannot damage anything while you learn how the assistant actually uses the tools; add the one or two write actions after a week of watching the call logs.

Rate limits: yours and theirs

Layer What limits it What to do
Client → MCP server OAuth gate before your container; plan quotas (free tier: 100k calls/mo) Nothing — enforced for you.
MCP server → upstream API The upstream's own rate limits Tell the generator: “respect a limit of N req/s; on 429, back off and surface the error”. Shaped tools help here too — one task call instead of five endpoint calls.
Model behavior Assistants retry failed calls Return clear error messages (“rate limited, retry in 30s”) — models read them and actually wait.

From prompt to installed tool, end to end

With the prompt written, the rest is mechanical:{' '} generation, static checks, container build and deploy {' '} take 45–90 seconds, and the dashboard gives you install snippets for{' '} Claude Desktop , Cursor and ChatGPT. The generated TypeScript is exportable — if your wrapper outgrows prompt-editing (complex retries, multi-step workflows), take the source and continue by hand with the boilerplate already written.

If your API resembles something common — Notion, GitHub, Stripe, PostgreSQL — check{' '} the templates {' '} first: forking a working server and swapping in your credential is faster than writing any prompt. Otherwise, the{' '} free tier {' '} covers one server — enough to wrap the API you use most and find out what your assistant does with it.

); }