Your internal API as an MCP server. An afternoon project.


Yesterday’s post was about pointing one MCP connection at 2,400 external tools. The complementary move is smaller and more immediately useful: expose the API you already have to the agents you already run.

Every team has that one internal service — the deploy dashboard, the inventory API, the thing with a REST endpoint and no client anyone wants to write. Wrap it as an MCP server and “what is the status of the checkout service” becomes a question you ask the agent instead of a bookmark you keep losing.

The whole protocol you need

MCP is JSON-RPC over two transports: stdio for servers that live on your machine, Streamable HTTP for shared ones. It has three primitives — resources, prompts, tools — and for this job you need exactly one: tools. Tools are what the agent decides to call.

In Python with the official SDK, the entire server is:

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

mcp = FastMCP("inventory")

API = os.environ["INVENTORY_API"]
TOKEN = os.environ["INVENTORY_TOKEN"]

async def _get(path: str) -> dict:
    async with httpx.AsyncClient(base_url=API, timeout=10) as c:
        r = await c.get(path, headers={"Authorization": f"Bearer {TOKEN}"})
        r.raise_for_status()
        return r.json()

@mcp.tool()
async def list_services() -> list[dict]:
    """List every service with owner and environment."""
    return (await _get("/services"))["items"]

@mcp.tool()
async def service_status(name: str) -> dict:
    """Health, version and last deploy of one service, by exact name."""
    return await _get(f"/services/{name}/status")

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

Wire it into Claude Code with claude mcp add inventory -- uv run inventory_mcp.py, or drop the same command into a committed .mcp.json so every teammate’s agent picks up the same tools. That is the afternoon. The rest is doing it well.

The three rules

  1. Descriptions are the interface. The model never sees your code. It sees names, docstrings and schemas. “by exact name” in the docstring is the difference between the agent calling the right thing and fuzzy-matching “checkout-svc” against a list and telling you, with total confidence, about the wrong service.
  2. Errors are for the model, not the traceback. A stack trace burns tokens and helps nobody. Return short text the agent can act on: “no service named X. closest matches: checkout-api, checkout-worker.” The agent recovers from that in one turn.
  3. Keep the tool count under twenty and the verbs boring. List, get, status first. A restart tool can come later, with a dry-run parameter and a permission conversation. Every write tool is a prompt-injection surface with your name on it.

The honest trade-offs

Where I landed

An afternoon, a hundred lines, and the deploy dashboard went from a URL I keep losing to a sentence I type. The external-tool gateway from yesterday covers the world outside the building; this covers the world inside it. Between the two, “ask the agent” is quietly becoming the interface to both.