How MCP Works¶
Architecture Overview¶
MCP follows a client-server architecture where AI models (clients) communicate with tool providers (servers) through a standardized protocol. Let's break down how this works in practice.
The Complete Flow¶
sequenceDiagram
participant User
participant AI as AI Model/Client
participant Transport
participant Server as MCP Server
participant Tool as External Tool/Service
User->>AI: "Analyze sales data"
AI->>AI: Determine needed tools
AI->>Transport: Initialize connection
Transport->>Server: Establish session
Server-->>Transport: Session confirmed
AI->>Server: tools/list
Server-->>AI: Available tools catalog
AI->>Server: tools/call "query_sales_db"
Server->>Tool: Execute query
Tool-->>Server: Results
Server-->>AI: Formatted response
AI->>User: "Sales analysis complete..."
Core Components Deep Dive¶
1. Transport Layer¶
The transport layer handles the physical communication between clients and servers. MCP supports multiple transport types:
Standard I/O (stdio)¶
Perfect for local tools and development:
# Server launches as subprocess
transport = StdioTransport(
command="mcp-server-filesystem",
args=["--root", "/data"]
)
HTTP/HTTPS¶
For network services and APIs:
# Connect over HTTP
transport = HttpTransport(
url="https://api.example.com/mcp",
headers={"Authorization": "Bearer token"}
)
WebSocket¶
For real-time, bidirectional communication:
2. Message Protocol¶
MCP uses JSON-RPC 2.0 for all communications. Every interaction follows this pattern:
Request Structure¶
{
"jsonrpc": "2.0",
"id": "unique-request-id",
"method": "tools/call",
"params": {
"name": "calculator",
"arguments": {
"operation": "sqrt",
"number": 16
}
}
}
Response Structure¶
{
"jsonrpc": "2.0",
"id": "unique-request-id",
"result": {
"content": [
{
"type": "text",
"text": "4"
}
]
}
}
3. Session Lifecycle¶
Every MCP interaction follows a defined lifecycle:
stateDiagram-v2
[*] --> Connecting: Client initiates
Connecting --> Initializing: Transport established
Initializing --> Ready: Capabilities exchanged
Ready --> Active: Normal operations
Active --> Active: Tool calls, resource access
Active --> Closing: Shutdown initiated
Closing --> [*]: Clean disconnect
Connecting --> [*]: Connection failed
Active --> [*]: Error/timeout
4. Capability Negotiation¶
When a connection is established, client and server exchange capabilities:
// Client announces what it supports
{
"method": "initialize",
"params": {
"protocolVersion": "1.0",
"capabilities": {
"tools": true,
"resources": true,
"prompts": true,
"streaming": true
}
}
}
// Server responds with its capabilities
{
"result": {
"protocolVersion": "1.0",
"capabilities": {
"tools": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
}
}
}
}
The Three Pillars of MCP¶
🔧 Tools¶
Functions that AI can execute:
# Server defines a tool
@server.tool()
async def calculate_metric(
metric: str,
start_date: str,
end_date: str
) -> ToolResult:
# Implementation
result = await db.query(...)
return ToolResult(content=[
TextContent(text=f"Result: {result}")
])
📁 Resources¶
Data sources AI can access:
# Server exposes resources
@server.list_resources()
async def list_resources() -> list[Resource]:
return [
Resource(
uri="file:///reports/2024/sales.csv",
name="2024 Sales Report",
mimeType="text/csv"
)
]
💬 Prompts¶
Templated interactions:
# Server provides prompts
@server.list_prompts()
async def list_prompts() -> list[Prompt]:
return [
Prompt(
name="analyze_performance",
description="Analyze system performance",
arguments=[
PromptArgument(
name="timeframe",
description="Time period to analyze",
required=True
)
]
)
]
Real-World Example: Database Query¶
Let's trace a complete database query through MCP:
Step 1: Client Requests Available Tools¶
Step 2: Server Returns Tool Catalog¶
{
"result": {
"tools": [{
"name": "sql_query",
"description": "Execute read-only SQL queries",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "SQL query to execute"
},
"database": {
"type": "string",
"enum": ["sales", "inventory", "customers"]
}
},
"required": ["query", "database"]
}
}]
}
}
Step 3: Client Executes Tool¶
{
"method": "tools/call",
"params": {
"name": "sql_query",
"arguments": {
"database": "sales",
"query": "SELECT SUM(revenue) FROM orders WHERE date >= '2024-01-01'"
}
}
}
Step 4: Server Returns Results¶
{
"result": {
"content": [{
"type": "text",
"text": "Total revenue for 2024: $1,234,567.89"
}],
"isError": false
}
}
Error Handling¶
MCP provides standardized error handling:
{
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"details": "Database 'production' not in allowed list"
}
}
}
Common error codes: - -32700
: Parse error - -32600
: Invalid request - -32601
: Method not found - -32602
: Invalid params - -32603
: Internal error
Performance Considerations¶
Batching Requests¶
MCP supports request batching for efficiency:
[
{"method": "tools/call", "params": {...}, "id": "1"},
{"method": "tools/call", "params": {...}, "id": "2"},
{"method": "resources/read", "params": {...}, "id": "3"}
]
Streaming Responses¶
For large data sets or real-time updates:
Security Model¶
MCP implements defense in depth:
- Transport Security: TLS/mTLS for network transports
- Authentication: Multiple methods supported
- Authorization: Fine-grained permissions
- Rate Limiting: Built-in DoS protection
- Input Validation: Automatic schema validation
Best Practices¶
- Always validate inputs - Use JSON Schema
- Handle errors gracefully - Provide helpful error messages
- Implement timeouts - Prevent hanging connections
- Log appropriately - Aid debugging without exposing secrets
- Version your servers - Support protocol evolution
Summary¶
MCP works by: - ✅ Establishing secure client-server connections - ✅ Negotiating capabilities between parties - ✅ Exposing tools, resources, and prompts - ✅ Using JSON-RPC 2.0 for all communication - ✅ Providing standardized error handling - ✅ Supporting multiple transport options
Ready to see these concepts in action? Head to our Quick Start Guide →