# Model Context Protocol (MCP) by [kirupa](https://www.kirupa.com/me/index.htm) | filed under [AI Fundamentals](https://www.kirupa.com/ai_fundamentals/index.htm) In [What Makes an AI Agent](https://www.kirupa.com/ai_fundamentals/ai_agents.htm), we gave Rye, the Crumb & Co. bakery's back-office agent, a handful of tools for looking up sales, pre-orders, and flour. We wrote every one of those tools ourselves, inside Rye's code. Now Odette wants the same order lookups inside the Claude app on her laptop, and the bakery's web developer wants them inside VS Code. Do we write all of that again, twice? That's the problem the Model Context Protocol (MCP) solves. It's a standard way for AI apps to connect to tools and data, so a tool written once works in any app that speaks MCP. In the following sections, we'll look at the problem it solves, the pieces it's made of, what actually travels over the wire, and how to build a tiny MCP server for the bakery. We'll finish with the security side, which matters more than it might seem at first. Onwards! ## The Wiring Problem Without a shared standard, every app that wants to use a system needs its own custom code for it. Three apps and three bakery systems add up to nine integrations: ![Without MCP, three apps (Rye, Claude, and VS Code) each need custom code for each of three bakery systems (orders, stock, and recipes), which is nine integrations. With MCP, each app speaks MCP once and each system offers one MCP server, which is three clients and three servers, six pieces in total.](https://www.kirupa.com/ai_fundamentals/images/model_context_protocol_wiring.png) Each app describes tools in its own format, calls them its own way, and handles errors its own way, so the work multiplies. A fourth system means three more integrations, and so does a fourth app. People often call this the N × M problem, where N apps times M systems gives the number of integrations somebody has to build and keep working. MCP turns that multiplication into addition. Each app learns to speak MCP once, and each system gets wrapped in one MCP server. Three apps plus three servers is six pieces, and a new server works in every app right away. When I talked with Den Delimarsky, one of MCP's [lead maintainers](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/MAINTAINERS.md), he put it like this: > If you go to Claude, you have one implementation. Then you go to OpenAI or Google and they have a different implementation. None of those things work together. MCP is trying to be that bridge. > > [Den Delimarsky, on The Model Context Protocol (MCP) Explained](https://www.kirupa.com/podcast/den_mcp.htm) [MCP's own introduction](https://modelcontextprotocol.io/introduction) suggests we "think of MCP like a USB-C port for AI applications", which is a good picture to keep in mind. There's one plug shape, and lots of things fit it. MCP started at Anthropic, which [open-sourced it in November 2024](https://www.anthropic.com/news/model-context-protocol). A year later, Anthropic [donated it to the Agentic AI Foundation](https://www.anthropic.com/news/donating-the-model-context-protocol-and-establishing-of-the-agentic-ai-foundation), a fund under the Linux Foundation, so the protocol no longer belongs to any single AI company. ## Hosts, Clients, and Servers MCP has three roles, and their names are easy to mix up, so let's go slowly. Here's how they fit together for Rye: ![Rye's app is the host. Inside it sit the model and two MCP clients. Client A connects to the bakery server, which runs on the same computer and talks over stdio. Client B connects to the supplier's server on the internet over Streamable HTTP. Each client talks to exactly one server.](https://www.kirupa.com/ai_fundamentals/images/model_context_protocol_architecture.png) The **host** is the AI app itself, like Rye, the Claude app, or VS Code. It owns the conversation with the model and decides what the model gets to see and do. [The MCP specification](https://modelcontextprotocol.io/specification/2026-07-28) describes hosts as "LLM applications that initiate connections". A **server** is a small program that exposes one system, like the bakery's stock and orders, through MCP. It can run on the same computer as the host or somewhere out on the internet. A **client** is the piece inside the host that talks to one server. If Rye connects to two servers, it has two clients. The spec's [architecture page](https://modelcontextprotocol.io/specification/2026-07-28/architecture) is specific about this, with "each client having a 1:1 relationship with a particular server". That separation means one server can't read another server's messages. Under the hood, every message is [JSON-RPC 2.0](https://www.jsonrpc.org/specification), a simple, long-established format where a request names a method and the response carries the same ID back. The spec says MCP takes some inspiration from the Language Server Protocol, which is the standard that lets one language plugin work across many code editors. MCP does the same thing for tools and AI apps. ## What a Server Offers A server can offer three kinds of things, and the main difference between them is who decides when each one gets used: ![Three cards. Tools are actions to run, and the model decides when to call them, like check_stock. Resources are data to read, and the app decides when to include them, like bakery://menu/today. Prompts are request templates, and the user picks them, like plan_catering.](https://www.kirupa.com/ai_fundamentals/images/model_context_protocol_primitives.png) **Tools** are actions, like `check_stock`. They're the same tools we met in [Tool Calling](https://www.kirupa.com/ai_fundamentals/tool_calling.htm), and the [spec's page on tools](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) calls them "model-controlled", which means the model decides when to call one. **Resources** are data, like today's menu, and each one has an address (a URI) such as `bakery://menu/today`. The [spec calls resources](https://modelcontextprotocol.io/specification/2026-07-28/server/resources) "application-driven", so the host decides when to include them, like attaching the menu to a conversation about catering. **Prompts** are request templates that the user picks, often from a menu of slash commands. A `plan_catering` prompt might ask for the number of guests and turn it into a well-written request. The [spec calls prompts](https://modelcontextprotocol.io/specification/2026-07-28/server/prompts) "user-controlled". ## What Travels Over the Wire Let's watch Rye check the stock through the bakery's MCP server, one message at a time: ![Three lanes: the model, the client, and the bakery server. The client sends tools/list, and the server answers that check_stock is available. The tool goes into the model's request. The model asks to call check_stock. The client sends tools/call, the server answers 6 left, and the result goes back into the model's context.](https://www.kirupa.com/ai_fundamentals/images/model_context_protocol_sequence.png) First, Rye's client asks the server what tools it has, with a `tools/list` request. The server answers with each tool's name, its description, and a [JSON schema](https://www.kirupa.com/ai_fundamentals/structured_outputs_json_schema.htm) for its arguments. Rye adds those tools to its request to the model. When the model asks for `check_stock`, the client sends a `tools/call` request: ```json { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "check_stock", "arguments": { "product": "cinnamon_roll" } } } ``` The server sends back a result with the same `id`: ```json { "jsonrpc": "2.0", "id": 2, "result": { "resultType": "complete", "content": [{ "type": "text", "text": "6" }], "structuredContent": { "result": 6 }, "isError": false } } ``` These are real messages from the small server we're about to build, trimmed a little. Like the examples in the spec, we've left out a `_meta` field that every request carries. In the current version of the spec, which is [2026-07-28 at the time of writing](https://modelcontextprotocol.io/specification/versioning), that field holds the protocol version and the client's capabilities, so each request stands on its own. Earlier versions set those up once, with an `initialize` handshake at the start of a connection, and you'll still see that handshake in a lot of tutorials and servers. Notice how little is new here. It's tool calling, with the note passed to a server instead of to a function in our own code. ## Building a Tiny Server Here is a complete MCP server for the bakery, written with the official [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) (version 2.3 at the time of writing): ```python from mcp.server import MCPServer mcp = MCPServer("Crumb & Co. Bakery") STOCK = {"cinnamon_roll": 6, "croissant": 14, "baguette": 3} @mcp.tool() def check_stock(product: str) -> int: """How many of a bakery item are on the shelf right now.""" return STOCK.get(product, 0) @mcp.resource("bakery://menu/today") def todays_menu() -> str: """Today's menu, with prices.""" return "Cinnamon roll $4.50\nCroissant $3.75\nBaguette $5.00" @mcp.prompt() def plan_catering(guests: int) -> str: """Plan a catering order for an event.""" return f"Plan a breakfast order for {guests} guests. Check stock first." if __name__ == "__main__": mcp.run() ``` Each decorator turns a plain function into one of the three things a server can offer. The type hints and docstrings do a lot of the work, too. The SDK turned `product: str` into the JSON schema for `check_stock`'s arguments and used the docstring as its description. The last line, `mcp.run()`, serves everything over stdio, which we'll look at next. (In a real bakery, `check_stock` would query the stock database instead of a dictionary, of course.) To try it, the SDK's `mcp dev bakery_server.py` command opens the server in the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), a small web page for listing and calling a server's tools by hand. To use it from a real host, we add the command that starts the server to that host's settings, and from then on its tools show up next to everything else. ## Local or Remote: Transports MCP messages can travel in two standard ways, which the spec calls [transports](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports). With **stdio**, the host starts the server as a child process on the same computer, and they talk through standard input and standard output, one JSON message per line. It's simple and needs no network, which makes it a great fit for servers that work with our own files and tools. There's one rule worth knowing. The server must not write anything to stdout that isn't an MCP message, so a stray debugging `print()` can garble the conversation. Logs go to stderr instead. With **Streamable HTTP**, the server lives at a URL, like a web API. Each message is an HTTP POST to one endpoint, and the answer comes back as either a plain JSON response or a stream of events for longer work. That's how the bakery's flour supplier could host one server that every bakery connects to. A remote server needs to know who's calling, so the spec bases its [authorization](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) on OAuth 2.1, the family of standards that lets one app act on our behalf in another without ever seeing our password. Local stdio servers skip all that and get their credentials from the environment. Here are the two transports side by side: ![Two ways MCP messages travel. With stdio, the server runs on the same computer as a child process, they talk through standard input and output with one JSON message per line, logs go to stderr, and credentials come from the environment. With Streamable HTTP, the server lives at a URL, each message is an HTTP POST to one endpoint, answers come back as JSON or a stream of events, and authorization is based on OAuth 2.1.](https://www.kirupa.com/ai_fundamentals/images/model_context_protocol_transports.png) Authorization is where Den first got involved. As he told me, his MCP journey started because "the authorization spec wasn't in the shape that was ideal for the average developer", so he jumped in with the community and helped rewrite it. ## MCP and Security MCP makes it very easy to plug tools into an agent, and that's exactly why it needs care. The spec is blunt about it: "Tools represent arbitrary code execution and must be treated with appropriate caution". It also says hosts "must obtain explicit user consent before invoking any tool". The first thing to keep in mind is that everything a server sends ends up as text in front of the model. A tool description from a server we don't trust could contain instructions aimed at the model, and so could a tool result, like a web page a fetch tool brings back. Text that tries to steer a model away from what we asked is called prompt injection, and [Prompt Injection Defense](https://www.kirupa.com/ai_fundamentals/prompt_injection_defense.htm) covers it later in this series. The spec makes a similar point about a tool's annotations, which are hints like ***this tool only reads data***. Clients must treat them as untrusted unless they come from a trusted server. The second thing is that servers combine in ways nobody planned. Each of these servers looks harmless on its own: ![Three MCP servers each look harmless alone: an orders server with private data, a web fetch server that brings in untrusted text, and an email server that can send messages out. When one agent, Rye, connects to all three, it has every leg of the lethal trifecta.](https://www.kirupa.com/ai_fundamentals/images/model_context_protocol_trifecta.png) Put them in one agent, though, and Rye now has what Simon Willison named the lethal trifecta: access to private data, exposure to untrusted content, and a way to send data out. A sentence hidden in a web page could tell Rye to email the customer list to a stranger, and every piece it needs to do that is plugged in. [The Lethal Trifecta](https://www.kirupa.com/ai_fundamentals/lethal_trifecta.htm) goes deeper later on, but the MCP lesson is simple. Think about the whole set of servers an agent has, not just each one by itself. A few habits go a long way. Install servers from sources you trust, the way you would any package. Den pointed out that MCP's registry of servers "doesn't do security scanning or vulnerability scanning", so trust is still our call. Give each server only the access it needs, like a read-only database login for `check_stock`, which is the idea behind [Least-Privilege Access for Tools](https://www.kirupa.com/ai_fundamentals/least_privilege_tools.htm). And keep a person in the loop for tools that change things, as in [Human Approval for Risky Actions](https://www.kirupa.com/ai_fundamentals/human_approval_for_risky_actions.htm). The spec agrees: > For trust & safety and security, there SHOULD always be a human in the loop with the ability to deny tool invocations. > > [MCP specification, Tools](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) ## Conclusion MCP solves a wiring problem. Without it, every AI app needs custom code for every system, and the work multiplies. With it, each app speaks MCP once through its clients, and each system gets wrapped once in a server. Servers offer tools the model calls, resources the app reads, and prompts the user picks, all as JSON-RPC messages over stdio or Streamable HTTP. And because every one of those messages ends up as text in front of a model, choosing servers carefully matters just as much as building them. If you'd like to hear it from someone who helps steer the protocol, my conversation with Den in [The Model Context Protocol (MCP) Explained](https://www.kirupa.com/podcast/den_mcp.htm) covers where MCP came from and where it's headed. Den and I also talked about [what makes for successful products](https://www.kirupa.com/podcast/den_delimarsky_successful_products.htm), which is a fun listen if you build things for other people. Next, we'll put everything in this section together and look at [how a chatbot inside Slack works](https://www.kirupa.com/ai_fundamentals/slack_chatbot.htm).