Model Context Protocol (MCP)

An open standard for plugging tools, data and prompts into any AI application through MCP servers.

What is it?

The Model Context Protocol (MCP) is an open protocol that standardizes how AI applications connect to external tools and data. Instead of every app writing custom integration code for every service (GitHub, a database, your file system, Slack...), a service is wrapped once as an MCP server, and any MCP-compatible application can use it. It is often compared to USB-C: one standard plug instead of a drawer full of adapters.

Without MCP, connecting M applications to N services needs up to M x N custom integrations. With MCP you need M clients and N servers.

MCP has three roles:

  • Host: the AI application the user works in - a desktop chat app, an IDE, a coding agent, or your own agent program. The host owns the model and decides what the model may do.
  • Client: a connector inside the host that maintains a connection to one server. A host with three servers runs three clients.
  • Server: a program that exposes capabilities - for example a 'notes' server, a 'postgres' server, a 'github' server. It can run locally as a subprocess or remotely over the network.

Servers offer three main kinds of capabilities (called primitives):

  • Tools: functions the model can call, with a name, description and JSON Schema - exactly like the tools in Tool Use. Model-controlled: the model decides when to call them.
  • Resources: read-only data identified by a URI (a file, a database schema, a document), which the host application can attach to the context. Application-controlled.
  • Prompts: reusable prompt templates with arguments (for example 'summarize-pr' with a PR number) that a user can pick, often shown as slash commands. User-controlled.

