diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd1dc7b --- /dev/null +++ b/README.md @@ -0,0 +1,181 @@ +# MCP Protocol Framework + +[Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/specification/2024-11-05/architecture/) 是一个开放协议,为AI模型和代理提供与各种资源、工具和服务交互的标准化方式。本框架实现了MCP协议的核心功能,符合2024-11-05基本协议规范。 + +## 核心特性 + +- **JSON-RPC 2.0通信**: 基于JSON-RPC 2.0标准的请求/响应通信 +- **资源抽象**: 文件、API等资源的标准接口 +- **工具注册**: 注册和调用带有结构化参数的工具 +- **可扩展架构**: 易于扩展新的资源类型和工具 +- **多传输支持**: 支持HTTP和标准输入/输出(stdio)通信方式 + +## 组件 + +### 核心协议 (`mcp_message.h`, `mcp_message.cpp`) + +定义MCP的基本结构和类型: +- 请求/响应处理 +- 错误代码 +- 工具定义 + +### HTTP服务器 (`mcp_server.h`, `mcp_server.cpp`) + +实现一个HTTP服务器,暴露MCP资源和工具: +- 在特定路径注册资源 +- 注册带有处理程序的工具 +- 处理传入的HTTP请求 +- 将请求路由到适当的资源 + +### 客户端 + +#### HTTP客户端 (`mcp_client.h`, `mcp_client.cpp`) + +实现连接到HTTP MCP服务器的HTTP客户端,符合SSE通信规范。 + +#### Stdio客户端 (`mcp_stdio_client.h`, `mcp_stdio_client.cpp`) + +通过标准输入/输出与MCP服务器通信的客户端: +- 启动本地服务器进程 +- 通过管道进行通信 +- 支持资源访问和工具调用 +- 适合与本地进程集成 + +### 资源 (`mcp_resource.h`, `mcp_resource.cpp`) + +提供常见资源类型的基本实现: +- 文件资源 +- API资源 + +### 工具 (`mcp_tool.h`, `mcp_tool.cpp`) + +提供工具相关定义与功能: +- 工具构建器 + +## 示例 + +### HTTP服务器示例 (`examples/server_example.cpp`) + +MCP服务器实现示例,带有自定义工具: +- 时间工具: 获取当前时间 +- 计算器工具: 执行数学运算 +- 回显工具: 处理和分析文本 +- 招呼工具:返回`Hello, `+传入名字+`!`,默认返回`Hello, World!` + +### HTTP客户端示例 (`examples/client_example.cpp`) + +连接到服务器的MCP客户端示例: +- 获取服务器信息 +- 列出可用工具 +- 使用参数调用工具 +- 访问资源 + +### Stdio客户端示例 (`examples/stdio_client_example.cpp`) + +展示如何使用stdio客户端与本地服务器通信: +- 启动本地服务器进程 +- 访问文件系统资源 +- 调用服务器工具 + +## 如何使用 + +### 设置HTTP服务器 + +```cpp +// 创建并配置服务器 +mcp::server server("localhost", 8080); +server.set_server_info("MCP Example Server", "2024-11-05"); + +// 注册工具 +mcp::json hello_handler(const mcp::json& params) { + std::string name = params.contains("name") ? params["name"].get() : "World"; + return { + { + {"type", "text"}, + {"text", "Hello, " + name + "!"} + } + }; +} + +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(hello_tool, hello_handler); + +// 注册资源 +auto file_resource = std::make_shared(""); +server.register_resource("file://", file_resource); + +// 启动服务器 +server.start(true); // 阻塞模式 +``` + +### 创建HTTP客户端 + +```cpp +// 连接到服务器 +mcp::client client("localhost", 8080); + +// 初始化连接 +client.initialize("My Client", "1.0.0"); + +// 调用工具 +mcp::json params = { + {"name", "Client"} +}; + +mcp::json result = client.call_tool("hello", params); +``` + +### 使用Stdio客户端 + +Stdio客户端可以与任何支持stdio传输的MCP服务器进行通信,例如: + +- @modelcontextprotocol/server-everything - 示例服务器 +- @modelcontextprotocol/server-filesystem - 文件系统服务器 +- 其他支持stdio传输的MCP服务器 + +```cpp +#include "mcp_stdio_client.h" + +// 创建客户端,指定服务器命令 +mcp::stdio_client client("npx -y @modelcontextprotocol/server-everything"); +// mcp::stdio_client client("npx -y @modelcontextprotocol/server-filesystem /path/to/directory"); + +// 初始化客户端 +if (!client.initialize("My Client", "1.0.0")) { + // 初始化失败处理 +} + +// 访问资源 +json resources = client.list_resources(); +json content = client.read_resource("resource://uri"); + +// 调用工具 +json result = client.call_tool("tool_name", { + {"param1", "value1"}, + {"param2", "value2"} +}); +``` + +## 构建框架 + +框架依赖以下库: +- httplib.h - HTTP服务器和客户端 +- json.hpp - JSON解析和生成 +- gtest - 测试 + +所有依赖项都包含在仓库中。 + +使用CMake构建示例: +```bash +cmake -B build +cmake --build build --config Release +``` + + +## 许可证 + +本框架根据MIT许可证提供。有关详细信息,请参阅LICENSE文件。 \ No newline at end of file diff --git a/README_zh.md b/README_zh.md deleted file mode 100644 index a668d5c..0000000 --- a/README_zh.md +++ /dev/null @@ -1,151 +0,0 @@ -# MCP Protocol Framework - -Model Context Protocol (MCP) 是一个开放协议,为AI模型和代理提供与各种资源、工具和服务交互的标准化方式。本框架实现了MCP协议的核心功能,符合2024-11-05基本协议规范。 - -## 核心特性 - -- **JSON-RPC 2.0通信**: 基于JSON-RPC 2.0标准的请求/响应通信 -- **资源抽象**: 文件、API等资源的标准接口 -- **工具注册**: 注册和调用带有结构化参数的工具 -- **可扩展架构**: 易于扩展新的资源类型和工具 - -## 组件 - -### 核心协议 (`mcp_message.h`, `mcp_message.cpp`) - -定义MCP的基本结构和类型: -- 请求/响应处理 -- 错误代码 -- 工具定义 - -### 服务器 (`mcp_server.h`, `mcp_server.cpp`) - -实现一个HTTP服务器,暴露MCP资源和工具: -- 在特定路径注册资源 -- 注册带有处理程序的工具 -- 处理传入的HTTP请求 -- 将请求路由到适当的资源 - -### 客户端 (`mcp_client.h`, `mcp_client.cpp`) - -实现连接到MCP服务器的客户端: -- 连接到服务器 -- 发现可用的资源和工具 -- 向资源发出请求 -- 使用参数调用工具 - -### 资源 (`mcp_resource.h`, `mcp_resource.cpp`) - -提供常见资源类型的基本实现: -- 文件资源 -- API资源 - -### 工具 (`mcp_tool.h`, `mcp_tool.cpp`) - -提供工具相关功能: -- 工具构建器 (流畅API) - -## 示例 - -### 服务器示例 (`examples/server_example.cpp`) - -MCP服务器实现示例,带有自定义工具: -- 时间工具: 获取当前时间 -- 计算器工具: 执行数学运算 -- 回显工具: 处理和分析文本 - -### 客户端示例 (`examples/client_example.cpp`) - -连接到服务器的MCP客户端示例: -- 获取服务器信息 -- 列出可用工具 -- 使用参数调用工具 -- 访问资源 - -## 如何使用 - -### 设置服务器 - -```cpp -// 创建并配置服务器 -mcp::server server("localhost", 8080); -server.set_server_info("MCP Example Server", "2024-11-05"); - -// 注册工具 -mcp::tool time_tool = mcp::tool_builder("get_time") - .with_description("Get the current time") - .build(); - -server.register_tool(time_tool, [](const mcp::json& params) { - // 工具实现 - return mcp::json::object(); -}); - -// 注册资源 -auto file_resource = std::make_shared("./files"); -server.register_resource("/files", file_resource); - -// 启动服务器 -server.start(true); // 阻塞模式 -``` - -### 创建客户端 - -```cpp -// 连接到服务器 -mcp::client client("localhost", 8080); - -// 初始化连接 -client.initialize("My Client", "1.0.0"); - -// 调用工具 -mcp::json params = { - {"key", "value"} -}; -mcp::json result = client.call_tool("tool_name", params); -``` - -## 构建框架 - -框架依赖以下库: -- httplib.h - HTTP服务器和客户端 -- json.hpp - JSON解析和生成 - -所有依赖项都包含在仓库中。 - -使用CMake构建示例: -```bash -cmake -B build -cmake --build build --config Release -``` - -## 扩展框架 - -### 添加新的资源类型 - -1. 定义一个继承自`mcp::resource`的新类 -2. 实现所需的方法: - - `json get_metadata() const` - - `json access(const json& params) const` - -### 创建自定义工具 - -使用工具构建器API: -```cpp -// 创建工具定义 -mcp::tool my_tool = mcp::tool_builder("my_tool") - .with_description("My custom tool") - .with_string_param("input", "Input parameter", true) - .with_number_param("count", "Count parameter", false) - .build(); - -// 注册工具处理程序 -server.register_tool(my_tool, [](const mcp::json& params) { - // 工具实现 - return mcp::json::object(); -}); -``` - -## 许可证 - -本框架根据MIT许可证提供。有关详细信息,请参阅LICENSE文件。 \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2e4d7fb..c38ceac 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -24,4 +24,9 @@ target_compile_features(${TARGET} PRIVATE cxx_std_17) file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/files) # Copy example files if needed -# file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/example_files/ DESTINATION ${CMAKE_BINARY_DIR}/files) \ No newline at end of file +# file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/example_files/ DESTINATION ${CMAKE_BINARY_DIR}/files) + +# 添加stdio客户端示例 +add_executable(stdio_client_example stdio_client_example.cpp) +target_link_libraries(stdio_client_example PRIVATE mcp) +target_include_directories(stdio_client_example PRIVATE ${CMAKE_SOURCE_DIR}/include) \ No newline at end of file diff --git a/examples/server_example.cpp b/examples/server_example.cpp index 2af0aca..801b17c 100644 --- a/examples/server_example.cpp +++ b/examples/server_example.cpp @@ -126,9 +126,12 @@ int main() { server.set_server_info("ExampleServer", "1.0.0"); // Set server capabilities + // mcp::json capabilities = { + // {"tools", {{"listChanged", true}}}, + // {"resources", {{"subscribe", false}, {"listChanged", true}}} + // }; mcp::json capabilities = { - {"tools", {{"listChanged", true}}}, - {"resources", {{"subscribe", false}, {"listChanged", true}}} + {"tools", mcp::json::object()} }; server.set_capabilities(capabilities); @@ -150,14 +153,20 @@ int main() { .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 resources - auto file_resource = std::make_shared("./Makefile"); - server.register_resource("file://./Makefile", file_resource); + // // Register resources + // auto file_resource = std::make_shared("./Makefile"); + // server.register_resource("file://./Makefile", file_resource); // Start server std::cout << "Starting MCP server at localhost:8888..." << std::endl; diff --git a/examples/stdio_client_example.cpp b/examples/stdio_client_example.cpp new file mode 100644 index 0000000..9db80c6 --- /dev/null +++ b/examples/stdio_client_example.cpp @@ -0,0 +1,84 @@ +/** + * @file stdio_client_example.cpp + * @brief Example of using the MCP stdio client + * + * This example demonstrates how to use the MCP stdio client to connect to a server + * using standard input/output as the transport mechanism. + */ + +#include "mcp_stdio_client.h" +#include "mcp_logger.h" + +#include +#include +#include +#include + +int main(int argc, char** argv) { + // 设置日志级别 + mcp::set_log_level(mcp::log_level::info); + + // 检查命令行参数 + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + std::cerr << "Example: " << argv[0] << " \"npx -y @modelcontextprotocol/server-filesystem /Users/username/Desktop\"" << std::endl; + return 1; + } + + std::string command = argv[1]; + + // 创建客户端 + mcp::stdio_client client(command); + + // 初始化客户端 + if (!client.initialize("MCP Stdio Client Example", "1.0.0")) { + std::cerr << "Failed to initialize client" << std::endl; + return 1; + } + + std::cout << "Client initialized successfully" << std::endl; + + try { + // 获取服务器能力 + auto capabilities = client.get_server_capabilities(); + std::cout << "Server capabilities: " << capabilities.dump(2) << std::endl; + + // 列出可用工具 + auto tools = client.get_tools(); + std::cout << "Available tools: " << tools.size() << std::endl; + for (const auto& tool : tools) { + std::cout << " - " << tool.name << ": " << tool.description << std::endl; + } + + // 列出可用资源 + auto resources = client.list_resources(); + std::cout << "Available resources: " << resources.dump(2) << std::endl; + + // 如果有资源,读取第一个资源 + if (resources.contains("resources") && resources["resources"].is_array() && !resources["resources"].empty()) { + auto resource = resources["resources"][0]; + if (resource.contains("uri")) { + std::string uri = resource["uri"]; + std::cout << "Reading resource: " << uri << std::endl; + + auto content = client.read_resource(uri); + std::cout << "Resource content: " << content.dump(2) << std::endl; + } + } + + // 保持连接一段时间 + std::cout << "Keeping connection alive for 5 seconds..." << std::endl; + std::this_thread::sleep_for(std::chrono::seconds(5)); + + // 发送ping请求 + bool ping_result = client.ping(); + std::cout << "Ping result: " << (ping_result ? "success" : "failure") << std::endl; + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + std::cout << "Example completed successfully" << std::endl; + return 0; +} \ No newline at end of file diff --git a/include/mcp_logger.h b/include/mcp_logger.h index 8ea3ef5..f768afb 100644 --- a/include/mcp_logger.h +++ b/include/mcp_logger.h @@ -116,6 +116,10 @@ private: #define LOG_WARNING(...) mcp::logger::instance().warning(__VA_ARGS__) #define LOG_ERROR(...) mcp::logger::instance().error(__VA_ARGS__) +inline void set_log_level(log_level level) { + mcp::logger::instance().set_level(level); +} + } // namespace mcp #endif // MCP_LOGGER_H \ No newline at end of file diff --git a/include/mcp_stdio_client.h b/include/mcp_stdio_client.h new file mode 100644 index 0000000..e37f91f --- /dev/null +++ b/include/mcp_stdio_client.h @@ -0,0 +1,205 @@ +/** + * @file mcp_stdio_client.h + * @brief MCP Stdio Client implementation + * + * This file implements the client-side functionality for the Model Context Protocol + * using standard input/output (stdio) as the transport mechanism. + * Follows the 2024-11-05 protocol specification. + */ + +#ifndef MCP_STDIO_CLIENT_H +#define MCP_STDIO_CLIENT_H + +#include "mcp_message.h" +#include "mcp_tool.h" +#include "mcp_logger.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mcp { + +/** + * @class stdio_client + * @brief Client for connecting to MCP servers using stdio transport + * + * The stdio_client class provides functionality to connect to MCP servers + * by spawning a separate process and communicating via standard input/output. + */ +class stdio_client { +public: + /** + * @brief Constructor + * @param command The command to execute to start the server + * @param capabilities The capabilities of the client + */ + stdio_client(const std::string& command, const json& capabilities = json::object()); + + /** + * @brief Destructor + */ + ~stdio_client(); + + /** + * @brief Initialize the connection with the server + * @param client_name The name of the client + * @param client_version The version of the client + * @return True if initialization was successful + */ + bool initialize(const std::string& client_name, const std::string& client_version); + + /** + * @brief Ping request + * @return True if the server is alive + */ + bool ping(); + + /** + * @brief Set client capabilities + * @param capabilities The capabilities of the client + */ + void set_capabilities(const json& capabilities); + + /** + * @brief Send a request and wait for a response + * @param method The method to call + * @param params The parameters to pass + * @return The response + * @throws mcp_exception on error + */ + response send_request(const std::string& method, const json& params = json::object()); + + /** + * @brief Send a notification (no response expected) + * @param method The method to call + * @param params The parameters to pass + * @throws mcp_exception on error + */ + void send_notification(const std::string& method, const json& params = json::object()); + + /** + * @brief Get server capabilities + * @return The server capabilities + * @throws mcp_exception on error + */ + json get_server_capabilities(); + + /** + * @brief Call a tool + * @param tool_name The name of the tool to call + * @param arguments The arguments to pass to the tool + * @return The result of the tool call + * @throws mcp_exception on error + */ + json call_tool(const std::string& tool_name, const json& arguments = json::object()); + + /** + * @brief Get available tools + * @return List of available tools + * @throws mcp_exception on error + */ + std::vector get_tools(); + + /** + * @brief Get client capabilities + * @return The client capabilities + */ + json get_capabilities(); + + /** + * @brief List available resources + * @param cursor Optional cursor for pagination + * @return List of resources + */ + json list_resources(const std::string& cursor = ""); + + /** + * @brief Read a resource + * @param resource_uri The URI of the resource + * @return The resource content + */ + json read_resource(const std::string& resource_uri); + + /** + * @brief Subscribe to resource changes + * @param resource_uri The URI of the resource + * @return Subscription result + */ + json subscribe_to_resource(const std::string& resource_uri); + + /** + * @brief List resource templates + * @return List of resource templates + */ + json list_resource_templates(); + + /** + * @brief Check if the server process is running + * @return True if the server process is running + */ + bool is_running() const; + +private: + // 启动服务器进程 + bool start_server_process(); + + // 停止服务器进程 + void stop_server_process(); + + // 读取线程函数 + void read_thread_func(); + + // 发送JSON-RPC请求 + json send_jsonrpc(const request& req); + + // 服务器命令 + std::string command_; + + // 进程ID + int process_id_ = -1; + + // 标准输入管道 + int stdin_pipe_[2] = {-1, -1}; + + // 标准输出管道 + int stdout_pipe_[2] = {-1, -1}; + + // 读取线程 + std::unique_ptr read_thread_; + + // 运行状态 + std::atomic running_{false}; + + // 客户端能力 + json capabilities_; + + // 服务器能力 + json server_capabilities_; + + // 互斥锁 + mutable std::mutex mutex_; + + // 请求ID到Promise的映射,用于异步等待响应 + std::map> pending_requests_; + + // 响应处理互斥锁 + std::mutex response_mutex_; + + // 初始化状态 + std::atomic initialized_{false}; + + // 初始化条件变量 + std::condition_variable init_cv_; +}; + +} // namespace mcp + +#endif // MCP_STDIO_CLIENT_H \ No newline at end of file diff --git a/include/mcp_thread_pool.h b/include/mcp_thread_pool.h index 735597f..456bfc5 100644 --- a/include/mcp_thread_pool.h +++ b/include/mcp_thread_pool.h @@ -14,6 +14,7 @@ #include #include #include +#include namespace mcp { @@ -74,8 +75,8 @@ public: * @return 任务的future */ template - auto enqueue(F&& f, Args&&... args) -> std::future::type> { - using return_type = typename std::result_of::type; + auto enqueue(F&& f, Args&&... args) -> std::future::type> { + using return_type = typename std::invoke_result::type; auto task = std::make_shared>( std::bind(std::forward(f), std::forward(args)...) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b860160..627d15b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,8 @@ add_library(${TARGET} STATIC ../include/mcp_server.h mcp_tool.cpp ../include/mcp_tool.h + mcp_stdio_client.cpp + ../include/mcp_stdio_client.h ) target_link_libraries(${TARGET} PUBLIC diff --git a/src/mcp_client.cpp b/src/mcp_client.cpp index 5821f41..5db91d6 100644 --- a/src/mcp_client.cpp +++ b/src/mcp_client.cpp @@ -299,9 +299,7 @@ void client::open_sse_connection() { retry_count = 0; LOG_INFO("SSE thread: Connection successful"); - } catch (const std::exception& e) { - LOG_ERROR("SSE connection error: ", e.what()); - + } catch (const std::exception& e) { if (!sse_running_) { LOG_INFO("SSE connection actively closed, no retry needed"); break; @@ -311,6 +309,8 @@ void client::open_sse_connection() { LOG_ERROR("Maximum retry count reached, stopping SSE connection attempts"); break; } + + LOG_ERROR("SSE connection error: ", e.what()); int delay = retry_delay_base * (1 << (retry_count - 1)); LOG_INFO("Will retry in ", delay, " ms (attempt ", retry_count, "/", max_retries, ")"); diff --git a/src/mcp_stdio_client.cpp b/src/mcp_stdio_client.cpp new file mode 100644 index 0000000..155bdd5 --- /dev/null +++ b/src/mcp_stdio_client.cpp @@ -0,0 +1,507 @@ +/** + * @file mcp_stdio_client.cpp + * @brief Implementation of the MCP stdio client + * + * This file implements the client-side functionality for the Model Context Protocol + * using standard input/output (stdio) as the transport mechanism. + * Follows the 2024-11-05 protocol specification. + */ + +#include "mcp_stdio_client.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mcp { + +stdio_client::stdio_client(const std::string& command, const json& capabilities) + : command_(command), capabilities_(capabilities) { + + LOG_INFO("Creating MCP stdio client for command: ", command); +} + +stdio_client::~stdio_client() { + stop_server_process(); +} + +bool stdio_client::initialize(const std::string& client_name, const std::string& client_version) { + LOG_INFO("Initializing MCP stdio client..."); + + if (!start_server_process()) { + LOG_ERROR("Failed to start server process"); + return false; + } + + request req = request::create("initialize", { + {"protocolVersion", MCP_VERSION}, + {"capabilities", capabilities_}, + {"clientInfo", { + {"name", client_name}, + {"version", client_version} + }} + }); + + try { + json result = send_jsonrpc(req); + + server_capabilities_ = result["capabilities"]; + + request notification = request::create_notification("initialized"); + send_jsonrpc(notification); + + initialized_ = true; + init_cv_.notify_all(); + + return true; + } catch (const std::exception& e) { + LOG_ERROR("Initialization failed: ", e.what()); + stop_server_process(); + return false; + } +} + +bool stdio_client::ping() { + if (!running_) { + return false; + } + + request req = request::create("ping", {}); + + try { + json result = send_jsonrpc(req); + return result.empty(); + } catch (const std::exception& e) { + return false; + } +} + +void stdio_client::set_capabilities(const json& capabilities) { + std::lock_guard lock(mutex_); + capabilities_ = capabilities; +} + +response stdio_client::send_request(const std::string& method, const json& params) { + if (!running_) { + throw mcp_exception(error_code::internal_error, "Server process not running"); + } + + request req = request::create(method, params); + json result = send_jsonrpc(req); + + response res; + res.jsonrpc = "2.0"; + res.id = req.id; + res.result = result; + + return res; +} + +void stdio_client::send_notification(const std::string& method, const json& params) { + if (!running_) { + throw mcp_exception(error_code::internal_error, "Server process not running"); + } + + request req = request::create_notification(method, params); + send_jsonrpc(req); +} + +json stdio_client::get_server_capabilities() { + return server_capabilities_; +} + +json stdio_client::call_tool(const std::string& tool_name, const json& arguments) { + return send_request("tools/call", { + {"name", tool_name}, + {"arguments", arguments} + }).result; +} + +std::vector stdio_client::get_tools() { + json response_json = send_request("tools/list", {}).result; + std::vector tools; + + json tools_json; + if (response_json.contains("tools") && response_json["tools"].is_array()) { + tools_json = response_json["tools"]; + } else if (response_json.is_array()) { + tools_json = response_json; + } else { + return tools; + } + + for (const auto& tool_json : tools_json) { + tool t; + t.name = tool_json["name"]; + t.description = tool_json["description"]; + + if (tool_json.contains("inputSchema")) { + t.parameters_schema = tool_json["inputSchema"]; + } + + tools.push_back(t); + } + + return tools; +} + +json stdio_client::get_capabilities() { + return capabilities_; +} + +json stdio_client::list_resources(const std::string& cursor) { + json params = json::object(); + if (!cursor.empty()) { + params["cursor"] = cursor; + } + return send_request("resources/list", params).result; +} + +json stdio_client::read_resource(const std::string& resource_uri) { + return send_request("resources/read", { + {"uri", resource_uri} + }).result; +} + +json stdio_client::subscribe_to_resource(const std::string& resource_uri) { + return send_request("resources/subscribe", { + {"uri", resource_uri} + }).result; +} + +json stdio_client::list_resource_templates() { + return send_request("resources/templates/list").result; +} + +bool stdio_client::is_running() const { + return running_; +} + +bool stdio_client::start_server_process() { + if (running_) { + LOG_INFO("Server process already running"); + return true; + } + + LOG_INFO("Starting server process: ", command_); + + // 创建管道 + if (pipe(stdin_pipe_) == -1) { + LOG_ERROR("Failed to create stdin pipe: ", strerror(errno)); + return false; + } + + if (pipe(stdout_pipe_) == -1) { + LOG_ERROR("Failed to create stdout pipe: ", strerror(errno)); + close(stdin_pipe_[0]); + close(stdin_pipe_[1]); + return false; + } + + // 创建子进程 + process_id_ = fork(); + + if (process_id_ == -1) { + LOG_ERROR("Failed to fork process: ", strerror(errno)); + close(stdin_pipe_[0]); + close(stdin_pipe_[1]); + close(stdout_pipe_[0]); + close(stdout_pipe_[1]); + return false; + } + + if (process_id_ == 0) { + // 子进程 + + // 关闭不需要的管道端 + close(stdin_pipe_[1]); // 关闭写入端 + close(stdout_pipe_[0]); // 关闭读取端 + + // 重定向标准输入/输出 + if (dup2(stdin_pipe_[0], STDIN_FILENO) == -1) { + LOG_ERROR("Failed to redirect stdin: ", strerror(errno)); + exit(EXIT_FAILURE); + } + + if (dup2(stdout_pipe_[1], STDOUT_FILENO) == -1) { + LOG_ERROR("Failed to redirect stdout: ", strerror(errno)); + exit(EXIT_FAILURE); + } + + // 关闭已重定向的文件描述符 + close(stdin_pipe_[0]); + close(stdout_pipe_[1]); + + // 设置非阻塞模式 + int flags = fcntl(STDIN_FILENO, F_GETFL, 0); + fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK); + + // 执行命令 + std::vector args; + std::istringstream iss(command_); + std::string arg; + + while (iss >> arg) { + args.push_back(arg); + } + + std::vector c_args; + for (auto& a : args) { + c_args.push_back(const_cast(a.c_str())); + } + c_args.push_back(nullptr); + + execvp(c_args[0], c_args.data()); + + // 如果execvp返回,则表示出错 + LOG_ERROR("Failed to execute command: ", strerror(errno)); + exit(EXIT_FAILURE); + } + + // 父进程 + + // 关闭不需要的管道端 + close(stdin_pipe_[0]); // 关闭读取端 + close(stdout_pipe_[1]); // 关闭写入端 + + // 设置非阻塞模式 + int flags = fcntl(stdout_pipe_[0], F_GETFL, 0); + fcntl(stdout_pipe_[0], F_SETFL, flags | O_NONBLOCK); + + running_ = true; + + // 启动读取线程 + read_thread_ = std::make_unique(&stdio_client::read_thread_func, this); + + // 等待一段时间,确保进程启动 + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + // 检查进程是否仍在运行 + int status; + pid_t result = waitpid(process_id_, &status, WNOHANG); + + if (result == process_id_) { + LOG_ERROR("Server process exited immediately with status: ", WEXITSTATUS(status)); + running_ = false; + + if (read_thread_ && read_thread_->joinable()) { + read_thread_->join(); + } + + close(stdin_pipe_[1]); + close(stdout_pipe_[0]); + + return false; + } else if (result == -1) { + LOG_ERROR("Failed to check process status: ", strerror(errno)); + running_ = false; + + if (read_thread_ && read_thread_->joinable()) { + read_thread_->join(); + } + + close(stdin_pipe_[1]); + close(stdout_pipe_[0]); + + return false; + } + + LOG_INFO("Server process started successfully, PID: ", process_id_); + return true; +} + +void stdio_client::stop_server_process() { + if (!running_) { + return; + } + + LOG_INFO("Stopping server process..."); + + running_ = false; + + // 关闭管道 + if (stdin_pipe_[1] != -1) { + close(stdin_pipe_[1]); + stdin_pipe_[1] = -1; + } + + if (stdout_pipe_[0] != -1) { + close(stdout_pipe_[0]); + stdout_pipe_[0] = -1; + } + + // 等待读取线程结束 + if (read_thread_ && read_thread_->joinable()) { + read_thread_->join(); + } + + // 终止进程 + if (process_id_ > 0) { + LOG_INFO("Sending SIGTERM to process: ", process_id_); + kill(process_id_, SIGTERM); + + // 等待进程结束 + int status; + pid_t result = waitpid(process_id_, &status, WNOHANG); + + if (result == 0) { + // 进程仍在运行,等待一段时间 + std::this_thread::sleep_for(std::chrono::seconds(2)); + + result = waitpid(process_id_, &status, WNOHANG); + + if (result == 0) { + // 进程仍在运行,强制终止 + LOG_WARNING("Process did not terminate, sending SIGKILL"); + kill(process_id_, SIGKILL); + waitpid(process_id_, &status, 0); + } + } + + process_id_ = -1; + } + + LOG_INFO("Server process stopped"); +} + +void stdio_client::read_thread_func() { + LOG_INFO("Read thread started"); + + const int buffer_size = 4096; + char buffer[buffer_size]; + std::string data_buffer; + + while (running_) { + // 读取数据 + ssize_t bytes_read = read(stdout_pipe_[0], buffer, buffer_size - 1); + + if (bytes_read > 0) { + buffer[bytes_read] = '\0'; + data_buffer.append(buffer, bytes_read); + + // 处理完整的JSON-RPC消息 + size_t pos = 0; + while ((pos = data_buffer.find('\n')) != std::string::npos) { + std::string line = data_buffer.substr(0, pos); + data_buffer.erase(0, pos + 1); + + if (!line.empty()) { + try { + json message = json::parse(line); + + if (message.contains("jsonrpc") && message["jsonrpc"] == "2.0") { + if (message.contains("id") && !message["id"].is_null()) { + // 这是一个响应 + json id = message["id"]; + + std::lock_guard lock(response_mutex_); + auto it = pending_requests_.find(id); + + if (it != pending_requests_.end()) { + if (message.contains("result")) { + it->second.set_value(message["result"]); + } else if (message.contains("error")) { + json error_result = { + {"isError", true}, + {"error", message["error"]} + }; + it->second.set_value(error_result); + } else { + it->second.set_value(json::object()); + } + + pending_requests_.erase(it); + } else { + LOG_WARNING("Received response for unknown request ID: ", id); + } + } else if (message.contains("method")) { + // 这是一个请求或通知 + LOG_INFO("Received request/notification: ", message["method"]); + // 目前不处理服务器发来的请求 + } + } + } catch (const json::exception& e) { + LOG_ERROR("Failed to parse JSON-RPC message: ", e.what(), ", message: ", line); + } + } + } + } else if (bytes_read == 0) { + // 管道已关闭 + LOG_WARNING("Pipe closed by server"); + break; + } else if (bytes_read == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + // 非阻塞模式下没有数据可读 + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } else { + LOG_ERROR("Error reading from pipe: ", strerror(errno)); + break; + } + } + } + + LOG_INFO("Read thread stopped"); +} + +json stdio_client::send_jsonrpc(const request& req) { + if (!running_) { + throw mcp_exception(error_code::internal_error, "Server process not running"); + } + + json req_json = req.to_json(); + std::string req_str = req_json.dump() + "\n"; + + // 发送请求 + ssize_t bytes_written = write(stdin_pipe_[1], req_str.c_str(), req_str.size()); + + if (bytes_written != static_cast(req_str.size())) { + LOG_ERROR("Failed to write complete request: ", strerror(errno)); + throw mcp_exception(error_code::internal_error, "Failed to write to pipe"); + } + + // 如果是通知,不需要等待响应 + if (req.is_notification()) { + return json::object(); + } + + // 创建Promise和Future + std::promise response_promise; + std::future response_future = response_promise.get_future(); + + { + std::lock_guard lock(response_mutex_); + pending_requests_[req.id] = std::move(response_promise); + } + + // 等待响应,设置超时 + const auto timeout = std::chrono::seconds(30); + auto status = response_future.wait_for(timeout); + + if (status == std::future_status::ready) { + json response = response_future.get(); + + if (response.contains("isError") && response["isError"].get()) { + int code = response["error"]["code"]; + std::string message = response["error"]["message"]; + + throw mcp_exception(static_cast(code), message); + } + + return response; + } else { + { + std::lock_guard lock(response_mutex_); + pending_requests_.erase(req.id); + } + + throw mcp_exception(error_code::internal_error, "Timeout waiting for response"); + } +} + +} // namespace mcp \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e00b464..ce6c7d0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -50,6 +50,11 @@ if(OPENSSL_FOUND) target_link_libraries(${TEST_PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES}) endif() +# 添加自定义链接选项,避免重复链接 +if(APPLE) + set_target_properties(${TEST_PROJECT_NAME} PROPERTIES LINK_FLAGS "-Wl,-no_warn_duplicate_libraries") +endif() + # Enable testing enable_testing() diff --git a/test/mcp_test.cpp b/test/mcp_test.cpp index 9c24d59..9103a2f 100644 --- a/test/mcp_test.cpp +++ b/test/mcp_test.cpp @@ -336,6 +336,7 @@ protected: // 测试Ping请求 TEST_F(PingTest, PingRequest) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); client_->initialize("TestClient", "1.0.0"); bool ping_result = client_->ping(); EXPECT_TRUE(ping_result); @@ -538,6 +539,8 @@ protected: // 测试列出工具 TEST_F(ToolsTest, ListTools) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + // 调用列出工具方法 json tools_list = client_->send_request("tools/list").result;