Skip to content

Improve MCP Initialize Request: Non-Compliant JSON Structure Rejected by Servers #183

@bettercallsaulj

Description

@bettercallsaulj

Issue Details

Problem Description

The MCP client's initialize request didn't comply with the MCP specification, causing spec-compliant servers to reject the connection:

  1. Wrong clientInfo structure: Client sent flat clientName and clientVersion fields, but MCP spec requires a nested clientInfo object with name and version properties
  2. Missing capabilities field: MCP spec requires a capabilities object (can be empty), but client didn't send it
  3. Metadata limitation: The Metadata type only supports flat key-value pairs, not nested objects - no way to represent nested structures

Root Cause

The initializeProtocol() function built the request params incorrectly:

// WRONG - Client code
auto init_params = make_metadata();
init_params["protocolVersion"] = config_.protocol_version;
init_params["clientName"] = config_.client_name;      // ❌ Flat field
init_params["clientVersion"] = config_.client_version; // ❌ Flat field
// ❌ Missing capabilities field

This produced:

{
  "protocolVersion": "2024-11-05",
  "clientName": "gopher-mcp",
  "clientVersion": "1.0.0"
}

But MCP spec requires:

{
  "protocolVersion": "2024-11-05",
  "clientInfo": {
    "name": "gopher-mcp",
    "version": "1.0.0"
  },
  "capabilities": {}
}

The Metadata type (std::map<std::string, MetadataValue>) only supports primitive values (string, int, double, bool, null) - not nested objects.

Technical Flow (Before Fix)

Client                                        Server (MCP-compliant)
  |                                              |
  |-- POST /rpc -------------------------------->|
  |   {                                          |
  |     "jsonrpc": "2.0",                        |
  |     "method": "initialize",                  |
  |     "params": {                              |
  |       "protocolVersion": "2024-11-05",       |
  |       "clientName": "gopher-mcp",    ❌      |
  |       "clientVersion": "1.0.0"       ❌      |
  |     },                                       |
  |     "id": 1                                  |
  |   }                                          |
  |                                              |
  |<-- HTTP 200 ---------------------------------|
  |   {                                          |
  |     "jsonrpc": "2.0",                        |
  |     "error": {                               |
  |       "code": -32602,                        |
  |       "message": "Invalid params: missing    |
  |                   required field clientInfo" |
  |     },                                       |
  |     "id": 1                                  |
  |   }                                          |
  |                                              |
  |-- Connection failed ----------------------->|  ❌

Technical Flow (After Fix)

Client                                        Server (MCP-compliant)
  |                                              |
  |-- POST /rpc -------------------------------->|
  |   {                                          |
  |     "jsonrpc": "2.0",                        |
  |     "method": "initialize",                  |
  |     "params": {                              |
  |       "protocolVersion": "2024-11-05",       |
  |       "clientInfo": {              ✅        |
  |         "name": "gopher-mcp",                |
  |         "version": "1.0.0"                   |
  |       },                                     |
  |       "capabilities": {}           ✅        |
  |     },                                       |
  |     "id": 1                                  |
  |   }                                          |
  |                                              |
  |<-- HTTP 200 ---------------------------------|
  |   {                                          |
  |     "jsonrpc": "2.0",                        |
  |     "result": {                              |
  |       "protocolVersion": "2024-11-05",       |
  |       "serverInfo": {...},                   |
  |       "capabilities": {...}                  |
  |     },                                       |
  |     "id": 1                                  |
  |   }                                          |
  |                                              |
  |-- Connection established ------------------>|  ✅

How to Reproduce

Issue 1: Initialize Request Rejected by Spec-Compliant Server

void reproduceInitializeRejection() {
  McpClient client;
  client.setUri("http://spec-compliant-server:8080/rpc");

  auto result = client.connect();
  ASSERT_TRUE(result.ok());

  // Try to initialize protocol
  auto init_future = client.initializeProtocol();
  auto init_result = init_future.get();

  // Before fix: init_result.error with "Invalid params"
  // Server expects clientInfo object, got clientName/clientVersion fields

  // After fix: init_result contains valid server capabilities
}

Issue 2: Cannot Represent Nested Objects in Metadata

void reproduceMetadataLimitation() {
  Metadata params;

  // Want to create:
  // { "clientInfo": { "name": "test", "version": "1.0" } }

  // Before fix: No way to do this!
  // Metadata only stores primitives, not nested objects
  params["clientInfo.name"] = "test";  // Wrong - creates flat key
  params["clientInfo.version"] = "1.0";

  // After fix: Store JSON string, it gets parsed during serialization
  params["clientInfo"] = "{\"name\":\"test\",\"version\":\"1.0\"}";

  JsonValue json = serialize(params);
  // json["clientInfo"] is now an object, not a string!
}

