Skip to content

ResponseResult variant cannot represent nested JSON structures #200

@bettercallsaulj

Description

@bettercallsaulj

1. What's the Issue

The ResponseResult variant type in gopher-mcp cannot represent arbitrary nested JSON structures, which are required by the MCP protocol for responses like initialize.

The ResponseResult variant only supported these types:

  • Primitive types: nullptr_t, bool, int, double, std::string
  • MCP types: Metadata, std::vector<ContentBlock>, std::vector<Tool>, std::vector<Prompt>, std::vector<Resource>, ListResourcesResult, ListToolsResult

None of these types can represent the nested object structure required by MCP protocol responses.

Example: Initialize response requires nested objects:

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

The Metadata type only supports flat key-value pairs and cannot represent nested objects like serverInfo or capabilities.

Problem:

  • Server code cannot construct proper MCP-compliant responses
  • Workarounds using flattened dot notation ("serverInfo.name") don't comply with the protocol
  • No way to return dynamically structured JSON responses

2. How to Reproduce

Prerequisites

  • gopher-mcp codebase before this fix

Steps to Reproduce

  1. Attempt to create a nested JSON response using existing types:
// This doesn't work - Metadata is flat key-value only
auto builder = make<Metadata>()
    .add("protocolVersion", "2024-11-05")
    .add("serverInfo.name", "my-server")  // Flattened, not nested!
    .add("serverInfo.version", "1.0.0");  // Wrong format

auto response = jsonrpc::Response::success(
    request.id, jsonrpc::ResponseResult(builder.build()));
  1. Expected: Ability to create properly nested JSON structures
  2. Actual: Only flat key-value pairs possible, resulting in:
{
  "result": {
    "serverInfo.name": "my-server",
    "serverInfo.version": "1.0.0"
  }
}

Compilation Error Example

Attempting to use JsonValue in ResponseResult before the fix:

json::JsonValue nested;
nested["serverInfo"]["name"] = "my-server";

// This fails to compile - JsonValue not in variant
auto response = jsonrpc::Response::success(
    request.id, jsonrpc::ResponseResult(nested));

Error:

error: no matching constructor for initialization of 'jsonrpc::ResponseResult'
note: candidate constructor not viable: no known conversion from 'json::JsonValue' to ...

3. How to Fix It

The fix adds json::JsonValue to the ResponseResult variant and adds the corresponding serialization handler.

Fix 1: Add JsonValue to ResponseResult Variant

In include/mcp/types.h, add json::JsonValue to the variant:

// Generic result type for responses
// json::JsonValue added to support arbitrary nested JSON responses (e.g., initialize)
using ResponseResult = variant<std::nullptr_t,
                               bool,
                               int,
                               double,
                               std::string,
                               Metadata,
                               std::vector<ContentBlock>,
                               std::vector<Tool>,
                               std::vector<Prompt>,
                               std::vector<Resource>,
                               ListResourcesResult,
                               ListToolsResult,
                               json::JsonValue>;  // Added for nested JSON

Fix 2: Add JsonValue Serialization Handler

In src/json/json_serialization.cc, add a handler for JsonValue in the serialize_ResponseResult function:

JsonValue serialize_ResponseResult(const jsonrpc::ResponseResult& result) {
  JsonValue json_result;

  visit(overloaded{
      // ... existing handlers ...

      [&json_result](const ListToolsResult& list_result) {
        json_result = to_json(list_result);
      },
      // NEW: Direct JsonValue passthrough
      [&json_result](const JsonValue& json_val) {
        // Direct JsonValue passthrough for arbitrary nested JSON responses
        json_result = json_val;
      }},
      result);

  return json_result;
}

Verification

After the fix, nested JSON structures can be created:

// Now works - create proper nested structure
json::JsonValue result_json;
result_json["protocolVersion"] = "2024-11-05";

json::JsonValue server_info;
server_info["name"] = "my-server";
server_info["version"] = "1.0.0";
result_json["serverInfo"] = std::move(server_info);

json::JsonValue capabilities = json::JsonValue::object();
capabilities["tools"] = json::JsonValue::object();
result_json["capabilities"] = std::move(capabilities);

// This now compiles and works correctly
auto response = jsonrpc::Response::success(
    request.id, jsonrpc::ResponseResult(result_json));

Output:

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

This enables MCP protocol compliance for all responses requiring nested JSON structures.

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