add stdio client; refine README.md
parent
28b8fd5376
commit
48c2c42d22
|
@ -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<std::string>() : "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<mcp::file_resource>("<file_path>");
|
||||
server.register_resource("file://<file_path>", 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文件。
|
151
README_zh.md
151
README_zh.md
|
@ -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<mcp::file_resource>("./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文件。
|
|
@ -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)
|
||||
# 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)
|
|
@ -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<mcp::file_resource>("./Makefile");
|
||||
server.register_resource("file://./Makefile", file_resource);
|
||||
// // Register resources
|
||||
// auto file_resource = std::make_shared<mcp::file_resource>("./Makefile");
|
||||
// server.register_resource("file://./Makefile", file_resource);
|
||||
|
||||
// Start server
|
||||
std::cout << "Starting MCP server at localhost:8888..." << std::endl;
|
||||
|
|
|
@ -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 <iostream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// 设置日志级别
|
||||
mcp::set_log_level(mcp::log_level::info);
|
||||
|
||||
// 检查命令行参数
|
||||
if (argc < 2) {
|
||||
std::cerr << "Usage: " << argv[0] << " <server_command>" << 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;
|
||||
}
|
|
@ -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
|
|
@ -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 <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <future>
|
||||
#include <thread>
|
||||
|
||||
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<tool> 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<std::thread> read_thread_;
|
||||
|
||||
// 运行状态
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
// 客户端能力
|
||||
json capabilities_;
|
||||
|
||||
// 服务器能力
|
||||
json server_capabilities_;
|
||||
|
||||
// 互斥锁
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
// 请求ID到Promise的映射,用于异步等待响应
|
||||
std::map<json, std::promise<json>> pending_requests_;
|
||||
|
||||
// 响应处理互斥锁
|
||||
std::mutex response_mutex_;
|
||||
|
||||
// 初始化状态
|
||||
std::atomic<bool> initialized_{false};
|
||||
|
||||
// 初始化条件变量
|
||||
std::condition_variable init_cv_;
|
||||
};
|
||||
|
||||
} // namespace mcp
|
||||
|
||||
#endif // MCP_STDIO_CLIENT_H
|
|
@ -14,6 +14,7 @@
|
|||
#include <functional>
|
||||
#include <future>
|
||||
#include <atomic>
|
||||
#include <type_traits>
|
||||
|
||||
namespace mcp {
|
||||
|
||||
|
@ -74,8 +75,8 @@ public:
|
|||
* @return 任务的future
|
||||
*/
|
||||
template<class F, class... Args>
|
||||
auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {
|
||||
using return_type = typename std::result_of<F(Args...)>::type;
|
||||
auto enqueue(F&& f, Args&&... args) -> std::future<typename std::invoke_result<F, Args...>::type> {
|
||||
using return_type = typename std::invoke_result<F, Args...>::type;
|
||||
|
||||
auto task = std::make_shared<std::packaged_task<return_type()>>(
|
||||
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, ")");
|
||||
|
|
|
@ -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 <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
|
||||
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<std::mutex> 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<tool> stdio_client::get_tools() {
|
||||
json response_json = send_request("tools/list", {}).result;
|
||||
std::vector<tool> 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<std::string> args;
|
||||
std::istringstream iss(command_);
|
||||
std::string arg;
|
||||
|
||||
while (iss >> arg) {
|
||||
args.push_back(arg);
|
||||
}
|
||||
|
||||
std::vector<char*> c_args;
|
||||
for (auto& a : args) {
|
||||
c_args.push_back(const_cast<char*>(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<std::thread>(&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<std::mutex> 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<ssize_t>(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<json> response_promise;
|
||||
std::future<json> response_future = response_promise.get_future();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<bool>()) {
|
||||
int code = response["error"]["code"];
|
||||
std::string message = response["error"]["message"];
|
||||
|
||||
throw mcp_exception(static_cast<error_code>(code), message);
|
||||
}
|
||||
|
||||
return response;
|
||||
} else {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(response_mutex_);
|
||||
pending_requests_.erase(req.id);
|
||||
}
|
||||
|
||||
throw mcp_exception(error_code::internal_error, "Timeout waiting for response");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mcp
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in New Issue