You already have a working REST API. To let an agent use it, you do not rewrite it — you write a thin MCP adapter that sits in front and calls the endpoints you already ship. Each MCP tool is a small typed Python function whose entire body makes one HTTP call to one existing endpoint and returns the result. The API stays exactly as it is; the MCP server is a separate process that talks to it like any other client. This is the hands-on companion to MCP vs API: when to build an MCP server — that piece decides whether to do this; this one shows the code.

The plumbing is easy. The part that actually determines whether this works is choosing which endpoints become tools and writing names and descriptions the model will call correctly. We'll do both.

1. Decide which endpoints become tools#

This is the real work, so do it before writing any code. Your API might have fifty endpoints. An agent needs maybe three. Wrapping your whole OpenAPI surface floods the tool list, and a longer list makes the model choose worse, not better.

Pick the few endpoints that let an agent complete a real task. A good default shape is: one lookup (fetch one thing by id), one search/list (find things by criteria), and at most one safe action. Leave destructive routes off the list entirely for now.

MCP separates capability types, and the split matters: expose an action the model should choose to take as a Tool; expose read-only reference data the app should just load as a Resource. Most REST endpoints you wrap will be Tools. The deeper reasoning lives in how to build an MCP server — here, assume Tools.

For each chosen endpoint, write the tool name and one-sentence description first, on paper. get_order — "Look up a single order by its ID." find_orders_by_status — "List orders filtered by status such as pending or shipped." That description is the only text the model reads to decide whether to call it.

The tool description, not the code, is what makes the model call it correctly. Spend your effort there.

2. Scaffold the FastMCP server#

Use the official Python SDK, which bundles FastMCP. Install it alongside httpx:

pip install "mcp[cli]" httpx

The canonical minimal server is tiny:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("orders-api")

if __name__ == "__main__":
    mcp.run()  # stdio transport by default

@mcp.tool() reads each function's type hints to build the input schema and lifts its docstring into the tool description. So your Python signatures and docstrings are the interface the model sees. If you prefer FastMCP's ergonomics elsewhere, see FastMCP vs the official SDK — the import above is the official SDK's bundled version and is all you need.

3. Wrap one endpoint as a tool#

Now the adapter pattern. Say your API serves GET /orders/{id}. The tool body just calls it:

import os
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("orders-api")

API_BASE = os.environ["ORDERS_API_BASE"]      # e.g. https://api.acme.internal
API_TOKEN = os.environ["ORDERS_API_TOKEN"]    # downstream bearer, from env

@mcp.tool()
def get_order(order_id: str) -> dict:
    """Look up a single order by its ID. Returns status, total, and line items."""
    resp = httpx.get(
        f"{API_BASE}/orders/{order_id}",
        headers={"Authorization": f"Bearer {API_TOKEN}"},
        timeout=10.0,
    )
    resp.raise_for_status()
    return resp.json()

if __name__ == "__main__":
    mcp.run()

That is the whole idea. The tool is an adapter: type hint in, HTTP call out, JSON back. Note what's load-bearing — the order_id: str hint becomes the schema, and the docstring is verbatim what the model reads. Say what it returns; the model uses that to decide when the tool is useful.

4. Add a second tool: search or list#

One lookup isn't enough for an agent to be useful. Add the list endpoint, GET /orders?status=..., mapping the query parameter to a typed argument:

@mcp.tool()
def find_orders_by_status(status: str, limit: int = 20) -> list[dict]:
    """List orders filtered by status. Valid statuses: pending, shipped, delivered, cancelled.
    Use this to find orders when you don't already know a specific order ID."""
    resp = httpx.get(
        f"{API_BASE}/orders",
        params={"status": status, "limit": limit},
        headers={"Authorization": f"Bearer {API_TOKEN}"},
        timeout=10.0,
    )
    resp.raise_for_status()
    return resp.json()

Two details earn their keep. The default limit: int = 20 becomes an optional field with a sane cap, so the model doesn't page your whole table. And the docstring lists the valid statuses and says when to reach for this tool versus get_order — that disambiguation is exactly what stops the model from guessing an order id it doesn't have.

5. Choose a transport#

mcp.run() serves over stdio by default — perfect when the server runs as a local subprocess of a desktop app or IDE. Nothing else to configure.

For a remote, networked server, serve Streamable HTTP instead. Do not use the old two-endpoint HTTP+SSE transport; it's deprecated for new work. Under the 2026-07-28 stateless spec, Streamable HTTP is stateless — no Mcp-Session-Id, no initialize handshake — so the server can sit behind a plain round-robin load balancer, which is ideal for fronting a stateless API.

One gotcha to hand your infra team: Streamable HTTP requests carry Mcp-Method and Mcp-Name headers. A proxy or WAF that strips unknown headers will silently break every call. Allowlist them.

6. Handle auth#

A remote MCP server inherits none of the trust a local stdio subprocess gets for free. You own two separate jobs.

First, protect the MCP endpoint itself — it's a new front door to your data. Second, authenticate downstream: pass a bearer token to your API from an environment variable, as in the code above. Never put secrets in tool arguments; the model can see those.

And keep write and destructive endpoints off the tool list unless you have a deliberate reason and guardrails. It's easy to wrap DELETE /orders/{id} in three lines — and that's exactly why you shouldn't do it casually. Start read-only; add actions once you trust the loop.

7. Test with the MCP Inspector first#

Before wiring this into any chat client, run it under the MCP Inspector. It launches your server, lists the tools, and lets you invoke each one by hand:

npx @modelcontextprotocol/inspector python server.py

You're checking three things: the tool names and descriptions read the way you intended, the input schema matches your endpoint's parameters, and a real call returns real JSON from your API. Fix the descriptions here — where it's cheap — not after the model has been calling the wrong tool in production. Once the Inspector is green, point your host at the same python server.py command and you're live.

That's the whole method: no rewrite, one thin tool per endpoint, careful names and descriptions, tested in isolation. The API you already shipped is now something an agent can drive.