hubagenticai

Tutorials Builder

How to wrap a REST API in an MCP server (without handing the agent the whole API)

Turn any REST API into agent-safe MCP tools: a narrow allowlist instead of a proxy, PII redaction before the model sees data, and a fake API so you can test the whole thing offline.

updated 2026-07-04 ⏱ 45 min

The most common real-world MCP server isn’t a database or a filesystem — it’s a wrapper around some REST API your organization already has. And the most common mistake is building it as a proxy: one call_api(method, path, body) tool that forwards anything. That hands the model your entire API surface, including endpoints you never considered. The right shape is a few narrow tools that each do one blessed thing.

Step 1 — A fake API to develop against

Never develop an agent integration against production. Ten lines of stdlib give us a realistic stand-in. Create fake_api.py:

import json
from http.server import BaseHTTPRequestHandler, HTTPServer

ORDERS = {
    "1001": {"id": "1001", "status": "shipped", "total": 49.90,
             "customer_email": "jane@example.com"},
    "1002": {"id": "1002", "status": "processing", "total": 129.00,
             "customer_email": "sam@example.com"},
}

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        order = ORDERS.get(self.path.removeprefix("/orders/"))
        self.send_response(200 if order else 404)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(order or {"error": "not found"}).encode())

    def log_message(self, *args):
        pass  # quiet logs

if __name__ == "__main__":
    print("fake API on http://127.0.0.1:8765")
    HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()

Run it in one terminal: python fake_api.py. Note the seeded data includes customer_email — deliberately. Real APIs return more than the agent should see, and we’ll deal with that properly.

Step 2 — The MCP server: narrow tools, not a proxy

Create api_mcp.py in the same venv as tutorial #1 (pip install "mcp[cli]"):

import json
import os
import urllib.request
from mcp.server.fastmcp import FastMCP

API_BASE = os.environ.get("ORDERS_API", "http://127.0.0.1:8765")

mcp = FastMCP("orders-api")

def _get(path: str) -> dict:
    # Allowlist: this server can reach /orders/* and nothing else.
    if not path.startswith("/orders/"):
        raise ValueError(f"path not allowed: {path}")
    with urllib.request.urlopen(f"{API_BASE}{path}", timeout=5) as resp:
        return json.load(resp)

@mcp.tool()
def get_order(order_id: str) -> str:
    """Fetch one order's status and total by its numeric ID."""
    if not order_id.isdigit():
        return "invalid order id: must be numeric"
    try:
        data = _get(f"/orders/{order_id}")
    except Exception:
        return f"order {order_id} not found"
    data.pop("customer_email", None)  # redact PII before the model sees it
    return json.dumps(data)

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

Three deliberate choices, in order of importance:

  1. Input validation before the requestisdigit() kills path injection ("1001/../admin") before it can exist.
  2. Redaction at the boundary — the model never receives customer_email, so it can’t leak, log, or reason about it. Anything the model sees can end up in a transcript; filter before, not after.
  3. The allowlist in _get — even if you add ten more tools, this server physically cannot reach other endpoints.

Step 3 — Verify with the Inspector, then a client

mcp dev api_mcp.py

Call get_order with 1001 — you should get status and total, and no email field. Then try 1001/../admin and watch validation reject it. Testing the misuse paths is the part everyone skips; for an agent-facing server, the misuse paths are the product.

Step 4 — Where auth goes (when the real API needs it)

The pattern extends without changing shape: the API key lives in an environment variable read by the server (os.environ["ORDERS_API_KEY"]), attached to requests inside _get. The model never sees the credential — it can’t; it’s not in any prompt or tool result. One process owns the secret, and rotating it touches one place.

Troubleshooting

URLError: Connection refused from every tool call

The fake API isn’t running, or the MCP client launched your server with a different environment. Check ORDERS_API and remember: MCP clients spawn servers as subprocesses that don’t inherit your shell exports unless configured to.

Works in the Inspector, fails when an agent uses it

Look at how the model formats arguments — agents pass "order 1001" or 1001.0 where you expect "1001". Be liberal in what you parse (strip non-digits), strict in what you forward.

newsletter

One practical agentic-AI guide in your inbox. No news, no hype.

Tutorials and decision frameworks as they ship. Unsubscribe anytime.