Model Context Protocol: The USB-C Port for AI Applications
30 min read
Before MCP, every AI agent needed a custom integration for every tool it touched: the M by N problem that does not scale. MCP is the open standard that solves it, defining how AI applications discover tools, read resources and execute actions through a single protocol.
Jonathan MukhobeAI and ML Engineer
Model Context Protocol (MCP)
This is an open-source standard for connecting A.I applications to external systems. Using MCP, A.I applications like Claude or ChatGPT can connect to data sources (e.g. local files, databases), tools (e.g. search engines, calculators) and workflows (e.g. specialized prompts), enabling them to access key information and perform tasks.
Note
Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect electronic devices, MCP provides a standardized way to connect AI applications to external systems.
The Problem That MCP Exists to Solve
Before MCP existed, here is what happened every time you wanted to give an AI agent access to an external tool or system. Say you are building a LangGraph agent and you want it to be able to:
- Read files from Google Drive
- Create tasks in Notion
- Look up employee records in HiBob
- Send Slack messages
For each one of those, you had to write a completely custom function, handle authentication your own way, manage errors your own way, format the data your own way, and wire it all up manually. If you later switched from LangGraph to a different agent framework, you would have to rewrite everything. If someone else built a better Google Drive connector, you could not easily plug it in; theirs was written for their system, not yours.
Note
This is what Anthropic called the M×N problem: M different AI models or apps, each needing to connect to N different tools. Without a standard, you need M×N custom integrations. That does not scale.
Why Does MCP Matter?
Depending on where you sit in the ecosystem, MCP can have a range of benefits.
Developers
MCP reduces development time and complexity when building, or integrating with, an AI application or agent.
AI Applications or Agents
MCP provides access to an ecosystem of data sources, tools and apps which will enhance capabilities and improve the end-user experience.
End-Users
MCP results in more capable AI applications or agents which can access your data and take actions on your behalf when necessary.
The Three Roles in MCP
Every MCP setup has exactly three roles. Understanding these is the foundation of everything else.
MCP Server
This is what exposes tools or data to the AI. A program that provides context to MCP clients. It is a program that says "here are the things I can do." For example, a Notion MCP server says: "I can read pages, create pages, update databases, search content." A HiBob MCP server would say: "I can get employee records, check leave balances, update profiles." The server does not know anything about AI; it just knows how to expose its capabilities in the MCP standard format.
MCP Client
This is the component inside your AI app that talks to the server. It is the bridge. It asks the server "what can you do?" and then makes requests on behalf of the AI. A component that maintains a connection to an MCP server and obtains context from an MCP server for the MCP host to use.
MCP Host
This is the application the user is actually interacting with. The AI application that coordinates and manages one or multiple MCP clients. Claude Desktop is a host. Claude Code is a host. Your custom LangGraph app would be a host.
The MCP client asks its server for a list of tools and resources the server provides; the server replies with a natural-language description of the capabilities of each tool and the expected format to call the tool. This information is given to the LLM; if it requires the services of one of these tools, the MCP host will instruct the relevant MCP client to call the tool. The MCP server performs the tool action and returns the results, which the MCP host then injects into the LLM conversation.
Note
So the flow goes: User talks to Host → Host tells the AI what tools are available via the Client → AI decides to use a tool → Client calls the Server → Server does the thing → Result comes back to AI → AI responds to User.
MCP follows a client-server architecture where an MCP host establishes connections to one or more MCP servers. The MCP host accomplishes this by creating one MCP client for each MCP server. Each MCP client maintains a dedicated connection with its corresponding MCP server.
Local MCP servers that use the STDIO transport typically serve a single MCP client, whereas remote MCP servers that use the Streamable HTTP transport will typically serve many MCP clients.
Note that MCP server refers to the program that serves context data, regardless of where it runs. MCP servers can execute locally or remotely. For example, when Claude Desktop launches the filesystem server, the server runs locally on the same machine because it uses the STDIO transport. This is commonly referred to as a "local" MCP server. The official Sentry MCP server runs on the Sentry platform, and uses the Streamable HTTP transport. This is commonly referred to as a "remote" MCP server.
What MCP Servers Actually Expose (The Three MCP Primitives)
MCP primitives are the most important concept within MCP. They define what clients and servers can offer each other. These primitives specify the types of contextual information that can be shared with AI applications and the range of actions that can be performed. An MCP server can expose three types of things.
1. Tools: Actions the AI Can Take
Things that do something. Examples: "create a Notion page", "send a Slack message", "query a database", "book a calendar event." Tools are what the AI calls when it decides it needs to take an action. Tools are executable functions that AI applications can invoke to perform actions. They change state, interact with the world, take actions. The AI reads the tool's name and description, decides if it needs it, and calls it.
Note
The mental model: A tool is like an API endpoint you have documented for the AI. You tell the AI "here is what this function is called, here is what it does, here is what arguments it needs", and the AI figures out when to call it.
Here is a real tool definition in the JSON format MCP uses:
{
"name": "create_hibob_leave_request",
"description": "Submit a leave request for an employee in HiBob. Use this when an employee wants to request time off.",
"inputSchema": {
"type": "object",
"properties": {
"employee_id": {
"type": "string",
"description": "The unique ID of the employee in HiBob"
},
"start_date": {
"type": "string",
"format": "date",
"description": "Leave start date in YYYY-MM-DD format"
},
"end_date": {
"type": "string",
"format": "date"
},
"leave_type": {
"type": "string",
"enum": ["annual", "sick", "emergency", "unpaid"]
}
},
"required": ["employee_id", "start_date", "end_date", "leave_type"]
}
}Notice the description on the tool and on each field. These descriptions are not just documentation for humans; the AI reads them to decide if and how to use the tool. Bad descriptions mean the AI misuses or ignores your tool. Good descriptions mean the AI uses it exactly right. This is one of the most important things to get right when building MCP servers.
The execution flow:
- Discovery: Client calls
tools/list→ server returns all available tool definitions. - AI receives those definitions in its context and understands what it can do.
- User sends a request.
- AI decides to use a tool → formats the call with proper arguments.
- Client calls
tools/callwith tool name and arguments. - Server executes the actual function (calls HiBob API, writes to a database, etc.).
- Server returns the result.
- AI incorporates the result into its response.
2. Resources: Data the AI Can Read
Data the AI can read but cannot act on directly. Think of them as files or documents the AI can access. They are data sources that provide contextual information to AI applications. Examples: a company wiki page, a list of employees, a CSV file, API responses. Resources are read-only context. The AI does not call resources directly the way it calls tools; the app retrieves them and includes them in the conversation. Resources are passive; tools are active.
Direct Resources have fixed URIs:
calendar://events/june-2026
file:///company/handbook.pdf
employees://directory/engineering-teamResource Templates are dynamic: URI patterns with parameters.
employees://{department}/{role}
→ employees://engineering/senior-engineer
→ employees://finance/analystNote
The real difference from tools in practice: If you want the AI to have company policy documents as background knowledge for a conversation, that is a resource; the app loads it and passes it to the AI before the conversation starts. If you want the AI to be able to write a new policy document, that is a tool.
3. Prompts: Pre-Built Templates
Pre-built prompt templates that the server exposes. They are reusable templates that help structure interactions with language models. Less commonly used but useful for standardizing how users interact with the server. For example, a "summarize this document" prompt template. They are for standardizing how people interact with complex workflows.
The practical use case: Imagine a company's HR team uses an AI tool daily. Instead of typing a different natural language request every time and getting inconsistent results, you create a prompt called employee_onboarding_review with defined fields: employee_name, start_date, department. The HR team fills in the fields and gets a consistent, well-structured interaction every time.
In Claude Code or Claude Desktop, prompts typically show up as slash commands: /plan_vacation, /onboard_employee, /generate_leave_report.
Note
As a concrete example, consider an MCP server that provides context about a database. It can expose tools for querying the database, a resource that contains the schema of the database, and a prompt that includes few-shot examples for interacting with the tools.
Where MCP Fits in Code You Actually Write
This is where it clicks for developers. There are two situations you will encounter.
Situation A: Using an Existing MCP Server Someone Else Built
There are already hundreds of pre-built MCP servers for popular platforms. Anthropic has shared pre-built MCP servers for popular enterprise systems like Google Drive, Slack, GitHub, Git, Postgres, and Puppeteer. Notion has one. GitHub has one. Many tools you already use have them.
In this situation, you do not write the server at all. You just configure your agent (or Claude Desktop/Code) to connect to it, and suddenly your agent can use those tools. This is where MCP saves the most time: you get a full Notion integration without writing a single line of Notion API code yourself.
Situation B: Building Your Own MCP Server
When a platform does not have an MCP server yet, or you need custom functionality, you build one. For example, building MCP servers for HiBob, Entra, internal platforms, etc., so that Claude can interact with those systems. A basic MCP server in Python looks conceptually like this (simplified):
from mcp.server import Server
from mcp.types import Tool
server = Server("my-hibob-server")
@server.list_tools()
async def list_tools():
return [
Tool(
name="get_employee",
description="Get an employee's details from HiBob by their ID",
inputSchema={
"type": "object",
"properties": {
"employee_id": {"type": "string"}
}
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "get_employee":
# call HiBob API here
return hibob_api.get_employee(arguments["employee_id"])You define what tools exist, what they are called, what arguments they take, and what they do. Claude then discovers these tools automatically and can call them when relevant.
MCP in Claude Code Specifically
Claude Code is where MCP becomes incredibly powerful for a developer's day-to-day. You configure MCP servers in a config file, and then Claude Code gains access to all those tools natively: it can browse your GitHub issues, read your Notion docs, query your database, all from within your coding session.
For example, if you add a GitHub MCP server to Claude Code, you can say: "Look at the open issues tagged 'bug', find the three most critical ones, and write fixes for them." Claude Code can actually read the issues, understand them, write the code, and if you have a Git MCP server too, even commit.
The Two Layers of MCP
MCP consists of two layers:
- Transport layer: Defines the communication mechanisms and channels that enable data exchange between clients and servers, including transport-specific connection establishment, message framing, and authorization.
- Data layer: Defines the JSON-RPC based protocol for client-server communication, including lifecycle management, and core primitives, such as tools, resources, prompts and notifications.
Conceptually the data layer is the inner layer, while the transport layer is the outer layer.
Transport Layer: How MCP Communicates Under the Hood
Client and server communicate using the JSON-RPC 2.0 transport protocol. There are two transport methods:
- stdio (Standard Input/Output): For local servers running on the same machine. The client literally communicates with the server by passing messages through the terminal's stdin/stdout. This is what you use for local development and local tools (file system access, local database, etc.).
- HTTP with SSE (Server-Sent Events): For remote servers. This is what you use when the server is hosted somewhere else, like an MCP server you deploy to a cloud service so multiple people or agents can use it. MCP uses HTTP POST for client-to-server messages with optional Server-Sent Events for streaming capabilities. This transport enables remote server communication and supports standard HTTP authentication methods including bearer tokens, API keys, and custom headers. MCP recommends using OAuth to obtain authentication tokens.
The transport method (stdio vs HTTP) describes how your code communicates with the MCP server process, not how that server talks to the external API.
When you "use the GitHub MCP server," you typically download and run that server locally on your own machine. It becomes a process running on your computer. Your MCP client talks to it via stdio (local pipes). That server then internally makes its own regular HTTP calls out to GitHub's API. But that is invisible to MCP: from MCP's perspective, it is just a local process.
So the two transport modes are not about "local service vs cloud service." They are about where the MCP server process itself is running.
stdio (Standard Input/Output)
The MCP server runs on the same machine as your code. Your client communicates by literally writing to and reading from that process's stdin/stdout, like piping text between programs in a terminal. When you use it:
- Running any of the pre-built MCP servers (GitHub, Notion, Google Drive, Postgres) locally on your dev machine
- Building a server that reads local files or talks to local databases
- Development and testing of any MCP server
- Claude Code with locally configured MCP servers
HTTP with SSE (Server-Sent Events)
The MCP server is running somewhere else: deployed to the cloud, or on a different machine on the network. Your client connects over HTTP. When you use it:
- An organization has deployed a shared MCP server that multiple engineers (or multiple Claude instances) all connect to
- You build an MCP server and deploy it to Railway or Render so your whole team can use it
- A company hosts a HiBob MCP server centrally that all their internal tools connect to
- A product that exposes its capabilities as a remote MCP server (this is where the ecosystem is heading)
SSE specifically is how the server pushes updates to the client in real time (like notifications). HTTP POST handles client-to-server messages, and SSE handles server-to-client streaming. Together they give you full two-way communication over HTTP.
Note
Practical rule: Start with stdio for all your projects. You will only need HTTP+SSE when you want to deploy an MCP server to the cloud for shared use.
Data Layer
The data layer implements a JSON-RPC 2.0 based exchange protocol that defines the message structure and semantics. This layer includes:
- Lifecycle management: Handles connection initialization, capability negotiation, and connection termination between clients and servers.
- Server features: Enables servers to provide core functionality including tools for AI actions, resources for context data, and prompts for interaction templates from and to the client.
- Client features: Enables servers to ask the client to sample from the host LLM, elicit input from the user, and log messages to the client.
- Utility features: Supports additional capabilities like notifications for real-time updates and progress tracking for long-running operations.
Data Layer Protocol
A core part of MCP is defining the schema and semantics between MCP clients and MCP servers. Developers will likely find the data layer, in particular the set of primitives, to be the most interesting part of MCP. It is the part of MCP that defines the ways developers can share context from MCP servers to MCP clients.
MCP uses JSON-RPC 2.0 as its underlying RPC protocol. Client and servers send requests to each other and respond accordingly. Notifications can be used when no response is required.
JSON-RPC 2.0, Demystified
- JSON is just a data format. A way of writing structured information that both humans and computers can read. Like:
{"name": "Jonathan", "role": "engineer"}. - RPC stands for Remote Procedure Call. "Procedure" is just an old word for function. So RPC means: calling a function that lives in a different process or machine, not in your own code. Think about it: when your LangGraph agent calls a tool, it is essentially saying "run this function for me." RPC is the general concept of doing that across process boundaries.
- 2.0 is just the version number.
So JSON-RPC 2.0 is a standard that says: here is exactly how you should format a message when you want to call a function on another process, using JSON. That is it.
What the Messages Look Like
There are three types of messages in JSON-RPC 2.0:
A Request
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_employee",
"arguments": { "employee_id": "EMP001" }
}
}jsonrpc: Always "2.0". Just identifies the protocol version.id: A unique number you assign so you can match the response to this specific request. If you send 10 requests, each gets a different id so you know which response belongs to which request.method: The function you want to call. In MCP, these follow a pattern liketools/list,tools/call,resources/read.params: The arguments you are passing to that function.
A Response
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{ "type": "text", "text": "Employee: Jonathan Mukhobe" }]
}
}- Same
idas the request: this is how you know "this response answers request #3." result: The returned data.
A Notification
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}Notice that there is no id here. That is the key difference. No id means no response is expected. The sender is just saying "FYI, something changed" and moving on.
That is genuinely all JSON-RPC 2.0 is. MCP uses it as its language: every single message between an MCP client and server is one of these three formats.
Lifecycle Management: The Handshake
The purpose of lifecycle management is to negotiate the capabilities that both client and server support. This section provides a step-by-step walkthrough of an MCP client-server interaction, focusing on the data layer protocol.
Every time your MCP client connects to a server, they go through a handshake before they do anything useful. This is lifecycle management. Think of it like two people meeting for the first time and establishing ground rules before working together. There are three phases:
Phase 1: Initialize Request (Client → Server)
The client sends an initialize request to establish the connection and negotiate supported features. The client says: "Hello, I'm connecting. Here's what version of MCP I speak, and here's what features I support."
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"clientInfo": { "name": "my-agent", "version": "1.0" },
"capabilities": {
"elicitation": {}
}
}
}The client is declaring: "I support the elicitation feature", meaning if the server needs to ask the user for input, the client can handle that.
Phase 2: Initialize Response (Server → Client)
The server responds: "Understood. Here's what version I speak, and here's what I offer."
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-11-25",
"serverInfo": { "name": "hibob-server", "version": "1.0" },
"capabilities": {
"tools": { "listChanged": true },
"resources": {}
}
}
}The server is declaring: "I have tools and resources. And I can send you notifications when my tool list changes (listChanged: true)."
Phase 3: Initialized Notification (Client → Server)
After successful initialization, the client sends a notification to indicate it is ready. This is a notification (no id, no response expected):
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}Understanding the Initialization Exchange
The initialization process is a key part of MCP's lifecycle management and serves several critical purposes:
- Protocol Version Negotiation: The
protocolVersionfield (e.g. "2025-11-25") ensures both client and server are using compatible protocol versions. This prevents communication errors that could occur when different versions attempt to interact. If a mutually compatible version is not negotiated, the connection should be terminated. - Capability Discovery: The
capabilitiesobject allows each party to declare what features they support, including which primitives they can handle (tools, resources, prompts) and whether they support features like notifications. This enables efficient communication by avoiding unsupported operations. - Identity Exchange: The
clientInfoandserverInfoobjects provide identification and versioning information for debugging and compatibility purposes.
In this example, the capability negotiation demonstrates how MCP primitives are declared:
Client Capabilities:
"elicitation": {}: The client is declaring "I support the elicitation feature", meaning if the server needs to ask the user for input, the client can handle that.
Server Capabilities:
"tools": {"listChanged": true}: The server supports the tools primitive AND can sendtools/list_changednotifications when its tool list changes."resources": {}: The server also supports the resources primitive (can handleresources/listandresources/readmethods).
Note
Why does this matter? The handshake solves a real problem: if your client and server support different versions or features, you find out immediately and can handle it gracefully rather than sending messages that neither side understands. After the handshake, both sides know exactly what they can ask of each other. From a production perspective, if a company updates a server with new features, old clients that do not support those features can still connect because they negotiated capabilities upfront. That is what makes MCP resilient at scale.
How This Works in AI Applications
During initialization, the AI application's MCP client manager establishes connections to configured servers and stores their capabilities for later use. The application uses this information to determine which servers can provide specific types of functionality (tools, resources, prompts) and whether they support real-time updates.
async with stdio_client(server_config) as (read, write):
async with ClientSession(read, write) as session:
init_response = await session.initialize()
if init_response.capabilities.tools:
app.register_mcp_server(session, supports_tools=True)
app.set_server_ready(session)Next Step 1: Tool Discovery (Primitives)
Now that the connection is established, the client can discover available tools by sending a tools/list request. This request is fundamental to MCP's tool discovery mechanism: it allows clients to understand what tools are available on the server before attempting to use them.
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "calculator_arithmetic",
"title": "Calculator",
"description": "Perform mathematical calculations including basic arithmetic, trigonometric functions, and algebraic operations",
"inputSchema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', 'sin(30)', 'sqrt(16)')"
}
},
"required": ["expression"]
}
},
{
"name": "weather_current",
"title": "Weather Information",
"description": "Get current weather information for any location worldwide",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name, address, or coordinates (latitude,longitude)"
},
"units": {
"type": "string",
"enum": ["metric", "imperial", "kelvin"],
"description": "Temperature units to use in response",
"default": "metric"
}
},
"required": ["location"]
}
}
]
}
}Understanding the Tool Discovery Exchange
The tools/list request is simple, containing no parameters. The response contains a tools array that provides comprehensive metadata about each available tool. This array-based structure allows servers to expose multiple tools simultaneously while maintaining clear boundaries between different functionalities. Each tool object in the response includes several key fields:
- name: A unique identifier for the tool within the server's namespace. This serves as the primary key for tool execution and should follow a clear naming pattern (e.g.
calculator_arithmeticrather than justcalculate). - title: A human-readable display name for the tool that clients can show to users.
- description: Detailed explanation of what the tool does and when to use it.
- inputSchema: A JSON Schema that defines the expected input parameters, enabling type validation and providing clear documentation about required and optional parameters.
How This Works in AI Applications
The AI application fetches available tools from all connected MCP servers and combines them into a unified tool registry that the language model can access. This allows the LLM to understand what actions it can perform and automatically generates the appropriate tool calls during conversations.
available_tools = []
for session in app.mcp_server_sessions():
tools_response = await session.list_tools()
available_tools.extend(tools_response.tools)
conversation.register_available_tools(available_tools)Next Step 2: Tool Execution (Primitives)
The client can now execute a tool using the tools/call method. This demonstrates how MCP primitives are used in practice: after discovering available tools, the client can invoke them with appropriate arguments.
The tools/call request follows a structured format that ensures type safety and clear communication between client and server. Note that we are using the proper tool name from the discovery response (weather_current) rather than a simplified name:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "weather_current",
"arguments": {
"location": "San Francisco",
"units": "imperial"
}
}
}{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "Current weather in San Francisco: 68°F, partly cloudy with light winds from the west at 8 mph. Humidity: 65%"
}
]
}
}Key Elements of Tool Execution
The request structure includes several important components:
- name: Must match exactly the tool name from the discovery response (
weather_current). This ensures the server can correctly identify which tool to execute. - arguments: Contains the input parameters as defined by the tool's inputSchema. In this example:
locationis "San Francisco" (required parameter) andunitsis "imperial" (optional parameter, defaults to "metric" if not specified). - JSON-RPC Structure: Uses standard JSON-RPC 2.0 format with unique id for request-response correlation.
Understanding the Tool Execution Response
The response demonstrates MCP's flexible content system:
- content Array: Tool responses return an array of content objects, allowing for rich, multi-format responses (text, images, resources, etc.).
- Content Types: Each content object has a type field. In this example,
"type": "text"indicates plain text content, but MCP supports various content types for different use cases. - Structured Output: The response provides actionable information that the AI application can use as context for language model interactions.
This execution pattern allows AI applications to dynamically invoke server functionality and receive structured responses that can be integrated into conversations with language models.
How This Works in AI Applications
When the language model decides to use a tool during a conversation, the AI application intercepts the tool call, routes it to the appropriate MCP server, executes it, and returns the results back to the LLM as part of the conversation flow. This enables the LLM to access real-time data and perform actions in the external world.
async def handle_tool_call(conversation, tool_name, arguments):
session = app.find_mcp_session_for_tool(tool_name)
result = await session.call_tool(tool_name, arguments)
conversation.add_tool_result(result.content)Next Step 3: Real-Time Updates (Notifications)
MCP supports real-time notifications that enable servers to inform clients about changes without being explicitly requested. This demonstrates the notification system, a key feature that keeps MCP connections synchronized and responsive.
When the server's available tools change, such as when new functionality becomes available, existing tools are modified, or tools become temporarily unavailable, the server can proactively notify connected clients:
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}Key Features of MCP Notifications
- No Response Required: Notice there is no id field in the notification. This follows JSON-RPC 2.0 notification semantics where no response is expected or sent.
- Capability-Based: This notification is only sent by servers that declared
"listChanged": truein their tools capability during initialization. - Event-Driven: The server decides when to send notifications based on internal state changes, making MCP connections dynamic and responsive.
Client Response to Notifications
Upon receiving this notification, the client typically reacts by requesting the updated tool list. This creates a refresh cycle that keeps the client's understanding of available tools current:
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/list"
}Why Notifications Matter
This notification system is crucial for several reasons:
- Dynamic Environments: Tools may come and go based on server state, external dependencies, or user permissions.
- Efficiency: Clients do not need to poll for changes; they are notified when updates occur.
- Consistency: Ensures clients always have accurate information about available server capabilities.
- Real-time Collaboration: Enables responsive AI applications that can adapt to changing contexts.
This notification pattern extends beyond tools to other MCP primitives, enabling comprehensive real-time synchronization between clients and servers.
How This Works in AI Applications
When the AI application receives a notification about changed tools, it immediately refreshes its tool registry and updates the LLM's available capabilities. This ensures that ongoing conversations always have access to the most current set of tools, and the LLM can dynamically adapt to new functionality as it becomes available.
async def handle_tools_changed_notification(session):
tools_response = await session.list_tools()
app.update_available_tools(session, tools_response.tools)
if app.conversation.is_active():
app.conversation.notify_llm_of_new_capabilities()Client Features: What the Client Offers the Server
This is the part most people miss. MCP is two-way. Servers expose tools and resources to clients, but clients also expose features to servers. The server can use these features to build richer, more powerful interactions.
Elicitation: The Server Asks the User a Question
Normally: User → Client → AI → Server (the server just receives and acts). With elicitation, the server can pause its work and say to the client: "I need more information from the user before I can continue."
A concrete example: Your agent is in the middle of booking a flight. The MCP server handling the booking has found options and is ready to confirm. But before finalizing, it needs the user's passport number and seat preference. The server can use elicitation to pause and ask:
{
"method": "elicitation/create",
"params": {
"message": "Before I confirm your booking, I need a few details:",
"requestedSchema": {
"type": "object",
"properties": {
"passport_number": { "type": "string" },
"seat_preference": {
"type": "string",
"enum": ["window", "aisle", "middle"]
},
"confirm_booking": { "type": "boolean" }
},
"required": ["passport_number", "confirm_booking"]
}
}
}The client shows this as a UI form to the user, collects the structured response, and sends it back to the server. The server then continues with that information.
Note
Why this matters for production agents: Without elicitation, if the server needs more info, it has to either fail or make assumptions. With elicitation, you can build agents that handle missing information gracefully mid-workflow, a critical feature for real enterprise automations where you cannot always predict what info you will need upfront.
Roots: Filesystem Scope Boundaries
Roots are how the client tells the server which directories it is allowed to operate in. If you connect a filesystem MCP server to help with a project, you do not want it looking through your entire machine; you point it at specific folders.
{
"roots": [
{ "uri": "file:///home/jonathan/projects/projects", "name": "Projects" },
{ "uri": "file:///home/jonathan/docs/research", "name": "Research" }
]
}Important caveat from the docs: Roots are a convention, not a hard security boundary. A well-behaved server respects them. But they do not enforce access at the OS level. Think of them as telling a trusted contractor "please only work in these rooms"; they should follow that, but it is not a locked door. Real security enforcement happens through OS-level file permissions, not MCP roots.
Claude Code uses roots automatically when you open a folder: it tells any connected MCP servers "this is the project scope."
Sampling: The Server Asks the AI to Think
This is the most advanced and counterintuitive client feature. Sampling allows the server to make a request back to the AI (through the client) during its own operation.
Why would a server need to ask the AI? Sometimes a tool needs intelligence to decide what to do. Example: your MCP server retrieves 47 flight options. It could return all 47 to the AI in the main conversation, but that would flood the context window. Instead, the server can use sampling: "Hey client, can you ask the AI to look at these 47 flights and tell me the top 3 for this user's preferences?" The AI analyzes them in a separate call, returns a structured answer, and the server uses that to give a clean response.
Why this keeps the client in control: The sampling request goes through the client, and the client can show it to the user for approval before forwarding it to the AI. The server cannot make secret AI calls on the user's behalf. The client is the gatekeeper.
For your projects: Sampling is advanced and you likely will not need it in your first MCP builds. But knowing it exists helps you understand why MCP is powerful: you can build server-side logic that uses AI reasoning without the server needing its own LLM integration.
Notifications: One-Way Messages
You have seen these throughout. The pattern is simple: a notification is a JSON-RPC message with no id field. This means no response is expected. It is a signal, not a request. The most important notifications in practice:
notifications/tools/list_changed: Server tells the client its tool list has changed. The client should calltools/listagain to get the updated list. This matters when servers dynamically add or remove tools based on user permissions or system state.notifications/initialized: Client tells the server it is ready to work (sent after the initialize handshake).notifications/progress: For long-running operations. The server sends progress updates: "Step 1 of 5 complete... Step 2 of 5..."
Note
Why notifications matter at large company scale: Imagine 2,500 employees all using an agent connected to an MCP server. If that server updates (new tools added, permissions changed for certain users), every connected client needs to know. Without notifications, clients would be working with stale information. With notifications, all 2,500 active connections get updated in real time without each one having to poll the server constantly.
Tasks (Experimental): Long-Running Operations
This is a newer, still-experimental feature. Normal tool calls are synchronous: you call a tool, you wait, you get the result. But some operations take minutes or hours, like "run a batch report on all 7 million customers" or "process this large dataset." Tasks are durable execution wrappers that let you fire off a long operation and check on it later:
- Call a tool that starts a task → get back a
task_idimmediately - Do other things
- Later, check
task_idfor status: "running" / "complete" / "failed" - When complete, retrieve the result
This is analogous to Celery in Django: you enqueue a job, it runs in the background, you check on it. Same concept, just standardized in MCP. For large company automations that touch large data sets or multi-step workflows, this is the mechanism that would make those agents non-blocking and production-grade.
Versioning: How MCP Manages Change
The Date-Based Version Format (YYYY-MM-DD)
MCP uses dates as version numbers, not v1.0 or v2.0. The current version as of this article is 2025-11-25. The date represents the last time a breaking (backwards-incompatible) change was made.
The key rule: If Anthropic adds a new feature to MCP but does not break anything that already works, the version date stays the same. They only bump the date when they make a change that would break existing implementations. This is a deliberately conservative approach: the protocol can keep improving without forcing everyone to upgrade constantly.
Individual Feature States
Beyond whole-version states, individual features within a version can have their own lifecycle:
- Deprecated: The feature still works but is being phased out. They give at least 12 months warning (90 days minimum in exceptional cases) before removing it. Use this time to migrate away.
- Removed: Gone in a new version. You will see it in older specs but it will not work in current servers.
Version Negotiation During Initialization
This is the practical part. During the initialization handshake (Phase 1 above), the client says "I speak version 2025-11-25." The server responds with what version it uses. If they are on different versions:
- If backwards compatible → they can still work together
- If incompatible → the connection terminates gracefully with an error, rather than silently failing or producing weird behavior
Note
Why this matters when you build: Always check the current protocol version before building. Build against "Current," not "Draft." If you ever see "Deprecated" next to a feature you are using, plan your migration: you have at least 12 months, but do not leave it to the last minute.
I rebuilt my own WhatsApp agent on this protocol, and the Kylie MCP case study shows what each of these pieces looks like in a real repository.
What You Actually Write, In Summary
When You Build an MCP Server
You write tool definitions (name, description, inputSchema), tool handlers (the actual function that runs), resource endpoints (if exposing data), and optionally notification senders (if your tool list changes dynamically).
When You Build an MCP Client (Your Agent)
You write the initialization handshake, the
tools/listcall on startup, tool call routing when the AI decides to use a tool, notification listeners to keep your tool registry fresh, and optionally elicitation UI if you want users to answer mid-task questions.When You Deploy to Production
You move from stdio (local) to HTTP+SSE (remote), add proper authentication (OAuth or API keys in the HTTP headers), add observability (logging on each tool call, the same discipline as an Opik-style tracing setup), and version pin to the Current spec.
You are now ready to go build. The jump from understanding to hands-on will make everything click, especially the initialization sequence, which looks abstract until you see it happen in logs for the first time.
Think of MCP like a USB-C port for AI applications: one standard connector between any AI application and any external system, so nobody has to keep soldering custom wires.