-
Notifications
You must be signed in to change notification settings - Fork 7
Open
Description
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:
- Wrong
clientInfostructure: Client sent flatclientNameandclientVersionfields, but MCP spec requires a nestedclientInfoobject withnameandversionproperties - Missing
capabilitiesfield: MCP spec requires acapabilitiesobject (can be empty), but client didn't send it - Metadata limitation: The
Metadatatype 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 fieldThis 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
}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels