Skip to content

1. What MCP is

Beginner · 11 min read

You already know tool calling: you describe a function as JSON, the model asks to call it, your code runs it. That works well inside one app. MCP is for the moment you want the same tools in many apps (Claude, Cursor, VS Code, your LangGraph agent, a teammate's chatbot) without rewriting the integration for each one.

1.1 The problem: N apps × M tools

Without a standard, every AI app writes its own connector for every system:

apps = ["Claude Desktop", "VS Code", "Cursor", "Your support bot", "Your LangGraph agent"]
systems = ["GitHub", "Postgres", "Jira", "Google Drive", "Orders API", "Slack"]

print(f"Custom connectors without a standard: {len(apps)} x {len(systems)} = {len(apps) * len(systems)}")
print(f"With MCP: {len(apps)} clients + {len(systems)} servers = {len(apps) + len(systems)}")
Output
Custom connectors without a standard: 5 x 6 = 30
With MCP: 5 clients + 6 servers = 11

That's the same trick USB and the Language Server Protocol used: agree on one plug, and each side builds its half once. The Model Context Protocol is an open standard, started by Anthropic in late 2024 and now supported by every major model provider, IDE and agent framework.

1.2 Host, client, server

flowchart LR
    subgraph Host [Host app: Claude, VS Code, your agent]
        LLM[LLM] --- C1[MCP client]
        LLM --- C2[MCP client]
    end
    C1 <-->|stdio| S1[Orders server<br/>local process]
    C2 <-->|Streamable HTTP| S2[Docs server<br/>remote service]
    S1 --- DB[(Orders DB)]
    S2 --- KB[(Knowledge base)]
Piece What it is Example
Host The AI application the user talks to. Owns the LLM, the UI and the security decisions Claude Desktop, Claude Code, Cursor, your FastAPI agent
Client A connector inside the host. One client per server mcp.Client(...) in Python
Server A program that exposes capabilities over MCP A GitHub server, a Postgres server, your orders_server.py

The server never talks to the LLM directly. The host decides what to show the model, which tool calls to allow and what to send back.

1.3 What a server exposes

Primitive Who decides to use it What it is Example
Tools The model Actions with a JSON-schema input (and optionally output) get_order_status(order_id), create_issue(title, body)
Resources The app (or user) Read-only data addressed by a URI policy://refunds, orders://A100, file:///repo/README.md
Prompts The user Reusable prompt templates with arguments /apology_email customer=Asha issue="late delivery"

That "who decides" column is the one interviewers like. Tools are model-controlled: the LLM picks them during the loop. Resources are application-controlled: the host attaches them as context (for example when a user picks a file). Prompts are user-controlled: they usually appear as slash commands.

Servers can also ask the client for things during a call. Elicitation asks the user a question ("Really cancel order A100?") and is the one you'll use most. Sampling (borrow the host's LLM) and roots (which folders the server may touch) still work but are deprecated in the latest spec (see 1.5).

1.4 The wire format: JSON-RPC 2.0

Every MCP message is a JSON-RPC 2.0 request, response or notification. Here's what a client sends to list tools and call one, and what comes back:

import json

list_tools = {"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}

call_tool = {
    "jsonrpc": "2.0", "id": 2, "method": "tools/call",
    "params": {"name": "get_order_status", "arguments": {"order_id": "A100"}},
}

tool_result = {
    "jsonrpc": "2.0", "id": 2,
    "result": {
        "content": [{"type": "text", "text": '{"status": "shipped", "eta": "Friday"}'}],
        "structuredContent": {"status": "shipped", "eta": "Friday"},
        "isError": False,
        "resultType": "complete",
    },
}

for msg in (list_tools, call_tool, tool_result):
    print(json.dumps(msg))
Output
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "get_order_status", "arguments": {"order_id": "A100"}}}
{"jsonrpc": "2.0", "id": 2, "result": {"content": [{"type": "text", "text": "{\"status\": \"shipped\", \"eta\": \"Friday\"}"}], "structuredContent": {"status": "shipped", "eta": "Friday"}, "isError": false, "resultType": "complete"}}

Three things to notice:

  • The id ties a response to its request, so several calls can be in flight at once.
  • A tool result carries content (text, images, audio, links for the model to read) and, optionally, structuredContent (typed JSON your code can use directly).
  • isError: true means "the tool ran and failed", and the model should read the message and try something else. A JSON-RPC error object means "the request itself was wrong" (unknown method, bad params).

1.5 Protocol versions: handshake era vs stateless era

MCP versions are dates. The one change you must know about is the 2026-07-28 revision:

2024-11-05 → 2025-11-25 ("handshake era") 2026-07-28 ("stateless")
Starting a connection initialize request + notifications/initialized, then a long-lived session No handshake. Optional server/discover to see versions and capabilities
Where version and capabilities go Agreed once in initialize Sent in every request's _meta
HTTP sessions Mcp-Session-Id header, sticky sessions Removed: any server replica can answer any request
Server asking the client (elicitation) Server sends its own request mid-call Multi round-trip: server returns resultType: "input_required", client retries with the answer
Change notifications resources/subscribe, HTTP GET stream subscriptions/listen
Sampling, roots, logging Active Deprecated (still work for at least 12 months)

Here's the same tools/call in both eras:

handshake_era = [
    {"jsonrpc": "2.0", "id": 0, "method": "initialize",
     "params": {"protocolVersion": "2025-11-25", "capabilities": {"elicitation": {}},
                "clientInfo": {"name": "my-agent", "version": "1.0"}}},
    {"jsonrpc": "2.0", "method": "notifications/initialized"},
    {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
     "params": {"name": "get_order_status", "arguments": {"order_id": "A100"}}},
]

stateless_era = [
    {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
     "params": {"name": "get_order_status", "arguments": {"order_id": "A100"},
                "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
                          "io.modelcontextprotocol/clientCapabilities": {"elicitation": {}},
                          "io.modelcontextprotocol/clientInfo": {"name": "my-agent", "version": "1.0"}}}},
]

print(f"Handshake era: {len(handshake_era)} messages, methods {[m['method'] for m in handshake_era]}")
print(f"Stateless era: {len(stateless_era)} message,  methods {[m['method'] for m in stateless_era]}")
Output
Handshake era: 3 messages, methods ['initialize', 'notifications/initialized', 'tools/call']
Stateless era: 1 message,  methods ['tools/call']

Why the change? Remote MCP servers run behind load balancers. A session pinned to one machine made them hard to scale and restart. Stateless requests let a server work like any other HTTP API. The official SDKs speak both eras: a modern client probes server/discover and falls back to initialize on older servers, so you'll meet both in real systems.

Versions on these pages

Examples use the 2026-07-28 spec and the official Python SDK mcp 2.x, where the server class is MCPServer (it was called FastMCP in mcp 1.x). Many tutorials still show from mcp.server.fastmcp import FastMCP. That's the same idea with the old name.

1.6 MCP vs plain function calling

MCP doesn't replace tool calling. The model still sees a tool name, a description and a JSON schema. MCP standardises where those tools come from and how they're run.

Plain function calling MCP
Where tools live Inside your app's code In a separate server process or service
Reuse across apps Copy the code Any MCP host can connect
Discovery Hard-coded list tools/list at runtime
Beyond tools No Resources, prompts, elicitation
Extra cost None A process or network hop, one more thing to secure

Use plain tools when the tools belong to one app and nobody else needs them. Use MCP when tools are shared across apps or teams, when you want users to plug your system into Claude, Cursor or ChatGPT, or when a ready-made MCP server already exists for the system (GitHub, Postgres, Slack, Sentry and hundreds more).

Interview questions

What is MCP, in one sentence?

An open protocol that standardises how AI applications connect to external tools and data: servers expose tools, resources and prompts over JSON-RPC, and any MCP-capable host can use them, turning N×M custom integrations into N+M.

Explain host, client and server.

The host is the AI app the user uses; it owns the LLM and the security decisions. Inside it, one MCP client per connection talks to one MCP server. The server wraps a system (a database, an API, files) and exposes it as tools, resources and prompts. The server never talks to the model directly.

Tools vs resources vs prompts?

Tools are model-controlled actions with a JSON schema; the LLM decides to call them. Resources are application-controlled, read-only data addressed by URIs that the host attaches as context. Prompts are user-controlled templates, usually shown as slash commands.

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

MCP became stateless: no initialize handshake and no HTTP sessions. Every request carries its protocol version and client capabilities in _meta, and servers must implement server/discover. Server-to-client requests such as elicitation became multi round-trip (the server returns input_required, the client retries with the answer). Sampling, roots and logging were deprecated. The goal was to let remote servers scale like ordinary HTTP APIs.

When would you not use MCP?

When the tools are used by a single app and nobody else needs them. A plain function-calling loop is simpler, faster and has less to secure. MCP pays off when tools are shared across apps or teams, or when you want external hosts to connect to your system.

Practice

  • Add a seventh system to 1.1 and work out how many connectors each approach needs.
  • Write the JSON-RPC message for resources/read with {"uri": "policy://refunds"} in the stateless format.
  • Pick three systems you use at work and decide for each: plain tools or MCP server? Why?

Next: 2. The protocol from scratch Back to: MCP overview · Notes overview