From 82ba732c97586e2c8acc4c4439da5782d4d640fa Mon Sep 17 00:00:00 2001 From: caomengxuan666 <2507560089@qq.com> Date: Mon, 25 Aug 2025 13:04:58 +0800 Subject: [PATCH 1/2] feat(plugins): Refactor Python plugin support and add new features - Redesigned the loading and execution mechanism for Python plugins - Added new mcp_sdk.py to simplify plugin development - Updated plugin examples and related CMake configurations - Optimized Python environment initialization and error handling - Added support for exporting plugins with different Python environments - Achieved seamless compatibility between C++-developed and Python-developed plugins in terms of user experience --- CMakeLists.txt | 18 +- README.md | 24 ++ README_zh.md | 24 ++ cmake/EnablePython.cmake | 28 ++ cmake/PluginCommon.cmake | 118 ++++++++ docs/PYTHON_PLUGINS.md | 118 ++++++++ docs/PYTHON_PLUGINS_zh.md | 118 ++++++++ .../python_example_plugin/CMakeLists.txt | 49 +--- .../python_example_plugin.py | 133 +++++---- plugins/sdk/CMakeLists.txt | 6 + plugins/sdk/mcp_plugin.h | 6 + plugins/sdk/mcp_python_plugin.py | 198 ------------- plugins/sdk/mcp_sdk.py | 250 ++++++++++++++++ plugins/sdk/pybind_module.cpp | 191 ------------ plugins/sdk/pybind_module_plugin.cpp | 276 ++++++------------ plugins/sdk/python_plugin_CMakeLists.txt | 50 ---- src/business/CMakeLists.txt | 6 +- src/business/plugin_manager.cpp | 32 +- src/business/plugin_manager.h | 2 + src/business/python_plugin_instance.cpp | 251 ++++++++++++++++ src/business/python_plugin_instance.h | 35 +++ src/business/python_runtime_manager.cpp | 117 ++++++++ src/business/python_runtime_manager.h | 32 ++ tools/plugin_ctl.cpp | 80 ++--- 24 files changed, 1362 insertions(+), 800 deletions(-) create mode 100644 cmake/EnablePython.cmake create mode 100644 docs/PYTHON_PLUGINS.md create mode 100644 docs/PYTHON_PLUGINS_zh.md delete mode 100644 plugins/sdk/mcp_python_plugin.py create mode 100644 plugins/sdk/mcp_sdk.py delete mode 100644 plugins/sdk/pybind_module.cpp delete mode 100644 plugins/sdk/python_plugin_CMakeLists.txt create mode 100644 src/business/python_plugin_instance.cpp create mode 100644 src/business/python_plugin_instance.h create mode 100644 src/business/python_runtime_manager.cpp create mode 100644 src/business/python_runtime_manager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9116f45..064cadf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.22) -project(MCPServer.cpp LANGUAGES CXX C VERSION 1.0.5.0) +project(MCPServer.cpp LANGUAGES CXX C VERSION 1.0.6.0) # Add option for Python support option(ENABLE_PYTHON_PLUGINS "Enable Python plugin support" ON) @@ -98,20 +98,8 @@ add_subdirectory(src/Resources) add_subdirectory(src/routers) add_subdirectory(src/Prompts) -# Conditionally add Python support -if(ENABLE_PYTHON_PLUGINS) - find_package(Python COMPONENTS Interpreter Development) - if(Python_FOUND) - message(STATUS "Python found: ${Python_VERSION}") - message(STATUS "Python executable: ${Python_EXECUTABLE}") - message(STATUS "Python include dirs: ${Python_INCLUDE_DIRS}") - message(STATUS "Python libraries: ${Python_LIBRARIES}") - else() - message(WARNING "Python not found. Disabling Python plugin support.") - set(ENABLE_PYTHON_PLUGINS OFF) - endif() -endif() - +include(cmake/EnablePython.cmake) +enable_python() # turn off mimalloc tests and examples set(MI_BUILD_TESTS OFF CACHE BOOL "Build mimalloc tests" FORCE) set(MI_BUILD_EXAMPLES OFF CACHE BOOL "Build mimalloc examples" FORCE) diff --git a/README.md b/README.md index 0ae495c..5c3f703 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,30 @@ MCPServer.cpp supports a powerful plugin system that allows extending functional - `safe_system_plugin`: Secure system command execution - `example_stream_plugin`: Streaming data example +### Python Plugins + +MCPServer++ now supports Python plugins through a new Python SDK that makes plugin development more intuitive. Python plugins are compiled to dynamic libraries (DLL/SO) that wrap Python code using pybind11. + +#### Creating Python Plugins + +To create a new Python plugin, use the `plugin_ctl` tool: + +```bash +./plugin_ctl create -p my_python_plugin +``` + +This will generate a Python plugin template that uses the new Python SDK with decorators and helper functions. + +#### Python Plugin Features + +- Decorator-based tool definition with `@tool` +- Automatic JSON handling +- Streaming tool support +- Parameter validation helpers +- Easy integration with the MCP protocol + +For detailed information about Python plugin development, see [Python Plugins Documentation](docs/PYTHON_PLUGINS.md). + ### Plugin Development See [plugins/README.md](plugins/README.md) for detailed information on developing custom plugins. diff --git a/README_zh.md b/README_zh.md index aebcfce..075a714 100644 --- a/README_zh.md +++ b/README_zh.md @@ -180,6 +180,30 @@ MCPServer.cpp 支持强大的插件系统,允许在不修改核心服务器的 - `safe_system_plugin`: 安全系统命令执行 - `example_stream_plugin`: 流式数据示例 +### Python 插件 + +MCPServer++ 现在通过新的 Python SDK 支持 Python 插件,这使得插件开发更加直观。Python 插件被编译为动态库 (DLL/SO),使用 pybind11 包装 Python 代码。 + +#### 创建 Python 插件 + +要创建新的 Python 插件,请使用 [plugin_ctl](file:///D:/codespace/MCPServer++/tools/plugin_ctl.cpp#L759-L759) 工具: + +```bash +./plugin_ctl create -p my_python_plugin +``` + +这将生成一个使用新的 Python SDK 的 Python 插件模板,其中包含装饰器和辅助函数。 + +#### Python 插件特性 + +- 基于装饰器的工具定义,使用 [@tool](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py#L173-L186) +- 自动 JSON 处理 +- 流式工具支持 +- 参数验证辅助函数 +- 与 MCP 协议的轻松集成 + +有关 Python 插件开发的详细信息,请参阅 [Python 插件文档](docs/PYTHON_PLUGINS_zh.md)。 + ### 插件开发 有关开发自定义插件的详细信息,请参阅 [plugins/README_zh.md](plugins/README_zh.md)。 diff --git a/cmake/EnablePython.cmake b/cmake/EnablePython.cmake new file mode 100644 index 0000000..47a8c1b --- /dev/null +++ b/cmake/EnablePython.cmake @@ -0,0 +1,28 @@ +include_guard(GLOBAL) + +function(target_enable_python target_name) +if(ENABLE_PYTHON_PLUGINS) + find_package(Python COMPONENTS Interpreter Development) + if(Python_FOUND) + target_include_directories(${target_name} PUBLIC ${Python_INCLUDE_DIRS}) + else() + message(WARNING "Python not found. Disabling Python plugin support.") + set(ENABLE_PYTHON_PLUGINS OFF) + endif() +endif() +endfunction() + +function(enable_python) +if(ENABLE_PYTHON_PLUGINS) + find_package(Python COMPONENTS Interpreter Development) + if(Python_FOUND) + message(STATUS "Python found: ${Python_VERSION}") + message(STATUS "Python executable: ${Python_EXECUTABLE}") + message(STATUS "Python include dirs: ${Python_INCLUDE_DIRS}") + message(STATUS "Python libraries: ${Python_LIBRARIES}") + else() + message(WARNING "Python not found. Disabling Python plugin support.") + set(ENABLE_PYTHON_PLUGINS OFF) + endif() +endif() +endfunction() \ No newline at end of file diff --git a/cmake/PluginCommon.cmake b/cmake/PluginCommon.cmake index 8464fcb..4175380 100644 --- a/cmake/PluginCommon.cmake +++ b/cmake/PluginCommon.cmake @@ -124,4 +124,122 @@ function(configure_plugin plugin_name src_files) endforeach() endif() endif() +endfunction() + +# Common function to configure a Python plugin +# args: +# plugin_name - Name of the plugin +# python_module - Path to the Python module file (.py) +function(configure_python_plugin plugin_name python_module) + # Find required packages + find_package(Python COMPONENTS Interpreter Development REQUIRED) + + # Add the plugin library using the pybind wrapper + add_library(${plugin_name} SHARED + ${PROJECT_SOURCE_DIR}/plugins/sdk/pybind_module_plugin.cpp + ) + + # Include directories + target_include_directories(${plugin_name} PRIVATE + # MCPServer++ include directories + ${PROJECT_SOURCE_DIR}/plugins/sdk + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/third_party/nlohmann + ${PROJECT_SOURCE_DIR}/third_party/pybind11/include + ${PROJECT_SOURCE_DIR}/src + ) + + # Add preprocessor definition for DLL export + target_compile_definitions(${plugin_name} PRIVATE MCPSERVER_API_EXPORTS PYBIND11_EXPORT_OVERRIDE) + + # Link libraries + target_link_libraries(${plugin_name} PRIVATE + pybind11::embed + mcp_business + ) + + # Set output directory for plugins to bin/plugins + set_target_properties(${plugin_name} PROPERTIES + PREFIX "" + RUNTIME_OUTPUT_DIRECTORY ${PLUGINS_OUTPUT_DIR} + LIBRARY_OUTPUT_DIRECTORY ${PLUGINS_OUTPUT_DIR} + ) + + if(WIN32) + set_target_properties(${plugin_name} PROPERTIES SUFFIX ".dll") + else() + set_target_properties(${plugin_name} PROPERTIES SUFFIX ".so") + endif() + + # Install the plugin to bin/plugins (always install plugins) + install(TARGETS ${plugin_name} + RUNTIME DESTINATION bin/plugins + LIBRARY DESTINATION bin/plugins + ) + + # Copy the Python module file to the output directory after build + add_custom_command(TARGET ${plugin_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${python_module} + $ + ) + + # Copy the SDK file to the output directory after build if it exists + set(sdk_file "${PROJECT_SOURCE_DIR}/plugins/sdk/mcp_sdk.py") + if(EXISTS ${sdk_file}) + add_custom_command(TARGET ${plugin_name} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${sdk_file} + $ + ) + endif() + + # Install the Python module file + install(FILES ${python_module} + DESTINATION bin/plugins + ) + + # Install the SDK file if it exists + if(EXISTS ${sdk_file}) + install(FILES ${sdk_file} + DESTINATION bin/plugins + ) + endif() + + # automatically generate tools.json - always do this for runtime configs + set(json_source_file "${CMAKE_CURRENT_SOURCE_DIR}/tools.json") + set(json_target_file "${plugin_name}_tools.json") + + if(EXISTS ${json_source_file}) + # Create configs directory if not exists + file(MAKE_DIRECTORY ${CONFIGS_OUTPUT_DIR}) + + configure_file( + ${json_source_file} + ${CONFIGS_OUTPUT_DIR}/${json_target_file} + COPYONLY + ) + + # build when the plugin is built + add_custom_command( + OUTPUT ${CONFIGS_OUTPUT_DIR}/${json_target_file} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${json_source_file} + ${CONFIGS_OUTPUT_DIR}/${json_target_file} + DEPENDS ${json_source_file} + COMMENT "Copying ${plugin_name} tools.json file to configs directory" + ) + + add_custom_target(${plugin_name}_json ALL + DEPENDS ${CONFIGS_OUTPUT_DIR}/${json_target_file} + ) + + add_dependencies(${plugin_name} ${plugin_name}_json) + + # Install configs to bin/configs (always install configs) + install(FILES ${json_source_file} + DESTINATION bin/configs + RENAME ${json_target_file} + ) + endif() endfunction() \ No newline at end of file diff --git a/docs/PYTHON_PLUGINS.md b/docs/PYTHON_PLUGINS.md new file mode 100644 index 0000000..6a6b58b --- /dev/null +++ b/docs/PYTHON_PLUGINS.md @@ -0,0 +1,118 @@ +# Python Plugins for MCP Server++ + +Python plugins provide an easy way to extend the functionality of the MCP Server++ using Python. With the new Python SDK, developers can create plugins more intuitively using decorators and other syntactic sugar. + +## Table of Contents + +- [Introduction](#introduction) +- [Python SDK Overview](#python-sdk-overview) +- [Creating a Python Plugin](#creating-a-python-plugin) +- [Plugin Structure](#plugin-structure) +- [Using the Python SDK](#using-the-python-sdk) +- [Building Python Plugins](#building-python-plugins) +- [Best Practices](#best-practices) + +## Introduction + +Python plugins are dynamic libraries (DLL on Windows, so on Linux) that wrap Python code using pybind11. They implement the standard MCP plugin interface while allowing the actual plugin logic to be written in Python. + +## Python SDK Overview + +The Python SDK (`mcp_sdk.py`) provides a set of utilities and decorators to make plugin development more intuitive: + +1. `@tool` decorator - Register functions as MCP tools +2. Parameter helper functions - Easily define tool parameters +3. `ToolType` enum - Specify standard or streaming tools +4. Automatic JSON handling - Automatically convert between Python objects and JSON + +## Creating a Python Plugin + +Use the `plugin_ctl` tool to generate a new Python plugin template: + +```bash +./plugin_ctl create -p my_python_plugin +``` + +This will create a new directory `my_python_plugin` with the basic Python plugin structure. + +## Plugin Structure + +A typical Python plugin consists of: + +- `plugin_name.py` - The main Python plugin implementation using the SDK +- `tools.json` - JSON file describing the tools provided by the plugin +- `CMakeLists.txt` - CMake configuration for building the plugin +- `mcp_sdk.py` - The Python SDK (automatically copied during build) + +## Using the Python SDK + +### Basic Tool Definition + +```python +from mcp_sdk import tool, string_param + +@tool( + name="hello_world", + description="A simple greeting tool", + name_param=string_param(description="The name to greet", required=True) +) +def hello_world_tool(name_param: str): + return f"Hello, {name_param}!" +``` + +### Streaming Tool Definition + +```python +from mcp_sdk import tool, integer_param, ToolType + +@tool( + name="stream_counter", + description="Stream a sequence of numbers", + tool_type=ToolType.STREAMING, + count=integer_param(description="Number of items to stream", default=5) +) +def stream_counter_tool(count: int = 5): + for i in range(count): + yield {"number": i, "text": f"Item {i}"} +``` + +### Parameter Helper Functions + +The SDK provides helper functions for defining common parameter types: + +- `string_param()` - Define a string parameter +- `integer_param()` - Define an integer parameter +- `number_param()` - Define a float parameter +- `boolean_param()` - Define a boolean parameter +- `array_param()` - Define an array parameter +- `object_param()` - Define an object parameter + +## Building Python Plugins + +Python plugins are built using CMake, just like C++ plugins. The build process automatically: + +1. Compiles the C++ wrapper code +2. Copies the Python plugin file +3. Copies the Python SDK + +To build a Python plugin: + +```bash +cd my_python_plugin +mkdir build +cd build +cmake .. +cmake --build . +``` + +The resulting DLL/SO file can then be used with the MCP Server++. + +## Best Practices + +1. **Use the SDK decorators**: They handle JSON conversion and tool registration automatically +2. **Define clear parameter schemas**: Use parameter helper functions to document your tool's interface +3. **Handle errors gracefully**: Python exceptions will be automatically converted to MCP error responses +4. **Use type hints**: They improve code readability and help catch errors early +5. **Test streaming tools**: Ensure your generators work correctly and clean up resources properly +6. **Document your tools**: Provide clear descriptions for tools and parameters +7. **Keep plugin files organized**: For complex plugins, consider splitting code into multiple modules \ No newline at end of file diff --git a/docs/PYTHON_PLUGINS_zh.md b/docs/PYTHON_PLUGINS_zh.md new file mode 100644 index 0000000..6ea30e9 --- /dev/null +++ b/docs/PYTHON_PLUGINS_zh.md @@ -0,0 +1,118 @@ +# MCP Server++ 的 Python 插件 + +Python 插件提供了一种简单的方法,可以使用 Python 扩展 MCP Server++ 的功能。通过新的 Python SDK,开发人员可以使用装饰器和其他语法糖更直观地创建插件。 + +## 目录 + +- [简介](#简介) +- [Python SDK 概述](#python-sdk-概述) +- [创建 Python 插件](#创建-python-插件) +- [插件结构](#插件结构) +- [使用 Python SDK](#使用-python-sdk) +- [构建 Python 插件](#构建-python-插件) +- [最佳实践](#最佳实践) + +## 简介 + +Python 插件是包装 Python 代码的动态库(Windows 上是 DLL,Linux 上是 so),使用 pybind11 实现。它们实现了标准的 MCP 插件接口,同时允许使用 Python 编写实际的插件逻辑。 + +## Python SDK 概述 + +Python SDK ([mcp_sdk.py](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py)) 提供了一组实用程序和装饰器,使插件开发更加直观: + +1. [@tool](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py#L173-L186) 装饰器 - 将函数注册为 MCP 工具 +2. 参数辅助函数 - 轻松定义工具参数 +3. [ToolType](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py#L17-L20) 枚举 - 指定标准或流式工具 +4. 自动 JSON 处理 - 在 Python 对象和 JSON 之间自动转换 + +## 创建 Python 插件 + +使用 [plugin_ctl](file:///D:/codespace/MCPServer++/tools/plugin_ctl.cpp#L759-L759) 工具生成新的 Python 插件模板: + +```bash +./plugin_ctl create -p my_python_plugin +``` + +这将创建一个新的目录 [my_python_plugin](file://d:\codespace\MCPServer++\tests\my_python_plugin),其中包含基本的 Python 插件结构。 + +## 插件结构 + +一个典型的 Python 插件包括: + +- `plugin_name.py` - 使用 SDK 的主要 Python 插件实现 +- `tools.json` - 描述插件提供的工具的 JSON 文件 +- `CMakeLists.txt` - 构建插件的 CMake 配置 +- [mcp_sdk.py](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py) - Python SDK(构建期间自动复制) + +## 使用 Python SDK + +### 基本工具定义 + +```python +from mcp_sdk import tool, string_param + +@tool( + name="hello_world", + description="一个简单的问候工具", + name_param=string_param(description="要问候的名称", required=True) +) +def hello_world_tool(name_param: str): + return f"Hello, {name_param}!" +``` + +### 流式工具定义 + +```python +from mcp_sdk import tool, integer_param, ToolType + +@tool( + name="stream_counter", + description="流式传输一系列数字", + tool_type=ToolType.STREAMING, + count=integer_param(description="要流式传输的项目数", default=5) +) +def stream_counter_tool(count: int = 5): + for i in range(count): + yield {"number": i, "text": f"项目 {i}"} +``` + +### 参数辅助函数 + +SDK 提供了用于定义常见参数类型的辅助函数: + +- [string_param()](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py#L222-L224) - 定义字符串参数 +- [integer_param()](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py#L227-L229) - 定义整数参数 +- [number_param()](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py#L232-L234) - 定义浮点数参数 +- [boolean_param()](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py#L237-L239) - 定义布尔参数 +- [array_param()](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py#L242-L244) - 定义数组参数 +- [object_param()](file://d:\codespace\MCPServer++\plugins\sdk\mcp_sdk.py#L247-L249) - 定义对象参数 + +## 构建 Python 插件 + +Python 插件使用 CMake 构建,就像 C++ 插件一样。构建过程会自动: + +1. 编译 C++ 包装代码 +2. 复制 Python 插件文件 +3. 复制 Python SDK + +构建 Python 插件: + +```bash +cd my_python_plugin +mkdir build +cd build +cmake .. +cmake --build . +``` + +生成的 DLL/SO 文件可以与 MCP Server++ 一起使用。 + +## 最佳实践 + +1. **使用 SDK 装饰器**:它们会自动处理 JSON 转换和工具注册 +2. **定义清晰的参数模式**:使用参数辅助函数记录工具的接口 +3. **妥善处理错误**:Python 异常将自动转换为 MCP 错误响应 +4. **使用类型提示**:它们提高了代码可读性并帮助及早发现错误 +5. **测试流式工具**:确保您的生成器正常工作并正确清理资源 +6. **记录您的工具**:为工具和参数提供清晰的描述 +7. **保持插件文件有序**:对于复杂的插件,考虑将代码拆分为多个模块 \ No newline at end of file diff --git a/plugins/official/python_example_plugin/CMakeLists.txt b/plugins/official/python_example_plugin/CMakeLists.txt index afb81f0..70229f0 100644 --- a/plugins/official/python_example_plugin/CMakeLists.txt +++ b/plugins/official/python_example_plugin/CMakeLists.txt @@ -1,50 +1,7 @@ # CMakeLists.txt for Python Example Plugin cmake_minimum_required(VERSION 3.23) -project(python_example_plugin) -# Set C++ standard -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +# Use the common function to configure the Python plugin +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../cmake/PluginCommon.cmake) -# Set the path to MCPServer++ root directory -set(MCP_SERVER_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." CACHE STRING "Path to MCPServer++ root directory") - -# Find required packages -find_package(Python COMPONENTS Interpreter Development REQUIRED) - -# Add the plugin library -add_library(${PROJECT_NAME} SHARED - # The pybind wrapper that exposes Python functions as C interface - ${MCP_SERVER_ROOT}/plugins/sdk/pybind_module_plugin.cpp -) - - -# Include directories -target_include_directories(${PROJECT_NAME} PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - # MCPServer++ include directories - ${MCP_SERVER_ROOT}/plugins/sdk - ${MCP_SERVER_ROOT}/include - ${MCP_SERVER_ROOT}/third_party/nlohmann - ${MCP_SERVER_ROOT}/third_party/pybind11/include -) - -# Add preprocessor definition for DLL export -target_compile_definitions(${PROJECT_NAME} PRIVATE MCPSERVER_API_EXPORTS) - -# Link libraries -target_link_libraries(${PROJECT_NAME} PRIVATE - pybind11::embed -) - -# Ensure the Python plugin file is available -configure_file(${CMAKE_CURRENT_SOURCE_DIR}/python_example_plugin.py - ${CMAKE_CURRENT_BINARY_DIR}/python_example_plugin.py - COPYONLY) - -# Copy the Python file to the output directory after build -add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_CURRENT_SOURCE_DIR}/python_example_plugin.py - $/python_example_plugin.py -) \ No newline at end of file +configure_python_plugin(python_example_plugin ${CMAKE_CURRENT_SOURCE_DIR}/python_example_plugin.py) \ No newline at end of file diff --git a/plugins/official/python_example_plugin/python_example_plugin.py b/plugins/official/python_example_plugin/python_example_plugin.py index 94b8a88..41c92d9 100644 --- a/plugins/official/python_example_plugin/python_example_plugin.py +++ b/plugins/official/python_example_plugin/python_example_plugin.py @@ -10,70 +10,93 @@ 3. Build using CMake as described in the CMakeLists.txt file """ -class ToolInfo: - """Tool information structure""" - def __init__(self, name, description, parameters, is_streaming=False): - self.name = name - self.description = description - self.parameters = parameters # JSON Schema string - self.is_streaming = is_streaming +# Import our new SDK +import sys +import os -def get_tools(): - """ - Get the list of tools provided by this plugin - This function will be called by the C++ wrapper - """ - return [ - ToolInfo( - name="python_echo", - description="Echo back the input text", - parameters='{"type": "object", "properties": {"text": {"type": "string", "description": "Text to echo"}}, "required": ["text"]}' - ), - ToolInfo( - name="python_calculate", - description="Perform a simple calculation", - parameters='{"type": "object", "properties": {"expression": {"type": "string", "description": "Mathematical expression to evaluate"}}, "required": ["expression"]}' - ) - ] +# Try multiple methods to find the SDK +def find_sdk_path(): + """Find the SDK path using multiple strategies""" + # Strategy 1: Relative to this file (development) + sdk_path = os.path.join(os.path.dirname(__file__), '..', '..', 'sdk') + if os.path.exists(os.path.join(sdk_path, 'mcp_sdk.py')): + return sdk_path + + # Strategy 2: Relative to plugin location (installed) + plugin_dir = os.path.dirname(__file__) + sdk_path = os.path.join(plugin_dir, 'mcp_sdk.py') + if os.path.exists(sdk_path): + # SDK is in the same directory as this plugin + return plugin_dir + + # Strategy 3: Check if mcp_sdk.py is directly accessible + if os.path.exists('mcp_sdk.py'): + return '.' + + # If all else fails, return the original path for error message + return os.path.join(os.path.dirname(__file__), '..', '..', 'sdk') -def call_tool(name, args_json): - """ - Call a specific tool with arguments - This function will be called by the C++ wrapper +# Add the SDK path to sys.path so we can import it +sdk_path = find_sdk_path() +sys.path.insert(0, sdk_path) + +try: + from mcp_sdk import tool, string_param, call_tool, get_tools +except ImportError as e: + # Create a fallback implementation for testing + print(f"Warning: Could not import mcp_sdk: {e}") + print(f" Tried path: {sdk_path}") - Args: - name: Tool name - args_json: JSON string with tool arguments - - Returns: - JSON string with tool result - """ - import json + # Minimal fallback implementation for testing + def tool(name=None, description="", **parameters): + def decorator(func): + func._tool_name = name or func.__name__ + func._tool_description = description + func._tool_parameters = parameters + return func + return decorator - try: - args = json.loads(args_json) if args_json else {} - except json.JSONDecodeError: - return json.dumps({"error": {"type": "invalid_json", "message": "Invalid JSON in arguments"}}) + def string_param(description="", required=False, default=None): + return {"type": "string", "description": description, + "required": required, "default": default} - if name == "python_echo": - text = args.get("text", "") - return json.dumps({"result": text}) - - elif name == "python_calculate": - expression = args.get("expression", "") - try: - result = eval(expression, {"__builtins__": {}}, {}) - return json.dumps({"result": result}) - except Exception as e: - return json.dumps({"error": {"type": "calculation_error", "message": str(e)}}) - - else: - return json.dumps({"error": {"type": "unknown_tool", "message": f"Unknown tool: {name}"}}) + # Fallback functions that mimic the real interface + def get_tools(): + return [] + + def call_tool(name, args_json): + import json + return json.dumps({"error": {"type": "unavailable", "message": "SDK not available"}}) + +@tool( + name="python_echo", + description="Echo back the input text", + text=string_param(description="Text to echo", required=True) +) +def echo_tool(text: str) -> str: + """Echo the input text""" + return text + +@tool( + name="python_calculate", + description="Perform a simple calculation", + expression=string_param(description="Mathematical expression to evaluate", required=True) +) +def calculate_tool(expression: str) -> str: + """Calculate a mathematical expression""" + try: + # Note: In a real implementation, you should use a safer evaluation method + # This is just for demonstration purposes + result = eval(expression, {"__builtins__": {}}, {}) + return str(result) + except Exception as e: + return f"Error: {str(e)}" # For testing purposes when running the script directly if __name__ == "__main__": # This section is for testing the plugin directly - print("Tools:", [tool.name for tool in get_tools()]) + tools = get_tools() + print("Tools:", [getattr(tool, '_tool_name', 'unknown') for tool in [echo_tool, calculate_tool]]) result = call_tool("python_echo", '{"text": "Hello from Python!"}') print("Echo result:", result) diff --git a/plugins/sdk/CMakeLists.txt b/plugins/sdk/CMakeLists.txt index 653fada..ae30819 100644 --- a/plugins/sdk/CMakeLists.txt +++ b/plugins/sdk/CMakeLists.txt @@ -4,6 +4,7 @@ set(TARGET_NAME mcp_plugin_sdk) set(SDK_SOURCES tool_info_parser.cpp tool_info_parser.h + pybind_module_plugin.cpp ) set(HEADERS @@ -11,8 +12,11 @@ set(HEADERS tool_info_parser.h ) + add_library(${TARGET_NAME} ${SDK_SOURCES} ${HEADERS}) +include(${CMAKE_SOURCE_DIR}/cmake/EnablePython.cmake) +target_enable_python(${TARGET_NAME}) target_include_directories(${TARGET_NAME} PUBLIC @@ -23,6 +27,7 @@ target_include_directories(${TARGET_NAME} $ $ $ + $ ) target_compile_features(${TARGET_NAME} PRIVATE cxx_std_17) @@ -32,6 +37,7 @@ target_link_libraries(${TARGET_NAME} PUBLIC mcp_core mcp_protocol + mcp_business ) target_compile_definitions(${TARGET_NAME} PUBLIC MCPSERVER_API_EXPORTS) diff --git a/plugins/sdk/mcp_plugin.h b/plugins/sdk/mcp_plugin.h index b5c3a1d..59bd90e 100644 --- a/plugins/sdk/mcp_plugin.h +++ b/plugins/sdk/mcp_plugin.h @@ -28,6 +28,12 @@ typedef ToolInfo *(*get_tools_func)(int *count); // Function pointer to free result typedef void (*free_result_func)(const char *); +// Function pointer to initialize python plugin +typedef bool (*initialize_plugin_func)(const char *plugin_path); + +// Function pointer to uninitialize python plugin +typedef void (*uninitialize_plugin_func)(const char *plugin_path); + // streaming support typedef void *StreamGenerator; diff --git a/plugins/sdk/mcp_python_plugin.py b/plugins/sdk/mcp_python_plugin.py deleted file mode 100644 index df29fbb..0000000 --- a/plugins/sdk/mcp_python_plugin.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -MCPServer++ Python Plugin SDK - -This module provides the base classes and utilities for creating Python plugins -that can be loaded by the MCPServer++. - -Example usage: -```python -from mcp_python_plugin import MCPPlugin, ToolInfo - -class MyPlugin(MCPPlugin): - def get_tools(self): - return [ - ToolInfo( - name="hello_world", - description="A simple hello world tool", - parameters='{"type": "object", "properties": {}}' - ) - ] - - def call_tool(self, name, args_json): - if name == "hello_world": - return '{"result": "Hello, World!"}' - else: - raise ValueError(f"Unknown tool: {name}") - -# Create plugin instance -plugin = MyPlugin() - -# Export required functions -get_tools = plugin.get_tools_wrapper -call_tool = plugin.call_tool_wrapper -free_result = plugin.free_result_wrapper -``` -""" - -import json -import ctypes -from typing import List, Optional, Any -from dataclasses import dataclass -from abc import ABC, abstractmethod - -@dataclass -class ToolInfo: - """Tool information structure""" - name: str - description: str - parameters: str # JSON Schema string - is_streaming: bool = False - -@dataclass -class MCPError: - """Error information structure""" - code: int = 0 - message: str = "" - details: str = "" - source: str = "" - -class MCPPlugin(ABC): - """ - Base class for MCP Python plugins - """ - - def __init__(self): - self._results = {} # Store results to prevent garbage collection - self._result_counter = 0 - - @abstractmethod - def get_tools(self) -> List[ToolInfo]: - """ - Get the list of tools provided by this plugin - """ - pass - - @abstractmethod - def call_tool(self, name: str, args_json: str) -> str: - """ - Call a specific tool with arguments - - Args: - name: Tool name - args_json: JSON string with tool arguments - - Returns: - JSON string with tool result - """ - pass - - def get_tools_wrapper(self, count) -> ctypes.POINTER(ToolInfo): - """ - Wrapper for the get_tools function to be exported - """ - try: - tools = self.get_tools() - count.contents.value = len(tools) - - # Convert to ctypes array - ToolInfoArray = ToolInfo * len(tools) - self._tools_array = ToolInfoArray(*tools) - return ctypes.cast(self._tools_array, ctypes.POINTER(ToolInfo)) - except Exception as e: - count.contents.value = 0 - return None - - def call_tool_wrapper(self, name: ctypes.c_char_p, args_json: ctypes.c_char_p, error: ctypes.POINTER(MCPError)) -> ctypes.c_char_p: - """ - Wrapper for the call_tool function to be exported - """ - try: - name_str = name.decode('utf-8') if name else "" - args_str = args_json.decode('utf-8') if args_json else "{}" - - result = self.call_tool(name_str, args_str) - - # Store result to prevent garbage collection - self._result_counter += 1 - result_key = self._result_counter - self._results[result_key] = result - - # Return as c_char_p - return ctypes.c_char_p(result.encode('utf-8')) - except Exception as e: - if error: - error.contents.code = -1 - error.contents.message = str(e) - return None - - def free_result_wrapper(self, result: ctypes.c_char_p): - """ - Wrapper for the free_result function to be exported - """ - # In Python, we don't need to manually free memory, - # but we can remove it from our tracking dict - try: - if result: - # Find and remove the result from our tracking dict - result_str = ctypes.cast(result, ctypes.c_char_p).value - if result_str: - # Find the key for this result and remove it - keys_to_remove = [] - for key, value in self._results.items(): - if value.encode('utf-8') == result_str: - keys_to_remove.append(key) - - for key in keys_to_remove: - del self._results[key] - except Exception: - pass # Ignore errors in cleanup - -# Example plugin implementation -class ExamplePlugin(MCPPlugin): - """ - Example plugin implementation showing how to create a Python plugin - """ - - def get_tools(self) -> List[ToolInfo]: - return [ - ToolInfo( - name="python_echo", - description="Echo back the input text", - parameters='{"type": "object", "properties": {"text": {"type": "string", "description": "Text to echo"}}, "required": ["text"]}' - ), - ToolInfo( - name="python_calculate", - description="Perform a simple calculation", - parameters='{"type": "object", "properties": {"expression": {"type": "string", "description": "Mathematical expression to evaluate"}}, "required": ["expression"]}' - ) - ] - - def call_tool(self, name: str, args_json: str) -> str: - args = json.loads(args_json) if args_json else {} - - if name == "python_echo": - text = args.get("text", "") - return json.dumps({"result": text}) - - elif name == "python_calculate": - expression = args.get("expression", "") - try: - result = eval(expression) - return json.dumps({"result": result}) - except Exception as e: - return json.dumps({"error": {"type": "calculation_error", "message": str(e)}}) - - else: - raise ValueError(f"Unknown tool: {name}") - -# For testing purposes -if __name__ == "__main__": - # This section is for testing the plugin directly - plugin = ExamplePlugin() - print("Tools:", plugin.get_tools()) - - result = plugin.call_tool("python_echo", '{"text": "Hello from Python!"}') - print("Echo result:", result) - - result = plugin.call_tool("python_calculate", '{"expression": "2+2"}') - print("Calculation result:", result) \ No newline at end of file diff --git a/plugins/sdk/mcp_sdk.py b/plugins/sdk/mcp_sdk.py new file mode 100644 index 0000000..89ae2f9 --- /dev/null +++ b/plugins/sdk/mcp_sdk.py @@ -0,0 +1,250 @@ +""" +MCP Server++ Python SDK + +This SDK provides a simple way to create MCP plugins using Python with decorators +and other syntactic sugar to make plugin development more intuitive and less error-prone. +""" + +import json +import functools +from typing import Any, Dict, List, Callable, Optional, Union +from dataclasses import dataclass, field +from enum import Enum + + +class ToolType(Enum): + """Tool types supported by MCP""" + STANDARD = "standard" + STREAMING = "streaming" + + +@dataclass +class ToolParameter: + """Represents a tool parameter""" + type: str + description: str + required: bool = False + default: Any = None + + +@dataclass +class ToolInfo: + """Tool information structure""" + name: str + description: str + parameters: Dict[str, ToolParameter] = field(default_factory=dict) + tool_type: ToolType = ToolType.STANDARD + + def to_schema(self) -> str: + """Convert to JSON schema string""" + schema = { + "type": "object", + "properties": {}, + "required": [] + } + + for param_name, param in self.parameters.items(): + prop = { + "type": param.type, + "description": param.description + } + + if param.default is not None: + prop["default"] = param.default + + schema["properties"][param_name] = prop + + if param.required: + schema["required"].append(param_name) + + return json.dumps(schema) + + +class MCPPlugin: + """ + Main plugin class that manages tools and their execution + """ + + def __init__(self, name: str = ""): + self.name = name + self._tools: Dict[str, Callable] = {} + self._tool_infos: Dict[str, ToolInfo] = {} + self._initialized = False + + def tool(self, + name: Optional[str] = None, + description: str = "", + tool_type: ToolType = ToolType.STANDARD, + **parameters: ToolParameter): + """ + Decorator to register a function as an MCP tool + + Args: + name: Tool name (defaults to function name) + description: Tool description + tool_type: Type of tool (standard or streaming) + **parameters: Tool parameters as ToolParameter objects + """ + def decorator(func: Callable) -> Callable: + tool_name = name or func.__name__ + + # Create tool info + tool_info = ToolInfo( + name=tool_name, + description=description, + parameters=parameters, + tool_type=tool_type + ) + + # Register tool + self._tools[tool_name] = func + self._tool_infos[tool_name] = tool_info + + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + return decorator + + def get_tools(self) -> List[ToolInfo]: + """Get list of all registered tools""" + return list(self._tool_infos.values()) + + def call_tool(self, name: str, args_json: str) -> str: + """ + Call a specific tool with JSON arguments + + Args: + name: Tool name + args_json: JSON string with tool arguments + + Returns: + JSON string with tool result + """ + try: + args = json.loads(args_json) if args_json else {} + except json.JSONDecodeError: + return json.dumps({ + "error": { + "type": "invalid_json", + "message": "Invalid JSON in arguments" + } + }) + + if name not in self._tools: + return json.dumps({ + "error": { + "type": "unknown_tool", + "message": f"Unknown tool: {name}" + } + }) + + try: + # Call the tool function with arguments + result = self._tools[name](**args) + + # Handle streaming tools differently + if self._tool_infos[name].tool_type == ToolType.STREAMING: + # For streaming tools, we might need special handling + # This is a simplified implementation + if hasattr(result, '__iter__') and not isinstance(result, (str, bytes)): + # If it's a generator or iterator, convert to list + result = list(result) + + # Return result as JSON + if isinstance(result, dict) and "error" in result: + return json.dumps(result) + else: + return json.dumps({"result": result}) + + except Exception as e: + return json.dumps({ + "error": { + "type": "execution_error", + "message": str(e) + } + }) + + +# Global plugin instance +_plugin = MCPPlugin() + + +def tool(name: Optional[str] = None, + description: str = "", + tool_type: ToolType = ToolType.STANDARD, + **parameters: ToolParameter): + """ + Decorator to register a function as an MCP tool at module level + + Args: + name: Tool name (defaults to function name) + description: Tool description + tool_type: Type of tool (standard or streaming) + **parameters: Tool parameters as ToolParameter objects + """ + return _plugin.tool(name, description, tool_type, **parameters) + + +def get_tools(): + """ + Get the list of tools provided by this plugin + This function will be called by the C++ wrapper + """ + # Convert our ToolInfo objects to the format expected by C++ + tools = [] + for tool_info in _plugin.get_tools(): + tools.append(type('ToolInfo', (), { + 'name': tool_info.name, + 'description': tool_info.description, + 'parameters': tool_info.to_schema(), + 'is_streaming': tool_info.tool_type == ToolType.STREAMING + })()) + return tools + + +def call_tool(name: str, args_json: str) -> str: + """ + Call a specific tool with arguments + This function will be called by the C++ wrapper + + Args: + name: Tool name + args_json: JSON string with tool arguments + + Returns: + JSON string with tool result + """ + return _plugin.call_tool(name, args_json) + + +# Convenience functions for creating parameters +def string_param(description: str = "", required: bool = False, default: str = None) -> ToolParameter: + """Create a string parameter""" + return ToolParameter(type="string", description=description, required=required, default=default) + + +def integer_param(description: str = "", required: bool = False, default: int = None) -> ToolParameter: + """Create an integer parameter""" + return ToolParameter(type="integer", description=description, required=required, default=default) + + +def number_param(description: str = "", required: bool = False, default: float = None) -> ToolParameter: + """Create a number parameter""" + return ToolParameter(type="number", description=description, required=required, default=default) + + +def boolean_param(description: str = "", required: bool = False, default: bool = None) -> ToolParameter: + """Create a boolean parameter""" + return ToolParameter(type="boolean", description=description, required=required, default=default) + + +def array_param(items_type: str = "string", description: str = "", required: bool = False) -> ToolParameter: + """Create an array parameter""" + return ToolParameter(type="array", description=description, required=required) + + +def object_param(description: str = "", required: bool = False) -> ToolParameter: + """Create an object parameter""" + return ToolParameter(type="object", description=description, required=required) \ No newline at end of file diff --git a/plugins/sdk/pybind_module.cpp b/plugins/sdk/pybind_module.cpp deleted file mode 100644 index 34c172a..0000000 --- a/plugins/sdk/pybind_module.cpp +++ /dev/null @@ -1,191 +0,0 @@ -/* -#include -#include -#include -#include -#include -#include -#include -#include -#include "mcp_plugin.h" -#include "../src/core/mcpserver_api.h" -namespace py = pybind11; -namespace fs = std::filesystem; - -// Global variables to hold the Python interpreter and module -static std::unique_ptr g_interpreter; -static py::module_ g_plugin_module; -static std::vector g_tools_cache; - -// Initialize Python and load the plugin module -static bool initialize_python_plugin(const std::string& plugin_dir) { - try { - // Initialize the Python interpreter if not already done - if (!g_interpreter) { - g_interpreter = std::make_unique(); - } - - // Add the plugin directory to Python path - py::module_ sys = py::module_::import("sys"); - sys.attr("path").attr("append")(plugin_dir); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to initialize Python: " << e.what() << std::endl; - return false; - } -} - -// Load the plugin module -static bool load_plugin_module(const std::string& module_name) { - try { - g_plugin_module = py::module_::import(module_name.c_str()); - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to load plugin module '" << module_name << "': " << e.what() << std::endl; - return false; - } -} - -extern "C" { - // Get tools function - MCP_API ToolInfo* get_tools(int* count) { - try { - // Clear the cache - for (auto& tool : g_tools_cache) { - // Clean up previous allocations - delete[] const_cast(tool.name); - delete[] const_cast(tool.description); - delete[] const_cast(tool.parameters); - } - g_tools_cache.clear(); - - // Call Python function - py::object get_tools_func = g_plugin_module.attr("get_tools"); - py::list tools_list = get_tools_func(); - - *count = static_cast(py::len(tools_list)); - g_tools_cache.reserve(*count); - - // Convert Python objects to ToolInfo structures - for (const auto& tool_obj : tools_list) { - ToolInfo tool{}; - - // Extract string fields and make copies - std::string name = py::str(tool_obj.attr("name")).cast(); - std::string description = py::str(tool_obj.attr("description")).cast(); - std::string parameters = py::str(tool_obj.attr("parameters")).cast(); - - // Allocate memory for strings (will be freed by the server or when cache is cleared) - tool.name = new char[name.length() + 1]; - tool.description = new char[description.length() + 1]; - tool.parameters = new char[parameters.length() + 1]; - - std::strcpy(const_cast(tool.name), name.c_str()); - std::strcpy(const_cast(tool.description), description.c_str()); - std::strcpy(const_cast(tool.parameters), parameters.c_str()); - - tool.is_streaming = tool_obj.attr("is_streaming").cast(); - - g_tools_cache.push_back(tool); - } - - return g_tools_cache.data(); - } catch (const std::exception& e) { - std::cerr << "Error in get_tools: " << e.what() << std::endl; - *count = 0; - return nullptr; - } - } - - // Call tool function - MCP_API const char* call_tool(const char* name, const char* args_json, MCPError* error) { - try { - py::object call_tool_func = g_plugin_module.attr("call_tool"); - py::object result = call_tool_func(std::string(name), std::string(args_json)); - std::string result_str = py::str(result).cast(); - - // Allocate memory for the result (will be freed by free_result) - char* result_cstr = new char[result_str.length() + 1]; - std::strcpy(result_cstr, result_str.c_str()); - - return result_cstr; - } catch (const std::exception& e) { - if (error) { - error->code = -1; - error->message = strdup(e.what()); - } - return nullptr; - } - } - - // Free result function - MCP_API void free_result(const char* result) { - if (result) { - delete[] result; - } - } - - // Initialize plugin function - this should be called by the plugin system - // before any other functions - MCP_API bool initialize_plugin() { - try { - // Get the directory of the current DLL - std::string plugin_dir = "."; // Default to current directory - - // Try to initialize Python environment - if (!initialize_python_plugin(plugin_dir)) { - std::cerr << "Failed to initialize Python environment" << std::endl; - return false; - } - - // Try to load the Python plugin module - if (!load_plugin_module("python_example_plugin")) { - std::cerr << "Failed to load Python plugin module" << std::endl; - return false; - } - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to initialize plugin: " << e.what() << std::endl; - return false; - } - } - - // Constructor function that is called when the DLL is loaded - #ifdef _WIN32 - #include - - // Use proper Windows API decoration - BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { - switch (ul_reason_for_call) { - case DLL_PROCESS_ATTACH: - // Initialize the plugin when the DLL is loaded - if (!initialize_plugin()) { - // If initialization fails, we could log an error or take other actions - std::cerr << "Plugin initialization failed during DLL_PROCESS_ATTACH" << std::endl; - // Note: Returning FALSE would prevent the DLL from loading - // return FALSE; - } - break; - case DLL_THREAD_ATTACH: - case DLL_THREAD_DETACH: - case DLL_PROCESS_DETACH: - break; - } - return TRUE; - } - #endif -} - -// PYBIND11_MODULE definition with proper export - allows the DLL to also be imported as a Python module -#define PYBIND11_EXPORTS // Ensure proper symbol exports -PYBIND11_MODULE(mcp_python_plugin, m) { - m.doc() = "MCPServer++ Python Plugin Support Module"; - - // This is just for testing that the module can be imported - m.def("test_initialization", []() { - return "Python plugin support module loaded"; - }); -} - */ \ No newline at end of file diff --git a/plugins/sdk/pybind_module_plugin.cpp b/plugins/sdk/pybind_module_plugin.cpp index 441f12a..94aecf7 100644 --- a/plugins/sdk/pybind_module_plugin.cpp +++ b/plugins/sdk/pybind_module_plugin.cpp @@ -1,212 +1,110 @@ +/* + * @Author: caomengxuan + * @Date: 2025-08-25 12:56:20 + * @note: We use this to load python plugins,but why not reuse it? + * Because if every plugin load the same python runtime,it will cause a problem. + * For example,if we have two plugins,one plugin use python3.9,another plugin use python3.10, + * in this case,we could not load two different python versions.So use seperate python runtimes for each plugin + * may be the best practice. + * + * @Last Modified by: caomengxuan + * @Last Modified time: 2025-08-25 12:59:17 + */ +#pragma once + +#include "../src/business/python_plugin_instance.h" +#include "../src/core/logger.h" +#include "../src/core/mcpserver_api.h" +#include "mcp_plugin.h" +#include #include -#include #include #include -#include -#include -#include -#include -#include "mcp_plugin.h" -#include "../src/core/mcpserver_api.h" +#include -#ifdef _WIN32 -#include -#endif namespace py = pybind11; namespace fs = std::filesystem; -// Global variables to hold the Python interpreter and module -static std::unique_ptr g_interpreter; -static py::module_ g_plugin_module; -static std::vector g_tools_cache; -static bool g_initialized = false; -// Initialize Python and load the plugin module -static bool initialize_python_plugin(const std::string& plugin_dir) { - try { - std::cerr << "[PLUGIN] Initializing Python interpreter in directory: " << plugin_dir << std::endl; - - // Initialize the Python interpreter if not already done - if (!g_interpreter) { - g_interpreter = std::make_unique(); - std::cerr << "[PLUGIN] Python interpreter initialized successfully" << std::endl; - } - - // Add the plugin directory to Python path - py::module_ sys = py::module_::import("sys"); - sys.attr("path").attr("append")(plugin_dir); - - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to initialize Python: " << e.what() << std::endl; - return false; - } -} +static std::unordered_map> g_plugin_instances; -// Load the plugin module -static bool load_plugin_module(const std::string& module_name) { - try { - std::cerr << "[PLUGIN] Loading module: " << module_name << std::endl; - g_plugin_module = py::module_::import(module_name.c_str()); - std::cerr << "[PLUGIN] Module loaded successfully" << std::endl; - return true; - } catch (const std::exception& e) { - std::cerr << "Failed to load plugin module '" << module_name << "': " << e.what() << std::endl; - return false; - } -} extern "C" { - // Get tools function - MCP_API ToolInfo* get_tools(int* count) { - if (!g_initialized) { - std::cerr << "[PLUGIN] Error: Plugin not initialized before calling get_tools" << std::endl; - *count = 0; - return nullptr; - } - - try { - // Clear the cache - for (auto& tool : g_tools_cache) { - // Clean up previous allocations - delete[] const_cast(tool.name); - delete[] const_cast(tool.description); - delete[] const_cast(tool.parameters); - } - g_tools_cache.clear(); - - // Call Python function - py::object get_tools_func = g_plugin_module.attr("get_tools"); - py::list tools_list = get_tools_func(); - - *count = static_cast(py::len(tools_list)); - g_tools_cache.reserve(*count); - - // Convert Python objects to ToolInfo structures - for (const auto& tool_obj : tools_list) { - ToolInfo tool{}; - - // Extract string fields and make copies - std::string name = py::str(tool_obj.attr("name")).cast(); - std::string description = py::str(tool_obj.attr("description")).cast(); - std::string parameters = py::str(tool_obj.attr("parameters")).cast(); - - // Allocate memory for strings (will be freed by the server or when cache is cleared) - tool.name = new char[name.length() + 1]; - tool.description = new char[description.length() + 1]; - tool.parameters = new char[parameters.length() + 1]; - - std::strcpy(const_cast(tool.name), name.c_str()); - std::strcpy(const_cast(tool.description), description.c_str()); - std::strcpy(const_cast(tool.parameters), parameters.c_str()); - - tool.is_streaming = tool_obj.attr("is_streaming").cast(); - - g_tools_cache.push_back(tool); - } - - return g_tools_cache.data(); - } catch (const std::exception& e) { - std::cerr << "Error in get_tools: " << e.what() << std::endl; - *count = 0; - return nullptr; - } +// Get tools function +//!Notice that we only use the first tool to make sure that every plugin is seperated. +MCP_API ToolInfo *get_tools(int *count) { + if (g_plugin_instances.empty()) { + MCP_ERROR("[PLUGIN] No plugin instances available"); + *count = 0; + return nullptr; } - - // Call tool function - MCP_API const char* call_tool(const char* name, const char* args_json, MCPError* error) { - if (!g_initialized) { - std::cerr << "[PLUGIN] Error: Plugin not initialized before calling call_tool" << std::endl; - if (error) { - error->code = -1; - std::string error_msg = "Plugin not initialized"; - error->message = new char[error_msg.length() + 1]; - std::strcpy(const_cast(error->message), error_msg.c_str()); - } - return nullptr; - } - - try { - std::cerr << "[PLUGIN] Calling tool: " << name << std::endl; - py::object call_tool_func = g_plugin_module.attr("call_tool"); - py::object result = call_tool_func(py::str(name), py::str(args_json)); - - // Use Python's built-in string conversion to ensure proper encoding - py::str result_str_obj = py::str(result); - std::string result_str = result_str_obj.cast(); - - // Allocate memory for the result (will be freed by free_result) - char* result_cstr = new char[result_str.length() + 1]; - std::strcpy(result_cstr, result_str.c_str()); - - return result_cstr; - } catch (const std::exception& e) { - if (error) { - error->code = -1; - // Make a copy of the error message - std::string error_msg = e.what(); - error->message = new char[error_msg.length() + 1]; - std::strcpy(const_cast(error->message), error_msg.c_str()); - } - return nullptr; + + PythonPluginInstance *instance = g_plugin_instances.begin()->second.get(); + return instance->get_tools(count); +} + +// Call tool function +//!Notice that we only use the first tool to make sure that every plugin is seperated. +MCP_API const char *call_tool(const char *name, const char *args_json, MCPError *error) { + if (g_plugin_instances.empty()) { + MCP_ERROR("[PLUGIN] No plugin instances available"); + if (error) { + error->code = -1; + error->message = strdup("No plugin instances available"); } + return nullptr; } - - // Free result function - MCP_API void free_result(const char* result) { - delete[] result; - } - - // Free error function - MCP_API void free_error(MCPError* error) { - if (error && error->message) { - delete[] error->message; + + PythonPluginInstance *instance = g_plugin_instances.begin()->second.get(); + return instance->call_tool(name, args_json, error); +} + +// Free result function +MCP_API void free_result(const char *result) { + free((void *) result); +} + +// Free error function +MCP_API void free_error(MCPError *error) { + if (error) { + if (error->message) { + free((void *) error->message); + error->message = nullptr; } + error->code = 0; } - - // Initialize plugin function - MCP_API bool initialize_plugin(const char* plugin_path) { - try { - std::string plugin_path_str(plugin_path); - fs::path path(plugin_path_str); - - // Extract directory and module name - std::string plugin_dir = path.parent_path().string(); - std::string module_name = path.stem().string(); - - std::cerr << "[PLUGIN] initialize_plugin called with path: " << plugin_path_str << std::endl; - std::cerr << "[PLUGIN] plugin_dir: " << plugin_dir << ", module_name: " << module_name << std::endl; - - // Initialize Python - if (!initialize_python_plugin(plugin_dir)) { - std::cerr << "[PLUGIN] Failed to initialize Python plugin" << std::endl; - return false; - } - - // Load the plugin module - if (!load_plugin_module(module_name)) { - std::cerr << "[PLUGIN] Failed to load plugin module" << std::endl; - return false; - } - - g_initialized = true; - std::cerr << "[PLUGIN] Plugin initialized successfully" << std::endl; - return true; - } catch (const std::exception& e) { - std::cerr << "Error initializing plugin: " << e.what() << std::endl; +} + +// Initialize plugin function +MCP_API bool initialize_plugin(const char *plugin_path) { + MCP_DEBUG("[PLUGIN] initialize_plugin called with path: {}", plugin_path); + + try { + std::string plugin_path_str(plugin_path); + + auto instance = std::make_unique(); + if (!instance->initialize(plugin_path)) { + MCP_ERROR("[PLUGIN] Failed to initialize plugin instance"); return false; } + + g_plugin_instances[plugin_path_str] = std::move(instance); + MCP_INFO("[PLUGIN] Plugin instance created and stored successfully"); + return true; + } catch (const std::exception &e) { + MCP_ERROR("[PLUGIN] Error creating plugin instance: {}", e.what()); + return false; } } +MCP_API void uninitialize_plugin(const char *plugin_path) { + MCP_DEBUG("[PLUGIN] uninitialize_plugin called with path: {}", plugin_path); -// PYBIND11_MODULE definition with proper export - allows the DLL to also be imported as a Python module -#define PYBIND11_EXPORTS // Ensure proper symbol exports -PYBIND11_MODULE(mcp_python_plugin, m) { - m.doc() = "MCPServer++ Python Plugin Support Module"; - - // This is just for testing that the module can be imported - m.def("test_initialization", []() { - return "Python plugin support module loaded"; - }); + auto it = g_plugin_instances.find(std::string(plugin_path)); + if (it != g_plugin_instances.end()) { + it->second->uninitialize(); + g_plugin_instances.erase(it); + MCP_INFO("[PLUGIN] Plugin instance removed successfully"); + } +} } \ No newline at end of file diff --git a/plugins/sdk/python_plugin_CMakeLists.txt b/plugins/sdk/python_plugin_CMakeLists.txt deleted file mode 100644 index 57edee5..0000000 --- a/plugins/sdk/python_plugin_CMakeLists.txt +++ /dev/null @@ -1,50 +0,0 @@ -# CMakeLists.txt template for building Python plugins as DLLs for MCPServer++ -# Copy this file to your plugin directory and modify as needed - -cmake_minimum_required(VERSION 3.23) -project(PYTHON_PLUGIN_NAME) - -# Set C++ standard -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Set the path to MCPServer++ root directory -set(MCP_SERVER_ROOT "path/to/mcpserver++" CACHE STRING "Path to MCPServer++ root directory") - -# Find required packages -find_package(Python COMPONENTS Interpreter Development REQUIRED) -# Add the plugin library -add_library(${PROJECT_NAME} SHARED - # Add your plugin source files here - # ${MCP_SERVER_ROOT}/plugins/sdk/pybind_module.cpp -) - -# Include directories -target_include_directories(${PROJECT_NAME} PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR} - ${pybind11_INCLUDE_DIRS} - # MCPServer++ include directories - ${MCP_SERVER_ROOT}/plugins/sdk - ${MCP_SERVER_ROOT}/include - ${MCP_SERVER_ROOT}/third_party/nlohmann -) - -# Link libraries -target_link_libraries(${PROJECT_NAME} PRIVATE - pybind11::embed -) - -# Platform-specific settings -if(WIN32) - # Windows-specific settings - target_compile_definitions(${PROJECT_NAME} PRIVATE MCP_API=__declspec(dllexport)) -else() - # Unix-specific settings - target_compile_definitions(${PROJECT_NAME} PRIVATE MCP_API=) -endif() - -# Example usage: -# mkdir build -# cd build -# cmake .. -DMCP_SERVER_ROOT=/path/to/mcpserver++ -# cmake --build . --config Release \ No newline at end of file diff --git a/src/business/CMakeLists.txt b/src/business/CMakeLists.txt index 3b2c3cf..efdd2c7 100644 --- a/src/business/CMakeLists.txt +++ b/src/business/CMakeLists.txt @@ -1,5 +1,5 @@ # src/business/CMakeLists.txt -file(GLOB BUSINESS_SOURCES *.cpp) +file(GLOB BUSINESS_SOURCES *.cpp *.h) add_library(mcp_business ${BUSINESS_SOURCES}) @@ -7,6 +7,7 @@ target_include_directories(mcp_business PUBLIC $ $ $ + $ $ ) @@ -24,6 +25,9 @@ target_link_libraries(mcp_business PUBLIC mcp_transport ) +include(${CMAKE_SOURCE_DIR}/cmake/EnablePython.cmake) +target_enable_python(mcp_transport) + # Conditionally install the library and headers if(CPACK_INCLUDE_LIBS) install(TARGETS mcp_business diff --git a/src/business/plugin_manager.cpp b/src/business/plugin_manager.cpp index 24b6b70..7daa787 100644 --- a/src/business/plugin_manager.cpp +++ b/src/business/plugin_manager.cpp @@ -42,7 +42,13 @@ namespace mcp::business { auto get_tools = (get_tools_func) GET_FUNC(handle, "get_tools"); auto call_tool = (call_tool_func) GET_FUNC(handle, "call_tool"); auto free_result = (free_result_func) GET_FUNC(handle, "free_result"); - + auto initialize_plugin = (initialize_plugin_func) GET_FUNC(handle, "initialize_plugin"); + auto uninitialize_plugin = (uninitialize_plugin_func) GET_FUNC(handle, "uninitialize_plugin"); + if (!initialize_plugin) { + MCP_TRACE("Plugin is not a python plugin: {}", plugin_file_path); + } else { + MCP_TRACE("Plugin is a python plugin: {}", plugin_file_path); + } // Load stream functions if available auto get_stream_next_loader = (get_stream_next_func) GET_FUNC(handle, "get_stream_next"); auto get_stream_free_loader = (get_stream_free_func) GET_FUNC(handle, "get_stream_free"); @@ -55,6 +61,21 @@ namespace mcp::business { return false; } + // Initialize plugin if it has initialize_plugin function + if (initialize_plugin) { + MCP_DEBUG("Initializing plugin: {}", plugin_file_path); + MCP_DEBUG("initialize_plugin function pointer: {}", (void*)initialize_plugin); + bool init_result = initialize_plugin(plugin_file_path.c_str()); + MCP_DEBUG("initialize_plugin returned: {}", init_result); + if (!init_result) { + MCP_ERROR("Failed to initialize plugin: {}", plugin_file_path); + CLOSE_LIB(handle); + return false; + } + } else { + MCP_DEBUG("Plugin does not have initialize_plugin function: {}", plugin_file_path); + } + // Loading tools from the plugin int tool_count = 0; ToolInfo *tool_infos = get_tools(&tool_count); @@ -62,12 +83,14 @@ namespace mcp::business { MCP_WARN("Plugin has no tools: {}", plugin_file_path); } - // Store the plugins + // Create plugin object auto plugin = std::make_unique(); plugin->handle = handle; plugin->get_tools = get_tools; plugin->call_tool = call_tool; plugin->free_result = free_result; + plugin->initialize_plugin = initialize_plugin; + plugin->uninitialize_plugin = uninitialize_plugin; plugin->get_stream_next = get_stream_next_loader; plugin->get_stream_free = get_stream_free_loader; for (int i = 0; i < tool_count; ++i) { @@ -411,6 +434,11 @@ namespace mcp::business { lib_handle handle = it->second->handle; + if (it->second->uninitialize_plugin) { + MCP_DEBUG("Uninitializing plugin: {}", plugin_name); + it->second->uninitialize_plugin(plugin_name.c_str()); + } + if (handle) { CLOSE_LIB(handle); MCP_DEBUG("Closed library handle for plugin: {}", plugin_name); diff --git a/src/business/plugin_manager.h b/src/business/plugin_manager.h index f8c1fd9..240469b 100644 --- a/src/business/plugin_manager.h +++ b/src/business/plugin_manager.h @@ -39,6 +39,8 @@ namespace mcp::business { get_tools_func get_tools; call_tool_func call_tool; free_result_func free_result; + initialize_plugin_func initialize_plugin; + uninitialize_plugin_func uninitialize_plugin; // 新增 uninitialize_plugin 函数指针 std::vector tool_list; get_stream_next_func get_stream_next; get_stream_free_func get_stream_free; diff --git a/src/business/python_plugin_instance.cpp b/src/business/python_plugin_instance.cpp new file mode 100644 index 0000000..3fa7c45 --- /dev/null +++ b/src/business/python_plugin_instance.cpp @@ -0,0 +1,251 @@ +#include "python_plugin_instance.h" +#include "python_runtime_manager.h" +#include +#include +#include +#include "core/logger.h" + +namespace fs = std::filesystem; + +PythonPluginInstance::PythonPluginInstance() : initialized_(false) { +} + +PythonPluginInstance::~PythonPluginInstance() { + uninitialize(); +} + +bool PythonPluginInstance::initialize(const char* plugin_path) { + MCP_DEBUG("[PLUGIN] PythonPluginInstance::initialize called with path: {}", plugin_path); + + try { + std::string plugin_path_str(plugin_path); + fs::path path(plugin_path_str); + + // Extract directory and module name + plugin_dir_ = path.parent_path().string(); + module_name_ = path.stem().string(); + + MCP_DEBUG("[PLUGIN] plugin_dir: {}, module_name: {}", plugin_dir_, module_name_); + + // Check if Python file exists + fs::path python_file_path = path.parent_path() / (module_name_ + ".py"); + if (!fs::exists(python_file_path)) { + MCP_ERROR("[PLUGIN] Python file does not exist: {}", python_file_path.string()); + return false; + } + + MCP_DEBUG("[PLUGIN] Python file found: {}", python_file_path.string()); + + // Get the Python runtime manager instance + auto& runtime_manager = PythonRuntimeManager::getInstance(); + + // Initialize Python runtime if not already done + if (!runtime_manager.isInitialized()) { + if (!runtime_manager.initialize(plugin_dir_)) { + MCP_ERROR("[PLUGIN] Failed to initialize Python runtime"); + return false; + } + } + + // Load the plugin module + if (!initialize_plugin_module()) { + MCP_ERROR("[PLUGIN] Failed to load plugin module"); + return false; + } + + initialized_ = true; + MCP_INFO("[PLUGIN] Plugin instance initialized successfully"); + return true; + } catch (const std::exception& e) { + MCP_ERROR("[PLUGIN] Error initializing plugin: {}", e.what()); + return false; + } +} + +void PythonPluginInstance::uninitialize() { + std::lock_guard lock(cache_mutex_); + + for (auto& tool : tools_cache_) { + free((void*)tool.name); + free((void*)tool.description); + free((void*)tool.parameters); + } + tools_cache_.clear(); + + plugin_module_ = py::module_(); + + initialized_ = false; + MCP_DEBUG("[PLUGIN] Plugin instance uninitialized"); +} + +bool PythonPluginInstance::initialize_plugin_module() { + try { + MCP_DEBUG("[PLUGIN] Initializing plugin module: {}", module_name_); + + // Critical fix: Must acquire GIL before importing module (main thread GIL has been released) + MCP_DEBUG("[PLUGIN] initialize_plugin_module: acquiring GIL for import..."); + py::gil_scoped_acquire acquire; // Explicitly acquire GIL + MCP_DEBUG("[PLUGIN] initialize_plugin_module: GIL acquired"); + + // Get the Python runtime manager instance + auto& runtime_manager = PythonRuntimeManager::getInstance(); + + // Import the plugin module (now safe to call Python API under GIL protection) + MCP_DEBUG("[PLUGIN] Trying to import Python module: {}", module_name_); + plugin_module_ = runtime_manager.importModule(module_name_); + MCP_DEBUG("[PLUGIN] Python module imported successfully: {}", module_name_); + + // Validate module has required functions + if (!py::hasattr(plugin_module_, "get_tools") || + !py::hasattr(plugin_module_, "call_tool")) { + MCP_ERROR("[PLUGIN] Module missing required functions"); + return false; + } + + MCP_INFO("[PLUGIN] Plugin module loaded successfully"); + return true; + } catch (const py::error_already_set& e) { + // Print detailed Python exception (including stack trace) to help locate import failure + MCP_ERROR("[PLUGIN] Python error loading module '{}': {}", module_name_, e.what()); + MCP_ERROR("[PLUGIN] Python traceback: {}", e.trace()); + return false; + } catch (const std::exception& e) { + MCP_ERROR("[PLUGIN] C++ error loading module '{}': {}", module_name_, e.what()); + return false; + } +} +ToolInfo* PythonPluginInstance::get_tools(int* count) { + MCP_DEBUG("[PLUGIN] get_tools called, initialized={}", (initialized_ ? "true" : "false")); + + if (!initialized_) { + MCP_ERROR("[PLUGIN] Error: Plugin not initialized before calling get_tools"); + *count = 0; + return nullptr; + } + + try { + py::gil_scoped_acquire acquire; + + std::lock_guard lock(cache_mutex_); + + // Clear the cache + for (auto& tool : tools_cache_) { + free((void*)tool.name); + free((void*)tool.description); + free((void*)tool.parameters); + } + tools_cache_.clear(); + + // Call Python function + py::object get_tools_func = plugin_module_.attr("get_tools"); + py::list tools_list = get_tools_func(); + + *count = static_cast(py::len(tools_list)); + tools_cache_.reserve(*count); + + // Convert Python objects to ToolInfo structures + for (const auto& tool_obj : tools_list) { + ToolInfo tool{}; + + // Extract string fields and make copies + std::string name = py::str(tool_obj.attr("name")).cast(); + std::string description = py::str(tool_obj.attr("description")).cast(); + std::string parameters = py::str(tool_obj.attr("parameters")).cast(); + + // Allocate memory for strings + tool.name = strdup(name.c_str()); + tool.description = strdup(description.c_str()); + tool.parameters = strdup(parameters.c_str()); + + tool.is_streaming = tool_obj.attr("is_streaming").cast(); + + tools_cache_.push_back(tool); + } + + return tools_cache_.data(); + } catch (const py::error_already_set& e) { + MCP_ERROR("[PLUGIN] Python error in get_tools: {}", e.what()); + *count = 0; + return nullptr; + } catch (const std::exception& e) { + MCP_ERROR("[PLUGIN] C++ error in get_tools: {}", e.what()); + *count = 0; + return nullptr; + } catch (...) { + MCP_ERROR("[PLUGIN] Unknown error in get_tools"); + *count = 0; + return nullptr; + } +} + +const char* PythonPluginInstance::call_tool(const char* name, const char* args_json, MCPError* error) { + MCP_DEBUG("[PLUGIN] call_tool called, initialized={}", (initialized_ ? "true" : "false")); + + // 1. First check basic parameter validity (avoid hidden crashes caused by null pointers) + if (!name || strlen(name) == 0) { + MCP_ERROR("[PLUGIN] call_tool: ERROR - tool name is null/empty"); + if (error) { error->code = -1; error->message = strdup("Invalid tool name"); } + return nullptr; + } + const char* actual_args = args_json ? args_json : "{}"; // Fallback to empty JSON + MCP_DEBUG("[PLUGIN] call_tool: tool name={}, args_json={}", name, actual_args); + + if (!initialized_) { + MCP_ERROR("[PLUGIN] call_tool: ERROR - plugin not initialized"); + if (error) { error->code = -1; error->message = strdup("Plugin not initialized"); } + return nullptr; + } + + // 2. Check if Python module is valid (exclude module not loaded) + if (plugin_module_.is_none()) { + MCP_ERROR("[PLUGIN] call_tool: ERROR - plugin_module_ is empty"); + if (error) { error->code = -1; error->message = strdup("Plugin module not loaded"); } + return nullptr; + } + MCP_DEBUG("[PLUGIN] call_tool: plugin_module_ is valid"); + + try { + // 3. Key: Acquire GIL (most likely blocking point) + MCP_DEBUG("[PLUGIN] call_tool: trying to acquire GIL..."); + py::gil_scoped_acquire acquire; // Acquire GIL, will block on failure + MCP_DEBUG("[PLUGIN] call_tool: GIL acquired successfully"); + + // 4. Find the call_tool function in Python layer (exclude function not exported) + MCP_DEBUG("[PLUGIN] call_tool: looking for 'call_tool' in Python module"); + if (!py::hasattr(plugin_module_, "call_tool")) { + MCP_ERROR("[PLUGIN] call_tool: ERROR - Python module has no 'call_tool' function"); + if (error) { error->code = -1; error->message = strdup("Python call_tool not found"); } + return nullptr; + } + py::object call_tool_func = plugin_module_.attr("call_tool"); + MCP_DEBUG("[PLUGIN] call_tool: got Python call_tool function"); + + // 5. Call Python function (if we reach this step, previous steps have no issues) + MCP_DEBUG("[PLUGIN] call_tool: calling Python call_tool (name={})", name); + py::object result = call_tool_func(py::str(name), py::str(actual_args)); + MCP_DEBUG("[PLUGIN] call_tool: Python call_tool returned"); + + // 6. Process return result + std::string result_str = py::str(result).cast(); + MCP_DEBUG("[PLUGIN] call_tool: Python result={}", result_str); + const char* ret = strdup(result_str.c_str()); + return ret; + + } catch (const py::error_already_set& e) { + // Catch Python exceptions (e.g. JSON parsing errors, tool not found) + MCP_ERROR("[PLUGIN] call_tool: PYTHON ERROR - {}", e.what()); + MCP_ERROR("[PLUGIN] call_tool: PYTHON TRACEBACK - {}", e.trace()); + if (error) { error->code = -1; error->message = strdup(e.what()); } + return nullptr; + } catch (const std::exception& e) { + // Catch C++ exceptions (e.g. memory allocation failure) + MCP_ERROR("[PLUGIN] call_tool: C++ ERROR - {}: {}", typeid(e).name(), e.what()); + if (error) { error->code = -1; error->message = strdup(e.what()); } + return nullptr; + } catch (...) { + // Catch unknown exceptions + MCP_ERROR("[PLUGIN] call_tool: UNKNOWN ERROR"); + if (error) { error->code = -1; error->message = strdup("Unknown error"); } + return nullptr; + } +} \ No newline at end of file diff --git a/src/business/python_plugin_instance.h b/src/business/python_plugin_instance.h new file mode 100644 index 0000000..d37a215 --- /dev/null +++ b/src/business/python_plugin_instance.h @@ -0,0 +1,35 @@ +#ifndef PYTHON_PLUGIN_INSTANCE_H +#define PYTHON_PLUGIN_INSTANCE_H + +#include +#include +#include +#include +#include +#include "mcp_plugin.h" + +namespace py = pybind11; + +class PythonPluginInstance { +public: + PythonPluginInstance(); + ~PythonPluginInstance(); + + bool initialize(const char* plugin_path); + void uninitialize(); + + ToolInfo* get_tools(int* count); + const char* call_tool(const char* name, const char* args_json, MCPError* error); + +private: + bool initialize_plugin_module(); + + py::module_ plugin_module_; + std::string plugin_dir_; + std::string module_name_; + std::vector tools_cache_; + bool initialized_; + std::mutex cache_mutex_; +}; + +#endif // PYTHON_PLUGIN_INSTANCE_H \ No newline at end of file diff --git a/src/business/python_runtime_manager.cpp b/src/business/python_runtime_manager.cpp new file mode 100644 index 0000000..7db1663 --- /dev/null +++ b/src/business/python_runtime_manager.cpp @@ -0,0 +1,117 @@ +#include "python_runtime_manager.h" +#include +#include "core/logger.h" + +PythonRuntimeManager& PythonRuntimeManager::getInstance() { + static PythonRuntimeManager instance; + return instance; +} + +PythonRuntimeManager::PythonRuntimeManager() : initialized_(false) { +} + + +PythonRuntimeManager::~PythonRuntimeManager() { + std::lock_guard lock(runtime_mutex_); + if (!initialized_) return; + + MCP_DEBUG("[PYTHON] Finalizing Python runtime (thread: {})", std::this_thread::get_id()); + + // 1. Restore main thread state (must be done before Py_Finalize()) + if (main_thread_state_ != nullptr) { + PyEval_RestoreThread(main_thread_state_); // Restore main thread GIL holding state + main_thread_state_ = nullptr; + MCP_DEBUG("[PYTHON] Main thread state restored"); + } + + // 2. Destroy Python interpreter + if (Py_IsInitialized()) { + Py_Finalize(); + MCP_DEBUG("[PYTHON] Py_Finalize() called"); + } + + initialized_ = false; + MCP_DEBUG("[PYTHON] Runtime finalized"); +} + +bool PythonRuntimeManager::initialize(const std::string& plugin_dir) { + std::lock_guard lock(runtime_mutex_); + if (initialized_) { + MCP_DEBUG("[PYTHON] Runtime already initialized (thread: {})", std::this_thread::get_id()); + return true; + } + + try { + MCP_DEBUG("[PYTHON] Initializing Python interpreter (thread: {})", std::this_thread::get_id()); + + // 1. Initialize Python interpreter (void type, no need to check return value) + Py_Initialize(); + MCP_DEBUG("[PYTHON] Py_Initialize() called (interpreter initialized)"); + + // 2. Enable multi-threading support (Python 3.9+ compatible, alternative to PyEval_InitThreads()) + PyGILState_STATE gil_state = PyGILState_Ensure(); + PyGILState_Release(gil_state); + MCP_DEBUG("[PYTHON] Multi-thread support enabled (via PyGILState_Ensure)"); + + // 3. Release main thread GIL and save thread state (child threads can acquire GIL) + main_thread_state_ = PyEval_SaveThread(); + if (main_thread_state_ == nullptr) { + MCP_WARN("[PYTHON] Warning: PyEval_SaveThread() returned null"); + } else { + MCP_DEBUG("[PYTHON] Main thread GIL released (thread: {})", std::this_thread::get_id()); + } + + // 4. Re-acquire GIL and add plugin directory to sys.path + py::gil_scoped_acquire acquire; + py::module_ sys = py::module_::import("sys"); + sys.attr("path").attr("append")(plugin_dir); + MCP_DEBUG("[PYTHON] Added plugin dir to sys.path: {}", plugin_dir); + + // Fix: Convert py::str to std::string before output + std::string sys_path_str = py::str(sys.attr("path")).cast(); + MCP_DEBUG("[PYTHON] sys.path: {}", sys_path_str); + + initialized_ = true; + MCP_INFO("[PYTHON] Runtime initialized successfully"); + return true; + } catch (const py::error_already_set& e) { + MCP_ERROR("[PYTHON] Init Python error: {}", e.what()); + //MCP_ERROR("[PYTHON] Traceback: {}", e.trace()); + if (Py_IsInitialized()) { + Py_Finalize(); // Only call when already initialized + } + return false; + } catch (const std::exception& e) { + MCP_ERROR("[PYTHON] Init C++ error: {}", e.what()); + if (Py_IsInitialized()) { + Py_Finalize(); + } + return false; + } +} + +bool PythonRuntimeManager::isInitialized() const { + std::lock_guard lock(runtime_mutex_); + return initialized_; +} + +py::module_ PythonRuntimeManager::importModule(const std::string& module_name) { + std::lock_guard lock(runtime_mutex_); + + if (!initialized_) { + throw std::runtime_error("Python runtime not initialized"); + } + + return py::module_::import(module_name.c_str()); +} + +void PythonRuntimeManager::addPath(const std::string& path) { + std::lock_guard lock(runtime_mutex_); + + if (!initialized_) { + throw std::runtime_error("Python runtime not initialized"); + } + + py::module_ sys = py::module_::import("sys"); + sys.attr("path").attr("append")(path); +} \ No newline at end of file diff --git a/src/business/python_runtime_manager.h b/src/business/python_runtime_manager.h new file mode 100644 index 0000000..39a55f0 --- /dev/null +++ b/src/business/python_runtime_manager.h @@ -0,0 +1,32 @@ +#ifndef PYTHON_RUNTIME_MANAGER_H +#define PYTHON_RUNTIME_MANAGER_H + +#include +#include +#include +#include +#include +#include + +namespace py = pybind11; + +class PythonRuntimeManager { +public: + static PythonRuntimeManager& getInstance(); + + bool initialize(const std::string& plugin_dir); + bool isInitialized() const; + py::module_ importModule(const std::string& module_name); + void addPath(const std::string& path); + +private: + PythonRuntimeManager(); + ~PythonRuntimeManager(); + + // Save the state of the main thread's GIL + PyThreadState* main_thread_state_ = nullptr; + bool initialized_ = false; + mutable std::mutex runtime_mutex_; +}; + +#endif // PYTHON_RUNTIME_MANAGER_H \ No newline at end of file diff --git a/tools/plugin_ctl.cpp b/tools/plugin_ctl.cpp index 6256131..9d4842d 100644 --- a/tools/plugin_ctl.cpp +++ b/tools/plugin_ctl.cpp @@ -227,56 +227,27 @@ namespace mcp { if (!fs::exists(plugin_file)) { std::ofstream ofs(plugin_file); ofs << "# Plugin: " << plugin_id << "\n"; - ofs << "# This is a template for your Python plugin implementation.\n\n"; - ofs << "import json\n\n"; - ofs << "def get_tools():\n"; - ofs << " \"\"\"Return a list of tools provided by this plugin\"\"\"\n"; - ofs << " return [\n"; - ofs << " {\n"; - ofs << " \"name\": \"" << plugin_id << "\",\n"; - ofs << " \"description\": \"Description of " << plugin_id << "\",\n"; - ofs << " \"parameters\": json.dumps({\n"; - ofs << " \"type\": \"object\",\n"; - ofs << " \"properties\": {\n"; - ofs << " \"param1\": {\n"; - ofs << " \"type\": \"string\",\n"; - ofs << " \"description\": \"An example parameter\"\n"; - ofs << " }\n"; - ofs << " },\n"; - ofs << " \"required\": []\n"; - ofs << " })\n"; - ofs << " },\n"; - ofs << " {\n"; - ofs << " \"name\": \"stream_" << plugin_id << "\",\n"; - ofs << " \"description\": \"Stream data from " << plugin_id << "\",\n"; - ofs << " \"parameters\": json.dumps({\n"; - ofs << " \"type\": \"object\",\n"; - ofs << " \"properties\": {\n"; - ofs << " \"param1\": {\n"; - ofs << " \"type\": \"string\",\n"; - ofs << " \"description\": \"An example parameter\"\n"; - ofs << " }\n"; - ofs << " },\n"; - ofs << " \"required\": []\n"; - ofs << " }),\n"; - ofs << " \"is_streaming\": True\n"; - ofs << " }\n"; - ofs << " ]\n\n"; - ofs << "def call_tool(name, args_json):\n"; - ofs << " \"\"\"Call a specific tool with given arguments\"\"\"\n"; - ofs << " args = json.loads(args_json)\n"; - ofs << " \n"; - ofs << " if name == \"" << plugin_id << "\":\n"; - ofs << " # Example implementation - replace with your actual logic\n"; - ofs << " return json.dumps({\"result\": \"Hello from " << plugin_id << "\"})\n"; - ofs << " \n"; - ofs << " raise ValueError(f\"Unknown tool: {name}\")\n\n"; - ofs << "# For streaming tools, you can implement a generator function\n"; - ofs << "def stream_" << plugin_id << "(args_json):\n"; - ofs << " \"\"\"Stream data from the plugin\"\"\"\n"; - ofs << " args = json.loads(args_json)\n"; - ofs << " # Example streaming implementation\n"; - ofs << " for i in range(5):\n"; + ofs << "# This is a template for your Python plugin implementation using the new MCP SDK.\n\n"; + ofs << "from mcp_sdk import tool, string_param, get_tools, call_tool\n\n"; + ofs << "# Example of a standard tool\n"; + ofs << "@tool(\n"; + ofs << " name=\"" << plugin_id << "\",\n"; + ofs << " description=\"Description of " << plugin_id << "\",\n"; + ofs << " param1=string_param(description=\"An example parameter\")\n"; + ofs << ")\n"; + ofs << "def " << plugin_id << "_tool(param1: str = \"default_value\"):\n"; + ofs << " \"\"\"Example tool implementation\"\"\"\n"; + ofs << " return f\"Hello from " << plugin_id << "! Parameter value: {param1}\"\n\n"; + ofs << "# Example of a streaming tool\n"; + ofs << "@tool(\n"; + ofs << " name=\"stream_" << plugin_id << "\",\n"; + ofs << " description=\"Stream data from " << plugin_id << "\",\n"; + ofs << " tool_type=\"streaming\",\n"; + ofs << " count=string_param(description=\"Number of items to stream\", required=False, default=\"5\")\n"; + ofs << ")\n"; + ofs << "def stream_" << plugin_id << "_tool(count: int = 5):\n"; + ofs << " \"\"\"Example streaming tool implementation\"\"\"\n"; + ofs << " for i in range(int(count)):\n"; ofs << " yield {\"text\": f\"Streamed data item {i}\"}\n"; } @@ -309,10 +280,13 @@ namespace mcp { ofs << "target_link_libraries(${PROJECT_NAME} PRIVATE \n"; ofs << " pybind11::embed\n"; ofs << ")\n\n"; - ofs << "# Ensure the Python plugin file is available\n"; + ofs << "# Ensure the Python plugin file and SDK are available\n"; ofs << "configure_file(${CMAKE_CURRENT_SOURCE_DIR}/" << plugin_id << ".py \n"; ofs << " ${CMAKE_CURRENT_BINARY_DIR}/" << plugin_id << ".py \n"; ofs << " COPYONLY)\n"; + ofs << "configure_file(${MCP_SERVER_ROOT}/plugins/sdk/mcp_sdk.py \n"; + ofs << " ${CMAKE_CURRENT_BINARY_DIR}/mcp_sdk.py \n"; + ofs << " COPYONLY)\n"; } // Create tools.json file @@ -341,9 +315,9 @@ namespace mcp { ofs << " \"parameters\": {\n"; ofs << " \"type\": \"object\",\n"; ofs << " \"properties\": {\n"; - ofs << " \"param1\": {\n"; + ofs << " \"count\": {\n"; ofs << " \"type\": \"string\",\n"; - ofs << " \"description\": \"An example parameter\"\n"; + ofs << " \"description\": \"Number of items to stream\"\n"; ofs << " }\n"; ofs << " },\n"; ofs << " \"required\": []\n"; From 8f58eb2b64a35af2cdcb1c3a92665471b828f9ef Mon Sep 17 00:00:00 2001 From: caomengxuan666 <2507560089@qq.com> Date: Mon, 25 Aug 2025 13:19:04 +0800 Subject: [PATCH 2/2] feat(core): Optimize code structure and add graceful shutdown functionality - Reorganized header file order and optimized code structure - Removed unnecessary exception handling and log records - Added signal handling mechanism to implement server graceful shutdown - Optimized the string conversion method for thread IDs --- src/Auth/AuthManager.hpp | 36 ++++++++++++++----------- src/business/python_plugin_instance.cpp | 9 +++---- src/business/python_runtime_manager.cpp | 17 +++++++++--- src/main.cpp | 28 +++++++++++++++++++ src/transport/ssl_session.cpp | 2 +- 5 files changed, 66 insertions(+), 26 deletions(-) diff --git a/src/Auth/AuthManager.hpp b/src/Auth/AuthManager.hpp index 61dc958..58d9bea 100644 --- a/src/Auth/AuthManager.hpp +++ b/src/Auth/AuthManager.hpp @@ -1,12 +1,13 @@ #pragma once -#include -#include -#include #include -#include #include +#include #include +#include +#include +#include + // ============================== // Abstract Auth Manager @@ -15,7 +16,7 @@ class AuthManagerBase { public: virtual ~AuthManagerBase() = default; - virtual bool validate(const std::unordered_map& headers) const = 0; + virtual bool validate(const std::unordered_map &headers) const = 0; // Get the type of the auth manager virtual std::string type() const = 0; }; @@ -29,14 +30,14 @@ class AuthManagerXApi final : public AuthManagerBase { public: explicit AuthManagerXApi(std::vector api_keys) { - for (auto& key : api_keys) { + for (auto &key: api_keys) { if (!key.empty()) { valid_keys_.insert(std::move(key)); } } } - bool validate(const std::unordered_map& headers) const override { + bool validate(const std::unordered_map &headers) const override { auto it = headers.find("X-API-Key"); if (it == headers.end()) { return false; @@ -58,20 +59,20 @@ class AuthManagerBearer final : public AuthManagerBase { public: explicit AuthManagerBearer(std::vector tokens) { - for (auto& token : tokens) { + for (auto &token: tokens) { if (!token.empty()) { valid_tokens_.insert(std::move(token)); } } } - bool validate(const std::unordered_map& headers) const override { + bool validate(const std::unordered_map &headers) const override { auto it = headers.find("Authorization"); if (it == headers.end()) { return false; } - const std::string& auth = it->second; + const std::string &auth = it->second; constexpr std::string_view prefix = "Bearer "; if (auth.substr(0, prefix.size()) != prefix) { return false; @@ -80,7 +81,9 @@ class AuthManagerBearer final : public AuthManagerBase { std::string token = auth.substr(prefix.size()); size_t start = token.find_first_not_of(" \t"); size_t end = token.find_last_not_of(" \t"); - if (start == std::string::npos) return false; + if (start == std::string::npos) { + return false; + } token = token.substr(start, end - start + 1); return valid_tokens_.find(token) != valid_tokens_.end(); @@ -99,17 +102,18 @@ class AuthManagerAny final : public AuthManagerBase { explicit AuthManagerAny(std::vector> managers) : managers_(std::move(managers)) {} - bool validate(const std::unordered_map& headers) const override { + bool validate(const std::unordered_map &headers) const override { return std::any_of(managers_.begin(), managers_.end(), - [&headers](const auto& mgr) { - return mgr->validate(headers); - }); + [&headers](const auto &mgr) { + return mgr->validate(headers); + }); } std::string type() const override { std::string types; for (size_t i = 0; i < managers_.size(); ++i) { - if (i > 0) types += "/"; + if (i > 0) { types += "/"; +} types += managers_[i]->type(); } return types; diff --git a/src/business/python_plugin_instance.cpp b/src/business/python_plugin_instance.cpp index 3fa7c45..2f80b94 100644 --- a/src/business/python_plugin_instance.cpp +++ b/src/business/python_plugin_instance.cpp @@ -1,6 +1,5 @@ #include "python_plugin_instance.h" #include "python_runtime_manager.h" -#include #include #include #include "core/logger.h" @@ -104,10 +103,9 @@ bool PythonPluginInstance::initialize_plugin_module() { MCP_INFO("[PLUGIN] Plugin module loaded successfully"); return true; - } catch (const py::error_already_set& e) { + } catch (const py::error_already_set& ) { // Print detailed Python exception (including stack trace) to help locate import failure - MCP_ERROR("[PLUGIN] Python error loading module '{}': {}", module_name_, e.what()); - MCP_ERROR("[PLUGIN] Python traceback: {}", e.trace()); + // MCP_ERROR("[PLUGIN] Python traceback: {}", e.trace()); return false; } catch (const std::exception& e) { MCP_ERROR("[PLUGIN] C++ error loading module '{}': {}", module_name_, e.what()); @@ -234,7 +232,8 @@ const char* PythonPluginInstance::call_tool(const char* name, const char* args_j } catch (const py::error_already_set& e) { // Catch Python exceptions (e.g. JSON parsing errors, tool not found) MCP_ERROR("[PLUGIN] call_tool: PYTHON ERROR - {}", e.what()); - MCP_ERROR("[PLUGIN] call_tool: PYTHON TRACEBACK - {}", e.trace()); + + // MCP_ERROR("[PLUGIN] call_tool: PYTHON TRACEBACK - {}", e.trace()); if (error) { error->code = -1; error->message = strdup(e.what()); } return nullptr; } catch (const std::exception& e) { diff --git a/src/business/python_runtime_manager.cpp b/src/business/python_runtime_manager.cpp index 7db1663..88d35ae 100644 --- a/src/business/python_runtime_manager.cpp +++ b/src/business/python_runtime_manager.cpp @@ -1,6 +1,7 @@ #include "python_runtime_manager.h" #include #include "core/logger.h" +#include PythonRuntimeManager& PythonRuntimeManager::getInstance() { static PythonRuntimeManager instance; @@ -15,7 +16,9 @@ PythonRuntimeManager::~PythonRuntimeManager() { std::lock_guard lock(runtime_mutex_); if (!initialized_) return; - MCP_DEBUG("[PYTHON] Finalizing Python runtime (thread: {})", std::this_thread::get_id()); + std::ostringstream oss; + oss << std::this_thread::get_id(); + MCP_DEBUG("[PYTHON] Finalizing Python runtime (thread: {})", oss.str()); // 1. Restore main thread state (must be done before Py_Finalize()) if (main_thread_state_ != nullptr) { @@ -37,12 +40,16 @@ PythonRuntimeManager::~PythonRuntimeManager() { bool PythonRuntimeManager::initialize(const std::string& plugin_dir) { std::lock_guard lock(runtime_mutex_); if (initialized_) { - MCP_DEBUG("[PYTHON] Runtime already initialized (thread: {})", std::this_thread::get_id()); + std::ostringstream oss; + oss << std::this_thread::get_id(); + MCP_DEBUG("[PYTHON] Runtime already initialized (thread: {})", oss.str()); return true; } try { - MCP_DEBUG("[PYTHON] Initializing Python interpreter (thread: {})", std::this_thread::get_id()); + std::ostringstream oss; + oss << std::this_thread::get_id(); + MCP_DEBUG("[PYTHON] Initializing Python interpreter (thread: {})", oss.str()); // 1. Initialize Python interpreter (void type, no need to check return value) Py_Initialize(); @@ -58,7 +65,9 @@ bool PythonRuntimeManager::initialize(const std::string& plugin_dir) { if (main_thread_state_ == nullptr) { MCP_WARN("[PYTHON] Warning: PyEval_SaveThread() returned null"); } else { - MCP_DEBUG("[PYTHON] Main thread GIL released (thread: {})", std::this_thread::get_id()); + std::ostringstream oss2; + oss2 << std::this_thread::get_id(); + MCP_DEBUG("[PYTHON] Main thread GIL released (thread: {})", oss2.str()); } // 4. Re-acquire GIL and add plugin directory to sys.path diff --git a/src/main.cpp b/src/main.cpp index 29d1bc5..7bb38f2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,11 @@ #include "metrics/rate_limiter.h" #include "metrics/performance_metrics.h" #include "utils/auth_utils.h" +#include +#include +#include +#include +#include /** @@ -130,6 +135,22 @@ int main() { .with_auth_manager(auth_manager) // Set authentication manager .build(); // Construct the server instance + // Setup signal handler for graceful shutdown + asio::io_context io_context; + asio::signal_set signals(io_context, SIGINT, SIGTERM); + + signals.async_wait([&](const asio::error_code& error, int signal_number) { + if (!error) { + MCP_INFO("Received signal {}, initiating graceful shutdown...", signal_number); + std::quick_exit(0); + } + }); + + // Run the signal handler in a separate thread + std::thread signal_thread([&io_context]() { + io_context.run(); + }); + // Notify that the server is ready to accept connections. MCP_INFO("MCPServer.cpp is ready."); MCP_INFO("Send JSON-RPC messages via /mcp."); @@ -138,6 +159,13 @@ int main() { // This call is expected to block until the server is stopped. server->run(); + // Stop the signal handler and wait for the thread to finish + io_context.stop(); + if (signal_thread.joinable()) { + signal_thread.join(); + } + + MCP_INFO("Server shutdown complete."); return 0;// Normal exit } catch (const std::exception &e) { diff --git a/src/transport/ssl_session.cpp b/src/transport/ssl_session.cpp index eee69ec..a831b35 100644 --- a/src/transport/ssl_session.cpp +++ b/src/transport/ssl_session.cpp @@ -139,7 +139,7 @@ namespace mcp::transport { size_t chunk_size = std::min(max_chunk_size, message.size() - total_bytes_written); std::string chunk = message.substr(total_bytes_written, chunk_size); - asio::error_code ec; + //asio::error_code ec; size_t bytes_written = co_await asio::async_write(ssl_stream_, asio::buffer(chunk), asio::redirect_error(asio::use_awaitable, ec)); if (ec) {