Sam Austin on October 10, 2026

Model Context Protocol (MCP) Explained: Connect LLMs to Tools and Data

Model Context Protocol (MCP) Explained: Connect LLMs to Tools and Data
Contents

Figure 1: One standard port beats a folder of one-off adapters

Every AI app you build needs the same plumbing. It needs a way to read files, query a database, call an API, and ask permission first. Without a standard, you write that glue code again for every app and every data source. The Model Context Protocol (MCP) exists to end that.

This guide explains what MCP is, how its pieces fit together, and what actually travels over the wire. It is built against real runs of the official Python SDK and a tiny server written from scratch to show the raw messages.

What MCP Is

MCP is an open protocol for connecting LLM applications to external tools and data. The spec describes it as a standard way to share context with models, expose tools to AI systems, and build composable integrations. It takes inspiration from the Language Server Protocol, which standardized how editors talk to language tooling.

Think of it as a USB port for AI apps. Build one MCP server for your database, and any MCP-capable app can use it. Why does every chat app have its own plugin system? MCP is the attempt to stop that. For the agent-side picture — how tools get called inside an agent loop — see the from-scratch agent guide in this series; MCP is the standardization layer under those calls.

The Three Roles

The spec names three participants:

  • Host: The LLM application that starts connections, like a chat app or an IDE.
  • Client: A connector inside the host that talks to one server.
  • Server: A service that provides context and capabilities.

Messages use JSON-RPC 2.0. A server can offer three kinds of features:

  • Tools: Functions the model can execute.
  • Resources: Context and data for the user or the model.
  • Prompts: Templated messages and workflows for users.

Tools are model-controlled, meaning the model decides when to call them. The spec says an app should still keep a human in the loop who can deny any invocation. The MCP fundamentals post covers the earlier conceptual groundwork; this guide tracks the current spec.

A Spec That Just Changed

Here's something older tutorials won't tell you. The latest specification revision, dated 2026-07-28, changed the protocol's core design.

Earlier revisions (2025-11-25 and before) opened every connection with an initialize handshake and kept a session. The new revision is stateless. Every request carries its own protocol version and client capabilities in a _meta field, and the server treats each request independently. A new server/discover method lets a client ask a server what versions and capabilities it supports.

This matters in practice:

  • Old servers still exist. A client that wants to talk to both eras probes with server/discover first. If the server answers with anything other than a recognized modern error, the client falls back to the old handshake.
  • Old tutorials show old messages. If a guide shows an initialize call, it describes the legacy era.
  • SDKs changed too. The current Python SDK docs describe a v2 with MCPServer and Client, and they tell v1 users to pin mcp>=1.28,<2.

The modern shape appears below, with flags where it differs from older guides.

Build a Server in Ten Lines

The official Python SDK makes a server tiny. This is the quickstart from the SDK docs, plus a run line:

from mcp.server import MCPServer

mcp = MCPServer("Demo")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

The type hints become the tool's input schema, and the docstring becomes its description. That's the whole trick: the model reads your docstring to decide when to use the tool, so write it for the model.

A run on SDK version 2.2.0 starts a server on 127.0.0.1:8000, with the endpoint answering at /mcp. Note that the mcp command-line tool needs the optional cli extra, so install mcp[cli] if you want it.

Call It From a Client

The SDK docs show a client that connects over HTTP:

import asyncio
from mcp import Client

async def main() -> None:
    async with Client("http://localhost:8000/mcp") as client:
        tools = await client.list_tools()
        print("tools:", [t.name for t in tools.tools])
        result = await client.call_tool("add", {"a": 1, "b": 2})
        print("add ->", result.structured_content)

asyncio.run(main())

A real run printed tools: ['add'] and add -> {'result': 3} — the server worked end to end. The list_tools call is worth keeping in your own clients: it's how you confirm what a server actually exposes before you trust it.

What Travels on the Wire

SDKs hide the messages, and you should still see them once. A small stdio server built from the spec pages — no SDK — driven by a client script, makes the protocol concrete. It's a teaching sketch, not an official conformance test, so build real servers on an SDK.

Over stdio, the client launches the server as a subprocess. Each message is one JSON line, with no embedded newlines. The server may log to stderr, but it must never write anything to stdout except valid messages.

Every modern request carries the required _meta fields. Here's the discovery exchange, as a real run printed it:

discover  -> result: {"supportedVersions": ["2026-07-28"],
                      "capabilities": {"tools": {}},
                      "instructions": "Arithmetic helpers.",
                      "resultType": "complete"}

Notice resultType. The spec requires it on every result, and "complete" means the request finished. Clients must treat a missing resultType as "complete" for older servers.

A successful tool call looks like this:

call add(2,3) -> result: {"content": [{"type": "text", "text": "5"}],
                          "structuredContent": {"result": 5},
                          "isError": false, "resultType": "complete"}

The content array is for the model, and structuredContent is for programs.

Two Kinds of Errors

Tools report problems in two different ways, and the difference teaches a good design habit. A real run triggers both.

A tool execution error means the call reached the tool and the tool failed. It comes back as a normal result with isError: true:

call divide(1,0) -> result: {"content": [{"type": "text",
    "text": "Cannot divide by zero. Pass a non-zero b."}], "isError": true, ...}