Clients can also offer capabilities to servers, such as sampling (a server asks the host's model to generate text, with the host staying in control) and roots (the host tells the server which folders it may work in).

Transports define how messages travel. Messages are JSON-RPC 2.0 - a simple standard where each request has a method, params and an id, and each response carries the matching id. The two standard transports are:

  • stdio: the host launches the server as a local subprocess and they exchange messages over standard input and output. Simple and private; ideal for local tools.
  • Streamable HTTP: the server runs as a web service; the client sends HTTP POST requests and the server can stream responses. Used for remote, shared or hosted servers, typically with authentication (OAuth).

A session starts with an initialize handshake where both sides declare their protocol version and capabilities. Then the client can call tools/list, tools/call, resources/list, resources/read, prompts/list and prompts/get.

Security matters. An MCP server runs code with whatever permissions you give it, and its tool descriptions and outputs go straight into the model's context. Only install servers you trust, review what tools they expose, prefer read-only or narrowly-scoped servers, keep approval prompts on for actions with side effects, and remember that data returned by a server (web pages, issues, emails) can contain prompt-injection attempts.

Explain like I'm 10

MCP is like the standard electrical socket. Appliance makers (server authors) build one plug; house builders (host apps) install standard sockets; anything plugs into anything. The host is the house, each client is a socket, each server is an appliance - and you still should not plug in a device from a stranger without checking it first.

Examples

A complete MCP server in Python with FastMCP

# notes_server.py     pip install mcp
import sys
from pathlib import Path
from mcp.server.fastmcp import FastMCP

NOTES = Path.home() / "mcp-notes.txt"
mcp = FastMCP("notes")

@mcp.tool()
def add_note(text: str) -> str:
    """Save a short note for the user. Use when they ask you to remember or jot something down."""
    with NOTES.open("a", encoding="utf-8") as f:
        f.write(text.strip() + "\n")
    print(f"saved note ({len(text)} chars)", file=sys.stderr)   # log to stderr, never stdout
    return "saved"

@mcp.tool()
def search_notes(query: str) -> str:
    """Return saved notes that contain the query (case-insensitive), newest last."""
    if not NOTES.exists():
        return "no notes yet"
    hits = [line for line in NOTES.read_text(encoding="utf-8").splitlines()
            if query.lower() in line.lower()]
    return "\n".join(hits) or "no matching notes"

@mcp.resource("notes://all")
def all_notes() -> str:
    """Every saved note, as plain text."""
    return NOTES.read_text(encoding="utf-8") if NOTES.exists() else ""

@mcp.prompt()
def weekly_review() -> str:
    """Review this week's notes."""
    return "Read my notes (resource notes://all) and group them into: done, in progress, ideas."

if __name__ == "__main__":
    mcp.run()          # stdio transport by default
    # mcp.run(transport="streamable-http")  # to serve over HTTP instead

FastMCP builds each tool's JSON Schema from type hints and its description from the docstring, just like the SDK tool runner. With the stdio transport, stdout carries protocol messages, so any print() to stdout would corrupt the stream - log to stderr.

Registering the server in a desktop host's config

{
  "mcpServers": {
    "notes": {
      "command": "python",
      "args": ["/absolute/path/to/notes_server.py"]
    }
  }
}

Hosts that launch local stdio servers take a command and arguments; this is the shape used by Claude Desktop's claude_desktop_config.json, and other hosts use similar entries. Use absolute paths: the host may start the server from a different working directory.

Your own agent as an MCP host: list tools and call them

# pip install mcp anthropic
import asyncio
import anthropic
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

client = anthropic.Anthropic()
server = StdioServerParameters(command="python", args=["notes_server.py"])

async def main():
    async with stdio_client(server) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()                       # handshake
            listed = await session.list_tools()              # tools/list
            tools = [{"name": t.name, "description": t.description or "",
                      "input_schema": t.inputSchema} for t in listed.tools]

            messages = [{"role": "user", "content": "Note that the demo is on Friday, then show my notes."}]
            for _ in range(8):                                # max-steps guard
                resp = client.messages.create(model="claude-opus-5-5", max_tokens=16000,
                                              tools=tools, messages=messages)
                messages.append({"role": "assistant", "content": resp.content})
                if resp.stop_reason != "tool_use":
                    break
                results = []
                for block in resp.content:
                    if block.type == "tool_use":
                        out = await session.call_tool(block.name, block.input)   # tools/call
                        text = "\n".join(c.text for c in out.content if c.type == "text")
                        results.append({"type": "tool_result", "tool_use_id": block.id,
                                        "content": text, "is_error": bool(out.isError)})
                messages.append({"role": "user", "content": results})
            print("".join(b.text for b in resp.content if b.type == "text"))

asyncio.run(main())

MCP tool definitions map one-to-one onto API tool definitions (name, description, inputSchema). The agent loop is the same one you already know - only run_tool is replaced by session.call_tool.

What travels over the wire: JSON-RPC messages (runnable simulation)

// A toy in-memory MCP-like server, to show the message shapes.
const server = {
  tools: [{ name: "add_note", description: "Save a note.",
            inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] } }],
  notes: [],
  handle(msg) {
    if (msg.method === "initialize")
      return { jsonrpc: "2.0", id: msg.id, result: { serverInfo: { name: "notes" }, capabilities: { tools: {} } } };
    if (msg.method === "tools/list")
      return { jsonrpc: "2.0", id: msg.id, result: { tools: this.tools } };
    if (msg.method === "tools/call" && msg.params.name === "add_note") {
      this.notes.push(msg.params.arguments.text);
      return { jsonrpc: "2.0", id: msg.id, result: { content: [{ type: "text", text: "saved" }], isError: false } };
    }
    return { jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "Method not found" } };
  },
};

let nextId = 1;
function send(method, params) {
  const req = { jsonrpc: "2.0", id: nextId++, method, params };
  console.log("client ->", JSON.stringify(req));
  const res = server.handle(req);
  console.log("server <-", JSON.stringify(res));
  return res;
}

send("initialize", { clientInfo: { name: "my-agent" } });
send("tools/list", {});
send("tools/call", { name: "add_note", arguments: { text: "demo on Friday" } });
send("tools/delete_everything", {});
console.log("server state:", server.notes);

Simplified (the real initialize also exchanges protocol versions, and transports add framing), but the essence is right: JSON-RPC requests with method, params and id; responses with the same id and either result or error. -32601 is JSON-RPC's standard 'method not found' code.

How it works

When the host starts, it launches or connects to each configured server and performs the initialize handshake. It then lists each server's tools (and resources and prompts) and merges the tool definitions into what it sends the model. When the model emits a tool_use for an MCP tool, the host routes it to the right client, which sends tools/call to its server and returns the result as a tool_result. To the model, MCP tools look like any other tools.

