Chapter 7: MCP Fundamentals: The Model Context Protocol
Learning Objectives
Explain what MCP standardizes and why it exists, including the N-by-M integration problem it was designed to solve and how it relates to the function calling from Chapter 5.
Distinguish the MCP client and server roles — along with the host that coordinates them — and articulate the responsibilities and security boundaries each owns.
Describe the primitives MCP exposes — tools, resources, and prompts — and reason about the control model that governs when each one is used.
Trace the JSON-RPC message lifecycle from initialization through discovery, retrieval, invocation, and dynamic list updates.
Pre-Reading Check — Why MCP Exists
1. Connecting N AI applications to M data sources with bespoke point-to-point connectors requires roughly how many integrations, and what does MCP reduce that to?
N+M point-to-point; MCP raises it to N×MN×M point-to-point; MCP reduces it to N+MN×M point-to-point; MCP reduces it to a constant 1N+M point-to-point; MCP keeps it at N+M
2. The "USB-C for AI" analogy is most useful for conveying which property of MCP?
That MCP transmits data faster than older protocolsThat MCP requires a proprietary cable from each vendorThat one universal interface lets any compliant app plug into any compliant toolThat MCP only works on machines with USB-C ports
3. How does MCP relate to the function calling covered in Chapter 5?
MCP replaces function calling entirely; the two cannot coexistThey are the same mechanism under two different namesFunction calling is model-facing (choosing a capability); MCP is integration-facing (discovering and reaching capabilities)Function calling is the wire protocol; MCP is the model's decision layer
Why MCP Exists
Building a hyper-personalized assistant means connecting a language model to the messy, heterogeneous world where a user's data actually lives: their calendar, email, a CRM, a document store, an internal database, a payments API. Before MCP, every one of those connections was a bespoke engineering project. Anthropic introduced the Model Context Protocol as an open standard on November 25, 2024, describing it as an open standard for "secure, two-way connections between data sources and AI-powered tools," motivated by the fact that even capable models were "trapped behind information silos and legacy systems."
Key Points
The N×M integration problem: wiring N apps to M data sources directly needs ~N×M custom connectors, and complexity explodes as either side grows.
MCP collapses N×M into N+M: each app implements the protocol once (as client/host), each tool once (as a server), and any compliant pair can talk.
MCP is often called "USB-C for AI" — one universal interface instead of a purpose-built adapter per pairing.
Because it is open and adopted by providers including OpenAI and Google DeepMind, a server built once is reusable across many assistants.
MCP complements function calling: function calling is model-facing; MCP is the integration-facing standard for discovery, negotiation, and invocation.
The N×M Integration Problem
Imagine N AI applications (chat assistants, IDEs, agents) and M tools or data sources (Slack, Google Drive, GitHub, a Postgres database). Wire them directly and each application needs a custom connector for each data source — roughly N×M bespoke integrations. Add one new tool and you build N new connectors; add one new application and you build M new connectors. With MCP, each side implements the protocol once and any compliant client can talk to any compliant server.
Suppose you have 4 assistants and 5 data sources:
Approach
Connectors required
Formula
Cost of adding one new tool
Point-to-point (no standard)
4 × 5 = 20
N × M
Build 4 new connectors (one per app)
MCP (shared protocol)
4 + 5 = 9
N + M
Build 1 server; every app already speaks MCP
Figure 7.1: Point-to-point integrations (N×M) versus a shared MCP protocol (N+M).
graph TD
subgraph P2P["Point-to-Point: N x M bespoke connectors"]
A1[App 1]
A2[App 2]
D1[Slack]
D2[Drive]
D3[GitHub]
A1 --> D1
A1 --> D2
A1 --> D3
A2 --> D1
A2 --> D2
A2 --> D3
end
subgraph MCP["MCP: each side implements protocol once, N + M"]
B1[App 1]
B2[App 2]
HUB{{MCP Protocol}}
S1[Slack Server]
S2[Drive Server]
S3[GitHub Server]
B1 --> HUB
B2 --> HUB
HUB --> S1
HUB --> S2
HUB --> S3
end
[Animation slot: the "add one new tool" moment — watch 4 new arrows sprout in the point-to-point graph while only a single server node appears in the MCP graph.]
MCP as a Universal Connector
Before USB-C, every device had its own proprietary cable. USB-C replaced that with a single port any peripheral could use. MCP does the same for software: one universal interface so any compliant model or app can connect to any compliant data source or service. A concrete benefit falls straight out: the ecosystem becomes shareable. The project ships not just a specification but language SDKs, tooling such as the MCP Inspector, and pre-built reference servers for Google Drive, Slack, and GitHub. Crucially the standard did not stay proprietary — cross-vendor adoption is exactly what makes the USB-C analogy hold, because a connector standard is only useful if everyone plugs into it.
Relationship to Function Calling
Function calling is the model-facing mechanism: presented with function definitions, the LLM decides which to call and produces a structured argument object — a capability of the model and its API. MCP is the integration-facing standard: how an application discovers what an external system offers, negotiates capabilities, and invokes them over a common wire format. MCP "focuses solely on the protocol for context exchange — it does not dictate how AI applications use their LLMs."
Concern
Function Calling (Ch. 5)
MCP (Ch. 7)
Layer
Model ↔ application
Application ↔ external systems
Question it answers
"Which capability should the model invoke, with what arguments?"
"How does my app discover, negotiate, and invoke capabilities from any external system?"
Owned by
The LLM and its API
An open, cross-vendor protocol
Analogy
The decision to plug something in
The universal socket you plug into
In practice, an MCP server exposes tools; the model, through function calling, decides to invoke one; and MCP carries the invocation to the server and the result back. Function calling is how the model reaches; MCP standardizes what it reaches into.
Post-Reading Check — Why MCP Exists
1. Connecting N AI applications to M data sources with bespoke point-to-point connectors requires roughly how many integrations, and what does MCP reduce that to?
N+M point-to-point; MCP raises it to N×MN×M point-to-point; MCP reduces it to N+MN×M point-to-point; MCP reduces it to a constant 1N+M point-to-point; MCP keeps it at N+M
2. The "USB-C for AI" analogy is most useful for conveying which property of MCP?
That MCP transmits data faster than older protocolsThat MCP requires a proprietary cable from each vendorThat one universal interface lets any compliant app plug into any compliant toolThat MCP only works on machines with USB-C ports
3. How does MCP relate to the function calling covered in Chapter 5?
MCP replaces function calling entirely; the two cannot coexistThey are the same mechanism under two different namesFunction calling is model-facing (choosing a capability); MCP is integration-facing (discovering and reaching capabilities)Function calling is the wire protocol; MCP is the model's decision layer
Pre-Reading Check — Client and Server Roles
4. In MCP's host-client-server architecture, what is the relationship between clients and servers?
One client multiplexes connections to many servers at onceEach client maintains a dedicated 1:1 connection to exactly one serverServers connect directly to each other, bypassing clientsThe host connects directly to servers without any client
5. Which participant owns security policy and access enforcement in MCP?
The server, because it holds the dataThe client, because it routes messagesThe host, which is the security boundary that mediates every interactionThe LLM, because it decides what tools to call
6. What do the three steps of the MCP initialize handshake accomplish?
They transfer the user's credentials to every serverThey negotiate protocol version, discover each side's capabilities, and exchange identityThey execute the first tool call to warm up the connectionThey compress messages so later traffic is smaller
Client and Server Roles
MCP's architecture is deliberately simple, but three words — host, client, server — sound interchangeable and are not. A host application establishes connections to one or more servers, creating one client per server.
Key Points
Host — the user-facing AI application (Claude Desktop, VS Code, Claude Code, a custom assistant). It coordinates the LLM and enforces security policies.
Client — maintains a dedicated, stateful 1:1 connection to exactly one server, handles protocol negotiation and message routing, and keeps servers isolated.
Server — a program that provides context (tools, resources, prompts). Local servers use STDIO transport; remote servers use Streamable HTTP.
Sessions open with a stateful three-step initialize handshake that negotiates version, discovers capabilities, and exchanges identity.
What is declared at initialization fixes what the session can do for its entire lifetime.
Host, Client, and Server
Think of the host as a restaurant's front-of-house manager, each client as a dedicated waiter assigned to exactly one kitchen station, and each server as a specialized station (the grill, the pastry counter). The manager coordinates the experience and decides what the guest may order; each waiter talks to only one station and never lets stations talk directly to each other or to the guest. The one-client-per-server (1:1) rule is a defining structural feature: a host connected to multiple servers runs multiple clients in parallel, which preserves security boundaries.
MCP is defined in two layers: a data layer (the JSON-RPC 2.0 protocol defining message structure, lifecycle, and the core primitives) and a transport layer (STDIO for local processes; Streamable HTTP with optional Server-Sent Events for remote, supporting bearer tokens, API keys, and recommending OAuth). The transport layer abstracts communication so the same JSON-RPC message format works across every transport.
Figure 7.2: The MCP host-client-server architecture with one client bound 1:1 to each server.
graph TD
HOST["MCP Host (Claude Desktop / VS Code) coordinates LLM, enforces security"]
CA[Client A]
CB[Client B]
CC[Client C]
SA["Server A (GitHub)"]
SB["Server B (Filesystem)"]
SC["Server C (Sentry)"]
HOST --> CA
HOST --> CB
HOST --> CC
CA -->|1:1| SA
CB -->|1:1| SB
CC -->|1:1| SC
[Animation slot: highlight one client-server 1:1 pair at a time to show isolation — each waiter serves exactly one kitchen station.]
Capability Negotiation and Initialization
MCP is a stateful protocol. The first thing a client and server do is negotiate the capabilities they both support, through a three-step handshake:
initialize response (server → client) — returns the selected protocolVersion, the server's capabilities (e.g., "tools": {"listChanged": true}, "resources": {}), and serverInfo.
notifications/initialized (client → server) — a notification (no id) acknowledging readiness. The session is now live.
Figure 7.3: The three-step initialize capability-negotiation handshake.
sequenceDiagram
participant C as MCP Client
participant S as MCP Server
C->>S: initialize request (protocolVersion, capabilities, clientInfo)
S-->>C: initialize response (selected version, capabilities, serverInfo)
C->>S: notifications/initialized (no id, no response)
Note over C,S: Session live; declared capabilities fix what the session can do
Reading a real exchange top to bottom: both sides agree on version 2025-06-18; the server advertises tools (and will notify on tool-list changes) and resources, but says nothing about prompts — so the client knows not to attempt prompt operations. The initialization exchange serves three critical purposes:
Purpose
What it does
Failure mode
Protocol version negotiation
Ensures both sides speak a compatible version
If no compatible version exists, the connection should be terminated
Capability discovery
Each party declares which primitives and features it supports
The session avoids attempting unsupported operations
Identity exchange
clientInfo / serverInfo carry name and version
Used for debugging and compatibility
Who Owns What
Responsibility
Owned by
Notes
User experience and LLM coordination
Host
Drives the conversation and decides how context is used
Security policy and access enforcement
Host
The host is the security boundary; servers do not trust each other
Protocol negotiation and message routing
Client
One client per server; routes messages both directions
Server isolation
Client
Each 1:1 binding keeps servers from interacting
Providing tools, resources, and prompts
Server
Serves context; does not manage the LLM
Because the host is the security boundary, a compromised or misbehaving server cannot reach into another server or silently exfiltrate context — the host mediates every interaction.
Post-Reading Check — Client and Server Roles
4. In MCP's host-client-server architecture, what is the relationship between clients and servers?
One client multiplexes connections to many servers at onceEach client maintains a dedicated 1:1 connection to exactly one serverServers connect directly to each other, bypassing clientsThe host connects directly to servers without any client
5. Which participant owns security policy and access enforcement in MCP?
The server, because it holds the dataThe client, because it routes messagesThe host, which is the security boundary that mediates every interactionThe LLM, because it decides what tools to call
6. What do the three steps of the MCP initialize handshake accomplish?
They transfer the user's credentials to every serverThey negotiate protocol version, discover each side's capabilities, and exchange identityThey execute the first tool call to warm up the connectionThey compress messages so later traffic is smaller
Pre-Reading Check — MCP Primitives
7. What is the single most important way to distinguish MCP's three primitives?
By the transport they use (STDIO vs. HTTP)By their control model — who decides when each gets usedBy how many bytes they consume on the wireBy which programming language the server is written in
8. A read-only database schema exposed by a server, pulled into context by the host, is best modeled as which primitive?
A tool (model-controlled)A prompt (user-controlled)A resource (application-controlled)A sampling request (server-controlled)
9. What does the client-side sampling primitive (sampling/createMessage) let a server do?
Request an LLM completion from the host, staying model-independentSample a random subset of the server's tools to reduce costRead a portion of a large file before loading the whole thingEmbed its own LLM SDK so it never needs the host
MCP Primitives
Anthropic's documentation calls the primitives "the most important concept within MCP." They define what clients and servers can offer each other. MCP defines three core primitives that servers expose: tools, resources, and prompts.
Key Points
Tools are model-controlled executable functions (side effects); the LLM decides when to invoke one via tools/call.
Resources are application-controlled, read-only data the host pulls into context; the server provides content, it does not act.
Prompts are user-controlled, reusable templates a human explicitly triggers (often a slash command).
The unifying idea is the control model: match each capability to the primitive whose "who decides when" fits reality — a verb becomes a tool, a noun a resource, a packaged workflow a prompt.
Clients can expose primitives back to servers: sampling, elicitation, and logging.
Tools, Resources, and Prompts
Tools are "executable functions that AI applications can invoke to perform actions" — a tool takes JSON Schema arguments (inputSchema) and does something. They are model-controlled and, because they have side effects, most demand careful permissioning by the host (e.g., create_calendar_event).
Resources are "data sources that provide contextual information" — read-only content the application pulls in. They are application-controlled. A tool is a verb (do something); a resource is a noun (here is some data), e.g., the contents of the user's latest invoice.
Prompts are "reusable templates that help structure interactions with language models." They are user-controlled — a human explicitly selects one, typically as a slash command — and often act as the "handoff artifact" packaging an expert's workflow.
The Control Model
The three primitives are best understood by who decides when they get used — three distinct control planes:
Primitive
Control model
Who decides invocation
Nature
Typical trigger
Tools
Model-controlled
The LLM decides, based on the conversation
Executable action (side effects)
Model emits a function call
Resources
Application-controlled
The host decides when to pull it into context
Read-only data / context
App injects context
Prompts
User-controlled
The human explicitly selects it
Reusable interaction template
Slash command / menu selection
A concrete illustration ties all three together: a server providing context about a database can expose tools for querying it, a resource containing the schema, and a prompt with few-shot examples. The model calls the tools, the app supplies the schema, and the user invokes the prompt.
[Animation slot: three lanes — Model, Application, User — each lighting up the primitive it controls, converging on one database server.]
Prompts and Sampling — Client-Side Primitives
For completeness, MCP also defines primitives that clients expose so servers can build richer interactions:
Sampling (sampling/createMessage) lets a server request an LLM completion from the host — so server authors can be "AI-powered" without embedding an LLM SDK, staying model-independent.
Elicitation (elicitation/create) lets a server request additional information or confirmation from the user.
Logging lets a server send log messages to the client for debugging and monitoring.
Sampling inverts the usual direction of control: instead of the model reaching out to the server, the server reaches back to borrow the host's model.
Post-Reading Check — MCP Primitives
7. What is the single most important way to distinguish MCP's three primitives?
By the transport they use (STDIO vs. HTTP)By their control model — who decides when each gets usedBy how many bytes they consume on the wireBy which programming language the server is written in
8. A read-only database schema exposed by a server, pulled into context by the host, is best modeled as which primitive?
A tool (model-controlled)A prompt (user-controlled)A resource (application-controlled)A sampling request (server-controlled)
9. What does the client-side sampling primitive (sampling/createMessage) let a server do?
Request an LLM completion from the host, staying model-independentSample a random subset of the server's tools to reduce costRead a portion of a large file before loading the whole thingEmbed its own LLM SDK so it never needs the host
Pre-Reading Check — The Message Lifecycle
10. In JSON-RPC 2.0, what distinguishes a notification from a request?
A notification carries an id; a request does notA notification has no id and expects no response; a request has an id and expects a matching responseA notification always contains an error fieldA notification uses HTTP while a request uses STDIO
11. Only one MCP primitive is executed rather than merely retrieved. Which, and with what method?
Resources, via resources/readPrompts, via prompts/getTools, via tools/callAll three, via a shared */call method
12. Why does capability negotiation happen before a server can push a notifications/tools/list_changed message?
Because only servers that declared listChanged at initialization may send such notificationsBecause notifications require the client to send an id firstBecause tool lists can never change once discoveredBecause the transport must switch from STDIO to HTTP first
The Message Lifecycle
Everything in MCP travels as JSON-RPC 2.0 messages, and every primitive is reached through a small, predictable set of methods.
Key Points
Three message kinds: requests (have id, expect a response), responses (same id, carry result or error), and notifications (no id, expect no response).
Discovery follows the */list convention: tools/list, resources/list, prompts/list.
Retrieval uses resources/read and prompts/get; execution uses tools/call — only tools have a call method because only tools have side effects.
Listings are dynamic: a server that declared listChanged can push notifications/tools/list_changed to prompt a fresh tools/list.
The same message format works identically over STDIO or Streamable HTTP because the transport layer abstracts communication.
JSON-RPC Message Flow
Every JSON-RPC message begins with "jsonrpc": "2.0". The id field pairs a response to its originating request; its absence is precisely what marks a message as a fire-and-forget notification (e.g., notifications/initialized, notifications/tools/list_changed).
Discovery and Listing
After the handshake, a client discovers what a server offers via the */list methods. Crucially, listings are dynamic and can change over time. A server whose tools depend on runtime state (say, which repository is open) can add or remove tools mid-session. This is where the listChanged capability earns its keep: a server that declared "tools": {"listChanged": true} can push a notifications/tools/list_changed message, prompting the client to call tools/list again. Only servers that declared this capability send such notifications — which is exactly why capability negotiation happens first.
Invocation and Results
Operation
Method(s)
Applies to
Discovery
tools/list, resources/list, prompts/list
All three primitives
Retrieval
resources/read, prompts/get
Resources and prompts (read data)
Execution
tools/call
Tools only (perform an action)
Note the asymmetry: only tools are executed via tools/call. Resources and prompts are retrieved — nothing is done in the world. This maps precisely back to the control model: the executable primitive (tools) is the one the model drives, and it is the only one with a call method.
Figure 7.4: The full JSON-RPC message lifecycle for a database assistant, from initialization to dynamic updates.
sequenceDiagram
participant C as Client / Host
participant S as Server
Note over C,S: 1. INITIALIZE
C->>S: initialize (negotiate version + capabilities)
S-->>C: capabilities { tools, resources }
C->>S: notifications/initialized
Note over C,S: 2. DISCOVER
C->>S: tools/list
S-->>C: [ run_query ]
C->>S: resources/list
S-->>C: [ db_schema ]
Note over C,S: 3. RETRIEVE (application-controlled)
C->>S: resources/read (db_schema)
S-->>C: schema text
Note over C,S: 4. INVOKE (model-controlled)
C->>S: tools/call run_query {sql}
S-->>C: result: query rows
Note over C,S: 5. UPDATE (dynamic listing)
S->>C: notifications/tools/list_changed
C->>S: tools/list (refresh)
[Animation slot: step through the five lifecycle phases (Initialize → Discover → Retrieve → Invoke → Update) one arrow at a time.]
Beyond these core methods, MCP defines cross-cutting utilities. Notifications enable the real-time updates just described, and experimental Tasks provide durable execution wrappers for long-running requests that cannot complete in a single quick round trip.
Post-Reading Check — The Message Lifecycle
10. In JSON-RPC 2.0, what distinguishes a notification from a request?
A notification carries an id; a request does notA notification has no id and expects no response; a request has an id and expects a matching responseA notification always contains an error fieldA notification uses HTTP while a request uses STDIO
11. Only one MCP primitive is executed rather than merely retrieved. Which, and with what method?
Resources, via resources/readPrompts, via prompts/getTools, via tools/callAll three, via a shared */call method
12. Why does capability negotiation happen before a server can push a notifications/tools/list_changed message?
Because only servers that declared listChanged at initialization may send such notificationsBecause notifications require the client to send an id firstBecause tool lists can never change once discoveredBecause the transport must switch from STDIO to HTTP first