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 defaultWire 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
- 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.
- 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.
- 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
- stdio is local trust. The moment you serve the same server over HTTP, you own auth — a bearer token at minimum, and the MCP authorization spec is still moving, so check it before inventing your own scheme.
- The MCP server is now part of your API’s public surface. Rename an upstream endpoint and the docstring lies to every agent in the building. Version it like an SDK, because it is one.
- It is a new artifact to maintain. A hundred lines, but owned lines, with an owner, or it becomes the unclaimed cron job of the agent era.
- Tool results land in the model’s context. A list_services that returns ten thousand rows is a context-window grenade; paginate at the tool boundary, not in the prompt.
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.