tooluniverse 0.2.0__py3-none-any.whl → 1.0.0__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 (190) hide show
  1. tooluniverse/__init__.py +340 -4
  2. tooluniverse/admetai_tool.py +84 -0
  3. tooluniverse/agentic_tool.py +563 -0
  4. tooluniverse/alphafold_tool.py +96 -0
  5. tooluniverse/base_tool.py +129 -6
  6. tooluniverse/boltz_tool.py +207 -0
  7. tooluniverse/chem_tool.py +192 -0
  8. tooluniverse/compose_scripts/__init__.py +1 -0
  9. tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
  10. tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
  11. tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
  12. tooluniverse/compose_scripts/literature_tool.py +34 -0
  13. tooluniverse/compose_scripts/output_summarizer.py +279 -0
  14. tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
  15. tooluniverse/compose_scripts/tool_discover.py +705 -0
  16. tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
  17. tooluniverse/compose_tool.py +371 -0
  18. tooluniverse/ctg_tool.py +1002 -0
  19. tooluniverse/custom_tool.py +81 -0
  20. tooluniverse/dailymed_tool.py +108 -0
  21. tooluniverse/data/admetai_tools.json +155 -0
  22. tooluniverse/data/agentic_tools.json +1156 -0
  23. tooluniverse/data/alphafold_tools.json +87 -0
  24. tooluniverse/data/boltz_tools.json +9 -0
  25. tooluniverse/data/chembl_tools.json +16 -0
  26. tooluniverse/data/clait_tools.json +108 -0
  27. tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
  28. tooluniverse/data/compose_tools.json +202 -0
  29. tooluniverse/data/dailymed_tools.json +70 -0
  30. tooluniverse/data/dataset_tools.json +646 -0
  31. tooluniverse/data/disease_target_score_tools.json +712 -0
  32. tooluniverse/data/efo_tools.json +17 -0
  33. tooluniverse/data/embedding_tools.json +319 -0
  34. tooluniverse/data/enrichr_tools.json +31 -0
  35. tooluniverse/data/europe_pmc_tools.json +22 -0
  36. tooluniverse/data/expert_feedback_tools.json +10 -0
  37. tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
  38. tooluniverse/data/fda_drug_labeling_tools.json +1 -1
  39. tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
  40. tooluniverse/data/finder_tools.json +209 -0
  41. tooluniverse/data/gene_ontology_tools.json +113 -0
  42. tooluniverse/data/gwas_tools.json +1082 -0
  43. tooluniverse/data/hpa_tools.json +333 -0
  44. tooluniverse/data/humanbase_tools.json +47 -0
  45. tooluniverse/data/idmap_tools.json +74 -0
  46. tooluniverse/data/mcp_client_tools_example.json +113 -0
  47. tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
  48. tooluniverse/data/medlineplus_tools.json +141 -0
  49. tooluniverse/data/monarch_tools.json +1 -1
  50. tooluniverse/data/openalex_tools.json +36 -0
  51. tooluniverse/data/opentarget_tools.json +1 -1
  52. tooluniverse/data/output_summarization_tools.json +101 -0
  53. tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
  54. tooluniverse/data/packages/categorized_tools.txt +206 -0
  55. tooluniverse/data/packages/cheminformatics_tools.json +347 -0
  56. tooluniverse/data/packages/earth_sciences_tools.json +74 -0
  57. tooluniverse/data/packages/genomics_tools.json +776 -0
  58. tooluniverse/data/packages/image_processing_tools.json +38 -0
  59. tooluniverse/data/packages/machine_learning_tools.json +789 -0
  60. tooluniverse/data/packages/neuroscience_tools.json +62 -0
  61. tooluniverse/data/packages/original_tools.txt +0 -0
  62. tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
  63. tooluniverse/data/packages/scientific_computing_tools.json +560 -0
  64. tooluniverse/data/packages/single_cell_tools.json +453 -0
  65. tooluniverse/data/packages/software_tools.json +4954 -0
  66. tooluniverse/data/packages/structural_biology_tools.json +396 -0
  67. tooluniverse/data/packages/visualization_tools.json +399 -0
  68. tooluniverse/data/pubchem_tools.json +215 -0
  69. tooluniverse/data/pubtator_tools.json +68 -0
  70. tooluniverse/data/rcsb_pdb_tools.json +1332 -0
  71. tooluniverse/data/reactome_tools.json +19 -0
  72. tooluniverse/data/semantic_scholar_tools.json +26 -0
  73. tooluniverse/data/special_tools.json +2 -25
  74. tooluniverse/data/tool_composition_tools.json +88 -0
  75. tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
  76. tooluniverse/data/txagent_client_tools.json +9 -0
  77. tooluniverse/data/uniprot_tools.json +211 -0
  78. tooluniverse/data/url_fetch_tools.json +94 -0
  79. tooluniverse/data/uspto_downloader_tools.json +9 -0
  80. tooluniverse/data/uspto_tools.json +811 -0
  81. tooluniverse/data/xml_tools.json +3275 -0
  82. tooluniverse/dataset_tool.py +296 -0
  83. tooluniverse/default_config.py +165 -0
  84. tooluniverse/efo_tool.py +42 -0
  85. tooluniverse/embedding_database.py +630 -0
  86. tooluniverse/embedding_sync.py +396 -0
  87. tooluniverse/enrichr_tool.py +266 -0
  88. tooluniverse/europe_pmc_tool.py +52 -0
  89. tooluniverse/execute_function.py +1775 -95
  90. tooluniverse/extended_hooks.py +444 -0
  91. tooluniverse/gene_ontology_tool.py +194 -0
  92. tooluniverse/graphql_tool.py +158 -36
  93. tooluniverse/gwas_tool.py +358 -0
  94. tooluniverse/hpa_tool.py +1645 -0
  95. tooluniverse/humanbase_tool.py +389 -0
  96. tooluniverse/logging_config.py +254 -0
  97. tooluniverse/mcp_client_tool.py +764 -0
  98. tooluniverse/mcp_integration.py +413 -0
  99. tooluniverse/mcp_tool_registry.py +925 -0
  100. tooluniverse/medlineplus_tool.py +337 -0
  101. tooluniverse/openalex_tool.py +228 -0
  102. tooluniverse/openfda_adv_tool.py +283 -0
  103. tooluniverse/openfda_tool.py +393 -160
  104. tooluniverse/output_hook.py +1122 -0
  105. tooluniverse/package_tool.py +195 -0
  106. tooluniverse/pubchem_tool.py +158 -0
  107. tooluniverse/pubtator_tool.py +168 -0
  108. tooluniverse/rcsb_pdb_tool.py +38 -0
  109. tooluniverse/reactome_tool.py +108 -0
  110. tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
  111. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
  112. tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
  113. tooluniverse/remote/expert_feedback/simple_test.py +23 -0
  114. tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
  115. tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
  116. tooluniverse/remote/expert_feedback_mcp/human_expert_mcp_server.py +1611 -0
  117. tooluniverse/remote/expert_feedback_mcp/simple_test.py +34 -0
  118. tooluniverse/remote/expert_feedback_mcp/start_web_interface.py +91 -0
  119. tooluniverse/remote/immune_compass/compass_tool.py +327 -0
  120. tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
  121. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
  122. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
  123. tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
  124. tooluniverse/remote_tool.py +99 -0
  125. tooluniverse/restful_tool.py +53 -30
  126. tooluniverse/scripts/generate_tool_graph.py +408 -0
  127. tooluniverse/scripts/visualize_tool_graph.py +829 -0
  128. tooluniverse/semantic_scholar_tool.py +62 -0
  129. tooluniverse/smcp.py +2452 -0
  130. tooluniverse/smcp_server.py +975 -0
  131. tooluniverse/test/mcp_server_test.py +0 -0
  132. tooluniverse/test/test_admetai_tool.py +370 -0
  133. tooluniverse/test/test_agentic_tool.py +129 -0
  134. tooluniverse/test/test_alphafold_tool.py +71 -0
  135. tooluniverse/test/test_chem_tool.py +37 -0
  136. tooluniverse/test/test_compose_lieraturereview.py +63 -0
  137. tooluniverse/test/test_compose_tool.py +448 -0
  138. tooluniverse/test/test_dailymed.py +69 -0
  139. tooluniverse/test/test_dataset_tool.py +200 -0
  140. tooluniverse/test/test_disease_target_score.py +56 -0
  141. tooluniverse/test/test_drugbank_filter_examples.py +179 -0
  142. tooluniverse/test/test_efo.py +31 -0
  143. tooluniverse/test/test_enrichr_tool.py +21 -0
  144. tooluniverse/test/test_europe_pmc_tool.py +20 -0
  145. tooluniverse/test/test_fda_adv.py +95 -0
  146. tooluniverse/test/test_fda_drug_labeling.py +91 -0
  147. tooluniverse/test/test_gene_ontology_tools.py +66 -0
  148. tooluniverse/test/test_gwas_tool.py +139 -0
  149. tooluniverse/test/test_hpa.py +625 -0
  150. tooluniverse/test/test_humanbase_tool.py +20 -0
  151. tooluniverse/test/test_idmap_tools.py +61 -0
  152. tooluniverse/test/test_mcp_server.py +211 -0
  153. tooluniverse/test/test_mcp_tool.py +247 -0
  154. tooluniverse/test/test_medlineplus.py +220 -0
  155. tooluniverse/test/test_openalex_tool.py +32 -0
  156. tooluniverse/test/test_opentargets.py +28 -0
  157. tooluniverse/test/test_pubchem_tool.py +116 -0
  158. tooluniverse/test/test_pubtator_tool.py +37 -0
  159. tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
  160. tooluniverse/test/test_reactome.py +54 -0
  161. tooluniverse/test/test_semantic_scholar_tool.py +24 -0
  162. tooluniverse/test/test_software_tools.py +147 -0
  163. tooluniverse/test/test_tool_description_optimizer.py +49 -0
  164. tooluniverse/test/test_tool_finder.py +26 -0
  165. tooluniverse/test/test_tool_finder_llm.py +252 -0
  166. tooluniverse/test/test_tools_find.py +195 -0
  167. tooluniverse/test/test_uniprot_tools.py +74 -0
  168. tooluniverse/test/test_uspto_tool.py +72 -0
  169. tooluniverse/test/test_xml_tool.py +113 -0
  170. tooluniverse/tool_finder_embedding.py +267 -0
  171. tooluniverse/tool_finder_keyword.py +693 -0
  172. tooluniverse/tool_finder_llm.py +699 -0
  173. tooluniverse/tool_graph_web_ui.py +955 -0
  174. tooluniverse/tool_registry.py +416 -0
  175. tooluniverse/uniprot_tool.py +155 -0
  176. tooluniverse/url_tool.py +253 -0
  177. tooluniverse/uspto_tool.py +240 -0
  178. tooluniverse/utils.py +369 -41
  179. tooluniverse/xml_tool.py +369 -0
  180. tooluniverse-1.0.0.dist-info/METADATA +377 -0
  181. tooluniverse-1.0.0.dist-info/RECORD +186 -0
  182. tooluniverse-1.0.0.dist-info/entry_points.txt +9 -0
  183. tooluniverse/generate_mcp_tools.py +0 -113
  184. tooluniverse/mcp_server.py +0 -3340
  185. tooluniverse-0.2.0.dist-info/METADATA +0 -139
  186. tooluniverse-0.2.0.dist-info/RECORD +0 -21
  187. tooluniverse-0.2.0.dist-info/entry_points.txt +0 -4
  188. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/WHEEL +0 -0
  189. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/licenses/LICENSE +0 -0
  190. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,925 @@
