DEV Community

Mano Nagarajan
Mano Nagarajan

Posted on

Understanding MCP Message Structure and Data Flow

Understanding MCP Message Structure and Data Flow

Hey there! If you've been diving into the Model Context Protocol (MCP) lately, you might have wondered how messages actually flow between clients and servers. I know I did when I first started exploring this fascinating protocol. Let me walk you through what I've learned about MCP's message structure and data flow in a way that (hopefully) makes sense.

What's MCP All About?

Before we jump into the nitty-gritty of messages, let's get on the same page. The Model Context Protocol is like a universal translator between AI applications and the services they need to interact with. Think of it as a standardized way for your AI assistant to talk to file systems, databases, APIs, or pretty much anything else.

The beauty of MCP? It's built on JSON-RPC 2.0, which means if you've worked with JSON-RPC before, you're already halfway there.

The Three Pillars of MCP Messages

MCP messages come in three flavors, and each one serves a specific purpose:

1. Requests - "Hey, Can You Do This?"

Requests are how one side asks the other to do something. When a client wants a server to perform an action, it sends a request. Every request has:

  • A unique ID - So responses don't get mixed up
  • A method name - What action needs to happen
  • Parameters - The details needed to complete the action

Here's what a typical request looks like:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "read_file",
    "arguments": {
      "path": "/home/user/document.txt"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Pretty straightforward, right? The client is basically saying, "Hey server, can you call the read_file tool with this path?"

2. Responses - "Here's What You Asked For"

Every request deserves an answer. Responses match up with requests using that ID we talked about. They can contain either:

  • A result - When everything went smoothly
  • An error - When something went wrong

Success response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": "File contents here..."
  }
}
Enter fullscreen mode Exit fullscreen mode

Error response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32600,
    "message": "File not found"
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Notifications - "Just FYI"

Notifications are the fire-and-forget messages of MCP. They don't have an ID because nobody's waiting for a response. Think of them as announcements:

{
  "jsonrpc": "2.0",
  "method": "notifications/progress",
  "params": {
    "progressToken": "task-123",
    "progress": 50,
    "total": 100
  }
}
Enter fullscreen mode Exit fullscreen mode

How Data Actually Flows

Now that we know the message types, let's see how they move through the system. The flow is surprisingly elegant once you understand it.

The Connection Dance

When a client and server first meet, they go through an initialization handshake. It's like two people introducing themselves:

  1. Client initiates: "Hi, I'm a client running version X with these capabilities..."
  2. Server responds: "Nice to meet you! I'm a server with these tools and resources..."

This handshake ensures both sides know what to expect from each other.

The Request-Response Cycle

Here's where the real magic happens. Let's walk through a complete cycle:

Step 1: Client Sends Request

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "resources/read",
  "params": {
    "uri": "file:///data/config.json"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Server Processes
The server receives this, validates the request, checks permissions, reads the file, and prepares a response.

Step 3: Server Sends Response

{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "contents": [
      {
        "uri": "file:///data/config.json",
        "mimeType": "application/json",
        "text": "{\"setting\": \"value\"}"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The ID matching ensures the client knows exactly which request this response is for.

Bidirectional Communication

Here's something cool: MCP isn't just client asking and server answering. Servers can make requests to clients too! This is super useful for things like:

  • Asking for user confirmation
  • Requesting additional permissions
  • Sampling from the AI model

So the data flow is truly bidirectional. Both sides can initiate conversations.

Message Structure Deep Dive

Let's break down what actually goes into these messages at a deeper level.

The Envelope

Every MCP message shares a common envelope:

  • jsonrpc: Always "2.0" (it's the protocol version)
  • id: Present for requests and responses, absent for notifications
  • method: The action to perform (requests and notifications)
  • params: Additional data needed for the method
  • result or error: The outcome (responses only)

Method Naming Convention

MCP uses a logical namespace structure for methods:

  • tools/* - Tool-related operations
  • resources/* - Resource management
  • prompts/* - Prompt handling
  • notifications/* - System notifications
  • completion/* - Auto-completion features

This makes it easy to understand what category a method falls into just by looking at its name.

Parameter Structures

Parameters vary by method, but they're always objects. Some common patterns:

For tool calls:

{
  "name": "tool_name",
  "arguments": {
    "param1": "value1"
  }
}
Enter fullscreen mode Exit fullscreen mode

For resource reads:

{
  "uri": "scheme://path/to/resource"
}
Enter fullscreen mode Exit fullscreen mode

For prompts:

{
  "name": "prompt_name",
  "arguments": {
    "key": "value"
  }
}
Enter fullscreen mode Exit fullscreen mode

Error Handling: When Things Go Wrong

Not everything always works perfectly (shocking, I know). MCP has a solid error handling system based on JSON-RPC error codes:

  • -32700: Parse error - The JSON is malformed
  • -32600: Invalid request - Something's wrong with the request structure
  • -32601: Method not found - The server doesn't know that method
  • -32602: Invalid params - The parameters aren't right
  • -32603: Internal error - Something went wrong on the server side

Custom application errors start at -32000.

Practical Example: Reading a File

Let's put it all together with a real-world example. Say you want to read a file through MCP:

1. Client sends request:

{
  "jsonrpc": "2.0",
  "id": 100,
  "method": "resources/read",
  "params": {
    "uri": "file:///home/user/notes.txt"
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Server processes and responds:

{
  "jsonrpc": "2.0",
  "id": 100,
  "result": {
    "contents": [
      {
        "uri": "file:///home/user/notes.txt",
        "mimeType": "text/plain",
        "text": "Remember to buy milk!"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Meanwhile, server sends progress notification:

{
  "jsonrpc": "2.0",
  "method": "notifications/progress",
  "params": {
    "progressToken": "read-100",
    "progress": 100,
    "total": 100
  }
}
Enter fullscreen mode Exit fullscreen mode

See how the request and response IDs match? That's crucial for keeping track of which response goes with which request, especially when multiple requests are in flight.

Transport Layer Considerations

While we've focused on the message structure, it's worth mentioning that MCP messages need a way to travel between client and server. Common transport mechanisms include:

  • Standard input/output (stdio) - For local processes
  • HTTP with Server-Sent Events - For web-based implementations
  • WebSockets - For real-time bidirectional communication

The transport layer handles the physical delivery, but the message structure remains the same regardless of how the messages travel.

Best Practices I've Learned

Through working with MCP, here are some tips that have saved me headaches:

  1. Always validate IDs: Make sure response IDs match request IDs before processing
  2. Handle errors gracefully: Don't just log errors; provide meaningful feedback
  3. Use appropriate message types: If you don't need a response, use a notification
  4. Keep payloads reasonable: Massive JSON objects can slow things down
  5. Implement timeouts: Don't wait forever for responses that might never come

Wrapping Up

Understanding MCP's message structure and data flow is like learning the grammar of a new language. Once you get the patterns, everything starts to make sense. The protocol's use of JSON-RPC 2.0 gives us a solid foundation, while the three message types (requests, responses, and notifications) provide flexibility for different communication patterns.

The bidirectional nature of MCP is particularly powerful, allowing rich interactions between clients and servers. Whether you're building an AI assistant that needs to access files, query databases, or call APIs, MCP provides a consistent way to structure these interactions.

I hope this breakdown helps you understand how data flows through MCP systems. The protocol might seem complex at first, but once you see how the pieces fit together, it's actually quite elegant. Now go build something awesome with it!


Have you worked with MCP? What aspects of the message structure did you find most interesting or challenging? I'd love to hear about your experiences in the comments!

Top comments (0)