Back to articles

February 3, 2026

Understanding MCP: How Servers and Clients Work Under the Hood

Understanding MCP: How Servers and Clients Work Under the Hood

The Model Context Protocol (MCP) is revolutionizing how AI applications interact with external systems and data sources. While many developers are using MCP servers and clients, understanding how they work under the hood can help you build more robust integrations and debug issues more effectively. Let's dive deep into the architecture and mechanics of MCP.

What is MCP?

MCP is an open protocol developed by Anthropic that standardizes how AI models interact with external data sources and tools. Think of it as a universal adapter that allows AI applications to securely connect to databases, APIs, file systems, and other resources in a consistent way.

The protocol defines a clear separation between:

  • MCP Hosts: The applications that want AI capabilities (like Claude Desktop, IDEs, or custom AI applications)
  • MCP Clients: The components within hosts that initiate connections
  • MCP Servers: Services that expose resources, tools, and prompts to clients

The Client-Server Architecture

The Handshake Process

When an MCP client connects to a server, they go through a structured initialization process:

  1. Transport Layer Establishment: The client and server establish a communication channel. MCP supports two primary transport mechanisms:

    • Standard I/O (stdio): The server runs as a subprocess, with communication happening over stdin/stdout
    • Server-Sent Events (SSE): HTTP-based communication for remote servers
  2. Protocol Negotiation: Both parties exchange their supported protocol versions and capabilities. This ensures compatibility and allows for graceful degradation if versions don't match perfectly.

  3. Capability Exchange: The client declares what it can handle (like support for sampling, roots, etc.), and the server announces what it provides (tools, resources, prompts).

Here's what this looks like in practice:

// Client sends initialization request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "roots": {
        "listChanged": true
      },
      "sampling": {}
    },
    "clientInfo": {
      "name": "ExampleClient",
      "version": "1.0.0"
    }
  }
}

// Server responds with its capabilities
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": {},
      "resources": {
        "subscribe": true,
        "listChanged": true
      },
      "prompts": {}
    },
    "serverInfo": {
      "name": "ExampleServer",
      "version": "1.0.0"
    }
  }
}

The Message Flow

MCP uses JSON-RPC 2.0 as its messaging format. Every interaction follows a request-response pattern or notification pattern:

Request-Response: The client sends a request with a unique ID, and the server responds with the same ID.

Notifications: One-way messages that don't expect a response (like progress updates or logging).

Core Primitives

Resources

Resources represent any data that an MCP server wants to expose. They're identified by URIs and can be anything from files to database queries to API responses.

When a client wants to access a resource:

// Client requests a resource
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "resources/read",
  "params": {
    "uri": "file:///project/README.md"
  }
}

// Server responds with the content
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "contents": [
      {
        "uri": "file:///project/README.md",
        "mimeType": "text/markdown",
        "text": "# My Project\n\nThis is a sample project..."
      }
    ]
  }
}

Resources can also support subscriptions, allowing clients to be notified when resource content changes.

Tools

Tools are functions that the server exposes for the AI model to call. The server defines the tool schema, and the client (on behalf of the AI) can invoke these tools with parameters.

The flow works like this:

  1. Discovery: Client requests the list of available tools
  2. Schema Understanding: Client receives JSON Schema definitions for each tool
  3. Invocation: When the AI decides to use a tool, the client sends a tool call request
  4. Execution: Server executes the tool and returns results
// Tool definition
{
  "name": "search_database",
  "description": "Search the product database",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "Search query"
      },
      "limit": {
        "type": "number",
        "description": "Maximum results"
      }
    },
    "required": ["query"]
  }
}

// Tool invocation
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "search_database",
    "arguments": {
      "query": "wireless headphones",
      "limit": 10
    }
  }
}

Prompts

Prompts are reusable templates that help structure interactions with the AI. They can include dynamic arguments and are useful for creating consistent experiences.

Transport Mechanisms Deep Dive

Standard I/O (stdio)

The stdio transport is elegant in its simplicity:

  1. The client spawns the server as a subprocess
  2. JSON-RPC messages are written to the server's stdin
  3. Responses come back through stdout
  4. Errors and logs go to stderr

This approach has several advantages:

  • Simplicity: No network configuration needed
  • Security: Process isolation provides a security boundary
  • Local-first: Perfect for desktop applications and CLI tools

Here's how you might implement this in Node.js:

import { spawn } from 'child_process';

const server = spawn('node', ['mcp-server.js']);

// Send request
const request = {
  jsonrpc: '2.0',
  id: 1,
  method: 'initialize',
  params: { /* ... */ }
};
server.stdin.write(JSON.stringify(request) + '\n');

// Receive response
server.stdout.on('data', (data) => {
  const response = JSON.parse(data.toString());
  console.log('Received:', response);
});

Server-Sent Events (SSE)

For remote servers or web-based applications, MCP uses SSE over HTTP:

  1. Client establishes an SSE connection to receive server messages
  2. Client sends requests via POST to a separate endpoint
  3. Server pushes responses back through the SSE connection

This enables:

  • Remote servers: Connect to MCP servers running anywhere
  • Web compatibility: Works in browser environments
  • Scalability: Server can handle multiple client connections

State Management and Lifecycle

MCP servers and clients must carefully manage state:

Server Lifecycle

  1. Initialization: Server starts and prepares its resources
  2. Ready State: After successful initialization handshake
  3. Active: Serving requests, sending notifications
  4. Shutdown: Graceful cleanup of resources

Client Responsibilities

  • Connection Management: Handling reconnections and timeouts
  • Request Queuing: Managing concurrent requests
  • State Synchronization: Keeping track of server capabilities
  • Resource Cleanup: Properly closing connections

Security Considerations

MCP includes several security features:

Transport Security:

  • stdio provides process isolation
  • SSE can use HTTPS and authentication headers

Capability Declaration: Servers explicitly declare what they can do, preventing unauthorized access

URI Validation: Servers should validate resource URIs to prevent path traversal attacks

Input Sanitization: Tool parameters should be validated against their schemas

Building Your Own MCP Server

Here's a minimal example of an MCP server structure:

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

// Create server instance
const server = new Server(
  {
    name: 'my-mcp-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      resources: {},
      tools: {},
    },
  }
);

// Register a tool
server.setRequestHandler('tools/list', async () => ({
  tools: [
    {
      name: 'hello',
      description: 'Say hello',
      inputSchema: {
        type: 'object',
        properties: {},
      },
    },
  ],
}));

server.setRequestHandler('tools/call', async (request) => {
  if (request.params.name === 'hello') {
    return {
      content: [
        {
          type: 'text',
          text: 'Hello from MCP server!',
        },
      ],
    };
  }
  throw new Error('Unknown tool');
});

// Start server with stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);

Debugging and Monitoring

Understanding the internals helps with debugging:

Logging: Both clients and servers should log all JSON-RPC messages in development

Inspector Tools: Use MCP Inspector to visually debug server interactions

Error Handling: Pay attention to JSON-RPC error codes and messages

Performance: Monitor request latency and implement timeouts

Real-World Use Cases

Understanding these internals enables powerful use cases:

  • Database Integration: Expose databases as MCP resources with query tools
  • API Gateways: Wrap REST APIs as MCP tools for AI access
  • File Systems: Provide secure, scoped access to file systems
  • Custom Workflows: Build domain-specific tools for your AI applications

Conclusion

MCP's architecture is both simple and powerful. The clean separation between clients and servers, standardized message format, and flexible transport options make it an excellent choice for building AI integrations.

By understanding how the protocol works under the hood from the initial handshake to resource access and tool invocation, you can build more robust integrations, debug issues more effectively, and create innovative applications that leverage AI capabilities.

Whether you're building a simple file system server or a complex multi-service integration, MCP provides the foundation for secure, scalable, and maintainable AI-powered applications.

Resources

Happy building!

MCPAIArchitectureProtocolDevelopmentTutorial