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.

Files changed (57) hide show
  1. tooluniverse/admetai_tool.py +1 -1
  2. tooluniverse/agentic_tool.py +65 -17
  3. tooluniverse/base_tool.py +19 -8
  4. tooluniverse/boltz_tool.py +1 -1
  5. tooluniverse/cache/result_cache_manager.py +167 -12
  6. tooluniverse/compose_scripts/drug_safety_analyzer.py +1 -1
  7. tooluniverse/compose_scripts/multi_agent_literature_search.py +1 -1
  8. tooluniverse/compose_scripts/output_summarizer.py +4 -4
  9. tooluniverse/compose_scripts/tool_graph_composer.py +1 -1
  10. tooluniverse/compose_scripts/tool_metadata_generator.py +1 -1
  11. tooluniverse/compose_tool.py +9 -9
  12. tooluniverse/core_tool.py +2 -2
  13. tooluniverse/ctg_tool.py +4 -4
  14. tooluniverse/custom_tool.py +1 -1
  15. tooluniverse/dataset_tool.py +2 -2
  16. tooluniverse/default_config.py +1 -1
  17. tooluniverse/enrichr_tool.py +14 -14
  18. tooluniverse/execute_function.py +520 -15
  19. tooluniverse/extended_hooks.py +4 -4
  20. tooluniverse/gene_ontology_tool.py +1 -1
  21. tooluniverse/generate_tools.py +3 -3
  22. tooluniverse/humanbase_tool.py +10 -10
  23. tooluniverse/logging_config.py +2 -2
  24. tooluniverse/mcp_client_tool.py +57 -129
  25. tooluniverse/mcp_integration.py +52 -49
  26. tooluniverse/mcp_tool_registry.py +147 -528
  27. tooluniverse/openalex_tool.py +8 -8
  28. tooluniverse/openfda_tool.py +2 -2
  29. tooluniverse/output_hook.py +15 -15
  30. tooluniverse/package_tool.py +1 -1
  31. tooluniverse/pmc_tool.py +2 -2
  32. tooluniverse/remote/boltz/boltz_mcp_server.py +1 -1
  33. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +2 -2
  34. tooluniverse/remote/immune_compass/compass_tool.py +3 -3
  35. tooluniverse/remote/pinnacle/pinnacle_tool.py +2 -2
  36. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +3 -3
  37. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +3 -3
  38. tooluniverse/remote_tool.py +4 -4
  39. tooluniverse/scripts/filter_tool_files.py +2 -2
  40. tooluniverse/smcp.py +93 -12
  41. tooluniverse/smcp_server.py +100 -20
  42. tooluniverse/space/__init__.py +46 -0
  43. tooluniverse/space/loader.py +133 -0
  44. tooluniverse/space/validator.py +353 -0
  45. tooluniverse/tool_finder_embedding.py +2 -2
  46. tooluniverse/tool_finder_keyword.py +9 -9
  47. tooluniverse/tool_finder_llm.py +6 -6
  48. tooluniverse/tools/_shared_client.py +3 -3
  49. tooluniverse/url_tool.py +1 -1
  50. tooluniverse/uspto_tool.py +1 -1
  51. tooluniverse/utils.py +10 -10
  52. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/METADATA +7 -3
  53. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/RECORD +57 -54
  54. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/WHEEL +0 -0
  55. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/entry_points.txt +0 -0
  56. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/licenses/LICENSE +0 -0
  57. {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 enables
5
- automatic loading of these tools on remote servers via ToolUniverse integration.
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
- ```python
12
- from tooluniverse.mcp_tool_registry import register_mcp_tool, start_mcp_server
13
-
14
- @register_mcp_tool(
15
- tool_type_name="my_analysis_tool",
16
- config={
17
- "description": "Performs custom data analysis"
18
- },
19
- mcp_config={
20
- "server_name": "Custom Analysis Server",
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
- import threading
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 exactly like register_tool, but also expose it as an MCP server.
80
+ Decorator to register a tool class for MCP server exposure.
76
81
 
77
- This decorator does everything that register_tool does, PLUS exposes the tool via SMCP protocol
78
- for remote access. The parameters and behavior are identical to register_tool, with an optional
79
- mcp_config parameter for server configuration.
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 both locally and as MCP server.
107
+ Decorator function that registers the tool class for MCP server only.
101
108
 
102
- Examples:
103
- =========
109
+ Examples
110
+ --------
104
111
 
105
- Same as register_tool, just with MCP exposure:
112
+ MCP tool registration:
106
113
  ```python
107
- @register_mcp_tool('CustomToolName', config={...}, mcp_config={"port": 8001})
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
- # First, do exactly what register_tool does
119
- from .tool_registry import register_tool
120
-
121
- # Apply register_tool decorator to register locally
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
- # Now, additionally register for MCP exposure
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
- "class": cls,
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 does everything register_tool would do
190
- PLUS exposes the tool via MCP.
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 both locally and for MCP
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, description, and port.
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 registered tools.
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
- pass
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
- for port in _mcp_server_configs:
298
- _start_server_for_port(port, **kwargs)
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
- # Keep main thread alive
301
- print("🎯 MCP server(s) started. Press Ctrl+C to stop.")
302
- try:
303
- while True:
304
- time.sleep(1)
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 SMCP server for compatibility
331
- server = _get_smcp()(
332
- name=config["server_name"],
333
- auto_expose_tools=False, # We'll add tools manually
334
- search_enabled=True,
335
- max_workers=config.get("max_workers", 5),
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
- # Add registered tools to the server
354
+ # Register MCP tools using the public API
340
355
  for tool_info in tools:
341
- _add_tool_to_smcp_server(server, tool_info)
342
-
343
- # Store server instance
344
- _mcp_server_instances[port] = server
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
- # Enable stateless mode for MCPAutoLoaderTool compatibility
350
- server.run_simple(
351
- transport=config["transport"],
352
- host=config["host"],
353
- port=port,
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"❌ Error running MCP server on port {port}: {e}")
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 server started on {config['host']}:{port}")
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
- def _add_tool_to_smcp_server(server, tool_info: Dict[str, Any]):
366
- """Add a registered tool to an SMCP server instance by reusing SMCP's proven method."""
367
- name = tool_info["name"]
368
- tool_class = tool_info["class"]
369
- description = tool_info["description"]
370
- schema = tool_info["parameter_schema"]
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
- # Create tool instance for execution
377
- tool_instance = tool_class()
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
- def _create_mcp_tool_from_tooluniverse_with_instance(
411
- server, tool_config: Dict[str, Any], tool_instance
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
- # Debug: Ensure tool_config is a dictionary
423
- if not isinstance(tool_config, dict):
424
- raise ValueError(
425
- f"tool_config must be a dictionary, got {type(tool_config)}: {tool_config}"
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
- return {{
766
- "error": f"Tool execution failed: {{str(e)}}",
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
- except Exception as e:
796
- print(f"❌ Error creating FastMCP function for '{name}': {e}")
797
- print(f"Generated code:\n{func_code}")
798
- raise
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": f"mcp_auto_loader_{url.replace(':', '_').replace('/', '_')}",
527
+ "name": loader_name,
909
528
  "type": "MCPAutoLoaderTool",
910
529
  "server_url": url,
911
530
  "auto_register": True,