From 3ea45ac560997e850b82da570591223fd046f75e Mon Sep 17 00:00:00 2001 From: haojiubudaqiu <262876729@qq.com> Date: Thu, 30 Apr 2026 21:06:22 +0800 Subject: [PATCH 1/5] feat: add support for MCP Prompts primitive Implement the missing Prompts capability in the MCP framework. Prompts are highly necessary as they act as a lightweight 'Skills' system, allowing developers to register predefined workflows and instructions directly into the MCP server. This significantly enhances the usefulness and extensibility of the framework for Agent integrations. Included comprehensive unit tests and examples. --- examples/server_example.cpp | 23 ++++++++ include/mcp_prompt.h | 85 ++++++++++++++++++++++++++++ include/mcp_server.h | 10 ++++ src/CMakeLists.txt | 4 ++ src/mcp_server.cpp | 48 ++++++++++++++++ test/mcp_test.cpp | 110 ++++++++++++++++++++++++++++++++++++ 6 files changed, 280 insertions(+) create mode 100644 include/mcp_prompt.h diff --git a/examples/server_example.cpp b/examples/server_example.cpp index b81fc78..0818909 100644 --- a/examples/server_example.cpp +++ b/examples/server_example.cpp @@ -170,6 +170,29 @@ int main() { server.register_tool(calc_tool, calculator_handler); server.register_tool(hello_tool, hello_handler); + // Register prompt + mcp::prompt hello_prompt = mcp::prompt_builder("hello_prompt") + .with_description("A prompt to generate a greeting") + .with_argument("name", "The name to greet", true) + .build(); + + server.register_prompt(hello_prompt, [](const mcp::json& args, const std::string& session_id) -> mcp::json { + std::string name = "World"; + if (args.contains("name")) { + name = args["name"].get(); + } + + mcp::json message = { + {"role", "user"}, + {"content", { + {"type", "text"}, + {"text", "Please greet " + name + " in a friendly way."} + }} + }; + + return mcp::json::array({message}); + }); + // // Register resources // auto file_resource = std::make_shared("./Makefile"); // server.register_resource("file://./Makefile", file_resource); diff --git a/include/mcp_prompt.h b/include/mcp_prompt.h new file mode 100644 index 0000000..efc45d6 --- /dev/null +++ b/include/mcp_prompt.h @@ -0,0 +1,85 @@ +/** + * @file mcp_prompt.h + * @brief Prompt definitions for MCP + * + * This file provides prompt-related functionality and abstractions for the MCP protocol. + */ + +#ifndef MCP_PROMPT_H +#define MCP_PROMPT_H + +#include "mcp_message.h" +#include +#include +#include + +namespace mcp { + +// MCP Prompt Argument definition +struct prompt_argument { + std::string name; + std::string description; + bool required = false; + + json to_json() const { + json j = { + {"name", name}, + {"description", description}, + {"required", required} + }; + return j; + } +}; + +// MCP Prompt definition +struct prompt { + std::string name; + std::string description; + std::vector arguments; + + json to_json() const { + json args_json = json::array(); + for (const auto& arg : arguments) { + args_json.push_back(arg.to_json()); + } + + json j = { + {"name", name}, + {"description", description} + }; + + if (!args_json.empty()) { + j["arguments"] = args_json; + } + + return j; + } +}; + +class prompt_builder { +public: + prompt_builder(const std::string& name) { + prompt_.name = name; + } + + prompt_builder& with_description(const std::string& desc) { + prompt_.description = desc; + return *this; + } + + prompt_builder& with_argument(const std::string& name, const std::string& desc, bool required = false) { + prompt_.arguments.push_back({name, desc, required}); + return *this; + } + + prompt build() const { + return prompt_; + } + +private: + prompt prompt_; +}; + +} // namespace mcp + +#endif // MCP_PROMPT_H \ No newline at end of file diff --git a/include/mcp_server.h b/include/mcp_server.h index 6e42d09..55718d5 100644 --- a/include/mcp_server.h +++ b/include/mcp_server.h @@ -12,6 +12,7 @@ #include "mcp_message.h" #include "mcp_resource.h" #include "mcp_tool.h" +#include "mcp_prompt.h" #include "mcp_thread_pool.h" #include "mcp_logger.h" @@ -36,6 +37,7 @@ namespace mcp { using method_handler = std::function; using tool_handler = method_handler; +using prompt_handler = method_handler; using notification_handler = std::function; using auth_handler = std::function; using session_cleanup_handler = std::function; @@ -325,6 +327,13 @@ class server { */ void register_tool(const tool& tool, tool_handler handler); + /** + * @brief Register a prompt + * @param prompt The prompt to register + * @param handler The function to call when the prompt is invoked + */ + void register_prompt(const prompt& prompt, prompt_handler handler); + /** * @brief Register a session cleanup handler * @param key Tool or resource name to be cleaned up @@ -423,6 +432,7 @@ class server { // Tools map (name -> handler) std::map> tools_; + std::map> prompts_; // Authentication handler auth_handler auth_handler_; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8af47e8..d6d2e2d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,10 @@ target_compile_definitions(${TARGET} PUBLIC target_link_libraries(${TARGET} PUBLIC ${CMAKE_THREAD_LIBS_INIT}) +if(WIN32) + target_link_libraries(${TARGET} PUBLIC ws2_32 wsock32) +endif() + # If OpenSSL is found, link the OpenSSL libraries if(OPENSSL_FOUND) target_link_libraries(${TARGET} PUBLIC ${OPENSSL_LIBRARIES}) diff --git a/src/mcp_server.cpp b/src/mcp_server.cpp index 4907fd4..58e3836 100644 --- a/src/mcp_server.cpp +++ b/src/mcp_server.cpp @@ -551,6 +551,54 @@ void server::register_tool(const tool& tool, tool_handler handler) { } } +void server::register_prompt(const prompt& prompt, prompt_handler handler) { + std::lock_guard lock(mutex_); + prompts_[prompt.name] = std::make_pair(prompt, handler); + + // Register methods for prompt listing and calling + if (method_handlers_.find("prompts/list") == method_handlers_.end()) { + method_handlers_["prompts/list"] = [this](const json& params, const std::string& session_id) -> json { + json prompts_json = json::array(); + for (const auto& [name, prompt_pair] : prompts_) { + prompts_json.push_back(prompt_pair.first.to_json()); + } + return json{{"prompts", prompts_json}}; + }; + } + + if (method_handlers_.find("prompts/get") == method_handlers_.end()) { + method_handlers_["prompts/get"] = [this](const json& params, const std::string& session_id) -> json { + if (!params.contains("name")) { + throw mcp_exception(error_code::invalid_params, "Missing 'name' parameter"); + } + + std::string prompt_name = params["name"]; + auto it = prompts_.find(prompt_name); + if (it == prompts_.end()) { + throw mcp_exception(error_code::invalid_params, "Prompt not found: " + prompt_name); + } + + json prompt_args = params.contains("arguments") ? params["arguments"] : json::object(); + + json handler_result = it->second.second(prompt_args, session_id); + + // Expected to return a GetPromptResult structure: {"description": "...", "messages": [...]} + // If it returns just an array, we can auto-wrap it into messages + if (handler_result.is_array()) { + json wrapped_result = { + {"messages", handler_result} + }; + if (!it->second.first.description.empty()) { + wrapped_result["description"] = it->second.first.description; + } + return wrapped_result; + } + + return handler_result; + }; + } +} + void server::register_session_cleanup(const std::string& key, session_cleanup_handler handler) { std::lock_guard lock(mutex_); session_cleanup_handler_[key] = handler; diff --git a/test/mcp_test.cpp b/test/mcp_test.cpp index f9dcd33..62de268 100644 --- a/test/mcp_test.cpp +++ b/test/mcp_test.cpp @@ -11,6 +11,7 @@ #include "mcp_client.h" #include "mcp_server.h" #include "mcp_tool.h" +#include "mcp_prompt.h" #include "mcp_sse_client.h" using namespace mcp; @@ -662,6 +663,114 @@ TEST_F(ToolsTest, CallTool) { EXPECT_EQ(tool_result["content"][0]["text"], "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"); } +class PromptsEnvironment : public ::testing::Environment { +public: + void SetUp() override { + // Set up test environment + server::configuration conf = {.host = "localhost", .port = 8084}; + server_ = std::make_unique(conf); + + // Create a test prompt + prompt test_prompt = prompt_builder("test_prompt") + .with_description("A test prompt") + .with_argument("name", "The user name", true) + .build(); + + // Register prompt + server_->register_prompt(test_prompt, [](const json& params, const std::string& /* session_id */) -> json { + std::string name = "World"; + if (params.contains("name")) { + name = params["name"].get(); + } + + return json::array({ + { + {"role", "user"}, + {"content", { + {"type", "text"}, + {"text", "Hello, " + name + "!"} + }} + } + }); + }); + + // Start server in background thread + server_thread_ = std::thread([this]() { + server_->start(); + }); + + // Wait for server to start + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Connect client + client_ = std::make_unique("http://localhost:8084"); + bool initialized = client_->initialize("TestClient", "1.0"); + EXPECT_TRUE(initialized); + } + + void TearDown() override { + if (client_) { + client_.reset(); + } + if (server_) { + server_->stop(); + } + if (server_thread_.joinable()) { + server_thread_.join(); + } + } + + static std::shared_ptr& GetClient() { + return client_; + } + +private: + std::unique_ptr server_; + std::thread server_thread_; + static std::shared_ptr client_; +}; + +std::shared_ptr PromptsEnvironment::client_ = nullptr; + +class PromptsTest : public ::testing::Test { +protected: + void SetUp() override { + client_ = PromptsEnvironment::GetClient().get(); + } + + sse_client* client_; +}; + +// Test listing prompts +TEST_F(PromptsTest, ListPrompts) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Call list prompts method directly + json prompts_list = client_->send_request("prompts/list").result; + + // Verify prompts list + EXPECT_TRUE(prompts_list.contains("prompts")); + EXPECT_EQ(prompts_list["prompts"].size(), 1); + EXPECT_EQ(prompts_list["prompts"][0]["name"], "test_prompt"); + EXPECT_EQ(prompts_list["prompts"][0]["description"], "A test prompt"); + EXPECT_TRUE(prompts_list["prompts"][0].contains("arguments")); + EXPECT_EQ(prompts_list["prompts"][0]["arguments"][0]["name"], "name"); +} + +// Test getting prompt +TEST_F(PromptsTest, GetPrompt) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Get prompt + json prompt_result = client_->send_request("prompts/get", {{"name", "test_prompt"}, {"arguments", {{"name", "Alice"}}}}).result; + + // Verify prompt result + EXPECT_TRUE(prompt_result.contains("messages")); + EXPECT_EQ(prompt_result["messages"].size(), 1); + EXPECT_EQ(prompt_result["messages"][0]["role"], "user"); + EXPECT_EQ(prompt_result["messages"][0]["content"]["text"], "Hello, Alice!"); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); @@ -670,6 +779,7 @@ int main(int argc, char **argv) { ::testing::AddGlobalTestEnvironment(new VersioningEnvironment()); ::testing::AddGlobalTestEnvironment(new PingEnvironment()); ::testing::AddGlobalTestEnvironment(new ToolsEnvironment()); + ::testing::AddGlobalTestEnvironment(new PromptsEnvironment()); return RUN_ALL_TESTS(); } \ No newline at end of file From cfdc1fc8d5c0e0106079a3544b00df801d090d83 Mon Sep 17 00:00:00 2001 From: haojiubudaqiu <262876729@qq.com> Date: Fri, 1 May 2026 13:28:25 +0800 Subject: [PATCH 2/5] docs: add Prompts feature documentation to README --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 897e9ec..75af5e2 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - **JSON-RPC 2.0 Communication**: Request/response communication based on JSON-RPC 2.0 standard - **Resource Abstraction**: Standard interfaces for resources such as files, APIs, etc. - **Tool Registration**: Register and call tools with structured parameters +- **Prompt Templates**: Register and expose prompt templates to clients for AI workflows - **Extensible Architecture**: Easy to extend with new resource types and tools - **Multi-Transport Support**: Supports HTTP and standard input/output (stdio) communication methods @@ -85,7 +86,7 @@ Example MCP server implementation with custom tools: Example MCP client connecting to a server: - Get server information -- List available tools +- List available tools and prompts - Call tools with parameters - Access resources @@ -156,6 +157,26 @@ server.register_tool(hello_tool, hello_handler); auto file_resource = std::make_shared(""); server.register_resource("file://", file_resource); +// Register prompts +mcp::prompt draft_prompt = mcp::prompt_builder("draft_article") + .with_description("Draft a new article") + .with_argument("topic", "The main topic to write about", true) + .build(); + +server.register_prompt(draft_prompt, [](const mcp::json& args, const std::string /* session_id */) -> mcp::json { + std::string topic = args.value("topic", "Unknown"); + std::string instruction = "Please write an article about " + topic; + + // Return messages array as specified in the MCP protocol + return mcp::json::array({{ + {"role", "user"}, + {"content", { + {"type", "text"}, + {"text", instruction} + }} + }}); +}); + // Start the server server.start(true); // Blocking mode ``` From 43563b7ccdea47a957a3c7197a6f92f4622d2b11 Mon Sep 17 00:00:00 2001 From: haojiubudaqiu <262876729@qq.com> Date: Sat, 2 May 2026 13:53:46 +0800 Subject: [PATCH 3/5] feat: implement start_stdio() for server-side standard I/O transport Prior to this commit, the C++ MCP server implementation only supported HTTP/SSE transport via `server::start()`, despite the README claiming stdio support. Clients expecting to communicate with the server via standard input/output (like Cursor, OpenCode, Claude Desktop) were unable to establish a connection. This commit introduces `server::start_stdio()`, a blocking read loop that: - Reads JSON-RPC messages line-by-line from `std::cin`. - Processes valid requests using the existing `process_request` pipeline. - Flushes responses directly to `std::cout` to prevent pipeline deadlocks. - Includes robust error handling to respond with standard JSON-RPC error formats if parsing fails. --- include/mcp_server.h | 11 ++++++-- src/mcp_server.cpp | 61 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/include/mcp_server.h b/include/mcp_server.h index 55718d5..e5339cb 100644 --- a/include/mcp_server.h +++ b/include/mcp_server.h @@ -243,12 +243,19 @@ class server { ~server(); /** - * @brief Start the server - * @param blocking If true, this call blocks until the server stops + * @brief Start the server (HTTP/SSE) + * @param blocking Whether to block the current thread * @return True if the server started successfully */ bool start(bool blocking = true); + /** + * @brief Start the server using stdio transport + * Reads JSON-RPC messages from stdin and writes responses to stdout. + * Blocks the current thread until stdin is closed. + */ + void start_stdio(); + /** * @brief Stop the server */ diff --git a/src/mcp_server.cpp b/src/mcp_server.cpp index 58e3836..0d8cf85 100644 --- a/src/mcp_server.cpp +++ b/src/mcp_server.cpp @@ -55,6 +55,67 @@ server::~server() { stop(); } +void server::start_stdio() { + running_ = true; + std::string line; + std::string session_id = "stdio_session_" + std::to_string(std::time(nullptr)); + + while (std::getline(std::cin, line)) { + if (line.empty()) continue; + try { + json req_json = json::parse(line); + request req; + + if (req_json.is_object() && req_json.contains("jsonrpc") && req_json["jsonrpc"] == "2.0") { + req.jsonrpc = "2.0"; + if (req_json.contains("id")) { + req.id = req_json["id"]; + } else { + req.id = nullptr; + } + req.method = req_json.value("method", ""); + if (req_json.contains("params")) { + req.params = req_json["params"]; + } + + json res = process_request(req, session_id); + if (!res.is_null() && !req.id.is_null()) { + std::cout << res.dump() << "\n" << std::flush; + } else { + std::cerr << "Response is null or ID is null. Method: " << req.method << std::endl; + if (!res.is_null()) { + std::cout << res.dump() << "\n" << std::flush; + } + } + } else { + json err_res = { + {"jsonrpc", "2.0"}, + {"error", { + {"code", static_cast(error_code::invalid_request)}, + {"message", "Invalid JSON-RPC format"} + }} + }; + if (req_json.contains("id")) { + err_res["id"] = req_json["id"]; + } else { + err_res["id"] = nullptr; + } + std::cout << err_res.dump() << "\n" << std::flush; + } + } catch (const std::exception& e) { + json err_res = { + {"jsonrpc", "2.0"}, + {"error", { + {"code", static_cast(error_code::parse_error)}, + {"message", std::string("Parse error: ") + e.what()} + }}, + {"id", nullptr} + }; + std::cout << err_res.dump() << "\n" << std::flush; + } + } + running_ = false; +} bool server::start(bool blocking) { if (running_) { From bcf2aa0ee57e43aea0a0868820f3e66a224daccd Mon Sep 17 00:00:00 2001 From: haojiubudaqiu <262876729@qq.com> Date: Sat, 2 May 2026 14:48:08 +0800 Subject: [PATCH 4/5] docs: add stdio_server_example.cpp and update examples This commit addresses the lack of a proper standard I/O (stdio) server example by introducing `examples/stdio_server_example.cpp`. This example demonstrates how to create a pipeline-based MCP server using the newly added `start_stdio()` method, fully mirroring the tools and logic in `server_example.cpp`. Additionally, the existing `server_example.cpp` was updated to explicitly declare `prompts` capabilities to match its actual implementation. --- README.md | 7 +- examples/CMakeLists.txt | 7 +- examples/server_example.cpp | 3 +- examples/stdio_server_example.cpp | 203 ++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 examples/stdio_server_example.cpp diff --git a/README.md b/README.md index 75af5e2..116a1f1 100644 --- a/README.md +++ b/README.md @@ -76,12 +76,17 @@ Implements MCP server functionality. ### HTTP Server Example (`examples/server_example.cpp`) -Example MCP server implementation with custom tools: +Example MCP server implementation over HTTP/SSE with custom tools: - Time tool: Get the current time - Calculator tool: Perform mathematical operations - Echo tool: Echo input with optional transformations (to uppercase, reverse) - Greeting tool: Returns `Hello, `+ input name + `!`, defaults to `Hello, World!` +### Stdio Server Example (`examples/stdio_server_example.cpp`) + +Example MCP server implementation over standard input/output (stdio), which is the default transport method for MCP clients like Claude Desktop, Cursor, and OpenCode. +It implements the same core tools but uses `server.start_stdio()` to communicate via pipes instead of opening an HTTP port. + ### HTTP Client Example (`examples/client_example.cpp`) Example MCP client connecting to a server: diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index fa215e6..7608d19 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -29,4 +29,9 @@ target_link_libraries(${TARGET} PRIVATE mcp) target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/include) if(OPENSSL_FOUND) target_link_libraries(${TARGET} PRIVATE ${OPENSSL_LIBRARIES}) -endif() \ No newline at end of file +endif() + +set(TARGET stdio_server_example) +add_executable(${TARGET} stdio_server_example.cpp) +target_link_libraries(${TARGET} PRIVATE mcp) +target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/include) diff --git a/examples/server_example.cpp b/examples/server_example.cpp index 0818909..11ad575 100644 --- a/examples/server_example.cpp +++ b/examples/server_example.cpp @@ -137,7 +137,8 @@ int main() { // {"resources", {{"subscribe", false}, {"listChanged", true}}} // }; mcp::json capabilities = { - {"tools", mcp::json::object()} + {"tools", mcp::json::object()}, + {"prompts", mcp::json::object()} }; server.set_capabilities(capabilities); diff --git a/examples/stdio_server_example.cpp b/examples/stdio_server_example.cpp new file mode 100644 index 0000000..ddebc9f --- /dev/null +++ b/examples/stdio_server_example.cpp @@ -0,0 +1,203 @@ +/** + * @file stdio_server_example.cpp + * @brief Server example based on MCP protocol using Standard I/O transport + * + * This example demonstrates how to create an MCP server that communicates + * over standard input (stdin) and standard output (stdout). + * Follows the 2024-11-05 basic protocol specification. + */ +#include "mcp_server.h" +#include "mcp_tool.h" +#include "mcp_resource.h" + +#include +#include +#include +#include +#include +#include + +// Tool handler for getting current time +mcp::json get_time_handler(const mcp::json& params, const std::string& /* session_id */) { + auto now = std::chrono::system_clock::now(); + auto time_t_now = std::chrono::system_clock::to_time_t(now); + + std::string time_str = std::ctime(&time_t_now); + // Remove trailing newline + if (!time_str.empty() && time_str[time_str.length() - 1] == '\n') { + time_str.erase(time_str.length() - 1); + } + + return { + { + {"type", "text"}, + {"text", time_str} + } + }; +} + +// Echo tool handler +mcp::json echo_handler(const mcp::json& params, const std::string& /* session_id */) { + mcp::json result = params; + + if (params.contains("text")) { + std::string text = params["text"]; + + if (params.contains("uppercase") && params["uppercase"].get()) { + std::transform(text.begin(), text.end(), text.begin(), ::toupper); + result["text"] = text; + } + + if (params.contains("reverse") && params["reverse"].get()) { + std::reverse(text.begin(), text.end()); + result["text"] = text; + } + } + + return { + { + {"type", "text"}, + {"text", result["text"].get()} + } + }; +} + +// Calculator tool handler +mcp::json calculator_handler(const mcp::json& params, const std::string& /* session_id */) { + if (!params.contains("operation")) { + throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'operation' parameter"); + } + + std::string operation = params["operation"]; + double result = 0.0; + + if (operation == "add") { + if (!params.contains("a") || !params.contains("b")) { + throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); + } + result = params["a"].get() + params["b"].get(); + } else if (operation == "subtract") { + if (!params.contains("a") || !params.contains("b")) { + throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); + } + result = params["a"].get() - params["b"].get(); + } else if (operation == "multiply") { + if (!params.contains("a") || !params.contains("b")) { + throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); + } + result = params["a"].get() * params["b"].get(); + } else if (operation == "divide") { + if (!params.contains("a") || !params.contains("b")) { + throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); + } + if (params["b"].get() == 0.0) { + throw mcp::mcp_exception(mcp::error_code::invalid_params, "Division by zero not allowed"); + } + result = params["a"].get() / params["b"].get(); + } else { + throw mcp::mcp_exception(mcp::error_code::invalid_params, "Unknown operation: " + operation); + } + + return { + { + {"type", "text"}, + {"text", std::to_string(result)} + } + }; +} + +// Custom API endpoint handler +mcp::json hello_handler(const mcp::json& params, const std::string& /* session_id */) { + std::string name = params.contains("name") ? params["name"].get() : "World"; + return { + { + {"type", "text"}, + {"text", "Hello, " + name + "!"} + } + }; +} + +int main() { + // Ensure file directory exists + std::filesystem::create_directories("./files"); + + // Create and configure server + mcp::server::configuration srv_conf; + // Note: host and port are ignored in stdio mode + + mcp::server server(srv_conf); + server.set_server_info("StdioExampleServer", "1.0.0"); + + // Set server capabilities + mcp::json capabilities = { + {"tools", mcp::json::object()}, + {"prompts", mcp::json::object()} + }; + server.set_capabilities(capabilities); + + // Register tools + mcp::tool time_tool = mcp::tool_builder("get_time") + .with_description("Get current time") + .build(); + + mcp::tool echo_tool = mcp::tool_builder("echo") + .with_description("Echo input with optional transformations") + .with_string_param("text", "Text to echo") + .with_boolean_param("uppercase", "Convert to uppercase", false) + .with_boolean_param("reverse", "Reverse the text", false) + .build(); + + mcp::tool calc_tool = mcp::tool_builder("calculator") + .with_description("Perform basic calculations") + .with_string_param("operation", "Operation to perform (add, subtract, multiply, divide)") + .with_number_param("a", "First operand") + .with_number_param("b", "Second operand") + .build(); + + mcp::tool hello_tool = mcp::tool_builder("hello") + .with_description("Say hello") + .with_string_param("name", "Name to say hello to", "World") + .build(); + + server.register_tool(time_tool, get_time_handler); + server.register_tool(echo_tool, echo_handler); + server.register_tool(calc_tool, calculator_handler); + server.register_tool(hello_tool, hello_handler); + + // Register prompt + mcp::prompt hello_prompt = mcp::prompt_builder("hello_prompt") + .with_description("A prompt to generate a greeting") + .with_argument("name", "The name to greet", true) + .build(); + + server.register_prompt(hello_prompt, [](const mcp::json& args, const std::string& session_id) -> mcp::json { + std::string name = "World"; + if (args.contains("name")) { + name = args["name"].get(); + } + + mcp::json message = { + {"role", "user"}, + {"content", { + {"type", "text"}, + {"text", "Please greet " + name + " in a friendly way."} + }} + }; + + return mcp::json::array({message}); + }); + + // // Register resources + // auto file_resource = std::make_shared("./Makefile"); + // server.register_resource("file://./Makefile", file_resource); + + // Start server + // CRITICAL: We use std::cerr here to output logs, because any output to std::cout + // will corrupt the JSON-RPC pipe and crash the MCP client! + std::cerr << "Starting MCP server in STDIO mode..." << std::endl; + std::cerr << "Awaiting JSON-RPC requests on stdin..." << std::endl; + + server.start_stdio(); + + return 0; +} \ No newline at end of file From 4557fbd438b2f76c43df66444f87689738f4cd46 Mon Sep 17 00:00:00 2001 From: haojiubudaqiu <262876729@qq.com> Date: Sat, 2 May 2026 16:36:25 +0800 Subject: [PATCH 5/5] test: add unit tests for start_stdio() stdio transport Verify that the new start_stdio() method correctly processes JSON-RPC requests from stdin and writes responses to stdout, including: - Initialize request (id=0) handling - Tool call (id=1) processing and result verification - Proper handling of notifications/initialized (no stdout output) All 13 tests pass (1 pre-existing VersioningTest.UnsupportedVersion crash is unrelated to this change). --- test/mcp_test.cpp | 98 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/test/mcp_test.cpp b/test/mcp_test.cpp index 62de268..72df65a 100644 --- a/test/mcp_test.cpp +++ b/test/mcp_test.cpp @@ -14,6 +14,9 @@ #include "mcp_prompt.h" #include "mcp_sse_client.h" +#include +#include + using namespace mcp; using json = nlohmann::ordered_json; @@ -782,4 +785,97 @@ int main(int argc, char **argv) { ::testing::AddGlobalTestEnvironment(new PromptsEnvironment()); return RUN_ALL_TESTS(); -} \ No newline at end of file +} +// Test Stdio Transport +class StdioTransportTest : public ::testing::Test { +protected: + void SetUp() override { + // Prepare original buffers + orig_cin = std::cin.rdbuf(); + orig_cout = std::cout.rdbuf(); + } + + void TearDown() override { + // Restore buffers + std::cin.rdbuf(orig_cin); + std::cout.rdbuf(orig_cout); + } + + std::streambuf* orig_cin; + std::streambuf* orig_cout; +}; + +TEST_F(StdioTransportTest, StartStdioProcessing) { + mcp::server::configuration srv_conf; + mcp::server server(srv_conf); + + // Register tool + mcp::tool echo_tool = mcp::tool_builder("echo") + .with_description("Echo tool") + .with_string_param("text", "Text to echo", true) + .build(); + + server.register_tool(echo_tool, [](const mcp::json& params, const std::string&) -> mcp::json { + return { + { + {"type", "text"}, + {"text", params["text"]} + } + }; + }); + + // Simulate input sequence + // 1. Initialize request + std::string init_req = "{\"jsonrpc\": \"2.0\", \"id\": 0, \"method\": \"initialize\", \"params\": {\"protocolVersion\": \"2025-03-26\", \"capabilities\": {}, \"clientInfo\": {\"name\": \"test_client\", \"version\": \"1.0\"}}}\n"; + // 2. Initialized notification + std::string init_notif = "{\"jsonrpc\": \"2.0\", \"method\": \"notifications/initialized\"}\n"; + // 3. Tool call request + std::string mock_input = "{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/call\", \"params\": {\"name\": \"echo\", \"arguments\": {\"text\": \"hello stdio test\"}}}\n"; + + std::istringstream in_stream(init_req + init_notif + mock_input); + std::ostringstream out_stream; + + std::cin.rdbuf(in_stream.rdbuf()); + std::cout.rdbuf(out_stream.rdbuf()); + + // Run processing + // It should process the lines, then exit when EOF is reached + server.start_stdio(); + + // Verify output + std::string raw_output = out_stream.str(); + ASSERT_FALSE(raw_output.empty()); + + // Collect all non-empty JSON lines from stdout + std::istringstream output_stream(raw_output); + std::string line; + std::vector responses; + while (std::getline(output_stream, line)) { + if (line.empty()) continue; + try { + responses.push_back(json::parse(line)); + } catch (...) { + continue; + } + } + + // Verify we have at least the init response and tool call response + ASSERT_GE(responses.size(), 2); + + // Find the tool call response (id=1) + bool found_tool_result = false; + bool found_init_result = false; + for (const auto& resp : responses) { + if (resp.contains("id") && resp["id"] == 0 && resp.contains("result")) { + found_init_result = true; + } + if (resp.contains("id") && resp["id"] == 1 && resp.contains("result")) { + EXPECT_EQ(resp["jsonrpc"], "2.0"); + EXPECT_EQ(resp["result"]["content"][0]["text"], "hello stdio test"); + found_tool_result = true; + } + } + + EXPECT_TRUE(found_init_result) << "Missing initialize response"; + EXPECT_TRUE(found_tool_result) << "Missing tools/call response"; +}