How-to cluster: create-mcp-server-without-code, claude-desktop-mcp-setup, chatgpt-mcp-connector (incl. honest Business/Enterprise write-connector plan limits), rest-api-to-mcp-server. Comparison cluster: mcp-server-hosting-pricing (5-platform matrix), composio-alternative, smithery-alternative — house style: explicitly state where competitors win. Technical/GEO cluster: mcp-transports-explained (SSE deprecation 2025-03-26), mcp-oauth-plain-english, mcp-server-security-checklist, and German DACH article mcp-server-ohne-code-erstellen (articleJsonLd gained optional inLanguage param). Every article: pageMetadata canonical, Article JSON-LD with Person author + image + wordCount, BreadcrumbList, FAQPage where applicable, 1200x630 OG image via shared helper, internal linking. Registry entries in lib/articles.ts feed guides index, sitemap and RSS automatically. Product claims sourced only from lib/seo.ts truth — no invented customers, SLAs or certifications. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SXUwmPVRTD8AKQtio6gCN5
180 lines
8.1 KiB
XML
180 lines
8.1 KiB
XML
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 (
|
||
<>
|
||
<JsonLd
|
||
data={articleJsonLd({
|
||
title: TITLE,
|
||
description: DESCRIPTION,
|
||
path: PATH,
|
||
datePublished: '2026-07-08',
|
||
authorName: 'Marco Sadjadi',
|
||
wordCount: 1400,
|
||
})}
|
||
/>
|
||
<JsonLd
|
||
data={breadcrumbJsonLd([
|
||
{ name: 'Home', path: '/' },
|
||
{ name: 'Guides', path: '/guides' },
|
||
{ name: TITLE, path: PATH },
|
||
])}
|
||
/>
|
||
<ArticleShell
|
||
title={TITLE}
|
||
subtitle="Every REST API is one wrapper away from being usable by Claude, Cursor or ChatGPT. The wrapper is an MCP server — and the difference between a useful one and a frustrating one is decided before any code exists, in how you design the tool surface."
|
||
updated="July 2026"
|
||
>
|
||
<H2>The core mistake: 1:1 endpoint mapping</H2>
|
||
<P>
|
||
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{' '}
|
||
<Strong>tasks</Strong>.
|
||
</P>
|
||
<StaticCodeBlock code={BAD_PROMPT} label="what not to do" />
|
||
<P>
|
||
Design the tool surface the way you would design CLI commands for a colleague:
|
||
task-shaped, few, with the joins already done.
|
||
</P>
|
||
<StaticCodeBlock code={GOOD_PROMPT} label="the same API, task-shaped" />
|
||
<UL>
|
||
<li>
|
||
<Strong>3–7 tools</Strong> is the sweet spot for a single-purpose server. More than ten
|
||
and selection quality drops.
|
||
</li>
|
||
<li>
|
||
<Strong>Fold lookups into the tool.</Strong> If sending a reminder needs a customer id,
|
||
let <Strong>find_customer</Strong> exist — but do not expose the four intermediate
|
||
endpoints the API needs internally.
|
||
</li>
|
||
<li>
|
||
<Strong>Return shaped results, not raw payloads.</Strong> “id, plan, status” beats the
|
||
full 80-field customer object; the model reads every byte you return.
|
||
</li>
|
||
<li>
|
||
<Strong>Separate reads from writes</Strong> and say so in the description — clients
|
||
like ChatGPT restrict write tools on individual plans (
|
||
<Link href="/guides/chatgpt-mcp-connector" className="text-[--color-accent] hover:underline">
|
||
details here
|
||
</Link>
|
||
), and a clean read/write split keeps the read tools usable everywhere.
|
||
</li>
|
||
</UL>
|
||
|
||
<H2>Auth: the API key never goes in the prompt</H2>
|
||
<P>
|
||
The wrapper needs your API's credential, and there is exactly one right place for it: an{' '}
|
||
<Strong>encrypted secret</Strong>, referenced by name. In the prompt above,{' '}
|
||
<Strong>BILLING_API_KEY</Strong> 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.
|
||
</P>
|
||
<P>
|
||
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:{' '}
|
||
<Link href="/docs/oauth" className="text-[--color-accent] hover:underline">
|
||
OAuth docs
|
||
</Link>
|
||
.
|
||
</P>
|
||
<Note>
|
||
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.
|
||
</Note>
|
||
|
||
<H2>Rate limits: yours and theirs</H2>
|
||
<Table>
|
||
<thead>
|
||
<tr>
|
||
<th>Layer</th>
|
||
<th>What limits it</th>
|
||
<th>What to do</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Client → MCP server</td>
|
||
<td>OAuth gate before your container; plan quotas (free tier: 100k calls/mo)</td>
|
||
<td>Nothing — enforced for you.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>MCP server → upstream API</td>
|
||
<td>The upstream's own rate limits</td>
|
||
<td>
|
||
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.
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Model behavior</td>
|
||
<td>Assistants retry failed calls</td>
|
||
<td>
|
||
Return clear error messages (“rate limited, retry in 30s”) — models read them and
|
||
actually wait.
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</Table>
|
||
|
||
<H2>From prompt to installed tool, end to end</H2>
|
||
<P>
|
||
With the prompt written, the rest is mechanical:{' '}
|
||
<Link href="/guides/create-mcp-server-without-code" className="text-[--color-accent] hover:underline">
|
||
generation, static checks, container build and deploy
|
||
</Link>{' '}
|
||
take 45–90 seconds, and the dashboard gives you install snippets for{' '}
|
||
<Link href="/guides/claude-desktop-mcp-setup" className="text-[--color-accent] hover:underline">
|
||
Claude Desktop
|
||
</Link>
|
||
, 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.
|
||
</P>
|
||
<P>
|
||
If your API resembles something common — Notion, GitHub, Stripe, PostgreSQL — check{' '}
|
||
<Link href="/templates" className="text-[--color-accent] hover:underline">
|
||
the templates
|
||
</Link>{' '}
|
||
first: forking a working server and swapping in your credential is faster than writing
|
||
any prompt. Otherwise, the{' '}
|
||
<Link href="/pricing" className="text-[--color-accent] hover:underline">
|
||
free tier
|
||
</Link>{' '}
|
||
covers one server — enough to wrap the API you use most and find out what your assistant
|
||
does with it.
|
||
</P>
|
||
</ArticleShell>
|
||
</>
|
||
);
|
||
}
|