Core architecture
The Model Context Protocol (MCP) is built on a flexible, extensible architecture that enables seamless communication between LLM applications and integrations. This document covers the core architectural components and concepts.
Overview
MCP follows a client-server architecture where:
- Hosts are LLM applications (like Claude Desktop or IDEs) that initiate connections
- Clients maintain 1:1 connections with servers, inside the host application
- Servers provide context, tools, and prompts to clients
flowchart LR
subgraph "Host"
client1[MCP Client]
client2[MCP Client]
end
subgraph "Server Process"
server1[MCP Server]
end
subgraph "Server Process"
server2[MCP Server]
end
client1 <-->|Transport Layer| server1
client2 <-->|Transport Layer| server2
Core components
Protocol layer
The protocol layer handles message framing, request/response linking, and high-level communication patterns.
// Handle incoming notifications
setNotificationHandler<T>(schema: T, handler: (notification: T) => Promise<void>): void
// Send requests and await responses
request<T>(request: Request, schema: T, options?: RequestOptions): Promise<T>
// Send one-way notifications
notification(notification: Notification): Promise<void>
}
```
async def send_notification(
self,
notification: NotificationT
) -> None:
"""Send one-way notification that doesn't expect response."""
# Notification handling implementation
async def _received_request(
self,
responder: RequestResponder[ReceiveRequestT, ResultT]
) -> None:
"""Handle incoming request from other side."""
# Request handling implementation
async def _received_notification(
self,
notification: ReceiveNotificationT
) -> None:
"""Handle incoming notification from other side."""
# Notification handling implementation
```
Key classes include:
Protocol
Client
Server
Transport layer
The transport layer handles the actual communication between clients and servers. MCP supports multiple transport mechanisms:
-
Stdio transport
- Uses standard input/output for communication
- Ideal for local processes
-
HTTP with SSE transport
- Uses Server-Sent Events for server-to-client messages
- HTTP POST for client-to-server messages
All transports use JSON-RPC 2.0 to exchange messages. See the specification for detailed information about the Model Context Protocol message format.
Message types
MCP has these main types of messages:
-
Requests expect a response from the other side:
interface Request { method: string; params?: { ... }; }
-
Results are successful responses to requests:
interface Result { [key: string]: unknown; }
-
Errors indicate that a request failed:
interface Error { code: number; message: string; data?: unknown; }
-
Notifications are one-way messages that don't expect a response:
interface Notification { method: string; params?: { ... }; }
Connection lifecycle
1. Initialization
sequenceDiagram
participant Client
participant Server
Client->>Server: initialize request
Server->>Client: initialize response
Client->>Server: initialized notification
Note over Client,Server: Connection ready for use
- Client sends
initialize
request with protocol version and capabilities - Server responds with its protocol version and capabilities
- Client sends
initialized
notification as acknowledgment - Normal message exchange begins
2. Message exchange
After initialization, the following patterns are supported:
- Request-Response: Client or server sends requests, the other responds
- Notifications: Either party sends one-way messages
3. Termination
Either party can terminate the connection:
- Clean shutdown via
close()
- Transport disconnection
- Error conditions
Error handling
MCP defines these standard error codes:
enum ErrorCode {
// Standard JSON-RPC error codes
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603
}
SDKs and applications can define their own error codes above -32000.
Errors are propagated through:
- Error responses to requests
- Error events on transports
- Protocol-level error handlers
Implementation example
Here's a basic example of implementing an MCP server:
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {
resources: {}
}
});
// Handle requests
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "example://resource",
name: "Example Resource"
}
]
};
});
// Connect transport
const transport = new StdioServerTransport();
await server.connect(transport);
```
app = Server("example-server")
@app.list_resources()
async def list_resources() -> list[types.Resource]:
return [
types.Resource(
uri="example://resource",
name="Example Resource"
)
]
async def main():
async with stdio_server() as streams:
await app.run(
streams[0],
streams[1],
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
```
Best practices
Transport selection
-
Local communication
- Use stdio transport for local processes
- Efficient for same-machine communication
- Simple process management
-
Remote communication
- Use SSE for scenarios requiring HTTP compatibility
- Consider security implications including authentication and authorization
Message handling
-
Request processing
- Validate inputs thoroughly
- Use type-safe schemas
- Handle errors gracefully
- Implement timeouts
-
Progress reporting
- Use progress tokens for long operations
- Report progress incrementally
- Include total progress when known
-
Error management
- Use appropriate error codes
- Include helpful error messages
- Clean up resources on errors
Security considerations
-
Transport security
- Use TLS for remote connections
- Validate connection origins
- Implement authentication when needed
-
Message validation
- Validate all incoming messages
- Sanitize inputs
- Check message size limits
- Verify JSON-RPC format
-
Resource protection
- Implement access controls
- Validate resource paths
- Monitor resource usage
- Rate limit requests
-
Error handling
- Don't leak sensitive information
- Log security-relevant errors
- Implement proper cleanup
- Handle DoS scenarios
Debugging and monitoring
-
Logging
- Log protocol events
- Track message flow
- Monitor performance
- Record errors
-
Diagnostics
- Implement health checks
- Monitor connection state
- Track resource usage
- Profile performance
-
Testing
- Test different transports
- Verify error handling
- Check edge cases
- Load test servers