Building AI Agents with Model Context Protocol
How MCP standardizes how LLMs talk to external tools, and a practical pattern for shipping a production-grade agent with a constrained tool surface.

Why a protocol at all
The first generation of LLM "agents" was just a model, a tools array, and a prayer. Every team reinvented the glue between the model and the outside world — a function here, a webhook there, a half-baked retry loop everywhere. The Model Context Protocol (MCP) is an attempt to standardize that glue: a single spec for how a model discovers, calls, and reasons over external capabilities.
If you've ever shipped a tool-calling agent and then watched a second team rebuild the same tool with a different signature, the value proposition is obvious. The interesting question is what it takes to do MCP well in production.
The mental model
MCP gives you three primitives:
- Resources — data the model can read. Think files, database rows, a Notion page.
- Tools — actions the model can take. Callable, side-effectful, ideally idempotent.
- Prompts — reusable instruction templates that compose with the above.
The discipline is in keeping the surface small. A model with 40 tools spends its budget guessing which one to call; a model with 5 well-named ones actually gets work done. Every tool you expose is a tax on every inference.
A practical pattern
Here's the shape that has survived real load for us:
- One server, one domain. Don't pile every integration onto a single MCP server. A
github-mcpserver that owns repo issues and PRs is easier to reason about than a "company-mcp" that also knows about billing. - Tools return structured data, not prose. Resist the temptation to have the tool pre-format text for the model. Return JSON, let the model render. Cheaper to test, cheaper to cache, cheaper to swap models.
- Constrain the tool surface per task. Even if a server exposes 20 tools, you don't have to hand all 20 to every agent run. Filter by task — a PR-review agent only needs
get_pr_diff,post_review_comment, andlist_pr_files. - Log the tool call, not just the result. When an agent goes off the rails, the receipts that matter are the call sequences, not the final answer.
What this looks like in code
A minimal tool definition in TypeScript:
const tools = [
{
name: "get_pr_diff",
description: "Fetch the unified diff for a pull request.",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
number: { type: "integer" },
},
required: ["owner", "repo", "number"],
},
},
];
Note the description. It's not documentation for a human — it's the only context the model has to decide whether to call this tool. Treat it like a one-line API contract, because that's exactly what it is.
A common mistake
Teams ship an agent that can "read and write to anything" and then act surprised when it writes to the wrong thing. The fix is almost never more prompt engineering. It's tighter permissions at the tool boundary: a tool that can delete a row should require an explicit confirm: true in its input schema, and the agent loop should refuse to populate that field without a human in the chain.
The protocol doesn't enforce this. You do.
Where this is going
MCP is still early. The spec will move, server implementations will churn, and the ergonomics will get better. But the underlying bet — that the interface between a model and the world should be a protocol, not a pile of one-off scripts — is the right one. Build your agents against the protocol, keep your surfaces small, and you'll ride the wave instead of being dragged by it.

