tool_builder_server.py•10.8 kB
import os
import json
import ast
import inspect
import importlib.util
from typing import Any, Dict, List, Optional
from pathlib import Path
from mcp.server import Server, NotificationOptions
from mcp.server.stdio import stdio_server
import mcp.types as types
class ToolBuilderServer:
    def __init__(self, tools_dir: str):
        """Initialize the Tool Builder Server.
        
        Args:
            tools_dir: Directory where tool scripts will be stored
        """
        self.tools_dir = Path(tools_dir)
        self.tools: Dict[str, Any] = {}
        self.tools_config: List[Dict] = []
        
        # Create tools directory if it doesn't exist
        self.tools_dir.mkdir(parents=True, exist_ok=True)
        
        # Load existing tools
        self.reload_tools()
        
        # Initialize MCP server
        self.server = Server("tool-builder")
        self._register_handlers()
    def _register_handlers(self):
        """Register all MCP protocol handlers."""
        
        @self.server.list_tools()
        async def handle_list_tools() -> List[types.Tool]:
            """List all available tools including tool management tools."""
            tools = [
                types.Tool(
                    name="create_tool",
                    description="Create a new Python tool with specified functionality",
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "tool_name": {
                                "type": "string",
                                "description": "Name of the new tool"
                            },
                            "description": {
                                "type": "string", 
                                "description": "Description of what the tool should do"
                            },
                            "code": {
                                "type": "string",
                                "description": "Python code implementing the tool"
                            }
                        },
                        "required": ["tool_name", "description", "code"]
                    }
                ),
                types.Tool(
                    name="list_available_tools",
                    description="List all currently available tools",
                    inputSchema={
                        "type": "object",
                        "properties": {},
                        "required": []
                    }
                ),
                # Add existing tools dynamically
                *[types.Tool(
                    name=tool["name"],
                    description=tool.get("description", "No description available"),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            param: {"type": "string"} 
                            for param in tool.get("parameters", {})
                        },
                        "required": list(tool.get("parameters", {}).keys())
                    }
                ) for tool in self.tools_config]
            ]
            return tools
        @self.server.call_tool()
        async def handle_call_tool(
            name: str, arguments: Dict | None
        ) -> List[types.TextContent | types.ImageContent | types.EmbeddedResource]:
            """Handle tool execution requests."""
            if not arguments:
                arguments = {}
            if name == "create_tool":
                result = await self._create_tool(
                    arguments["tool_name"],
                    arguments["description"],
                    arguments["code"]
                )
                return [types.TextContent(
                    type="text",
                    text=result
                )]
            elif name == "list_available_tools":
                tools_list = "\n".join([
                    f"- {tool['name']}: {tool.get('description', 'No description')}"
                    for tool in self.tools_config
                ])
                return [types.TextContent(
                    type="text",
                    text=f"Available tools:\n{tools_list}"
                )]
            # Handle dynamically loaded tools
            elif name in self.tools:
                # Check if the tool is properly loaded
                if name not in self.tools or self.tools[name] is None:
                    return [types.TextContent(
                        type="text",
                        text=(f"Tool '{name}' exists but cannot be used yet.\n"
                              "IMPORTANT: New tools require a client restart before they can be used.\n"
                              "Please:\n"
                              "1. Restart Claude Desktop\n"
                              "2. Start a new conversation\n"
                              "3. Try using the tool again")
                    )]
                try:
                    tool_func = self.tools[name]
                    if inspect.iscoroutinefunction(tool_func):
                        result = await tool_func(**arguments)
                    else:
                        result = tool_func(**arguments)
                    return [types.TextContent(
                        type="text",
                        text=str(result)
                    )]
                except Exception as e:
                    return [types.TextContent(
                        type="text",
                        text=f"Error executing tool {name}: {str(e)}"
                    )]
            else:
                return [types.TextContent(
                    type="text",
                    text=(f"Tool {name} not found or not fully initialized. "
                        "Please ensure the tool was created correctly "
                        "and restart Claude Desktop.")
                )]
    async def _create_tool(self, tool_name: str, description: str, code: str) -> str:
        try:
            # Validate the code is Python
            try:
                ast.parse(code)
            except SyntaxError:
                return f"Error: Invalid Python syntax in the tool code for {tool_name}"
            if any(tool["name"] == tool_name for tool in self.tools_config):
                return f"Tool {tool_name} already exists"
            tool_path = self.tools_dir / f"{tool_name}.py"
            tool_path.write_text(code)
            # Parse the function definition
            tree = ast.parse(code)
            func_def = next(
                (node for node in ast.walk(tree) 
                if isinstance(node, ast.FunctionDef) and node.name == tool_name), 
                None
            )
            if not func_def:
                return f"Error: Could not find a function named {tool_name} in the provided code"
            # Extract parameters
            parameters = {
                arg.arg: "string" 
                for arg in func_def.args.args 
                if arg.arg != "self"
            }
            tool_config = {
                "name": tool_name,
                "description": description,
                "parameters": parameters,
                "file": f"{tool_name}.py",  # Store relative path
                "function": tool_name
            }
            self.tools_config.append(tool_config)
            
            tools_json_path = self.tools_dir / "tools.json"
            tools_json_path.write_text(json.dumps(self.tools_config, indent=4))
            self.reload_tools()
                        
            # Return success message with explicit restart instructions
            return (f"Tool '{tool_name}' has been successfully created and will be available after client restart.\n"
                f"Description: {description}\n"
                f"Status: Added to tools.json\n"
                "IMPORTANT: You must restart Claude Desktop before you can use this tool.\n"
                "Please restart the client before attempting to use the newly created tool.")
        except Exception as e:
            # Ensure a string is always returned, even in error cases
            return f"Error creating tool: {str(e)}" or "Unknown tool creation error"
    def reload_tools(self):
        try:
            tools_json_path = self.tools_dir / "tools.json"
            if tools_json_path.exists():
                self.tools_config = json.loads(tools_json_path.read_text())
            else:
                self.tools_config = []
            self.tools.clear()
            for tool in self.tools_config:
                try:
                    # Resolve relative path
                    tool_path = self.tools_dir / tool["file"]
                    spec = importlib.util.spec_from_file_location(
                        tool["name"], 
                        str(tool_path)
                    )
                    if spec and spec.loader:
                        module = importlib.util.module_from_spec(spec)
                        spec.loader.exec_module(module)
                        tool_func = getattr(module, tool["function"])
                        self.tools[tool["name"]] = tool_func
                    else:
                        print(f"Could not load module for tool {tool['name']}")
                        self.tools[tool["name"]] = None
                except Exception as e:
                    print(f"Error loading tool {tool['name']}: {e}")
                    # Explicitly set to None to prevent execution
                    self.tools[tool["name"]] = None
        except Exception as e:
            print(f"Error reloading tools: {e}")
async def main():
    import argparse
    from pathlib import Path
    
    # Get project root directory (parent of src)
    project_root = Path(__file__).parent.parent.parent
    default_tools_dir = project_root / "tools"
    
    parser = argparse.ArgumentParser(description='Tool Builder MCP Server')
    parser.add_argument('--tools-dir', type=str, default=str(default_tools_dir),
                       help='Directory where tool scripts will be stored')
    
    args = parser.parse_args()
    server = ToolBuilderServer(tools_dir=args.tools_dir)
    
    async with stdio_server() as (read_stream, write_stream):
        await server.server.run(
            read_stream,
            write_stream,
            server.server.create_initialization_options(
                notification_options=NotificationOptions(),
                experimental_capabilities={}
            )
        )
if __name__ == "__main__":
    import asyncio
    import sys
    if sys.platform == 'win32':
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    asyncio.run(main())