MCP, explained with a to-do list
What the Model Context Protocol actually is, why it exists, and how an AI app talks to a tool. With two demos you can click through.
6 min read
- ai
- mcp
- typescript
A language model on its own can only talk. It can't read your calendar, check a database, or tick something off your to-do list. It has no hands.
To give it hands, people started writing glue code: this app talks to GitHub like so, that app talks to Slack like so, and every new pairing means writing it again. MCP, the Model Context Protocol, is an attempt to stop doing that. It's an open standard for how an AI app connects to the things around it.
The usual analogy is USB-C, and it's a fair one. Before it, every device had its own charger. After it, one port fits most things. MCP is the port.
The problem it solves
Say you have a few AI apps (a chat assistant, a code editor, an internal bot) and a few things they should reach (your files, a database, a ticket system, a calendar). If every app has to be wired to every tool by hand, the number of connectors is apps × tools. Add one app and you write a connector for every tool. Add one tool and you write one for every app.
With a shared protocol, each app learns to speak MCP once, and each tool learns to speak MCP once. The count drops to apps + tools. Play with the numbers:
Tap or hover an app or tool to see what it has to build.
MCP saves 5 connectors. Add one more app and it saves 8.
At 3 apps and 4 tools the difference is 12 versus 7. At 6 and 6 it's 36 versus 12. It stops being a nice-to-have fast.
Three roles
MCP has three moving parts, and the names are easy to mix up.
| Role | What it is | Example |
|---|---|---|
| Host | The AI app you actually use. It owns the model and the conversation. | Claude Desktop, an editor, your own agent |
| Client | A piece inside the host that holds one connection to one server. | Created by the host, one per server |
| Server | A small program that exposes some capability in a standard shape. | A to-do server, a GitHub server, a Postgres server |
The host is the one you talk to. For each server it connects to, it spins up a client, and the client does the protocol work. The server doesn't know or care which model is on the other end. That's the point.
What a server can offer
A server can expose three kinds of things:
| Primitive | Who decides to use it | Think of it as |
|---|---|---|
| Tools | The model | Actions it can take: add a to-do, run a query, send a message |
| Resources | The app | Read-only context: a file, a record, a schema |
| Prompts | The user | Reusable templates, like a slash command |
Most of the attention goes to tools, because that's where the model gets hands. A tool is just three things: a name, a plain-language description, and a schema for its inputs. The description matters more than it looks, because it's what the model reads to decide whether this is the right tool.
See it work
Below is a pretend AI app and a tiny to-do server. The strip at the top shows what's happening under the hood. The chat is what you'd see in a real app. The panel on the right is the server, and the bottom shows every message on the wire. The model's side is scripted, but the server runs real code on real state, and the messages have the same shape a real server sends.
Do it in this order, because the contrast is the lesson:
- Ask something before connecting. The model has no way to reach your list.
- Connect todo-server and watch the discovery exchange.
- Ask again. Pause or step through it if it moves too fast.
- Type your own: add something, complete something, or try Delete everything.
Ask something first, before connecting. Then connect todo-server and ask again.
On the wire
nothing sent yet
Connect todo-server and the first message appears here.
Three things worth noticing:
- Discovery comes first. The client asks
tools/list, and the server answers with its tools. Nothing is hard-coded in the app. Connect a different server and the model gets different abilities with no code change in the host. - The model never touches your data. It picks a tool and fills in arguments. The server runs the code and sends back text. When you ask it to mark something done, it lists first, because
complete_todowants an id, not a name. That's the model working out a plan from tool descriptions alone. - The tools are the boundary. Ask it to delete everything and nothing happens, because the server has no delete tool. Whoever writes the server decides what the model can do.
What's on the wire
Underneath, the messages are JSON-RPC 2.0, which is just requests and responses with an id to match them up. The two you saw are the heart of it:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": { "name": "add_todo", "arguments": { "title": "Publish the MCP post" } } }{ "jsonrpc": "2.0", "id": 2,
"result": { "content": [{ "type": "text", "text": "Added #4: Publish the MCP post" }] } }A server can run as a local process the host launches and talks to over stdin and stdout, or as a remote service over HTTP. The messages are the same either way.
The server is the easy part
Here's roughly what the to-do server looks like with the TypeScript SDK. This is the shape of the 1.x API, so check the SDK docs for the current names before copying it.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const todos: { id: number; title: string; done: boolean }[] = [];
const server = new McpServer({ name: "todo-server", version: "1.0.0" });
server.registerTool(
"add_todo",
{
description: "Add a new to-do.",
inputSchema: { title: z.string() },
},
async ({ title }) => {
const id = todos.length + 1;
todos.push({ id, title, done: false });
return { content: [{ type: "text", text: `Added #${id}: ${title}` }] };
},
);
await server.connect(new StdioServerTransport());That's a working server. Point a host at it and the tool shows up. The SDK handles discovery, validation and the wire format. You write the function.
Where it gets hard
The protocol is simple. Running it safely is not.
- Tool descriptions are input to the model. A server you don't control can write instructions into its descriptions or its results, and the model may follow them. Treat servers like browser extensions: only connect ones you trust.
- Write tools need a person. Reading a list is harmless. Sending an email or deleting a record isn't. The best hosts ask before running those. I wrote about the same idea in AI tools need an off-ramp.
- More tools is not better. Every tool description costs context, and too many similar ones confuse the model. Small, sharp servers beat one that does everything.
Where it's going
MCP started at Anthropic in late 2024 and has since been handed to the Linux Foundation's Agentic AI Foundation, so it's no longer one company's project. The spec has been revised several times, and details like how connections are set up and authorised keep moving. I couldn't check the latest revision while writing this, so treat the wire examples as illustrative and read the official spec before building on them.
What I don't expect to change is the shape: a host, a client per server, and tools that a model discovers and calls. If you remember one sentence, make it this one.
MCP lets an app ask a server what it can do, then lets the model do it.
If you want to build something with it, a to-do server is a decent first project. Then swap the array for a real database and see how quickly it stops being a toy.