How to debug the Claude Model Context Protocol?

What is the Model Context Protocol?

Anthropic announced an interesting new specification this week: the Model Context Protocol (MCP). With this new specification, developers can create an MCP server that connects to the Claude app locally on their machine.

This MCP server allows users to provide local data to Claude under their own control.

For example, using the Filesystem MCP Server, you can expose parts of your local file system to Claude and ask questions about files in that directory.

To illustrate, I configured the Filesystem MCP server to access a directory in one of my TypeScript projects and was then able to ask questions about the files there.

Pretty cool right?

time to build my own MCP server

The best way to understand a new technology is to build an example.

I went to the Your First MCP Server section of the documentation and decided to follow the tutorial to build a Weather sample MCP server in typescript.

I configured Claude to access my brand new server and…

Ouch.

I opened the “View Result from get_forecast” section and I got:

The question becomes: How to I debug that thing?

Debugging the MCP protocol

setup of a MCP server

The way you configure the connection between Claude and your server is though a json configuration file at: ~/Library/Application\ Support/Claude/claude_desktop_config.json

In that file you put the command to run your MCP server, the arguments to use when starting the server and optionally the environment variables to set before the Claude application spawns the process.

For my Weather application I put a shell script as the command to run my typescript server:

{
  "mcpServers": {
    "weather": {
      "command": "/usr/local/bin/mcp_test.sh",
      "args":[]
    }
  }
}

The script was :

#!/bin/bash
cd <omitted>/claude_mcp/notion-server
node build/index.js

capturing the messages

The way that the Claude interact with a MCP server is pretty simple:

  • it follows the JSON-RPC 2.0 Specification
  • Claude sends JSON message to the standard input of the MCP process
  • Claude reads the response, and the notification from the standard output of the MCP process

In order to intercept those messages, you need to remember your Unix IPC classes from college and in particular the Named pipe aka FIFO (available on a MAC).

The script to start the server was changed to that:

#!/bin/bash
cd <omitted>/claude_mcp/notion-server
tee /tmp/mcp.fifo | node build/index.js | tee /tmp/mcp.fifo

As you can see the command is now piping 3 processes:

  • the first process is a tee command that forks its stdin to the named pipe /tmp/mcp.fifo
  • the second process is the usual process for the MCP process
  • the third process is a tee again that takes the stdout of the MCP process (though the pipe) and forks that output to the same named pipe

Using those tee command and a fifo IPC file, you can now see the message exchanged between Claude and the MCP server.

Before restarting Claude, you do first in your terminal:

mkfifo /tmp/mcp.fifo
tail -f /tmp/mcp.fifo

The tail -f command will print the data in real time being pushed to the named pipe.

Optional

Instead of starting Claude though the Finder, I started it from the command line. Why? faster to start and stop, and could see potential errors too:

/Applications/Claude.app/Contents/MacOS/Claude 2>&1 | grep -v "EGL Driver message"

The grep -v is required to filter out this EGL error that is constantly being sent to stdout. That seems to be a classic but not a problem for Electron applications.

After a restart, I asked again about the weather in San Diego and saw that messages between my test server and the Claude application.

{"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-ai","version":"0.1.0"}},"jsonrpc":"2.0","id":0}
{"result":{"protocolVersion":"2024-11-05","capabilities":{"resources":{},"tools":{}},"serverInfo":{"name":"example-weather-server","version":"0.1.0"}},"jsonrpc":"2.0","id":0}
{"method":"notifications/initialized","jsonrpc":"2.0"}
{"method":"tools/list","params":{},"jsonrpc":"2.0","id":1}
{"result":{"tools":[{"name":"get_forecast","description":"Get weather forecast for a city","inputSchema":{"type":"object","properties":{"city":{"type":"string","description":"City name"},"days":{"type":"number","description":"Number of days (1-5)","minimum":1,"maximum":5}},"required":["city"]}}]},"jsonrpc":"2.0","id":1}
{"method":"resources/list","params":{},"jsonrpc":"2.0","id":2}
{"result":{"resources":[{"uri":"weather://San Francisco/current","name":"Current weather in San Francisco","mimeType":"application/json","description":"Real-time weather data including temperature, conditions, humidity, and wind speed"}]},"jsonrpc":"2.0","id":2}
{"method":"prompts/list","params":{},"jsonrpc":"2.0","id":3}
{"jsonrpc":"2.0","id":3,"error":{"code":-32601,"message":"Method not found"}}

protocol debugging