Issue 3: Missing Capabilities Field

void reproduceCapabilitiesMissing() {
  // MCP spec: "capabilities" field is REQUIRED (can be empty object)

  Metadata params;
  params["protocolVersion"] = "2024-11-05";
  params["clientInfo"] = "{\"name\":\"test\",\"version\":\"1.0\"}";
  // Missing: params["capabilities"] = "{}";

  // Before fix: No capabilities field sent
  // Server rejects: "missing required field capabilities"

  // After fix: Empty capabilities object sent
}

Minimal Reproduction Test

TEST(McpInitializeRequest, StructureMatchesSpec) {
  // Simulate what initializeProtocol() does
  std::string client_name = "gopher-mcp";
  std::string client_version = "1.0.0";
  std::string protocol_version = "2024-11-05";

  Metadata init_params;
  init_params["protocolVersion"] = protocol_version;

  // Before fix: Flat fields (WRONG)
  // init_params["clientName"] = client_name;
  // init_params["clientVersion"] = client_version;

  // After fix: Nested object via JSON string
  std::string client_info_json = "{\"name\":\"" + client_name +
                                 "\",\"version\":\"" + client_version + "\"}";
  init_params["clientInfo"] = client_info_json;
  init_params["capabilities"] = "{}";

  JsonValue json = serialize(init_params);

  // Verify MCP-compliant structure
  EXPECT_TRUE(json["clientInfo"].isObject());  // Not string!
  EXPECT_EQ(json["clientInfo"]["name"].getString(), "gopher-mcp");
  EXPECT_EQ(json["clientInfo"]["version"].getString(), "1.0.0");
  EXPECT_TRUE(json["capabilities"].isObject());
}

TEST(JsonSerialization, JsonStringBecomesNestedObject) {
  Metadata metadata;
  metadata["nested"] = "{\"key\":\"value\"}";

  JsonValue json = serialize(metadata);

  // Before fix: json["nested"] is string "{\"key\":\"value\"}"
  // After fix: json["nested"] is object {"key":"value"}
  EXPECT_TRUE(json["nested"].isObject());
  EXPECT_EQ(json["nested"]["key"].getString(), "value");
}

Key Changes in the Fix

Component Change
initializeProtocol() Replace clientName/clientVersion with nested clientInfo object
initializeProtocol() Add capabilities field (empty object {})
JsonSerializeTraits<Metadata> Detect JSON-like strings and parse them as nested structures
JSON detection Check if string starts with {/[ and ends with }/]
Fallback Invalid JSON strings gracefully serialize as regular strings

Updated initializeProtocol() Code

std::future<InitializeResult> McpClient::initializeProtocol() {
  // Build initialize request with client capabilities
  // MCP spec requires: protocolVersion, capabilities, clientInfo (nested object)
  auto init_params = make_metadata();
  init_params["protocolVersion"] = config_.protocol_version;

  // clientInfo must be a nested object with name and version
  // Store as JSON string - the serializer will parse it back to an object
  std::string client_info_json = "{\"name\":\"" + config_.client_name +
                                 "\",\"version\":\"" + config_.client_version + "\"}";
  init_params["clientInfo"] = client_info_json;

  // capabilities must be an object (can be empty)
  init_params["capabilities"] = "{}";

  // Send request...
}

JSON String Detection in Serialization

template <>
struct JsonSerializeTraits<Metadata> {
  static JsonValue serialize(const Metadata& metadata) {
    JsonObjectBuilder builder;
    for (const auto& kv : metadata) {
      match(
        kv.second,
        [&](const std::string& s) {
          // Check if string looks like JSON object or array
          if (!s.empty() && ((s.front() == '{' && s.back() == '}') ||
                             (s.front() == '[' && s.back() == ']'))) {
            try {
              auto parsed = JsonValue::parse(s);
              builder.add(kv.first, parsed);  // Add as nested structure
            } catch (...) {
              builder.add(kv.first, s);  // Invalid JSON, add as string
            }
          } else {
            builder.add(kv.first, s);  // Regular string
          }
        },
        // ... other type handlers
      );
    }
    return builder.build();
  }
};

MCP Initialize Request Comparison

Before Fix:

{
  "jsonrpc": "2.0",
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "clientName": "gopher-mcp",
    "clientVersion": "1.0.0"
  },
  "id": 1
}

After Fix (MCP-Compliant):

{
  "jsonrpc": "2.0",
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "clientInfo": {
      "name": "gopher-mcp",
      "version": "1.0.0"
    },
    "capabilities": {}
  },
  "id": 1
}

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