Skip to main content
Glama

mcp-tool-builder

by hanweg
tool_builder_server.py10.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())

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hanweg/mcp-tool-builder'

If you have feedback or need assistance with the MCP directory API, please join our Discord server