tooluniverse 0.2.0__py3-none-any.whl → 1.0.1__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/__init__.py +340 -4
- tooluniverse/admetai_tool.py +84 -0
- tooluniverse/agentic_tool.py +563 -0
- tooluniverse/alphafold_tool.py +96 -0
- tooluniverse/base_tool.py +129 -6
- tooluniverse/boltz_tool.py +207 -0
- tooluniverse/chem_tool.py +192 -0
- tooluniverse/compose_scripts/__init__.py +1 -0
- tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
- tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
- tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
- tooluniverse/compose_scripts/literature_tool.py +34 -0
- tooluniverse/compose_scripts/output_summarizer.py +279 -0
- tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
- tooluniverse/compose_scripts/tool_discover.py +705 -0
- tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
- tooluniverse/compose_tool.py +371 -0
- tooluniverse/ctg_tool.py +1002 -0
- tooluniverse/custom_tool.py +81 -0
- tooluniverse/dailymed_tool.py +108 -0
- tooluniverse/data/admetai_tools.json +155 -0
- tooluniverse/data/adverse_event_tools.json +108 -0
- tooluniverse/data/agentic_tools.json +1156 -0
- tooluniverse/data/alphafold_tools.json +87 -0
- tooluniverse/data/boltz_tools.json +9 -0
- tooluniverse/data/chembl_tools.json +16 -0
- tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
- tooluniverse/data/compose_tools.json +202 -0
- tooluniverse/data/dailymed_tools.json +70 -0
- tooluniverse/data/dataset_tools.json +646 -0
- tooluniverse/data/disease_target_score_tools.json +712 -0
- tooluniverse/data/efo_tools.json +17 -0
- tooluniverse/data/embedding_tools.json +319 -0
- tooluniverse/data/enrichr_tools.json +31 -0
- tooluniverse/data/europe_pmc_tools.json +22 -0
- tooluniverse/data/expert_feedback_tools.json +10 -0
- tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
- tooluniverse/data/fda_drug_labeling_tools.json +1 -1
- tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
- tooluniverse/data/finder_tools.json +209 -0
- tooluniverse/data/gene_ontology_tools.json +113 -0
- tooluniverse/data/gwas_tools.json +1082 -0
- tooluniverse/data/hpa_tools.json +333 -0
- tooluniverse/data/humanbase_tools.json +47 -0
- tooluniverse/data/idmap_tools.json +74 -0
- tooluniverse/data/mcp_client_tools_example.json +113 -0
- tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
- tooluniverse/data/medlineplus_tools.json +141 -0
- tooluniverse/data/monarch_tools.json +1 -1
- tooluniverse/data/openalex_tools.json +36 -0
- tooluniverse/data/opentarget_tools.json +1 -1
- tooluniverse/data/output_summarization_tools.json +101 -0
- tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
- tooluniverse/data/packages/categorized_tools.txt +206 -0
- tooluniverse/data/packages/cheminformatics_tools.json +347 -0
- tooluniverse/data/packages/earth_sciences_tools.json +74 -0
- tooluniverse/data/packages/genomics_tools.json +776 -0
- tooluniverse/data/packages/image_processing_tools.json +38 -0
- tooluniverse/data/packages/machine_learning_tools.json +789 -0
- tooluniverse/data/packages/neuroscience_tools.json +62 -0
- tooluniverse/data/packages/original_tools.txt +0 -0
- tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
- tooluniverse/data/packages/scientific_computing_tools.json +560 -0
- tooluniverse/data/packages/single_cell_tools.json +453 -0
- tooluniverse/data/packages/structural_biology_tools.json +396 -0
- tooluniverse/data/packages/visualization_tools.json +399 -0
- tooluniverse/data/pubchem_tools.json +215 -0
- tooluniverse/data/pubtator_tools.json +68 -0
- tooluniverse/data/rcsb_pdb_tools.json +1332 -0
- tooluniverse/data/reactome_tools.json +19 -0
- tooluniverse/data/semantic_scholar_tools.json +26 -0
- tooluniverse/data/special_tools.json +2 -25
- tooluniverse/data/tool_composition_tools.json +88 -0
- tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
- tooluniverse/data/txagent_client_tools.json +9 -0
- tooluniverse/data/uniprot_tools.json +211 -0
- tooluniverse/data/url_fetch_tools.json +94 -0
- tooluniverse/data/uspto_downloader_tools.json +9 -0
- tooluniverse/data/uspto_tools.json +811 -0
- tooluniverse/data/xml_tools.json +3275 -0
- tooluniverse/dataset_tool.py +296 -0
- tooluniverse/default_config.py +165 -0
- tooluniverse/efo_tool.py +42 -0
- tooluniverse/embedding_database.py +630 -0
- tooluniverse/embedding_sync.py +396 -0
- tooluniverse/enrichr_tool.py +266 -0
- tooluniverse/europe_pmc_tool.py +52 -0
- tooluniverse/execute_function.py +1775 -95
- tooluniverse/extended_hooks.py +444 -0
- tooluniverse/gene_ontology_tool.py +194 -0
- tooluniverse/graphql_tool.py +158 -36
- tooluniverse/gwas_tool.py +358 -0
- tooluniverse/hpa_tool.py +1645 -0
- tooluniverse/humanbase_tool.py +389 -0
- tooluniverse/logging_config.py +254 -0
- tooluniverse/mcp_client_tool.py +764 -0
- tooluniverse/mcp_integration.py +413 -0
- tooluniverse/mcp_tool_registry.py +925 -0
- tooluniverse/medlineplus_tool.py +337 -0
- tooluniverse/openalex_tool.py +228 -0
- tooluniverse/openfda_adv_tool.py +283 -0
- tooluniverse/openfda_tool.py +393 -160
- tooluniverse/output_hook.py +1122 -0
- tooluniverse/package_tool.py +195 -0
- tooluniverse/pubchem_tool.py +158 -0
- tooluniverse/pubtator_tool.py +168 -0
- tooluniverse/rcsb_pdb_tool.py +38 -0
- tooluniverse/reactome_tool.py +108 -0
- tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
- tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
- tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
- tooluniverse/remote/expert_feedback/simple_test.py +23 -0
- tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
- tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
- tooluniverse/remote/immune_compass/compass_tool.py +327 -0
- tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
- tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
- tooluniverse/remote_tool.py +99 -0
- tooluniverse/restful_tool.py +53 -30
- tooluniverse/scripts/generate_tool_graph.py +408 -0
- tooluniverse/scripts/visualize_tool_graph.py +829 -0
- tooluniverse/semantic_scholar_tool.py +62 -0
- tooluniverse/smcp.py +2452 -0
- tooluniverse/smcp_server.py +975 -0
- tooluniverse/test/mcp_server_test.py +0 -0
- tooluniverse/test/test_admetai_tool.py +370 -0
- tooluniverse/test/test_agentic_tool.py +129 -0
- tooluniverse/test/test_alphafold_tool.py +71 -0
- tooluniverse/test/test_chem_tool.py +37 -0
- tooluniverse/test/test_compose_lieraturereview.py +63 -0
- tooluniverse/test/test_compose_tool.py +448 -0
- tooluniverse/test/test_dailymed.py +69 -0
- tooluniverse/test/test_dataset_tool.py +200 -0
- tooluniverse/test/test_disease_target_score.py +56 -0
- tooluniverse/test/test_drugbank_filter_examples.py +179 -0
- tooluniverse/test/test_efo.py +31 -0
- tooluniverse/test/test_enrichr_tool.py +21 -0
- tooluniverse/test/test_europe_pmc_tool.py +20 -0
- tooluniverse/test/test_fda_adv.py +95 -0
- tooluniverse/test/test_fda_drug_labeling.py +91 -0
- tooluniverse/test/test_gene_ontology_tools.py +66 -0
- tooluniverse/test/test_gwas_tool.py +139 -0
- tooluniverse/test/test_hpa.py +625 -0
- tooluniverse/test/test_humanbase_tool.py +20 -0
- tooluniverse/test/test_idmap_tools.py +61 -0
- tooluniverse/test/test_mcp_server.py +211 -0
- tooluniverse/test/test_mcp_tool.py +247 -0
- tooluniverse/test/test_medlineplus.py +220 -0
- tooluniverse/test/test_openalex_tool.py +32 -0
- tooluniverse/test/test_opentargets.py +28 -0
- tooluniverse/test/test_pubchem_tool.py +116 -0
- tooluniverse/test/test_pubtator_tool.py +37 -0
- tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
- tooluniverse/test/test_reactome.py +54 -0
- tooluniverse/test/test_semantic_scholar_tool.py +24 -0
- tooluniverse/test/test_software_tools.py +147 -0
- tooluniverse/test/test_tool_description_optimizer.py +49 -0
- tooluniverse/test/test_tool_finder.py +26 -0
- tooluniverse/test/test_tool_finder_llm.py +252 -0
- tooluniverse/test/test_tools_find.py +195 -0
- tooluniverse/test/test_uniprot_tools.py +74 -0
- tooluniverse/test/test_uspto_tool.py +72 -0
- tooluniverse/test/test_xml_tool.py +113 -0
- tooluniverse/tool_finder_embedding.py +267 -0
- tooluniverse/tool_finder_keyword.py +693 -0
- tooluniverse/tool_finder_llm.py +699 -0
- tooluniverse/tool_graph_web_ui.py +955 -0
- tooluniverse/tool_registry.py +416 -0
- tooluniverse/uniprot_tool.py +155 -0
- tooluniverse/url_tool.py +253 -0
- tooluniverse/uspto_tool.py +240 -0
- tooluniverse/utils.py +369 -41
- tooluniverse/xml_tool.py +369 -0
- tooluniverse-1.0.1.dist-info/METADATA +387 -0
- tooluniverse-1.0.1.dist-info/RECORD +182 -0
- tooluniverse-1.0.1.dist-info/entry_points.txt +9 -0
- tooluniverse/generate_mcp_tools.py +0 -113
- tooluniverse/mcp_server.py +0 -3340
- tooluniverse-0.2.0.dist-info/METADATA +0 -139
- tooluniverse-0.2.0.dist-info/RECORD +0 -21
- tooluniverse-0.2.0.dist-info/entry_points.txt +0 -4
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/WHEEL +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.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!")
|