My gut feeling was that something was wrong in the message sent by my MCP server to Claude.

I narrowed it down to that message, that show how Claude queries my server for the weather in San Diego:

{"method":"tools/call","params":{"name":"get_forecast","arguments":{"city":"San Diego","days":3}},"jsonrpc":"2.0","id":16}
{"result":{"content":{"mimeType":"application/json","text":"[\n  {\n    \"date\": \"2024-11-27\",\n    \"temperature\": 19.91,\n    \"conditions\": \"broken clouds\"\n  },\n  {\n    \"date\": \"2024-11-28\",\n    \"temperature\": 16.42,\n    \"conditions\": \"overcast clouds\"\n  },\n  {\n    \"date\": \"2024-11-29\",\n    \"temperature\": 19.23,\n    \"conditions\": \"broken clouds\"\n  }\n]"}},"jsonrpc":"2.0","id":16}

The error in Claude is still: n.content.map is not a function.

I can see that my backend is returning a content property that is an object and that could explain the reason the error: a string does not have a map method, but an array does.

To confirm my gut feeling, I added the same tooling around the Filesystem MCP server, as I know this server is working:

{"method":"tools/call","params":{"name":"read_file","arguments":{"path":"<omitted>/claude_mcp/notion-server/package.json"}},"jsonrpc":"2.0","id":443}
{"result":{"content":[{"type":"text","text":"{\n  \"name\": \"notion-server\",\n  \"version\": \"0.1.0\",\n  \"description\": \"A mcp server to query notion documents\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"bin\": {\n    \"notion-server\": \"./build/index.js\"\n  },\n  \"files\": [\n    \"build\"\n  ],\n  \"scripts\": <omitted>}]}

This confirmed that the Filesystem MCP server is returning content as an array.

The first change in the weather app was to change the content to be an array:

return {
  content: [{
    mimeType: "application/json",
    text: JSON.stringify(forecasts, null, 2)
  }]
};
{"method":"tools/call","params":{"name":"get_forecast","arguments":{"city":"San Diego","days":3}},"jsonrpc":"2.0","id":16}
{"result":{"content":[{"mimeType":"application/json","text":"[\n  {\n    \"date\": \"2024-11-27\",\n    \"temperature\": 19.91,\n    \"conditions\": \"broken clouds\"\n  },\n  {\n    \"date\": \"2024-11-28\",\n    \"temperature\": 16.42,\n    \"conditions\": \"overcast clouds\"\n  },\n  {\n    \"date\": \"2024-11-29\",\n    \"temperature\": 19.23,\n    \"conditions\": \"broken clouds\"\n  }\n]"}]},"jsonrpc":"2.0","id":16}

Claude gave me another error.

let’s stop the guess - check the spec

The documentation site has a link to the Model Control JSON RPC specification: https://spec.modelcontextprotocol.io/specification/server/tools/#tool-result

It seems that the content type is type, not mineType and that type can be either text or image

The final change was:

return {
  content: [{
    type: "text",
    text: JSON.stringify(forecasts, null, 2)
  }]
};

And this time, that works:

and the output was:

{"method":"tools/call","params":{"name":"get_forecast","arguments":{"city":"San Diego","days":3}},"jsonrpc":"2.0","id":12}
{"result":{"content":[{"type":"text","text":"[\n  {\n    \"date\": \"2024-11-27\",\n    \"temperature\": 19.31,\n    \"conditions\": \"broken clouds\"\n  },\n  {\n    \"date\": \"2024-11-28\",\n    \"temperature\": 16.42,\n    \"conditions\": \"overcast clouds\"\n  },\n  {\n    \"date\": \"2024-11-29\",\n    \"temperature\": 19.23,\n    \"conditions\": \"broken clouds\"\n  }\n]"}]},"jsonrpc":"2.0","id":12}

Conclusion

The good thing about encountering a bug like this is that it forces you to dig deeper: had the sample worked out of the box, I might have stopped my exploration right there.

Instead, I had to delve into the specification and discover that the protocol is quite impressive but easy to grasp, and the debugging process wasn’t particularly complicated either.

That gave me an idea to develop a “real” MCP server for a real problem I have daily.

Note: I suspect that most LLM/AI developers are using Python, which might explain why there is less scrutiny on the TypeScript code. I took a look at the Python samples, and the bug is not present there.

update

feel like leaving a comment?

The easiest way is to use bluesky: https://bsky.app/profile/pcarion.com/post/3lbvnqeugpk22