Skip to content

MCP server responses use incorrect format, causing client validation errors #199

@bettercallsaulj

Description

@bettercallsaulj

1. What's the Issue

MCP clients (such as MCP Inspector) fail to process responses from gopher-mcp servers because the response formats don't comply with the MCP protocol specification.

Three methods return incorrectly formatted responses:

Problem 1: initialize Uses Flattened Dot Notation

The initialize response used flattened keys with dot notation instead of proper nested JSON objects.

Actual (incorrect):

{
  "result": {
    "protocolVersion": "2024-11-05",
    "serverInfo.name": "my-server",
    "serverInfo.version": "1.0.0",
    "capabilities.tools": true
  }
}

Expected (correct):

{
  "result": {
    "protocolVersion": "2024-11-05",
    "serverInfo": {
      "name": "my-server",
      "version": "1.0.0"
    },
    "capabilities": {
      "tools": {}
    }
  }
}

Problem 2: tools/list Returns Bare Array

The tools/list response returned a bare array instead of wrapping it in an object with a "tools" key.

Actual (incorrect):

{
  "result": [
    {"name": "get-weather", "description": "..."}
  ]
}

Expected (correct):

{
  "result": {
    "tools": [
      {"name": "get-weather", "description": "..."}
    ]
  }
}

Problem 3: prompts/list Returns Bare Array

Same issue as tools/list - returned bare array instead of wrapped object.

Error observed in MCP Inspector:

Error [ { "code": "invalid_union", "unionErrors": [...] } ]
Invalid input

2. How to Reproduce

Prerequisites

  • Build gopher-mcp with HTTP/SSE transport enabled
  • Run an MCP server with tools and prompts registered

Steps to Reproduce

  1. Start the MCP server:

    ./mcp_example_server  # or any gopher-mcp server with HTTP/SSE transport
  2. Send an initialize request and inspect the response format:

    curl -s -X POST http://localhost:3001/mcp \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | jq .
  3. Expected: Nested serverInfo and capabilities objects

  4. Actual (before fix): Flattened serverInfo.name, capabilities.tools keys

  5. Send a tools/list request:

    curl -s -X POST http://localhost:3001/mcp \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | jq .
  6. Expected: {"result": {"tools": [...]}}

  7. Actual (before fix): {"result": [...]}

Using MCP Inspector

  1. Connect to the server via MCP Inspector
  2. Click "List Tools" or "List Prompts"
  3. Before fix: Error dialog shows "Invalid input" with validation errors

3. How to Fix It

The fix modifies three handler methods in src/server/mcp_server.cc.

Fix 1: handleInitialize - Use Nested JSON Structure

Replace the flattened Metadata builder with proper nested JsonValue construction:

jsonrpc::Response McpServer::handleInitialize(const jsonrpc::Request& request,
                                              SessionContext& session) {
  // Build initialize result as proper nested JSON structure
  // MCP protocol requires nested objects, not flattened dot notation
  json::JsonValue result_json;
  result_json["protocolVersion"] = config_.protocol_version;

  // Add serverInfo as nested object
  json::JsonValue server_info;
  server_info["name"] = config_.server_name;
  server_info["version"] = config_.server_version;
  result_json["serverInfo"] = std::move(server_info);

  // Add capabilities as nested object with empty objects for enabled caps
  json::JsonValue capabilities = json::JsonValue::object();
  if (config_.capabilities.tools.has_value() &&
      config_.capabilities.tools.value()) {
    capabilities["tools"] = json::JsonValue::object();
  }
  if (config_.capabilities.prompts.has_value() &&
      config_.capabilities.prompts.value()) {
    capabilities["prompts"] = json::JsonValue::object();
  }
  result_json["capabilities"] = std::move(capabilities);

  return jsonrpc::Response::success(request.id,
                                    jsonrpc::ResponseResult(result_json));
}

Fix 2: handleListTools - Wrap Array in Object

Wrap the tools array in an object with "tools" key:

jsonrpc::Response McpServer::handleListTools(const jsonrpc::Request& request,
                                             SessionContext& session) {
  // Get tools from tool registry
  auto result = tool_registry_->listTools();

  // Build response as JsonValue with "tools" key per MCP spec
  json::JsonValue tools_array = json::JsonValue::array();
  for (const auto& tool : result.tools) {
    tools_array.push_back(json::to_json(tool));
  }

  json::JsonValue response_obj = json::JsonValue::object();
  response_obj["tools"] = std::move(tools_array);

  return jsonrpc::Response::success(request.id,
                                    jsonrpc::ResponseResult(response_obj));
}

Fix 3: handleListPrompts - Wrap Array in Object

Same pattern as tools/list:

jsonrpc::Response McpServer::handleListPrompts(const jsonrpc::Request& request,
                                               SessionContext& session) {
  auto result = prompt_registry_->listPrompts(cursor);

  // Build response as JsonValue with "prompts" key per MCP spec
  json::JsonValue prompts_array = json::JsonValue::array();
  for (const auto& prompt : result.prompts) {
    prompts_array.push_back(json::to_json(prompt));
  }

  json::JsonValue response_obj = json::JsonValue::object();
  response_obj["prompts"] = std::move(prompts_array);

  return jsonrpc::Response::success(request.id,
                                    jsonrpc::ResponseResult(response_obj));
}

Verification

After the fix:

# Initialize returns nested structure
$ curl -s -X POST http://localhost:3001/mcp \
    -H "Content-Type: application/json" \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}' | jq .result
{
  "protocolVersion": "2024-11-05",
  "serverInfo": {
    "name": "my-server",
    "version": "1.0.0"
  },
  "capabilities": {
    "tools": {},
    "prompts": {}
  }
}

# tools/list returns wrapped array
$ curl -s -X POST http://localhost:3001/mcp \
    -H "Content-Type: application/json" \
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | jq .result
{
  "tools": [
    {"name": "get-weather", "description": "..."}
  ]
}

MCP Inspector can now successfully parse and display tools and prompts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions