Skip to content

Commit d0bd33f

Browse files
committed
samples: dynamic mcp with input json schema
1 parent 078e195 commit d0bd33f

File tree

2 files changed

+118
-75
lines changed

2 files changed

+118
-75
lines changed

samples/mcp-dynamic-server/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mcp-dynamic-server"
3-
version = "0.0.7"
3+
version = "0.0.16"
44
description = "Dynamic tools MCP Server"
55
authors = [{ name = "John Doe" }]
66
dependencies = [
Lines changed: 117 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,75 @@
11
from typing import Any, Callable, Dict, List, Optional
22

33
from mcp.server.fastmcp import FastMCP
4+
from mcp.types import Tool as MCPTool
45

56
# Initialize the MCP server
67
mcp = FastMCP("Self-Extending MCP Server")
78

8-
built_in_tools = {
9-
"get_tools": {
10-
"description": "Get a list of all available tools in the MCP server.",
11-
"parameters": {}
12-
},
13-
"add_tool": {
14-
"description": "Add a new tool to the MCP server.",
15-
"parameters": {
16-
"name": "Name of the tool (required)",
17-
"code": "Python code implementing the tool function (required)",
18-
"description": "Description of what the tool does (required)",
19-
"param_descriptions": "Dictionary of parameter names to descriptions (optional)"
20-
}
21-
},
22-
"call_tool": {
23-
"description": "Call a registered tool with the given arguments.",
24-
"parameters": {
25-
"name": "Name of the tool to call (required)",
26-
"args": "Dictionary of arguments to pass to the tool"
27-
}
28-
}
29-
}
9+
built_in_tools: List[MCPTool] = [
10+
MCPTool(
11+
name="add_tool",
12+
description="Add a new tool to the MCP server by providing its Python code.",
13+
inputSchema={
14+
"type": "object",
15+
"properties": {
16+
"name": {"type": "string", "description": "Name of the tool"},
17+
"code": {
18+
"type": "string",
19+
"description": "Python code implementing the tool's function. Must define a function with the specified 'name'. Type hints in the function signature for the input schema.",
20+
},
21+
"description": {
22+
"type": "string",
23+
"description": "Description of what the tool does",
24+
},
25+
"inputSchema": {
26+
"type": "object",
27+
"description": "JSON schema object describing the parameters the new tool expects (optional). This schema will be returned by get_tools and used for documentation.",
28+
},
29+
},
30+
"required": ["name", "code", "description"],
31+
},
32+
),
33+
MCPTool(
34+
name="call_tool",
35+
description="Call a registered dynamic tool with the given arguments.",
36+
inputSchema={
37+
"type": "object",
38+
"properties": {
39+
"name": {
40+
"type": "string",
41+
"description": "Name of the dynamic tool to call",
42+
},
43+
"args": {
44+
"type": "object",
45+
"description": "Dictionary of arguments to pass to the tool. Must conform to the dynamic tool's inferred JSON input schema.",
46+
},
47+
},
48+
"required": ["name", "args"],
49+
},
50+
),
51+
]
52+
3053

3154
# Tool registry to track dynamically added tools
3255
class ToolRegistry:
3356
def __init__(self):
3457
self.tools = {} # name -> function
3558
self.metadata = {} # name -> metadata
3659

37-
def register(self, name: str, func: Callable, description: str, param_descriptions: Dict[str, str] = None):
60+
def register(
61+
self,
62+
name: str,
63+
func: Callable,
64+
description: str,
65+
inputSchema: Dict[str, str] = None,
66+
):
3867
"""Register a new tool in the registry."""
3968
self.tools[name] = func
4069
self.metadata[name] = {
4170
"name": name,
4271
"description": description,
43-
"parameters": param_descriptions or {}
72+
"inputSchema": inputSchema or {},
4473
}
4574

4675
def get_tool(self, name: str) -> Optional[Callable]:
@@ -60,54 +89,46 @@ def has_tool(self, name: str) -> bool:
6089
return name in self.tools
6190

6291

63-
# Create the registry
6492
registry = ToolRegistry()
6593

6694

67-
# Core functionality: List available tools
68-
@mcp.tool()
69-
def get_tools() -> Dict[str, Any]:
95+
@mcp._mcp_server.list_tools()
96+
async def get_tools() -> list[MCPTool]:
7097
"""Get a list of all available tools in the MCP server.
7198
7299
Returns:
73-
Dictionary with list of available tools and their metadata
100+
List of available tools and their metadata
74101
"""
75-
try:
76-
tools = registry.list_tools()
77-
78-
# Combine built-in tools with dynamic tools
79-
all_tools = []
80-
for name, info in built_in_tools.items():
81-
all_tools.append({
82-
"name": name,
83-
"description": info["description"],
84-
"parameters": info["parameters"],
85-
"built_in": True
86-
})
87-
88-
for tool in tools:
89-
all_tools.append({
90-
"name": tool["name"],
91-
"description": tool["description"],
92-
"parameters": tool["parameters"],
93-
"built_in": False
94-
})
95-
96-
return {"status": "success", "tools": all_tools}
97-
except Exception as e:
98-
return {"status": "error", "message": str(e)}
102+
all_tools = list(built_in_tools)
103+
104+
tools = registry.list_tools()
105+
106+
for tool in tools:
107+
all_tools.append(
108+
MCPTool(
109+
name=tool["name"],
110+
description=tool["description"],
111+
inputSchema=tool["inputSchema"],
112+
)
113+
)
114+
115+
return all_tools
99116

100117

101-
# Core functionality: Add a new tool
102118
@mcp.tool()
103-
def add_tool(name: str = None, code: str = None, description: str = None, param_descriptions: Dict[str, str] = None) -> Dict[str, Any]:
104-
"""Add a new tool to the MCP server.
119+
def add_tool(
120+
name: str = None,
121+
code: str = None,
122+
description: str = None,
123+
inputSchema: Dict[str, Any] = None,
124+
) -> Dict[str, Any]:
125+
"""Add a new tool to the MCP server by providing its Python code.
105126
106127
Args:
107-
name: Name of the tool
108-
code: Python code implementing the tool function
109-
description: Description of what the tool does
110-
param_descriptions: Dictionary of parameter names to descriptions
128+
name: Name of the tool (required)
129+
code: Python code implementing the tool's function. Must define a function with the specified 'name'. Type hints in the function signature will be used to infer the input schema. (required)
130+
description: Description of what the tool does (required)
131+
inputSchema: JSON schema object describing the parameters the new tool expects (optional). This schema will be returned by get_tools and used for documentation.
111132
112133
Returns:
113134
Dictionary with operation status
@@ -126,7 +147,7 @@ def add_tool(name: str = None, code: str = None, description: str = None, param_
126147
return {
127148
"status": "error",
128149
"message": f"Missing required parameters: {', '.join(missing_params)}",
129-
"usage_example": "add_tool(name='tool_name', code='def tool_name(param1, param2):\\n # code here\\n return {\"status\": \"success\"}', description='Tool description', param_descriptions={'param1': 'Description of param1', 'param2': 'Description of param2'})"
150+
"example": "add_tool(name='tool_name', code='def tool_name(param1: str, param2: str):\\n # code here\\n return {\"status\": \"success\"}', description='Tool description', inputSchema={'param1': 'Description of param1', 'param2': 'Description of param2'})",
130151
}
131152

132153
# Check if tool already exists
@@ -156,15 +177,15 @@ def add_tool(name: str = None, code: str = None, description: str = None, param_
156177
}
157178

158179
# Register the tool with our registry
159-
registry.register(name, func, description, param_descriptions)
180+
registry.register(name, func, description, inputSchema)
160181

161182
# Get the parameter information to return
162-
params = registry.get_metadata(name)["parameters"]
183+
params = registry.get_metadata(name)["inputSchema"]
163184

164185
return {
165186
"status": "success",
166187
"message": f"Tool '{name}' added successfully",
167-
"parameters": params
188+
"inputSchema": params,
168189
}
169190

170191
except SyntaxError as e:
@@ -179,14 +200,13 @@ def add_tool(name: str = None, code: str = None, description: str = None, param_
179200
return {"status": "error", "message": str(e)}
180201

181202

182-
# Core functionality: Call a tool
183203
@mcp.tool()
184204
def call_tool(name: str, args: Dict[str, Any] = None) -> Dict[str, Any]:
185-
"""Call a registered tool with the given arguments.
205+
"""Call a registered dynamic tool with the given arguments.
186206
187207
Args:
188-
name: Name of the tool to call
189-
args: Dictionary of arguments to pass to the tool
208+
name: Name of the dynamic tool to call (required)
209+
args: Dictionary of arguments to pass to the tool. You should consult the tool's schema from get_tools to know the expected structure. (required)
190210
191211
Returns:
192212
Dictionary with the tool's response
@@ -201,7 +221,7 @@ def call_tool(name: str, args: Dict[str, Any] = None) -> Dict[str, Any]:
201221
"status": "error",
202222
"message": f"Cannot call built-in tool '{name}' using call_tool",
203223
"note": f"Use the {name} function directly instead of call_tool",
204-
"parameters": built_in_tools[name]["parameters"]
224+
"inputSchema": built_in_tools[name]["inputSchema"],
205225
}
206226

207227
# Get the tool
@@ -211,7 +231,7 @@ def call_tool(name: str, args: Dict[str, Any] = None) -> Dict[str, Any]:
211231
return {
212232
"status": "error",
213233
"message": f"Tool '{name}' not found",
214-
"available_tools": [t["name"] for t in registry.list_tools()]
234+
"available_tools": [t["name"] for t in registry.list_tools()],
215235
}
216236

217237
# Call the tool with the provided arguments
@@ -220,12 +240,34 @@ def call_tool(name: str, args: Dict[str, Any] = None) -> Dict[str, Any]:
220240
return result
221241
except TypeError as e:
222242
# Likely an argument mismatch
223-
params = registry.get_metadata(name)["parameters"]
243+
params = registry.get_metadata(name)["inputSchema"]
244+
245+
# Build a usage example with actual parameter names
246+
param_examples = {}
247+
248+
# Handle different possible inputSchema structures
249+
if isinstance(params, dict):
250+
if "properties" in params:
251+
# Standard JSON Schema format
252+
for param_name in params["properties"]:
253+
param_examples[param_name] = f"<{param_name}_value>"
254+
else:
255+
# Simple dict of param_name -> description
256+
for param_name in params:
257+
param_examples[param_name] = f"<{param_name}_value>"
258+
259+
# If no parameters found or empty schema, provide generic example
260+
if not param_examples:
261+
param_examples = {"param1": "<value1>", "param2": "<value2>"}
262+
263+
# Format the dictionary for better readability
264+
usage_str = str(param_examples).replace("'<", "<").replace(">'", ">")
265+
224266
return {
225267
"status": "error",
226-
"message": f"Argument error calling tool '{name}': {str(e)}",
227-
"expected_parameters": params,
228-
"usage_example": f"call_tool(name='{name}', args={{'param1': value1, 'param2': value2, ...}})"
268+
"message": f"Argument error calling tool '{name}': {str(e)}. Please fix your mistakes, add proper 'args' values!",
269+
"inputSchema": params,
270+
"example": f"call_tool(name='{name}', args={usage_str})",
229271
}
230272
except Exception as e:
231273
return {
@@ -236,6 +278,7 @@ def call_tool(name: str, args: Dict[str, Any] = None) -> Dict[str, Any]:
236278
except Exception as e:
237279
return {"status": "error", "message": str(e)}
238280

281+
239282
# Run the server when the script is executed
240283
if __name__ == "__main__":
241284
mcp.run()

0 commit comments

Comments
 (0)