Servers can notify clients when their lists change (for example new tools appear), and long operations can report progress. Remote servers over Streamable HTTP typically use OAuth so that each user grants access with their own identity.

Because every connected server's tool definitions are added to the context, connecting dozens of servers costs tokens on every call and makes tool selection harder. Connect what the task needs.

  +------------------ HOST (AI app) -------------------+
  |   model  <-->  agent loop                          |
  |                  |        |          |             |
  |              [client]  [client]   [client]         |
  +-----------------|---------|----------|-------------+
                  stdio     stdio   Streamable HTTP
                    |         |          |
               [notes     [files     [remote API
                server]   server]    server]
     each server offers: tools | resources | prompts
     messages: JSON-RPC 2.0  (initialize, tools/list,
               tools/call, resources/read, prompts/get)

Why does it exist?

Every AI application needed the same integrations, and every integration was rewritten for every app with slightly different tool formats. MCP separates who builds the integration from who uses it: write a server once and it works in many hosts; build a host once and it gains access to an ecosystem of servers.

When to use it

Use MCP when you want your tools usable from several AI applications (desktop chat, IDE, your own agent), when you want to reuse existing community or vendor servers instead of writing integrations, or when you want a clean process boundary between the agent and the systems it touches.

When not to use it

If you have one agent with three small in-process tools, plain tool definitions in your code are simpler and faster - MCP adds a process, a protocol and a dependency. Do not connect untrusted or unreviewed servers to an agent that has access to sensitive data or powerful actions.

Common mistakes

  • Printing debug output to stdout in a stdio server, corrupting the JSON-RPC stream.

  • Using relative paths in host configs, so the server fails to start from the host's working directory.

  • Installing third-party servers without reading what tools they expose and what permissions they need.

  • Connecting many servers at once, flooding the context with tool definitions and confusing tool choice.

  • Writing vague tool docstrings - with FastMCP the docstring is the description the model reads.

  • Treating server output as trusted; text from issues, pages or emails can carry prompt injection.

  • Confusing tools with resources: tools are model-invoked actions, resources are data the application attaches.

Practice exercises

  1. Easy:

    Explain host, client and server using an IDE with two MCP servers as the example. How many clients are there?

  2. Easy:

    Extend the runnable JSON-RPC simulation with a resources/read method for notes://all that returns all notes.

  3. Medium:

    Run the FastMCP notes server and connect it to a desktop host or to the Python client example. Save and search notes through the model.

  4. Medium:

    Add a delete_note(index: int) tool to the server. In your client, require human approval before forwarding it.

  5. Hard:

    Wrap the read-only tools of mini_agent.py (list_files, read_file, search_notes) as an MCP server with a configurable workspace root, then use it from the client example. Keep the path guard.

Interview questions

What problem does MCP solve?

It standardizes how AI applications discover and use external tools, data and prompts, turning an M x N integration problem into M + N: each service is wrapped once as a server, each app implements the client side once.

Explain host, client and server.

The host is the AI application that owns the model and user interaction. Inside it, each client holds a connection to exactly one server. Servers expose tools, resources and prompts, running locally over stdio or remotely over HTTP.

Tools vs resources vs prompts?

Tools are model-controlled functions with side effects or computation. Resources are application-controlled read-only data addressed by URI. Prompts are user-controlled templates, often surfaced as slash commands.

What transports does MCP use?

Messages are JSON-RPC 2.0. stdio runs the server as a local subprocess communicating over stdin/stdout; Streamable HTTP runs it as a web service for remote use, usually with OAuth for authorization.

What are the security risks of MCP servers?

Servers execute code with your permissions, their descriptions and outputs enter the model context (enabling prompt injection or misleading tool descriptions), and broad servers expand what a hijacked agent could do. Mitigate with trusted sources, least privilege, approvals for side effects and isolation.

How does an agent use MCP tools in its loop?

It lists tools from each session, converts them to API tool definitions, and when the model returns a tool_use for one, calls session.call_tool with the name and input, then returns the content as a tool_result (marking is_error when the server reports an error).