From 485ef016eb70dddf5bd14fcc473b12429bccb601 Mon Sep 17 00:00:00 2001 From: ksgfk <1537100607@qq.com> Date: Fri, 15 May 2026 14:17:21 +0800 Subject: [PATCH] Negotiate MCP protocol version from client initialize request --- src/main.cpp | 1 - src/mcp/mcp_server.cpp | 68 +++++++++++---- src/mcp/mcp_server.h | 18 +++- tests/integration/test_protocol.cpp | 35 ++++---- tests/integration/test_workflow.cpp | 4 +- tests/unit/test_mcp_server.cpp | 123 +++++++++++++++++----------- 6 files changed, 160 insertions(+), 89 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 3b066bb..3fcefd7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -78,7 +78,6 @@ int main(int argc, char* argv[]) json response; if(msg.is_array()) { - // JSON-RPC batch if(msg.empty()) { json errorResp; diff --git a/src/mcp/mcp_server.cpp b/src/mcp/mcp_server.cpp index d707ca6..ed0d53b 100644 --- a/src/mcp/mcp_server.cpp +++ b/src/mcp/mcp_server.cpp @@ -50,7 +50,7 @@ json McpServer::makeError(const json& id, int code, const std::string& message) return resp; } -json McpServer::makeToolResult(const json& data, bool isError) +json McpServer::makeToolResult(const json& data, bool isError) const { json result; json content; @@ -62,11 +62,39 @@ json McpServer::makeToolResult(const json& data, bool isError) content["text"] = data.dump(); result["content"] = json::array({content}); + if(m_protocolVersion == ProtocolVersion::V2025_06_18 && !isError && data.is_object()) + result["structuredContent"] = data; if(isError) result["isError"] = true; return result; } +ProtocolVersion McpServer::negotiateProtocolVersion(const json& params) +{ + if(params.contains("protocolVersion")) + { + const auto clientVersion = params["protocolVersion"].get(); + if(clientVersion == kProtocolVersion2025_03_26) + return ProtocolVersion::V2025_03_26; + if(clientVersion == kProtocolVersion2025_06_18) + return ProtocolVersion::V2025_06_18; + } + + return ProtocolVersion::V2025_06_18; +} + +const char* McpServer::protocolVersionString(ProtocolVersion version) +{ + switch(version) + { + case ProtocolVersion::V2025_03_26: + return kProtocolVersion2025_03_26; + case ProtocolVersion::V2025_06_18: + return kProtocolVersion2025_06_18; + } + return kProtocolVersion; +} + // ── Message dispatch ──────────────────────────────────────────────────────── json McpServer::handleMessage(const json& msg) @@ -80,9 +108,13 @@ json McpServer::handleMessage(const json& msg) json id = msg.value("id", json(nullptr)); // Route methods - if(method == "initialize") + if(method == "ping") { - if(m_initialized) + return makeResponse(id, json::object()); + } + else if(method == "initialize") + { + if(m_initializeSeen) return makeError(id, -32600, "Server already initialized"); return handleInitialize(msg); } @@ -112,11 +144,10 @@ json McpServer::handleMessage(const json& msg) json McpServer::handleBatch(const json& arr) { - // Check for initialize in batch (forbidden by MCP spec) - for(const auto& msg : arr) + if(!m_initializeSeen || m_protocolVersion == ProtocolVersion::V2025_06_18) { - if(msg.is_object() && msg.value("method", "") == "initialize") - return makeError(nullptr, -32600, "Invalid Request: initialize must not appear in a JSON-RPC batch"); + return makeError(nullptr, -32600, + "Invalid Request: JSON-RPC batching is not supported by the negotiated MCP protocol"); } json responses = json::array(); @@ -127,12 +158,12 @@ json McpServer::handleBatch(const json& arr) responses.push_back(makeError(nullptr, -32600, "Invalid Request: batch element is not an object")); continue; } + json resp = handleMessage(msg); if(!resp.is_null()) responses.push_back(resp); } - // If all were notifications, return nothing if(responses.empty()) return nullptr; @@ -145,24 +176,25 @@ json McpServer::handleInitialize(const json& msg) { json id = msg.value("id", json(nullptr)); - // Validate client protocol version for compatibility - static constexpr const char* kSupportedProtocolVersion = "2025-03-26"; json params = msg.value("params", json::object()); - if (params.contains("protocolVersion")) { - std::string clientVersion = params["protocolVersion"].get(); - if (clientVersion != kSupportedProtocolVersion) { - return makeError(id, -32602, - "Unsupported protocol version: " + clientVersion + - " (server supports " + kSupportedProtocolVersion + ")"); - } + if(!params.is_object()) + { + return makeError(id, -32602, "Invalid params: initialize params must be an object"); } + if(params.contains("protocolVersion") && !params["protocolVersion"].is_string()) + { + return makeError(id, -32602, "Invalid params: protocolVersion must be a string"); + } + + m_protocolVersion = negotiateProtocolVersion(params); json result; - result["protocolVersion"] = kSupportedProtocolVersion; + result["protocolVersion"] = protocolVersionString(m_protocolVersion); result["capabilities"]["tools"] = json::object(); result["serverInfo"]["name"] = "renderdoc-mcp"; result["serverInfo"]["version"] = "1.0.0"; + m_initializeSeen = true; return makeResponse(id, result); } diff --git a/src/mcp/mcp_server.h b/src/mcp/mcp_server.h index 1638696..b397907 100644 --- a/src/mcp/mcp_server.h +++ b/src/mcp/mcp_server.h @@ -9,6 +9,16 @@ namespace renderdoc::core { class DiffSession; } namespace renderdoc::mcp { +inline constexpr const char* kProtocolVersion2025_03_26 = "2025-03-26"; +inline constexpr const char* kProtocolVersion2025_06_18 = "2025-06-18"; +inline constexpr const char* kProtocolVersion = kProtocolVersion2025_06_18; + +enum class ProtocolVersion +{ + V2025_03_26, + V2025_06_18, +}; + class McpServer { public: @@ -21,7 +31,7 @@ class McpServer // Process a single JSON-RPC message. Returns response JSON, or nullptr for notifications. nlohmann::json handleMessage(const nlohmann::json& msg); - // Process a JSON-RPC batch (array). Returns response array. + // Process a JSON-RPC batch. Supported only after negotiating a batch-capable protocol. nlohmann::json handleBatch(const nlohmann::json& arr); void shutdown(); @@ -35,7 +45,9 @@ class McpServer // JSON-RPC response helpers static nlohmann::json makeResponse(const nlohmann::json& id, const nlohmann::json& result); static nlohmann::json makeError(const nlohmann::json& id, int code, const std::string& message); - static nlohmann::json makeToolResult(const nlohmann::json& data, bool isError = false); + nlohmann::json makeToolResult(const nlohmann::json& data, bool isError = false) const; + static ProtocolVersion negotiateProtocolVersion(const nlohmann::json& params); + static const char* protocolVersionString(ProtocolVersion version); std::unique_ptr m_ownedSession; // owned, only set by default ctor core::Session* m_session = nullptr; // always valid (points to owned or injected) @@ -43,6 +55,8 @@ class McpServer core::DiffSession* m_diffSession = nullptr; // always valid (points to owned or injected) std::unique_ptr m_ownedRegistry; // owned, only set by default ctor ToolRegistry* m_registry = nullptr; // always valid (points to owned or injected) + ProtocolVersion m_protocolVersion = ProtocolVersion::V2025_06_18; + bool m_initializeSeen = false; bool m_initialized = false; }; diff --git a/tests/integration/test_protocol.cpp b/tests/integration/test_protocol.cpp index b5b49a2..010818f 100644 --- a/tests/integration/test_protocol.cpp +++ b/tests/integration/test_protocol.cpp @@ -5,6 +5,8 @@ #include #include +#include "mcp/mcp_server.h" + #ifdef _WIN32 #include #endif @@ -250,7 +252,7 @@ class ProtocolTest : public ::testing::Test { initReq["jsonrpc"] = "2.0"; initReq["id"] = 0; initReq["method"] = "initialize"; - initReq["params"]["protocolVersion"] = "2025-03-26"; + initReq["params"]["protocolVersion"] = renderdoc::mcp::kProtocolVersion; initReq["params"]["clientInfo"]["name"] = "test-runner"; initReq["params"]["clientInfo"]["version"] = "1.0.0"; initReq["params"]["capabilities"] = json::object(); @@ -326,6 +328,7 @@ TEST_F(ProtocolTest, InitializeHandshake) auto& result = s_initResponse["result"]; EXPECT_TRUE(result.contains("protocolVersion")); + EXPECT_EQ(result["protocolVersion"], renderdoc::mcp::kProtocolVersion); EXPECT_TRUE(result.contains("serverInfo")); EXPECT_TRUE(result["serverInfo"].contains("name")); EXPECT_EQ(result["serverInfo"]["name"], "renderdoc-mcp"); @@ -408,7 +411,17 @@ TEST_F(ProtocolTest, MethodNotFound_UnknownMethod) EXPECT_EQ((*resp)["error"]["code"].get(), -32601); } -TEST_F(ProtocolTest, BatchRequest_ArrayResponse) +TEST_F(ProtocolTest, Ping_ReturnsEmptyResult) +{ + auto req = makeRequest("ping"); + auto resp = send(req); + ASSERT_TRUE(resp.has_value()); + ASSERT_TRUE(resp->contains("result")); + EXPECT_TRUE((*resp)["result"].is_object()); + EXPECT_TRUE((*resp)["result"].empty()); +} + +TEST_F(ProtocolTest, BatchRequest_Rejected) { json batch = json::array(); batch.push_back(makeRequest("tools/list", json::object(), 1)); @@ -416,22 +429,8 @@ TEST_F(ProtocolTest, BatchRequest_ArrayResponse) auto resp = send(batch); ASSERT_TRUE(resp.has_value()); - ASSERT_TRUE(resp->is_array()); - EXPECT_EQ(resp->size(), 2u); - - // Find responses by id - bool foundToolsList = false; - bool foundMethodNotFound = false; - for (auto& r : *resp) { - if (r.contains("id")) { - if (r["id"] == 1 && r.contains("result")) - foundToolsList = true; - if (r["id"] == 2 && r.contains("error") && r["error"]["code"] == -32601) - foundMethodNotFound = true; - } - } - EXPECT_TRUE(foundToolsList) << "Batch should contain tools/list result"; - EXPECT_TRUE(foundMethodNotFound) << "Batch should contain method-not-found error"; + ASSERT_TRUE(resp->contains("error")); + EXPECT_EQ((*resp)["error"]["code"].get(), -32600); } TEST_F(ProtocolTest, ProcessStable_MultipleRequests) diff --git a/tests/integration/test_workflow.cpp b/tests/integration/test_workflow.cpp index 8c578d4..acf63cc 100644 --- a/tests/integration/test_workflow.cpp +++ b/tests/integration/test_workflow.cpp @@ -5,6 +5,8 @@ #include #include +#include "mcp/mcp_server.h" + #ifdef _WIN32 #include #endif @@ -218,7 +220,7 @@ class WorkflowTest : public ::testing::Test { initReq["jsonrpc"] = "2.0"; initReq["id"] = 0; initReq["method"] = "initialize"; - initReq["params"]["protocolVersion"] = "2025-03-26"; + initReq["params"]["protocolVersion"] = renderdoc::mcp::kProtocolVersion; initReq["params"]["clientInfo"]["name"] = "test-runner"; initReq["params"]["clientInfo"]["version"] = "1.0.0"; initReq["params"]["capabilities"] = json::object(); diff --git a/tests/unit/test_mcp_server.cpp b/tests/unit/test_mcp_server.cpp index ce7b95b..17d6242 100644 --- a/tests/unit/test_mcp_server.cpp +++ b/tests/unit/test_mcp_server.cpp @@ -54,6 +54,15 @@ class McpServerTest : public ::testing::Test { m_server->handleMessage(notif); } + void doInitializeWithProtocol(const char* protocolVersion) { + m_server->handleMessage(makeRequest("initialize", + {{"protocolVersion", protocolVersion}})); + json notif; + notif["jsonrpc"] = "2.0"; + notif["method"] = "notifications/initialized"; + m_server->handleMessage(notif); + } + renderdoc::core::Session m_session; renderdoc::core::DiffSession m_diffSession; ToolRegistry m_registry; @@ -65,7 +74,7 @@ TEST_F(McpServerTest, Initialize_ReturnsServerInfo) auto resp = m_server->handleMessage(makeRequest("initialize")); ASSERT_TRUE(resp.contains("result")); EXPECT_EQ(resp["result"]["serverInfo"]["name"], "renderdoc-mcp"); - EXPECT_EQ(resp["result"]["protocolVersion"], "2025-03-26"); + EXPECT_EQ(resp["result"]["protocolVersion"], kProtocolVersion); } TEST_F(McpServerTest, Initialize_HasToolsCapability) @@ -139,6 +148,14 @@ TEST_F(McpServerTest, UnknownMethod_ReturnsMethodNotFound) EXPECT_EQ(resp["error"]["code"], -32601); } +TEST_F(McpServerTest, Ping_ReturnsEmptyResult) +{ + auto resp = m_server->handleMessage(makeRequest("ping")); + ASSERT_TRUE(resp.contains("result")); + EXPECT_TRUE(resp["result"].is_object()); + EXPECT_TRUE(resp["result"].empty()); +} + TEST_F(McpServerTest, InvalidParams_MissingToolName_Returns32602) { doInitialize(); @@ -148,29 +165,6 @@ TEST_F(McpServerTest, InvalidParams_MissingToolName_Returns32602) EXPECT_EQ(resp["error"]["code"], -32602); } -TEST_F(McpServerTest, BatchRequest_ReturnsBatchResponse) -{ - doInitialize(); - json batch = json::array({ - makeRequest("tools/list", json::object(), 1), - makeRequest("tools/list", json::object(), 2) - }); - auto resp = m_server->handleBatch(batch); - ASSERT_TRUE(resp.is_array()); - EXPECT_EQ(resp.size(), 2u); -} - -TEST_F(McpServerTest, BatchWithInitialize_Rejected) -{ - json batch = json::array({ - makeRequest("initialize", json::object(), 1), - makeRequest("tools/list", json::object(), 2) - }); - auto resp = m_server->handleBatch(batch); - ASSERT_TRUE(resp.contains("error")); - EXPECT_EQ(resp["error"]["code"], -32600); -} - TEST_F(McpServerTest, ToolsCall_BeforeInitialize_ReturnsNotInitialized) { auto resp = m_server->handleMessage(makeRequest("tools/call", @@ -200,26 +194,6 @@ TEST_F(McpServerTest, ToolsList_AfterInitialize_Succeeds) EXPECT_TRUE(resp["result"].contains("tools")); } -TEST_F(McpServerTest, BatchWithNonObjectElement_ReturnsError) -{ - json batch = json::array({42, "bad"}); - auto resp = m_server->handleBatch(batch); - ASSERT_TRUE(resp.is_array()); - EXPECT_EQ(resp.size(), 2u); - EXPECT_EQ(resp[0]["error"]["code"], -32600); - EXPECT_EQ(resp[1]["error"]["code"], -32600); -} - -TEST_F(McpServerTest, BatchAllNotifications_ReturnsNull) -{ - json notif; - notif["jsonrpc"] = "2.0"; - notif["method"] = "notifications/initialized"; - json batch = json::array({notif}); - auto resp = m_server->handleBatch(batch); - EXPECT_TRUE(resp.is_null()); -} - TEST_F(McpServerTest, Shutdown_ReturnsEmptyResult) { doInitialize(); @@ -228,20 +202,71 @@ TEST_F(McpServerTest, Shutdown_ReturnsEmptyResult) EXPECT_TRUE(resp["result"].is_object()); } -TEST_F(McpServerTest, Initialize_UnsupportedProtocolVersion_ReturnsError) +TEST_F(McpServerTest, Initialize_ClientProtocolMismatch_ReturnsServerProtocol) { auto resp = m_server->handleMessage(makeRequest("initialize", {{"protocolVersion", "9999-01-01"}})); - ASSERT_TRUE(resp.contains("error")); - EXPECT_EQ(resp["error"]["code"], -32602); + ASSERT_TRUE(resp.contains("result")); + EXPECT_EQ(resp["result"]["protocolVersion"], kProtocolVersion); } TEST_F(McpServerTest, Initialize_MatchingProtocolVersion_Succeeds) { auto resp = m_server->handleMessage(makeRequest("initialize", - {{"protocolVersion", "2025-03-26"}})); + {{"protocolVersion", kProtocolVersion}})); + ASSERT_TRUE(resp.contains("result")); + EXPECT_EQ(resp["result"]["protocolVersion"], kProtocolVersion); +} + +TEST_F(McpServerTest, Initialize_ClientRequestsOldProtocol_ReturnsOldProtocol) +{ + auto resp = m_server->handleMessage(makeRequest("initialize", + {{"protocolVersion", kProtocolVersion2025_03_26}})); + ASSERT_TRUE(resp.contains("result")); + EXPECT_EQ(resp["result"]["protocolVersion"], kProtocolVersion2025_03_26); +} + +TEST_F(McpServerTest, ToolsCall_ValidTool_IncludesStructuredContent) +{ + doInitializeWithProtocol(kProtocolVersion2025_06_18); + auto resp = m_server->handleMessage(makeRequest("tools/call", + {{"name", "echo_tool"}, {"arguments", {{"msg", "hello"}}}})); + ASSERT_TRUE(resp.contains("result")); + ASSERT_TRUE(resp["result"].contains("structuredContent")); + EXPECT_EQ(resp["result"]["structuredContent"]["echo"], "hello"); +} + +TEST_F(McpServerTest, ToolsCall_OldProtocol_OmitsStructuredContent) +{ + doInitializeWithProtocol(kProtocolVersion2025_03_26); + auto resp = m_server->handleMessage(makeRequest("tools/call", + {{"name", "echo_tool"}, {"arguments", {{"msg", "hello"}}}})); ASSERT_TRUE(resp.contains("result")); - EXPECT_EQ(resp["result"]["protocolVersion"], "2025-03-26"); + EXPECT_FALSE(resp["result"].contains("structuredContent")); +} + +TEST_F(McpServerTest, BatchRequest_OldProtocol_ReturnsBatchResponse) +{ + doInitializeWithProtocol(kProtocolVersion2025_03_26); + json batch = json::array({ + makeRequest("tools/list", json::object(), 1), + makeRequest("tools/list", json::object(), 2) + }); + auto resp = m_server->handleBatch(batch); + ASSERT_TRUE(resp.is_array()); + EXPECT_EQ(resp.size(), 2u); +} + +TEST_F(McpServerTest, BatchRequest_NewProtocol_ReturnsError) +{ + doInitializeWithProtocol(kProtocolVersion2025_06_18); + json batch = json::array({ + makeRequest("tools/list", json::object(), 1), + makeRequest("tools/list", json::object(), 2) + }); + auto resp = m_server->handleBatch(batch); + ASSERT_TRUE(resp.contains("error")); + EXPECT_EQ(resp["error"]["code"], -32600); } TEST_F(McpServerTest, Initialize_DoubleInitialize_ReturnsError)