Learning how MCP works by reading logs - and building MCP Interceptor

I’ve used MCP servers here and there (e.g. filesystem, Git, Puppeteer, docs, etc.) and I’ve built some of my own using Cloudflare’s Agent SDK. It’s sort of magical, it just works and all you have to do to use it is use Anthropic’s MCP SDK. But I wanted to understand how it works at the protocol level, so I built MCP Interceptor: https://mcp-interceptor.appsinprogress.com/

MCP Interceptor is the result of some of my own digging around to learn how MCP works and make it easier for others to access the underlying protocol. I’ll explain all about it. But before we jump into it, some context on model context protocol if you aren’t familiar with it yet (jump to Enter MCP Interceptor section if you’re already familiar with MCP).

Short recap of MCP

The model context protocol (MCP) was proposed by Anthropic as an open, standard protocol that makes it easier for LLMs like Claude and Cursor to interact with external tools. And it’s been adopted by many other tools since.

What problem does MCP solve

When building agents, you want to connect your LLMs to tools to perform actions. This is done via function calling, a process in a function’s signature is passed as context to the LLM so the LLM can decide to execute the function. But what if you aren’t the one building the agent? What if you want your tool to be accessible to generic agents like Claude Desktop? That’s what MCP is for. By exposing the list of functions that your server supports in a standard way, agents that support the MCP protocol can immediately use any MCP server that they gain access to, without requiring any code changes.

Learning how MCP works by looking at logs

When trying to understand how MCP works, you’ll notice that the Model Context Protocol website provides a lot of information explaining how clients and servers interact with one another, how the SDKs work, and the different types of transports. The MCP docs have evolved a lot over the past few months and have now become a great resource to learn about MCP.

But personally, the best way to truly understand how things work for me is to unwrap all of the layers of abstraction and the SDKs and get to the core of what the messages being sent back and forth are between an MCP client and an MCP server.

So I started by looking at MCP server logs locally with Claude. You can quite easily do that by spinning up one of the sample local MCP servers (like the filesystem) and then following the debugging steps for Claude Desktop.

That helped me understand how clients like Claude actually connect to MCP servers. In the logs, I could see the following:

Screenshot 2025-08-20 at 10.38.33 PM.pngSpecifically, you can see through the logs the following key messages:

  1. {“method”:“initialize”,“params”: {…} ,“jsonrpc”:“2.0”,“id”:0}: The first message that a client needs to send to a server to connect to the server.

  2. {“method”:“notifications/initialized”,“jsonrpc”:“2.0”}: The confirmation message from the server that the client is properly connected.

  3. {“method”:“tools/list”,“params”:{},“jsonrpc”:“2.0”,“id”:2}: The message that the client sends in order to learn about the tools that the MCP server provides.

  4. {“jsonrpc”:“2.0”,“id”:2,“result”:{“tools”:[{“name”:“puppeteer_navigate”,“description”:“Navigate to a URL”,“inputSchema”:{“type”:“object”,“properties”:{“url”:{“type”:“string”}},“required”:[“url”]}}…]}}: The list of tools that the server exposes to the MCP client. Notice that the result provides the full schema for parameters that the client will need to include for the tool call.

  5. { “jsonrpc”: “2.0”, “id”: 3, “method”: “tools/call”, “params”: { “name”: “puppeteer_navigate”, “arguments”: { “url”: “https://example.com” } } } (not in screenshot): The actual call, from the client to the server invoking the puppeteer_navigate method and receiving the results of the navigation.

By observing the logs, we can quickly understand that what the MCP “data layer”, the schema of the MCP messages, actually is: simple remote procedure calls, communicated using JSON and following a specific structure.

You can actually see a similar explanation by example in the MCP docs. But seeing it in actual application logs confirms that this is how it actually works in practice.

Enter MCP Interceptor - a remote MCP proxy

Digging into the logs for each MCP server I was trying out was tedious. And I wanted to have an easier way to visualize the MCP messages of a live conversation, as the conversation was happening, without the MCP client hiding the exact JSON-RPC messages being sent back & forth. So I built MCP Interceptor.

Screenshot 2025-08-20 at 11.01.06 PM.pngMCP Interceptor is a simple web application that acts as a proxy to a remote MCP server (that uses the Streamable HTTP Transport). Once you configure the remote MCP server you want to use, you will receive a proxy URL. You can then use this proxy URL to configure your MCP client.

Screenshot 2025-08-20 at 11.03.50 PM.pngFor instance, you can configure Claude Code to use MCP Interceptor claude mcp add --transport http demo-weather <MCP-Interceptor-Proxy-Endpoint-Here> . Then, when you load up Claude Code, you’ll observe that Claude Code will immediately connect to the MCP server and run the tools/list command to learn what tools the MCP server provides.

Try it out yourself! https://mcp-interceptor.appsinprogress.com/ You can even test it with the sample random weather MCP server I built as part of this process.

What started out as an attempt to peek behind the curtain of the MCP SDKs ended up turning into a cool little side project that uses Cloudflare Durable Objects and Workers under the hood to simplify visualizing MCP logs in real-time (the code is available here). I think it’s pretty cool to see the logs show up in real time. There is no magic here: it’s LLMs generating the parameters to call functions, with MCP acting as the standard protocol that makes it easy for new tools to be added to clients on the fly.