├── examples ├── server_example.png ├── CMakeLists.txt ├── stdio_client_example.cpp ├── sse_client_example.cpp ├── server_example.cpp └── agent_example.cpp ├── .gitmodules ├── src ├── mcp_message.cpp ├── CMakeLists.txt ├── mcp_tool.cpp ├── mcp_resource.cpp └── mcp_sse_client.cpp ├── LICENSE ├── .gitignore ├── CMakeLists.txt ├── test ├── CMakeLists.txt ├── testcase.md └── mcp_test.cpp ├── include ├── mcp_thread_pool.h ├── mcp_logger.h ├── mcp_client.h ├── mcp_message.h ├── mcp_tool.h ├── mcp_stdio_client.h ├── mcp_sse_client.h ├── mcp_resource.h └── mcp_server.h ├── README.md └── common └── base64.hpp /examples/server_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hkr04/cpp-mcp/HEAD/examples/server_example.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/googletest"] 2 | path = test/googletest 3 | url = https://github.com/google/googletest.git -------------------------------------------------------------------------------- /src/mcp_message.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_protocol.cpp 3 | * @brief Implementation of the MCP protocol 4 | * 5 | * This file implements the core protocol functionality for the MCP protocol. 6 | * Follows the 2024-11-05 basic protocol specification. 7 | */ 8 | 9 | #include "mcp_message.h" 10 | #include 11 | #include 12 | 13 | namespace mcp { 14 | 15 | // Implementation of any protocol-related functions 16 | 17 | } // namespace mcp -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(TARGET mcp) 2 | 3 | add_library(${TARGET} STATIC 4 | ../include/mcp_client.h 5 | mcp_message.cpp 6 | ../include/mcp_message.h 7 | mcp_resource.cpp 8 | ../include/mcp_resource.h 9 | mcp_server.cpp 10 | ../include/mcp_server.h 11 | mcp_tool.cpp 12 | ../include/mcp_tool.h 13 | mcp_stdio_client.cpp 14 | ../include/mcp_stdio_client.h 15 | mcp_sse_client.cpp 16 | ../include/mcp_sse_client.h 17 | ) 18 | 19 | target_link_libraries(${TARGET} PUBLIC ${CMAKE_THREAD_LIBS_INIT}) 20 | 21 | # If OpenSSL is found, link the OpenSSL libraries 22 | if(OPENSSL_FOUND) 23 | target_link_libraries(${TARGET} PUBLIC ${OPENSSL_LIBRARIES}) 24 | endif() 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-2025 The cpp-mcp authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}) 4 | 5 | set(TARGET sse_client_example) 6 | add_executable(${TARGET} sse_client_example.cpp) 7 | target_link_libraries(${TARGET} PRIVATE mcp) 8 | target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/include) 9 | if(OPENSSL_FOUND) 10 | target_link_libraries(${TARGET} PRIVATE ${OPENSSL_LIBRARIES}) 11 | endif() 12 | 13 | set(TARGET stdio_client_example) 14 | add_executable(${TARGET} stdio_client_example.cpp) 15 | target_link_libraries(${TARGET} PRIVATE mcp) 16 | target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/include) 17 | 18 | set(TARGET server_example) 19 | add_executable(${TARGET} server_example.cpp) 20 | target_link_libraries(${TARGET} PRIVATE mcp) 21 | target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/include) 22 | if(OPENSSL_FOUND) 23 | target_link_libraries(${TARGET} PRIVATE ${OPENSSL_LIBRARIES}) 24 | endif() 25 | 26 | set(TARGET agent_example) 27 | add_executable(${TARGET} agent_example.cpp) 28 | target_link_libraries(${TARGET} PRIVATE mcp) 29 | target_include_directories(${TARGET} PRIVATE ${CMAKE_SOURCE_DIR}/include) 30 | if(OPENSSL_FOUND) 31 | target_link_libraries(${TARGET} PRIVATE ${OPENSSL_LIBRARIES}) 32 | endif() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Extensions 2 | 3 | *.a 4 | *.bat 5 | *.bin 6 | *.d 7 | *.dll 8 | *.dot 9 | *.etag 10 | *.exe 11 | *.gcda 12 | *.gcno 13 | *.gcov 14 | *.gguf 15 | *.gguf.json 16 | *.lastModified 17 | *.log 18 | *.metallib 19 | *.o 20 | *.so 21 | *.tmp 22 | 23 | # IDE / OS 24 | 25 | .cache/ 26 | .ccls-cache/ 27 | .direnv/ 28 | .DS_Store 29 | .envrc 30 | .idea/ 31 | .swiftpm 32 | .vs/ 33 | .vscode/ 34 | nppBackup 35 | 36 | 37 | # Coverage 38 | 39 | gcovr-report/ 40 | lcov-report/ 41 | 42 | # Build Artifacts 43 | 44 | tags 45 | .build/ 46 | build* 47 | !build-info.cmake 48 | !build-info.cpp.in 49 | !build-info.sh 50 | !build.zig 51 | !docs/build.md 52 | /libllama.so 53 | /llama-* 54 | /vulkan-shaders-gen 55 | android-ndk-* 56 | arm_neon.h 57 | cmake-build-* 58 | CMakeSettings.json 59 | compile_commands.json 60 | ggml-metal-embed.metal 61 | llama-batched-swift 62 | /rpc-server 63 | out/ 64 | tmp/ 65 | autogen-*.md 66 | 67 | # CI 68 | 69 | !.github/workflows/*.yml 70 | 71 | # Models 72 | 73 | models/* 74 | models-mnt 75 | !models/.editorconfig 76 | 77 | # Zig 78 | 79 | # Logs 80 | 81 | 82 | 83 | # Examples 84 | 85 | 86 | # Server Web UI temporary files 87 | node_modules 88 | examples/server/webui/dist 89 | 90 | # Python 91 | 92 | /.venv 93 | __pycache__/ 94 | */poetry.lock 95 | poetry.toml 96 | 97 | # Nix 98 | /result 99 | 100 | # Test binaries 101 | 102 | # Scripts 103 | 104 | # Test models for lora adapters 105 | 106 | # Local scripts 107 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(MCP VERSION 2024.11.05 LANGUAGES CXX) 3 | 4 | set(CMAKE_WARN_UNUSED_CLI YES) 5 | 6 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 7 | 8 | if (NOT XCODE AND NOT MSVC AND NOT CMAKE_BUILD_TYPE) 9 | set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) 10 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") 11 | endif() 12 | 13 | option(BUILD_SHARED_LIBS "build shared libraries" ${BUILD_SHARED_LIBS_DEFAULT}) 14 | 15 | if (WIN32) 16 | add_compile_definitions(_CRT_SECURE_NO_WARNINGS) 17 | endif() 18 | 19 | if (MSVC) 20 | add_compile_options("$<$:/utf-8>") 21 | add_compile_options("$<$:/utf-8>") 22 | add_compile_options("$<$:/bigobj>") 23 | add_compile_options("$<$:/bigobj>") 24 | endif() 25 | 26 | # Set C++ standard 27 | set(CMAKE_CXX_STANDARD 17) 28 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 29 | 30 | # Find required packages 31 | find_package(Threads REQUIRED) 32 | 33 | option(MCP_SSL "Enable SSL support" OFF) 34 | 35 | if(MCP_SSL) 36 | find_package(OpenSSL 3.0.0 COMPONENTS Crypto SSL REQUIRED) 37 | include_directories(${OPENSSL_INCLUDE_DIR}) # before including cpp-httplib to avoid OpenSSL with lower version included 38 | message(STATUS "OpenSSL include directory: ${OPENSSL_INCLUDE_DIR}") 39 | add_compile_definitions(MCP_SSL CPPHTTPLIB_OPENSSL_SUPPORT) 40 | endif() 41 | 42 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) 43 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/common) 44 | 45 | # Add MCP library 46 | add_subdirectory(src) 47 | 48 | # Add examples 49 | add_subdirectory(examples) 50 | 51 | # Add test directory 52 | option(MCP_BUILD_TESTS "Build the tests" OFF) 53 | if(MCP_BUILD_TESTS) 54 | enable_testing() 55 | add_subdirectory(test) 56 | endif() 57 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | # Set test project 4 | set(TEST_PROJECT_NAME "mcp_tests") 5 | project(${TEST_PROJECT_NAME}) 6 | 7 | # Set C++ standard 8 | set(CMAKE_CXX_STANDARD 17) 9 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 10 | 11 | # Find required packages 12 | find_package(Threads REQUIRED) 13 | 14 | # Use local Google Test 15 | set(GTEST_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/googletest) 16 | add_subdirectory(${GTEST_ROOT} googletest-build) 17 | 18 | # For older versions of CMake, this option needs to be set 19 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 20 | 21 | # Include header directories 22 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../include) 23 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../common) 24 | include_directories(${GTEST_ROOT}/googletest/include) 25 | include_directories(${GTEST_ROOT}/googlemock/include) 26 | 27 | # Add test source files 28 | set(TEST_SOURCES 29 | mcp_test.cpp 30 | ) 31 | 32 | # Create test executable 33 | add_executable(${TEST_PROJECT_NAME} ${TEST_SOURCES}) 34 | 35 | # Link directories 36 | link_directories(${CMAKE_CURRENT_SOURCE_DIR}/../build/src) 37 | 38 | if(WIN32) 39 | set(MCP_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../build/src/mcp.lib") 40 | else() 41 | set(MCP_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../build/src/libmcp.a") 42 | endif() 43 | 44 | # Link Google Test and MCP library 45 | target_link_libraries(${TEST_PROJECT_NAME} PRIVATE 46 | gtest 47 | gtest_main 48 | gmock 49 | gmock_main 50 | mcp 51 | Threads::Threads 52 | ) 53 | 54 | # If OpenSSL is found, link OpenSSL libraries 55 | if(OPENSSL_FOUND) 56 | target_link_libraries(${TEST_PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES}) 57 | endif() 58 | 59 | if(APPLE) 60 | set_target_properties(${TEST_PROJECT_NAME} PROPERTIES LINK_FLAGS "-Wl,-no_warn_duplicate_libraries") 61 | endif() 62 | 63 | # Enable testing 64 | enable_testing() 65 | 66 | # Add test 67 | add_test( 68 | NAME ${TEST_PROJECT_NAME} 69 | COMMAND ${TEST_PROJECT_NAME} 70 | ) 71 | 72 | # Add custom target to run tests 73 | add_custom_target(run_tests 74 | COMMAND ${CMAKE_CTEST_COMMAND} --verbose 75 | DEPENDS ${TEST_PROJECT_NAME} 76 | ) -------------------------------------------------------------------------------- /include/mcp_thread_pool.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_thread_pool.h 3 | * @brief Simple thread pool implementation 4 | */ 5 | 6 | #ifndef MCP_THREAD_POOL_H 7 | #define MCP_THREAD_POOL_H 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace mcp { 20 | 21 | class thread_pool { 22 | public: 23 | /** 24 | * @brief Constructor 25 | * @param num_threads Number of threads in the thread pool 26 | */ 27 | explicit thread_pool(unsigned int num_threads = std::thread::hardware_concurrency()) : stop_(false) { 28 | for (unsigned int i = 0; i < num_threads; ++i) { 29 | workers_.emplace_back([this] { 30 | while (true) { 31 | std::function task; 32 | 33 | { 34 | std::unique_lock lock(queue_mutex_); 35 | condition_.wait(lock, [this] { 36 | return stop_ || !tasks_.empty(); 37 | }); 38 | 39 | if (stop_ && tasks_.empty()) { 40 | return; 41 | } 42 | 43 | task = std::move(tasks_.front()); 44 | tasks_.pop(); 45 | } 46 | 47 | task(); 48 | } 49 | }); 50 | } 51 | } 52 | 53 | /** 54 | * @brief Destructor 55 | */ 56 | ~thread_pool() { 57 | { 58 | std::unique_lock lock(queue_mutex_); 59 | stop_ = true; 60 | } 61 | 62 | condition_.notify_all(); 63 | 64 | for (std::thread& worker : workers_) { 65 | if (worker.joinable()) { 66 | worker.join(); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * @brief Submit task to thread pool 73 | * @param f Task function 74 | * @param args Task parameters 75 | * @return Task future 76 | */ 77 | template 78 | auto enqueue(F&& f, Args&&... args) -> std::future::type> { 79 | using return_type = typename std::invoke_result::type; 80 | 81 | auto task = std::make_shared>( 82 | std::bind(std::forward(f), std::forward(args)...) 83 | ); 84 | 85 | std::future result = task->get_future(); 86 | 87 | { 88 | std::unique_lock lock(queue_mutex_); 89 | 90 | if (stop_) { 91 | throw std::runtime_error("Thread pool stopped, cannot add task"); 92 | } 93 | 94 | tasks_.emplace([task]() { (*task)(); }); 95 | } 96 | 97 | condition_.notify_one(); 98 | return result; 99 | } 100 | 101 | private: 102 | // Worker threads 103 | std::vector workers_; 104 | 105 | // Task queue 106 | std::queue> tasks_; 107 | 108 | // Mutex and condition variable 109 | std::mutex queue_mutex_; 110 | std::condition_variable condition_; 111 | 112 | // Stop flag 113 | std::atomic stop_; 114 | }; 115 | 116 | } // namespace mcp 117 | 118 | #endif // MCP_THREAD_POOL_H -------------------------------------------------------------------------------- /examples/stdio_client_example.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file stdio_client_example.cpp 3 | * @brief Example of using the MCP stdio client 4 | * 5 | * This example demonstrates how to use the MCP stdio client to connect to a server 6 | * using standard input/output as the transport mechanism. 7 | */ 8 | 9 | #include "mcp_stdio_client.h" 10 | #include "mcp_logger.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | int main(int argc, char** argv) { 18 | // Set log level to info 19 | mcp::set_log_level(mcp::log_level::info); 20 | 21 | // Check command line arguments 22 | if (argc < 2) { 23 | std::cerr << "Usage: " << argv[0] << " " << std::endl; 24 | std::cerr << "Example: " << argv[0] << " \"npx -y @modelcontextprotocol/server-everything\"" << std::endl; 25 | return 1; 26 | } 27 | 28 | std::string command = argv[1]; 29 | 30 | // Set example environment variables 31 | mcp::json env_vars = { 32 | {"MCP_DEBUG", "1"}, 33 | {"MCP_LOG_LEVEL", "debug"}, 34 | {"CUSTOM_VAR", "custom_value"} 35 | }; 36 | 37 | // Create client 38 | mcp::stdio_client client(command, env_vars); 39 | 40 | // Initialize client 41 | if (!client.initialize("MCP Stdio Client Example", "1.0.0")) { 42 | std::cerr << "Failed to initialize client" << std::endl; 43 | return 1; 44 | } 45 | 46 | std::cout << "Client initialized successfully" << std::endl; 47 | 48 | try { 49 | // Get server capabilities 50 | auto capabilities = client.get_server_capabilities(); 51 | std::cout << "Server capabilities: " << capabilities.dump(2) << std::endl; 52 | 53 | // List available tools 54 | if (capabilities.contains("tools")) { 55 | auto tools = client.get_tools(); 56 | std::cout << "Available tools: " << tools.size() << std::endl; 57 | for (const auto& tool : tools) { 58 | std::cout << " - " << tool.name << ": " << tool.description << std::endl; 59 | // std::cout << tool.to_json().dump(2) << std::endl; 60 | } 61 | } 62 | 63 | // List available resources 64 | if (capabilities.contains("resources")) { 65 | auto resources = client.list_resources(); 66 | std::cout << "Available resources: " << resources.dump(2) << std::endl; 67 | 68 | // If there are resources, read the first one 69 | if (resources.contains("resources") && resources["resources"].is_array() && !resources["resources"].empty()) { 70 | auto resource = resources["resources"][0]; 71 | if (resource.contains("uri")) { 72 | std::string uri = resource["uri"]; 73 | std::cout << "Reading resource: " << uri << std::endl; 74 | 75 | auto content = client.read_resource(uri); 76 | std::cout << "Resource content: " << content.dump(2) << std::endl; 77 | } 78 | } 79 | } 80 | 81 | // Keep connection alive for 5 seconds 82 | std::cout << "Keeping connection alive for 5 seconds..." << std::endl; 83 | std::this_thread::sleep_for(std::chrono::seconds(5)); 84 | 85 | // Send ping request 86 | bool ping_result = client.ping(); 87 | std::cout << "Ping result: " << (ping_result ? "success" : "failure") << std::endl; 88 | 89 | } catch (const std::exception& e) { 90 | std::cerr << "Error: " << e.what() << std::endl; 91 | return 1; 92 | } 93 | 94 | std::cout << "Example completed successfully" << std::endl; 95 | return 0; 96 | } -------------------------------------------------------------------------------- /include/mcp_logger.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_logger.h 3 | * @brief Simple logger 4 | */ 5 | 6 | #ifndef MCP_LOGGER_H 7 | #define MCP_LOGGER_H 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace mcp { 17 | 18 | enum class log_level { 19 | debug, 20 | info, 21 | warning, 22 | error 23 | }; 24 | 25 | class logger { 26 | public: 27 | static logger& instance() { 28 | static logger instance; 29 | return instance; 30 | } 31 | 32 | void set_level(log_level level) { 33 | std::lock_guard lock(mutex_); 34 | level_ = level; 35 | } 36 | 37 | template 38 | void debug(Args&&... args) { 39 | log(log_level::debug, std::forward(args)...); 40 | } 41 | 42 | template 43 | void info(Args&&... args) { 44 | log(log_level::info, std::forward(args)...); 45 | } 46 | 47 | template 48 | void warning(Args&&... args) { 49 | log(log_level::warning, std::forward(args)...); 50 | } 51 | 52 | template 53 | void error(Args&&... args) { 54 | log(log_level::error, std::forward(args)...); 55 | } 56 | 57 | private: 58 | logger() : level_(log_level::info) {} 59 | 60 | template 61 | void log_impl(std::stringstream& ss, T&& arg) { 62 | ss << std::forward(arg); 63 | } 64 | 65 | template 66 | void log_impl(std::stringstream& ss, T&& arg, Args&&... args) { 67 | ss << std::forward(arg); 68 | log_impl(ss, std::forward(args)...); 69 | } 70 | 71 | template 72 | void log(log_level level, Args&&... args) { 73 | if (level < level_) { 74 | return; 75 | } 76 | 77 | std::stringstream ss; 78 | 79 | // Add timestamp 80 | auto now = std::chrono::system_clock::now(); 81 | auto now_c = std::chrono::system_clock::to_time_t(now); 82 | auto now_tm = std::localtime(&now_c); 83 | 84 | ss << std::put_time(now_tm, "%Y-%m-%d %H:%M:%S") << " "; 85 | 86 | // Add log level and color 87 | switch (level) { 88 | case log_level::debug: 89 | ss << "\033[36m[DEBUG]\033[0m "; // Cyan 90 | break; 91 | case log_level::info: 92 | ss << "\033[32m[INFO]\033[0m "; // Green 93 | break; 94 | case log_level::warning: 95 | ss << "\033[33m[WARNING]\033[0m "; // Yellow 96 | break; 97 | case log_level::error: 98 | ss << "\033[31m[ERROR]\033[0m "; // Red 99 | break; 100 | } 101 | 102 | // Add log content 103 | log_impl(ss, std::forward(args)...); 104 | 105 | // Output log 106 | std::lock_guard lock(mutex_); 107 | std::cerr << ss.str() << std::endl; 108 | } 109 | 110 | log_level level_; 111 | std::mutex mutex_; 112 | }; 113 | 114 | #define LOG_DEBUG(...) mcp::logger::instance().debug(__VA_ARGS__) 115 | #define LOG_INFO(...) mcp::logger::instance().info(__VA_ARGS__) 116 | #define LOG_WARNING(...) mcp::logger::instance().warning(__VA_ARGS__) 117 | #define LOG_ERROR(...) mcp::logger::instance().error(__VA_ARGS__) 118 | 119 | inline void set_log_level(log_level level) { 120 | mcp::logger::instance().set_level(level); 121 | } 122 | 123 | } // namespace mcp 124 | 125 | #endif // MCP_LOGGER_H -------------------------------------------------------------------------------- /examples/sse_client_example.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file client_example.cpp 3 | * @brief Example of an MCP client implementation 4 | * 5 | * This file demonstrates how to create an MCP client that connects to a server. 6 | * Follows the 2024-11-05 basic protocol specification. 7 | */ 8 | 9 | #include "mcp_sse_client.h" 10 | #include 11 | #include 12 | 13 | int main() { 14 | // Create a client 15 | // mcp::sse_client client("https://localhost:8888", "/sse", true, "/etc/ssl/certs/ca-certificates.crt"); 16 | // mcp::sse_client client("https://localhost:8888", "/sse", true, "./ca.cert.pem"); 17 | mcp::sse_client client("http://localhost:8888"); 18 | 19 | // Set capabilites 20 | mcp::json capabilities = { 21 | {"roots", {{"listChanged", true}}} 22 | }; 23 | client.set_capabilities(capabilities); 24 | 25 | // Set timeout 26 | client.set_timeout(10); 27 | 28 | try { 29 | // Initialize the connection 30 | std::cout << "Initializing connection to MCP server..." << std::endl; 31 | bool initialized = client.initialize("ExampleClient", mcp::MCP_VERSION); 32 | 33 | if (!initialized) { 34 | std::cerr << "Failed to initialize connection to server" << std::endl; 35 | return 1; 36 | } 37 | 38 | // Ping the server 39 | std::cout << "Pinging server..." << std::endl; 40 | if (!client.ping()) { 41 | std::cerr << "Failed to ping server" << std::endl; 42 | return 1; 43 | } 44 | 45 | // Get server capabilities 46 | std::cout << "Getting server capabilities..." << std::endl; 47 | mcp::json capabilities = client.get_server_capabilities(); 48 | std::cout << "Server capabilities: " << capabilities.dump(4) << std::endl; 49 | 50 | // Get available tools 51 | std::cout << "\nGetting available tools..." << std::endl; 52 | auto tools = client.get_tools(); 53 | std::cout << "Available tools:" << std::endl; 54 | for (const auto& tool : tools) { 55 | std::cout << "- " << tool.name << ": " << tool.description << std::endl; 56 | } 57 | 58 | // Get available resources 59 | 60 | // Call the get_time tool 61 | std::cout << "\nCalling get_time tool..." << std::endl; 62 | mcp::json time_result = client.call_tool("get_time"); 63 | std::cout << "Current time: " << time_result["content"][0]["text"].get() << std::endl; 64 | 65 | // Call the echo tool 66 | std::cout << "\nCalling echo tool..." << std::endl; 67 | mcp::json echo_params = { 68 | {"text", "Hello, MCP!"}, 69 | {"uppercase", true} 70 | }; 71 | mcp::json echo_result = client.call_tool("echo", echo_params); 72 | std::cout << "Echo result: " << echo_result["content"][0]["text"].get() << std::endl; 73 | 74 | // Call the calculator tool 75 | std::cout << "\nCalling calculator tool..." << std::endl; 76 | mcp::json calc_params = { 77 | {"operation", "add"}, 78 | {"a", 10}, 79 | {"b", 5} 80 | }; 81 | mcp::json calc_result = client.call_tool("calculator", calc_params); 82 | std::cout << "10 + 5 = " << calc_result["content"][0]["text"].get() << std::endl; 83 | } catch (const mcp::mcp_exception& e) { 84 | std::cerr << "MCP error: " << e.what() << " (code: " << static_cast(e.code()) << ")" << std::endl; 85 | return 1; 86 | } catch (const std::exception& e) { 87 | std::cerr << "Error: " << e.what() << std::endl; 88 | return 1; 89 | } 90 | 91 | std::cout << "\nClient example completed successfully" << std::endl; 92 | return 0; 93 | } -------------------------------------------------------------------------------- /src/mcp_tool.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_tool.cpp 3 | * @brief Implementation of the MCP tools 4 | * 5 | * This file implements the tool-related functionality for the MCP protocol. 6 | * Follows the 2024-11-05 basic protocol specification. 7 | */ 8 | 9 | #include "mcp_tool.h" 10 | #include 11 | #include 12 | 13 | namespace mcp { 14 | 15 | // Implementation for tool_builder 16 | tool_builder::tool_builder(const std::string& name) 17 | : name_(name) { 18 | } 19 | 20 | tool_builder& tool_builder::with_description(const std::string& description) { 21 | description_ = description; 22 | return *this; 23 | } 24 | 25 | tool_builder& tool_builder::add_param(const std::string& name, 26 | const std::string& description, 27 | const std::string& type, 28 | bool required) { 29 | json param = { 30 | {"type", type}, 31 | {"description", description} 32 | }; 33 | 34 | parameters_["properties"][name] = param; 35 | 36 | if (required) { 37 | required_params_.push_back(name); 38 | } 39 | 40 | return *this; 41 | } 42 | 43 | tool_builder& tool_builder::with_string_param(const std::string& name, 44 | const std::string& description, 45 | bool required) { 46 | return add_param(name, description, "string", required); 47 | } 48 | 49 | tool_builder& tool_builder::with_number_param(const std::string& name, 50 | const std::string& description, 51 | bool required) { 52 | return add_param(name, description, "number", required); 53 | } 54 | 55 | tool_builder& tool_builder::with_boolean_param(const std::string& name, 56 | const std::string& description, 57 | bool required) { 58 | return add_param(name, description, "boolean", required); 59 | } 60 | 61 | tool_builder& tool_builder::with_array_param(const std::string& name, 62 | const std::string& description, 63 | const std::string& item_type, 64 | bool required) { 65 | json param = { 66 | {"type", "array"}, 67 | {"description", description}, 68 | {"items", { 69 | {"type", item_type} 70 | }} 71 | }; 72 | 73 | parameters_["properties"][name] = param; 74 | 75 | if (required) { 76 | required_params_.push_back(name); 77 | } 78 | 79 | return *this; 80 | } 81 | 82 | tool_builder& tool_builder::with_object_param(const std::string& name, 83 | const std::string& description, 84 | const json& properties, 85 | bool required) { 86 | json param = { 87 | {"type", "object"}, 88 | {"description", description}, 89 | {"properties", properties} 90 | }; 91 | 92 | parameters_["properties"][name] = param; 93 | 94 | if (required) { 95 | required_params_.push_back(name); 96 | } 97 | 98 | return *this; 99 | } 100 | 101 | tool tool_builder::build() const { 102 | tool t; 103 | t.name = name_; 104 | t.description = description_; 105 | 106 | // Create the parameters schema 107 | json schema = parameters_; 108 | schema["type"] = "object";; 109 | 110 | if (!required_params_.empty()) { 111 | schema["required"] = required_params_; 112 | } 113 | 114 | t.parameters_schema = schema; 115 | 116 | return t; 117 | } 118 | 119 | } // namespace mcp -------------------------------------------------------------------------------- /include/mcp_client.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_client.h 3 | * @brief MCP Client interface 4 | * 5 | * This file defines the interface for the Model Context Protocol clients. 6 | * Follows the 2024-11-05 protocol specification. 7 | */ 8 | 9 | #ifndef MCP_CLIENT_H 10 | #define MCP_CLIENT_H 11 | 12 | #include "mcp_message.h" 13 | #include "mcp_tool.h" 14 | #include "mcp_logger.h" 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | namespace mcp { 21 | 22 | /** 23 | * @class client 24 | * @brief Abstract interface for MCP clients 25 | * 26 | * The client class defines the interface for all MCP client implementations, 27 | * regardless of the transport mechanism used (HTTP/SSE, stdio, etc.). 28 | */ 29 | class client { 30 | public: 31 | /** 32 | * @brief Virtual destructor 33 | */ 34 | virtual ~client() = default; 35 | 36 | /** 37 | * @brief Initialize the connection with the server 38 | * @param client_name The name of the client 39 | * @param client_version The version of the client 40 | * @return True if initialization was successful 41 | */ 42 | virtual bool initialize(const std::string& client_name, const std::string& client_version) = 0; 43 | 44 | /** 45 | * @brief Ping request 46 | * @return True if the server is alive 47 | */ 48 | virtual bool ping() = 0; 49 | 50 | /** 51 | * @brief Set client capabilities 52 | * @param capabilities The capabilities of the client 53 | */ 54 | virtual void set_capabilities(const json& capabilities) = 0; 55 | 56 | /** 57 | * @brief Send a request and wait for a response 58 | * @param method The method to call 59 | * @param params The parameters to pass 60 | * @return The response 61 | * @throws mcp_exception on error 62 | */ 63 | virtual response send_request(const std::string& method, const json& params = json::object()) = 0; 64 | 65 | /** 66 | * @brief Send a notification (no response expected) 67 | * @param method The method to call 68 | * @param params The parameters to pass 69 | * @throws mcp_exception on error 70 | */ 71 | virtual void send_notification(const std::string& method, const json& params = json::object()) = 0; 72 | 73 | /** 74 | * @brief Get server capabilities 75 | * @return The server capabilities 76 | * @throws mcp_exception on error 77 | */ 78 | virtual json get_server_capabilities() = 0; 79 | 80 | /** 81 | * @brief Call a tool 82 | * @param tool_name The name of the tool to call 83 | * @param arguments The arguments to pass to the tool 84 | * @return The result of the tool call 85 | * @throws mcp_exception on error 86 | */ 87 | virtual json call_tool(const std::string& tool_name, const json& arguments = json::object()) = 0; 88 | 89 | /** 90 | * @brief Get available tools 91 | * @return List of available tools 92 | * @throws mcp_exception on error 93 | */ 94 | virtual std::vector get_tools() = 0; 95 | 96 | /** 97 | * @brief Get client capabilities 98 | * @return The client capabilities 99 | */ 100 | virtual json get_capabilities() = 0; 101 | 102 | /** 103 | * @brief List available resources 104 | * @param cursor Optional cursor for pagination 105 | * @return List of resources 106 | */ 107 | virtual json list_resources(const std::string& cursor = "") = 0; 108 | 109 | /** 110 | * @brief Read a resource 111 | * @param resource_uri The URI of the resource 112 | * @return The resource content 113 | */ 114 | virtual json read_resource(const std::string& resource_uri) = 0; 115 | 116 | /** 117 | * @brief Subscribe to resource changes 118 | * @param resource_uri The URI of the resource 119 | * @return Subscription result 120 | */ 121 | virtual json subscribe_to_resource(const std::string& resource_uri) = 0; 122 | 123 | /** 124 | * @brief List resource templates 125 | * @return List of resource templates 126 | */ 127 | virtual json list_resource_templates() = 0; 128 | 129 | /** 130 | * @brief Check if the client is running 131 | * @return True if the client is running 132 | */ 133 | virtual bool is_running() const = 0; 134 | }; 135 | 136 | } // namespace mcp 137 | 138 | #endif // MCP_CLIENT_H -------------------------------------------------------------------------------- /include/mcp_message.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_message.h 3 | * @brief Core definitions for the Model Context Protocol (MCP) framework 4 | * 5 | * This file contains the core structures and definitions for the MCP protocol. 6 | * Implements the 2024-11-05 basic protocol specification. 7 | */ 8 | 9 | #ifndef MCP_MESSAGE_H 10 | #define MCP_MESSAGE_H 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // Include the JSON library for parsing and generating JSON 20 | #include "json.hpp" 21 | 22 | namespace mcp { 23 | 24 | // Use the nlohmann json library 25 | using json = nlohmann::ordered_json; 26 | 27 | // MCP version 28 | constexpr const char* MCP_VERSION = "2024-11-05"; 29 | 30 | // MCP error codes (JSON-RPC 2.0 standard codes) 31 | enum class error_code { 32 | parse_error = -32700, // Invalid JSON 33 | invalid_request = -32600, // Invalid Request object 34 | method_not_found = -32601, // Method not found 35 | invalid_params = -32602, // Invalid method parameters 36 | internal_error = -32603, // Internal JSON-RPC error 37 | server_error_start = -32000, // Server error start 38 | server_error_end = -32099 // Server error end 39 | }; 40 | 41 | // MCP exception class 42 | class mcp_exception : public std::runtime_error { 43 | public: 44 | mcp_exception(error_code code, const std::string& message) 45 | : std::runtime_error(message), code_(code) {} 46 | 47 | error_code code() const { return code_; } 48 | 49 | private: 50 | error_code code_; 51 | }; 52 | 53 | // JSON-RPC 2.0 Request 54 | struct request { 55 | std::string jsonrpc = "2.0"; 56 | json id; 57 | std::string method; 58 | json params; 59 | 60 | // Create a request 61 | static request create(const std::string& method, const json& params = json::object()) { 62 | request req; 63 | req.jsonrpc = "2.0"; 64 | req.id = generate_id(); 65 | req.method = method; 66 | req.params = params; 67 | return req; 68 | } 69 | 70 | // Create a request with a specific ID 71 | static request create_with_id(const json& id, const std::string& method, const json& params = json::object()) { 72 | request req; 73 | req.jsonrpc = "2.0"; 74 | req.id = id; 75 | req.method = method; 76 | req.params = params; 77 | return req; 78 | } 79 | 80 | // Create a notification (no response expected) 81 | static request create_notification(const std::string& method, const json& params = json::object()) { 82 | request req; 83 | req.jsonrpc = "2.0"; 84 | req.id = nullptr; 85 | req.method = "notifications/" + method; 86 | req.params = params; 87 | return req; 88 | } 89 | 90 | // Check if this is a notification 91 | bool is_notification() const { 92 | return id.is_null(); 93 | } 94 | 95 | // Convert to JSON 96 | json to_json() const { 97 | json j = { 98 | {"jsonrpc", jsonrpc}, 99 | {"method", method} 100 | }; 101 | 102 | if (!params.empty()) { 103 | j["params"] = params; 104 | } 105 | 106 | if (!is_notification()) { 107 | j["id"] = id; 108 | } 109 | 110 | return j; 111 | } 112 | 113 | static request from_json(const json& j) { 114 | request req; 115 | req.jsonrpc = j["jsonrpc"].get(); 116 | req.id = j["id"]; 117 | req.method = j["method"].get(); 118 | req.params = j["params"]; 119 | return req; 120 | } 121 | 122 | private: 123 | // Generate a unique ID 124 | static json generate_id() { 125 | static int next_id = 1; 126 | return next_id++; 127 | } 128 | }; 129 | 130 | // JSON-RPC 2.0 Response 131 | struct response { 132 | std::string jsonrpc = "2.0"; 133 | json id; 134 | json result; 135 | json error; 136 | 137 | // Create a success response 138 | static response create_success(const json& req_id, const json& result_data = json::object()) { 139 | response res; 140 | res.jsonrpc = "2.0"; 141 | res.id = req_id; 142 | res.result = result_data; 143 | return res; 144 | } 145 | 146 | // Create an error response 147 | static response create_error(const json& req_id, error_code code, const std::string& message, const json& data = json::object()) { 148 | response res; 149 | res.jsonrpc = "2.0"; 150 | res.id = req_id; 151 | res.error = { 152 | {"code", static_cast(code)}, 153 | {"message", message} 154 | }; 155 | 156 | if (!data.empty()) { 157 | res.error["data"] = data; 158 | } 159 | 160 | return res; 161 | } 162 | 163 | // Check if this is an error response 164 | bool is_error() const { 165 | return !error.empty(); 166 | } 167 | 168 | // Convert to JSON 169 | json to_json() const { 170 | json j = { 171 | {"jsonrpc", jsonrpc}, 172 | {"id", id} 173 | }; 174 | 175 | if (is_error()) { 176 | j["error"] = error; 177 | } else { 178 | j["result"] = result; 179 | } 180 | 181 | return j; 182 | } 183 | 184 | static response from_json(const json& j) { 185 | response res; 186 | res.jsonrpc = j["jsonrpc"].get(); 187 | res.id = j["id"]; 188 | res.result = j["result"]; 189 | res.error = j["error"]; 190 | return res; 191 | } 192 | }; 193 | 194 | } // namespace mcp 195 | 196 | #endif // MCP_MESSAGE_H -------------------------------------------------------------------------------- /include/mcp_tool.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_tool.h 3 | * @brief Tool definitions and helper functions for MCP 4 | * 5 | * This file provides tool-related functionality and abstractions for the MCP protocol. 6 | */ 7 | 8 | #ifndef MCP_TOOL_H 9 | #define MCP_TOOL_H 10 | 11 | #include "mcp_message.h" 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace mcp { 21 | 22 | // MCP Tool definition 23 | struct tool { 24 | std::string name; 25 | std::string description; 26 | json parameters_schema; 27 | 28 | // Convert to JSON for API documentation 29 | json to_json() const { 30 | return { 31 | {"name", name}, 32 | {"description", description}, 33 | {"inputSchema", parameters_schema} // You may need `parameters` instead of `inputSchema` for OAI format 34 | }; 35 | } 36 | }; 37 | 38 | /** 39 | * @class tool_builder 40 | * @brief Utility class for building tools with a fluent API 41 | * 42 | * The tool_builder class provides a simple way to create tools with 43 | * a fluent (chain-based) API. 44 | */ 45 | class tool_builder { 46 | public: 47 | /** 48 | * @brief Constructor 49 | * @param name The name of the tool 50 | */ 51 | explicit tool_builder(const std::string& name); 52 | 53 | /** 54 | * @brief Set the tool description 55 | * @param description The description 56 | * @return Reference to this builder 57 | */ 58 | tool_builder& with_description(const std::string& description); 59 | 60 | /** 61 | * @brief Add a string parameter 62 | * @param name The parameter name 63 | * @param description The parameter description 64 | * @param required Whether the parameter is required 65 | * @return Reference to this builder 66 | */ 67 | tool_builder& with_string_param(const std::string& name, 68 | const std::string& description, 69 | bool required = true); 70 | 71 | /** 72 | * @brief Add a number parameter 73 | * @param name The parameter name 74 | * @param description The parameter description 75 | * @param required Whether the parameter is required 76 | * @return Reference to this builder 77 | */ 78 | tool_builder& with_number_param(const std::string& name, 79 | const std::string& description, 80 | bool required = true); 81 | 82 | /** 83 | * @brief Add a boolean parameter 84 | * @param name The parameter name 85 | * @param description The parameter description 86 | * @param required Whether the parameter is required 87 | * @return Reference to this builder 88 | */ 89 | tool_builder& with_boolean_param(const std::string& name, 90 | const std::string& description, 91 | bool required = true); 92 | 93 | /** 94 | * @brief Add an array parameter 95 | * @param name The parameter name 96 | * @param description The parameter description 97 | * @param item_type The type of the array items ("string", "number", "object", etc.) 98 | * @param required Whether the parameter is required 99 | * @return Reference to this builder 100 | */ 101 | tool_builder& with_array_param(const std::string& name, 102 | const std::string& description, 103 | const std::string& item_type, 104 | bool required = true); 105 | 106 | /** 107 | * @brief Add an object parameter 108 | * @param name The parameter name 109 | * @param description The parameter description 110 | * @param properties JSON schema for the object properties 111 | * @param required Whether the parameter is required 112 | * @return Reference to this builder 113 | */ 114 | tool_builder& with_object_param(const std::string& name, 115 | const std::string& description, 116 | const json& properties, 117 | bool required = true); 118 | 119 | /** 120 | * @brief Build the tool 121 | * @return The constructed tool 122 | */ 123 | tool build() const; 124 | 125 | private: 126 | std::string name_; 127 | std::string description_; 128 | json parameters_; 129 | std::vector required_params_; 130 | 131 | // Helper to add a parameter of any type 132 | tool_builder& add_param(const std::string& name, 133 | const std::string& description, 134 | const std::string& type, 135 | bool required); 136 | }; 137 | 138 | /** 139 | * @brief Create a simple tool with a function-based approach 140 | * @param name Tool name 141 | * @param description Tool description 142 | * @param handler Function to handle tool invocations 143 | * @param parameter_definitions A vector of parameter definitions as {name, description, type, required} 144 | * @return The created tool 145 | */ 146 | inline tool create_tool( 147 | const std::string& name, 148 | const std::string& description, 149 | const std::vector>& parameter_definitions) { 150 | 151 | tool_builder builder(name); 152 | builder.with_description(description); 153 | 154 | for (const auto& [param_name, param_desc, param_type, required] : parameter_definitions) { 155 | if (param_type == "string") { 156 | builder.with_string_param(param_name, param_desc, required); 157 | } else if (param_type == "number") { 158 | builder.with_number_param(param_name, param_desc, required); 159 | } else if (param_type == "boolean") { 160 | builder.with_boolean_param(param_name, param_desc, required); 161 | } else if (param_type == "array") { 162 | builder.with_array_param(param_name, param_desc, "string", required); 163 | } else if (param_type == "object") { 164 | builder.with_object_param(param_name, param_desc, json::object(), required); 165 | } 166 | } 167 | 168 | return builder.build(); 169 | } 170 | 171 | } // namespace mcp 172 | 173 | #endif // MCP_TOOL_H -------------------------------------------------------------------------------- /examples/server_example.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file server_example.cpp 3 | * @brief Server example based on MCP protocol 4 | * 5 | * This example demonstrates how to create an MCP server, register tools and resources, 6 | * and handle client requests. Follows the 2024-11-05 basic protocol specification. 7 | */ 8 | #include "mcp_server.h" 9 | #include "mcp_tool.h" 10 | #include "mcp_resource.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // Tool handler for getting current time 20 | mcp::json get_time_handler(const mcp::json& params, const std::string& /* session_id */) { 21 | auto now = std::chrono::system_clock::now(); 22 | auto time_t_now = std::chrono::system_clock::to_time_t(now); 23 | 24 | std::string time_str = std::ctime(&time_t_now); 25 | // Remove trailing newline 26 | if (!time_str.empty() && time_str[time_str.length() - 1] == '\n') { 27 | time_str.erase(time_str.length() - 1); 28 | } 29 | 30 | return { 31 | { 32 | {"type", "text"}, 33 | {"text", time_str} 34 | } 35 | }; 36 | } 37 | 38 | // Echo tool handler 39 | mcp::json echo_handler(const mcp::json& params, const std::string& /* session_id */) { 40 | mcp::json result = params; 41 | 42 | if (params.contains("text")) { 43 | std::string text = params["text"]; 44 | 45 | if (params.contains("uppercase") && params["uppercase"].get()) { 46 | std::transform(text.begin(), text.end(), text.begin(), ::toupper); 47 | result["text"] = text; 48 | } 49 | 50 | if (params.contains("reverse") && params["reverse"].get()) { 51 | std::reverse(text.begin(), text.end()); 52 | result["text"] = text; 53 | } 54 | } 55 | 56 | return { 57 | { 58 | {"type", "text"}, 59 | {"text", result["text"].get()} 60 | } 61 | }; 62 | } 63 | 64 | // Calculator tool handler 65 | mcp::json calculator_handler(const mcp::json& params, const std::string& /* session_id */) { 66 | if (!params.contains("operation")) { 67 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'operation' parameter"); 68 | } 69 | 70 | std::string operation = params["operation"]; 71 | double result = 0.0; 72 | 73 | if (operation == "add") { 74 | if (!params.contains("a") || !params.contains("b")) { 75 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); 76 | } 77 | result = params["a"].get() + params["b"].get(); 78 | } else if (operation == "subtract") { 79 | if (!params.contains("a") || !params.contains("b")) { 80 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); 81 | } 82 | result = params["a"].get() - params["b"].get(); 83 | } else if (operation == "multiply") { 84 | if (!params.contains("a") || !params.contains("b")) { 85 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); 86 | } 87 | result = params["a"].get() * params["b"].get(); 88 | } else if (operation == "divide") { 89 | if (!params.contains("a") || !params.contains("b")) { 90 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); 91 | } 92 | if (params["b"].get() == 0.0) { 93 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Division by zero not allowed"); 94 | } 95 | result = params["a"].get() / params["b"].get(); 96 | } else { 97 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Unknown operation: " + operation); 98 | } 99 | 100 | return { 101 | { 102 | {"type", "text"}, 103 | {"text", std::to_string(result)} 104 | } 105 | }; 106 | } 107 | 108 | // Custom API endpoint handler 109 | mcp::json hello_handler(const mcp::json& params, const std::string& /* session_id */) { 110 | std::string name = params.contains("name") ? params["name"].get() : "World"; 111 | return { 112 | { 113 | {"type", "text"}, 114 | {"text", "Hello, " + name + "!"} 115 | } 116 | }; 117 | } 118 | 119 | int main() { 120 | // Ensure file directory exists 121 | std::filesystem::create_directories("./files"); 122 | 123 | // Create and configure server 124 | mcp::server::configuration srv_conf; 125 | srv_conf.host = "localhost"; 126 | srv_conf.port = 8888; 127 | // srv_conf.threadpool_size = 1; 128 | // srv_conf.ssl.server_cert_path = "./server.cert.pem"; 129 | // srv_conf.ssl.server_private_key_path = "./server.key.pem"; 130 | 131 | mcp::server server(srv_conf); 132 | server.set_server_info("ExampleServer", "1.0.0"); 133 | 134 | // Set server capabilities 135 | // mcp::json capabilities = { 136 | // {"tools", {{"listChanged", true}}}, 137 | // {"resources", {{"subscribe", false}, {"listChanged", true}}} 138 | // }; 139 | mcp::json capabilities = { 140 | {"tools", mcp::json::object()} 141 | }; 142 | server.set_capabilities(capabilities); 143 | 144 | // Register tools 145 | mcp::tool time_tool = mcp::tool_builder("get_time") 146 | .with_description("Get current time") 147 | .build(); 148 | 149 | mcp::tool echo_tool = mcp::tool_builder("echo") 150 | .with_description("Echo input with optional transformations") 151 | .with_string_param("text", "Text to echo") 152 | .with_boolean_param("uppercase", "Convert to uppercase", false) 153 | .with_boolean_param("reverse", "Reverse the text", false) 154 | .build(); 155 | 156 | mcp::tool calc_tool = mcp::tool_builder("calculator") 157 | .with_description("Perform basic calculations") 158 | .with_string_param("operation", "Operation to perform (add, subtract, multiply, divide)") 159 | .with_number_param("a", "First operand") 160 | .with_number_param("b", "Second operand") 161 | .build(); 162 | 163 | mcp::tool hello_tool = mcp::tool_builder("hello") 164 | .with_description("Say hello") 165 | .with_string_param("name", "Name to say hello to", "World") 166 | .build(); 167 | 168 | server.register_tool(time_tool, get_time_handler); 169 | server.register_tool(echo_tool, echo_handler); 170 | server.register_tool(calc_tool, calculator_handler); 171 | server.register_tool(hello_tool, hello_handler); 172 | 173 | // // Register resources 174 | // auto file_resource = std::make_shared("./Makefile"); 175 | // server.register_resource("file://./Makefile", file_resource); 176 | 177 | // Start server 178 | std::cout << "Starting MCP server at " << srv_conf.host << ":" << srv_conf.port << std::endl; 179 | std::cout << "Press Ctrl+C to stop the server" << std::endl; 180 | server.start(true); // Blocking mode 181 | 182 | return 0; 183 | } 184 | -------------------------------------------------------------------------------- /include/mcp_stdio_client.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_stdio_client.h 3 | * @brief MCP Stdio Client implementation 4 | * 5 | * This file implements the client-side functionality for the Model Context Protocol 6 | * using standard input/output (stdio) as the transport mechanism. 7 | * Follows the 2024-11-05 protocol specification. 8 | */ 9 | 10 | #ifndef MCP_STDIO_CLIENT_H 11 | #define MCP_STDIO_CLIENT_H 12 | 13 | #include "mcp_client.h" 14 | #include "mcp_message.h" 15 | #include "mcp_tool.h" 16 | #include "mcp_logger.h" 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #if defined(_WIN32) 30 | #include 31 | #endif 32 | 33 | namespace mcp { 34 | 35 | /** 36 | * @class stdio_client 37 | * @brief Client for connecting to MCP servers using stdio transport 38 | * 39 | * The stdio_client class provides functionality to connect to MCP servers 40 | * by spawning a separate process and communicating via standard input/output. 41 | */ 42 | class stdio_client : public client { 43 | public: 44 | /** 45 | * @brief Constructor 46 | * @param command The command to execute to start the server 47 | * @param env_vars Optional environment variables to set for the server process 48 | * @param capabilities The capabilities of the client 49 | */ 50 | stdio_client(const std::string& command, 51 | const json& env_vars = json::object(), 52 | const json& capabilities = json::object()); 53 | 54 | /** 55 | * @brief Destructor 56 | */ 57 | ~stdio_client() override; 58 | 59 | /** 60 | * @brief Set environment variables for the server process 61 | * @param env_vars JSON object containing environment variables (key: variable name, value: variable value) 62 | * @note This must be called before initialize() 63 | */ 64 | void set_environment_variables(const json& env_vars); 65 | 66 | /** 67 | * @brief Initialize the connection with the server 68 | * @param client_name The name of the client 69 | * @param client_version The version of the client 70 | * @return True if initialization was successful 71 | */ 72 | bool initialize(const std::string& client_name, const std::string& client_version) override; 73 | 74 | /** 75 | * @brief Ping request 76 | * @return True if the server is alive 77 | */ 78 | bool ping() override; 79 | 80 | /** 81 | * @brief Set client capabilities 82 | * @param capabilities The capabilities of the client 83 | */ 84 | void set_capabilities(const json& capabilities) override; 85 | 86 | /** 87 | * @brief Send a request and wait for a response 88 | * @param method The method to call 89 | * @param params The parameters to pass 90 | * @return The response 91 | * @throws mcp_exception on error 92 | */ 93 | response send_request(const std::string& method, const json& params = json::object()) override; 94 | 95 | /** 96 | * @brief Send a notification (no response expected) 97 | * @param method The method to call 98 | * @param params The parameters to pass 99 | * @throws mcp_exception on error 100 | */ 101 | void send_notification(const std::string& method, const json& params = json::object()) override; 102 | 103 | /** 104 | * @brief Get server capabilities 105 | * @return The server capabilities 106 | * @throws mcp_exception on error 107 | */ 108 | json get_server_capabilities() override; 109 | 110 | /** 111 | * @brief Call a tool 112 | * @param tool_name The name of the tool to call 113 | * @param arguments The arguments to pass to the tool 114 | * @return The result of the tool call 115 | * @throws mcp_exception on error 116 | */ 117 | json call_tool(const std::string& tool_name, const json& arguments = json::object()) override; 118 | 119 | /** 120 | * @brief Get available tools 121 | * @return List of available tools 122 | * @throws mcp_exception on error 123 | */ 124 | std::vector get_tools() override; 125 | 126 | /** 127 | * @brief Get client capabilities 128 | * @return The client capabilities 129 | */ 130 | json get_capabilities() override; 131 | 132 | /** 133 | * @brief List available resources 134 | * @param cursor Optional cursor for pagination 135 | * @return List of resources 136 | */ 137 | json list_resources(const std::string& cursor = "") override; 138 | 139 | /** 140 | * @brief Read a resource 141 | * @param resource_uri The URI of the resource 142 | * @return The resource content 143 | */ 144 | json read_resource(const std::string& resource_uri) override; 145 | 146 | /** 147 | * @brief Subscribe to resource changes 148 | * @param resource_uri The URI of the resource 149 | * @return Subscription result 150 | */ 151 | json subscribe_to_resource(const std::string& resource_uri) override; 152 | 153 | /** 154 | * @brief List resource templates 155 | * @return List of resource templates 156 | */ 157 | json list_resource_templates() override; 158 | 159 | /** 160 | * @brief Check if the server process is running 161 | * @return True if the server process is running 162 | */ 163 | bool is_running() const override; 164 | 165 | private: 166 | // Start server process 167 | bool start_server_process(); 168 | 169 | // Stop server process 170 | void stop_server_process(); 171 | 172 | // Read thread function 173 | void read_thread_func(); 174 | 175 | // Send JSON-RPC request 176 | json send_jsonrpc(const request& req); 177 | 178 | // Server command 179 | std::string command_; 180 | 181 | // Process ID 182 | int process_id_ = -1; 183 | 184 | #if defined(_WIN32) 185 | // Windows platform specific process handle 186 | HANDLE process_handle_ = NULL; 187 | 188 | // Standard input/output pipes (Windows) 189 | HANDLE stdin_pipe_[2] = {NULL, NULL}; 190 | HANDLE stdout_pipe_[2] = {NULL, NULL}; 191 | #else 192 | // Standard input pipe (POSIX) 193 | int stdin_pipe_[2] = {-1, -1}; 194 | 195 | // Standard output pipe (POSIX) 196 | int stdout_pipe_[2] = {-1, -1}; 197 | #endif 198 | 199 | // Read thread 200 | std::unique_ptr read_thread_; 201 | 202 | // Running status 203 | std::atomic running_{false}; 204 | 205 | // Client capabilities 206 | json capabilities_; 207 | 208 | // Server capabilities 209 | json server_capabilities_; 210 | 211 | // Mutex 212 | mutable std::mutex mutex_; 213 | 214 | // Request ID to Promise mapping, used for asynchronous waiting for responses 215 | std::map> pending_requests_; 216 | 217 | // Response processing mutex 218 | std::mutex response_mutex_; 219 | 220 | // Initialization status 221 | std::atomic initialized_{false}; 222 | 223 | // Initialization condition variable 224 | std::condition_variable init_cv_; 225 | 226 | // Environment variables 227 | json env_vars_; 228 | }; 229 | 230 | } // namespace mcp 231 | 232 | #endif // MCP_STDIO_CLIENT_H -------------------------------------------------------------------------------- /include/mcp_sse_client.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_client.h 3 | * @brief MCP Client implementation 4 | * 5 | * This file implements the client-side functionality for the Model Context Protocol. 6 | * Follows the 2024-11-05 protocol specification. 7 | */ 8 | 9 | #ifndef MCP_SSE_CLIENT_H 10 | #define MCP_SSE_CLIENT_H 11 | 12 | #include "mcp_client.h" 13 | #include "mcp_message.h" 14 | #include "mcp_tool.h" 15 | #include "mcp_logger.h" 16 | 17 | // Include the HTTP library 18 | #include "httplib.h" 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | namespace mcp { 31 | 32 | /** 33 | * @class client 34 | * @brief Client for connecting to MCP servers 35 | * 36 | * The client class provides functionality to connect to MCP servers, 37 | * initialize the connection, and send/receive JSON-RPC messages. 38 | */ 39 | class sse_client : public client { 40 | public: 41 | /** 42 | * @brief Constructor 43 | * @param scheme_host_port The base URL of the server (e.g., "http://localhost:8080") 44 | * @param sse_endpoint The SSE endpoint (default: "/sse") 45 | * @param validate_certificates Whether to validate SSL certificates (default: true) 46 | * @param ca_cert_path path to CA certificate file for SSL validation (optional). 47 | * This is required if validate_certificates is true. 48 | */ 49 | sse_client(const std::string& scheme_host_port, const std::string& sse_endpoint = "/sse", 50 | bool validate_certificates = true, const std::string& ca_cert_path = ""); 51 | 52 | /** 53 | * @brief Destructor 54 | */ 55 | ~sse_client(); 56 | 57 | /** 58 | * @brief Initialize the connection with the server 59 | * @param client_name The name of the client 60 | * @param client_version The version of the client 61 | * @return True if initialization was successful 62 | */ 63 | bool initialize(const std::string& client_name, const std::string& client_version) override; 64 | 65 | /** 66 | * @brief Ping request 67 | * @return True if the server is alive 68 | */ 69 | bool ping() override; 70 | 71 | /** 72 | * @brief Set authentication token 73 | * @param token The authentication token 74 | */ 75 | void set_auth_token(const std::string& token); 76 | 77 | /** 78 | * @brief Set a request header that will be sent with all requests 79 | * @param key Header name 80 | * @param value Header value 81 | */ 82 | void set_header(const std::string& key, const std::string& value); 83 | 84 | /** 85 | * @brief Set timeout for requests 86 | * @param timeout_seconds Timeout in seconds 87 | */ 88 | void set_timeout(int timeout_seconds); 89 | 90 | /** 91 | * @brief Set client capabilities 92 | * @param capabilities The capabilities of the client 93 | */ 94 | void set_capabilities(const json& capabilities) override; 95 | 96 | /** 97 | * @brief Send a request and wait for a response 98 | * @param method The method to call 99 | * @param params The parameters to pass 100 | * @return The response 101 | * @throws mcp_exception on error 102 | */ 103 | response send_request(const std::string& method, const json& params = json::object()) override; 104 | 105 | /** 106 | * @brief Send a notification (no response expected) 107 | * @param method The method to call 108 | * @param params The parameters to pass 109 | * @throws mcp_exception on error 110 | */ 111 | void send_notification(const std::string& method, const json& params = json::object()) override; 112 | 113 | /** 114 | * @brief Get server capabilities 115 | * @return The server capabilities 116 | * @throws mcp_exception on error 117 | */ 118 | json get_server_capabilities() override; 119 | 120 | /** 121 | * @brief Call a tool 122 | * @param tool_name The name of the tool to call 123 | * @param arguments The arguments to pass to the tool 124 | * @return The result of the tool call 125 | * @throws mcp_exception on error 126 | */ 127 | json call_tool(const std::string& tool_name, const json& arguments = json::object()) override; 128 | 129 | /** 130 | * @brief Get available tools 131 | * @return List of available tools 132 | * @throws mcp_exception on error 133 | */ 134 | std::vector get_tools() override; 135 | 136 | /** 137 | * @brief Get client capabilities 138 | * @return The client capabilities 139 | */ 140 | json get_capabilities() override; 141 | 142 | /** 143 | * @brief List available resources 144 | * @param cursor Optional cursor for pagination 145 | * @return List of resources 146 | */ 147 | json list_resources(const std::string& cursor = "") override; 148 | 149 | /** 150 | * @brief Read a resource 151 | * @param resource_uri The URI of the resource 152 | * @return The resource content 153 | */ 154 | json read_resource(const std::string& resource_uri) override; 155 | 156 | /** 157 | * @brief Subscribe to resource changes 158 | * @param resource_uri The URI of the resource 159 | * @return Subscription result 160 | */ 161 | json subscribe_to_resource(const std::string& resource_uri) override; 162 | 163 | /** 164 | * @brief List resource templates 165 | * @return List of resource templates 166 | */ 167 | json list_resource_templates() override; 168 | 169 | /** 170 | * @brief Check if the client is running 171 | * @return True if the client is running 172 | */ 173 | bool is_running() const override; 174 | 175 | private: 176 | // Initialize HTTP client 177 | void init_client(const std::string& scheme_host_port, bool validate_certificates, const std::string& ca_cert_path); 178 | 179 | // Open SSE connection 180 | void open_sse_connection(); 181 | 182 | // Parse SSE data 183 | bool parse_sse_data(const char* data, size_t length); 184 | 185 | // Close SSE connection 186 | void close_sse_connection(); 187 | 188 | // Send JSON-RPC request 189 | json send_jsonrpc(const request& req); 190 | 191 | // Server host and port 192 | std::string host_; 193 | int port_ = 8080; 194 | 195 | // scheme://host:port 196 | std::string scheme_host_port_; 197 | 198 | // SSE endpoint 199 | std::string sse_endpoint_ = "/sse"; 200 | 201 | // Message endpoint 202 | std::string msg_endpoint_; 203 | 204 | // HTTP client 205 | std::unique_ptr http_client_; 206 | 207 | // SSE HTTP client 208 | std::unique_ptr sse_client_; 209 | 210 | // SSE thread 211 | std::unique_ptr sse_thread_; 212 | 213 | // SSE running status 214 | std::atomic sse_running_{false}; 215 | 216 | // Authentication token 217 | std::string auth_token_; 218 | 219 | // Default request headers 220 | std::map default_headers_; 221 | 222 | // Timeout (seconds) 223 | int timeout_seconds_ = 30; 224 | 225 | // Client capabilities 226 | json capabilities_; 227 | 228 | // Server capabilities 229 | json server_capabilities_; 230 | 231 | // Mutex 232 | mutable std::mutex mutex_; 233 | 234 | // Condition variable, used to wait for message endpoint setting 235 | std::condition_variable endpoint_cv_; 236 | 237 | // Request ID to Promise mapping, used for asynchronous waiting for responses 238 | std::map> pending_requests_; 239 | 240 | // Response processing mutex 241 | std::mutex response_mutex_; 242 | 243 | // Response condition variable 244 | std::condition_variable response_cv_; 245 | }; 246 | 247 | } // namespace mcp 248 | 249 | #endif // MCP_SSE_CLIENT_H 250 | -------------------------------------------------------------------------------- /test/testcase.md: -------------------------------------------------------------------------------- 1 | # Model Context Protocol Testcases 2 | 3 | Testcases collected from https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/ and https://spec.modelcontextprotocol.io/specification/2024-11-05/server/. 4 | 5 | ## Basic Protocol Testcases 6 | 7 | ### Message Format 8 | 9 | #### Request Message 10 | 11 | ```json 12 | { 13 | "jsonrpc": "2.0", 14 | "id": "string | number", 15 | "method": "string", 16 | "params": { 17 | "key": "value" 18 | } 19 | } 20 | ``` 21 | 22 | #### Response Message 23 | 24 | ```json 25 | { 26 | "jsonrpc": "2.0", 27 | "id": "string | number", 28 | "result": { 29 | "key": "value" 30 | } 31 | } 32 | ``` 33 | 34 | #### Notification Message 35 | 36 | ```json 37 | { 38 | "jsonrpc": "2.0", 39 | "method": "string", 40 | "params": { 41 | "key": "value" 42 | } 43 | } 44 | ``` 45 | 46 | ### Lifecycle Testcases 47 | 48 | #### Initialization Request 49 | 50 | ```json 51 | { 52 | "jsonrpc": "2.0", 53 | "id": 1, 54 | "method": "initialize", 55 | "params": { 56 | "protocolVersion": "2024-11-05", 57 | "capabilities": { 58 | "roots": { 59 | "listChanged": true 60 | }, 61 | "sampling": {} 62 | }, 63 | "clientInfo": { 64 | "name": "ExampleClient", 65 | "version": "1.0.0" 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | #### Initialization Response 72 | 73 | ```json 74 | { 75 | "jsonrpc": "2.0", 76 | "id": 1, 77 | "result": { 78 | "protocolVersion": "2024-11-05", 79 | "capabilities": { 80 | "logging": {}, 81 | "prompts": { 82 | "listChanged": true 83 | }, 84 | "resources": { 85 | "subscribe": true, 86 | "listChanged": true 87 | }, 88 | "tools": { 89 | "listChanged": true 90 | } 91 | }, 92 | "serverInfo": { 93 | "name": "ExampleServer", 94 | "version": "1.0.0" 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | #### Initialization Notification 101 | 102 | ```json 103 | { 104 | "jsonrpc": "2.0", 105 | "method": "notifications/initialized" 106 | } 107 | ``` 108 | 109 | #### Initialization Error (Unsupported Version) 110 | 111 | ```json 112 | { 113 | "jsonrpc": "2.0", 114 | "id": 1, 115 | "error": { 116 | "code": -32602, 117 | "message": "Unsupported protocol version", 118 | "data": { 119 | "supported": ["2024-11-05"], 120 | "requested": "1.0.0" 121 | } 122 | } 123 | } 124 | ``` 125 | 126 | ### Tool Testcases 127 | 128 | #### Ping Request 129 | 130 | ```json 131 | { 132 | "jsonrpc": "2.0", 133 | "id": "123", 134 | "method": "ping" 135 | } 136 | ``` 137 | 138 | #### Ping Response 139 | 140 | ```json 141 | { 142 | "jsonrpc": "2.0", 143 | "id": "123", 144 | "result": {} 145 | } 146 | ``` 147 | 148 | #### Cancellation Notification 149 | 150 | ```json 151 | { 152 | "jsonrpc": "2.0", 153 | "method": "notifications/cancelled", 154 | "params": { 155 | "requestId": "123", 156 | "reason": "User requested cancellation" 157 | } 158 | } 159 | ``` 160 | 161 | #### Progress Request 162 | 163 | ```json 164 | { 165 | "jsonrpc": "2.0", 166 | "id": 1, 167 | "method": "some_method", 168 | "params": { 169 | "_meta": { 170 | "progressToken": "abc123" 171 | } 172 | } 173 | } 174 | ``` 175 | 176 | #### Progress Notification 177 | 178 | ```json 179 | { 180 | "jsonrpc": "2.0", 181 | "method": "notifications/progress", 182 | "params": { 183 | "progressToken": "abc123", 184 | "progress": 50, 185 | "total": 100 186 | } 187 | } 188 | ``` 189 | 190 | ## Server Testcases 191 | 192 | ### Tool Testcases 193 | 194 | #### Tool List Request 195 | 196 | ```json 197 | { 198 | "jsonrpc": "2.0", 199 | "id": 1, 200 | "method": "tools/list", 201 | "params": { 202 | "cursor": "optional-cursor-value" 203 | } 204 | } 205 | ``` 206 | 207 | #### Tool List Response 208 | 209 | ```json 210 | { 211 | "jsonrpc": "2.0", 212 | "id": 1, 213 | "result": { 214 | "tools": [ 215 | { 216 | "name": "get_weather", 217 | "description": "Get current weather information for a location", 218 | "inputSchema": { 219 | "type": "object", 220 | "properties": { 221 | "location": { 222 | "type": "string", 223 | "description": "City name or zip code" 224 | } 225 | }, 226 | "required": ["location"] 227 | } 228 | } 229 | ], 230 | "nextCursor": "next-page-cursor" 231 | } 232 | } 233 | ``` 234 | 235 | #### Tool Call Request 236 | 237 | ```json 238 | { 239 | "jsonrpc": "2.0", 240 | "id": 2, 241 | "method": "tools/call", 242 | "params": { 243 | "name": "get_weather", 244 | "arguments": { 245 | "location": "New York" 246 | } 247 | } 248 | } 249 | ``` 250 | 251 | #### Tool Call Response 252 | 253 | ```json 254 | { 255 | "jsonrpc": "2.0", 256 | "id": 2, 257 | "result": { 258 | "content": [ 259 | { 260 | "type": "text", 261 | "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" 262 | } 263 | ], 264 | "isError": false 265 | } 266 | } 267 | ``` 268 | 269 | #### Tool List Changed Notification 270 | 271 | ```json 272 | { 273 | "jsonrpc": "2.0", 274 | "method": "notifications/tools/list_changed" 275 | } 276 | ``` 277 | 278 | ### Resource Testcases 279 | 280 | #### Resource List Request 281 | 282 | ```json 283 | { 284 | "jsonrpc": "2.0", 285 | "id": 1, 286 | "method": "resources/list", 287 | "params": { 288 | "cursor": "optional-cursor-value" 289 | } 290 | } 291 | ``` 292 | 293 | #### Resource List Response 294 | 295 | ```json 296 | { 297 | "jsonrpc": "2.0", 298 | "id": 1, 299 | "result": { 300 | "resources": [ 301 | { 302 | "uri": "file:///project/src/main.rs", 303 | "name": "main.rs", 304 | "description": "Primary application entry point", 305 | "mimeType": "text/x-rust" 306 | } 307 | ], 308 | "nextCursor": "next-page-cursor" 309 | } 310 | } 311 | ``` 312 | 313 | #### Resource Read Request 314 | 315 | ```json 316 | { 317 | "jsonrpc": "2.0", 318 | "id": 2, 319 | "method": "resources/read", 320 | "params": { 321 | "uri": "file:///project/src/main.rs" 322 | } 323 | } 324 | ``` 325 | 326 | #### Resource Read Response 327 | 328 | ```json 329 | { 330 | "jsonrpc": "2.0", 331 | "id": 2, 332 | "result": { 333 | "contents": [ 334 | { 335 | "uri": "file:///project/src/main.rs", 336 | "mimeType": "text/x-rust", 337 | "text": "fn main() {\n println!(\"Hello world!\");\n}" 338 | } 339 | ] 340 | } 341 | } 342 | ``` 343 | 344 | #### Resource Template List Request 345 | 346 | ```json 347 | { 348 | "jsonrpc": "2.0", 349 | "id": 3, 350 | "method": "resources/templates/list" 351 | } 352 | ``` 353 | 354 | #### Resource Template List Response 355 | 356 | ```json 357 | { 358 | "jsonrpc": "2.0", 359 | "id": 3, 360 | "result": { 361 | "resourceTemplates": [ 362 | { 363 | "uriTemplate": "file:///{path}", 364 | "name": "Project Files", 365 | "description": "Access files in the project directory", 366 | "mimeType": "application/octet-stream" 367 | } 368 | ] 369 | } 370 | } 371 | ``` 372 | 373 | #### Resource List Changes Notification 374 | 375 | ```json 376 | { 377 | "jsonrpc": "2.0", 378 | "method": "notifications/resources/list_changed" 379 | } 380 | ``` 381 | 382 | #### Resource Description Request 383 | 384 | ```json 385 | { 386 | "jsonrpc": "2.0", 387 | "id": 4, 388 | "method": "resources/subscribe", 389 | "params": { 390 | "uri": "file:///project/src/main.rs" 391 | } 392 | } 393 | ``` 394 | 395 | #### Resource Updated Notification 396 | 397 | ```json 398 | { 399 | "jsonrpc": "2.0", 400 | "method": "notifications/resources/updated", 401 | "params": { 402 | "uri": "file:///project/src/main.rs" 403 | } 404 | } 405 | ``` 406 | 407 | #### Resource Error (Not Found) 408 | 409 | ```json 410 | { 411 | "jsonrpc": "2.0", 412 | "id": 5, 413 | "error": { 414 | "code": -32002, 415 | "message": "Resource not found", 416 | "data": { 417 | "uri": "file:///nonexistent.txt" 418 | } 419 | } 420 | } 421 | ``` 422 | 423 | ### Tool Result 424 | 425 | #### Text Content 426 | 427 | ```json 428 | { 429 | "type": "text", 430 | "text": "Tool result text" 431 | } 432 | ``` 433 | 434 | #### Image Content 435 | 436 | ```json 437 | { 438 | "type": "image", 439 | "data": "base64-encoded-data", 440 | "mimeType": "image/png" 441 | } 442 | ``` 443 | 444 | #### Resource Content 445 | 446 | ```json 447 | { 448 | "type": "resource", 449 | "resource": { 450 | "uri": "resource://example", 451 | "mimeType": "text/plain", 452 | "text": "Resource content" 453 | } 454 | } 455 | ``` -------------------------------------------------------------------------------- /include/mcp_resource.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_resource.h 3 | * @brief Resource implementation for MCP 4 | * 5 | * This file defines the base resource class and common resource types for the MCP protocol. 6 | * Follows the 2024-11-05 protocol specification. 7 | */ 8 | 9 | #ifndef MCP_RESOURCE_H 10 | #define MCP_RESOURCE_H 11 | 12 | #include "mcp_message.h" 13 | #include "base64.hpp" 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace mcp { 21 | 22 | /** 23 | * @class resource 24 | * @brief Base class for MCP resources 25 | * 26 | * The resource class defines the interface for resources that can be 27 | * accessed through the MCP protocol. Each resource is identified by a URI. 28 | */ 29 | class resource { 30 | public: 31 | virtual ~resource() = default; 32 | 33 | /** 34 | * @brief Get resource metadata 35 | * @return Metadata as JSON 36 | */ 37 | virtual json get_metadata() const = 0; 38 | 39 | /** 40 | * @brief Read the resource content 41 | * @return The resource content as JSON 42 | */ 43 | virtual json read() const = 0; 44 | 45 | /** 46 | * @brief Check if the resource has been modified 47 | * @return True if the resource has been modified since last read 48 | */ 49 | virtual bool is_modified() const = 0; 50 | 51 | /** 52 | * @brief Get the URI of the resource 53 | * @return The URI as string 54 | */ 55 | virtual std::string get_uri() const = 0; 56 | }; 57 | 58 | /** 59 | * @class text_resource 60 | * @brief Resource containing text content 61 | * 62 | * The text_resource class provides a base implementation for resources 63 | * that contain text content. 64 | */ 65 | class text_resource : public resource { 66 | public: 67 | /** 68 | * @brief Constructor 69 | * @param uri The URI of the resource 70 | * @param name The name of the resource 71 | * @param mime_type The MIME type of the resource 72 | * @param description Optional description of the resource 73 | */ 74 | text_resource(const std::string& uri, 75 | const std::string& name, 76 | const std::string& mime_type, 77 | const std::string& description = ""); 78 | 79 | /** 80 | * @brief Get resource metadata 81 | * @return Metadata as JSON 82 | */ 83 | json get_metadata() const override; 84 | 85 | /** 86 | * @brief Read the resource content 87 | * @return The resource content as JSON 88 | */ 89 | json read() const override; 90 | 91 | /** 92 | * @brief Check if the resource has been modified 93 | * @return True if the resource has been modified since last read 94 | */ 95 | bool is_modified() const override; 96 | 97 | /** 98 | * @brief Get the URI of the resource 99 | * @return The URI as string 100 | */ 101 | std::string get_uri() const override; 102 | 103 | /** 104 | * @brief Set the text content of the resource 105 | * @param text The text content 106 | */ 107 | void set_text(const std::string& text); 108 | 109 | /** 110 | * @brief Get the text content of the resource 111 | * @return The text content 112 | */ 113 | std::string get_text() const; 114 | 115 | protected: 116 | std::string uri_; 117 | std::string name_; 118 | std::string mime_type_; 119 | std::string description_; 120 | std::string text_; 121 | mutable bool modified_; 122 | }; 123 | 124 | /** 125 | * @class binary_resource 126 | * @brief Resource containing binary content 127 | * 128 | * The binary_resource class provides a base implementation for resources 129 | * that contain binary content. 130 | */ 131 | class binary_resource : public resource { 132 | public: 133 | /** 134 | * @brief Constructor 135 | * @param uri The URI of the resource 136 | * @param name The name of the resource 137 | * @param mime_type The MIME type of the resource 138 | * @param description Optional description of the resource 139 | */ 140 | binary_resource(const std::string& uri, 141 | const std::string& name, 142 | const std::string& mime_type, 143 | const std::string& description = ""); 144 | 145 | /** 146 | * @brief Get resource metadata 147 | * @return Metadata as JSON 148 | */ 149 | json get_metadata() const override; 150 | 151 | /** 152 | * @brief Read the resource content 153 | * @return The resource content as JSON with base64-encoded data 154 | */ 155 | json read() const override; 156 | 157 | /** 158 | * @brief Check if the resource has been modified 159 | * @return True if the resource has been modified since last read 160 | */ 161 | bool is_modified() const override; 162 | 163 | /** 164 | * @brief Get the URI of the resource 165 | * @return The URI as string 166 | */ 167 | std::string get_uri() const override; 168 | 169 | /** 170 | * @brief Set the binary content of the resource 171 | * @param data Pointer to the binary data 172 | * @param size Size of the binary data 173 | */ 174 | void set_data(const uint8_t* data, size_t size); 175 | 176 | /** 177 | * @brief Get the binary content of the resource 178 | * @return The binary content 179 | */ 180 | const std::vector& get_data() const; 181 | 182 | protected: 183 | std::string uri_; 184 | std::string name_; 185 | std::string mime_type_; 186 | std::string description_; 187 | std::vector data_; 188 | mutable bool modified_; 189 | }; 190 | 191 | /** 192 | * @class file_resource 193 | * @brief Resource for file system operations 194 | * 195 | * The file_resource class provides access to files as resources. 196 | */ 197 | class file_resource : public text_resource { 198 | public: 199 | /** 200 | * @brief Constructor 201 | * @param file_path The path to the file 202 | * @param mime_type The MIME type of the file (optional, will be guessed if not provided) 203 | * @param description Optional description of the resource 204 | */ 205 | file_resource(const std::string& file_path, 206 | const std::string& mime_type = "", 207 | const std::string& description = ""); 208 | 209 | /** 210 | * @brief Read the resource content 211 | * @return The resource content as JSON 212 | */ 213 | json read() const override; 214 | 215 | /** 216 | * @brief Check if the resource has been modified 217 | * @return True if the resource has been modified since last read 218 | */ 219 | bool is_modified() const override; 220 | 221 | private: 222 | std::string file_path_; 223 | mutable time_t last_modified_; 224 | 225 | /** 226 | * @brief Guess the MIME type from file extension 227 | * @param file_path The file path 228 | * @return The guessed MIME type 229 | */ 230 | static std::string guess_mime_type(const std::string& file_path); 231 | }; 232 | 233 | /** 234 | * @class resource_manager 235 | * @brief Manager for MCP resources 236 | * 237 | * The resource_manager class provides a central registry for resources 238 | * and handles resource operations. 239 | */ 240 | class resource_manager { 241 | public: 242 | /** 243 | * @brief Get the singleton instance 244 | * @return Reference to the singleton instance 245 | */ 246 | static resource_manager& instance(); 247 | 248 | /** 249 | * @brief Register a resource 250 | * @param resource Shared pointer to the resource 251 | */ 252 | void register_resource(std::shared_ptr resource); 253 | 254 | /** 255 | * @brief Unregister a resource 256 | * @param uri The URI of the resource to unregister 257 | * @return True if the resource was unregistered 258 | */ 259 | bool unregister_resource(const std::string& uri); 260 | 261 | /** 262 | * @brief Get a resource by URI 263 | * @param uri The URI of the resource 264 | * @return Shared pointer to the resource, or nullptr if not found 265 | */ 266 | std::shared_ptr get_resource(const std::string& uri) const; 267 | 268 | /** 269 | * @brief List all registered resources 270 | * @return JSON array of resource metadata 271 | */ 272 | json list_resources() const; 273 | 274 | /** 275 | * @brief Subscribe to resource changes 276 | * @param uri The URI of the resource to subscribe to 277 | * @param callback The callback function to call when the resource changes 278 | * @return Subscription ID 279 | */ 280 | int subscribe(const std::string& uri, std::function callback); 281 | 282 | /** 283 | * @brief Unsubscribe from resource changes 284 | * @param subscription_id The subscription ID 285 | * @return True if the subscription was removed 286 | */ 287 | bool unsubscribe(int subscription_id); 288 | 289 | /** 290 | * @brief Notify subscribers of resource changes 291 | * @param uri The URI of the resource that changed 292 | */ 293 | void notify_resource_changed(const std::string& uri); 294 | 295 | private: 296 | resource_manager() = default; 297 | ~resource_manager() = default; 298 | 299 | resource_manager(const resource_manager&) = delete; 300 | resource_manager& operator=(const resource_manager&) = delete; 301 | 302 | std::map> resources_; 303 | std::map>> subscriptions_; 304 | int next_subscription_id_ = 1; 305 | }; 306 | 307 | } // namespace mcp 308 | 309 | #endif // MCP_RESOURCE_H -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP Protocol Framework 2 | 3 | [Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/specification/2024-11-05/architecture/) is an open protocol that provides a standardized way for AI models and agents to interact with various resources, tools, and services. This framework implements the core functionality of the MCP protocol, conforming to the 2024-11-05 basic protocol specification. 4 | 5 | ## Core Features 6 | 7 | - **JSON-RPC 2.0 Communication**: Request/response communication based on JSON-RPC 2.0 standard 8 | - **Resource Abstraction**: Standard interfaces for resources such as files, APIs, etc. 9 | - **Tool Registration**: Register and call tools with structured parameters 10 | - **Extensible Architecture**: Easy to extend with new resource types and tools 11 | - **Multi-Transport Support**: Supports HTTP and standard input/output (stdio) communication methods 12 | 13 | ## How to Build 14 | 15 | Example of building with CMake: 16 | ```bash 17 | cmake -B build 18 | cmake --build build --config Release 19 | ``` 20 | 21 | Build with tests: 22 | ``` 23 | git submodule update --init --recursive # Get GoogleTest 24 | 25 | cmake -B build -DMCP_BUILD_TESTS=ON 26 | cmake --build build --config Release 27 | ``` 28 | 29 | Build with SSL support: 30 | ``` 31 | git submodule update --init --recursive # Get GoogleTest 32 | 33 | cmake -B build -DMCP_SSL=ON 34 | cmake --build build --config Release 35 | ``` 36 | 37 | ## Adopters 38 | 39 | Here are some open-source projects that are using this repository. 40 | If you're using it too, feel free to submit a PR to be featured here! 41 | 42 | - [humanus.cpp](https://github.com/WHU-MYTH-Lab/humanus.cpp): Lightweight C++ LLM agent framework 43 | - ...waiting for your contribution... 44 | 45 | 46 | 47 | ## Components 48 | 49 | The MCP C++ library includes the following main components: 50 | 51 | ### Core Components 52 | 53 | #### Client Interface (`mcp_client.h`) 54 | Defines the abstract interface for MCP clients, which all concrete client implementations inherit from. 55 | 56 | #### SSE Client (`mcp_sse_client.h`, `mcp_sse_client.cpp`) 57 | Client implementation that communicates with MCP servers using HTTP and Server-Sent Events (SSE). 58 | 59 | #### Stdio Client (`mcp_stdio_client.h`, `mcp_stdio_client.cpp`) 60 | Client implementation that communicates with MCP servers using standard input/output, capable of launching subprocesses and communicating with them. 61 | 62 | #### Message Processing (`mcp_message.h`, `mcp_message.cpp`) 63 | Handles serialization and deserialization of JSON-RPC messages. 64 | 65 | #### Tool Management (`mcp_tool.h`, `mcp_tool.cpp`) 66 | Manages and invokes MCP tools. 67 | 68 | #### Resource Management (`mcp_resource.h`, `mcp_resource.cpp`) 69 | Manages MCP resources. 70 | 71 | #### Server (`mcp_server.h`, `mcp_server.cpp`) 72 | Implements MCP server functionality. 73 | 74 | ## Examples 75 | 76 | ### HTTP Server Example (`examples/server_example.cpp`) 77 | 78 | Example MCP server implementation with custom tools: 79 | - Time tool: Get the current time 80 | - Calculator tool: Perform mathematical operations 81 | - Echo tool: Echo input with optional transformations (to uppercase, reverse) 82 | - Greeting tool: Returns `Hello, `+ input name + `!`, defaults to `Hello, World!` 83 | 84 | ### HTTP Client Example (`examples/client_example.cpp`) 85 | 86 | Example MCP client connecting to a server: 87 | - Get server information 88 | - List available tools 89 | - Call tools with parameters 90 | - Access resources 91 | 92 | ### Stdio Client Example (`examples/stdio_client_example.cpp`) 93 | 94 | Demonstrates how to use the stdio client to communicate with a local server: 95 | - Launch a local server process 96 | - Access filesystem resources 97 | - Call server tools 98 | 99 | ### Agent Example (`examples/agent_example.cpp`) 100 | 101 | | Option | Description | 102 | | :- | :- | 103 | | `--base-url` | LLM base URL (e.g. `https://openrouter.ai`) | 104 | | `--endpoint` | LLM endpoint (default to `/v1/chat/completions/`) | 105 | | `--api-key` | LLM API key | 106 | | `--model` | Model name (e.g. `gpt-3.5-turbo`) | 107 | | `--system-prompt` | System prompt | 108 | | `--max-tokens` | Maximum number of tokens to generate (default to 2048) | 109 | | `--temperature` | Temperature (default to 0.0) | 110 | | `--max-steps` | Maximum steps calling tools without user input (default to 3) | 111 | 112 | Example usage: 113 | ``` 114 | ./build/examples/agent_example --base-url --endpoint --api-key --model 115 | ``` 116 | 117 | **Note**: Remember to compile with `-DMCP_SSL=ON` when connecting to an https base URL. 118 | 119 | ## How to Use 120 | 121 | ### Setting up an HTTP Server 122 | 123 | ```cpp 124 | // Create and configure the server 125 | mcp::server::configuration srv_conf; 126 | srv_conf.host = "localhost"; 127 | srv_conf.port = 8888; 128 | 129 | mcp::server server(srv_conf); 130 | server.set_server_info("MCP Example Server", "0.1.0"); // Name and version 131 | 132 | // Register tools 133 | mcp::json hello_handler(const mcp::json& params, const std::string /* session_id */) { 134 | std::string name = params.contains("name") ? params["name"].get() : "World"; 135 | 136 | // Server will catch exceptions and return error contents 137 | // For example, you can use `throw mcp::mcp_exception(mcp::error_code::invalid_params, "Invalid name");` to report an error 138 | 139 | // Content should be a JSON array, see: https://modelcontextprotocol.io/specification/2024-11-05/server/tools#tool-result 140 | return { 141 | { 142 | {"type", "text"}, 143 | {"text", "Hello, " + name + "!"} 144 | } 145 | }; 146 | } 147 | 148 | mcp::tool hello_tool = mcp::tool_builder("hello") 149 | .with_description("Say hello") 150 | .with_string_param("name", "Name to say hello to", "World") 151 | .build(); 152 | 153 | server.register_tool(hello_tool, hello_handler); 154 | 155 | // Register resources 156 | auto file_resource = std::make_shared(""); 157 | server.register_resource("file://", file_resource); 158 | 159 | // Start the server 160 | server.start(true); // Blocking mode 161 | ``` 162 | 163 | ### Creating an HTTP Client 164 | 165 | ```cpp 166 | // Connect to the server 167 | mcp::sse_client client("http://localhost:8080"); 168 | 169 | // Initialize the connection 170 | client.initialize("My Client", "1.0.0"); 171 | 172 | // Call a tool 173 | mcp::json params = { 174 | {"name", "Client"} 175 | }; 176 | 177 | mcp::json result = client.call_tool("hello", params); 178 | ``` 179 | 180 | ### Using the SSE Client 181 | 182 | The SSE client uses HTTP and Server-Sent Events (SSE) to communicate with MCP servers. This is a communication method based on Web standards, suitable for communicating with servers that support HTTP/SSE. 183 | 184 | ```cpp 185 | #include "mcp_sse_client.h" 186 | 187 | // Create a client, specifying the server address and port 188 | mcp::sse_client client("http://localhost:8080"); 189 | 190 | // Set an authentication token (if needed) 191 | client.set_auth_token("your_auth_token"); 192 | 193 | // Set custom request headers (if needed) 194 | client.set_header("X-Custom-Header", "value"); 195 | 196 | // Initialize the client 197 | if (!client.initialize("My Client", "1.0.0")) { 198 | // Handle initialization failure 199 | } 200 | 201 | // Call a tool 202 | json result = client.call_tool("tool_name", { 203 | {"param1", "value1"}, 204 | {"param2", 42} 205 | }); 206 | ``` 207 | 208 | ### Using the Stdio Client 209 | 210 | The Stdio client can communicate with any MCP server that supports stdio transport, such as: 211 | 212 | - @modelcontextprotocol/server-everything - Example server 213 | - @modelcontextprotocol/server-filesystem - Filesystem server 214 | - Other [MCP servers](https://www.pulsemcp.com/servers) that support stdio transport 215 | 216 | ```cpp 217 | #include "mcp_stdio_client.h" 218 | 219 | // Create a client, specifying the server command 220 | mcp::stdio_client client("npx -y @modelcontextprotocol/server-everything"); 221 | // mcp::stdio_client client("npx -y @modelcontextprotocol/server-filesystem /path/to/directory"); 222 | 223 | // Initialize the client 224 | if (!client.initialize("My Client", "1.0.0")) { 225 | // Handle initialization failure 226 | } 227 | 228 | // Access resources 229 | json resources = client.list_resources(); 230 | json content = client.read_resource("resource://uri"); 231 | 232 | // Call a tool 233 | json result = client.call_tool("tool_name", { 234 | {"param1", "value1"}, 235 | {"param2", "value2"} 236 | }); 237 | ``` 238 | 239 | 240 | ## Using TLS clients and servers 241 | 242 | ### Creating test certificates on Linux 243 | 1. Generate Certificate Authority (CA) private key 244 | ```bash 245 | openssl genrsa -out ca.key.pem 2048 246 | ``` 247 | 1. Generate CA certificate 248 | ```bash 249 | openssl req -x509 -new -nodes -key ca.key.pem -sha256 -days 1 -out ca.cert.pem -subj "/CN=Test CA" 250 | ``` 251 | 1. Generate server private key 252 | ```bash 253 | openssl genrsa -out server.key.pem 2048 254 | ``` 255 | 1. Generate Certificate Signing Request (CSR) 256 | ``` 257 | openssl req -new -key server.key.pem -out server.csr.pem -subj "/O=TestServer/OU=Dev/CN=localhost" 258 | ``` 259 | 1. Generate server certificate signed by CA 260 | ``` 261 | openssl x509 -req -in server.csr.pem -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial -out server.cert.pem -days 1 -sha256 262 | ``` 263 | ### Setting up an HTTPs server 264 | 265 | ```cpp 266 | mcp::server::configuration srv_conf; 267 | srv_conf.host = "localhost"; 268 | srv_conf.port = 8888; 269 | srv_conf.ssl.server_cert_path = "./server.cert.pem"; 270 | srv_conf.ssl.server_private_key_path = "./server.key.pem"; 271 | ``` 272 | 273 | ### Setting up an SSE client with TLS 274 | 275 | ```cpp 276 | mcp::sse_client client("https://localhost:8888"); 277 | ``` 278 | 279 | ## License 280 | 281 | This framework is provided under the MIT license. For details, please see the LICENSE file. 282 | 283 | -------------------------------------------------------------------------------- /src/mcp_resource.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_resource.cpp 3 | * @brief Resource implementation for MCP 4 | * 5 | * This file implements the resource classes for the Model Context Protocol. 6 | * Follows the 2024-11-05 protocol specification. 7 | */ 8 | #include "mcp_resource.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace fs = std::filesystem; 19 | 20 | namespace mcp { 21 | 22 | // text_resource implementation 23 | text_resource::text_resource(const std::string& uri, 24 | const std::string& name, 25 | const std::string& mime_type, 26 | const std::string& description) 27 | : uri_(uri), name_(name), mime_type_(mime_type), description_(description), modified_(false) { 28 | } 29 | 30 | json text_resource::get_metadata() const { 31 | return { 32 | {"uri", uri_}, 33 | {"name", name_}, 34 | {"mimeType", mime_type_}, 35 | {"description", description_} 36 | }; 37 | } 38 | 39 | json text_resource::read() const { 40 | modified_ = false; 41 | return { 42 | {"uri", uri_}, 43 | {"mimeType", mime_type_}, 44 | {"text", text_} 45 | }; 46 | } 47 | 48 | bool text_resource::is_modified() const { 49 | return modified_; 50 | } 51 | 52 | std::string text_resource::get_uri() const { 53 | return uri_; 54 | } 55 | 56 | void text_resource::set_text(const std::string& text) { 57 | if (text_ != text) { 58 | text_ = text; 59 | modified_ = true; 60 | } 61 | } 62 | 63 | std::string text_resource::get_text() const { 64 | return text_; 65 | } 66 | 67 | // binary_resource implementation 68 | binary_resource::binary_resource(const std::string& uri, 69 | const std::string& name, 70 | const std::string& mime_type, 71 | const std::string& description) 72 | : uri_(uri), name_(name), mime_type_(mime_type), description_(description), modified_(false) { 73 | } 74 | 75 | json binary_resource::get_metadata() const { 76 | return { 77 | {"uri", uri_}, 78 | {"name", name_}, 79 | {"mimeType", mime_type_}, 80 | {"description", description_} 81 | }; 82 | } 83 | 84 | json binary_resource::read() const { 85 | modified_ = false; 86 | 87 | // Base64 encode the binary data 88 | std::string base64_data; 89 | if (!data_.empty()) { 90 | base64_data = base64::encode(reinterpret_cast(data_.data()), data_.size()); 91 | } 92 | 93 | return { 94 | {"uri", uri_}, 95 | {"mimeType", mime_type_}, 96 | {"blob", base64_data} 97 | }; 98 | } 99 | 100 | bool binary_resource::is_modified() const { 101 | return modified_; 102 | } 103 | 104 | std::string binary_resource::get_uri() const { 105 | return uri_; 106 | } 107 | 108 | void binary_resource::set_data(const uint8_t* data, size_t size) { 109 | data_.resize(size); 110 | if (size > 0) { 111 | std::memcpy(data_.data(), data, size); 112 | } 113 | modified_ = true; 114 | } 115 | 116 | const std::vector& binary_resource::get_data() const { 117 | return data_; 118 | } 119 | 120 | // file_resource implementation 121 | file_resource::file_resource(const std::string& file_path, 122 | const std::string& mime_type, 123 | const std::string& description) 124 | : text_resource("file://" + file_path, 125 | fs::path(file_path).filename().string(), 126 | mime_type.empty() ? guess_mime_type(file_path) : mime_type, 127 | description), 128 | file_path_(file_path), 129 | last_modified_(0) { 130 | 131 | // Check if file exists 132 | if (!fs::exists(file_path_)) { 133 | throw mcp_exception(error_code::invalid_params, 134 | "File not found: " + file_path_); 135 | } 136 | } 137 | 138 | json file_resource::read() const { 139 | // Read file content 140 | std::ifstream file(file_path_, std::ios::binary); 141 | if (!file) { 142 | throw mcp_exception(error_code::internal_error, 143 | "Failed to open file: " + file_path_); 144 | } 145 | 146 | std::stringstream buffer; 147 | buffer << file.rdbuf(); 148 | 149 | // Update text content 150 | const_cast(this)->set_text(buffer.str()); 151 | 152 | // Update last modified time 153 | last_modified_ = fs::last_write_time(file_path_).time_since_epoch().count(); 154 | 155 | // Mark as not modified after read 156 | modified_ = false; 157 | 158 | return { 159 | {"uri", uri_}, 160 | {"mimeType", mime_type_}, 161 | {"text", text_} 162 | }; 163 | } 164 | 165 | bool file_resource::is_modified() const { 166 | if (!fs::exists(file_path_)) { 167 | return true; // File was deleted 168 | } 169 | 170 | time_t current_modified = fs::last_write_time(file_path_).time_since_epoch().count(); 171 | return current_modified != last_modified_; 172 | } 173 | 174 | std::string file_resource::guess_mime_type(const std::string& file_path) { 175 | std::string ext = fs::path(file_path).extension().string(); 176 | 177 | // Convert to lowercase 178 | std::transform(ext.begin(), ext.end(), ext.begin(), 179 | [](unsigned char c) { return std::tolower(c); }); 180 | 181 | // Common MIME types 182 | if (ext == ".txt") return "text/plain"; 183 | if (ext == ".html" || ext == ".htm") return "text/html"; 184 | if (ext == ".css") return "text/css"; 185 | if (ext == ".js") return "text/javascript"; 186 | if (ext == ".json") return "application/json"; 187 | if (ext == ".xml") return "application/xml"; 188 | if (ext == ".pdf") return "application/pdf"; 189 | if (ext == ".png") return "image/png"; 190 | if (ext == ".jpg" || ext == ".jpeg") return "image/jpeg"; 191 | if (ext == ".gif") return "image/gif"; 192 | if (ext == ".svg") return "image/svg+xml"; 193 | if (ext == ".mp3") return "audio/mpeg"; 194 | if (ext == ".mp4") return "video/mp4"; 195 | if (ext == ".wav") return "audio/wav"; 196 | if (ext == ".zip") return "application/zip"; 197 | if (ext == ".doc" || ext == ".docx") return "application/msword"; 198 | if (ext == ".xls" || ext == ".xlsx") return "application/vnd.ms-excel"; 199 | if (ext == ".ppt" || ext == ".pptx") return "application/vnd.ms-powerpoint"; 200 | if (ext == ".csv") return "text/csv"; 201 | if (ext == ".md") return "text/markdown"; 202 | if (ext == ".py") return "text/x-python"; 203 | if (ext == ".cpp" || ext == ".cc") return "text/x-c++src"; 204 | if (ext == ".h" || ext == ".hpp") return "text/x-c++hdr"; 205 | if (ext == ".c") return "text/x-csrc"; 206 | if (ext == ".rs") return "text/x-rust"; 207 | if (ext == ".go") return "text/x-go"; 208 | if (ext == ".java") return "text/x-java"; 209 | if (ext == ".ts") return "text/x-typescript"; 210 | if (ext == ".rb") return "text/x-ruby"; 211 | 212 | // Default to binary if unknown 213 | return "application/octet-stream"; 214 | } 215 | 216 | // resource_manager implementation 217 | static std::mutex g_resource_manager_mutex; 218 | 219 | resource_manager& resource_manager::instance() { 220 | static resource_manager instance; 221 | return instance; 222 | } 223 | 224 | void resource_manager::register_resource(std::shared_ptr resource) { 225 | if (!resource) { 226 | throw mcp_exception(error_code::invalid_params, "Cannot register null resource"); 227 | } 228 | 229 | std::string uri = resource->get_uri(); 230 | 231 | std::lock_guard lock(g_resource_manager_mutex); 232 | resources_[uri] = resource; 233 | } 234 | 235 | bool resource_manager::unregister_resource(const std::string& uri) { 236 | std::lock_guard lock(g_resource_manager_mutex); 237 | 238 | auto it = resources_.find(uri); 239 | if (it == resources_.end()) { 240 | return false; 241 | } 242 | 243 | resources_.erase(it); 244 | 245 | // Remove any subscriptions for this resource 246 | auto sub_it = subscriptions_.begin(); 247 | while (sub_it != subscriptions_.end()) { 248 | if (sub_it->second.first == uri) { 249 | sub_it = subscriptions_.erase(sub_it); 250 | } else { 251 | ++sub_it; 252 | } 253 | } 254 | 255 | return true; 256 | } 257 | 258 | std::shared_ptr resource_manager::get_resource(const std::string& uri) const { 259 | std::lock_guard lock(g_resource_manager_mutex); 260 | 261 | auto it = resources_.find(uri); 262 | if (it == resources_.end()) { 263 | return nullptr; 264 | } 265 | 266 | return it->second; 267 | } 268 | 269 | json resource_manager::list_resources() const { 270 | std::lock_guard lock(g_resource_manager_mutex); 271 | 272 | json resources = json::array(); 273 | 274 | for (const auto& [uri, res] : resources_) { 275 | resources.push_back(res->get_metadata()); 276 | } 277 | 278 | return { 279 | {"resources", resources} 280 | }; 281 | } 282 | 283 | int resource_manager::subscribe(const std::string& uri, std::function callback) { 284 | if (!callback) { 285 | throw mcp_exception(error_code::invalid_params, "Cannot subscribe with null callback"); 286 | } 287 | 288 | std::lock_guard lock(g_resource_manager_mutex); 289 | 290 | // Check if resource exists 291 | auto it = resources_.find(uri); 292 | if (it == resources_.end()) { 293 | throw mcp_exception(error_code::invalid_params, "Resource not found: " + uri); 294 | } 295 | 296 | int id = next_subscription_id_++; 297 | subscriptions_[id] = std::make_pair(uri, callback); 298 | 299 | return id; 300 | } 301 | 302 | bool resource_manager::unsubscribe(int subscription_id) { 303 | std::lock_guard lock(g_resource_manager_mutex); 304 | 305 | auto it = subscriptions_.find(subscription_id); 306 | if (it == subscriptions_.end()) { 307 | return false; 308 | } 309 | 310 | subscriptions_.erase(it); 311 | return true; 312 | } 313 | 314 | void resource_manager::notify_resource_changed(const std::string& uri) { 315 | std::lock_guard lock(g_resource_manager_mutex); 316 | 317 | // Check if resource exists 318 | auto it = resources_.find(uri); 319 | if (it == resources_.end()) { 320 | return; 321 | } 322 | 323 | // Notify all subscribers for this resource 324 | for (const auto& [id, sub] : subscriptions_) { 325 | if (sub.first == uri) { 326 | try { 327 | sub.second(uri); 328 | } catch (...) { 329 | // Ignore exceptions in callbacks 330 | } 331 | } 332 | } 333 | } 334 | 335 | } // namespace mcp -------------------------------------------------------------------------------- /common/base64.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | This is free and unencumbered software released into the public domain. 3 | 4 | Anyone is free to copy, modify, publish, use, compile, sell, or 5 | distribute this software, either in source code form or as a compiled 6 | binary, for any purpose, commercial or non-commercial, and by any 7 | means. 8 | 9 | In jurisdictions that recognize copyright laws, the author or authors 10 | of this software dedicate any and all copyright interest in the 11 | software to the public domain. We make this dedication for the benefit 12 | of the public at large and to the detriment of our heirs and 13 | successors. We intend this dedication to be an overt act of 14 | relinquishment in perpetuity of all present and future rights to this 15 | software under copyright law. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | For more information, please refer to 26 | */ 27 | 28 | #ifndef PUBLIC_DOMAIN_BASE64_HPP_ 29 | #define PUBLIC_DOMAIN_BASE64_HPP_ 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | class base64_error : public std::runtime_error 37 | { 38 | public: 39 | using std::runtime_error::runtime_error; 40 | }; 41 | 42 | class base64 43 | { 44 | public: 45 | enum class alphabet 46 | { 47 | /** the alphabet is detected automatically */ 48 | auto_, 49 | /** the standard base64 alphabet is used */ 50 | standard, 51 | /** like `standard` except that the characters `+` and `/` are replaced by `-` and `_` respectively*/ 52 | url_filename_safe 53 | }; 54 | 55 | enum class decoding_behavior 56 | { 57 | /** if the input is not padded, the remaining bits are ignored */ 58 | moderate, 59 | /** if a padding character is encounter decoding is finished */ 60 | loose 61 | }; 62 | 63 | /** 64 | Encodes all the elements from `in_begin` to `in_end` to `out`. 65 | 66 | @warning The source and destination cannot overlap. The destination must be able to hold at least 67 | `required_encode_size(std::distance(in_begin, in_end))`, otherwise the behavior depends on the output iterator. 68 | 69 | @tparam Input_iterator the source; the returned elements are cast to `std::uint8_t` and should not be greater than 70 | 8 bits 71 | @tparam Output_iterator the destination; the elements written to it are from the type `char` 72 | @param in_begin the beginning of the source 73 | @param in_end the ending of the source 74 | @param out the destination iterator 75 | @param alphabet which alphabet should be used 76 | @returns the iterator to the next element past the last element copied 77 | @throws see `Input_iterator` and `Output_iterator` 78 | */ 79 | template 80 | static Output_iterator encode(Input_iterator in_begin, Input_iterator in_end, Output_iterator out, 81 | alphabet alphabet = alphabet::standard) 82 | { 83 | constexpr auto pad = '='; 84 | const char* alpha = alphabet == alphabet::url_filename_safe 85 | ? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" 86 | : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 87 | 88 | while (in_begin != in_end) { 89 | std::uint8_t i0 = 0, i1 = 0, i2 = 0; 90 | 91 | // first character 92 | i0 = static_cast(*in_begin); 93 | ++in_begin; 94 | 95 | *out = alpha[i0 >> 2 & 0x3f]; 96 | ++out; 97 | 98 | // part of first character and second 99 | if (in_begin != in_end) { 100 | i1 = static_cast(*in_begin); 101 | ++in_begin; 102 | 103 | *out = alpha[((i0 & 0x3) << 4) | (i1 >> 4 & 0x0f)]; 104 | ++out; 105 | } else { 106 | *out = alpha[(i0 & 0x3) << 4]; 107 | ++out; 108 | 109 | // last padding 110 | *out = pad; 111 | ++out; 112 | 113 | // last padding 114 | *out = pad; 115 | ++out; 116 | 117 | break; 118 | } 119 | 120 | // part of second character and third 121 | if (in_begin != in_end) { 122 | i2 = static_cast(*in_begin); 123 | ++in_begin; 124 | 125 | *out = alpha[((i1 & 0xf) << 2) | (i2 >> 6 & 0x03)]; 126 | ++out; 127 | } else { 128 | *out = alpha[(i1 & 0xf) << 2]; 129 | ++out; 130 | 131 | // last padding 132 | *out = pad; 133 | ++out; 134 | 135 | break; 136 | } 137 | 138 | // rest of third 139 | *out = alpha[i2 & 0x3f]; 140 | ++out; 141 | } 142 | 143 | return out; 144 | } 145 | /** 146 | Encodes a string. 147 | 148 | @param str the string that should be encoded 149 | @param alphabet which alphabet should be used 150 | @returns the encoded base64 string 151 | @throws see base64::encode() 152 | */ 153 | static std::string encode(const std::string& str, alphabet alphabet = alphabet::standard) 154 | { 155 | std::string result; 156 | 157 | result.reserve(required_encode_size(str.length()) + 1); 158 | 159 | encode(str.begin(), str.end(), std::back_inserter(result), alphabet); 160 | 161 | return result; 162 | } 163 | /** 164 | Encodes a char array. 165 | 166 | @param buffer the char array 167 | @param size the size of the array 168 | @param alphabet which alphabet should be used 169 | @returns the encoded string 170 | */ 171 | static std::string encode(const char* buffer, std::size_t size, alphabet alphabet = alphabet::standard) 172 | { 173 | std::string result; 174 | 175 | result.reserve(required_encode_size(size) + 1); 176 | 177 | encode(buffer, buffer + size, std::back_inserter(result), alphabet); 178 | 179 | return result; 180 | } 181 | /** 182 | Decodes all the elements from `in_begin` to `in_end` to `out`. `in_begin` may point to the same location as `out`, 183 | in other words: inplace decoding is possible. 184 | 185 | @warning The destination must be able to hold at least `required_decode_size(std::distance(in_begin, in_end))`, 186 | otherwise the behavior depends on the output iterator. 187 | 188 | @tparam Input_iterator the source; the returned elements are cast to `char` 189 | @tparam Output_iterator the destination; the elements written to it are from the type `std::uint8_t` 190 | @param in_begin the beginning of the source 191 | @param in_end the ending of the source 192 | @param out the destination iterator 193 | @param alphabet which alphabet should be used 194 | @param behavior the behavior when an error was detected 195 | @returns the iterator to the next element past the last element copied 196 | @throws base64_error depending on the set behavior 197 | @throws see `Input_iterator` and `Output_iterator` 198 | */ 199 | template 200 | static Output_iterator decode(Input_iterator in_begin, Input_iterator in_end, Output_iterator out, 201 | alphabet alphabet = alphabet::auto_, 202 | decoding_behavior behavior = decoding_behavior::moderate) 203 | { 204 | //constexpr auto pad = '='; 205 | std::uint8_t last = 0; 206 | auto bits = 0; 207 | 208 | while (in_begin != in_end) { 209 | auto c = *in_begin; 210 | ++in_begin; 211 | 212 | if (c == '=') { 213 | break; 214 | } 215 | 216 | auto part = _base64_value(alphabet, c); 217 | 218 | // enough bits for one byte 219 | if (bits + 6 >= 8) { 220 | *out = (last << (8 - bits)) | (part >> (bits - 2)); 221 | ++out; 222 | 223 | bits -= 2; 224 | } else { 225 | bits += 6; 226 | } 227 | 228 | last = part; 229 | } 230 | 231 | // check padding 232 | if (behavior != decoding_behavior::loose) { 233 | while (in_begin != in_end) { 234 | auto c = *in_begin; 235 | ++in_begin; 236 | 237 | if (c != '=') { 238 | throw base64_error("invalid base64 character."); 239 | } 240 | } 241 | } 242 | 243 | return out; 244 | } 245 | /** 246 | Decodes a string. 247 | 248 | @param str the base64 encoded string 249 | @param alphabet which alphabet should be used 250 | @param behavior the behavior when an error was detected 251 | @returns the decoded string 252 | @throws see base64::decode() 253 | */ 254 | static std::string decode(const std::string& str, alphabet alphabet = alphabet::auto_, 255 | decoding_behavior behavior = decoding_behavior::moderate) 256 | { 257 | std::string result; 258 | 259 | result.reserve(max_decode_size(str.length())); 260 | 261 | decode(str.begin(), str.end(), std::back_inserter(result), alphabet, behavior); 262 | 263 | return result; 264 | } 265 | /** 266 | Decodes a string. 267 | 268 | @param buffer the base64 encoded buffer 269 | @param size the size of the buffer 270 | @param alphabet which alphabet should be used 271 | @param behavior the behavior when an error was detected 272 | @returns the decoded string 273 | @throws see base64::decode() 274 | */ 275 | static std::string decode(const char* buffer, std::size_t size, alphabet alphabet = alphabet::auto_, 276 | decoding_behavior behavior = decoding_behavior::moderate) 277 | { 278 | std::string result; 279 | 280 | result.reserve(max_decode_size(size)); 281 | 282 | decode(buffer, buffer + size, std::back_inserter(result), alphabet, behavior); 283 | 284 | return result; 285 | } 286 | /** 287 | Decodes a string inplace. 288 | 289 | @param[in,out] str the base64 encoded string 290 | @param alphabet which alphabet should be used 291 | @param behavior the behavior when an error was detected 292 | @throws base64::decode_inplace() 293 | */ 294 | static void decode_inplace(std::string& str, alphabet alphabet = alphabet::auto_, 295 | decoding_behavior behavior = decoding_behavior::moderate) 296 | { 297 | str.resize(decode(str.begin(), str.end(), str.begin(), alphabet, behavior) - str.begin()); 298 | } 299 | /** 300 | Decodes a char array inplace. 301 | 302 | @param[in,out] str the string array 303 | @param size the length of the array 304 | @param alphabet which alphabet should be used 305 | @param behavior the behavior when an error was detected 306 | @returns the pointer to the next element past the last element decoded 307 | @throws base64::decode_inplace() 308 | */ 309 | static char* decode_inplace(char* str, std::size_t size, alphabet alphabet = alphabet::auto_, 310 | decoding_behavior behavior = decoding_behavior::moderate) 311 | { 312 | return decode(str, str + size, str, alphabet, behavior); 313 | } 314 | /** 315 | Returns the required decoding size for a given size. The value is calculated with the following formula: 316 | 317 | $$ 318 | \lceil \frac{size}{4} \rceil \cdot 3 319 | $$ 320 | 321 | @param size the size of the encoded input 322 | @returns the size of the resulting decoded buffer; this the absolute maximum 323 | */ 324 | static std::size_t max_decode_size(std::size_t size) noexcept 325 | { 326 | return (size / 4 + (size % 4 ? 1 : 0)) * 3; 327 | } 328 | /** 329 | Returns the required encoding size for a given size. The value is calculated with the following formula: 330 | 331 | $$ 332 | \lceil \frac{size}{3} \rceil \cdot 4 333 | $$ 334 | 335 | @param size the size of the decoded input 336 | @returns the size of the resulting encoded buffer 337 | */ 338 | static std::size_t required_encode_size(std::size_t size) noexcept 339 | { 340 | return (size / 3 + (size % 3 ? 1 : 0)) * 4; 341 | } 342 | 343 | private: 344 | static std::uint8_t _base64_value(alphabet& alphabet, char c) 345 | { 346 | if (c >= 'A' && c <= 'Z') { 347 | return c - 'A'; 348 | } else if (c >= 'a' && c <= 'z') { 349 | return c - 'a' + 26; 350 | } else if (c >= '0' && c <= '9') { 351 | return c - '0' + 52; 352 | } 353 | 354 | // comes down to alphabet 355 | if (alphabet == alphabet::standard) { 356 | if (c == '+') { 357 | return 62; 358 | } else if (c == '/') { 359 | return 63; 360 | } 361 | } else if (alphabet == alphabet::url_filename_safe) { 362 | if (c == '-') { 363 | return 62; 364 | } else if (c == '_') { 365 | return 63; 366 | } 367 | } // auto detect 368 | else { 369 | if (c == '+') { 370 | alphabet = alphabet::standard; 371 | 372 | return 62; 373 | } else if (c == '/') { 374 | alphabet = alphabet::standard; 375 | 376 | return 63; 377 | } else if (c == '-') { 378 | alphabet = alphabet::url_filename_safe; 379 | 380 | return 62; 381 | } else if (c == '_') { 382 | alphabet = alphabet::url_filename_safe; 383 | 384 | return 63; 385 | } 386 | } 387 | 388 | throw base64_error("invalid base64 character."); 389 | } 390 | }; 391 | 392 | #endif // !PUBLIC_DOMAIN_BASE64_HPP_ 393 | -------------------------------------------------------------------------------- /include/mcp_server.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_server.h 3 | * @brief MCP Server implementation 4 | * 5 | * This file implements the server-side functionality for the Model Context Protocol. 6 | * Follows the 2024-11-05 basic protocol specification. 7 | */ 8 | 9 | #ifndef MCP_SERVER_H 10 | #define MCP_SERVER_H 11 | 12 | #include "mcp_message.h" 13 | #include "mcp_resource.h" 14 | #include "mcp_tool.h" 15 | #include "mcp_thread_pool.h" 16 | #include "mcp_logger.h" 17 | 18 | // Include the HTTP library 19 | #include "httplib.h" 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | 35 | namespace mcp { 36 | 37 | using method_handler = std::function; 38 | using tool_handler = method_handler; 39 | using notification_handler = std::function; 40 | using auth_handler = std::function; 41 | using session_cleanup_handler = std::function; 42 | 43 | class event_dispatcher { 44 | public: 45 | event_dispatcher() { 46 | message_.reserve(128); // Pre-allocate space for messages 47 | } 48 | 49 | ~event_dispatcher() { 50 | close(); 51 | } 52 | 53 | bool wait_event(httplib::DataSink* sink, const std::chrono::milliseconds& timeout = std::chrono::milliseconds(10000)) { 54 | if (!sink || closed_.load(std::memory_order_acquire)) { 55 | return false; 56 | } 57 | 58 | std::string message_copy; 59 | { 60 | std::unique_lock lk(m_); 61 | 62 | if (closed_.load(std::memory_order_acquire)) { 63 | return false; 64 | } 65 | 66 | int id = id_.load(std::memory_order_relaxed); 67 | 68 | bool result = cv_.wait_for(lk, timeout, [&] { 69 | return cid_.load(std::memory_order_relaxed) == id || closed_.load(std::memory_order_acquire); 70 | }); 71 | 72 | if (closed_.load(std::memory_order_acquire)) { 73 | return false; 74 | } 75 | 76 | if (!result) { 77 | return false; 78 | } 79 | 80 | // Only copy the message if there is one 81 | if (!message_.empty()) { 82 | message_copy.swap(message_); 83 | } else { 84 | return true; // No message but condition satisfied 85 | } 86 | } 87 | 88 | try { 89 | if (!message_copy.empty()) { 90 | if (!sink->write(message_copy.data(), message_copy.size())) { 91 | close(); 92 | return false; 93 | } 94 | } 95 | return true; 96 | } catch (...) { 97 | close(); 98 | return false; 99 | } 100 | } 101 | 102 | bool send_event(const std::string& message) { 103 | if (closed_.load(std::memory_order_acquire) || message.empty()) { 104 | return false; 105 | } 106 | 107 | try { 108 | std::lock_guard lk(m_); 109 | 110 | if (closed_.load(std::memory_order_acquire)) { 111 | return false; 112 | } 113 | 114 | // Efficiently set the message and allocate space as needed 115 | if (message.size() > message_.capacity()) { 116 | message_.reserve(message.size() + 64); // Pre-allocate extra space to avoid frequent reallocations 117 | } 118 | message_ = message; 119 | 120 | cid_.store(id_.fetch_add(1, std::memory_order_relaxed), std::memory_order_relaxed); 121 | cv_.notify_one(); // Notify waiting threads 122 | return true; 123 | } catch (...) { 124 | return false; 125 | } 126 | } 127 | 128 | void close() { 129 | bool was_closed = closed_.exchange(true, std::memory_order_release); 130 | if (was_closed) { 131 | return; 132 | } 133 | 134 | try { 135 | cv_.notify_all(); 136 | } catch (...) { 137 | // Ignore exceptions 138 | } 139 | } 140 | 141 | bool is_closed() const { 142 | return closed_.load(std::memory_order_acquire); 143 | } 144 | 145 | // Get the last activity time 146 | std::chrono::steady_clock::time_point last_activity() const { 147 | std::lock_guard lk(m_); 148 | return last_activity_; 149 | } 150 | 151 | // Update the activity time (when sending or receiving a message) 152 | void update_activity() { 153 | std::lock_guard lk(m_); 154 | last_activity_ = std::chrono::steady_clock::now(); 155 | } 156 | 157 | private: 158 | mutable std::mutex m_; 159 | std::condition_variable cv_; 160 | std::atomic id_{0}; 161 | std::atomic cid_{-1}; 162 | std::string message_; 163 | std::atomic closed_{false}; 164 | std::chrono::steady_clock::time_point last_activity_{std::chrono::steady_clock::now()}; 165 | }; 166 | 167 | /** 168 | * @class server 169 | * @brief Main MCP server class 170 | * 171 | * The server class implements an HTTP server that handles JSON-RPC requests 172 | * according to the Model Context Protocol specification. 173 | */ 174 | class server { 175 | public: 176 | 177 | /** 178 | * @struct configuration 179 | * @brief Configuration settings for the server. 180 | * 181 | * This struct holds all configurable parameters for the server, including 182 | * network bindings, identification, and endpoint paths. If SSL is enabled, 183 | * it also includes paths to the server certificate and private key. 184 | */ 185 | struct configuration { 186 | /** Host to bind to (e.g., "localhost", "0.0.0.0") */ 187 | std::string host{ "localhost" }; 188 | 189 | /** Port to listen on */ 190 | int port{ 8080 }; 191 | 192 | /** Server name */ 193 | std::string name{ "MCP Server" }; 194 | 195 | /** Server version */ 196 | std::string version{ "0.0.1" }; 197 | 198 | /** SSE endpoint path */ 199 | std::string sse_endpoint{ "/sse" }; 200 | 201 | /** Message endpoint path */ 202 | std::string msg_endpoint{ "/message" }; 203 | 204 | unsigned int threadpool_size{ std::thread::hardware_concurrency() }; 205 | 206 | #ifdef MCP_SSL 207 | /** 208 | * @brief SSL configuration settings. 209 | * 210 | * Contains optional paths to the server certificate and private key. 211 | * These are used when SSL support is enabled. 212 | */ 213 | struct { 214 | /** Path to the server certificate */ 215 | std::optional server_cert_path{ std::nullopt }; 216 | 217 | /** Path to the server private key */ 218 | std::optional server_private_key_path{ std::nullopt }; 219 | } ssl; 220 | #endif 221 | }; 222 | 223 | /** 224 | * @brief Constructor 225 | * @param conf The server configuration 226 | */ 227 | server(const server::configuration& conf); 228 | 229 | /** 230 | * @brief Destructor 231 | */ 232 | ~server(); 233 | 234 | /** 235 | * @brief Start the server 236 | * @param blocking If true, this call blocks until the server stops 237 | * @return True if the server started successfully 238 | */ 239 | bool start(bool blocking = true); 240 | 241 | /** 242 | * @brief Stop the server 243 | */ 244 | void stop(); 245 | 246 | /** 247 | * @brief Check if the server is running 248 | * @return True if the server is running 249 | */ 250 | bool is_running() const; 251 | 252 | /** 253 | * @brief Set server information 254 | * @param name The name of the server 255 | * @param version The version of the server 256 | */ 257 | void set_server_info(const std::string& name, const std::string& version); 258 | 259 | /** 260 | * @brief Set server capabilities 261 | * @param capabilities The capabilities of the server 262 | */ 263 | void set_capabilities(const json& capabilities); 264 | 265 | /** 266 | * @brief Register a method handler 267 | * @param method The method name 268 | * @param handler The function to call when the method is invoked 269 | */ 270 | void register_method(const std::string& method, method_handler handler); 271 | 272 | /** 273 | * @brief Register a notification handler 274 | * @param method The notification method name 275 | * @param handler The function to call when the notification is received 276 | */ 277 | void register_notification(const std::string& method, notification_handler handler); 278 | 279 | /** 280 | * @brief Register a resource 281 | * @param path The path to mount the resource at 282 | * @param resource The resource to register 283 | */ 284 | void register_resource(const std::string& path, std::shared_ptr resource); 285 | 286 | /** 287 | * @brief Register a tool 288 | * @param tool The tool to register 289 | * @param handler The function to call when the tool is invoked 290 | */ 291 | void register_tool(const tool& tool, tool_handler handler); 292 | 293 | /** 294 | * @brief Register a session cleanup handler 295 | * @param key Tool or resource name to be cleaned up 296 | * @param handler The function to call when the session is closed 297 | */ 298 | void register_session_cleanup(const std::string& key, session_cleanup_handler handler); 299 | 300 | /** 301 | * @brief Get the list of available tools 302 | * @return JSON array of available tools 303 | */ 304 | std::vector get_tools() const; 305 | 306 | /** 307 | * @brief Set authentication handler 308 | * @param handler Function that takes a token and returns true if valid 309 | * @note The handler should return true if the token is valid, otherwise false 310 | * @note Not used in the current implementation 311 | */ 312 | void set_auth_handler(auth_handler handler); 313 | 314 | /** 315 | * @brief Send a request (or notification) to a client 316 | * @param session_id The session ID of the client 317 | * @param req The request to send 318 | */ 319 | void send_request(const std::string& session_id, const request& req); 320 | 321 | /** 322 | * @brief Set mount point for server 323 | * @param mount_point The mount point to set 324 | * @param dir The directory to serve from the mount point 325 | * @param headers Optional headers to include in the response 326 | * @return True if the mount point was set successfully 327 | */ 328 | bool set_mount_point(const std::string& mount_point, const std::string& dir, httplib::Headers headers = httplib::Headers()); 329 | 330 | private: 331 | std::string host_; 332 | int port_; 333 | std::string name_; 334 | std::string version_; 335 | json capabilities_; 336 | 337 | // The HTTP server 338 | std::unique_ptr http_server_; 339 | 340 | // Server thread (for non-blocking mode) 341 | std::unique_ptr server_thread_; 342 | 343 | // SSE thread 344 | std::map> sse_threads_; 345 | 346 | // Event dispatcher for server-sent events 347 | event_dispatcher sse_dispatcher_; 348 | 349 | // Session-specific event dispatchers 350 | std::map> session_dispatchers_; 351 | 352 | // Server-sent events endpoint 353 | std::string sse_endpoint_; 354 | std::string msg_endpoint_; 355 | 356 | // Method handlers 357 | std::map method_handlers_; 358 | 359 | // Notification handlers 360 | std::map notification_handlers_; 361 | 362 | // Resources map (path -> resource) 363 | std::map> resources_; 364 | 365 | // Tools map (name -> handler) 366 | std::map> tools_; 367 | 368 | // Authentication handler 369 | auth_handler auth_handler_; 370 | 371 | // Mutex for thread safety 372 | mutable std::mutex mutex_; 373 | 374 | // Running flag 375 | bool running_ = false; 376 | 377 | // Thread pool for async method handlers 378 | thread_pool thread_pool_; 379 | 380 | // Map to track session initialization status (session_id -> initialized) 381 | std::map session_initialized_; 382 | 383 | // Handle SSE requests 384 | void handle_sse(const httplib::Request& req, httplib::Response& res); 385 | 386 | // Handle incoming JSON-RPC requests 387 | void handle_jsonrpc(const httplib::Request& req, httplib::Response& res); 388 | 389 | // Send a JSON-RPC message to a client 390 | void send_jsonrpc(const std::string& session_id, const json& message); 391 | 392 | // Process a JSON-RPC request 393 | json process_request(const request& req, const std::string& session_id); 394 | 395 | // Handle initialization request 396 | json handle_initialize(const request& req, const std::string& session_id); 397 | 398 | // Check if a session is initialized 399 | bool is_session_initialized(const std::string& session_id) const; 400 | 401 | // Set session initialization status 402 | void set_session_initialized(const std::string& session_id, bool initialized); 403 | 404 | // Generate a random session ID 405 | std::string generate_session_id() const; 406 | 407 | // Auxiliary function to create an async handler from a regular handler 408 | template 409 | std::function(const json&, const std::string&)> make_async_handler(F&& handler) { 410 | return [handler = std::forward(handler)](const json& params, const std::string& session_id) -> std::future { 411 | return std::async(std::launch::async, [handler, params, session_id]() -> json { 412 | return handler(params, session_id); 413 | }); 414 | }; 415 | } 416 | 417 | // Helper class to simplify lock management 418 | class auto_lock { 419 | public: 420 | explicit auto_lock(std::mutex& mutex) : lock_(mutex) {} 421 | private: 422 | std::lock_guard lock_; 423 | }; 424 | 425 | // Get auto lock 426 | auto_lock get_lock() const { 427 | return auto_lock(mutex_); 428 | } 429 | 430 | // Session management and maintenance 431 | void check_inactive_sessions(); 432 | 433 | std::mutex maintenance_mutex_; 434 | std::condition_variable maintenance_cond_; 435 | std::unique_ptr maintenance_thread_; 436 | bool maintenance_thread_run_ = false; 437 | 438 | // Session cleanup handler 439 | std::map session_cleanup_handler_; 440 | 441 | // Close session 442 | void close_session(const std::string& session_id); 443 | }; 444 | 445 | } // namespace mcp 446 | 447 | #endif // MCP_SERVER_H 448 | -------------------------------------------------------------------------------- /examples/agent_example.cpp: -------------------------------------------------------------------------------- 1 | #include "httplib.h" 2 | #include "mcp_server.h" 3 | #include "mcp_sse_client.h" 4 | 5 | struct Config { 6 | // LLM Config 7 | std::string base_url; 8 | std::string endpoint = "/v1/chat/completions"; 9 | std::string api_key = "sk-"; 10 | std::string model = "gpt-3.5-turbo"; 11 | std::string system_prompt = "You are a helpful agent with access to some tools. Please think what tools you need to use to answer the question before you choose them"; 12 | int max_tokens = 2048; 13 | double temperature = 0.0; 14 | 15 | // Server Config 16 | int port = 8889; 17 | 18 | // Agent Config 19 | int max_steps = 3; 20 | } config; 21 | 22 | static Config parse_config(int argc, char* argv[]) { 23 | Config config; 24 | for (size_t i = 1; i < argc; i++) { 25 | if (strcmp(argv[i], "--base-url") == 0) { 26 | try { 27 | config.base_url = argv[++i]; 28 | } catch (const std::exception& e) { 29 | std::cerr << "Error parsing base URL for LLM: " << e.what() << std::endl; 30 | exit(1); 31 | } 32 | } else if (strcmp(argv[i], "--endpoint") == 0) { 33 | try { 34 | config.endpoint = argv[++i]; 35 | } catch (const std::exception& e) { 36 | std::cerr << "Error parsing endpoint for LLM: " << e.what() << std::endl; 37 | exit(1); 38 | } 39 | } else if (strcmp(argv[i], "--api-key") == 0) { 40 | try { 41 | config.api_key = argv[++i]; 42 | } catch (const std::exception& e) { 43 | std::cerr << "Error parsing API key for LLM: " << e.what() << std::endl; 44 | exit(1); 45 | } 46 | } else if (strcmp(argv[i], "--model") == 0) { 47 | try { 48 | config.model = argv[++i]; 49 | } catch (const std::exception& e) { 50 | std::cerr << "Error parsing model for LLM: " << e.what() << std::endl; 51 | exit(1); 52 | } 53 | } else if (strcmp(argv[i], "--system-prompt") == 0) { 54 | try { 55 | config.system_prompt = argv[++i]; 56 | } catch (const std::exception& e) { 57 | std::cerr << "Error parsing system prompt for LLM: " << e.what() << std::endl; 58 | exit(1); 59 | } 60 | } else if (strcmp(argv[i], "--max-tokens") == 0) { 61 | try { 62 | config.max_tokens = std::stoi(argv[++i]); 63 | if (config.max_tokens < 1) { 64 | throw std::invalid_argument("Max tokens must be greater than 0"); 65 | } 66 | } catch (const std::exception& e) { 67 | std::cerr << "Error parsing max tokens for LLM: " << e.what() << std::endl; 68 | exit(1); 69 | } 70 | } else if (strcmp(argv[i], "--temperature") == 0) { 71 | try { 72 | config.temperature = std::stod(argv[++i]); 73 | if (config.temperature < 0.0 || config.temperature > 1.0) { 74 | throw std::invalid_argument("Temperature must be between 0 and 1"); 75 | } 76 | } catch (const std::exception& e) { 77 | std::cerr << "Error parsing temperature for LLM: " << e.what() << std::endl; 78 | exit(1); 79 | } 80 | } else if (strcmp(argv[i], "--port") == 0) { 81 | try { 82 | config.port = std::stoi(argv[++i]); 83 | } catch (const std::exception& e) { 84 | std::cerr << "Error parsing port for server: " << e.what() << std::endl; 85 | exit(1); 86 | } 87 | } 88 | } 89 | return config; 90 | } 91 | 92 | // Calculator tool handler 93 | static mcp::json calculator_handler(const mcp::json& params, const std::string& /* session_id */) { 94 | if (!params.contains("operation")) { 95 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'operation' parameter"); 96 | } 97 | 98 | std::string operation = params["operation"]; 99 | double result = 0.0; 100 | 101 | if (operation == "add") { 102 | if (!params.contains("a") || !params.contains("b")) { 103 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); 104 | } 105 | result = params["a"].get() + params["b"].get(); 106 | } else if (operation == "subtract") { 107 | if (!params.contains("a") || !params.contains("b")) { 108 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); 109 | } 110 | result = params["a"].get() - params["b"].get(); 111 | } else if (operation == "multiply") { 112 | if (!params.contains("a") || !params.contains("b")) { 113 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); 114 | } 115 | result = params["a"].get() * params["b"].get(); 116 | } else if (operation == "divide") { 117 | if (!params.contains("a") || !params.contains("b")) { 118 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Missing 'a' or 'b' parameter"); 119 | } 120 | if (params["b"].get() == 0.0) { 121 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Division by zero not allowed"); 122 | } 123 | result = params["a"].get() / params["b"].get(); 124 | } else { 125 | throw mcp::mcp_exception(mcp::error_code::invalid_params, "Unknown operation: " + operation); 126 | } 127 | 128 | return { 129 | { 130 | {"type", "text"}, 131 | {"text", std::to_string(result)} 132 | } 133 | }; 134 | } 135 | 136 | static bool readline_utf8(std::string & line, bool multiline_input) { 137 | #if defined(_WIN32) 138 | std::wstring wline; 139 | if (!std::getline(std::wcin, wline)) { 140 | // Input stream is bad or EOF received 141 | line.clear(); 142 | GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); 143 | return false; 144 | } 145 | 146 | int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wline[0], (int)wline.size(), NULL, 0, NULL, NULL); 147 | line.resize(size_needed); 148 | WideCharToMultiByte(CP_UTF8, 0, &wline[0], (int)wline.size(), &line[0], size_needed, NULL, NULL); 149 | #else 150 | if (!std::getline(std::cin, line)) { 151 | // Input stream is bad or EOF received 152 | line.clear(); 153 | return false; 154 | } 155 | #endif 156 | if (!line.empty()) { 157 | char last = line.back(); 158 | if (last == '/') { // Always return control on '/' symbol 159 | line.pop_back(); 160 | return false; 161 | } 162 | if (last == '\\') { // '\\' changes the default action 163 | line.pop_back(); 164 | multiline_input = !multiline_input; 165 | } 166 | } 167 | 168 | // By default, continue input if multiline_input is set 169 | return multiline_input; 170 | } 171 | 172 | static mcp::json ask_tool(const mcp::json& messages, const mcp::json& tools, int max_retries = 3) { 173 | static httplib::Client client(config.base_url); 174 | client.set_default_headers({ 175 | {"Authorization", "Bearer " + config.api_key} 176 | }); 177 | 178 | mcp::json body = { 179 | {"model", config.model}, 180 | {"max_tokens", config.max_tokens}, 181 | {"temperature", config.temperature}, 182 | {"messages", messages}, 183 | {"tools", tools}, 184 | {"tool_choice", "auto"} 185 | }; 186 | 187 | std::string body_str = body.dump(); 188 | 189 | int retry = 0; 190 | 191 | while (retry <= max_retries) { 192 | // send request 193 | auto res = client.Post(config.endpoint, body_str, "application/json"); 194 | 195 | if (!res) { 196 | std::cerr << std::string(__func__) << ": Failed to send request: " << httplib::to_string(res.error()); 197 | } else if (res->status == 200) { 198 | try { 199 | mcp::json json_data = mcp::json::parse(res->body); 200 | mcp::json message = json_data["choices"][0]["message"]; 201 | return message; 202 | } catch (const std::exception& e) { 203 | std::cerr << std::string(__func__) << ": Failed to parse response: error=" << std::string(e.what()) << ", body=" << res->body; 204 | } 205 | } else { 206 | std::cerr << std::string(__func__) << ": Failed to send request: status=" << std::to_string(res->status) << ", body=" << res->body; 207 | } 208 | 209 | retry++; 210 | 211 | if (retry > max_retries) { 212 | break; 213 | } 214 | 215 | // wait for a while before retrying 216 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); 217 | 218 | std::cerr << "Retrying " << retry << "/" << max_retries << std::endl; 219 | } 220 | 221 | std::cerr << "Failed to get response from LLM" << std::endl; 222 | exit(1); 223 | } 224 | 225 | static void display_message(const mcp::json& message) { 226 | mcp::json content = message.value("content", mcp::json::array()); 227 | mcp::json tool_calls = message.value("tool_calls", mcp::json::array()); 228 | 229 | std::string content_to_display; 230 | if (content.is_string()) { 231 | content_to_display = content.get(); 232 | } else if (content.is_array()) { 233 | for (const auto& item : content) { 234 | if (!item.is_object()) { 235 | throw std::invalid_argument("Invalid content item type"); 236 | } 237 | 238 | if (item["type"] == "text") { 239 | content_to_display += item["text"].get(); 240 | } else if (item["type"] == "image") { 241 | content_to_display += "[Image: " + item["image_url"]["url"].get() + "]"; 242 | } else { 243 | throw std::invalid_argument("Invalid content type: " + item["type"].get()); 244 | } 245 | } 246 | } else if (!content.empty()){ 247 | throw std::invalid_argument("Invalid content type"); 248 | } 249 | 250 | if (!tool_calls.empty()) { 251 | content_to_display += "\n\nTool calls:\n"; 252 | for (const auto& tool_call : tool_calls) { 253 | content_to_display += "- " + tool_call["function"]["name"].get() + "\n"; 254 | } 255 | } 256 | 257 | std::cout << content_to_display << "\n\n"; 258 | } 259 | 260 | int main(int argc, char* argv[]) { 261 | #if defined (_WIN32) 262 | SetConsoleCP(CP_UTF8); 263 | SetConsoleOutputCP(CP_UTF8); 264 | _setmode(_fileno(stdin), _O_WTEXT); // wide character input mode 265 | #endif 266 | 267 | // Global config 268 | config = parse_config(argc, argv); 269 | 270 | // Create example server with Calculator tool 271 | mcp::server::configuration srv_conf; 272 | srv_conf.port = config.port; 273 | srv_conf.host = "localhost"; 274 | 275 | mcp::server server(srv_conf); 276 | server.set_server_info("ExampleServer", "0.1.0"); 277 | mcp::json capabilities = { 278 | {"tools", mcp::json::object()} 279 | }; 280 | server.set_capabilities(capabilities); 281 | 282 | mcp::tool calc_tool = mcp::tool_builder("calculator") 283 | .with_description("Perform basic calculations") 284 | .with_string_param("operation", "Operation to perform (add, subtract, multiply, divide)") 285 | .with_number_param("a", "First operand") 286 | .with_number_param("b", "Second operand") 287 | .build(); 288 | 289 | server.register_tool(calc_tool, calculator_handler); 290 | 291 | mcp::json tools = mcp::json::array(); 292 | 293 | for (const auto& tool : server.get_tools()) { 294 | mcp::json converted_tool = { 295 | {"type", "function"}, 296 | {"function", { 297 | {"name", tool.name}, 298 | {"description", tool.description}, 299 | {"parameters", { 300 | {"type", "object"}, 301 | {"properties", tool.parameters_schema["properties"]}, 302 | {"required", tool.parameters_schema["required"]} 303 | }} 304 | }} 305 | }; 306 | tools.push_back(converted_tool); 307 | } 308 | 309 | // Start server 310 | server.start(false); // Non-blocking mode 311 | 312 | // Create a client 313 | mcp::sse_client client("http://localhost:" + std::to_string(config.port)); 314 | 315 | // Set timeout 316 | client.set_timeout(10); 317 | 318 | bool initialized = client.initialize("ExampleClient", "0.1.0"); 319 | 320 | if (!initialized) { 321 | std::cerr << "Failed to initialize connection to server" << std::endl; 322 | return 1; 323 | } 324 | 325 | // Get available tools 326 | { 327 | std::cout << "\nGetting available tools..." << std::endl; 328 | auto tools = client.get_tools(); 329 | std::cout << "Available tools:" << std::endl; 330 | for (const auto& tool : tools) { 331 | std::cout << "- " << tool.name << ": " << tool.description << std::endl; 332 | } 333 | } 334 | 335 | // Initialize messages 336 | mcp::json messages; 337 | 338 | if (!config.system_prompt.empty()) { 339 | mcp::json system_message = { 340 | {"role", "system"}, 341 | {"content", config.system_prompt} 342 | }; 343 | messages.push_back(system_message); 344 | } 345 | 346 | // Start chating with LLM 347 | while (true) { 348 | std::cout << "\n>>> "; 349 | 350 | std::string prompt; 351 | readline_utf8(prompt, false); 352 | 353 | messages.push_back({ 354 | {"role", "user"}, 355 | {"content", prompt} 356 | }); 357 | 358 | // Maximum steps calling tools without user input 359 | int steps = config.max_steps; 360 | 361 | while (steps--) { 362 | auto response = ask_tool(messages, tools); 363 | messages.push_back(response); 364 | 365 | display_message(response); 366 | 367 | // No tool calls, exit loop 368 | if (response["tool_calls"].empty()) { 369 | break; 370 | } 371 | 372 | // Call tool 373 | for (const auto& tool_call : response["tool_calls"]) { 374 | try { 375 | std::string tool_name = tool_call["function"]["name"].get(); 376 | 377 | std::cout << "\nCalling tool " << tool_name << "...\n\n"; 378 | 379 | // Parse arguments 380 | mcp::json args = tool_call["function"]["arguments"]; 381 | 382 | if (args.is_string()) { 383 | args = mcp::json::parse(args.get()); 384 | } 385 | 386 | // Execute the tool 387 | mcp::json result = client.call_tool(tool_name, args); 388 | 389 | auto content = result.value("content", mcp::json::array()); 390 | 391 | std::cout << "\nResult for " << tool_name << ": "; 392 | 393 | // Add response to messages 394 | messages.push_back({ 395 | {"role", "tool"}, 396 | {"tool_call_id", tool_call["id"]}, 397 | {"content", content} 398 | }); 399 | } catch (const std::exception& e) { 400 | // Handle error 401 | messages.push_back({ 402 | {"role", "tool"}, 403 | {"tool_call_id", tool_call["id"]}, 404 | {"content", "Error: " + std::string(e.what())} 405 | }); 406 | } 407 | 408 | display_message(messages.back()); 409 | } 410 | } 411 | } 412 | 413 | return 0; 414 | } -------------------------------------------------------------------------------- /src/mcp_sse_client.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_sse_client.cpp 3 | * @brief Implementation of the MCP SSE client 4 | * 5 | * This file implements the client-side functionality for the Model Context Protocol using SSE. 6 | * Follows the 2024-11-05 basic protocol specification. 7 | */ 8 | 9 | #include "mcp_sse_client.h" 10 | #include "base64.hpp" 11 | 12 | namespace mcp { 13 | 14 | sse_client::sse_client(const std::string& scheme_host_port, const std::string& sse_endpoint, bool validate_certificates, 15 | const std::string& ca_cert_path) 16 | : scheme_host_port_(scheme_host_port), sse_endpoint_(sse_endpoint) { 17 | init_client(scheme_host_port, validate_certificates, ca_cert_path); 18 | } 19 | 20 | sse_client::~sse_client() { 21 | close_sse_connection(); 22 | } 23 | 24 | 25 | void sse_client::init_client(const std::string& scheme_host_port, bool validate_certificates, const std::string& ca_cert_path) { 26 | http_client_ = std::make_unique(scheme_host_port.c_str()); 27 | sse_client_ = std::make_unique(scheme_host_port.c_str()); 28 | 29 | http_client_->set_connection_timeout(timeout_seconds_, 0); 30 | http_client_->set_read_timeout(timeout_seconds_, 0); 31 | http_client_->set_write_timeout(timeout_seconds_, 0); 32 | 33 | sse_client_->set_connection_timeout(timeout_seconds_ * 2, 0); 34 | sse_client_->set_write_timeout(timeout_seconds_, 0); 35 | 36 | #ifdef MCP_SSL 37 | http_client_->enable_server_certificate_verification(validate_certificates); 38 | sse_client_->enable_server_certificate_verification(validate_certificates); 39 | if (!ca_cert_path.empty()) { 40 | http_client_->set_ca_cert_path(ca_cert_path.c_str()); 41 | sse_client_->set_ca_cert_path(ca_cert_path.c_str()); 42 | } 43 | #endif 44 | } 45 | 46 | bool sse_client::initialize(const std::string& client_name, const std::string& client_version) { 47 | LOG_INFO("Initializing MCP client..."); 48 | 49 | request req = request::create("initialize", { 50 | {"protocolVersion", MCP_VERSION}, 51 | {"capabilities", capabilities_}, 52 | {"clientInfo", { 53 | {"name", client_name}, 54 | {"version", client_version} 55 | }} 56 | }); 57 | 58 | try { 59 | LOG_INFO("Opening SSE connection..."); 60 | open_sse_connection(); 61 | 62 | const auto timeout = std::chrono::milliseconds(5000); 63 | 64 | { 65 | std::unique_lock lock(mutex_); 66 | 67 | bool success = endpoint_cv_.wait_for(lock, timeout, [this]() { 68 | if (!sse_running_) { 69 | LOG_WARNING("SSE connection closed, stopping wait"); 70 | return true; 71 | } 72 | if (!msg_endpoint_.empty()) { 73 | LOG_INFO("Message endpoint set, stopping wait"); 74 | return true; 75 | } 76 | return false; 77 | }); 78 | 79 | if (!success) { 80 | LOG_WARNING("Condition variable wait timed out"); 81 | } 82 | 83 | if (!sse_running_) { 84 | throw std::runtime_error("SSE connection closed, failed to get message endpoint"); 85 | } 86 | 87 | if (msg_endpoint_.empty()) { 88 | throw std::runtime_error("Timeout waiting for SSE connection, failed to get message endpoint"); 89 | } 90 | 91 | LOG_INFO("Successfully got message endpoint: ", msg_endpoint_); 92 | } 93 | 94 | json result = send_jsonrpc(req); 95 | 96 | server_capabilities_ = result["capabilities"]; 97 | 98 | request notification = request::create_notification("initialized"); 99 | send_jsonrpc(notification); 100 | 101 | return true; 102 | } catch (const std::exception& e) { 103 | LOG_ERROR("Initialization failed: ", e.what()); 104 | close_sse_connection(); 105 | return false; 106 | } 107 | } 108 | 109 | bool sse_client::ping() { 110 | request req = request::create("ping", {}); 111 | 112 | try { 113 | json result = send_jsonrpc(req); 114 | return result.empty(); 115 | } catch (...) { 116 | return false; 117 | } 118 | } 119 | 120 | void sse_client::set_auth_token(const std::string& token) { 121 | { 122 | std::lock_guard lock(mutex_); 123 | auth_token_ = token; 124 | } 125 | set_header("Authorization", "Bearer " + auth_token_); 126 | } 127 | 128 | void sse_client::set_header(const std::string& key, const std::string& value) { 129 | std::lock_guard lock(mutex_); 130 | default_headers_[key] = value; 131 | 132 | if (http_client_) { 133 | http_client_->set_default_headers({{key, value}}); 134 | } 135 | if (sse_client_) { 136 | sse_client_->set_default_headers({{key, value}}); 137 | } 138 | } 139 | 140 | void sse_client::set_timeout(int timeout_seconds) { 141 | std::lock_guard lock(mutex_); 142 | timeout_seconds_ = timeout_seconds; 143 | 144 | if (http_client_) { 145 | http_client_->set_connection_timeout(timeout_seconds_, 0); 146 | http_client_->set_write_timeout(timeout_seconds_, 0); 147 | } 148 | 149 | if (sse_client_) { 150 | sse_client_->set_connection_timeout(timeout_seconds_ * 2, 0); 151 | sse_client_->set_write_timeout(timeout_seconds_, 0); 152 | } 153 | } 154 | 155 | void sse_client::set_capabilities(const json& capabilities) { 156 | std::lock_guard lock(mutex_); 157 | capabilities_ = capabilities; 158 | } 159 | 160 | response sse_client::send_request(const std::string& method, const json& params) { 161 | request req = request::create(method, params); 162 | json result = send_jsonrpc(req); 163 | 164 | response res; 165 | res.jsonrpc = "2.0"; 166 | res.id = req.id; 167 | res.result = result; 168 | 169 | return res; 170 | } 171 | 172 | void sse_client::send_notification(const std::string& method, const json& params) { 173 | request req = request::create_notification(method, params); 174 | send_jsonrpc(req); 175 | } 176 | 177 | json sse_client::get_server_capabilities() { 178 | return server_capabilities_; 179 | } 180 | 181 | json sse_client::call_tool(const std::string& tool_name, const json& arguments) { 182 | return send_request("tools/call", { 183 | {"name", tool_name}, 184 | {"arguments", arguments} 185 | }).result; 186 | } 187 | 188 | std::vector sse_client::get_tools() { 189 | json response_json = send_request("tools/list", {}).result; 190 | std::vector tools; 191 | 192 | json tools_json; 193 | if (response_json.contains("tools") && response_json["tools"].is_array()) { 194 | tools_json = response_json["tools"]; 195 | } else if (response_json.is_array()) { 196 | tools_json = response_json; 197 | } else { 198 | return tools; 199 | } 200 | 201 | for (const auto& tool_json : tools_json) { 202 | tool t; 203 | t.name = tool_json["name"]; 204 | t.description = tool_json["description"]; 205 | 206 | if (tool_json.contains("inputSchema")) { 207 | t.parameters_schema = tool_json["inputSchema"]; 208 | } 209 | 210 | tools.push_back(t); 211 | } 212 | 213 | return tools; 214 | } 215 | 216 | json sse_client::get_capabilities() { 217 | return capabilities_; 218 | } 219 | 220 | json sse_client::list_resources(const std::string& cursor) { 221 | json params = json::object(); 222 | if (!cursor.empty()) { 223 | params["cursor"] = cursor; 224 | } 225 | return send_request("resources/list", params).result; 226 | } 227 | 228 | json sse_client::read_resource(const std::string& resource_uri) { 229 | return send_request("resources/read", { 230 | {"uri", resource_uri} 231 | }).result; 232 | } 233 | 234 | json sse_client::subscribe_to_resource(const std::string& resource_uri) { 235 | return send_request("resources/subscribe", { 236 | {"uri", resource_uri} 237 | }).result; 238 | } 239 | 240 | json sse_client::list_resource_templates() { 241 | return send_request("resources/templates/list").result; 242 | } 243 | 244 | void sse_client::open_sse_connection() { 245 | sse_running_ = true; 246 | 247 | { 248 | std::lock_guard lock(mutex_); 249 | msg_endpoint_.clear(); 250 | endpoint_cv_.notify_all(); 251 | } 252 | 253 | std::string connection_info = "Base URL: " + scheme_host_port_ + ", SSE Endpoint: " + sse_endpoint_; 254 | LOG_INFO("Attempting to establish SSE connection: ", connection_info); 255 | 256 | sse_thread_ = std::make_unique([this]() { 257 | int retry_count = 0; 258 | const int max_retries = 5; 259 | const int retry_delay_base = 1000; 260 | 261 | while (sse_running_) { 262 | try { 263 | LOG_INFO("SSE thread: Attempting to connect to ", sse_endpoint_); 264 | 265 | std::string buffer; 266 | auto res = sse_client_->Get(sse_endpoint_, 267 | [&,this](const char *data, size_t data_length) { 268 | buffer.append(data, data_length); 269 | 270 | // Normalize CRLF to LF 271 | size_t crlf_pos = buffer.find("\r\n"); 272 | while (crlf_pos != std::string::npos) { 273 | buffer.replace(crlf_pos, 2, "\n"); 274 | crlf_pos = buffer.find("\r\n", crlf_pos + 1); 275 | } 276 | 277 | // Process complete events in buffer 278 | size_t start_pos = 0; 279 | while ((start_pos = buffer.find("\n\n", start_pos)) != std::string::npos) { 280 | size_t end_pos = start_pos + 2; 281 | std::string event = buffer.substr(0, start_pos); 282 | buffer.erase(0, end_pos); 283 | start_pos = 0; 284 | 285 | if (!parse_sse_data(event.data(), event.size())) { 286 | LOG_ERROR("SSE thread: Failed to parse event"); 287 | } 288 | } 289 | 290 | return sse_running_.load(); 291 | }); 292 | 293 | if (!res || res->status / 100 != 2) { 294 | std::string error_msg = "SSE connection failed: "; 295 | error_msg += httplib::to_string(res.error()); 296 | throw std::runtime_error(error_msg); 297 | } 298 | 299 | retry_count = 0; 300 | LOG_INFO("SSE thread: Connection successful"); 301 | } catch (const std::exception& e) { 302 | if (!sse_running_) { 303 | LOG_INFO("SSE connection actively closed, no retry needed"); 304 | break; 305 | } 306 | 307 | if (++retry_count > max_retries) { 308 | LOG_ERROR("Maximum retry count reached, stopping SSE connection attempts"); 309 | break; 310 | } 311 | 312 | LOG_ERROR("SSE connection error: ", e.what()); 313 | 314 | int delay = retry_delay_base * (1 << (retry_count - 1)); 315 | LOG_INFO("Will retry in ", delay, " ms (attempt ", retry_count, "/", max_retries, ")"); 316 | 317 | const int check_interval = 100; 318 | for (int waited = 0; waited < delay && sse_running_; waited += check_interval) { 319 | std::this_thread::sleep_for(std::chrono::milliseconds(check_interval)); 320 | } 321 | 322 | if (!sse_running_) { 323 | LOG_INFO("SSE connection actively closed during retry wait, stopping retry"); 324 | break; 325 | } 326 | } 327 | } 328 | 329 | LOG_INFO("SSE thread: Exiting"); 330 | }); 331 | } 332 | 333 | bool sse_client::parse_sse_data(const char* data, size_t length) { 334 | try { 335 | // Split into lines and process event fields 336 | std::istringstream stream(std::string(data, length)); 337 | std::string line; 338 | std::string event_type = "message"; 339 | std::vector data_lines; 340 | 341 | while (std::getline(stream, line)) { 342 | // Trim trailing CR if present 343 | if (!line.empty() && line.back() == '\r') { 344 | line.pop_back(); 345 | } 346 | 347 | if (line.substr(0, 7) == "event: ") { 348 | event_type = line.substr(7); 349 | } else if (line.substr(0, 6) == "data: ") { 350 | data_lines.push_back(line.substr(6)); 351 | } else if (line.empty()) { 352 | break; // End of event 353 | } 354 | } 355 | 356 | if (data_lines.empty()) { 357 | return true; 358 | } 359 | 360 | // Join data lines with newlines 361 | std::string data_content; 362 | for (size_t i = 0; i < data_lines.size(); ++i) { 363 | if (i > 0) data_content += '\n'; 364 | data_content += data_lines[i]; 365 | } 366 | 367 | if (event_type == "heartbeat") { 368 | return true; 369 | } else if (event_type == "endpoint") { 370 | std::lock_guard lock(mutex_); 371 | msg_endpoint_ = data_content; 372 | endpoint_cv_.notify_all(); 373 | return true; 374 | } else if (event_type == "message") { 375 | try { 376 | json response = json::parse(data_content); 377 | 378 | if (response.contains("jsonrpc") && response.contains("id") && !response["id"].is_null()) { 379 | json id = response["id"]; 380 | 381 | std::lock_guard lock(response_mutex_); 382 | auto it = pending_requests_.find(id); 383 | if (it != pending_requests_.end()) { 384 | if (response.contains("result")) { 385 | it->second.set_value(response["result"]); 386 | } else if (response.contains("error")) { 387 | json error_result = { 388 | {"isError", true}, 389 | {"error", response["error"]} 390 | }; 391 | it->second.set_value(error_result); 392 | } else { 393 | it->second.set_value(json::object()); 394 | } 395 | 396 | pending_requests_.erase(it); 397 | } else { 398 | LOG_WARNING("Received response for unknown request ID: ", id); 399 | } 400 | } else { 401 | LOG_WARNING("Received invalid JSON-RPC response: ", response.dump()); 402 | } 403 | } catch (const json::exception& e) { 404 | LOG_ERROR("Failed to parse JSON-RPC response: ", e.what()); 405 | } 406 | return true; 407 | } else { 408 | LOG_WARNING("Received unknown event type: ", event_type); 409 | return true; 410 | } 411 | } catch (const std::exception& e) { 412 | LOG_ERROR("Error parsing SSE data: ", e.what()); 413 | return false; 414 | } 415 | } 416 | 417 | void sse_client::close_sse_connection() { 418 | if (!sse_running_) { 419 | LOG_INFO("SSE connection already closed"); 420 | return; 421 | } 422 | 423 | LOG_INFO("Actively closing SSE connection (normal exit flow)..."); 424 | 425 | sse_running_ = false; 426 | 427 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); 428 | 429 | if (sse_thread_ && sse_thread_->joinable()) { 430 | auto timeout = std::chrono::seconds(5); 431 | auto start = std::chrono::steady_clock::now(); 432 | 433 | LOG_INFO("Waiting for SSE thread to end..."); 434 | 435 | while (sse_thread_->joinable() && 436 | std::chrono::steady_clock::now() - start < timeout) { 437 | try { 438 | sse_thread_->join(); 439 | LOG_INFO("SSE thread successfully ended"); 440 | break; 441 | } catch (const std::exception& e) { 442 | LOG_ERROR("Error waiting for SSE thread: ", e.what()); 443 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 444 | } 445 | } 446 | 447 | if (sse_thread_->joinable()) { 448 | LOG_WARNING("SSE thread did not end within timeout, detaching thread"); 449 | sse_thread_->detach(); 450 | } 451 | } 452 | 453 | { 454 | std::lock_guard lock(mutex_); 455 | msg_endpoint_.clear(); 456 | endpoint_cv_.notify_all(); 457 | } 458 | 459 | LOG_INFO("SSE connection successfully closed (normal exit flow)"); 460 | } 461 | 462 | json sse_client::send_jsonrpc(const request& req) { 463 | std::lock_guard lock(mutex_); 464 | 465 | if (msg_endpoint_.empty()) { 466 | throw mcp_exception(error_code::internal_error, "Message endpoint not set, SSE connection may not be established"); 467 | } 468 | 469 | json req_json = req.to_json(); 470 | std::string req_body = req_json.dump(); 471 | 472 | httplib::Headers headers; 473 | headers.emplace("Content-Type", "application/json"); 474 | 475 | for (const auto& [key, value] : default_headers_) { 476 | headers.emplace(key, value); 477 | } 478 | 479 | if (req.is_notification()) { 480 | auto result = http_client_->Post(msg_endpoint_, headers, req_body, "application/json"); 481 | 482 | if (!result) { 483 | auto err = result.error(); 484 | std::string error_msg = httplib::to_string(err); 485 | LOG_ERROR("JSON-RPC request failed: ", error_msg); 486 | throw mcp_exception(error_code::internal_error, error_msg); 487 | } 488 | 489 | return json::object(); 490 | } 491 | 492 | std::promise response_promise; 493 | std::future response_future = response_promise.get_future(); 494 | 495 | { 496 | std::lock_guard response_lock(response_mutex_); 497 | pending_requests_[req.id] = std::move(response_promise); 498 | } 499 | 500 | auto result = http_client_->Post(msg_endpoint_, headers, req_body, "application/json"); 501 | 502 | if (!result) { 503 | auto err = result.error(); 504 | std::string error_msg = httplib::to_string(err); 505 | 506 | { 507 | std::lock_guard response_lock(response_mutex_); 508 | pending_requests_.erase(req.id); 509 | } 510 | 511 | LOG_ERROR("JSON-RPC request failed: ", error_msg); 512 | throw mcp_exception(error_code::internal_error, error_msg); 513 | } 514 | 515 | if (result->status / 100 != 2) { 516 | try { 517 | json res_json = json::parse(result->body); 518 | 519 | { 520 | std::lock_guard response_lock(response_mutex_); 521 | pending_requests_.erase(req.id); 522 | } 523 | 524 | if (res_json.contains("error")) { 525 | int code = res_json["error"]["code"]; 526 | std::string message = res_json["error"]["message"]; 527 | 528 | throw mcp_exception(static_cast(code), message); 529 | } 530 | 531 | if (res_json.contains("result")) { 532 | return res_json["result"]; 533 | } else { 534 | return json::object(); 535 | } 536 | } catch (const json::exception& e) { 537 | { 538 | std::lock_guard response_lock(response_mutex_); 539 | pending_requests_.erase(req.id); 540 | } 541 | 542 | throw mcp_exception(error_code::parse_error, 543 | "Failed to parse JSON-RPC response: " + std::string(e.what())); 544 | } 545 | } else { 546 | const auto timeout = std::chrono::seconds(timeout_seconds_); 547 | 548 | auto status = response_future.wait_for(timeout); 549 | 550 | if (status == std::future_status::ready) { 551 | json response = response_future.get(); 552 | 553 | if (response.contains("isError") && response["isError"].is_boolean() && response["isError"].get()) { 554 | if (response.contains("error") && response["error"].is_object()) { 555 | const auto& err_obj = response["error"]; 556 | int code = err_obj.contains("code") ? err_obj["code"].get() : static_cast(error_code::internal_error); 557 | std::string message = err_obj.value("message", ""); 558 | // Handle error 559 | throw mcp_exception(static_cast(code), message); 560 | } 561 | } 562 | 563 | return response; 564 | } else { 565 | { 566 | std::lock_guard response_lock(response_mutex_); 567 | pending_requests_.erase(req.id); 568 | } 569 | 570 | throw mcp_exception(error_code::internal_error, "Timeout waiting for SSE response"); 571 | } 572 | } 573 | } 574 | 575 | bool sse_client::is_running() const { 576 | return sse_running_; 577 | } 578 | 579 | } // namespace mcp 580 | -------------------------------------------------------------------------------- /test/mcp_test.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file mcp_test.cpp 3 | * @brief Test the basic functions of the MCP framework 4 | * 5 | * This file contains tests for the message format, lifecycle, version control, ping, and tool functionality of the MCP framework. 6 | */ 7 | 8 | #include 9 | #include 10 | #include "mcp_message.h" 11 | #include "mcp_client.h" 12 | #include "mcp_server.h" 13 | #include "mcp_tool.h" 14 | #include "mcp_sse_client.h" 15 | 16 | using namespace mcp; 17 | using json = nlohmann::ordered_json; 18 | 19 | // Test message format 20 | class MessageFormatTest : public ::testing::Test { 21 | protected: 22 | void SetUp() override { 23 | // Set up test environment 24 | } 25 | 26 | void TearDown() override { 27 | // Clean up test environment 28 | } 29 | }; 30 | 31 | // Test request message format 32 | TEST_F(MessageFormatTest, RequestMessageFormat) { 33 | // Create a request message 34 | request req = request::create("test_method", {{"key", "value"}}); 35 | 36 | // Convert to JSON 37 | json req_json = req.to_json(); 38 | 39 | // Verify JSON format is correct 40 | EXPECT_EQ(req_json["jsonrpc"], "2.0"); 41 | EXPECT_TRUE(req_json.contains("id")); 42 | EXPECT_EQ(req_json["method"], "test_method"); 43 | EXPECT_EQ(req_json["params"]["key"], "value"); 44 | } 45 | 46 | // Test response message format 47 | TEST_F(MessageFormatTest, ResponseMessageFormat) { 48 | // Create a successful response 49 | response res = response::create_success("test_id", {{"key", "value"}}); 50 | 51 | // Convert to JSON 52 | json res_json = res.to_json(); 53 | 54 | // Verify JSON format is correct 55 | EXPECT_EQ(res_json["jsonrpc"], "2.0"); 56 | EXPECT_EQ(res_json["id"], "test_id"); 57 | EXPECT_EQ(res_json["result"]["key"], "value"); 58 | EXPECT_FALSE(res_json.contains("error")); 59 | } 60 | 61 | // Test error response message format 62 | TEST_F(MessageFormatTest, ErrorResponseMessageFormat) { 63 | // Create an error response 64 | response res = response::create_error("test_id", error_code::invalid_params, "Invalid parameters", {{"details", "Missing required field"}}); 65 | 66 | // Convert to JSON 67 | json res_json = res.to_json(); 68 | 69 | // Verify JSON format is correct 70 | EXPECT_EQ(res_json["jsonrpc"], "2.0"); 71 | EXPECT_EQ(res_json["id"], "test_id"); 72 | EXPECT_FALSE(res_json.contains("result")); 73 | EXPECT_EQ(res_json["error"]["code"], static_cast(error_code::invalid_params)); 74 | EXPECT_EQ(res_json["error"]["message"], "Invalid parameters"); 75 | EXPECT_EQ(res_json["error"]["data"]["details"], "Missing required field"); 76 | } 77 | 78 | // Test notification message format 79 | TEST_F(MessageFormatTest, NotificationMessageFormat) { 80 | // Create a notification message 81 | request notification = request::create_notification("test_notification", {{"key", "value"}}); 82 | 83 | // Convert to JSON 84 | json notification_json = notification.to_json(); 85 | 86 | // Verify JSON format is correct 87 | EXPECT_EQ(notification_json["jsonrpc"], "2.0"); 88 | EXPECT_FALSE(notification_json.contains("id")); 89 | EXPECT_EQ(notification_json["method"], "notifications/test_notification"); 90 | EXPECT_EQ(notification_json["params"]["key"], "value"); 91 | 92 | // Verify if it is a notification message 93 | EXPECT_TRUE(notification.is_notification()); 94 | } 95 | 96 | class LifecycleEnvironment : public ::testing::Environment { 97 | public: 98 | void SetUp() override { 99 | // Set up test environment 100 | server_ = std::make_unique("localhost", 8080); 101 | server_->set_server_info("TestServer", "1.0.0"); 102 | 103 | // Set server capabilities 104 | json server_capabilities = { 105 | {"logging", json::object()}, 106 | {"prompts", {{"listChanged", true}}}, 107 | {"resources", {{"subscribe", true}, {"listChanged", true}}}, 108 | {"tools", {{"listChanged", true}}} 109 | }; 110 | server_->set_capabilities(server_capabilities); 111 | 112 | // Start server (non-blocking mode) 113 | server_->start(false); 114 | 115 | // Create client 116 | json client_capabilities = { 117 | {"roots", {{"listChanged", true}}}, 118 | {"sampling", json::object()} 119 | }; 120 | client_ = std::make_unique("localhost", 8080); 121 | client_->set_capabilities(client_capabilities); 122 | } 123 | 124 | void TearDown() override { 125 | // Clean up test environment 126 | client_.reset(); 127 | server_->stop(); 128 | server_.reset(); 129 | } 130 | 131 | static std::unique_ptr& GetServer() { 132 | return server_; 133 | } 134 | 135 | static std::unique_ptr& GetClient() { 136 | return client_; 137 | } 138 | 139 | private: 140 | static std::unique_ptr server_; 141 | static std::unique_ptr client_; 142 | }; 143 | 144 | // Static member variable definition 145 | std::unique_ptr LifecycleEnvironment::server_; 146 | std::unique_ptr LifecycleEnvironment::client_; 147 | 148 | class LifecycleTest : public ::testing::Test { 149 | protected: 150 | void SetUp() override { 151 | // Get client pointer 152 | client_ = LifecycleEnvironment::GetClient().get(); 153 | } 154 | 155 | // Use raw pointer instead of reference 156 | sse_client* client_; 157 | }; 158 | 159 | // Test initialize process 160 | TEST_F(LifecycleTest, InitializeProcess) { 161 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 162 | // Execute initialize 163 | bool init_result = client_->initialize("TestClient", "1.0.0"); 164 | 165 | // Verify initialize result 166 | EXPECT_TRUE(init_result); 167 | 168 | // Verify server capabilities 169 | json server_capabilities = client_->get_server_capabilities(); 170 | EXPECT_TRUE(server_capabilities.contains("logging")); 171 | EXPECT_TRUE(server_capabilities.contains("prompts")); 172 | EXPECT_TRUE(server_capabilities.contains("resources")); 173 | EXPECT_TRUE(server_capabilities.contains("tools")); 174 | } 175 | 176 | // Version control test environment 177 | class VersioningEnvironment : public ::testing::Environment { 178 | public: 179 | void SetUp() override { 180 | // Set up test environment 181 | server_ = std::make_unique("localhost", 8081); 182 | server_->set_server_info("TestServer", "1.0.0"); 183 | 184 | // Set server capabilities 185 | json server_capabilities = { 186 | {"logging", json::object()}, 187 | {"prompts", {{"listChanged", true}}}, 188 | {"resources", {{"subscribe", true}, {"listChanged", true}}}, 189 | {"tools", {{"listChanged", true}}} 190 | }; 191 | server_->set_capabilities(server_capabilities); 192 | 193 | // Start server (non-blocking mode) 194 | server_->start(false); 195 | 196 | client_ = std::make_unique("localhost", 8081); 197 | } 198 | 199 | void TearDown() override { 200 | // Clean up test environment 201 | client_.reset(); 202 | server_->stop(); 203 | server_.reset(); 204 | } 205 | 206 | static std::unique_ptr& GetServer() { 207 | return server_; 208 | } 209 | 210 | static std::unique_ptr& GetClient() { 211 | return client_; 212 | } 213 | 214 | private: 215 | static std::unique_ptr server_; 216 | static std::unique_ptr client_; 217 | }; 218 | 219 | std::unique_ptr VersioningEnvironment::server_; 220 | std::unique_ptr VersioningEnvironment::client_; 221 | 222 | // Test version control 223 | class VersioningTest : public ::testing::Test { 224 | protected: 225 | void SetUp() override { 226 | // Get client pointer 227 | client_ = VersioningEnvironment::GetClient().get(); 228 | } 229 | 230 | // Use raw pointer instead of reference 231 | sse_client* client_; 232 | }; 233 | 234 | // Test supported version 235 | TEST_F(VersioningTest, SupportedVersion) { 236 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 237 | // Execute initialize 238 | bool init_result = client_->initialize("TestClient", "1.0.0"); 239 | 240 | // Verify initialize result 241 | EXPECT_TRUE(init_result); 242 | } 243 | 244 | // Test unsupported version 245 | TEST_F(VersioningTest, UnsupportedVersion) { 246 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 247 | try { 248 | // Use httplib::Client to send unsupported version request 249 | std::unique_ptr sse_client = std::make_unique("localhost", 8081); 250 | std::unique_ptr http_client = std::make_unique("localhost", 8081); 251 | 252 | // Open SSE connection 253 | std::promise msg_endpoint_promise; 254 | std::promise sse_promise; 255 | std::future msg_endpoint = msg_endpoint_promise.get_future(); 256 | std::future sse_response = sse_promise.get_future(); 257 | 258 | std::atomic sse_running{true}; 259 | std::atomic msg_endpoint_received{false}; 260 | std::atomic sse_response_received{false}; 261 | 262 | std::thread sse_thread([&]() { 263 | sse_client->Get("/sse", [&](const char* data, size_t len) { 264 | try { 265 | std::string response(data, len); 266 | size_t pos = response.find("data: "); 267 | if (pos != std::string::npos) { 268 | std::string data_content = response.substr(pos + 6); 269 | data_content = data_content.substr(0, data_content.find("\r\n")); 270 | 271 | if (!msg_endpoint_received.load() && response.find("endpoint") != std::string::npos) { 272 | msg_endpoint_received.store(true); 273 | try { 274 | msg_endpoint_promise.set_value(data_content); 275 | } catch (...) { 276 | // Ignore duplicate exception setting 277 | } 278 | } else if (!sse_response_received.load() && response.find("message") != std::string::npos) { 279 | sse_response_received.store(true); 280 | try { 281 | sse_promise.set_value(data_content); 282 | } catch (...) { 283 | // Ignore duplicate exception setting 284 | } 285 | } 286 | } 287 | } catch (const std::exception& e) { 288 | GTEST_LOG_(ERROR) << "SSE processing error: " << e.what(); 289 | } 290 | return sse_running.load(); 291 | }); 292 | }); 293 | 294 | std::string endpoint = msg_endpoint.get(); 295 | EXPECT_FALSE(endpoint.empty()); 296 | 297 | // Send unsupported version request 298 | json req = request::create("initialize", {{"protocolVersion", "0.0.1"}}).to_json(); 299 | auto res = http_client->Post(endpoint.c_str(), req.dump(), "application/json"); 300 | 301 | EXPECT_TRUE(res != nullptr); 302 | EXPECT_EQ(res->status / 100, 2); 303 | 304 | auto mcp_res = json::parse(sse_response.get()); 305 | EXPECT_EQ(mcp_res["error"]["code"].get(), static_cast(error_code::invalid_params)); 306 | 307 | // Close all connections 308 | sse_running.store(false); 309 | 310 | // Try to interrupt SSE connection 311 | try { 312 | sse_client->Get("/sse", [](const char*, size_t) { return false; }); 313 | } catch (...) { 314 | // Ignore any exception 315 | } 316 | 317 | // Wait for thread to finish (max 1 second) 318 | if (sse_thread.joinable()) { 319 | std::thread detacher([](std::thread& t) { 320 | try { 321 | if (t.joinable()) { 322 | t.join(); 323 | } 324 | } catch (...) { 325 | if (t.joinable()) { 326 | t.detach(); 327 | } 328 | } 329 | }, std::ref(sse_thread)); 330 | detacher.detach(); 331 | } 332 | 333 | // Clean up resources 334 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 335 | sse_client.reset(); 336 | http_client.reset(); 337 | 338 | // Add delay to ensure resources are fully released 339 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 340 | } catch (...) { 341 | EXPECT_TRUE(false); 342 | } 343 | } 344 | 345 | // Ping test environment 346 | class PingEnvironment : public ::testing::Environment { 347 | public: 348 | void SetUp() override { 349 | // Set up test environment 350 | server_ = std::make_unique("localhost", 8082); 351 | 352 | // Start server (non-blocking mode) 353 | server_->start(false); 354 | 355 | // Create client 356 | json client_capabilities = { 357 | {"roots", {{"listChanged", true}}}, 358 | {"sampling", json::object()} 359 | }; 360 | client_ = std::make_unique("localhost", 8082); 361 | client_->set_capabilities(client_capabilities); 362 | } 363 | 364 | void TearDown() override { 365 | // Clean up test environment 366 | client_.reset(); 367 | server_->stop(); 368 | server_.reset(); 369 | } 370 | 371 | static std::unique_ptr& GetServer() { 372 | return server_; 373 | } 374 | 375 | static std::unique_ptr& GetClient() { 376 | return client_; 377 | } 378 | 379 | private: 380 | static std::unique_ptr server_; 381 | static std::unique_ptr client_; 382 | }; 383 | 384 | // Static member variable definition 385 | std::unique_ptr PingEnvironment::server_; 386 | std::unique_ptr PingEnvironment::client_; 387 | 388 | // Test Ping functionality 389 | class PingTest : public ::testing::Test { 390 | protected: 391 | void SetUp() override { 392 | // Get client pointer 393 | client_ = PingEnvironment::GetClient().get(); 394 | } 395 | 396 | // Use raw pointer instead of reference 397 | sse_client* client_; 398 | }; 399 | 400 | // Test Ping request 401 | TEST_F(PingTest, PingRequest) { 402 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 403 | client_->initialize("TestClient", "1.0.0"); 404 | bool ping_result = client_->ping(); 405 | EXPECT_TRUE(ping_result); 406 | } 407 | 408 | TEST_F(PingTest, DirectPing) { 409 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 410 | try { 411 | // Use httplib::Client to send Ping request 412 | std::unique_ptr sse_client = std::make_unique("localhost", 8082); 413 | std::unique_ptr http_client = std::make_unique("localhost", 8082); 414 | 415 | // Open SSE connection 416 | std::promise msg_endpoint_promise; 417 | std::promise sse_promise; 418 | std::future msg_endpoint = msg_endpoint_promise.get_future(); 419 | std::future sse_response = sse_promise.get_future(); 420 | 421 | std::atomic sse_running{true}; 422 | std::atomic msg_endpoint_received{false}; 423 | std::atomic sse_response_received{false}; 424 | 425 | std::thread sse_thread([&]() { 426 | sse_client->Get("/sse", [&](const char* data, size_t len) { 427 | try { 428 | std::string response(data, len); 429 | size_t pos = response.find("data: "); 430 | if (pos != std::string::npos) { 431 | std::string data_content = response.substr(pos + 6); 432 | data_content = data_content.substr(0, data_content.find("\r\n")); 433 | 434 | if (!msg_endpoint_received.load() && response.find("endpoint") != std::string::npos) { 435 | msg_endpoint_received.store(true); 436 | try { 437 | msg_endpoint_promise.set_value(data_content); 438 | } catch (...) { 439 | // Ignore duplicate exception setting 440 | } 441 | } else if (!sse_response_received.load() && response.find("message") != std::string::npos) { 442 | sse_response_received.store(true); 443 | try { 444 | sse_promise.set_value(data_content); 445 | } catch (...) { 446 | // Ignore duplicate exception setting 447 | } 448 | } 449 | } 450 | } catch (const std::exception& e) { 451 | GTEST_LOG_(ERROR) << "SSE processing error: " << e.what(); 452 | } 453 | return sse_running.load(); 454 | }); 455 | }); 456 | 457 | std::string endpoint = msg_endpoint.get(); 458 | EXPECT_FALSE(endpoint.empty()); 459 | 460 | // Even if the SSE connection is not established, you can send a ping request 461 | json ping_req = request::create("ping").to_json(); 462 | auto ping_res = http_client->Post(endpoint.c_str(), ping_req.dump(), "application/json"); 463 | EXPECT_TRUE(ping_res != nullptr); 464 | EXPECT_EQ(ping_res->status / 100, 2); 465 | 466 | auto mcp_res = json::parse(sse_response.get()); 467 | EXPECT_EQ(mcp_res["result"], json::object()); 468 | 469 | // Close all connections 470 | sse_running.store(false); 471 | 472 | // Try to interrupt SSE connection 473 | try { 474 | sse_client->Get("/sse", [](const char*, size_t) { return false; }); 475 | } catch (...) { 476 | // Ignore any exception 477 | } 478 | 479 | // Wait for thread to finish (max 1 second) 480 | if (sse_thread.joinable()) { 481 | std::thread detacher([](std::thread& t) { 482 | try { 483 | if (t.joinable()) { 484 | t.join(); 485 | } 486 | } catch (...) { 487 | if (t.joinable()) { 488 | t.detach(); 489 | } 490 | } 491 | }, std::ref(sse_thread)); 492 | detacher.detach(); 493 | } 494 | 495 | // Clean up resources 496 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 497 | sse_client.reset(); 498 | http_client.reset(); 499 | 500 | // Add delay to ensure resources are fully released 501 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 502 | } catch (...) { 503 | EXPECT_TRUE(false); 504 | } 505 | } 506 | 507 | // Tools test environment 508 | class ToolsEnvironment : public ::testing::Environment { 509 | public: 510 | void SetUp() override { 511 | // Set up test environment 512 | server_ = std::make_unique("localhost", 8083); 513 | 514 | // Create a test tool 515 | tool test_tool; 516 | test_tool.name = "get_weather"; 517 | test_tool.description = "Get current weather information for a location"; 518 | test_tool.parameters_schema = { 519 | {"type", "object"}, 520 | {"properties", { 521 | {"location", { 522 | {"type", "string"}, 523 | {"description", "City name or zip code"} 524 | }} 525 | }}, 526 | {"required", json::array({"location"})} 527 | }; 528 | 529 | // Register tool 530 | server_->register_tool(test_tool, [](const json& params, const std::string& /* session_id */) -> json { 531 | // Simple tool implementation 532 | std::string location = params["location"]; 533 | return { 534 | {"content", json::array({ 535 | { 536 | {"type", "text"}, 537 | {"text", "Current weather in " + location + ":\nTemperature: 72°F\nConditions: Partly cloudy"} 538 | } 539 | })}, 540 | {"isError", false} 541 | }; 542 | }); 543 | 544 | // Register tools list method 545 | server_->register_method("tools/list", [](const json& params, const std::string& /* session_id */) -> json { 546 | return { 547 | {"tools", json::array({ 548 | { 549 | {"name", "get_weather"}, 550 | {"description", "Get current weather information for a location"}, 551 | {"inputSchema", { 552 | {"type", "object"}, 553 | {"properties", { 554 | {"location", { 555 | {"type", "string"}, 556 | {"description", "City name or zip code"} 557 | }} 558 | }}, 559 | {"required", json::array({"location"})} 560 | }} 561 | } 562 | })}, 563 | {"nextCursor", nullptr} 564 | }; 565 | }); 566 | 567 | // Register tools call method 568 | server_->register_method("tools/call", [](const json& params, const std::string& /* session_id */) -> json { 569 | // Verify parameters 570 | EXPECT_EQ(params["name"], "get_weather"); 571 | EXPECT_EQ(params["arguments"]["location"], "New York"); 572 | 573 | // Return tool call result 574 | return { 575 | {"content", json::array({ 576 | { 577 | {"type", "text"}, 578 | {"text", "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"} 579 | } 580 | })}, 581 | {"isError", false} 582 | }; 583 | }); 584 | 585 | // Start server (non-blocking mode) 586 | server_->start(false); 587 | 588 | // Create client 589 | json client_capabilities = { 590 | {"roots", {{"listChanged", true}}}, 591 | {"sampling", json::object()} 592 | }; 593 | client_ = std::make_unique("localhost", 8083); 594 | client_->set_capabilities(client_capabilities); 595 | client_->initialize("TestClient", "1.0.0"); 596 | } 597 | 598 | void TearDown() override { 599 | // Clean up test environment 600 | client_.reset(); 601 | server_->stop(); 602 | server_.reset(); 603 | } 604 | 605 | static std::unique_ptr& GetServer() { 606 | return server_; 607 | } 608 | 609 | static std::unique_ptr& GetClient() { 610 | return client_; 611 | } 612 | 613 | private: 614 | static std::unique_ptr server_; 615 | static std::unique_ptr client_; 616 | }; 617 | 618 | // Static member variable definition 619 | std::unique_ptr ToolsEnvironment::server_; 620 | std::unique_ptr ToolsEnvironment::client_; 621 | 622 | // Test tools functionality 623 | class ToolsTest : public ::testing::Test { 624 | protected: 625 | void SetUp() override { 626 | // Get client pointer 627 | client_ = ToolsEnvironment::GetClient().get(); 628 | } 629 | 630 | // Use raw pointer instead of reference 631 | sse_client* client_; 632 | }; 633 | 634 | // Test listing tools 635 | TEST_F(ToolsTest, ListTools) { 636 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 637 | 638 | // Call list tools method 639 | json tools_list = client_->send_request("tools/list").result; 640 | 641 | // Verify tools list 642 | EXPECT_TRUE(tools_list.contains("tools")); 643 | EXPECT_EQ(tools_list["tools"].size(), 1); 644 | EXPECT_EQ(tools_list["tools"][0]["name"], "get_weather"); 645 | EXPECT_EQ(tools_list["tools"][0]["description"], "Get current weather information for a location"); 646 | } 647 | 648 | // Test calling tool 649 | TEST_F(ToolsTest, CallTool) { 650 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 651 | // Call tool 652 | json tool_result = client_->call_tool("get_weather", {{"location", "New York"}}); 653 | 654 | // Verify tool call result 655 | EXPECT_TRUE(tool_result.contains("content")); 656 | EXPECT_FALSE(tool_result["isError"]); 657 | EXPECT_EQ(tool_result["content"][0]["type"], "text"); 658 | EXPECT_EQ(tool_result["content"][0]["text"], "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"); 659 | } 660 | 661 | int main(int argc, char **argv) { 662 | ::testing::InitGoogleTest(&argc, argv); 663 | 664 | // Add global test environment 665 | ::testing::AddGlobalTestEnvironment(new LifecycleEnvironment()); 666 | ::testing::AddGlobalTestEnvironment(new VersioningEnvironment()); 667 | ::testing::AddGlobalTestEnvironment(new PingEnvironment()); 668 | ::testing::AddGlobalTestEnvironment(new ToolsEnvironment()); 669 | 670 | return RUN_ALL_TESTS(); 671 | } --------------------------------------------------------------------------------