Blogs

Your MCP server is a production API wearing a hoodie

Tool calls are still API calls. They need auth, logs, timeouts, and limits.

MCP servers are very easy to demo. You expose a tool, point an agent at it, ask it to do something, and it works.

The model can read a file, call an API, query a database, or create some object in another system. That first demo feels nice, and it can make the thing feel smaller than it is.

Underneath, you still built an API. The client happens to be an LLM app, but the usual backend problems still exist.

tools become serious quickly

A toy tool is harmless:

getWeather(city: string)

A real tool can be very different:

refundPayment(paymentId: string, reason: string)
deleteDeployment(appId: string)
createInvoice(customerId: string, amount: number)
rotateProductionSecret(service: string)

At that point the model is changing your system. Reading context is the harmless part.

The MCP spec talks about servers exposing resources, prompts, and tools to clients. It uses JSON-RPC messages and has capabilities, cancellation, progress, logging, and errors.

That sounds like API infrastructure because it is API infrastructure.

So I want the normal boring things: auth, input validation, timeouts, rate limits, and audit logs.

a local token is a thin security model

The first version often looks like this:

agent -> mcp server -> internal service

The MCP server has a token in an env var. The tool trusts the arguments. Logs print whatever came in. The timeout is whatever the runtime does by default.

For a local demo, that might be fine. For a tool that mutates shared state, I would want more structure around it.

I would rather have the call carry enough identity to answer simple questions:

who asked?
which workspace?
which tool?
was it allowed?
what changed?
how long did it take?

Without those answers after a bad tool call, debugging will be painful.

prompts are not permission checks

A tool description might say:

Only use this tool for safe read-only operations.

That helps the model, while policy still belongs in code.

Policy should be code:

function authorizeToolCall(session, toolName, args) {
  if (!session.userId) throw new Error("missing user");
  if (!session.allowedTools.includes(toolName)) throw new Error("tool denied");
  if (args.workspaceId !== session.workspaceId) throw new Error("wrong workspace");
}

Nothing fancy. Rely on code instead of model politeness.

logs need care

MCP logs can easily contain secrets, source code, customer data, or random text someone pasted into chat. So I would not log everything by default.

I would log metadata first:

{
  "event": "mcp_tool_call",
  "trace_id": "tr_123",
  "user_id": "u_42",
  "workspace_id": "w_7",
  "tool": "deploy.restart",
  "allowed": true,
  "duration_ms": 812,
  "result": "success"
}

For risky tools, maybe store redacted arguments somewhere restricted.

I care about knowing what happened without dumping secrets into normal logs.

timeouts are not optional

Every tool should have a timeout.

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  return await callInternalApi(args, { signal: controller.signal });
} finally {
  clearTimeout(timeout);
}

Write tools also need idempotency if retries are possible. Otherwise the agent asks once, the network flakes, the client retries, and now you have two refunds or two deployments. The boring part is still necessary.

my minimum bar

If I review an MCP server that touches real systems, I want to see denied-by-default tools, schema validation, redacted logs, timeouts, trace ids, and a way to turn off one risky tool without taking down the whole server.

This is normal API discipline.

I still like MCP. A standard way to expose tools to LLM apps is useful.

I want the demo feeling to stay separate from production judgement.