Model Context Protocol (MCP), explained for people who already build agents
Model Context Protocol explained for agent builders: what an MCP server is, tools vs resources vs prompts, a minimal TypeScript example, and MCP vs tool-calling.
On this page
- The real problem MCP solves: every tool integration is bespoke glue
- The client/server model: who connects to whom
- Three primitives: tools, resources, prompts
- A minimal MCP server in TypeScript
- stdio vs Streamable HTTP: pick by where the server runs
- MCP vs plain tool-calling: different axes, not a fork in the road
- When MCP is worth it — and when it's overkill
- Takeaways
- FAQ
I've wired tools into agents by hand, and that's exactly why Model Context Protocol (MCP) caught my attention. My news-desk discovery agent talks to search APIs, a Google News RSS crawl, and a primary-source confirmer through a provider-agnostic LLM layer running in JSON mode. My RAG chatbot exposes retrievers and lookups as LangChain tools with structured tool-calling. In both, the agent and its tools are welded together: the tool schemas, the dispatch code, the error handling — all bespoke glue living inside one codebase.
That glue is precisely the problem MCP sets out to kill. So here's the honest version for people who already build agents: what an MCP server actually is, what it changes, and — the part most write-ups skip — when it's worth the ceremony and when it's pure overhead.
The real problem MCP solves: every tool integration is bespoke glue
Today, if I want my agent to use a tool, I write a function, hand-author its JSON schema, register it with whatever model SDK I'm holding, and parse the model's tool-call back into a real invocation. Do that for ten tools and you have ten integrations. Now I want the same tools in Claude Desktop, in my IDE, and in a teammate's separate agent — I get to reimplement the glue three more times, once per host, because each one couples tools to the agent in its own way.
The coupling runs both directions. The tool doesn't know how to expose itself; the agent doesn't know how to discover tools it wasn't compiled with. There's no shared interface. MCP is the answer to "can we standardize that interface so any client talks to any tool server?" — the same move LSP made with "any editor, any language server."
The client/server model: who connects to whom
MCP splits the world into two roles:
- An MCP client lives inside the host application — Claude Desktop, an IDE, or your own agent. It connects to servers and surfaces their capabilities to the model.
- An MCP server is a standalone process that exposes capabilities. It knows nothing about which model or agent is calling it.
They speak JSON-RPC 2.0 over a transport. On connect, client and server negotiate capabilities, and the server advertises what it offers. The agent loop doesn't change shape — it still reasons, picks a tool, calls it, feeds the result back. What changes is that "the tool" now lives behind a standard wire protocol instead of an in-process function reference.
Worth nailing early: MCP is a protocol and transport, not a replacement for tool-calling. The model still emits tool calls the way it always has. MCP standardizes how those tools are described and reached. More on that distinction below — it trips people up constantly.
Three primitives: tools, resources, prompts
A server exposes three kinds of capability, and the difference is about who is in control:
- A tool is a model-controlled action with side effects or computation —
search_news,create_ticket,run_query. The model decides when to invoke it. This is the primitive that maps directly onto function/tool-calling. - A resource is application-controlled, read-only context identified by a URI — a file, a database row, a doc. Think
GETdata the client can attach to context, not an action the model fires.file:///logs/today.txtis a resource. - A prompt is a user-controlled, reusable template — a parameterized message the user deliberately invokes, like a slash-command. "Summarize this PR," diff slotted in.
The split matters more than it looks. In my hand-rolled agents, everything became "a tool" because tools were the only hammer I had — even read-only context fetches. MCP gives read-only data its own lane (resources) and human-triggered workflows their own lane (prompts), so the model stops treating a document fetch like an action it has to decide to take.
A minimal MCP server in TypeScript
Here's a complete server using the official TypeScript SDK (@modelcontextprotocol/sdk) that exposes one tool. This is the real, currently-installable API: McpServer plus registerTool, where inputSchema is a raw Zod shape the SDK compiles into the JSON Schema the client advertises.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "news-tools", version: "1.0.0" });
server.registerTool(
"search_news",
{
title: "Search News",
description: "Search recent articles by keyword and country.",
inputSchema: { query: z.string(), country: z.string().default("IN") },
},
async ({ query, country }) => {
const hits = await searchProvider(query, country); // your real call
return { content: [{ type: "text", text: JSON.stringify(hits) }] };
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
That's the whole server. inputSchema is a plain object of Zod validators — not wrapped in z.object(...) — and because country has a .default(), it's advertised as optional. The handler returns a content array; text here, but it can be structured data, images, or embedded resources. Any MCP client can now spawn this process, list its tools, see search_news and its schema, and call it. I wrote zero client-specific glue.
stdio vs Streamable HTTP: pick by where the server runs
The transport is a deployment choice, not a feature one:
- stdio (above): the client launches the server as a child process and they exchange JSON-RPC over stdin/stdout. Perfect for local tools — a filesystem server, a git server, anything touching your machine. Zero network, zero auth, dead simple. This is what most desktop MCP configs use.
- Streamable HTTP: the server runs as a remote service; the client POSTs JSON-RPC to an endpoint and can receive a stream back over Server-Sent Events. This is what you want for a hosted, multi-user, or cross-network server, and it's where you bolt on auth.
Swapping is roughly a two-line change — same server, different transport:
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
// construct the transport, then: await transport.handleRequest(req, res, body)
// on your HTTP route (Express, etc.), and server.connect(transport).
The tool definitions don't move. That portability is the entire point.
MCP vs plain tool-calling: different axes, not a fork in the road
This is the confusion worth clearing up for good. Tool-calling is a model capability: the LLM emits a structured request to call a named function. MCP is a protocol for exposing and reaching those functions across process and host boundaries. They sit on different axes, and MCP doesn't replace tool-calling — it feeds it.
The flow: your MCP client calls tools/list on the server, gets back tool definitions, hands those to the model as its tool list, the model emits a tool-call, and the client routes that call to the server via tools/call. The model never knows MCP exists. It just sees tools.
agent ──tools/list──▶ MCP server (discover)
model ──"call search_news"──▶ agent (normal tool-calling)
agent ──tools/call──▶ MCP server (dispatch)
So in my two-stage RAG retriever, the LangChain tool-calling would stay exactly as it is. MCP only changes where the tool lives — behind a standard server instead of an import — so the same retriever could serve my chatbot and someone else's IDE without a rewrite.
When MCP is worth it — and when it's overkill
Straight talk: I build tool-using agents for real, but I have not shipped MCP servers in production. This is the mental model and how I'd reach for it, not a war story. With that caveat, here's my honest read on the trade-off.
Reach for MCP when:
- Multiple clients need the same tools. One server, reused by Claude Desktop, your IDE, and your own agent, beats N bespoke integrations. This is the killer case.
- You want the ecosystem. Dropping in a community Postgres, GitHub, or filesystem server in minutes is genuine leverage.
- Tools and agent have separate lifecycles — different teams, repos, or deploy cadences. The protocol boundary is a clean seam.
Skip it when:
- One agent, one codebase, a handful of tools. That's most of my work. Native tool-calling with a few typed functions is less code, fewer moving parts, and easier to debug than running a JSON-RPC server. MCP here is pure ceremony.
- Latency or call volume is tight. An in-process function call beats a JSON-RPC round-trip, and for high-frequency triage like the discovery phase in the news-scraper case study, I'd keep tools native.
- You haven't felt the pain yet. MCP solves a coupling problem. No second consumer, no problem — so don't pre-pay the abstraction.
Takeaways
- MCP standardizes the tool interface so any client can use any server over JSON-RPC — it kills bespoke per-host glue.
- Three primitives: tools (model-controlled actions), resources (read-only context by URI), prompts (user-triggered templates).
- It is not a replacement for tool-calling — it discovers and dispatches the tools the model still calls normally.
- stdio for local, Streamable HTTP for remote — same server, swappable transport.
- Worth it for multiple clients and shared ecosystems; overkill for one self-contained agent.
Standards earn their keep when more than one thing has to agree. If your agent is the only thing talking to your tools, hand-rolled glue is fine — but the day a second client wants in, MCP is the seam you'll wish you'd built.
Frequently asked questions
- What is the Model Context Protocol (MCP)?
- MCP is an open standard for how AI applications connect to tools and data. An MCP server exposes tools, resources, and prompts over JSON-RPC 2.0, so any MCP client — Claude Desktop, an IDE, or your own agent — can use any server without bespoke integration code.
- Is MCP a replacement for function or tool calling?
- No. Tool-calling is a model capability where the LLM emits a structured call. MCP is a protocol for exposing and reaching those tools across process and host boundaries. The client lists MCP tools, hands them to the model, and routes the model's tool-call back to the server — the model never knows MCP is involved.
- What's the difference between a tool, a resource, and a prompt in MCP?
- A tool is a model-controlled action with side effects, like search_news. A resource is application-controlled, read-only context identified by a URI, like a file or database row. A prompt is a user-triggered, reusable template, like a slash-command. The split is about who controls invoking it.
- When should you use MCP instead of native tool-calling?
- Use MCP when multiple clients need the same tools, when you want to drop in community servers, or when tools and the agent have separate lifecycles. Skip it for a single self-contained agent with a few tools, or when latency is tight — native in-process tool-calling is simpler and faster there.
An editorial team's morning routine — search dozens of sources, judge what matters, confirm it, then draft — turned into a two-phase, human-in-the-loop agent streamed live over SSE. Here's the architecture and the decisions that kept it cheap and trustworthy.
ReadMetadata-filtered RAG fixes single-shot retrieval that returns junk on multi-topic corpora. How I built a metadata pre-filter, vector search, and LLM rerank pipeline.
Read