← Back to Articles
ArchitectureTechnicalProtocol

MCP Architecture Deep Dive: How the Protocol Works Under the Hood

A technical deep dive into MCP's architecture, including the transport layer, JSON-RPC protocol, and lifecycle management.

By Web MCP GuideFebruary 9, 20265 min read


Understanding MCP's architecture helps you build better integrations and debug issues more effectively. If you're new to MCP, we recommend starting with What is MCP? first. Let's dive deep into how the protocol actually works.

The Two-Layer Architecture

MCP consists of two distinct layers:

1. Data Layer (Inner): Defines the JSON-RPC protocol for client-server communication, including lifecycle management and core primitives (tools, resources, prompts).

2. Transport Layer (Outer): Handles the actual communication channels — how messages get from client to server and back.

The Transport Layer

MCP supports two transport mechanisms:

STDIO Transport

Standard input/output streams for local process communication.

How it works:
1. Host spawns server as a child process
2. Messages sent via process stdin
3. Responses received via process stdout
4. stderr used for logging (not protocol messages)

Advantages:

  • Zero network overhead

  • Simple process management

  • Works offline

  • Easy debugging
  • Message Format:

    Content-Length: 123\r\n
    \r\n
    {"jsonrpc":"2.0","id":1,"method":"tools/list"}

    Streamable HTTP Transport

    HTTP-based communication for remote servers.

    How it works:
    1. Client sends requests via HTTP POST
    2. Server can respond immediately or use Server-Sent Events (SSE) for streaming
    3. Supports standard HTTP authentication

    Advantages:

  • Remote server access

  • Standard authentication (OAuth, API keys)

  • Can serve multiple clients

  • Firewall-friendly
  • The Data Layer Protocol

    MCP uses JSON-RPC 2.0 for all communication. There are three message types:

    Requests

    Messages that expect a response:

    {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
    "name": "get_weather",
    "arguments": { "city": "NYC" }
    }
    }

    Responses

    Replies to requests:

    {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
    "content": [{
    "type": "text",
    "text": "Weather in NYC: 72°F, Sunny"
    }]
    }
    }

    Notifications

    One-way messages (no response expected):

    {
    "jsonrpc": "2.0",
    "method": "notifications/tools/list_changed"
    }

    Lifecycle Management

    Every MCP connection follows a specific lifecycle:

    1. Initialization

    Client sends initialize request:

    {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
    "tools": {},
    "resources": {}
    },
    "clientInfo": {
    "name": "claude-desktop",
    "version": "1.0.0"
    }
    }
    }

    Server responds with its capabilities:

    {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
    "tools": { "listChanged": true },
    "resources": { "listChanged": true },
    "prompts": {}
    },
    "serverInfo": {
    "name": "my-server",
    "version": "1.0.0"
    }
    }
    }

    2. Initialized Notification

    Client confirms initialization complete:

    {
    "jsonrpc": "2.0",
    "method": "notifications/initialized"
    }

    3. Normal Operation

    Now the connection is ready for tool calls, resource reads, etc.

    4. Shutdown

    Clean disconnection via transport-specific mechanism.

    Capability Negotiation

    During initialization, both sides declare their capabilities. This enables:

    Version Compatibility: Different protocol versions can negotiate common features.

    Feature Discovery: Clients know what the server supports before making requests.

    Graceful Degradation: Missing capabilities handled without errors.

    Common capabilities:

    {
    tools: {
    listChanged: boolean // Server will notify when tools change
    },
    resources: {
    listChanged: boolean,
    subscribe: boolean // Server supports subscriptions
    },
    prompts: {
    listChanged: boolean
    },
    logging: {}, // Server can log to client
    sampling: {}, // Client supports LLM sampling
    elicitation: {} // Client can ask user for input
    }

    Tool Invocation Flow

    Here's the complete flow when AI calls a tool:

    AI Application          MCP Client           MCP Server
    | | |
    |-- "call weather" --> | |
    | |-- tools/call -----> |
    | | |-- fetch API
    | | |<- API response
    | |<---- result ------- |
    |<-- tool result ----- | |
    | | |

    Request:

    {
    "jsonrpc": "2.0",
    "id": 42,
    "method": "tools/call",
    "params": {
    "name": "get_weather",
    "arguments": { "city": "London" }
    }
    }

    Response:

    {
    "jsonrpc": "2.0",
    "id": 42,
    "result": {
    "content": [{
    "type": "text",
    "text": "London: 18°C, Cloudy"
    }],
    "isError": false
    }
    }

    Resource Subscriptions

    For real-time data, MCP supports resource subscriptions:

    Subscribe:

    {
    "method": "resources/subscribe",
    "params": { "uri": "file:///logs/app.log" }
    }

    Update Notification:

    {
    "method": "notifications/resources/updated",
    "params": { "uri": "file:///logs/app.log" }
    }

    Error Handling

    MCP uses standard JSON-RPC error codes:

    | Code | Meaning |
    |------|---------|
    | -32700 | Parse error |
    | -32600 | Invalid request |
    | -32601 | Method not found |
    | -32602 | Invalid params |
    | -32603 | Internal error |

    Tool execution errors use the isError flag instead:

    {
    "result": {
    "content": [{ "type": "text", "text": "API timeout" }],
    "isError": true
    }
    }

    Client Primitives

    Servers can also request things from clients:

    Sampling

    Request LLM completion from the host:

    {
    "method": "sampling/createMessage",
    "params": {
    "messages": [{ "role": "user", "content": "Summarize this..." }],
    "maxTokens": 1000
    }
    }

    Elicitation

    Ask user for input:

    {
    "method": "elicitation/create",
    "params": {
    "message": "Please confirm deletion",
    "options": ["Yes", "No"]
    }
    }

    Performance Considerations

    Batching: JSON-RPC supports batching multiple requests:

    [
    {"jsonrpc":"2.0","id":1,"method":"tools/list"},
    {"jsonrpc":"2.0","id":2,"method":"resources/list"}
    ]

    Streaming: Long operations can use progress notifications:

    {
    "method": "notifications/progress",
    "params": {
    "progressToken": "task-123",
    "progress": 50,
    "total": 100
    }
    }

    Caching: Clients can cache tool/resource lists until listChanged notifications.

    Debugging Tips

    1. Inspect with MCP Inspector: npx @modelcontextprotocol/inspector your-server

    2. Log JSON-RPC messages: Add logging at the transport layer

    3. Check stderr: Servers should log debug info to stderr

    4. Validate schemas: Use the official schemas to validate messages

    Conclusion

    MCP's architecture is elegant in its simplicity — JSON-RPC over configurable transports with clear lifecycle management. Understanding these internals helps you build more robust integrations, debug issues faster, and contribute to the ecosystem.

    The two-layer design means you can focus on what matters: building great tools, resources, and prompts for AI applications. Learn more about these primitives in our Tools vs Resources vs Prompts guide, or get hands-on by building your first MCP server.