tooluniverse 1.0.9__py3-none-any.whl → 1.0.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tooluniverse might be problematic. Click here for more details.
- tooluniverse/admetai_tool.py +1 -1
- tooluniverse/agentic_tool.py +65 -17
- tooluniverse/base_tool.py +19 -8
- tooluniverse/boltz_tool.py +1 -1
- tooluniverse/cache/result_cache_manager.py +167 -12
- tooluniverse/compose_scripts/drug_safety_analyzer.py +1 -1
- tooluniverse/compose_scripts/multi_agent_literature_search.py +1 -1
- tooluniverse/compose_scripts/output_summarizer.py +4 -4
- tooluniverse/compose_scripts/tool_graph_composer.py +1 -1
- tooluniverse/compose_scripts/tool_metadata_generator.py +1 -1
- tooluniverse/compose_tool.py +9 -9
- tooluniverse/core_tool.py +2 -2
- tooluniverse/ctg_tool.py +4 -4
- tooluniverse/custom_tool.py +1 -1
- tooluniverse/dataset_tool.py +2 -2
- tooluniverse/default_config.py +1 -1
- tooluniverse/enrichr_tool.py +14 -14
- tooluniverse/execute_function.py +520 -15
- tooluniverse/extended_hooks.py +4 -4
- tooluniverse/gene_ontology_tool.py +1 -1
- tooluniverse/generate_tools.py +3 -3
- tooluniverse/humanbase_tool.py +10 -10
- tooluniverse/logging_config.py +2 -2
- tooluniverse/mcp_client_tool.py +57 -129
- tooluniverse/mcp_integration.py +52 -49
- tooluniverse/mcp_tool_registry.py +147 -528
- tooluniverse/openalex_tool.py +8 -8
- tooluniverse/openfda_tool.py +2 -2
- tooluniverse/output_hook.py +15 -15
- tooluniverse/package_tool.py +1 -1
- tooluniverse/pmc_tool.py +2 -2
- tooluniverse/remote/boltz/boltz_mcp_server.py +1 -1
- tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +2 -2
- tooluniverse/remote/immune_compass/compass_tool.py +3 -3
- tooluniverse/remote/pinnacle/pinnacle_tool.py +2 -2
- tooluniverse/remote/transcriptformer/transcriptformer_tool.py +3 -3
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +3 -3
- tooluniverse/remote_tool.py +4 -4
- tooluniverse/scripts/filter_tool_files.py +2 -2
- tooluniverse/smcp.py +93 -12
- tooluniverse/smcp_server.py +100 -20
- tooluniverse/space/__init__.py +46 -0
- tooluniverse/space/loader.py +133 -0
- tooluniverse/space/validator.py +353 -0
- tooluniverse/tool_finder_embedding.py +2 -2
- tooluniverse/tool_finder_keyword.py +9 -9
- tooluniverse/tool_finder_llm.py +6 -6
- tooluniverse/tools/_shared_client.py +3 -3
- tooluniverse/url_tool.py +1 -1
- tooluniverse/uspto_tool.py +1 -1
- tooluniverse/utils.py +10 -10
- {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/METADATA +7 -3
- {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/RECORD +57 -54
- {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/WHEEL +0 -0
- {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/entry_points.txt +0 -0
- {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/top_level.txt +0 -0
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
"""
|
|
2
2
|
MCP Tool Registration System for ToolUniverse
|
|
3
3
|
|
|
4
|
-
This module provides functionality to register local tools as MCP tools and
|
|
5
|
-
automatic loading of these tools on remote servers via ToolUniverse
|
|
4
|
+
This module provides functionality to register local tools as MCP tools and
|
|
5
|
+
enables automatic loading of these tools on remote servers via ToolUniverse
|
|
6
|
+
integration.
|
|
6
7
|
|
|
7
|
-
Usage
|
|
8
|
-
|
|
8
|
+
Usage
|
|
9
|
+
-----
|
|
9
10
|
|
|
10
11
|
Server Side (Tool Provider):
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
.. code-block:: python
|
|
13
|
+
|
|
14
|
+
from tooluniverse.mcp_tool_registry import (
|
|
15
|
+
register_mcp_tool, start_mcp_server
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
@register_mcp_tool(
|
|
19
|
+
tool_type_name="my_analysis_tool",
|
|
20
|
+
config={
|
|
21
|
+
"description": "Performs custom data analysis"
|
|
22
|
+
},
|
|
23
|
+
mcp_config={
|
|
24
|
+
"server_name": "Custom Analysis Server",
|
|
21
25
|
"host": "0.0.0.0",
|
|
22
26
|
"port": 8001
|
|
23
27
|
}
|
|
@@ -43,10 +47,10 @@ result = tu.run_tool("my_analysis_tool", {"data": "input"})
|
|
|
43
47
|
```
|
|
44
48
|
"""
|
|
45
49
|
|
|
46
|
-
import json
|
|
47
50
|
import asyncio
|
|
48
51
|
from typing import Dict, Any, List, Optional
|
|
49
|
-
|
|
52
|
+
|
|
53
|
+
from .tool_registry import register_tool
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
# Import SMCP and ToolUniverse dynamically to avoid circular imports
|
|
@@ -68,18 +72,21 @@ def _get_tooluniverse():
|
|
|
68
72
|
_mcp_tool_registry: Dict[str, Any] = {}
|
|
69
73
|
_mcp_server_configs: Dict[int, Dict[str, Any]] = {}
|
|
70
74
|
_mcp_server_instances: Dict[int, Any] = {}
|
|
75
|
+
_mcp_tool_configs: List[Dict[str, Any]] = [] # Store tool configs
|
|
71
76
|
|
|
72
77
|
|
|
73
78
|
def register_mcp_tool(tool_type_name=None, config=None, mcp_config=None):
|
|
74
79
|
"""
|
|
75
|
-
Decorator to register a tool class
|
|
80
|
+
Decorator to register a tool class for MCP server exposure.
|
|
76
81
|
|
|
77
|
-
This decorator
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
This decorator registers tools both globally (via register_tool) and for
|
|
83
|
+
MCP server management. The global registration allows ToolUniverse to
|
|
84
|
+
properly instantiate tools, while MCP registration controls server
|
|
85
|
+
exposure. The parameters and behavior are identical to register_tool,
|
|
86
|
+
with an optional mcp_config parameter for server configuration.
|
|
80
87
|
|
|
81
|
-
Parameters
|
|
82
|
-
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
83
90
|
tool_type_name : str, optional
|
|
84
91
|
Custom name for the tool type. Same as register_tool.
|
|
85
92
|
|
|
@@ -94,17 +101,18 @@ def register_mcp_tool(tool_type_name=None, config=None, mcp_config=None):
|
|
|
94
101
|
- transport: "http" or "stdio" (default: "http")
|
|
95
102
|
- auto_start: Whether to auto-start server when tool is registered
|
|
96
103
|
|
|
97
|
-
Returns
|
|
98
|
-
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
99
106
|
function
|
|
100
|
-
Decorator function that registers the tool class
|
|
107
|
+
Decorator function that registers the tool class for MCP server only.
|
|
101
108
|
|
|
102
|
-
Examples
|
|
103
|
-
|
|
109
|
+
Examples
|
|
110
|
+
--------
|
|
104
111
|
|
|
105
|
-
|
|
112
|
+
MCP tool registration:
|
|
106
113
|
```python
|
|
107
|
-
@register_mcp_tool('CustomToolName', config={...},
|
|
114
|
+
@register_mcp_tool('CustomToolName', config={...},
|
|
115
|
+
mcp_config={"port": 8001})
|
|
108
116
|
class MyTool:
|
|
109
117
|
pass
|
|
110
118
|
|
|
@@ -115,13 +123,13 @@ def register_mcp_tool(tool_type_name=None, config=None, mcp_config=None):
|
|
|
115
123
|
"""
|
|
116
124
|
|
|
117
125
|
def decorator(cls):
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
#
|
|
126
|
+
# Step 1: Register tool class to global registry
|
|
127
|
+
# This allows ToolUniverse to properly instantiate and manage the tool
|
|
128
|
+
# Note: Registration doesn't mean auto-loading. The lightweight
|
|
129
|
+
# ToolUniverse with keep_default_tools=False remains isolated.
|
|
122
130
|
registered_cls = register_tool(tool_type_name, config)(cls)
|
|
123
131
|
|
|
124
|
-
#
|
|
132
|
+
# Step 2: Additionally register for MCP server management
|
|
125
133
|
tool_name = tool_type_name or cls.__name__
|
|
126
134
|
tool_config = config or {}
|
|
127
135
|
tool_description = (
|
|
@@ -153,7 +161,8 @@ def register_mcp_tool(tool_type_name=None, config=None, mcp_config=None):
|
|
|
153
161
|
# Register for MCP exposure
|
|
154
162
|
tool_info = {
|
|
155
163
|
"name": tool_name,
|
|
156
|
-
"
|
|
164
|
+
"type": tool_type_name or cls.__name__, # 新增:保存工具类型
|
|
165
|
+
"class": registered_cls, # Use registered class
|
|
157
166
|
"description": tool_description,
|
|
158
167
|
"parameter_schema": tool_schema,
|
|
159
168
|
"server_config": server_config,
|
|
@@ -168,6 +177,9 @@ def register_mcp_tool(tool_type_name=None, config=None, mcp_config=None):
|
|
|
168
177
|
_mcp_server_configs[port] = {"config": server_config, "tools": []}
|
|
169
178
|
_mcp_server_configs[port]["tools"].append(tool_info)
|
|
170
179
|
|
|
180
|
+
# Note: Removed _mcp_tool_configs append since we're not using global
|
|
181
|
+
# registry
|
|
182
|
+
|
|
171
183
|
print(f"✅ Registered MCP tool: {tool_name} (server port: {port})")
|
|
172
184
|
|
|
173
185
|
# Auto-start server if requested
|
|
@@ -175,7 +187,7 @@ def register_mcp_tool(tool_type_name=None, config=None, mcp_config=None):
|
|
|
175
187
|
if auto_start:
|
|
176
188
|
start_mcp_server_for_tool(tool_name)
|
|
177
189
|
|
|
178
|
-
return registered_cls
|
|
190
|
+
return registered_cls # Return registered class
|
|
179
191
|
|
|
180
192
|
return decorator
|
|
181
193
|
|
|
@@ -186,11 +198,11 @@ def register_mcp_tool_from_config(tool_class: type, config: Dict[str, Any]):
|
|
|
186
198
|
|
|
187
199
|
This function provides a programmatic way to register tools as MCP tools
|
|
188
200
|
without using decorators, useful for dynamic tool registration.
|
|
189
|
-
Just like register_mcp_tool decorator, this
|
|
190
|
-
|
|
201
|
+
Just like register_mcp_tool decorator, this registers tools for MCP
|
|
202
|
+
exposure only.
|
|
191
203
|
|
|
192
|
-
Parameters
|
|
193
|
-
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
194
206
|
tool_class : type
|
|
195
207
|
The tool class to register
|
|
196
208
|
config : dict
|
|
@@ -200,8 +212,8 @@ def register_mcp_tool_from_config(tool_class: type, config: Dict[str, Any]):
|
|
|
200
212
|
- parameter_schema: JSON schema for parameters
|
|
201
213
|
- mcp_config: MCP server configuration
|
|
202
214
|
|
|
203
|
-
Examples
|
|
204
|
-
|
|
215
|
+
Examples
|
|
216
|
+
--------
|
|
205
217
|
```python
|
|
206
218
|
class ExistingTool:
|
|
207
219
|
def run(self, arguments):
|
|
@@ -218,12 +230,17 @@ def register_mcp_tool_from_config(tool_class: type, config: Dict[str, Any]):
|
|
|
218
230
|
tool_config = {k: v for k, v in config.items() if k != "mcp_config"}
|
|
219
231
|
mcp_config = config.get("mcp_config", {})
|
|
220
232
|
|
|
221
|
-
# Use the decorator to register
|
|
233
|
+
# Use the decorator to register for MCP only
|
|
222
234
|
register_mcp_tool(tool_type_name=name, config=tool_config, mcp_config=mcp_config)(
|
|
223
235
|
tool_class
|
|
224
236
|
)
|
|
225
237
|
|
|
226
238
|
|
|
239
|
+
def get_mcp_tool_configs() -> List[Dict[str, Any]]:
|
|
240
|
+
"""Get the MCP tool configurations for ToolUniverse."""
|
|
241
|
+
return _mcp_tool_configs.copy()
|
|
242
|
+
|
|
243
|
+
|
|
227
244
|
def get_mcp_tool_registry() -> Dict[str, Any]:
|
|
228
245
|
"""Get the current MCP tool registry."""
|
|
229
246
|
return _mcp_tool_registry.copy()
|
|
@@ -233,8 +250,9 @@ def get_registered_tools() -> List[Dict[str, Any]]:
|
|
|
233
250
|
"""
|
|
234
251
|
Get a list of all registered MCP tools with their information.
|
|
235
252
|
|
|
236
|
-
Returns
|
|
237
|
-
List of dictionaries containing tool information including name,
|
|
253
|
+
Returns
|
|
254
|
+
List of dictionaries containing tool information including name,
|
|
255
|
+
description, and port.
|
|
238
256
|
"""
|
|
239
257
|
tools = []
|
|
240
258
|
for tool_name, tool_info in _mcp_tool_registry.items():
|
|
@@ -258,15 +276,16 @@ def start_mcp_server(port: Optional[int] = None, **kwargs):
|
|
|
258
276
|
"""
|
|
259
277
|
Start MCP server(s) for registered tools.
|
|
260
278
|
|
|
261
|
-
Parameters
|
|
262
|
-
|
|
279
|
+
Parameters
|
|
280
|
+
----------
|
|
263
281
|
port : int, optional
|
|
264
|
-
Specific port to start server for. If None, starts servers for all
|
|
282
|
+
Specific port to start server for. If None, starts servers for all
|
|
283
|
+
registered tools.
|
|
265
284
|
**kwargs
|
|
266
285
|
Additional arguments passed to SMCP server
|
|
267
286
|
|
|
268
|
-
Examples
|
|
269
|
-
|
|
287
|
+
Examples
|
|
288
|
+
--------
|
|
270
289
|
```python
|
|
271
290
|
# Start server for specific port
|
|
272
291
|
start_mcp_server(port=8001)
|
|
@@ -278,10 +297,10 @@ def start_mcp_server(port: Optional[int] = None, **kwargs):
|
|
|
278
297
|
start_mcp_server(max_workers=20, debug=True)
|
|
279
298
|
```
|
|
280
299
|
"""
|
|
281
|
-
import time
|
|
282
300
|
|
|
283
301
|
try:
|
|
284
|
-
|
|
302
|
+
# Test if SMCP is available
|
|
303
|
+
_get_smcp()
|
|
285
304
|
except ImportError:
|
|
286
305
|
print("❌ SMCP not available. Cannot start MCP server.")
|
|
287
306
|
return
|
|
@@ -289,30 +308,27 @@ def start_mcp_server(port: Optional[int] = None, **kwargs):
|
|
|
289
308
|
if port is not None:
|
|
290
309
|
# Start server for specific port
|
|
291
310
|
if port in _mcp_server_configs:
|
|
311
|
+
print("🎯 MCP server(s) starting. Press Ctrl+C to stop.")
|
|
292
312
|
_start_server_for_port(port, **kwargs)
|
|
293
313
|
else:
|
|
294
314
|
print(f"❌ No tools registered for port {port}")
|
|
295
315
|
else:
|
|
296
316
|
# Start servers for all registered ports
|
|
297
|
-
|
|
298
|
-
|
|
317
|
+
ports = list(_mcp_server_configs.keys())
|
|
318
|
+
if len(ports) > 1:
|
|
319
|
+
print(
|
|
320
|
+
f"⚠️ Multiple ports registered ({len(ports)}), starting server for port {ports[0]} only"
|
|
321
|
+
)
|
|
322
|
+
print(f" Other ports: {ports[1:]}")
|
|
323
|
+
port_to_start = ports[0]
|
|
324
|
+
else:
|
|
325
|
+
port_to_start = ports[0]
|
|
299
326
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
except KeyboardInterrupt:
|
|
306
|
-
print("\n🛑 Shutting down MCP server(s)...")
|
|
307
|
-
# Cleanup server instances
|
|
308
|
-
for port, _server in _mcp_server_instances.items():
|
|
309
|
-
try:
|
|
310
|
-
print(f"🧹 Stopping server on port {port}...")
|
|
311
|
-
# Note: FastMCP cleanup is handled automatically
|
|
312
|
-
except Exception as e:
|
|
313
|
-
print(f"⚠️ Error stopping server on port {port}: {e}")
|
|
314
|
-
_mcp_server_instances.clear()
|
|
315
|
-
print("✅ All servers stopped.")
|
|
327
|
+
print("🎯 MCP server(s) starting. Press Ctrl+C to stop.")
|
|
328
|
+
_start_server_for_port(port_to_start, **kwargs)
|
|
329
|
+
|
|
330
|
+
# Note: No need for while True loop - run_simple() is blocking
|
|
331
|
+
# Server will run until interrupted
|
|
316
332
|
|
|
317
333
|
|
|
318
334
|
def _start_server_for_port(port: int, **kwargs):
|
|
@@ -327,475 +343,76 @@ def _start_server_for_port(port: int, **kwargs):
|
|
|
327
343
|
|
|
328
344
|
print(f"🚀 Starting MCP server on port {port} with {len(tools)} tools...")
|
|
329
345
|
|
|
330
|
-
# Create
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
**kwargs,
|
|
346
|
+
# Create a lightweight ToolUniverse instance WITHOUT default tools
|
|
347
|
+
# This ensures only registered MCP tools are loaded
|
|
348
|
+
ToolUniverse = _get_tooluniverse()
|
|
349
|
+
tu = ToolUniverse(
|
|
350
|
+
tool_files={}, # Empty tool files - no default categories
|
|
351
|
+
keep_default_tools=False, # Don't load any default tools
|
|
337
352
|
)
|
|
338
353
|
|
|
339
|
-
#
|
|
354
|
+
# Register MCP tools using the public API
|
|
340
355
|
for tool_info in tools:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
356
|
+
tool_config = {
|
|
357
|
+
"name": tool_info["name"],
|
|
358
|
+
"type": tool_info["type"], # 使用正确的type字段
|
|
359
|
+
"description": tool_info["description"],
|
|
360
|
+
"parameter": tool_info["parameter_schema"],
|
|
361
|
+
"category": "mcp_tools",
|
|
362
|
+
}
|
|
345
363
|
|
|
346
|
-
# Start server in background thread
|
|
347
|
-
def run_server():
|
|
348
364
|
try:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
stateless_http=True,
|
|
365
|
+
tu.register_custom_tool(
|
|
366
|
+
tool_class=tool_info["class"],
|
|
367
|
+
tool_name=tool_info["type"],
|
|
368
|
+
tool_config=tool_config,
|
|
369
|
+
instantiate=True, # 立即实例化并缓存
|
|
355
370
|
)
|
|
356
371
|
except Exception as e:
|
|
357
|
-
print(f"❌
|
|
358
|
-
|
|
359
|
-
server_thread = threading.Thread(target=run_server, daemon=True)
|
|
360
|
-
server_thread.start()
|
|
372
|
+
print(f"❌ Failed to register tool {tool_info['name']}: {e}")
|
|
373
|
+
continue
|
|
361
374
|
|
|
362
|
-
print(f"✅ MCP
|
|
375
|
+
print(f"✅ Registered {len(tools)} MCP tool(s) using ToolUniverse API")
|
|
376
|
+
tool_names = ", ".join([t["name"] for t in tools])
|
|
377
|
+
print(f" Tools: {tool_names}")
|
|
363
378
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
print(
|
|
373
|
-
f"🔧 Adding tool '{name}' using SMCP's _create_mcp_tool_from_tooluniverse approach..."
|
|
379
|
+
# Create SMCP server with pre-configured lightweight ToolUniverse
|
|
380
|
+
server = _get_smcp()(
|
|
381
|
+
name=config["server_name"],
|
|
382
|
+
tooluniverse_config=tu, # Pass pre-configured ToolUniverse
|
|
383
|
+
auto_expose_tools=True, # Auto-expose since tools are in ToolUniverse
|
|
384
|
+
search_enabled=False, # Disable search for remote tool servers
|
|
385
|
+
max_workers=config.get("max_workers", 5),
|
|
386
|
+
**kwargs,
|
|
374
387
|
)
|
|
375
388
|
|
|
376
|
-
#
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
# Convert our tool_info to the format expected by SMCP's method
|
|
380
|
-
# SMCP expects tool_config with 'name', 'description', and 'parameter' fields
|
|
381
|
-
tool_config = {
|
|
382
|
-
"name": name,
|
|
383
|
-
"description": description,
|
|
384
|
-
"parameter": schema, # SMCP expects 'parameter' not 'parameter_schema'
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
# Check if the server has the SMCP method available
|
|
388
|
-
if hasattr(server, "_create_mcp_tool_from_tooluniverse"):
|
|
389
|
-
print("✅ Using server's _create_mcp_tool_from_tooluniverse method")
|
|
390
|
-
# Temporarily store our tool instance so SMCP's method can access it
|
|
391
|
-
# We need to modify SMCP's approach to use our tool_instance instead of tooluniverse
|
|
392
|
-
server._temp_tool_instance = tool_instance
|
|
393
|
-
|
|
394
|
-
# Create a modified version of SMCP's approach
|
|
395
|
-
_create_mcp_tool_from_tooluniverse_with_instance(
|
|
396
|
-
server, tool_config, tool_instance
|
|
397
|
-
)
|
|
398
|
-
else:
|
|
399
|
-
print(
|
|
400
|
-
"⚠️ Server doesn't have _create_mcp_tool_from_tooluniverse, using fallback"
|
|
401
|
-
)
|
|
402
|
-
# Fallback to standard method
|
|
403
|
-
server.add_custom_tool(
|
|
404
|
-
name=name,
|
|
405
|
-
function=lambda arguments="{}": tool_instance.run(json.loads(arguments)),
|
|
406
|
-
description=description,
|
|
407
|
-
)
|
|
408
|
-
|
|
389
|
+
# Store server instance
|
|
390
|
+
_mcp_server_instances[port] = server
|
|
409
391
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
)
|
|
413
|
-
""
|
|
414
|
-
Create an MCP tool from a ToolUniverse tool configuration using a tool instance.
|
|
392
|
+
# Start server (blocking call)
|
|
393
|
+
host = config["host"]
|
|
394
|
+
print(f"✅ MCP server starting on {host}:{port}")
|
|
395
|
+
print(f" Server URL: http://{host}:{port}/mcp")
|
|
415
396
|
|
|
416
|
-
This method reuses the proven approach from SMCP's _create_mcp_tool_from_tooluniverse
|
|
417
|
-
method, but adapts it to work with tool instances instead of ToolUniverse.
|
|
418
|
-
It creates functions with proper parameter signatures that match the ToolUniverse
|
|
419
|
-
tool schema, enabling FastMCP's automatic parameter validation.
|
|
420
|
-
"""
|
|
421
397
|
try:
|
|
422
|
-
#
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
tool_name = tool_config["name"]
|
|
429
|
-
description = tool_config.get("description", f"Tool: {tool_name}")
|
|
430
|
-
parameters = tool_config.get("parameter", {})
|
|
431
|
-
|
|
432
|
-
# Extract parameter information from the schema
|
|
433
|
-
# Handle case where properties might be None (like in Finish tool)
|
|
434
|
-
properties = parameters.get("properties")
|
|
435
|
-
if properties is None:
|
|
436
|
-
properties = {}
|
|
437
|
-
required_params = parameters.get("required", [])
|
|
438
|
-
|
|
439
|
-
# Handle non-standard schema format where 'required' is set on individual properties
|
|
440
|
-
# instead of at the object level (common in ToolUniverse schemas)
|
|
441
|
-
if not required_params and properties:
|
|
442
|
-
required_params = [
|
|
443
|
-
param_name
|
|
444
|
-
for param_name, param_info in properties.items()
|
|
445
|
-
if param_info.get("required", False)
|
|
446
|
-
]
|
|
447
|
-
|
|
448
|
-
# Build function signature dynamically with Pydantic Field support
|
|
449
|
-
import inspect
|
|
450
|
-
from typing import Union
|
|
451
|
-
|
|
452
|
-
try:
|
|
453
|
-
from typing import Annotated
|
|
454
|
-
from pydantic import Field
|
|
455
|
-
|
|
456
|
-
PYDANTIC_AVAILABLE = True
|
|
457
|
-
except ImportError:
|
|
458
|
-
PYDANTIC_AVAILABLE = False
|
|
459
|
-
|
|
460
|
-
# Create parameter signature for the function
|
|
461
|
-
func_params = []
|
|
462
|
-
param_annotations = {}
|
|
463
|
-
|
|
464
|
-
for param_name, param_info in properties.items():
|
|
465
|
-
param_type = param_info.get("type", "string")
|
|
466
|
-
param_description = param_info.get("description", f"{param_name} parameter")
|
|
467
|
-
is_required = param_name in required_params
|
|
468
|
-
|
|
469
|
-
# Map JSON schema types to Python types
|
|
470
|
-
python_type: type
|
|
471
|
-
if param_type == "string":
|
|
472
|
-
python_type = str
|
|
473
|
-
elif param_type == "integer":
|
|
474
|
-
python_type = int
|
|
475
|
-
elif param_type == "number":
|
|
476
|
-
python_type = float
|
|
477
|
-
elif param_type == "boolean":
|
|
478
|
-
python_type = bool
|
|
479
|
-
elif param_type == "array":
|
|
480
|
-
python_type = list
|
|
481
|
-
elif param_type == "object":
|
|
482
|
-
python_type = dict
|
|
483
|
-
else:
|
|
484
|
-
python_type = str # Default to string for unknown types
|
|
485
|
-
|
|
486
|
-
# Create proper type annotation
|
|
487
|
-
if PYDANTIC_AVAILABLE:
|
|
488
|
-
# Use Pydantic Field for enhanced schema information
|
|
489
|
-
field_kwargs = {"description": param_description}
|
|
490
|
-
pydantic_field = Field(**field_kwargs)
|
|
491
|
-
|
|
492
|
-
if is_required:
|
|
493
|
-
annotated_type: Any = Annotated[python_type, pydantic_field]
|
|
494
|
-
param_annotations[param_name] = annotated_type
|
|
495
|
-
func_params.append(
|
|
496
|
-
inspect.Parameter(
|
|
497
|
-
param_name,
|
|
498
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
499
|
-
annotation=annotated_type,
|
|
500
|
-
)
|
|
501
|
-
)
|
|
502
|
-
else:
|
|
503
|
-
optional_annotated_type: Any = Annotated[
|
|
504
|
-
Union[python_type, type(None)], pydantic_field
|
|
505
|
-
]
|
|
506
|
-
param_annotations[param_name] = optional_annotated_type
|
|
507
|
-
func_params.append(
|
|
508
|
-
inspect.Parameter(
|
|
509
|
-
param_name,
|
|
510
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
511
|
-
default=None,
|
|
512
|
-
annotation=optional_annotated_type,
|
|
513
|
-
)
|
|
514
|
-
)
|
|
515
|
-
else:
|
|
516
|
-
# Fallback without Pydantic
|
|
517
|
-
if is_required:
|
|
518
|
-
param_annotations[param_name] = python_type
|
|
519
|
-
func_params.append(
|
|
520
|
-
inspect.Parameter(
|
|
521
|
-
param_name,
|
|
522
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
523
|
-
annotation=python_type,
|
|
524
|
-
)
|
|
525
|
-
)
|
|
526
|
-
else:
|
|
527
|
-
param_annotations[param_name] = Union[python_type, type(None)]
|
|
528
|
-
func_params.append(
|
|
529
|
-
inspect.Parameter(
|
|
530
|
-
param_name,
|
|
531
|
-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
532
|
-
default=None,
|
|
533
|
-
annotation=Union[python_type, type(None)],
|
|
534
|
-
)
|
|
535
|
-
)
|
|
536
|
-
|
|
537
|
-
# Create the async function with dynamic signature
|
|
538
|
-
if not properties:
|
|
539
|
-
# Tool has no parameters - create simple function
|
|
540
|
-
async def dynamic_tool_function() -> str:
|
|
541
|
-
"""Execute tool with no arguments."""
|
|
542
|
-
try:
|
|
543
|
-
# Execute our custom tool instance
|
|
544
|
-
result = tool_instance.run({})
|
|
545
|
-
|
|
546
|
-
# Format the result
|
|
547
|
-
if isinstance(result, str):
|
|
548
|
-
return result
|
|
549
|
-
else:
|
|
550
|
-
return json.dumps(result, indent=2, default=str)
|
|
551
|
-
|
|
552
|
-
except Exception as e:
|
|
553
|
-
error_msg = f"Error executing {tool_name}: {str(e)}"
|
|
554
|
-
print(f"❌ {error_msg}")
|
|
555
|
-
return json.dumps({"error": error_msg}, indent=2)
|
|
556
|
-
|
|
557
|
-
# Set function metadata
|
|
558
|
-
dynamic_tool_function.__name__ = tool_name
|
|
559
|
-
dynamic_tool_function.__signature__ = inspect.Signature([])
|
|
560
|
-
dynamic_tool_function.__annotations__ = {"return": str}
|
|
561
|
-
|
|
562
|
-
else:
|
|
563
|
-
# Tool has parameters - create function with dynamic signature
|
|
564
|
-
async def dynamic_tool_function(**kwargs) -> str:
|
|
565
|
-
"""Execute tool with provided arguments."""
|
|
566
|
-
try:
|
|
567
|
-
# Filter out None values for optional parameters
|
|
568
|
-
args_dict = {k: v for k, v in kwargs.items() if v is not None}
|
|
569
|
-
|
|
570
|
-
# Validate required parameters
|
|
571
|
-
missing_required = [
|
|
572
|
-
param for param in required_params if param not in args_dict
|
|
573
|
-
]
|
|
574
|
-
if missing_required:
|
|
575
|
-
return json.dumps(
|
|
576
|
-
{
|
|
577
|
-
"error": f"Missing required parameters: {missing_required}",
|
|
578
|
-
"required": required_params,
|
|
579
|
-
"provided": list(args_dict.keys()),
|
|
580
|
-
},
|
|
581
|
-
indent=2,
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
# Execute our custom tool instance instead of tooluniverse
|
|
585
|
-
result = tool_instance.run(args_dict)
|
|
586
|
-
|
|
587
|
-
# Format the result
|
|
588
|
-
if isinstance(result, str):
|
|
589
|
-
return result
|
|
590
|
-
else:
|
|
591
|
-
return json.dumps(result, indent=2, default=str)
|
|
592
|
-
|
|
593
|
-
except Exception as e:
|
|
594
|
-
error_msg = f"Error executing {tool_name}: {str(e)}"
|
|
595
|
-
print(f"❌ {error_msg}")
|
|
596
|
-
return json.dumps({"error": error_msg}, indent=2)
|
|
597
|
-
|
|
598
|
-
# Set function metadata
|
|
599
|
-
dynamic_tool_function.__name__ = tool_name
|
|
600
|
-
|
|
601
|
-
# Set function signature dynamically for tools with parameters
|
|
602
|
-
if func_params:
|
|
603
|
-
dynamic_tool_function.__signature__ = inspect.Signature(func_params)
|
|
604
|
-
|
|
605
|
-
# Set annotations for type hints
|
|
606
|
-
dynamic_tool_function.__annotations__ = param_annotations.copy()
|
|
607
|
-
dynamic_tool_function.__annotations__["return"] = str
|
|
608
|
-
|
|
609
|
-
# Create detailed docstring
|
|
610
|
-
param_docs = []
|
|
611
|
-
for param_name, param_info in properties.items():
|
|
612
|
-
param_desc = param_info.get("description", f"{param_name} parameter")
|
|
613
|
-
param_type = param_info.get("type", "string")
|
|
614
|
-
is_required = param_name in required_params
|
|
615
|
-
required_text = "required" if is_required else "optional"
|
|
616
|
-
param_docs.append(
|
|
617
|
-
f" {param_name} ({param_type}, {required_text}): {param_desc}"
|
|
618
|
-
)
|
|
619
|
-
|
|
620
|
-
# Set function docstring
|
|
621
|
-
dynamic_tool_function.__doc__ = f"""{description}
|
|
622
|
-
|
|
623
|
-
Parameters:
|
|
624
|
-
{chr(10).join(param_docs) if param_docs else ' No parameters required'}
|
|
625
|
-
|
|
626
|
-
Returns:
|
|
627
|
-
str: Tool execution result
|
|
628
|
-
"""
|
|
629
|
-
|
|
630
|
-
print(f"✅ Created function with {len(func_params)} parameters")
|
|
631
|
-
print("📋 Docstring includes parameter descriptions:")
|
|
632
|
-
for i, doc in enumerate(param_docs[:3], 1): # Show first 3 for brevity
|
|
633
|
-
print(f" {i}. {doc.strip()}")
|
|
634
|
-
if len(param_docs) > 3:
|
|
635
|
-
print(f" ... and {len(param_docs) - 3} more")
|
|
636
|
-
|
|
637
|
-
# Register with FastMCP using the tool decorator approach (following SMCP pattern)
|
|
638
|
-
server.tool(description=description)(dynamic_tool_function)
|
|
639
|
-
|
|
640
|
-
print(
|
|
641
|
-
f"📦 Tool '{tool_name}' registered successfully with parameter descriptions"
|
|
398
|
+
# Enable stateless mode for MCPAutoLoaderTool compatibility
|
|
399
|
+
server.run_simple(
|
|
400
|
+
transport=config["transport"],
|
|
401
|
+
host=config["host"],
|
|
402
|
+
port=port,
|
|
403
|
+
stateless_http=True,
|
|
642
404
|
)
|
|
643
|
-
|
|
644
|
-
except Exception as e:
|
|
645
|
-
print(f"❌ Error creating tool from config: {e}")
|
|
646
|
-
import traceback
|
|
647
|
-
|
|
648
|
-
traceback.print_exc()
|
|
649
|
-
# Don't raise - continue with other tools
|
|
650
|
-
return
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
def _build_fastmcp_tool_function(
|
|
654
|
-
name: str, description: str, schema: Dict[str, Any], tool_instance
|
|
655
|
-
):
|
|
656
|
-
"""
|
|
657
|
-
Build a FastMCP-compatible function with proper docstring and type annotations.
|
|
658
|
-
|
|
659
|
-
FastMCP generates parameter schema from the function signature and docstring,
|
|
660
|
-
so we need to create a function that matches the expected format exactly.
|
|
661
|
-
"""
|
|
662
|
-
properties = schema.get("properties", {})
|
|
663
|
-
required = schema.get("required", [])
|
|
664
|
-
|
|
665
|
-
# Build parameter definitions and documentation
|
|
666
|
-
param_definitions = []
|
|
667
|
-
param_names = []
|
|
668
|
-
docstring_params = []
|
|
669
|
-
|
|
670
|
-
for param_name, param_info in properties.items():
|
|
671
|
-
param_type = param_info.get("type", "string")
|
|
672
|
-
param_description = param_info.get("description", f"{param_name} parameter")
|
|
673
|
-
has_default = param_name not in required
|
|
674
|
-
default_value = param_info.get("default", None)
|
|
675
|
-
enum_values = param_info.get("enum", None)
|
|
676
|
-
|
|
677
|
-
# Map JSON schema types to Python types
|
|
678
|
-
if param_type == "string":
|
|
679
|
-
py_type = "str"
|
|
680
|
-
if has_default and default_value is None:
|
|
681
|
-
default_value = '""'
|
|
682
|
-
elif has_default:
|
|
683
|
-
default_value = f'"{default_value}"'
|
|
684
|
-
elif param_type == "integer":
|
|
685
|
-
py_type = "int"
|
|
686
|
-
if has_default and default_value is None:
|
|
687
|
-
default_value = "0"
|
|
688
|
-
elif has_default:
|
|
689
|
-
default_value = str(default_value)
|
|
690
|
-
elif param_type == "number":
|
|
691
|
-
py_type = "float"
|
|
692
|
-
if has_default and default_value is None:
|
|
693
|
-
default_value = "0.0"
|
|
694
|
-
elif has_default:
|
|
695
|
-
default_value = str(default_value)
|
|
696
|
-
elif param_type == "boolean":
|
|
697
|
-
py_type = "bool"
|
|
698
|
-
if has_default and default_value is None:
|
|
699
|
-
default_value = "False"
|
|
700
|
-
elif has_default:
|
|
701
|
-
default_value = str(default_value)
|
|
702
|
-
else:
|
|
703
|
-
py_type = "str" # Default to string
|
|
704
|
-
if has_default and default_value is None:
|
|
705
|
-
default_value = '""'
|
|
706
|
-
elif has_default:
|
|
707
|
-
default_value = f'"{default_value}"'
|
|
708
|
-
|
|
709
|
-
# Build parameter definition for function signature
|
|
710
|
-
if has_default:
|
|
711
|
-
param_def = f"{param_name}: {py_type} = {default_value}"
|
|
712
|
-
else:
|
|
713
|
-
param_def = f"{param_name}: {py_type}"
|
|
714
|
-
|
|
715
|
-
param_definitions.append(param_def)
|
|
716
|
-
param_names.append(param_name)
|
|
717
|
-
|
|
718
|
-
# Build docstring parameter documentation
|
|
719
|
-
param_doc = f" {param_name} ({py_type}): {param_description}"
|
|
720
|
-
if enum_values:
|
|
721
|
-
param_doc += f". Options: {enum_values}"
|
|
722
|
-
if has_default and default_value is not None:
|
|
723
|
-
param_doc += f". Default: {default_value}"
|
|
724
|
-
|
|
725
|
-
docstring_params.append(param_doc)
|
|
726
|
-
|
|
727
|
-
# Create function signature
|
|
728
|
-
params_str = ", ".join(param_definitions)
|
|
729
|
-
|
|
730
|
-
# Create comprehensive docstring following Google style
|
|
731
|
-
# This is critical for FastMCP to extract parameter information
|
|
732
|
-
docstring_parts = [
|
|
733
|
-
f' """{description}',
|
|
734
|
-
"",
|
|
735
|
-
" This tool provides expert consultation functionality.",
|
|
736
|
-
"",
|
|
737
|
-
]
|
|
738
|
-
|
|
739
|
-
if docstring_params:
|
|
740
|
-
docstring_parts.extend([" Args:", *docstring_params, ""])
|
|
741
|
-
|
|
742
|
-
docstring_parts.extend(
|
|
743
|
-
[
|
|
744
|
-
" Returns:",
|
|
745
|
-
" dict: Tool execution result with status and response data",
|
|
746
|
-
' """',
|
|
747
|
-
]
|
|
748
|
-
)
|
|
749
|
-
|
|
750
|
-
docstring = "\n".join(docstring_parts)
|
|
751
|
-
|
|
752
|
-
# Create the function code with comprehensive docstring
|
|
753
|
-
func_code = f"""
|
|
754
|
-
def fastmcp_tool_function({params_str}) -> dict:
|
|
755
|
-
{docstring}
|
|
756
|
-
# Collect all parameters into arguments dict for tool execution
|
|
757
|
-
arguments = {{}}
|
|
758
|
-
{chr(10).join(f' arguments["{pname}"] = {pname}' for pname in param_names)}
|
|
759
|
-
|
|
760
|
-
# Execute the original tool
|
|
761
|
-
try:
|
|
762
|
-
result = tool_instance.run(arguments)
|
|
763
|
-
return result
|
|
764
405
|
except Exception as e:
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
"status": "error"
|
|
768
|
-
}}
|
|
769
|
-
"""
|
|
770
|
-
|
|
771
|
-
# Execute the function definition in a clean namespace
|
|
772
|
-
namespace = {
|
|
773
|
-
"tool_instance": tool_instance,
|
|
774
|
-
"str": str, # Ensure str is available for error handling
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
try:
|
|
778
|
-
exec(func_code, namespace)
|
|
779
|
-
fastmcp_function = namespace["fastmcp_tool_function"]
|
|
780
|
-
|
|
781
|
-
# Verify the function was created correctly
|
|
782
|
-
if not callable(fastmcp_function):
|
|
783
|
-
raise ValueError("Generated function is not callable")
|
|
784
|
-
|
|
785
|
-
# Verify docstring exists
|
|
786
|
-
if not fastmcp_function.__doc__:
|
|
787
|
-
raise ValueError("Generated function has no docstring")
|
|
788
|
-
|
|
789
|
-
print(f"✅ FastMCP function created successfully for '{name}'")
|
|
790
|
-
print(f" Parameters: {len(param_names)} ({', '.join(param_names)})")
|
|
791
|
-
print(f" Docstring length: {len(fastmcp_function.__doc__)} chars")
|
|
406
|
+
print(f"❌ Error running MCP server on port {port}: {e}")
|
|
407
|
+
raise
|
|
792
408
|
|
|
793
|
-
return fastmcp_function
|
|
794
409
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
410
|
+
# Note: Removed 438 lines of dead code:
|
|
411
|
+
# - _add_tool_to_smcp_server (lines 410-457)
|
|
412
|
+
# - _create_mcp_tool_from_tooluniverse_with_instance (lines 459-700)
|
|
413
|
+
# - _build_fastmcp_tool_function (lines 702-848)
|
|
414
|
+
# These functions were never called and have been replaced by SMCP's
|
|
415
|
+
# built-in tool exposure mechanism.
|
|
799
416
|
|
|
800
417
|
|
|
801
418
|
def start_mcp_server_for_tool(tool_name: str):
|
|
@@ -813,8 +430,8 @@ def stop_mcp_server(port: Optional[int] = None):
|
|
|
813
430
|
"""
|
|
814
431
|
Stop MCP server(s).
|
|
815
432
|
|
|
816
|
-
Parameters
|
|
817
|
-
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
818
435
|
port : int, optional
|
|
819
436
|
Specific port to stop server for. If None, stops all servers.
|
|
820
437
|
"""
|
|
@@ -867,15 +484,15 @@ def load_mcp_tools_to_tooluniverse(tu, server_urls: Optional[List[str]] = None):
|
|
|
867
484
|
"""
|
|
868
485
|
Load MCP tools from servers into a ToolUniverse instance.
|
|
869
486
|
|
|
870
|
-
Parameters
|
|
871
|
-
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
872
489
|
tu : ToolUniverse
|
|
873
490
|
ToolUniverse instance to load tools into
|
|
874
491
|
server_urls : list of str, optional
|
|
875
492
|
List of MCP server URLs. If None, uses all registered local servers.
|
|
876
493
|
|
|
877
|
-
Examples
|
|
878
|
-
|
|
494
|
+
Examples
|
|
495
|
+
--------
|
|
879
496
|
```python
|
|
880
497
|
from tooluniverse import ToolUniverse
|
|
881
498
|
from tooluniverse.mcp_tool_registry import load_mcp_tools_to_tooluniverse
|
|
@@ -904,8 +521,10 @@ def load_mcp_tools_to_tooluniverse(tu, server_urls: Optional[List[str]] = None):
|
|
|
904
521
|
for url in server_urls:
|
|
905
522
|
try:
|
|
906
523
|
# Create auto-loader for this server
|
|
524
|
+
url_clean = url.replace(":", "_").replace("/", "_")
|
|
525
|
+
loader_name = f"mcp_auto_loader_{url_clean}"
|
|
907
526
|
loader_config = {
|
|
908
|
-
"name":
|
|
527
|
+
"name": loader_name,
|
|
909
528
|
"type": "MCPAutoLoaderTool",
|
|
910
529
|
"server_url": url,
|
|
911
530
|
"auto_register": True,
|