A protocol error means the request itself was wrong, like an unknown tool name or a missing field. It comes back as a JSON-RPC error:

call unknown tool -> error: {"code": -32602, "message": "Unknown tool: nope"}
no _meta at all   -> error: {"code": -32602, "message": "Invalid params: missing required _meta fields"}
bad version       -> error: {"code": -32022, "message": "Unsupported protocol version",
                             "data": {"supported": ["2026-07-28"], "requested": "1900-01-01"}}

Why split them? The spec says clients should feed tool execution errors back to the model so it can correct itself and retry. Protocol errors rarely help a model, so clients may skip them. Write your error text like advice to the model, as in "Pass a non-zero b."

Choose a Transport

The spec defines two standard transports:

  • stdio: The client launches your server as a local subprocess. It's simple, and it suits local tools.
  • Streamable HTTP: Each message is an HTTP POST to a single endpoint, and replies arrive as JSON or a streamed response. It suits remote servers.

Authorization applies to HTTP transports. The spec says stdio servers should not follow it and should read credentials from the environment instead.

Start with stdio for anything personal and local, and switch to HTTP only when other people or machines need the server.

Security Comes First

MCP gives models real power, so the spec spends real words on safety. The key principles:

  • User consent: Hosts must get explicit consent before invoking any tool or exposing user data to a server.
  • Tools are code execution: Treat tool descriptions and annotations as untrusted unless they come from a trusted server.
  • Server duties: Validate all inputs, enforce access controls, rate limit calls, and sanitize outputs.
  • Client duties: Show tool inputs to the user before calling, validate results before they reach the model, set timeouts, and log usage.

A malicious or compromised server can put instructions inside its tool descriptions or results, hoping the model obeys them. That's a widely discussed risk with any tool-using LLM, and it's the reason those client duties exist. Install servers you trust, and review what each one can do.

Design Tips That Hold Up

  • Write tools for the model. Clear names, specific docstrings, and typed arguments.
  • Keep tools small. One job per tool beats one giant multi-purpose tool.
  • Return actionable errors. Tell the model how to fix the call.
  • Avoid hidden state. The new spec says to pass explicit handles, like a basket ID, instead of relying on the connection.
  • Version-check your clients. Probe with server/discover if you must support old servers.
  • Never print to stdout in a stdio server. A stray print corrupts the stream.

Common Pitfalls

  • Mixing spec eras. Match your examples to the protocol version your server speaks.
  • Skipping consent UI. A tool call without a human check is a liability.
  • Trusting tool descriptions. They come from the server, not from you.
  • Logging to stdout. Use stderr.
  • Returning huge payloads. Models choke on giant tool results.
CoverBookDescriptionGet it
Cover of “AI Agents in Action” AI Agents in Action - hands-on coverage of tool-using agents that sit on top of protocols like MCP. View on Amazon
Cover of “Designing Agentic AI Systems” Designing Agentic AI Systems - architecture decisions for tool boundaries, consent, and integration design. View on Amazon
Cover of “AI Engineering” AI Engineering - production context for building and operating AI integrations at scale. View on Amazon

Unlock AI That Actually Works

Get lifetime access to GPT-6 Astra, Claude Fable 5.1, Gemini 3.5, Grok 4.5, and more — all in one platform. Build websites, apps, videos, content, and digital products from a single command. No monthly fees. No tool-hopping.

Click here to get GPTAstra Max now — one-time payment, lifetime access.

Frequently Asked Questions

What is MCP in one paragraph?

The Model Context Protocol is an open, JSON-RPC 2.0-based protocol that connects LLM applications (hosts) to external tools and data (servers) through clients. A server can expose tools the model can call, resources with context data, and prompt templates. Build one MCP server for your database, and any MCP-capable app can use it — the USB-port idea, applied to AI apps.

What changed in the 2026-07-28 spec revision?

The protocol went stateless. Earlier revisions opened every connection with an initialize handshake and kept a session; the new revision has every request carry its own protocol version and client capabilities in a _meta field, and servers treat each request independently. A new server/discover method lets clients ask what versions and capabilities a server supports. Old servers still exist, so clients probe with server/discover and fall back to the legacy handshake when needed.

stdio or streamable HTTP — which transport should I use?

stdio for personal, local tools: the client launches your server as a subprocess, and it's the simplest setup. Streamable HTTP for remote servers: each message is an HTTP POST to a single endpoint. Authorization applies to HTTP transports; stdio servers should read credentials from the environment instead.

Why are there two kinds of MCP errors?

A tool execution error means the call reached the tool and the tool failed — it comes back as a normal result with isError: true, and clients should feed it back to the model so it can correct itself and retry. A protocol error means the request itself was wrong (unknown tool, missing _meta, bad version) — it comes back as a JSON-RPC error, and clients may skip feeding it to the model. Write tool error text like advice: "Pass a non-zero b."

Wrapping This Up

MCP gives AI apps one standard way to find and call tools and data: hosts connect through clients to servers, servers expose tools, resources, and prompts, and JSON-RPC carries every message. Start with a ten-line SDK server, look at the raw messages once, and treat security as part of the design.

Build the add server this week, then add one real tool of your own, like a file search or a database lookup. Write its docstring for a model that has never seen your code — the same discipline every agent framework in this series demands of its tools.

What are You Looking For?

esc