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>
    </>
  );
}
