Skip to content

Commit 7af16ff

Browse files
authored
Merge pull request #90 from UiPath/feat/slack_agent_smaple
samples: add functions and dynamic mcp servers
2 parents 80c1764 + 20ee892 commit 7af16ff

File tree

8 files changed

+1722
-212
lines changed

8 files changed

+1722
-212
lines changed

samples/mcp-dynamic-server/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "mcp-dynamic-server"
3-
version = "0.0.16"
4-
description = "Dynamic tools MCP Server"
3+
version = "0.0.18"
4+
description = "Dynamic MCP Server with self-extending tools"
55
authors = [{ name = "John Doe" }]
66
dependencies = [
77
"uipath-mcp==0.0.77",
Lines changed: 10 additions & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,10 @@
1-
from typing import Any, Callable, Dict, List, Optional
1+
from typing import Any, Dict
22

33
from mcp.server.fastmcp import FastMCP
4-
from mcp.types import Tool as MCPTool
54

65
# Initialize the MCP server
76
mcp = FastMCP("Self-Extending MCP Server")
87

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-
53-
54-
# Tool registry to track dynamically added tools
55-
class ToolRegistry:
56-
def __init__(self):
57-
self.tools = {} # name -> function
58-
self.metadata = {} # name -> metadata
59-
60-
def register(
61-
self,
62-
name: str,
63-
func: Callable,
64-
description: str,
65-
inputSchema: Dict[str, Any] = None,
66-
):
67-
"""Register a new tool in the registry."""
68-
self.tools[name] = func
69-
self.metadata[name] = {
70-
"name": name,
71-
"description": description,
72-
"inputSchema": inputSchema or {},
73-
}
74-
75-
def get_tool(self, name: str) -> Optional[Callable]:
76-
"""Get a tool by name."""
77-
return self.tools.get(name)
78-
79-
def get_metadata(self, name: str) -> Optional[Dict[str, Any]]:
80-
"""Get tool metadata by name."""
81-
return self.metadata.get(name)
82-
83-
def list_tools(self) -> List[Dict[str, Any]]:
84-
"""List all registered tools."""
85-
return [self.metadata[name] for name in sorted(self.tools.keys())]
86-
87-
def has_tool(self, name: str) -> bool:
88-
"""Check if a tool exists."""
89-
return name in self.tools
90-
91-
92-
registry = ToolRegistry()
93-
94-
95-
@mcp._mcp_server.list_tools()
96-
async def get_tools() -> List[MCPTool]:
97-
"""Get a list of all available tools in the MCP server.
98-
99-
Returns:
100-
List of available tools and their metadata
101-
"""
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
116-
1178

1189
@mcp.tool()
11910
def add_tool(
@@ -151,7 +42,8 @@ def add_tool(
15142
}
15243

15344
# Check if tool already exists
154-
if registry.has_tool(name) or hasattr(mcp, name):
45+
existing_tools = [tool.name for tool in mcp._tool_manager.list_tools()]
46+
if name in existing_tools:
15547
return {"status": "error", "message": f"Tool '{name}' already exists"}
15648

15749
# Validate the code
@@ -160,32 +52,21 @@ def add_tool(
16052
namespace = {}
16153
exec(code, namespace)
16254

163-
# Get the function
164-
if name not in namespace:
165-
return {
166-
"status": "error",
167-
"message": f"Function '{name}' not found in the provided code",
168-
}
169-
170-
func = namespace[name]
171-
172-
# Check if it's a function
173-
if not callable(func):
55+
if name not in namespace or not callable(namespace[name]):
17456
return {
17557
"status": "error",
176-
"message": f"'{name}' is not a callable function",
58+
"message": f"Valid function '{name}' not found in code",
17759
}
17860

179-
# Register the tool with our registry
180-
registry.register(name, func, description, inputSchema)
181-
182-
# Get the parameter information to return
183-
params = registry.get_metadata(name)["inputSchema"]
61+
# Register the tool with fast mcp
62+
mcp._tool_manager.add_tool(
63+
namespace[name], name=name, description=description
64+
)
18465

18566
return {
18667
"status": "success",
18768
"message": f"Tool '{name}' added successfully",
188-
"inputSchema": params,
69+
"inputSchema": inputSchema,
18970
}
19071

19172
except SyntaxError as e:
@@ -200,86 +81,6 @@ def add_tool(
20081
return {"status": "error", "message": str(e)}
20182

20283

203-
@mcp.tool()
204-
def call_tool(name: str, args: Dict[str, Any] = None) -> Dict[str, Any]:
205-
"""Call a registered dynamic tool with the given arguments.
206-
207-
Args:
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)
210-
211-
Returns:
212-
Dictionary with the tool's response
213-
"""
214-
215-
args = args or {}
216-
217-
try:
218-
# Check if it's a built-in tool
219-
matching_tool = next((tool for tool in built_in_tools if tool.name == name), None)
220-
if matching_tool:
221-
return {
222-
"status": "error",
223-
"message": f"Cannot call built-in tool '{name}' using call_tool",
224-
"note": f"Use the {name} function directly instead of call_tool",
225-
"inputSchema": matching_tool.inputSchema,
226-
}
227-
228-
# Get the tool
229-
tool = registry.get_tool(name)
230-
231-
if not tool:
232-
return {
233-
"status": "error",
234-
"message": f"Tool '{name}' not found",
235-
"available_tools": [t["name"] for t in registry.list_tools()],
236-
}
237-
238-
# Call the tool with the provided arguments
239-
try:
240-
result = tool(**args)
241-
return result
242-
except TypeError as e:
243-
# Likely an argument mismatch
244-
params = registry.get_metadata(name)["inputSchema"]
245-
246-
# Build a usage example with actual parameter names
247-
param_examples = {}
248-
249-
# Handle different possible inputSchema structures
250-
if isinstance(params, dict):
251-
if "properties" in params:
252-
# Standard JSON Schema format
253-
for param_name in params["properties"]:
254-
param_examples[param_name] = f"<{param_name}_value>"
255-
else:
256-
# Simple dict of param_name -> description
257-
for param_name in params:
258-
param_examples[param_name] = f"<{param_name}_value>"
259-
260-
# If no parameters found or empty schema, provide generic example
261-
if not param_examples:
262-
param_examples = {"param1": "<value1>", "param2": "<value2>"}
263-
264-
# Format the dictionary for better readability
265-
usage_str = str(param_examples).replace("'<", "<").replace(">'", ">")
266-
267-
return {
268-
"status": "error",
269-
"message": f"Argument error calling tool '{name}': {str(e)}. Please fix your mistakes, add proper 'args' values!",
270-
"inputSchema": params,
271-
"example": f"call_tool(name='{name}', args={usage_str})",
272-
}
273-
except Exception as e:
274-
return {
275-
"status": "error",
276-
"message": f"Error calling tool '{name}': {str(e)}",
277-
}
278-
279-
except Exception as e:
280-
return {"status": "error", "message": str(e)}
281-
282-
28384
# Run the server when the script is executed
28485
if __name__ == "__main__":
28586
mcp.run()

samples/mcp-dynamic-server/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"servers": {
3+
"functions-server": {
4+
"transport": "stdio",
5+
"command": "python",
6+
"args": ["server.py"]
7+
}
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[project]
2+
name = "mcp-functions-server"
3+
version = "0.0.1"
4+
description = "MCP Server that allows dynamic code functions creation and executions"
5+
authors = [{ name = "John Doe" }]
6+
dependencies = [
7+
"uipath-mcp==0.0.77",
8+
]
9+
requires-python = ">=3.10"

0 commit comments

Comments
 (0)