1
+ """
2
+ MCP Tool Registration System for ToolUniverse
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.
6
+
7
+ Usage:
8
+ ======
9
+
10
+ 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",
21
+ "host": "0.0.0.0",
22
+ "port": 8001
23
+ }
24
+ )
25
+ class MyAnalysisTool:
26
+ def run(self, arguments):
27
+ return {"result": "analysis complete"}
28
+
29
+ # Start MCP server with registered tools
30
+ start_mcp_server()
31
+ ```
32
+
33
+ Client Side (Tool Consumer):
34
+ ```python
35
+ from tooluniverse import ToolUniverse
36
+
37
+ # Auto-discover and load MCP tools from remote servers
38
+ tu = ToolUniverse()
39
+ tu.load_mcp_tools(server_urls=["http://localhost:8001"])
40
+
41
+ # Use the remote tool
42
+ result = tu.run_tool("my_analysis_tool", {"data": "input"})
43
+ ```
44
+ """
45
+
46
+ import json
47
+ import asyncio
48
+ from typing import Dict, Any, List, Optional
49
+ import threading
50
+
51
+
52
+ # Import SMCP and ToolUniverse dynamically to avoid circular imports
53
+ def _get_smcp():
54
+ """Get SMCP class with delayed import to avoid circular import"""
55
+ from tooluniverse import SMCP
56
+
57
+ return SMCP
58
+
59
+
60
+ def _get_tooluniverse():
61
+ """Get ToolUniverse class with delayed import to avoid circular import"""
62
+ from tooluniverse import ToolUniverse
63
+
64
+ return ToolUniverse
65
+
66
+
67
+ # Global MCP tool registry
68
+ _mcp_tool_registry: Dict[str, Any] = {}
69
+ _mcp_server_configs: Dict[int, Dict[str, Any]] = {}
70
+ _mcp_server_instances: Dict[int, Any] = {}
71
+
72
+
73
+ def register_mcp_tool(tool_type_name=None, config=None, mcp_config=None):
74
+ """
75
+ Decorator to register a tool class exactly like register_tool, but also expose it as an MCP server.
76
+
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.
80
+
81
+ Parameters:
82
+ ===========
83
+ tool_type_name : str, optional
84
+ Custom name for the tool type. Same as register_tool.
85
+
86
+ config : dict, optional
87
+ Tool configuration dictionary. Same as register_tool.
88
+
89
+ mcp_config : dict, optional
90
+ Additional MCP server configuration. Can include:
91
+ - server_name: Name of the MCP server
92
+ - host: Server host (default: "localhost")
93
+ - port: Server port (default: 8000)
94
+ - transport: "http" or "stdio" (default: "http")
95
+ - auto_start: Whether to auto-start server when tool is registered
96
+
97
+ Returns:
98
+ ========
99
+ function
100
+ Decorator function that registers the tool class both locally and as MCP server.
101
+
102
+ Examples:
103
+ =========
104
+
105
+ Same as register_tool, just with MCP exposure:
106
+ ```python
107
+ @register_mcp_tool('CustomToolName', config={...}, mcp_config={"port": 8001})
108
+ class MyTool:
109
+ pass
110
+
111
+ @register_mcp_tool() # Uses class name, default MCP config
112
+ class AnotherTool:
113
+ pass
114
+ ```
115
+ """
116
+
117
+ 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
122
+ registered_cls = register_tool(tool_type_name, config)(cls)
123
+
124
+ # Now, additionally register for MCP exposure
125
+ tool_name = tool_type_name or cls.__name__
126
+ tool_config = config or {}
127
+ tool_description = (
128
+ tool_config.get("description")
129
+ or (cls.__doc__ or f"Tool: {tool_name}").strip()
130
+ )
131
+
132
+ # Create default parameter schema if not provided
133
+ tool_schema = tool_config.get("parameter_schema") or {
134
+ "type": "object",
135
+ "properties": {
136
+ "arguments": {"type": "object", "description": "Tool arguments"}
137
+ },
138
+ }
139
+
140
+ # Default MCP server configuration
141
+ default_mcp_config = {
142
+ "server_name": f"MCP Server for {tool_name}",
143
+ "host": "localhost",
144
+ "port": 8000,
145
+ "transport": "http",
146
+ "auto_start": False,
147
+ "max_workers": 5,
148
+ }
149
+
150
+ # Merge with provided mcp_config
151
+ server_config = {**default_mcp_config, **(mcp_config or {})}
152
+
153
+ # Register for MCP exposure
154
+ tool_info = {
155
+ "name": tool_name,
156
+ "class": cls,
157
+ "description": tool_description,
158
+ "parameter_schema": tool_schema,
159
+ "server_config": server_config,
160
+ "tool_config": tool_config,
161
+ }
162
+
163
+ _mcp_tool_registry[tool_name] = tool_info
164
+
165
+ # Register server config by port to group tools on same server
166
+ port = server_config["port"]
167
+ if port not in _mcp_server_configs:
168
+ _mcp_server_configs[port] = {"config": server_config, "tools": []}
169
+ _mcp_server_configs[port]["tools"].append(tool_info)
170
+
171
+ print(f"✅ Registered MCP tool: {tool_name} (server port: {port})")
172
+
173
+ # Auto-start server if requested
174
+ auto_start = server_config.get("auto_start", False)
175
+ if auto_start:
176
+ start_mcp_server_for_tool(tool_name)
177
+
178
+ return registered_cls
179
+
180
+ return decorator
181
+
182
+
183
+ def register_mcp_tool_from_config(tool_class: type, config: Dict[str, Any]):
184
+ """
185
+ Register an existing tool class as MCP tool using configuration.
186
+
187
+ This function provides a programmatic way to register tools as MCP tools
188
+ 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.
191
+
192
+ Parameters:
193
+ ===========
194
+ tool_class : type
195
+ The tool class to register
196
+ config : dict
197
+ Configuration containing:
198
+ - name: Tool name (required)
199
+ - description: Tool description
200
+ - parameter_schema: JSON schema for parameters
201
+ - mcp_config: MCP server configuration
202
+
203
+ Examples:
204
+ =========
205
+ ```python
206
+ class ExistingTool:
207
+ def run(self, arguments):
208
+ return {"status": "processed"}
209
+
210
+ register_mcp_tool_from_config(ExistingTool, {
211
+ "name": "existing_tool",
212
+ "description": "An existing tool exposed via MCP",
213
+ "mcp_config": {"port": 8002}
214
+ })
215
+ ```
216
+ """
217
+ name = config.get("name") or tool_class.__name__
218
+ tool_config = {k: v for k, v in config.items() if k != "mcp_config"}
219
+ mcp_config = config.get("mcp_config", {})
220
+
221
+ # Use the decorator to register both locally and for MCP
222
+ register_mcp_tool(tool_type_name=name, config=tool_config, mcp_config=mcp_config)(
223
+ tool_class
224
+ )
225
+
226
+
227
+ def get_mcp_tool_registry() -> Dict[str, Any]:
228
+ """Get the current MCP tool registry."""
229
+ return _mcp_tool_registry.copy()
230
+
231
+
232
+ def get_registered_tools() -> List[Dict[str, Any]]:
233
+ """
234
+ Get a list of all registered MCP tools with their information.
235
+
236
+ Returns:
237
+ List of dictionaries containing tool information including name, description, and port.
238
+ """
239
+ tools = []
240
+ for tool_name, tool_info in _mcp_tool_registry.items():
241
+ tools.append(
242
+ {
243
+ "name": tool_name,
244
+ "description": tool_info["description"],
245
+ "port": tool_info["server_config"]["port"],
246
+ "class": tool_info["class"].__name__,
247
+ }
248
+ )
249
+ return tools
250
+
251
+
252
+ def get_mcp_server_configs() -> Dict[int, Dict[str, Any]]:
253
+ """Get the current MCP server configurations grouped by port."""
254
+ return _mcp_server_configs.copy()
255
+
256
+
257
+ def start_mcp_server(port: Optional[int] = None, **kwargs):
258
+ """
259
+ Start MCP server(s) for registered tools.
260
+
261
+ Parameters:
262
+ ===========
263
+ port : int, optional
264
+ Specific port to start server for. If None, starts servers for all registered tools.
265
+ **kwargs
266
+ Additional arguments passed to SMCP server
267
+
268
+ Examples:
269
+ =========
270
+ ```python
271
+ # Start server for specific port
272
+ start_mcp_server(port=8001)
273
+
274
+ # Start all servers
275
+ start_mcp_server()
276
+
277
+ # Start with custom configuration
278
+ start_mcp_server(max_workers=20, debug=True)
279
+ ```
280
+ """
281
+ import time
282
+
283
+ try:
284
+ pass
285
+ except ImportError:
286
+ print("❌ SMCP not available. Cannot start MCP server.")
287
+ return
288
+
289
+ if port is not None:
290
+ # Start server for specific port
291
+ if port in _mcp_server_configs:
292
+ _start_server_for_port(port, **kwargs)
293
+ else:
294
+ print(f"❌ No tools registered for port {port}")
295
+ else:
296
+ # Start servers for all registered ports
297
+ for port in _mcp_server_configs:
298
+ _start_server_for_port(port, **kwargs)
299
+
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.")
316
+
317
+
318
+ def _start_server_for_port(port: int, **kwargs):
319
+ """Start SMCP server for tools on a specific port."""
320
+ if port in _mcp_server_instances:
321
+ print(f"🔄 MCP server already running on port {port}")
322
+ return
323
+
324
+ server_info = _mcp_server_configs[port]
325
+ config = server_info["config"]
326
+ tools = server_info["tools"]
327
+
328
+ print(f"🚀 Starting MCP server on port {port} with {len(tools)} tools...")
329
+
330
+ # Create SMCP server with stateless mode 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
+ stateless_http=True, # Enable stateless mode for MCPAutoLoaderTool compatibility
337
+ **kwargs,
338
+ )
339
+
340
+ # Add registered tools to the server
341
+ for tool_info in tools:
342
+ _add_tool_to_smcp_server(server, tool_info)
343
+
344
+ # Store server instance
345
+ _mcp_server_instances[port] = server
346
+
347
+ # Start server in background thread
348
+ def run_server():
349
+ try:
350
+ server.run_simple(
351
+ transport=config["transport"], host=config["host"], port=port
352
+ )
353
+ except Exception as e:
354
+ print(f"❌ Error running MCP server on port {port}: {e}")
355
+
356
+ server_thread = threading.Thread(target=run_server, daemon=True)
357
+ server_thread.start()
358
+
359
+ print(f"✅ MCP server started on {config['host']}:{port}")
360
+
361
+
362
+ def _add_tool_to_smcp_server(server, tool_info: Dict[str, Any]):
363
+ """Add a registered tool to an SMCP server instance by reusing SMCP's proven method."""
364
+ name = tool_info["name"]
365
+ tool_class = tool_info["class"]
366
+ description = tool_info["description"]
367
+ schema = tool_info["parameter_schema"]
368
+
369
+ print(
370
+ f"🔧 Adding tool '{name}' using SMCP's _create_mcp_tool_from_tooluniverse approach..."
371
+ )
372
+
373
+ # Create tool instance for execution
374
+ tool_instance = tool_class()
375
+
376
+ # Convert our tool_info to the format expected by SMCP's method
377
+ # SMCP expects tool_config with 'name', 'description', and 'parameter' fields
378
+ tool_config = {
379
+ "name": name,
380
+ "description": description,
381
+ "parameter": schema, # SMCP expects 'parameter' not 'parameter_schema'
382
+ }
383
+
384
+ # Check if the server has the SMCP method available
385
+ if hasattr(server, "_create_mcp_tool_from_tooluniverse"):
386
+ print("✅ Using server's _create_mcp_tool_from_tooluniverse method")
387
+ # Temporarily store our tool instance so SMCP's method can access it
388
+ # We need to modify SMCP's approach to use our tool_instance instead of tooluniverse
389
+ server._temp_tool_instance = tool_instance
390
+
391
+ # Create a modified version of SMCP's approach
392
+ _create_mcp_tool_from_tooluniverse_with_instance(
393
+ server, tool_config, tool_instance
394
+ )
395
+ else:
396
+ print(
397
+ "⚠️ Server doesn't have _create_mcp_tool_from_tooluniverse, using fallback"
398
+ )
399
+ # Fallback to standard method
400
+ server.add_custom_tool(
401
+ name=name,
402
+ function=lambda arguments="{}": tool_instance.run(json.loads(arguments)),
403
+ description=description,
404
+ )
405
+
406
+
407
+ def _create_mcp_tool_from_tooluniverse_with_instance(
408
+ server, tool_config: Dict[str, Any], tool_instance
409
+ ):
410
+ """
411
+ Create an MCP tool from a ToolUniverse tool configuration using a tool instance.
412
+
413
+ This method reuses the proven approach from SMCP's _create_mcp_tool_from_tooluniverse
414
+ method, but adapts it to work with tool instances instead of ToolUniverse.
415
+ It creates functions with proper parameter signatures that match the ToolUniverse
416
+ tool schema, enabling FastMCP's automatic parameter validation.
417
+ """
418
+ try:
419
+ # Debug: Ensure tool_config is a dictionary
420
+ if not isinstance(tool_config, dict):
421
+ raise ValueError(
422
+ f"tool_config must be a dictionary, got {type(tool_config)}: {tool_config}"
423
+ )
424
+
425
+ tool_name = tool_config["name"]
426
+ description = tool_config.get("description", f"Tool: {tool_name}")
427
+ parameters = tool_config.get("parameter", {})
428
+
429
+ # Extract parameter information from the schema
430
+ # Handle case where properties might be None (like in Finish tool)
431
+ properties = parameters.get("properties")
432
+ if properties is None:
433
+ properties = {}
434
+ required_params = parameters.get("required", [])
435
+
436
+ # Handle non-standard schema format where 'required' is set on individual properties
437
+ # instead of at the object level (common in ToolUniverse schemas)
438
+ if not required_params and properties:
439
+ required_params = [
440
+ param_name
441
+ for param_name, param_info in properties.items()
442
+ if param_info.get("required", False)
443
+ ]
444
+
445
+ # Build function signature dynamically with Pydantic Field support
446
+ import inspect
447
+ from typing import Union
448
+
449
+ try:
450
+ from typing import Annotated
451
+ from pydantic import Field
452
+
453
+ PYDANTIC_AVAILABLE = True
454
+ except ImportError:
455
+ PYDANTIC_AVAILABLE = False
456
+
457
+ # Create parameter signature for the function
458
+ func_params = []
459
+ param_annotations = {}
460
+
461
+ for param_name, param_info in properties.items():
462
+ param_type = param_info.get("type", "string")
463
+ param_description = param_info.get("description", f"{param_name} parameter")
464
+ is_required = param_name in required_params
465
+
466
+ # Map JSON schema types to Python types
467
+ python_type: type
468
+ if param_type == "string":
469
+ python_type = str
470
+ elif param_type == "integer":
471
+ python_type = int
472
+ elif param_type == "number":
473
+ python_type = float
474
+ elif param_type == "boolean":
475
+ python_type = bool
476
+ elif param_type == "array":
477
+ python_type = list
478
+ elif param_type == "object":
479
+ python_type = dict
480
+ else:
481
+ python_type = str # Default to string for unknown types
482
+
483
+ # Create proper type annotation
484
+ if PYDANTIC_AVAILABLE:
485
+ # Use Pydantic Field for enhanced schema information
486
+ field_kwargs = {"description": param_description}
487
+ pydantic_field = Field(**field_kwargs)
488
+
489
+ if is_required:
490
+ annotated_type: Any = Annotated[python_type, pydantic_field]
491
+ param_annotations[param_name] = annotated_type
492
+ func_params.append(
493
+ inspect.Parameter(
494
+ param_name,
495
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
496
+ annotation=annotated_type,
497
+ )
498
+ )
499
+ else:
500
+ optional_annotated_type: Any = Annotated[
501
+ Union[python_type, type(None)], pydantic_field
502
+ ]
503
+ param_annotations[param_name] = optional_annotated_type
504
+ func_params.append(
505
+ inspect.Parameter(
506
+ param_name,
507
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
508
+ default=None,
509
+ annotation=optional_annotated_type,
510
+ )
511
+ )
512
+ else:
513
+ # Fallback without Pydantic
514
+ if is_required:
515
+ param_annotations[param_name] = python_type
516
+ func_params.append(
517
+ inspect.Parameter(
518
+ param_name,
519
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
520
+ annotation=python_type,
521
+ )
522
+ )
523
+ else:
524
+ param_annotations[param_name] = Union[python_type, type(None)]
525
+ func_params.append(
526
+ inspect.Parameter(
527
+ param_name,
528
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
529
+ default=None,
530
+ annotation=Union[python_type, type(None)],
531
+ )
532
+ )
533
+
534
+ # Create the async function with dynamic signature
535
+ if not properties:
536
+ # Tool has no parameters - create simple function
537
+ async def dynamic_tool_function() -> str:
538
+ """Execute tool with no arguments."""
539
+ try:
540
+ # Execute our custom tool instance
541
+ result = tool_instance.run({})
542
+
543
+ # Format the result
544
+ if isinstance(result, str):
545
+ return result
546
+ else:
547
+ return json.dumps(result, indent=2, default=str)
548
+
549
+ except Exception as e:
550
+ error_msg = f"Error executing {tool_name}: {str(e)}"
551
+ print(f"❌ {error_msg}")
552
+ return json.dumps({"error": error_msg}, indent=2)
553
+
554
+ # Set function metadata
555
+ dynamic_tool_function.__name__ = tool_name
556
+ dynamic_tool_function.__signature__ = inspect.Signature([])
557
+ dynamic_tool_function.__annotations__ = {"return": str}
558
+
559
+ else:
560
+ # Tool has parameters - create function with dynamic signature
561
+ async def dynamic_tool_function(**kwargs) -> str:
562
+ """Execute tool with provided arguments."""
563
+ try:
564
+ # Filter out None values for optional parameters
565
+ args_dict = {k: v for k, v in kwargs.items() if v is not None}
566
+
567
+ # Validate required parameters
568
+ missing_required = [
569
+ param for param in required_params if param not in args_dict
570
+ ]
571
+ if missing_required:
572
+ return json.dumps(
573
+ {
574
+ "error": f"Missing required parameters: {missing_required}",
575
+ "required": required_params,
576
+ "provided": list(args_dict.keys()),
577
+ },
578
+ indent=2,
579
+ )
580
+
581
+ # Execute our custom tool instance instead of tooluniverse
582
+ result = tool_instance.run(args_dict)
583
+
584
+ # Format the result
585
+ if isinstance(result, str):
586
+ return result
587
+ else:
588
+ return json.dumps(result, indent=2, default=str)
589
+
590
+ except Exception as e:
591
+ error_msg = f"Error executing {tool_name}: {str(e)}"
592
+ print(f"❌ {error_msg}")
593
+ return json.dumps({"error": error_msg}, indent=2)
594
+
595
+ # Set function metadata
596
+ dynamic_tool_function.__name__ = tool_name
597
+
598
+ # Set function signature dynamically for tools with parameters
599
+ if func_params:
600
+ dynamic_tool_function.__signature__ = inspect.Signature(func_params)
601
+
602
+ # Set annotations for type hints
603
+ dynamic_tool_function.__annotations__ = param_annotations.copy()
604
+ dynamic_tool_function.__annotations__["return"] = str
605
+
606
+ # Create detailed docstring
607
+ param_docs = []
608
+ for param_name, param_info in properties.items():
609
+ param_desc = param_info.get("description", f"{param_name} parameter")
610
+ param_type = param_info.get("type", "string")
611
+ is_required = param_name in required_params
612
+ required_text = "required" if is_required else "optional"
613
+ param_docs.append(
614
+ f" {param_name} ({param_type}, {required_text}): {param_desc}"
615
+ )
616
+
617
+ # Set function docstring
618
+ dynamic_tool_function.__doc__ = f"""{description}
619
+
620
+ Parameters:
621
+ {chr(10).join(param_docs) if param_docs else ' No parameters required'}
622
+
623
+ Returns:
624
+ str: Tool execution result
625
+ """
626
+
627
+ print(f"✅ Created function with {len(func_params)} parameters")
628
+ print("📋 Docstring includes parameter descriptions:")
629
+ for i, doc in enumerate(param_docs[:3], 1): # Show first 3 for brevity
630
+ print(f" {i}. {doc.strip()}")
631
+ if len(param_docs) > 3:
632
+ print(f" ... and {len(param_docs) - 3} more")
633
+
634
+ # Register with FastMCP using the tool decorator approach (following SMCP pattern)
635
+ server.tool(description=description)(dynamic_tool_function)
636
+
637
+ print(
638
+ f"📦 Tool '{tool_name}' registered successfully with parameter descriptions"
639
+ )
640
+
641
+ except Exception as e:
642
+ print(f"❌ Error creating tool from config: {e}")
643
+ import traceback
644
+
645
+ traceback.print_exc()
646
+ # Don't raise - continue with other tools
647
+ return
648
+
649
+
650
+ def _build_fastmcp_tool_function(
651
+ name: str, description: str, schema: Dict[str, Any], tool_instance
652
+ ):
653
+ """
654
+ Build a FastMCP-compatible function with proper docstring and type annotations.
655
+
656
+ FastMCP generates parameter schema from the function signature and docstring,
657
+ so we need to create a function that matches the expected format exactly.
658
+ """
659
+ properties = schema.get("properties", {})
660
+ required = schema.get("required", [])
661
+
662
+ # Build parameter definitions and documentation
663
+ param_definitions = []
664
+ param_names = []
665
+ docstring_params = []
666
+
667
+ for param_name, param_info in properties.items():
668
+ param_type = param_info.get("type", "string")
669
+ param_description = param_info.get("description", f"{param_name} parameter")
670
+ has_default = param_name not in required
671
+ default_value = param_info.get("default", None)
672
+ enum_values = param_info.get("enum", None)
673
+
674
+ # Map JSON schema types to Python types
675
+ if param_type == "string":
676
+ py_type = "str"
677
+ if has_default and default_value is None:
678
+ default_value = '""'
679
+ elif has_default:
680
+ default_value = f'"{default_value}"'
681
+ elif param_type == "integer":
682
+ py_type = "int"
683
+ if has_default and default_value is None:
684
+ default_value = "0"
685
+ elif has_default:
686
+ default_value = str(default_value)
687
+ elif param_type == "number":
688
+ py_type = "float"
689
+ if has_default and default_value is None:
690
+ default_value = "0.0"
691
+ elif has_default:
692
+ default_value = str(default_value)
693
+ elif param_type == "boolean":
694
+ py_type = "bool"
695
+ if has_default and default_value is None:
696
+ default_value = "False"
697
+ elif has_default:
698
+ default_value = str(default_value)
699
+ else:
700
+ py_type = "str" # Default to string
701
+ if has_default and default_value is None:
702
+ default_value = '""'
703
+ elif has_default:
704
+ default_value = f'"{default_value}"'
705
+
706
+ # Build parameter definition for function signature
707
+ if has_default:
708
+ param_def = f"{param_name}: {py_type} = {default_value}"
709
+ else:
710
+ param_def = f"{param_name}: {py_type}"
711
+
712
+ param_definitions.append(param_def)
713
+ param_names.append(param_name)
714
+
715
+ # Build docstring parameter documentation
716
+ param_doc = f" {param_name} ({py_type}): {param_description}"
717
+ if enum_values:
718
+ param_doc += f". Options: {enum_values}"
719
+ if has_default and default_value is not None:
720
+ param_doc += f". Default: {default_value}"
721
+
722
+ docstring_params.append(param_doc)
723
+
724
+ # Create function signature
725
+ params_str = ", ".join(param_definitions)
726
+
727
+ # Create comprehensive docstring following Google style
728
+ # This is critical for FastMCP to extract parameter information
729
+ docstring_parts = [
730
+ f' """{description}',
731
+ "",
732
+ " This tool provides expert consultation functionality.",
733
+ "",
734
+ ]
735
+
736
+ if docstring_params:
737
+ docstring_parts.extend([" Args:", *docstring_params, ""])
738
+
739
+ docstring_parts.extend(
740
+ [
741
+ " Returns:",
742
+ " dict: Tool execution result with status and response data",
743
+ ' """',
744
+ ]
745
+ )
746
+
747
+ docstring = "\n".join(docstring_parts)
748
+
749
+ # Create the function code with comprehensive docstring
750
+ func_code = f"""
751
+ def fastmcp_tool_function({params_str}) -> dict:
752
+ {docstring}
753
+ # Collect all parameters into arguments dict for tool execution
754
+ arguments = {{}}
755
+ {chr(10).join(f' arguments["{pname}"] = {pname}' for pname in param_names)}
756
+
757
+ # Execute the original tool
758
+ try:
759
+ result = tool_instance.run(arguments)
760
+ return result
761
+ except Exception as e:
762
+ return {{
763
+ "error": f"Tool execution failed: {{str(e)}}",
764
+ "status": "error"
765
+ }}
766
+ """
767
+
768
+ # Execute the function definition in a clean namespace
769
+ namespace = {
770
+ "tool_instance": tool_instance,
771
+ "str": str, # Ensure str is available for error handling
772
+ }
773
+
774
+ try:
775
+ exec(func_code, namespace)
776
+ fastmcp_function = namespace["fastmcp_tool_function"]
777
+
778
+ # Verify the function was created correctly
779
+ if not callable(fastmcp_function):
780
+ raise ValueError("Generated function is not callable")
781
+
782
+ # Verify docstring exists
783
+ if not fastmcp_function.__doc__:
784
+ raise ValueError("Generated function has no docstring")
785
+
786
+ print(f"✅ FastMCP function created successfully for '{name}'")
787
+ print(f" Parameters: {len(param_names)} ({', '.join(param_names)})")
788
+ print(f" Docstring length: {len(fastmcp_function.__doc__)} chars")
789
+
790
+ return fastmcp_function
791
+
792
+ except Exception as e:
793
+ print(f"❌ Error creating FastMCP function for '{name}': {e}")
794
+ print(f"Generated code:\n{func_code}")
795
+ raise
796
+
797
+
798
+ def start_mcp_server_for_tool(tool_name: str):
799
+ """Start MCP server for a specific tool."""
800
+ if tool_name not in _mcp_tool_registry:
801
+ print(f"❌ Tool '{tool_name}' not found in MCP registry")
802
+ return
803
+
804
+ tool_info = _mcp_tool_registry[tool_name]
805
+ port = tool_info["server_config"]["port"]
806
+ start_mcp_server(port=port)
807
+
808
+
809
+ def stop_mcp_server(port: Optional[int] = None):
810
+ """
811
+ Stop MCP server(s).
812
+
813
+ Parameters:
814
+ ===========
815
+ port : int, optional
816
+ Specific port to stop server for. If None, stops all servers.
817
+ """
818
+ if port is not None:
819
+ if port in _mcp_server_instances:
820
+ server = _mcp_server_instances[port]
821
+ asyncio.create_task(server.close())
822
+ del _mcp_server_instances[port]
823
+ print(f"🛑 Stopped MCP server on port {port}")
824
+ else:
825
+ print(f"❌ No server running on port {port}")
826
+ else:
827
+ # Stop all servers
828
+ for port in list(_mcp_server_instances.keys()):
829
+ stop_mcp_server(port)
830
+
831
+
832
+ def list_mcp_tools():
833
+ """List all registered MCP tools with their configurations."""
834
+ if not _mcp_tool_registry:
835
+ print("📭 No MCP tools registered")
836
+ return
837
+
838
+ print("📋 Registered MCP Tools:")
839
+ print("=" * 50)
840
+
841
+ for name, tool_info in _mcp_tool_registry.items():
842
+ config = tool_info["server_config"]
843
+ print(f"🔧 {name}")
844
+ print(f" Description: {tool_info['description']}")
845
+ print(f" Class: {tool_info['class'].__name__}")
846
+ print(f" Server: {config['host']}:{config['port']}")
847
+ print(f" Transport: {config['transport']}")
848
+ print()
849
+
850
+
851
+ def get_mcp_tool_urls() -> List[str]:
852
+ """Get list of MCP server URLs for all registered tools."""
853
+ urls = []
854
+ for port, server_info in _mcp_server_configs.items():
855
+ config = server_info["config"]
856
+ if config["transport"] == "http":
857
+ url = f"http://{config['host']}:{port}"
858
+ urls.append(url)
859
+ return urls
860
+
861
+
862
+ # Convenience functions for ToolUniverse integration
863
+ def load_mcp_tools_to_tooluniverse(tu, server_urls: Optional[List[str]] = None):
864
+ """
865
+ Load MCP tools from servers into a ToolUniverse instance.
866
+
867
+ Parameters:
868
+ ===========
869
+ tu : ToolUniverse
870
+ ToolUniverse instance to load tools into
871
+ server_urls : list of str, optional
872
+ List of MCP server URLs. If None, uses all registered local servers.
873
+
874
+ Examples:
875
+ =========
876
+ ```python
877
+ from tooluniverse import ToolUniverse
878
+ from tooluniverse.mcp_tool_registry import load_mcp_tools_to_tooluniverse
879
+
880
+ tu = ToolUniverse()
881
+
882
+ # Load from specific servers
883
+ load_mcp_tools_to_tooluniverse(tu, [
884
+ "http://localhost:8001",
885
+ "http://analysis-server:8002"
886
+ ])
887
+
888
+ # Load from all local registered servers
889
+ load_mcp_tools_to_tooluniverse(tu)
890
+ ```
891
+ """
892
+ if server_urls is None:
893
+ server_urls = get_mcp_tool_urls()
894
+
895
+ if not server_urls:
896
+ print("📭 No MCP servers available to load tools from")
897
+ return
898
+
899
+ print(f"🔄 Loading MCP tools from {len(server_urls)} servers...")
900
+
901
+ for url in server_urls:
902
+ try:
903
+ # Create auto-loader for this server
904
+ loader_config = {
905
+ "name": f"mcp_auto_loader_{url.replace(':', '_').replace('/', '_')}",
906
+ "type": "MCPAutoLoaderTool",
907
+ "server_url": url,
908
+ "auto_register": True,
909
+ "tool_prefix": "mcp_",
910
+ "timeout": 30,
911
+ }
912
+
913
+ # Add auto-loader to ToolUniverse
914
+ tu.register_custom_tool(
915
+ tool_class=None, # Will be loaded by MCPAutoLoaderTool
916
+ tool_type="MCPAutoLoaderTool",
917
+ config=loader_config,
918
+ )
919
+
920
+ print(f"✅ Added MCP auto-loader for {url}")
921
+
922
+ except Exception as e:
923
+ print(f"❌ Failed to load tools from {url}: {e}")
924
+
925
+ print("🎉 MCP tools loading complete!")