Model Context Protocol (MCP): Complete Guide
Source: mortalapps.com- The Model Context Protocol (MCP) is an open standard for connecting AI models to external data sources and tools via a unified JSON-RPC 2.0 interface.
- It eliminates the N-to-M integration problem by allowing any MCP-compliant host (e.g., Claude, IDEs) to interact with any MCP-compliant server without custom glue code.
- The protocol is critical for production agent systems requiring secure, discoverable, and type-safe access to enterprise data and local environments.
- Implementation of the 2025-03-26 specification enables stateless Streamable HTTP transports and standardized OAuth 2.1 authorization flows.
- Tradeoff: While MCP simplifies integration, it introduces network serialization overhead and requires robust error handling for remote tool execution.
Why This Matters
The Model Context Protocol (MCP) addresses the fundamental fragmentation of the AI agent ecosystem. Before MCP, developers building agentic systems were forced to write bespoke integration layers for every combination of LLM provider and data source. This 'N-to-M' problem created significant technical debt, as a tool built for one framework (e.g., LangChain) could not be easily reused in another (e.g., Semantic Kernel) or directly within an LLM's native interface. MCP provides a standardized 'USB-C' for AI context, decoupling the model's reasoning capabilities from the specific implementation details of the tools it uses.
In production environments, ignoring MCP leads to brittle architectures where tool definitions are scattered across multiple repositories, making security auditing and version control nearly impossible. MCP was invented to centralize these definitions into discoverable servers that can be shared across an entire organization. For enterprises, this means a single 'Database MCP Server' can serve a developer's IDE, a customer support chatbot, and an automated data analysis agent simultaneously. The 2025-03-26 specification further matures the protocol by moving away from stateful session IDs in favor of Streamable HTTP, which aligns with modern cloud-native scaling patterns. This shift is essential for high-concurrency agent loops where maintaining long-lived TCP connections or sticky sessions is a significant operational burden. Without MCP, teams face higher maintenance costs and slower deployment cycles as they struggle to keep up with the rapidly evolving landscape of model-specific tool-calling formats.
Core Concepts
The Model Context Protocol is built upon a client-server architecture where the 'Host' (client) initiates requests and the 'Server' provides capabilities. Understanding these core primitives is essential for implementing the 2025-03-26 specification.
- Host: The application that integrates an LLM and manages the user session (e.g., Claude Desktop, VS Code, or a custom agentic pipeline). It is responsible for orchestrating tool calls and managing the context window.
- Server: A lightweight process or service that exposes specific capabilities (Tools, Resources, Prompts) to the Host. Servers can run locally via stdio or remotely via HTTP.
- Transport: The mechanism used to move JSON-RPC messages between Host and Server. The protocol supports stdio for local processes and Server-Sent Events (SSE) or Streamable HTTP for remote connections.
- Capabilities: During the handshake, both parties negotiate what they can do. Servers might offer
tools/listorresources/subscribe, while Hosts might offersampling(allowing the server to ask the LLM to generate text). - Tools: Executable functions that the LLM can invoke. They have a defined input schema (JSON Schema) and return a result that the model can use in its next reasoning step.
- Resources: Static or dynamic data that the model can read. Unlike tools, resources are typically passive (e.g., a file's content or a database row) and are identified by URIs.
- Prompts: Pre-defined templates provided by the server that help the host structure interactions for specific tasks, such as 'Analyze this codebase' or 'Debug this error'.
How It Works
The MCP lifecycle follows a strict sequence of negotiation and execution designed to ensure type safety and security across different environments.
1. Transport Establishment
The Host starts the Server process (for stdio) or connects to a remote endpoint (for SSE/HTTP). In the 2025-03-26 spec, remote connections utilize Streamable HTTP, which allows for bidirectional communication without the overhead of traditional WebSockets. The Host sends an initial connection request to establish the communication channel.
2. The Handshake (Initialize)
Before any data is exchanged, the Host sends an initialize request. This request contains the Host's version, capabilities, and client information. The Server responds with its own capabilities (e.g., whether it supports tools, resources, or prompts) and its protocol version. If the versions are incompatible, the connection is terminated here. In the latest spec, this phase also includes the negotiation of authentication parameters via OAuth 2.1 if required.
3. Discovery Phase
Once initialized, the Host typically calls tools/list and resources/list. The Server returns a list of available tools, including their names, descriptions, and JSON Schema definitions for arguments. This information is injected into the LLM's context window, enabling the model to understand what actions it can take.
4. The Request-Response Loop
When the LLM decides to use a tool, the Host sends a tools/call request to the Server. The Server executes the logic (e.g., querying a database or running a script) and returns a tool/call/result. If the tool execution fails, the Server must return a structured error object within the JSON-RPC response rather than a generic transport-level error (like HTTP 500), allowing the LLM to attempt self-correction.
5. Notifications and Subscriptions
Servers can send asynchronous notifications to the Host, such as notifications/resources/updated if a resource the Host is watching changes. This allows for reactive agent behavior without constant polling. The Host can also send notifications to the Server, such as notifications/initialized to signal that it is ready to start the main loop.
Architecture
The MCP architecture is a layered system designed for transport independence and strict schema adherence. At the base is the Transport Layer, which handles the raw byte stream. Above this is the Message Layer, which implements the JSON-RPC 2.0 protocol, ensuring every message has an ID, a method, and parameters. The Abstraction Layer defines the MCP-specific primitives: Tools, Resources, and Prompts. Execution starts at the Host Application, which acts as the orchestrator. Data flows from the Host to the MCP Server via the Transport. The Server processes the request, potentially interacting with External Systems (Databases, APIs, Local Filesystems), and returns the result back up the stack. In the 2025-03-26 specification, the architecture emphasizes a Stateless Execution Model for HTTP transports, where authentication and context are passed via standardized headers or tokens rather than persistent session identifiers, facilitating easier horizontal scaling of MCP server clusters.
JSON-RPC 2.0 Specification Adherence
MCP utilizes JSON-RPC 2.0 for all communications. Every request must include a unique id (string or integer), the jsonrpc: "2.0" version string, a method name, and a params object. Responses must include the same id and either a result or an error object. This strict structure allows for easy debugging and the use of standard JSON-RPC libraries across different programming languages.
| Field | Type | Description |
|---|---|---|
jsonrpc |
string | Must be exactly "2.0" |
id |
string/int | Unique identifier for the request/response pair |
method |
string | The MCP method being called (e.g., tools/call) |
params |
object | Method-specific parameters |
The 2025-03-26 Transport Evolution
The most significant change in the 2025-03-26 specification is the formalization of Streamable HTTP and the deprecation of the Mcp-Session-Id header. Previously, remote MCP required stateful tracking of sessions at the transport level. The new spec moves toward a stateless model where each request is self-contained or uses standard OAuth 2.1 tokens for identity. Streamable HTTP uses a specialized POST-based protocol where the response body remains open for server-to-client notifications, effectively providing a lightweight alternative to WebSockets that is more compatible with corporate firewalls and load balancers.
Tool Schema Design (JSON Schema 2020-12)
MCP tools are defined using JSON Schema 2020-12. This allows for complex validation logic, including nested objects, arrays, and enums. A well-designed tool schema is critical for LLM performance; if the schema is too vague, the model may generate invalid arguments.
{
"name": "get_weather",
"description": "Get the current weather for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": { "type": "string" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["location"]
}
}
Resource Templates and URI Schemes
Resources are identified by URIs. MCP supports Resource Templates, which allow servers to expose parameterized data sources. For example, a server might expose postgres://{database}/{table}. The Host can then resolve these templates by providing the necessary variables. This is more efficient than listing every individual table as a separate resource. Servers must provide a listChanged capability if they want the Host to know when the available resources have changed dynamically.
Capability Negotiation Matrix
During the initialize phase, both the client and server exchange a capabilities object. This determines which features are active for the duration of the connection.
| Capability | Provider | Description |
|---|---|---|
tools |
Server | Supports tool discovery and execution |
resources |
Server | Supports reading static/dynamic data |
prompts |
Server | Provides pre-defined prompt templates |
logging |
Server | Can send log messages to the host |
sampling |
Host | Allows the server to request LLM completions |
roots |
Host | Informs the server of the accessible filesystem paths |
Security and OAuth 2.1 Integration
With the removal of custom session management, the 2025-03-26 spec standardizes on OAuth 2.1 for remote server authorization. Servers can specify an auth requirement in their metadata. The Host is responsible for performing the OAuth flow (e.g., Authorization Code Flow with PKCE) and attaching the resulting Bearer token to subsequent MCP requests. This ensures that agentic tools respect the same identity and access management (IAM) policies as human users.
Error Handling and Protocol Codes
MCP defines specific error codes to help the Host distinguish between transport failures, protocol violations, and tool execution errors.
-32700: Parse error (Invalid JSON)-32600: Invalid Request (Not a valid JSON-RPC object)-32601: Method not found-32602: Invalid params (Schema validation failed)-32603: Internal error-32000 to -32099: Reserved for MCP-specific protocol errors
When a tool fails, the server should return a successful JSON-RPC response where the result object contains isError: true and the error message in the content array. This allows the LLM to see the error as part of its conversation history and potentially fix the input.
Code Example
import os
import logging
from mcp.server.fastmcp import FastMCP
# Initialize FastMCP server with metadata
mcp = FastMCP("Enterprise-Data-Service")
# Configure logging for production observability
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mcp-server")
@mcp.tool()
def query_customer_db(customer_id: str) -> str:
"""Queries the internal database for customer records by ID."""
# Access secrets from environment variables
db_url = os.getenv("DATABASE_URL")
if not db_url:
logger.error("DATABASE_URL not set")
return "Error: Database connection not configured."
try:
# Simulated database logic
logger.info(f"Fetching record for: {customer_id}")
if customer_id == "123":
return "Customer: Alice, Status: Active, Tier: Gold"
return f"No record found for ID: {customer_id}"
except Exception as e:
logger.exception("Database query failed")
return f"Error executing query: {str(e)}"
if __name__ == "__main__":
# Run using stdio transport by default
mcp.run()
The server starts and waits for JSON-RPC input on stdin. When queried with tools/list, it returns the query_customer_db schema.
// Request from Host to Server
{
"jsonrpc": "2.0",
"id": "req_8821",
"method": "tools/call",
"params": {
"name": "query_customer_db",
"arguments": {
"customer_id": "123"
}
}
}
// Response from Server to Host
{
"jsonrpc": "2.0",
"id": "req_8821",
"result": {
"content": [
{
"type": "text",
"text": "Customer: Alice, Status: Active, Tier: Gold"
}
],
"isError": false
}
}
A valid JSON-RPC response containing the tool execution result formatted for LLM consumption.