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.
- 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/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/clait_tools.json +108 -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/software_tools.json +4954 -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/expert_feedback_mcp/human_expert_mcp_server.py +1611 -0
- tooluniverse/remote/expert_feedback_mcp/simple_test.py +34 -0
- tooluniverse/remote/expert_feedback_mcp/start_web_interface.py +91 -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.0.dist-info/METADATA +377 -0
- tooluniverse-1.0.0.dist-info/RECORD +186 -0
- tooluniverse-1.0.0.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.0.dist-info}/WHEEL +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/top_level.txt +0 -0
tooluniverse/smcp.py
ADDED
|
@@ -0,0 +1,2452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scientific Model Context Protocol (SMCP) - Enhanced MCP Server with ToolUniverse Integration
|
|
3
|
+
|
|
4
|
+
SMCP is a sophisticated MCP (Model Context Protocol) server that bridges the gap between
|
|
5
|
+
AI agents and scientific tools. It seamlessly integrates ToolUniverse's extensive
|
|
6
|
+
collection of 350+ scientific tools with the MCP protocol, enabling AI systems to
|
|
7
|
+
access scientific databases, perform complex analyses, and execute scientific workflows.
|
|
8
|
+
|
|
9
|
+
The SMCP module provides a complete solution for exposing scientific computational
|
|
10
|
+
resources through the standardized MCP protocol, making it easy for AI agents to
|
|
11
|
+
discover, understand, and execute scientific tools in a unified manner.
|
|
12
|
+
|
|
13
|
+
Usage Patterns:
|
|
14
|
+
===============
|
|
15
|
+
|
|
16
|
+
Quick Start:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
# High-performance server with custom configuration
|
|
20
|
+
server = SMCP(
|
|
21
|
+
name="Production Scientific API",
|
|
22
|
+
tool_categories=["uniprot", "ChEMBL", "opentarget", "hpa"],
|
|
23
|
+
max_workers=20,
|
|
24
|
+
search_enabled=True
|
|
25
|
+
)
|
|
26
|
+
server.run_simple(
|
|
27
|
+
transport="http",
|
|
28
|
+
host="0.0.0.0",
|
|
29
|
+
port=7000
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Client Integration:
|
|
34
|
+
```python
|
|
35
|
+
# Using MCP client to discover and use tools
|
|
36
|
+
import json
|
|
37
|
+
|
|
38
|
+
# Discover protein analysis tools
|
|
39
|
+
response = await client.call_tool("find_tools", {
|
|
40
|
+
"query": "protein structure analysis",
|
|
41
|
+
"limit": 5
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
# Use discovered tool
|
|
45
|
+
result = await client.call_tool("UniProt_get_entry_by_accession", {
|
|
46
|
+
"arguments": json.dumps({"accession": "P05067"})
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Architecture:
|
|
51
|
+
=============
|
|
52
|
+
|
|
53
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
54
|
+
│ MCP Client │◄──►│ SMCP │◄──►│ ToolUniverse │
|
|
55
|
+
│ (AI Agent) │ │ Server │ │ (350+ Tools) │
|
|
56
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
57
|
+
│
|
|
58
|
+
▼
|
|
59
|
+
┌──────────────────┐
|
|
60
|
+
│ Scientific │
|
|
61
|
+
│ Databases & │
|
|
62
|
+
│ Services │
|
|
63
|
+
└──────────────────┘
|
|
64
|
+
|
|
65
|
+
The SMCP server acts as an intelligent middleware layer that:
|
|
66
|
+
1. Receives MCP requests from AI agents/clients
|
|
67
|
+
2. Translates requests to ToolUniverse tool calls
|
|
68
|
+
3. Executes tools against scientific databases/services
|
|
69
|
+
4. Returns formatted results via MCP protocol
|
|
70
|
+
5. Provides intelligent tool discovery and recommendation
|
|
71
|
+
|
|
72
|
+
Integration Points:
|
|
73
|
+
==================
|
|
74
|
+
|
|
75
|
+
MCP Protocol Layer:
|
|
76
|
+
- Standard MCP methods (tools/list, tools/call, etc.)
|
|
77
|
+
- Custom scientific methods (tools/find, tools/search)
|
|
78
|
+
- Transport-agnostic communication (stdio, HTTP, SSE)
|
|
79
|
+
- Proper error codes and JSON-RPC 2.0 compliance
|
|
80
|
+
|
|
81
|
+
ToolUniverse Integration:
|
|
82
|
+
- Dynamic tool loading and configuration
|
|
83
|
+
- Schema transformation and validation
|
|
84
|
+
- Execution wrapper with error handling
|
|
85
|
+
- Category-based tool organization
|
|
86
|
+
|
|
87
|
+
AI Agent Interface:
|
|
88
|
+
- Natural language tool discovery
|
|
89
|
+
- Contextual tool recommendations
|
|
90
|
+
- Structured parameter schemas
|
|
91
|
+
- Comprehensive tool documentation
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
import asyncio
|
|
95
|
+
import json
|
|
96
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
97
|
+
from typing import Any, Dict, List, Optional, Union, Callable, Literal
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
from fastmcp import FastMCP
|
|
101
|
+
|
|
102
|
+
FASTMCP_AVAILABLE = True
|
|
103
|
+
except ImportError:
|
|
104
|
+
# Use a simple print here since logging isn't available yet
|
|
105
|
+
print(
|
|
106
|
+
"FastMCP is not available. SMCP is built on top of FastMCP, which is a required dependency."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
from .execute_function import ToolUniverse
|
|
110
|
+
from .logging_config import (
|
|
111
|
+
get_logger,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class SMCP(FastMCP):
|
|
116
|
+
"""
|
|
117
|
+
Scientific Model Context Protocol (SMCP) Server
|
|
118
|
+
|
|
119
|
+
SMCP is an enhanced MCP (Model Context Protocol) server that seamlessly integrates
|
|
120
|
+
ToolUniverse's extensive collection of scientific and scientific tools with the
|
|
121
|
+
FastMCP framework. It provides a unified, AI-accessible interface for scientific
|
|
122
|
+
computing, data analysis, and research workflows.
|
|
123
|
+
|
|
124
|
+
The SMCP server extends standard MCP capabilities with scientific domain expertise,
|
|
125
|
+
intelligent tool discovery, and optimized configurations for research applications.
|
|
126
|
+
It automatically handles the complex task of exposing hundreds of specialized tools
|
|
127
|
+
through a consistent, well-documented interface.
|
|
128
|
+
|
|
129
|
+
Key Features:
|
|
130
|
+
============
|
|
131
|
+
🔬 **Scientific Tool Integration**: Native access to 350+ specialized tools covering
|
|
132
|
+
scientific databases, literature search, clinical data, genomics, proteomics,
|
|
133
|
+
chemical informatics, and AI-powered analysis capabilities.
|
|
134
|
+
|
|
135
|
+
🧠 **AI-Powered Tool Discovery**: Multi-tiered intelligent search system using:
|
|
136
|
+
- ToolFinderLLM: Cost-optimized LLM-based semantic understanding with pre-filtering
|
|
137
|
+
- Tool_RAG: Embedding-based similarity search
|
|
138
|
+
- Keyword Search: Simple text matching as reliable fallback
|
|
139
|
+
|
|
140
|
+
📡 **Full MCP Protocol Support**: Complete implementation of MCP specification with:
|
|
141
|
+
- Standard methods (tools/list, tools/call, resources/*, prompts/*)
|
|
142
|
+
- Custom scientific methods (tools/find, tools/search)
|
|
143
|
+
- Multi-transport support (stdio, HTTP, SSE)
|
|
144
|
+
- JSON-RPC 2.0 compliance with proper error handling
|
|
145
|
+
|
|
146
|
+
⚡ **High-Performance Architecture**: Production-ready features including:
|
|
147
|
+
- Configurable thread pools for concurrent tool execution
|
|
148
|
+
- Intelligent tool loading and caching
|
|
149
|
+
- Resource management and graceful degradation
|
|
150
|
+
- Comprehensive error handling and recovery
|
|
151
|
+
|
|
152
|
+
🔧 **Developer-Friendly**: Simplified configuration and deployment with:
|
|
153
|
+
- Sensible defaults for scientific computing
|
|
154
|
+
- Flexible customization options
|
|
155
|
+
- Comprehensive documentation and examples
|
|
156
|
+
- Built-in diagnostic and monitoring tools
|
|
157
|
+
|
|
158
|
+
Custom MCP Methods:
|
|
159
|
+
==================
|
|
160
|
+
tools/find:
|
|
161
|
+
AI-powered tool discovery using natural language queries. Supports semantic
|
|
162
|
+
search, category filtering, and flexible response formats.
|
|
163
|
+
|
|
164
|
+
tools/search:
|
|
165
|
+
Alternative endpoint for tool discovery with identical functionality to
|
|
166
|
+
tools/find, provided for compatibility and convenience.
|
|
167
|
+
|
|
168
|
+
Parameters:
|
|
169
|
+
===========
|
|
170
|
+
name : str, optional
|
|
171
|
+
Human-readable server name used in logs and identification.
|
|
172
|
+
Default: "SMCP Server"
|
|
173
|
+
Examples: "Scientific Research API", "Drug Discovery Server"
|
|
174
|
+
|
|
175
|
+
tooluniverse_config : ToolUniverse or dict, optional
|
|
176
|
+
Either a pre-configured ToolUniverse instance or configuration dict.
|
|
177
|
+
If None, creates a new ToolUniverse with default settings.
|
|
178
|
+
Allows reuse of existing tool configurations and customizations.
|
|
179
|
+
|
|
180
|
+
tool_categories : list of str, optional
|
|
181
|
+
Specific ToolUniverse categories to load. If None and auto_expose_tools=True,
|
|
182
|
+
loads all available tools. Common combinations:
|
|
183
|
+
- Scientific: ["ChEMBL", "uniprot", "opentarget", "pubchem", "hpa"]
|
|
184
|
+
- Literature: ["EuropePMC", "semantic_scholar", "pubtator", "agents"]
|
|
185
|
+
- Clinical: ["fda_drug_label", "clinical_trials", "adverse_events"]
|
|
186
|
+
|
|
187
|
+
exclude_tools : list of str, optional
|
|
188
|
+
Specific tool names to exclude from loading. These tools will not be
|
|
189
|
+
exposed via the MCP interface even if they are in the loaded categories.
|
|
190
|
+
Useful for removing specific problematic or unwanted tools.
|
|
191
|
+
|
|
192
|
+
exclude_categories : list of str, optional
|
|
193
|
+
Tool categories to exclude from loading. These entire categories will
|
|
194
|
+
be skipped during tool loading. Can be combined with tool_categories
|
|
195
|
+
to first select categories and then exclude specific ones.
|
|
196
|
+
|
|
197
|
+
include_tools : list of str, optional
|
|
198
|
+
Specific tool names to include. If provided, only these tools will be
|
|
199
|
+
loaded regardless of categories. Overrides category-based selection.
|
|
200
|
+
|
|
201
|
+
tools_file : str, optional
|
|
202
|
+
Path to a text file containing tool names to include (one per line).
|
|
203
|
+
Alternative to include_tools parameter. Comments (lines starting with #)
|
|
204
|
+
and empty lines are ignored.
|
|
205
|
+
|
|
206
|
+
tool_config_files : dict of str, optional
|
|
207
|
+
Additional tool configuration files to load. Format:
|
|
208
|
+
{"category_name": "/path/to/config.json"}. These files will be loaded
|
|
209
|
+
in addition to the default tool files.
|
|
210
|
+
|
|
211
|
+
include_tool_types : list of str, optional
|
|
212
|
+
Specific tool types to include. If provided, only tools of these types
|
|
213
|
+
will be loaded. Available types include: 'OpenTarget', 'ToolFinderEmbedding',
|
|
214
|
+
'ToolFinderKeyword', 'ToolFinderLLM', etc.
|
|
215
|
+
|
|
216
|
+
exclude_tool_types : list of str, optional
|
|
217
|
+
Tool types to exclude from loading. These tool types will be skipped
|
|
218
|
+
during tool loading. Useful for excluding entire categories of tools
|
|
219
|
+
(e.g., all ToolFinder types or all OpenTarget tools).
|
|
220
|
+
|
|
221
|
+
auto_expose_tools : bool, default True
|
|
222
|
+
Whether to automatically expose ToolUniverse tools as MCP tools.
|
|
223
|
+
When True, all loaded tools become available via the MCP interface
|
|
224
|
+
with automatic schema conversion and execution wrapping.
|
|
225
|
+
|
|
226
|
+
search_enabled : bool, default True
|
|
227
|
+
Enable AI-powered tool search functionality via tools/find method.
|
|
228
|
+
Includes ToolFinderLLM (cost-optimized LLM-based), Tool_RAG (embedding-based),
|
|
229
|
+
and simple keyword search capabilities with intelligent fallback.
|
|
230
|
+
|
|
231
|
+
max_workers : int, default 5
|
|
232
|
+
Maximum number of concurrent worker threads for tool execution.
|
|
233
|
+
Higher values allow more parallel tool calls but use more resources.
|
|
234
|
+
Recommended: 5-20 depending on server capacity and expected load.
|
|
235
|
+
|
|
236
|
+
hooks_enabled : bool, default False
|
|
237
|
+
Whether to enable output processing hooks for intelligent post-processing
|
|
238
|
+
of tool outputs. When True, hooks can automatically summarize long outputs,
|
|
239
|
+
save results to files, or apply other transformations.
|
|
240
|
+
|
|
241
|
+
hook_config : dict, optional
|
|
242
|
+
Custom hook configuration dictionary. If provided, overrides default
|
|
243
|
+
hook settings. Should contain 'hooks' list with hook definitions.
|
|
244
|
+
Example: {"hooks": [{"name": "summarization_hook", "type": "SummarizationHook", ...}]}
|
|
245
|
+
|
|
246
|
+
hook_type : str, optional
|
|
247
|
+
Simple hook type selection. Can be 'SummarizationHook', 'FileSaveHook',
|
|
248
|
+
or a list of both. Provides an easy way to enable hooks without full configuration.
|
|
249
|
+
Takes precedence over hooks_enabled when specified.
|
|
250
|
+
|
|
251
|
+
**kwargs
|
|
252
|
+
Additional arguments passed to the underlying FastMCP server instance.
|
|
253
|
+
Supports all FastMCP configuration options for advanced customization.
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
=======
|
|
257
|
+
ImportError
|
|
258
|
+
If FastMCP is not installed. FastMCP is a required dependency for SMCP.
|
|
259
|
+
Install with: pip install fastmcp
|
|
260
|
+
|
|
261
|
+
Notes:
|
|
262
|
+
======
|
|
263
|
+
- SMCP automatically handles ToolUniverse tool loading and MCP conversion
|
|
264
|
+
- Tool search uses ToolFinderLLM (optimized for cost) when available, gracefully falls back to simpler methods
|
|
265
|
+
- All tools support JSON argument passing for maximum flexibility
|
|
266
|
+
- Server supports graceful shutdown and comprehensive resource cleanup
|
|
267
|
+
- Thread pool execution ensures non-blocking operation for concurrent requests
|
|
268
|
+
- Built-in error handling provides informative debugging information
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
def __init__(
|
|
272
|
+
self,
|
|
273
|
+
name: Optional[str] = None,
|
|
274
|
+
tooluniverse_config: Optional[Union[ToolUniverse, Dict[str, Any]]] = None,
|
|
275
|
+
tool_categories: Optional[List[str]] = None,
|
|
276
|
+
exclude_tools: Optional[List[str]] = None,
|
|
277
|
+
exclude_categories: Optional[List[str]] = None,
|
|
278
|
+
include_tools: Optional[List[str]] = None,
|
|
279
|
+
tools_file: Optional[str] = None,
|
|
280
|
+
tool_config_files: Optional[Dict[str, str]] = None,
|
|
281
|
+
include_tool_types: Optional[List[str]] = None,
|
|
282
|
+
exclude_tool_types: Optional[List[str]] = None,
|
|
283
|
+
auto_expose_tools: bool = True,
|
|
284
|
+
search_enabled: bool = True,
|
|
285
|
+
max_workers: int = 5,
|
|
286
|
+
hooks_enabled: bool = False,
|
|
287
|
+
hook_config: Optional[Dict[str, Any]] = None,
|
|
288
|
+
hook_type: Optional[str] = None,
|
|
289
|
+
**kwargs,
|
|
290
|
+
):
|
|
291
|
+
if not FASTMCP_AVAILABLE:
|
|
292
|
+
raise ImportError(
|
|
293
|
+
"FastMCP is required for SMCP. Install it with: pip install fastmcp"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Filter out SMCP-specific kwargs before passing to FastMCP
|
|
297
|
+
fastmcp_kwargs = kwargs.copy()
|
|
298
|
+
fastmcp_kwargs.pop("tooluniverse", None) # Remove if accidentally passed
|
|
299
|
+
|
|
300
|
+
# Initialize FastMCP with default settings optimized for scientific use
|
|
301
|
+
super().__init__(name=name or "SMCP Server", **fastmcp_kwargs)
|
|
302
|
+
|
|
303
|
+
# Get logger for this class
|
|
304
|
+
self.logger = get_logger("SMCP")
|
|
305
|
+
|
|
306
|
+
# Initialize ToolUniverse with hook support
|
|
307
|
+
if isinstance(tooluniverse_config, ToolUniverse):
|
|
308
|
+
self.tooluniverse = tooluniverse_config
|
|
309
|
+
else:
|
|
310
|
+
self.tooluniverse = ToolUniverse(
|
|
311
|
+
tool_files=tooluniverse_config,
|
|
312
|
+
keep_default_tools=True,
|
|
313
|
+
hooks_enabled=hooks_enabled,
|
|
314
|
+
hook_config=hook_config,
|
|
315
|
+
hook_type=hook_type,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Configuration
|
|
319
|
+
self.tool_categories = tool_categories
|
|
320
|
+
self.exclude_tools = exclude_tools or []
|
|
321
|
+
self.exclude_categories = exclude_categories or []
|
|
322
|
+
self.include_tools = include_tools or []
|
|
323
|
+
self.tools_file = tools_file
|
|
324
|
+
self.tool_config_files = tool_config_files or {}
|
|
325
|
+
self.include_tool_types = include_tool_types or []
|
|
326
|
+
self.exclude_tool_types = exclude_tool_types or []
|
|
327
|
+
self.auto_expose_tools = auto_expose_tools
|
|
328
|
+
self.search_enabled = search_enabled
|
|
329
|
+
self.max_workers = max_workers
|
|
330
|
+
self.hooks_enabled = hooks_enabled
|
|
331
|
+
self.hook_config = hook_config
|
|
332
|
+
self.hook_type = hook_type
|
|
333
|
+
|
|
334
|
+
# Thread pool for concurrent tool execution
|
|
335
|
+
self.executor = ThreadPoolExecutor(max_workers=max_workers)
|
|
336
|
+
|
|
337
|
+
# Track exposed tools to avoid duplicates
|
|
338
|
+
self._exposed_tools = set()
|
|
339
|
+
|
|
340
|
+
# Initialize SMCP-specific features
|
|
341
|
+
self._setup_smcp_tools()
|
|
342
|
+
|
|
343
|
+
# Register custom MCP methods
|
|
344
|
+
self._register_custom_mcp_methods()
|
|
345
|
+
|
|
346
|
+
def _register_custom_mcp_methods(self):
|
|
347
|
+
"""
|
|
348
|
+
Register custom MCP protocol methods for enhanced functionality.
|
|
349
|
+
|
|
350
|
+
This method extends the standard MCP protocol by registering custom handlers
|
|
351
|
+
for scientific tool discovery and search operations. It safely patches the
|
|
352
|
+
FastMCP request handler to support additional methods while maintaining
|
|
353
|
+
compatibility with standard MCP operations.
|
|
354
|
+
|
|
355
|
+
Custom Methods Registered:
|
|
356
|
+
=========================
|
|
357
|
+
- tools/find: AI-powered tool discovery using natural language queries
|
|
358
|
+
- tools/search: Alternative endpoint for tool search (alias for tools/find)
|
|
359
|
+
|
|
360
|
+
Implementation Details:
|
|
361
|
+
======================
|
|
362
|
+
- Preserves original FastMCP request handler for standard methods
|
|
363
|
+
- Uses method interception pattern to handle custom methods first
|
|
364
|
+
- Falls back to original handler for unrecognized methods
|
|
365
|
+
- Implements proper error handling and JSON-RPC 2.0 compliance
|
|
366
|
+
|
|
367
|
+
Error Handling:
|
|
368
|
+
==============
|
|
369
|
+
- Gracefully handles missing request handlers
|
|
370
|
+
- Logs warnings for debugging when handler patching fails
|
|
371
|
+
- Ensures server continues to function even if custom methods fail to register
|
|
372
|
+
|
|
373
|
+
Notes:
|
|
374
|
+
======
|
|
375
|
+
This method is called automatically during SMCP initialization and should
|
|
376
|
+
not be called manually. It uses a guard to prevent double-patching.
|
|
377
|
+
"""
|
|
378
|
+
try:
|
|
379
|
+
# Override the default request handler to support custom methods
|
|
380
|
+
if hasattr(self, "_original_handle_request"):
|
|
381
|
+
return # Already patched
|
|
382
|
+
|
|
383
|
+
# Store original handler
|
|
384
|
+
self._original_handle_request = getattr(self, "_handle_request", None)
|
|
385
|
+
|
|
386
|
+
# Replace with custom handler
|
|
387
|
+
if hasattr(self, "_handle_request"):
|
|
388
|
+
self._handle_request = self._custom_handle_request
|
|
389
|
+
elif hasattr(self, "handle_request"):
|
|
390
|
+
self._original_handle_request = self.handle_request
|
|
391
|
+
self.handle_request = self._custom_handle_request
|
|
392
|
+
else:
|
|
393
|
+
self.logger.warning("Could not find request handler to override")
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
self.logger.error(f"Error registering custom MCP methods: {e}")
|
|
397
|
+
|
|
398
|
+
def _get_valid_categories(self):
|
|
399
|
+
"""
|
|
400
|
+
Get valid tool categories from ToolUniverse.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Set[str]: Set of valid tool category names
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
# Use the existing ToolUniverse instance if available
|
|
407
|
+
if hasattr(self.tooluniverse, "get_tool_types"):
|
|
408
|
+
return set(self.tooluniverse.get_tool_types())
|
|
409
|
+
else:
|
|
410
|
+
# Create a temporary instance to get categories
|
|
411
|
+
temp_tu = ToolUniverse()
|
|
412
|
+
return set(temp_tu.get_tool_types())
|
|
413
|
+
except Exception as e:
|
|
414
|
+
self.logger.error(f"❌ Error getting valid categories: {e}")
|
|
415
|
+
return set()
|
|
416
|
+
|
|
417
|
+
async def _custom_handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
418
|
+
"""
|
|
419
|
+
Custom MCP request handler that supports enhanced scientific tool operations.
|
|
420
|
+
|
|
421
|
+
This handler intercepts MCP requests and provides specialized handling for
|
|
422
|
+
scientific tool discovery methods while maintaining full compatibility with
|
|
423
|
+
standard MCP protocol operations.
|
|
424
|
+
|
|
425
|
+
Parameters:
|
|
426
|
+
===========
|
|
427
|
+
request : dict
|
|
428
|
+
JSON-RPC 2.0 request object containing:
|
|
429
|
+
- method: The MCP method being called
|
|
430
|
+
- id: Request identifier for response correlation
|
|
431
|
+
- params: Method-specific parameters
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
========
|
|
435
|
+
dict
|
|
436
|
+
JSON-RPC 2.0 response object with either:
|
|
437
|
+
- result: Successful operation result
|
|
438
|
+
- error: Error information with code and message
|
|
439
|
+
|
|
440
|
+
Supported Custom Methods:
|
|
441
|
+
========================
|
|
442
|
+
tools/find:
|
|
443
|
+
Search for tools using natural language queries with AI-powered recommendations.
|
|
444
|
+
Parameters:
|
|
445
|
+
- query (required): Natural language description of desired functionality
|
|
446
|
+
- categories (optional): List of tool categories to filter by
|
|
447
|
+
- limit (optional): Maximum number of results (default: 10)
|
|
448
|
+
- use_advanced_search (optional): Use AI vs keyword search (default: True)
|
|
449
|
+
- search_method (optional): Specific search method - 'auto', 'llm', 'embedding', 'keyword' (default: 'auto')
|
|
450
|
+
- format (optional): Response format - 'detailed' or 'mcp_standard'
|
|
451
|
+
|
|
452
|
+
tools/search:
|
|
453
|
+
Alias for tools/find method with identical parameters and behavior.
|
|
454
|
+
|
|
455
|
+
Standard MCP Methods:
|
|
456
|
+
All other methods are forwarded to the original FastMCP handler,
|
|
457
|
+
ensuring full compatibility with MCP specification.
|
|
458
|
+
|
|
459
|
+
Error Codes:
|
|
460
|
+
============
|
|
461
|
+
- -32601: Method not found (unknown method)
|
|
462
|
+
- -32602: Invalid params (missing required parameters)
|
|
463
|
+
- -32603: Internal error (server-side failures)
|
|
464
|
+
|
|
465
|
+
Examples:
|
|
466
|
+
=========
|
|
467
|
+
Request for tool discovery:
|
|
468
|
+
```json
|
|
469
|
+
{
|
|
470
|
+
"jsonrpc": "2.0",
|
|
471
|
+
"id": "search_123",
|
|
472
|
+
"method": "tools/find",
|
|
473
|
+
"params": {
|
|
474
|
+
"query": "protein structure analysis",
|
|
475
|
+
"limit": 5,
|
|
476
|
+
"format": "mcp_standard"
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Successful response:
|
|
482
|
+
```json
|
|
483
|
+
{
|
|
484
|
+
"jsonrpc": "2.0",
|
|
485
|
+
"id": "search_123",
|
|
486
|
+
"result": {
|
|
487
|
+
"tools": [...],
|
|
488
|
+
"_meta": {
|
|
489
|
+
"search_query": "protein structure analysis",
|
|
490
|
+
"search_method": "AI-powered (ToolFinderLLM)",
|
|
491
|
+
"total_matches": 5
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
"""
|
|
497
|
+
try:
|
|
498
|
+
method = request.get("method")
|
|
499
|
+
request_id = request.get("id")
|
|
500
|
+
params = request.get("params", {})
|
|
501
|
+
|
|
502
|
+
# Handle custom methods
|
|
503
|
+
if method == "tools/find":
|
|
504
|
+
return await self._handle_tools_find(request_id, params)
|
|
505
|
+
elif method == "tools/search": # Alternative endpoint name
|
|
506
|
+
return await self._handle_tools_find(request_id, params)
|
|
507
|
+
|
|
508
|
+
# For all other methods, use the original handler
|
|
509
|
+
if self._original_handle_request:
|
|
510
|
+
if asyncio.iscoroutinefunction(self._original_handle_request):
|
|
511
|
+
return await self._original_handle_request(request)
|
|
512
|
+
else:
|
|
513
|
+
return self._original_handle_request(request)
|
|
514
|
+
else:
|
|
515
|
+
# Fallback: return method not found error
|
|
516
|
+
return {
|
|
517
|
+
"jsonrpc": "2.0",
|
|
518
|
+
"id": request_id,
|
|
519
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
except Exception as e:
|
|
523
|
+
return {
|
|
524
|
+
"jsonrpc": "2.0",
|
|
525
|
+
"id": request.get("id"),
|
|
526
|
+
"error": {"code": -32603, "message": f"Internal error: {str(e)}"},
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async def _handle_tools_find(
|
|
530
|
+
self, request_id: str, params: Dict[str, Any]
|
|
531
|
+
) -> Dict[str, Any]:
|
|
532
|
+
"""
|
|
533
|
+
Handle the tools/find MCP method for AI-powered tool discovery.
|
|
534
|
+
|
|
535
|
+
This method implements the core functionality for the custom tools/find MCP method,
|
|
536
|
+
enabling clients to discover relevant scientific tools using natural language
|
|
537
|
+
queries. It supports both AI-powered semantic search and simple keyword matching.
|
|
538
|
+
|
|
539
|
+
Parameters:
|
|
540
|
+
===========
|
|
541
|
+
request_id : str
|
|
542
|
+
Unique identifier for this request, used in the JSON-RPC response
|
|
543
|
+
params : dict
|
|
544
|
+
Request parameters containing:
|
|
545
|
+
- query (required): Natural language description of desired functionality
|
|
546
|
+
- categories (optional): List of tool categories to filter results
|
|
547
|
+
- limit (optional): Maximum number of tools to return (default: 10)
|
|
548
|
+
- use_advanced_search (optional): Whether to use AI search (default: True)
|
|
549
|
+
- search_method (optional): Specific search method - 'auto', 'llm', 'embedding', 'keyword' (default: 'auto')
|
|
550
|
+
- format (optional): Response format - 'detailed' or 'mcp_standard' (default: 'detailed')
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
========
|
|
554
|
+
dict
|
|
555
|
+
JSON-RPC 2.0 response containing either:
|
|
556
|
+
- Success: Result with discovered tools and metadata
|
|
557
|
+
- Error: Error object with appropriate code and message
|
|
558
|
+
|
|
559
|
+
Response Formats:
|
|
560
|
+
================
|
|
561
|
+
Detailed Format (default):
|
|
562
|
+
Returns comprehensive tool information including:
|
|
563
|
+
- Tool names, descriptions, types
|
|
564
|
+
- Parameter schemas with detailed property information
|
|
565
|
+
- Search metadata (query, method used, match count)
|
|
566
|
+
|
|
567
|
+
MCP Standard Format:
|
|
568
|
+
Returns tools in standard MCP tools/list format:
|
|
569
|
+
- Simplified tool schema compatible with MCP clients
|
|
570
|
+
- inputSchema formatted for direct MCP consumption
|
|
571
|
+
- Metadata included in separate _meta field
|
|
572
|
+
|
|
573
|
+
Search Methods:
|
|
574
|
+
==============
|
|
575
|
+
AI-Powered Search (ToolFinderLLM):
|
|
576
|
+
- Uses Large Language Model to understand query semantics
|
|
577
|
+
- Analyzes tool descriptions for intelligent matching
|
|
578
|
+
- Provides relevance scoring and reasoning
|
|
579
|
+
- Automatically used when available and use_advanced_search=True
|
|
580
|
+
|
|
581
|
+
Simple Keyword Search:
|
|
582
|
+
- Basic text matching against tool names and descriptions
|
|
583
|
+
- Case-insensitive substring matching
|
|
584
|
+
- Used as fallback or when use_advanced_search=False
|
|
585
|
+
|
|
586
|
+
Error Handling:
|
|
587
|
+
==============
|
|
588
|
+
- Validates required parameters (query must be provided)
|
|
589
|
+
- Handles search failures gracefully with informative messages
|
|
590
|
+
- Provides detailed error context for debugging
|
|
591
|
+
|
|
592
|
+
Examples:
|
|
593
|
+
=========
|
|
594
|
+
Basic protein analysis search:
|
|
595
|
+
```python
|
|
596
|
+
params = {
|
|
597
|
+
"query": "protein structure analysis",
|
|
598
|
+
"limit": 3
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Category-filtered drug search:
|
|
603
|
+
```python
|
|
604
|
+
params = {
|
|
605
|
+
"query": "drug interactions",
|
|
606
|
+
"categories": ["ChEMBL", "fda_drug_label"],
|
|
607
|
+
"limit": 5,
|
|
608
|
+
"format": "mcp_standard"
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
"""
|
|
612
|
+
try:
|
|
613
|
+
# Extract parameters
|
|
614
|
+
query = params.get("query", "")
|
|
615
|
+
categories = params.get("categories")
|
|
616
|
+
limit = params.get("limit", 10)
|
|
617
|
+
use_advanced_search = params.get("use_advanced_search", True)
|
|
618
|
+
search_method = params.get(
|
|
619
|
+
"search_method", "auto"
|
|
620
|
+
) # 'auto', 'llm', 'embedding', 'keyword'
|
|
621
|
+
format_type = params.get(
|
|
622
|
+
"format", "detailed"
|
|
623
|
+
) # 'detailed' or 'mcp_standard'
|
|
624
|
+
|
|
625
|
+
if not query:
|
|
626
|
+
return {
|
|
627
|
+
"jsonrpc": "2.0",
|
|
628
|
+
"id": request_id,
|
|
629
|
+
"error": {
|
|
630
|
+
"code": -32602,
|
|
631
|
+
"message": "Invalid params: 'query' is required",
|
|
632
|
+
},
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
# Perform the search using existing search functionality
|
|
636
|
+
search_result = await self._perform_tool_search(
|
|
637
|
+
query=query,
|
|
638
|
+
categories=categories,
|
|
639
|
+
limit=limit,
|
|
640
|
+
use_advanced_search=use_advanced_search,
|
|
641
|
+
search_method=search_method,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# Parse the search result
|
|
645
|
+
search_data = json.loads(search_result)
|
|
646
|
+
|
|
647
|
+
# Format response based on requested format
|
|
648
|
+
if format_type == "mcp_standard":
|
|
649
|
+
# Format as standard MCP tools/list style response
|
|
650
|
+
tools_list = []
|
|
651
|
+
for tool in search_data.get("tools", []):
|
|
652
|
+
mcp_tool = {
|
|
653
|
+
"name": tool.get("name"),
|
|
654
|
+
"description": tool.get("description", ""),
|
|
655
|
+
"inputSchema": {
|
|
656
|
+
"type": "object",
|
|
657
|
+
"properties": tool.get("parameters", {}),
|
|
658
|
+
"required": tool.get("required", []),
|
|
659
|
+
},
|
|
660
|
+
}
|
|
661
|
+
tools_list.append(mcp_tool)
|
|
662
|
+
|
|
663
|
+
result = {
|
|
664
|
+
"tools": tools_list,
|
|
665
|
+
"_meta": {
|
|
666
|
+
"search_query": query,
|
|
667
|
+
"search_method": search_data.get("search_method"),
|
|
668
|
+
"total_matches": search_data.get("total_matches"),
|
|
669
|
+
"categories_filtered": categories,
|
|
670
|
+
},
|
|
671
|
+
}
|
|
672
|
+
else:
|
|
673
|
+
# Return detailed format (default)
|
|
674
|
+
result = search_data
|
|
675
|
+
|
|
676
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
677
|
+
|
|
678
|
+
except json.JSONDecodeError as e:
|
|
679
|
+
return {
|
|
680
|
+
"jsonrpc": "2.0",
|
|
681
|
+
"id": request_id,
|
|
682
|
+
"error": {
|
|
683
|
+
"code": -32603,
|
|
684
|
+
"message": f"Search result parsing error: {str(e)}",
|
|
685
|
+
},
|
|
686
|
+
}
|
|
687
|
+
except Exception as e:
|
|
688
|
+
return {
|
|
689
|
+
"jsonrpc": "2.0",
|
|
690
|
+
"id": request_id,
|
|
691
|
+
"error": {
|
|
692
|
+
"code": -32603,
|
|
693
|
+
"message": f"Internal error in tools/find: {str(e)}",
|
|
694
|
+
},
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async def _perform_tool_search(
|
|
698
|
+
self,
|
|
699
|
+
query: str,
|
|
700
|
+
categories: Optional[List[str]],
|
|
701
|
+
limit: int,
|
|
702
|
+
use_advanced_search: bool,
|
|
703
|
+
search_method: str = "auto",
|
|
704
|
+
) -> str:
|
|
705
|
+
"""
|
|
706
|
+
Execute tool search using the most appropriate search method available.
|
|
707
|
+
|
|
708
|
+
Simplified unified interface that leverages the consistent tool interfaces.
|
|
709
|
+
All search tools now return JSON format directly.
|
|
710
|
+
|
|
711
|
+
Parameters:
|
|
712
|
+
===========
|
|
713
|
+
query : str
|
|
714
|
+
Natural language query describing the desired tool functionality
|
|
715
|
+
categories : list of str, optional
|
|
716
|
+
Tool categories to filter results by
|
|
717
|
+
limit : int
|
|
718
|
+
Maximum number of tools to return
|
|
719
|
+
use_advanced_search : bool
|
|
720
|
+
Whether to prefer AI-powered search when available
|
|
721
|
+
search_method : str, default 'auto'
|
|
722
|
+
Specific search method: 'auto', 'llm', 'embedding', 'keyword'
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
========
|
|
726
|
+
str
|
|
727
|
+
JSON string containing search results
|
|
728
|
+
"""
|
|
729
|
+
try:
|
|
730
|
+
# Determine which tool to use based on method and availability
|
|
731
|
+
tool_name = self._select_search_tool(search_method, use_advanced_search)
|
|
732
|
+
|
|
733
|
+
# Prepare unified function call - all search tools now use same interface
|
|
734
|
+
function_call = {
|
|
735
|
+
"name": tool_name,
|
|
736
|
+
"arguments": {"description": query, "limit": limit},
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
# Add categories only if provided to avoid validation issues
|
|
740
|
+
if categories is not None:
|
|
741
|
+
function_call["arguments"]["categories"] = categories
|
|
742
|
+
|
|
743
|
+
# Execute the search tool
|
|
744
|
+
loop = asyncio.get_event_loop()
|
|
745
|
+
result = await loop.run_in_executor(
|
|
746
|
+
self.executor, self.tooluniverse.run_one_function, function_call
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# All search tools now return JSON format directly
|
|
750
|
+
if isinstance(result, str):
|
|
751
|
+
return result
|
|
752
|
+
elif isinstance(result, dict) or isinstance(result, list):
|
|
753
|
+
return json.dumps(result, indent=2)
|
|
754
|
+
else:
|
|
755
|
+
return str(result)
|
|
756
|
+
|
|
757
|
+
except Exception as e:
|
|
758
|
+
return json.dumps(
|
|
759
|
+
{
|
|
760
|
+
"error": f"Search error: {str(e)}",
|
|
761
|
+
"query": query,
|
|
762
|
+
"fallback_used": True,
|
|
763
|
+
"tools": [],
|
|
764
|
+
},
|
|
765
|
+
indent=2,
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
def _select_search_tool(self, search_method: str, use_advanced_search: bool) -> str:
|
|
769
|
+
"""
|
|
770
|
+
Select the appropriate search tool based on method and availability.
|
|
771
|
+
|
|
772
|
+
Returns:
|
|
773
|
+
str: Tool name to use for search
|
|
774
|
+
"""
|
|
775
|
+
# Get available tools
|
|
776
|
+
all_tools = self.tooluniverse.return_all_loaded_tools()
|
|
777
|
+
available_tool_names = [tool.get("name", "") for tool in all_tools]
|
|
778
|
+
|
|
779
|
+
# Handle specific method requests
|
|
780
|
+
if search_method == "keyword":
|
|
781
|
+
return "Tool_Finder_Keyword"
|
|
782
|
+
elif search_method == "llm" and "Tool_Finder_LLM" in available_tool_names:
|
|
783
|
+
return "Tool_Finder_LLM"
|
|
784
|
+
elif search_method == "embedding" and "Tool_Finder" in available_tool_names:
|
|
785
|
+
return "Tool_Finder"
|
|
786
|
+
elif search_method == "auto":
|
|
787
|
+
# Auto-selection priority: Keyword > RAG > LLM
|
|
788
|
+
if use_advanced_search:
|
|
789
|
+
if "Tool_Finder_Keyword" in available_tool_names:
|
|
790
|
+
return "Tool_Finder_Keyword"
|
|
791
|
+
if "Tool_Finder" in available_tool_names:
|
|
792
|
+
return "Tool_Finder"
|
|
793
|
+
elif "Tool_Finder_LLM" in available_tool_names:
|
|
794
|
+
return "Tool_Finder_LLM"
|
|
795
|
+
else:
|
|
796
|
+
# Invalid method or method not available, fallback to keyword
|
|
797
|
+
return "Tool_Finder_Keyword"
|
|
798
|
+
|
|
799
|
+
def _setup_smcp_tools(self):
|
|
800
|
+
"""
|
|
801
|
+
Initialize and configure SMCP-specific tools and features.
|
|
802
|
+
|
|
803
|
+
This method orchestrates the complete setup of SMCP functionality including
|
|
804
|
+
ToolUniverse tool loading, validation, automatic tool exposure to the MCP
|
|
805
|
+
interface, search functionality initialization, and utility tool registration.
|
|
806
|
+
|
|
807
|
+
The setup process is designed to be robust, handle various edge cases gracefully,
|
|
808
|
+
and provide informative feedback about the configuration process. It implements
|
|
809
|
+
intelligent fallback strategies to ensure functionality even when specific
|
|
810
|
+
components are unavailable.
|
|
811
|
+
|
|
812
|
+
Setup Process Overview:
|
|
813
|
+
=====================
|
|
814
|
+
1. **Tool Loading Assessment**: Check if ToolUniverse already has tools loaded
|
|
815
|
+
to avoid unnecessary reloading and potential conflicts
|
|
816
|
+
|
|
817
|
+
2. **Category Validation**: If specific categories are requested, validate them
|
|
818
|
+
against available categories and provide helpful feedback for invalid ones
|
|
819
|
+
|
|
820
|
+
3. **Tool Loading Strategy**: Load tools using the most appropriate method:
|
|
821
|
+
- Category-specific loading for focused deployments
|
|
822
|
+
- Full loading for comprehensive access
|
|
823
|
+
- Graceful fallback when category loading fails
|
|
824
|
+
|
|
825
|
+
4. **Tool Exposure**: Convert loaded ToolUniverse tools to MCP format with
|
|
826
|
+
proper schema transformation and execution wrapping
|
|
827
|
+
|
|
828
|
+
5. **Search Setup**: Initialize multi-tiered search capabilities including
|
|
829
|
+
AI-powered and fallback methods
|
|
830
|
+
|
|
831
|
+
6. **Utility Registration**: Add server management and diagnostic tools
|
|
832
|
+
|
|
833
|
+
Tool Loading Strategy:
|
|
834
|
+
=====================
|
|
835
|
+
**Already Loaded Check**:
|
|
836
|
+
If ToolUniverse already contains loaded tools (len(all_tools) > 0), skip
|
|
837
|
+
the loading phase to prevent duplication and preserve existing configuration.
|
|
838
|
+
This supports scenarios where users pre-configure ToolUniverse instances.
|
|
839
|
+
|
|
840
|
+
**Category-Specific Loading**:
|
|
841
|
+
When tool_categories is specified:
|
|
842
|
+
- Validate each category against available tool categories
|
|
843
|
+
- Log warnings for invalid categories with suggestions
|
|
844
|
+
- Load only valid categories to optimize performance
|
|
845
|
+
- Fall back to full loading if no valid categories remain
|
|
846
|
+
|
|
847
|
+
**Full Loading (Default)**:
|
|
848
|
+
When auto_expose_tools=True and no specific categories are requested,
|
|
849
|
+
load all available tools to provide comprehensive functionality.
|
|
850
|
+
|
|
851
|
+
**Graceful Fallback**:
|
|
852
|
+
If category-specific loading fails for any reason, automatically
|
|
853
|
+
fall back to loading all tools to ensure basic functionality.
|
|
854
|
+
|
|
855
|
+
Tool Exposure Process:
|
|
856
|
+
=====================
|
|
857
|
+
**Schema Transformation**:
|
|
858
|
+
- Convert ToolUniverse parameter schemas to MCP-compatible format
|
|
859
|
+
- Handle complex parameter types and validation rules
|
|
860
|
+
- Preserve documentation and examples where available
|
|
861
|
+
|
|
862
|
+
**Execution Wrapping**:
|
|
863
|
+
- Create async wrappers for synchronous ToolUniverse tools
|
|
864
|
+
- Implement proper error handling and result formatting
|
|
865
|
+
- Use thread pool execution to prevent blocking
|
|
866
|
+
|
|
867
|
+
**Safety Mechanisms**:
|
|
868
|
+
- Skip meta-tools (MCPAutoLoaderTool, MCPClientTool) that shouldn't be exposed
|
|
869
|
+
- Track exposed tools to prevent duplicates
|
|
870
|
+
- Handle tool conversion failures gracefully without stopping entire process
|
|
871
|
+
|
|
872
|
+
Search Setup:
|
|
873
|
+
============
|
|
874
|
+
**Multi-Tiered Search Architecture**:
|
|
875
|
+
1. **ToolFinderLLM** (Primary): Cost-optimized AI-powered semantic understanding using LLM
|
|
876
|
+
2. **Tool_RAG** (Secondary): Embedding-based similarity search
|
|
877
|
+
3. **Keyword Search** (Fallback): Simple text matching, always available
|
|
878
|
+
|
|
879
|
+
**Initialization Process**:
|
|
880
|
+
- Check for availability of advanced search tools in loaded tools
|
|
881
|
+
- Attempt to load search tools if not already present
|
|
882
|
+
- Configure search capabilities based on what's available
|
|
883
|
+
- Provide clear feedback about search capabilities
|
|
884
|
+
|
|
885
|
+
**Search Tool Loading**:
|
|
886
|
+
Attempts to load tool_finder_llm and tool_finder categories which include:
|
|
887
|
+
- ToolFinderLLM: Cost-optimized LLM-based intelligent tool discovery
|
|
888
|
+
- Tool_RAG: Embedding-based semantic search
|
|
889
|
+
- Supporting utilities and configuration tools
|
|
890
|
+
|
|
891
|
+
Error Handling:
|
|
892
|
+
==============
|
|
893
|
+
**Category Validation Errors**:
|
|
894
|
+
- Log specific invalid categories with available alternatives
|
|
895
|
+
- Continue with valid categories only
|
|
896
|
+
- Fall back to full loading if no valid categories
|
|
897
|
+
|
|
898
|
+
**Tool Loading Errors**:
|
|
899
|
+
- Log detailed error information for debugging
|
|
900
|
+
- Continue setup process with already loaded tools
|
|
901
|
+
- Ensure server remains functional even with partial failures
|
|
902
|
+
|
|
903
|
+
**Search Setup Errors**:
|
|
904
|
+
- Gracefully handle missing search tool dependencies
|
|
905
|
+
- Fall back to simpler search methods automatically
|
|
906
|
+
- Log informative messages about search capabilities
|
|
907
|
+
|
|
908
|
+
**Tool Exposure Errors**:
|
|
909
|
+
- Handle individual tool conversion failures without stopping process
|
|
910
|
+
- Log specific tool errors for debugging
|
|
911
|
+
- Continue with remaining tools to maximize functionality
|
|
912
|
+
|
|
913
|
+
Performance Considerations:
|
|
914
|
+
==========================
|
|
915
|
+
- **Lazy Loading**: Only load tools when needed to minimize startup time
|
|
916
|
+
- **Efficient Validation**: Quick category checks before expensive operations
|
|
917
|
+
- **Parallel Processing**: Use thread pools for tool conversion where possible
|
|
918
|
+
- **Memory Management**: Efficient tool representation and storage
|
|
919
|
+
|
|
920
|
+
Diagnostic Output:
|
|
921
|
+
=================
|
|
922
|
+
Provides informative logging throughout the setup process:
|
|
923
|
+
```
|
|
924
|
+
Tools already loaded in ToolUniverse (356 tools), skipping reload
|
|
925
|
+
Exposing 356 tools from ToolUniverse
|
|
926
|
+
✅ ToolFinderLLM (cost-optimized) available for advanced search
|
|
927
|
+
Exposed tool: UniProt_get_entry_by_accession (type: uniprot)
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
Notes:
|
|
931
|
+
======
|
|
932
|
+
- This method is called automatically during SMCP initialization
|
|
933
|
+
- Should not be called manually after server initialization
|
|
934
|
+
- Setup is idempotent - can be called multiple times safely
|
|
935
|
+
- All setup phases include comprehensive error handling
|
|
936
|
+
- Performance scales with the number of tools being loaded and exposed
|
|
937
|
+
"""
|
|
938
|
+
# Always ensure full tool set is loaded (hooks may have preloaded a minimal set)
|
|
939
|
+
# Deduplication in ToolUniverse.load_tools prevents duplicates, so reloading is safe
|
|
940
|
+
if self.tool_categories:
|
|
941
|
+
try:
|
|
942
|
+
# Validate categories first
|
|
943
|
+
valid_categories = self._get_valid_categories()
|
|
944
|
+
invalid_categories = [
|
|
945
|
+
cat for cat in self.tool_categories if cat not in valid_categories
|
|
946
|
+
]
|
|
947
|
+
|
|
948
|
+
if invalid_categories:
|
|
949
|
+
available_categories = list(valid_categories)
|
|
950
|
+
self.logger.warning(
|
|
951
|
+
f"Invalid categories {invalid_categories}. Available categories: {available_categories}"
|
|
952
|
+
)
|
|
953
|
+
# Filter to valid categories only
|
|
954
|
+
valid_only = [
|
|
955
|
+
cat for cat in self.tool_categories if cat in valid_categories
|
|
956
|
+
]
|
|
957
|
+
if valid_only:
|
|
958
|
+
self.logger.info(f"Loading valid categories: {valid_only}")
|
|
959
|
+
self.tooluniverse.load_tools(
|
|
960
|
+
tool_type=valid_only,
|
|
961
|
+
exclude_tools=self.exclude_tools,
|
|
962
|
+
exclude_categories=self.exclude_categories,
|
|
963
|
+
include_tools=self.include_tools,
|
|
964
|
+
tools_file=self.tools_file,
|
|
965
|
+
tool_config_files=self.tool_config_files,
|
|
966
|
+
include_tool_types=self.include_tool_types,
|
|
967
|
+
exclude_tool_types=self.exclude_tool_types,
|
|
968
|
+
)
|
|
969
|
+
else:
|
|
970
|
+
self.logger.warning(
|
|
971
|
+
"No valid categories found, loading all tools instead"
|
|
972
|
+
)
|
|
973
|
+
self.tooluniverse.load_tools(
|
|
974
|
+
exclude_tools=self.exclude_tools,
|
|
975
|
+
exclude_categories=self.exclude_categories,
|
|
976
|
+
include_tools=self.include_tools,
|
|
977
|
+
tools_file=self.tools_file,
|
|
978
|
+
tool_config_files=self.tool_config_files,
|
|
979
|
+
include_tool_types=self.include_tool_types,
|
|
980
|
+
exclude_tool_types=self.exclude_tool_types,
|
|
981
|
+
)
|
|
982
|
+
else:
|
|
983
|
+
self.tooluniverse.load_tools(
|
|
984
|
+
tool_type=self.tool_categories,
|
|
985
|
+
exclude_tools=self.exclude_tools,
|
|
986
|
+
exclude_categories=self.exclude_categories,
|
|
987
|
+
include_tools=self.include_tools,
|
|
988
|
+
tools_file=self.tools_file,
|
|
989
|
+
tool_config_files=self.tool_config_files,
|
|
990
|
+
include_tool_types=self.include_tool_types,
|
|
991
|
+
exclude_tool_types=self.exclude_tool_types,
|
|
992
|
+
)
|
|
993
|
+
except Exception as e:
|
|
994
|
+
self.logger.error(f"Error loading specified categories: {e}")
|
|
995
|
+
self.logger.info("Falling back to loading all tools")
|
|
996
|
+
self.tooluniverse.load_tools(
|
|
997
|
+
exclude_tools=self.exclude_tools,
|
|
998
|
+
exclude_categories=self.exclude_categories,
|
|
999
|
+
include_tools=self.include_tools,
|
|
1000
|
+
tools_file=self.tools_file,
|
|
1001
|
+
tool_config_files=self.tool_config_files,
|
|
1002
|
+
include_tool_types=self.include_tool_types,
|
|
1003
|
+
exclude_tool_types=self.exclude_tool_types,
|
|
1004
|
+
)
|
|
1005
|
+
elif self.auto_expose_tools:
|
|
1006
|
+
# Load all tools by default
|
|
1007
|
+
self.tooluniverse.load_tools(
|
|
1008
|
+
exclude_tools=self.exclude_tools,
|
|
1009
|
+
exclude_categories=self.exclude_categories,
|
|
1010
|
+
include_tools=self.include_tools,
|
|
1011
|
+
tools_file=self.tools_file,
|
|
1012
|
+
tool_config_files=self.tool_config_files,
|
|
1013
|
+
include_tool_types=self.include_tool_types,
|
|
1014
|
+
exclude_tool_types=self.exclude_tool_types,
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
# Auto-expose ToolUniverse tools as MCP tools
|
|
1018
|
+
if self.auto_expose_tools:
|
|
1019
|
+
self._expose_tooluniverse_tools()
|
|
1020
|
+
|
|
1021
|
+
# Add search functionality if enabled
|
|
1022
|
+
if self.search_enabled:
|
|
1023
|
+
self._add_search_tools()
|
|
1024
|
+
|
|
1025
|
+
# Add utility tools
|
|
1026
|
+
self._add_utility_tools()
|
|
1027
|
+
|
|
1028
|
+
def _expose_tooluniverse_tools(self):
|
|
1029
|
+
"""
|
|
1030
|
+
Automatically expose ToolUniverse tools as MCP-compatible tools.
|
|
1031
|
+
|
|
1032
|
+
This method performs the critical task of converting ToolUniverse's tool
|
|
1033
|
+
definitions into FastMCP-compatible tools that can be called via the MCP
|
|
1034
|
+
protocol. It handles the complex mapping between different tool formats
|
|
1035
|
+
while ensuring compatibility and usability.
|
|
1036
|
+
|
|
1037
|
+
Process Overview:
|
|
1038
|
+
================
|
|
1039
|
+
1. **Tool Inventory**: Enumerate all loaded ToolUniverse tools
|
|
1040
|
+
2. **Type Filtering**: Skip meta-tools that shouldn't be exposed
|
|
1041
|
+
3. **Schema Conversion**: Transform ToolUniverse schemas to MCP format
|
|
1042
|
+
4. **Function Wrapping**: Create async wrappers for tool execution
|
|
1043
|
+
5. **Registration**: Register tools with FastMCP framework
|
|
1044
|
+
|
|
1045
|
+
Tool Type Filtering:
|
|
1046
|
+
===================
|
|
1047
|
+
Skips these internal tool types:
|
|
1048
|
+
- MCPAutoLoaderTool: Used for loading other MCP servers
|
|
1049
|
+
- MCPClientTool: Used for connecting to external MCP servers
|
|
1050
|
+
|
|
1051
|
+
These are meta-tools that manage other tools rather than providing
|
|
1052
|
+
end-user functionality, so they're excluded from the MCP interface.
|
|
1053
|
+
|
|
1054
|
+
Schema Transformation:
|
|
1055
|
+
=====================
|
|
1056
|
+
ToolUniverse Tool Format:
|
|
1057
|
+
```json
|
|
1058
|
+
{
|
|
1059
|
+
"name": "tool_name",
|
|
1060
|
+
"parameter": {
|
|
1061
|
+
"type": "object",
|
|
1062
|
+
"properties": {...},
|
|
1063
|
+
"required": [...]
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
MCP Tool Format:
|
|
1069
|
+
```python
|
|
1070
|
+
async def tool_function(arguments: str = "{}") -> str:
|
|
1071
|
+
# Tool execution logic
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
Execution Model:
|
|
1075
|
+
===============
|
|
1076
|
+
- **JSON Arguments**: All tools accept a single 'arguments' parameter
|
|
1077
|
+
containing JSON-encoded tool parameters
|
|
1078
|
+
- **Async Execution**: Tools run in thread pool to prevent blocking
|
|
1079
|
+
- **Error Handling**: Comprehensive error catching and reporting
|
|
1080
|
+
- **Type Safety**: Proper argument parsing and validation
|
|
1081
|
+
|
|
1082
|
+
Duplicate Prevention:
|
|
1083
|
+
====================
|
|
1084
|
+
- Tracks exposed tools in self._exposed_tools set
|
|
1085
|
+
- Prevents re-registration of already exposed tools
|
|
1086
|
+
- Handles tool reloading scenarios gracefully
|
|
1087
|
+
|
|
1088
|
+
Error Recovery:
|
|
1089
|
+
==============
|
|
1090
|
+
- Individual tool failures don't stop the entire process
|
|
1091
|
+
- Detailed error logging for debugging
|
|
1092
|
+
- Continues with remaining tools if some fail to convert
|
|
1093
|
+
|
|
1094
|
+
Performance Optimization:
|
|
1095
|
+
========================
|
|
1096
|
+
- Lazy evaluation of tool schemas
|
|
1097
|
+
- Minimal memory footprint per tool
|
|
1098
|
+
- Efficient tool lookup and execution
|
|
1099
|
+
- Thread pool reuse for all tool executions
|
|
1100
|
+
|
|
1101
|
+
Examples:
|
|
1102
|
+
=========
|
|
1103
|
+
Original ToolUniverse tool call:
|
|
1104
|
+
```python
|
|
1105
|
+
tu.run_one_function({
|
|
1106
|
+
"name": "UniProt_get_entry_by_accession",
|
|
1107
|
+
"arguments": {"accession": "P05067"}
|
|
1108
|
+
})
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
Equivalent MCP tool call:
|
|
1112
|
+
```python
|
|
1113
|
+
await tool_function('{"accession": "P05067"}')
|
|
1114
|
+
```
|
|
1115
|
+
"""
|
|
1116
|
+
if not hasattr(self.tooluniverse, "all_tools"):
|
|
1117
|
+
self.logger.warning("No all_tools attribute in tooluniverse")
|
|
1118
|
+
return
|
|
1119
|
+
|
|
1120
|
+
self.logger.info(
|
|
1121
|
+
f"Exposing {len(self.tooluniverse.all_tools)} tools from ToolUniverse"
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
# Define tool types that should not be exposed as MCP tools
|
|
1125
|
+
# These are internal/meta tools that are used for loading other tools
|
|
1126
|
+
skip_tool_types = {"MCPAutoLoaderTool", "MCPClientTool"}
|
|
1127
|
+
|
|
1128
|
+
for i, tool_config in enumerate(self.tooluniverse.all_tools):
|
|
1129
|
+
try:
|
|
1130
|
+
# Debug: Check the type of tool_config
|
|
1131
|
+
if not isinstance(tool_config, dict):
|
|
1132
|
+
self.logger.warning(
|
|
1133
|
+
f"tool_config at index {i} is not a dict, it's {type(tool_config)}: {tool_config}"
|
|
1134
|
+
)
|
|
1135
|
+
continue
|
|
1136
|
+
|
|
1137
|
+
tool_name = tool_config.get("name")
|
|
1138
|
+
tool_type = tool_config.get("type")
|
|
1139
|
+
|
|
1140
|
+
# Skip internal/meta tools that are used for loading other tools
|
|
1141
|
+
if tool_type in skip_tool_types:
|
|
1142
|
+
self.logger.debug(
|
|
1143
|
+
f"Skipping exposure of meta tool: {tool_name} (type: {tool_type})"
|
|
1144
|
+
)
|
|
1145
|
+
continue
|
|
1146
|
+
|
|
1147
|
+
if tool_name and tool_name not in self._exposed_tools:
|
|
1148
|
+
self._create_mcp_tool_from_tooluniverse(tool_config)
|
|
1149
|
+
self._exposed_tools.add(tool_name)
|
|
1150
|
+
self.logger.debug(f"Exposed tool: {tool_name} (type: {tool_type})")
|
|
1151
|
+
|
|
1152
|
+
except Exception as e:
|
|
1153
|
+
self.logger.error(f"Error processing tool at index {i}: {e}")
|
|
1154
|
+
self.logger.debug(f"Tool config: {tool_config}")
|
|
1155
|
+
continue
|
|
1156
|
+
|
|
1157
|
+
exposed_count = len(self._exposed_tools)
|
|
1158
|
+
self.logger.info(f"Successfully exposed {exposed_count} tools to MCP interface")
|
|
1159
|
+
|
|
1160
|
+
def _add_search_tools(self):
|
|
1161
|
+
"""
|
|
1162
|
+
Register AI-powered tool search and discovery functionality.
|
|
1163
|
+
|
|
1164
|
+
This method adds sophisticated tool discovery capabilities to the SMCP server,
|
|
1165
|
+
enabling clients to find relevant tools using natural language queries.
|
|
1166
|
+
It provides both programmatic (MCP tool) and protocol-level (tools/find method)
|
|
1167
|
+
interfaces for tool discovery.
|
|
1168
|
+
|
|
1169
|
+
Registered Tools:
|
|
1170
|
+
================
|
|
1171
|
+
|
|
1172
|
+
find_tools:
|
|
1173
|
+
Primary tool discovery interface with AI-powered search capabilities.
|
|
1174
|
+
|
|
1175
|
+
Parameters:
|
|
1176
|
+
- query (str): Natural language description of desired functionality
|
|
1177
|
+
- categories (list, optional): Tool categories to filter by
|
|
1178
|
+
- limit (int, default=10): Maximum number of results
|
|
1179
|
+
- use_advanced_search (bool, default=True): Use AI vs keyword search
|
|
1180
|
+
|
|
1181
|
+
Returns: JSON string with discovered tools and search metadata
|
|
1182
|
+
|
|
1183
|
+
search_tools:
|
|
1184
|
+
Backward-compatible alias for find_tools with identical functionality.
|
|
1185
|
+
Maintained for compatibility with existing integrations.
|
|
1186
|
+
|
|
1187
|
+
Search Capabilities:
|
|
1188
|
+
===================
|
|
1189
|
+
|
|
1190
|
+
AI-Powered Search (ToolFinderLLM):
|
|
1191
|
+
- Uses Large Language Model to understand query semantics with optimized context
|
|
1192
|
+
- Pre-filters tools using keyword matching to reduce LLM context cost
|
|
1193
|
+
- Analyzes only essential tool information (name + description) for cost efficiency
|
|
1194
|
+
- Provides relevance scoring and reasoning
|
|
1195
|
+
- Handles complex queries like "analyze protein interactions in cancer"
|
|
1196
|
+
|
|
1197
|
+
Embedding-Based Search (Tool_RAG):
|
|
1198
|
+
- Uses vector embeddings for semantic similarity matching
|
|
1199
|
+
- Fast approximate matching for large tool collections
|
|
1200
|
+
- Good balance between speed and semantic understanding
|
|
1201
|
+
|
|
1202
|
+
Keyword Search (Fallback):
|
|
1203
|
+
- Simple text matching against tool names and descriptions
|
|
1204
|
+
- Always available regardless of AI tool availability
|
|
1205
|
+
- Provides basic but reliable tool discovery
|
|
1206
|
+
|
|
1207
|
+
Search Strategy:
|
|
1208
|
+
===============
|
|
1209
|
+
1. **Preference**: ToolFinderLLM (most intelligent, cost-optimized)
|
|
1210
|
+
2. **Fallback**: Tool_RAG (semantic similarity)
|
|
1211
|
+
3. **Final**: Simple keyword matching (always works)
|
|
1212
|
+
|
|
1213
|
+
Integration Details:
|
|
1214
|
+
===================
|
|
1215
|
+
- Automatically initializes available search tools during setup
|
|
1216
|
+
- Shares search logic with tools/find MCP method
|
|
1217
|
+
- Provides consistent results across different interfaces
|
|
1218
|
+
- Handles tool loading and availability detection
|
|
1219
|
+
|
|
1220
|
+
Error Handling:
|
|
1221
|
+
==============
|
|
1222
|
+
- Graceful degradation when AI tools unavailable
|
|
1223
|
+
- Informative error messages for debugging
|
|
1224
|
+
- Fallback mechanisms ensure search always works
|
|
1225
|
+
- Detailed logging of search method selection
|
|
1226
|
+
|
|
1227
|
+
Usage Examples:
|
|
1228
|
+
==============
|
|
1229
|
+
Via MCP tool interface:
|
|
1230
|
+
```python
|
|
1231
|
+
result = await find_tools(
|
|
1232
|
+
query="protein structure prediction",
|
|
1233
|
+
categories=["uniprot", "hpa"],
|
|
1234
|
+
limit=5
|
|
1235
|
+
)
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
Via tools/find MCP method:
|
|
1239
|
+
```json
|
|
1240
|
+
{
|
|
1241
|
+
"method": "tools/find",
|
|
1242
|
+
"params": {
|
|
1243
|
+
"query": "drug interaction analysis",
|
|
1244
|
+
"limit": 3
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
```
|
|
1248
|
+
"""
|
|
1249
|
+
|
|
1250
|
+
# Initialize tool finder (prefer LLM-based if available, fallback to embedding-based)
|
|
1251
|
+
self._init_tool_finder()
|
|
1252
|
+
|
|
1253
|
+
@self.tool()
|
|
1254
|
+
async def find_tools(
|
|
1255
|
+
query: str,
|
|
1256
|
+
categories: Optional[List[str]] = None,
|
|
1257
|
+
limit: int = 10,
|
|
1258
|
+
use_advanced_search: bool = True,
|
|
1259
|
+
search_method: str = "auto",
|
|
1260
|
+
) -> str:
|
|
1261
|
+
"""
|
|
1262
|
+
Find and search available ToolUniverse tools using AI-powered search.
|
|
1263
|
+
|
|
1264
|
+
This tool provides the same functionality as the tools/find MCP method.
|
|
1265
|
+
|
|
1266
|
+
Args:
|
|
1267
|
+
query: Search query describing the desired functionality
|
|
1268
|
+
categories: Optional list of categories to filter by
|
|
1269
|
+
limit: Maximum number of results to return (default: 10)
|
|
1270
|
+
use_advanced_search: Use AI-powered search if available (default: True)
|
|
1271
|
+
search_method: Specific search method - 'auto', 'llm', 'embedding', 'keyword' (default: 'auto')
|
|
1272
|
+
|
|
1273
|
+
Returns:
|
|
1274
|
+
JSON string containing matching tools with detailed information
|
|
1275
|
+
"""
|
|
1276
|
+
return await self._perform_tool_search(
|
|
1277
|
+
query, categories, limit, use_advanced_search, search_method
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
# # Keep the original search_tools as an alias for backward compatibility
|
|
1281
|
+
# @self.tool()
|
|
1282
|
+
# async def search_tools(
|
|
1283
|
+
# query: str,
|
|
1284
|
+
# categories: Optional[List[str]] = None,
|
|
1285
|
+
# limit: int = 10,
|
|
1286
|
+
# use_advanced_search: bool = True,
|
|
1287
|
+
# search_method: str = 'auto'
|
|
1288
|
+
# ) -> str:
|
|
1289
|
+
# """
|
|
1290
|
+
# Search available ToolUniverse tools (alias for find_tools).
|
|
1291
|
+
|
|
1292
|
+
# Args:
|
|
1293
|
+
# query: Search query string describing the desired functionality
|
|
1294
|
+
# categories: Optional list of categories to filter by
|
|
1295
|
+
# limit: Maximum number of results to return
|
|
1296
|
+
# use_advanced_search: Whether to use AI-powered tool finder
|
|
1297
|
+
# search_method: Specific search method - 'auto', 'llm', 'embedding', 'keyword' (default: 'auto')
|
|
1298
|
+
|
|
1299
|
+
# Returns:
|
|
1300
|
+
# JSON string containing matching tools information
|
|
1301
|
+
# """
|
|
1302
|
+
# return await self._perform_tool_search(query, categories, limit, use_advanced_search, search_method)
|
|
1303
|
+
|
|
1304
|
+
def _init_tool_finder(self):
|
|
1305
|
+
"""
|
|
1306
|
+
Initialize intelligent tool discovery system with automatic fallback.
|
|
1307
|
+
|
|
1308
|
+
This method sets up the tool finder infrastructure that powers AI-driven
|
|
1309
|
+
tool discovery. It implements a tiered approach, trying the most advanced
|
|
1310
|
+
search methods first and falling back to simpler methods if needed.
|
|
1311
|
+
|
|
1312
|
+
Initialization Strategy:
|
|
1313
|
+
=======================
|
|
1314
|
+
|
|
1315
|
+
Phase 1 - Detection:
|
|
1316
|
+
Scans loaded ToolUniverse tools to identify available search tools:
|
|
1317
|
+
- ToolFinderLLM: Advanced LLM-based semantic search
|
|
1318
|
+
- Tool_RAG: Embedding-based similarity search
|
|
1319
|
+
|
|
1320
|
+
Phase 2 - Loading (if needed):
|
|
1321
|
+
If no search tools are found, attempts to load them:
|
|
1322
|
+
- Loads 'tool_finder_llm' and 'tool_finder' categories
|
|
1323
|
+
- Re-scans for available tools after loading
|
|
1324
|
+
|
|
1325
|
+
Phase 3 - Selection:
|
|
1326
|
+
Selects the best available search method:
|
|
1327
|
+
1. ToolFinderLLM (preferred - most intelligent)
|
|
1328
|
+
2. Tool_RAG (fallback - good semantic understanding)
|
|
1329
|
+
3. Simple keyword search (always available)
|
|
1330
|
+
|
|
1331
|
+
Tool Finder Capabilities:
|
|
1332
|
+
========================
|
|
1333
|
+
|
|
1334
|
+
ToolFinderLLM:
|
|
1335
|
+
- Uses GPT-4 or similar LLM for query understanding
|
|
1336
|
+
- Analyzes tool descriptions for semantic matching
|
|
1337
|
+
- Provides relevance scoring and selection reasoning
|
|
1338
|
+
- Handles complex, multi-faceted queries effectively
|
|
1339
|
+
- Best for: "Find tools to analyze protein-drug interactions in cancer research"
|
|
1340
|
+
|
|
1341
|
+
Tool_RAG:
|
|
1342
|
+
- Uses pre-computed embeddings for fast similarity search
|
|
1343
|
+
- Good semantic understanding without LLM overhead
|
|
1344
|
+
- Faster than LLM-based search for simple queries
|
|
1345
|
+
- Best for: "protein analysis", "drug discovery"
|
|
1346
|
+
|
|
1347
|
+
Simple Search:
|
|
1348
|
+
- Basic keyword matching against names and descriptions
|
|
1349
|
+
- No dependencies, always available
|
|
1350
|
+
- Fast and reliable for exact term matches
|
|
1351
|
+
- Best for: "chembl", "uniprot", "fda"
|
|
1352
|
+
|
|
1353
|
+
Configuration Management:
|
|
1354
|
+
========================
|
|
1355
|
+
Sets instance attributes:
|
|
1356
|
+
- tool_finder_available (bool): Whether advanced search is available
|
|
1357
|
+
- tool_finder_type (str): Type of search tool loaded ("ToolFinderLLM" | "Tool_RAG")
|
|
1358
|
+
|
|
1359
|
+
Error Handling:
|
|
1360
|
+
==============
|
|
1361
|
+
- Handles missing dependencies gracefully
|
|
1362
|
+
- Provides informative console output about search capabilities
|
|
1363
|
+
- Ensures search functionality always works (via simple fallback)
|
|
1364
|
+
- Logs detailed information for debugging
|
|
1365
|
+
|
|
1366
|
+
Performance Considerations:
|
|
1367
|
+
==========================
|
|
1368
|
+
- Tool loading only happens if search tools aren't already available
|
|
1369
|
+
- Search tool detection is cached to avoid repeated scans
|
|
1370
|
+
- ToolFinderLLM requires network access and API keys
|
|
1371
|
+
- Tool_RAG requires embedding files but works offline
|
|
1372
|
+
|
|
1373
|
+
Dependencies:
|
|
1374
|
+
============
|
|
1375
|
+
- ToolFinderLLM: Requires OpenAI API access or compatible endpoint
|
|
1376
|
+
- Tool_RAG: Requires sentence-transformers and embedding data
|
|
1377
|
+
- Simple search: No external dependencies
|
|
1378
|
+
"""
|
|
1379
|
+
self.tool_finder_available = False
|
|
1380
|
+
self.tool_finder_type = None
|
|
1381
|
+
|
|
1382
|
+
# Check if ToolFinderLLM is available in loaded tools
|
|
1383
|
+
try:
|
|
1384
|
+
all_tools = self.tooluniverse.return_all_loaded_tools()
|
|
1385
|
+
available_tool_names = [tool.get("name", "") for tool in all_tools]
|
|
1386
|
+
|
|
1387
|
+
# Try ToolFinderLLM first (more advanced)
|
|
1388
|
+
if "Tool_Finder_LLM" in available_tool_names:
|
|
1389
|
+
self.tool_finder_available = True
|
|
1390
|
+
self.tool_finder_type = "Tool_Finder_LLM"
|
|
1391
|
+
self.logger.info(
|
|
1392
|
+
"✅ Tool_Finder_LLM (cost-optimized) available for advanced search"
|
|
1393
|
+
)
|
|
1394
|
+
return
|
|
1395
|
+
|
|
1396
|
+
# Fallback to Tool_RAG (embedding-based)
|
|
1397
|
+
if "Tool_RAG" in available_tool_names:
|
|
1398
|
+
self.tool_finder_available = True
|
|
1399
|
+
self.tool_finder_type = "Tool_RAG"
|
|
1400
|
+
self.logger.info(
|
|
1401
|
+
"✅ Tool_RAG (embedding-based) available for advanced search"
|
|
1402
|
+
)
|
|
1403
|
+
return
|
|
1404
|
+
|
|
1405
|
+
# Check if ToolFinderKeyword is available for simple search
|
|
1406
|
+
if "Tool_Finder_Keyword" in available_tool_names:
|
|
1407
|
+
self.logger.info("✅ ToolFinderKeyword available for simple search")
|
|
1408
|
+
|
|
1409
|
+
self.logger.warning("⚠️ No advanced tool finders available in loaded tools")
|
|
1410
|
+
self.logger.debug(
|
|
1411
|
+
f"Available tools: {available_tool_names[:5]}..."
|
|
1412
|
+
) # Show first 5 tools
|
|
1413
|
+
|
|
1414
|
+
except Exception as e:
|
|
1415
|
+
self.logger.warning(f"⚠️ Failed to check for tool finders: {e}")
|
|
1416
|
+
|
|
1417
|
+
# Try to load tool finder tools if not already loaded
|
|
1418
|
+
try:
|
|
1419
|
+
self.logger.debug("🔄 Attempting to load tool finder tools...")
|
|
1420
|
+
|
|
1421
|
+
# Load tool_finder category which includes ToolFinderLLM, Tool_RAG, and ToolFinderKeyword
|
|
1422
|
+
self.tooluniverse.load_tools(tool_type=["tool_finder"])
|
|
1423
|
+
|
|
1424
|
+
# Re-check availability
|
|
1425
|
+
all_tools = self.tooluniverse.return_all_loaded_tools()
|
|
1426
|
+
available_tool_names = [tool.get("name", "") for tool in all_tools]
|
|
1427
|
+
|
|
1428
|
+
if "Tool_Finder_LLM" in available_tool_names:
|
|
1429
|
+
self.tool_finder_available = True
|
|
1430
|
+
self.tool_finder_type = "Tool_Finder_LLM"
|
|
1431
|
+
self.logger.info(
|
|
1432
|
+
"✅ Successfully loaded Tool_Finder_LLM for advanced search"
|
|
1433
|
+
)
|
|
1434
|
+
elif "Tool_RAG" in available_tool_names:
|
|
1435
|
+
self.tool_finder_available = True
|
|
1436
|
+
self.tool_finder_type = "Tool_RAG"
|
|
1437
|
+
self.logger.info("✅ Successfully loaded Tool_RAG for advanced search")
|
|
1438
|
+
else:
|
|
1439
|
+
self.logger.warning("⚠️ Failed to load any advanced tool finder tools")
|
|
1440
|
+
|
|
1441
|
+
# Check if ToolFinderKeyword is available for simple search
|
|
1442
|
+
if "Tool_Finder_Keyword" in available_tool_names:
|
|
1443
|
+
self.logger.info("✅ Tool_Finder_Keyword available for simple search")
|
|
1444
|
+
else:
|
|
1445
|
+
self.logger.warning(
|
|
1446
|
+
"⚠️ ToolFinderKeyword not available, using fallback search"
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
except Exception as e:
|
|
1450
|
+
self.logger.warning(f"⚠️ Failed to load tool finder tools: {e}")
|
|
1451
|
+
self.logger.info(
|
|
1452
|
+
"📝 Advanced search will not be available, using simple keyword search only"
|
|
1453
|
+
)
|
|
1454
|
+
|
|
1455
|
+
def _add_utility_tools(self):
|
|
1456
|
+
"""
|
|
1457
|
+
Register essential server management and diagnostic tools.
|
|
1458
|
+
|
|
1459
|
+
This method adds a suite of utility tools that provide server introspection,
|
|
1460
|
+
tool management, and direct execution capabilities. These tools are essential
|
|
1461
|
+
for monitoring server health, understanding available capabilities, and
|
|
1462
|
+
providing administrative functionality.
|
|
1463
|
+
|
|
1464
|
+
Registered Utility Tools:
|
|
1465
|
+
========================
|
|
1466
|
+
|
|
1467
|
+
get_server_info:
|
|
1468
|
+
Comprehensive server status and capability reporting.
|
|
1469
|
+
|
|
1470
|
+
Returns detailed JSON with:
|
|
1471
|
+
- Server identification (name, type, version info)
|
|
1472
|
+
- Tool statistics (total tools, exposed tools, categories)
|
|
1473
|
+
- Feature flags (search enabled, FastMCP status)
|
|
1474
|
+
- Resource usage (max workers, thread pool status)
|
|
1475
|
+
|
|
1476
|
+
Use cases:
|
|
1477
|
+
- Health checks and monitoring
|
|
1478
|
+
- Capability discovery by clients
|
|
1479
|
+
- Debugging server configuration issues
|
|
1480
|
+
|
|
1481
|
+
execute_tooluniverse_function:
|
|
1482
|
+
Direct interface for executing ToolUniverse functions with custom parameters.
|
|
1483
|
+
|
|
1484
|
+
Parameters:
|
|
1485
|
+
- function_name (str): Name of the ToolUniverse tool to execute
|
|
1486
|
+
- arguments (str): JSON string containing tool parameters
|
|
1487
|
+
|
|
1488
|
+
Features:
|
|
1489
|
+
- Bypasses MCP tool wrappers for direct execution
|
|
1490
|
+
- Supports any loaded ToolUniverse tool
|
|
1491
|
+
- Provides detailed error reporting
|
|
1492
|
+
- Uses thread pool for non-blocking execution
|
|
1493
|
+
|
|
1494
|
+
Use cases:
|
|
1495
|
+
- Administrative tool execution
|
|
1496
|
+
- Debugging tool behavior
|
|
1497
|
+
- Custom automation scripts
|
|
1498
|
+
|
|
1499
|
+
list_available_tooluniverse_tools:
|
|
1500
|
+
Comprehensive inventory of all available ToolUniverse tools.
|
|
1501
|
+
|
|
1502
|
+
Returns:
|
|
1503
|
+
- Complete tool catalog with names, descriptions, types
|
|
1504
|
+
- Parameter schemas and requirements for each tool
|
|
1505
|
+
- Tool statistics and categorization
|
|
1506
|
+
|
|
1507
|
+
Use cases:
|
|
1508
|
+
- Tool discovery and exploration
|
|
1509
|
+
- Documentation generation
|
|
1510
|
+
- Client capability mapping
|
|
1511
|
+
- Integration planning
|
|
1512
|
+
|
|
1513
|
+
Implementation Details:
|
|
1514
|
+
======================
|
|
1515
|
+
|
|
1516
|
+
Error Handling:
|
|
1517
|
+
- Each tool includes comprehensive try-catch blocks
|
|
1518
|
+
- Detailed error messages with context information
|
|
1519
|
+
- Graceful degradation when tools or data unavailable
|
|
1520
|
+
- JSON-formatted error responses for consistency
|
|
1521
|
+
|
|
1522
|
+
Thread Safety:
|
|
1523
|
+
- All tools use async execution patterns
|
|
1524
|
+
- Thread pool executor for CPU-intensive operations
|
|
1525
|
+
- Proper resource cleanup and management
|
|
1526
|
+
- Non-blocking I/O for network operations
|
|
1527
|
+
|
|
1528
|
+
Security Considerations:
|
|
1529
|
+
- execute_tooluniverse_function provides direct tool access
|
|
1530
|
+
- JSON parsing with proper validation
|
|
1531
|
+
- No file system access beyond ToolUniverse scope
|
|
1532
|
+
- Appropriate error message sanitization
|
|
1533
|
+
|
|
1534
|
+
Performance Optimization:
|
|
1535
|
+
- Lazy loading of tool information
|
|
1536
|
+
- Caching where appropriate
|
|
1537
|
+
- Minimal memory footprint
|
|
1538
|
+
- Efficient JSON serialization
|
|
1539
|
+
|
|
1540
|
+
Examples:
|
|
1541
|
+
=========
|
|
1542
|
+
|
|
1543
|
+
Server health check:
|
|
1544
|
+
```python
|
|
1545
|
+
info = await get_server_info()
|
|
1546
|
+
status = json.loads(info)
|
|
1547
|
+
if status['total_tooluniverse_tools'] > 0:
|
|
1548
|
+
# Server healthy
|
|
1549
|
+
pass
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
Direct tool execution:
|
|
1553
|
+
```python
|
|
1554
|
+
result = await execute_tooluniverse_function(
|
|
1555
|
+
function_name="UniProt_get_entry_by_accession",
|
|
1556
|
+
arguments='{"accession": "P05067"}'
|
|
1557
|
+
)
|
|
1558
|
+
```
|
|
1559
|
+
|
|
1560
|
+
Tool inventory:
|
|
1561
|
+
```python
|
|
1562
|
+
tools = await list_available_tooluniverse_tools()
|
|
1563
|
+
catalog = json.loads(tools)
|
|
1564
|
+
# Available: {catalog['total_tools']} tools
|
|
1565
|
+
```
|
|
1566
|
+
"""
|
|
1567
|
+
|
|
1568
|
+
@self.tool()
|
|
1569
|
+
async def get_server_info() -> str:
|
|
1570
|
+
"""
|
|
1571
|
+
Get information about the SMCP server and its capabilities.
|
|
1572
|
+
|
|
1573
|
+
Returns:
|
|
1574
|
+
JSON string containing server information
|
|
1575
|
+
"""
|
|
1576
|
+
try:
|
|
1577
|
+
info = {
|
|
1578
|
+
"server_name": self.name,
|
|
1579
|
+
"server_type": "SMCP (Scientific Model Context Protocol)",
|
|
1580
|
+
"fastmcp_available": FASTMCP_AVAILABLE,
|
|
1581
|
+
"tooluniverse_loaded": hasattr(self, "tooluniverse"),
|
|
1582
|
+
"total_exposed_tools": len(self._exposed_tools),
|
|
1583
|
+
"search_enabled": self.search_enabled,
|
|
1584
|
+
"max_workers": self.max_workers,
|
|
1585
|
+
"tool_categories_loaded": len(
|
|
1586
|
+
getattr(self.tooluniverse, "tool_category_dicts", {})
|
|
1587
|
+
),
|
|
1588
|
+
"total_tooluniverse_tools": len(
|
|
1589
|
+
getattr(self.tooluniverse, "all_tools", [])
|
|
1590
|
+
),
|
|
1591
|
+
}
|
|
1592
|
+
return json.dumps(info, indent=2)
|
|
1593
|
+
|
|
1594
|
+
except Exception as e:
|
|
1595
|
+
return f"Error getting server info: {str(e)}"
|
|
1596
|
+
|
|
1597
|
+
@self.tool()
|
|
1598
|
+
async def execute_tooluniverse_function(
|
|
1599
|
+
function_name: str, arguments: str
|
|
1600
|
+
) -> str:
|
|
1601
|
+
"""
|
|
1602
|
+
Execute a ToolUniverse function directly with custom arguments.
|
|
1603
|
+
|
|
1604
|
+
Args:
|
|
1605
|
+
function_name: Name of the ToolUniverse function to execute
|
|
1606
|
+
arguments: JSON string of arguments to pass to the function
|
|
1607
|
+
|
|
1608
|
+
Returns:
|
|
1609
|
+
Function execution result
|
|
1610
|
+
"""
|
|
1611
|
+
try:
|
|
1612
|
+
# Parse arguments from JSON string
|
|
1613
|
+
import json
|
|
1614
|
+
|
|
1615
|
+
if isinstance(arguments, str):
|
|
1616
|
+
parsed_args = json.loads(arguments)
|
|
1617
|
+
else:
|
|
1618
|
+
parsed_args = arguments
|
|
1619
|
+
|
|
1620
|
+
function_call = {"name": function_name, "arguments": parsed_args}
|
|
1621
|
+
|
|
1622
|
+
# Execute in thread pool
|
|
1623
|
+
loop = asyncio.get_event_loop()
|
|
1624
|
+
result = await loop.run_in_executor(
|
|
1625
|
+
self.executor, self.tooluniverse.run_one_function, function_call
|
|
1626
|
+
)
|
|
1627
|
+
|
|
1628
|
+
return str(result)
|
|
1629
|
+
|
|
1630
|
+
except Exception as e:
|
|
1631
|
+
return f"Error executing {function_name}: {str(e)}"
|
|
1632
|
+
|
|
1633
|
+
@self.tool()
|
|
1634
|
+
async def list_available_tooluniverse_tools() -> str:
|
|
1635
|
+
"""
|
|
1636
|
+
List all available ToolUniverse tools that can be executed.
|
|
1637
|
+
|
|
1638
|
+
Returns:
|
|
1639
|
+
JSON string containing available tools and their descriptions
|
|
1640
|
+
"""
|
|
1641
|
+
try:
|
|
1642
|
+
# Check if ToolUniverse has loaded tools
|
|
1643
|
+
if (
|
|
1644
|
+
not hasattr(self.tooluniverse, "all_tools")
|
|
1645
|
+
or not self.tooluniverse.all_tools
|
|
1646
|
+
):
|
|
1647
|
+
return json.dumps({"error": "No ToolUniverse tools loaded"})
|
|
1648
|
+
|
|
1649
|
+
tools_info = []
|
|
1650
|
+
for tool_config in self.tooluniverse.all_tools:
|
|
1651
|
+
if isinstance(tool_config, dict):
|
|
1652
|
+
tool_info = {
|
|
1653
|
+
"name": tool_config.get("name", "Unknown"),
|
|
1654
|
+
"description": tool_config.get(
|
|
1655
|
+
"description", "No description available"
|
|
1656
|
+
),
|
|
1657
|
+
"type": tool_config.get("type", "Unknown"),
|
|
1658
|
+
"parameter_schema": tool_config.get("parameter", {}),
|
|
1659
|
+
}
|
|
1660
|
+
tools_info.append(tool_info)
|
|
1661
|
+
|
|
1662
|
+
return json.dumps(
|
|
1663
|
+
{"total_tools": len(tools_info), "tools": tools_info}, indent=2
|
|
1664
|
+
)
|
|
1665
|
+
|
|
1666
|
+
except Exception as e:
|
|
1667
|
+
return json.dumps({"error": f"Error listing tools: {str(e)}"}, indent=2)
|
|
1668
|
+
|
|
1669
|
+
def add_custom_tool(
|
|
1670
|
+
self, name: str, function: Callable, description: Optional[str] = None, **kwargs
|
|
1671
|
+
):
|
|
1672
|
+
"""
|
|
1673
|
+
Add a custom Python function as an MCP tool to the SMCP server.
|
|
1674
|
+
|
|
1675
|
+
This method provides a convenient way to extend SMCP functionality with
|
|
1676
|
+
custom tools beyond those provided by ToolUniverse. Custom tools are
|
|
1677
|
+
automatically integrated into the MCP interface and can be discovered
|
|
1678
|
+
and used by clients alongside existing tools.
|
|
1679
|
+
|
|
1680
|
+
Parameters:
|
|
1681
|
+
===========
|
|
1682
|
+
name : str
|
|
1683
|
+
Unique name for the tool in the MCP interface. Should be descriptive
|
|
1684
|
+
and follow naming conventions (lowercase with underscores preferred).
|
|
1685
|
+
Examples: "analyze_protein_sequence", "custom_data_processor"
|
|
1686
|
+
|
|
1687
|
+
function : Callable
|
|
1688
|
+
Python function to execute when the tool is called. The function:
|
|
1689
|
+
- Can be synchronous or asynchronous
|
|
1690
|
+
- Should have proper type annotations for parameters
|
|
1691
|
+
- Should include a comprehensive docstring
|
|
1692
|
+
- Will be automatically wrapped for MCP compatibility
|
|
1693
|
+
|
|
1694
|
+
description : str, optional
|
|
1695
|
+
Human-readable description of the tool's functionality. If provided,
|
|
1696
|
+
this will be set as the function's __doc__ attribute. If None, the
|
|
1697
|
+
function's existing docstring will be used.
|
|
1698
|
+
|
|
1699
|
+
**kwargs
|
|
1700
|
+
Additional FastMCP tool configuration options:
|
|
1701
|
+
- parameter_schema: Custom JSON schema for parameters
|
|
1702
|
+
- return_schema: Schema for return values
|
|
1703
|
+
- examples: Usage examples for the tool
|
|
1704
|
+
- tags: Categorization tags
|
|
1705
|
+
|
|
1706
|
+
Returns:
|
|
1707
|
+
========
|
|
1708
|
+
Callable
|
|
1709
|
+
The decorated function registered with FastMCP framework.
|
|
1710
|
+
|
|
1711
|
+
Usage Examples:
|
|
1712
|
+
==============
|
|
1713
|
+
|
|
1714
|
+
Simple synchronous function:
|
|
1715
|
+
```python
|
|
1716
|
+
def analyze_text(text: str, max_length: int = 100) -> str:
|
|
1717
|
+
'''Analyze text and return summary.'''
|
|
1718
|
+
return text[:max_length] + "..." if len(text) > max_length else text
|
|
1719
|
+
|
|
1720
|
+
server.add_custom_tool(
|
|
1721
|
+
name="text_analyzer",
|
|
1722
|
+
function=analyze_text,
|
|
1723
|
+
description="Analyze and summarize text content"
|
|
1724
|
+
)
|
|
1725
|
+
```
|
|
1726
|
+
|
|
1727
|
+
Asynchronous function with complex parameters:
|
|
1728
|
+
```python
|
|
1729
|
+
async def process_data(
|
|
1730
|
+
data: List[Dict[str, Any]],
|
|
1731
|
+
processing_type: str = "standard"
|
|
1732
|
+
) -> Dict[str, Any]:
|
|
1733
|
+
'''Process scientific data with specified method.'''
|
|
1734
|
+
# Custom processing logic here
|
|
1735
|
+
return {"processed_items": len(data), "type": processing_type}
|
|
1736
|
+
|
|
1737
|
+
server.add_custom_tool(
|
|
1738
|
+
name="data_processor",
|
|
1739
|
+
function=process_data
|
|
1740
|
+
)
|
|
1741
|
+
```
|
|
1742
|
+
|
|
1743
|
+
Function with custom schema:
|
|
1744
|
+
```python
|
|
1745
|
+
def calculate_score(values: List[float]) -> float:
|
|
1746
|
+
'''Calculate composite score from values.'''
|
|
1747
|
+
return sum(values) / len(values) if values else 0.0
|
|
1748
|
+
|
|
1749
|
+
server.add_custom_tool(
|
|
1750
|
+
name="score_calculator",
|
|
1751
|
+
function=calculate_score,
|
|
1752
|
+
parameter_schema={
|
|
1753
|
+
"type": "object",
|
|
1754
|
+
"properties": {
|
|
1755
|
+
"values": {
|
|
1756
|
+
"type": "array",
|
|
1757
|
+
"items": {"type": "number"},
|
|
1758
|
+
"description": "List of numeric values to process"
|
|
1759
|
+
}
|
|
1760
|
+
},
|
|
1761
|
+
"required": ["values"]
|
|
1762
|
+
}
|
|
1763
|
+
)
|
|
1764
|
+
```
|
|
1765
|
+
|
|
1766
|
+
Integration with ToolUniverse:
|
|
1767
|
+
=============================
|
|
1768
|
+
Custom tools work seamlessly alongside ToolUniverse tools:
|
|
1769
|
+
- Appear in tool discovery searches
|
|
1770
|
+
- Follow same calling conventions
|
|
1771
|
+
- Include in server diagnostics and listings
|
|
1772
|
+
- Support all MCP client interaction patterns
|
|
1773
|
+
|
|
1774
|
+
Best Practices:
|
|
1775
|
+
==============
|
|
1776
|
+
- Use descriptive, unique tool names
|
|
1777
|
+
- Include comprehensive docstrings
|
|
1778
|
+
- Add proper type annotations for parameters
|
|
1779
|
+
- Handle errors gracefully within the function
|
|
1780
|
+
- Consider async functions for I/O-bound operations
|
|
1781
|
+
- Test tools thoroughly before deployment
|
|
1782
|
+
|
|
1783
|
+
Notes:
|
|
1784
|
+
======
|
|
1785
|
+
- Custom tools are registered immediately upon addition
|
|
1786
|
+
- Tools can be added before or after server startup
|
|
1787
|
+
- Function signature determines parameter schema automatically
|
|
1788
|
+
- Custom tools support all FastMCP features and conventions
|
|
1789
|
+
"""
|
|
1790
|
+
if description:
|
|
1791
|
+
function.__doc__ = description
|
|
1792
|
+
|
|
1793
|
+
# Use FastMCP's tool decorator
|
|
1794
|
+
decorated_function = self.tool(name=name, **kwargs)(function)
|
|
1795
|
+
return decorated_function
|
|
1796
|
+
|
|
1797
|
+
async def close(self):
|
|
1798
|
+
"""
|
|
1799
|
+
Perform comprehensive cleanup and resource management during server shutdown.
|
|
1800
|
+
|
|
1801
|
+
This method ensures graceful shutdown of the SMCP server by properly cleaning
|
|
1802
|
+
up all resources, stopping background tasks, and releasing system resources.
|
|
1803
|
+
It's designed to be safe to call multiple times and handles errors gracefully.
|
|
1804
|
+
|
|
1805
|
+
Cleanup Operations:
|
|
1806
|
+
==================
|
|
1807
|
+
|
|
1808
|
+
**Thread Pool Shutdown:**
|
|
1809
|
+
- Gracefully stops the ThreadPoolExecutor used for tool execution
|
|
1810
|
+
- Waits for currently running tasks to complete
|
|
1811
|
+
- Prevents new tasks from being submitted
|
|
1812
|
+
- Times out after reasonable wait period to prevent hanging
|
|
1813
|
+
|
|
1814
|
+
**Resource Cleanup:**
|
|
1815
|
+
- Releases any open file handles or network connections
|
|
1816
|
+
- Clears internal caches and temporary data
|
|
1817
|
+
- Stops background monitoring tasks
|
|
1818
|
+
- Frees memory allocated for tool configurations
|
|
1819
|
+
|
|
1820
|
+
**Error Handling:**
|
|
1821
|
+
- Continues cleanup even if individual operations fail
|
|
1822
|
+
- Logs cleanup errors for debugging without raising exceptions
|
|
1823
|
+
- Ensures critical resources are always released
|
|
1824
|
+
|
|
1825
|
+
Usage Patterns:
|
|
1826
|
+
==============
|
|
1827
|
+
|
|
1828
|
+
**Automatic Cleanup (Recommended):**
|
|
1829
|
+
```python
|
|
1830
|
+
server = SMCP("My Server")
|
|
1831
|
+
try:
|
|
1832
|
+
server.run_simple() # Cleanup happens automatically on exit
|
|
1833
|
+
except KeyboardInterrupt:
|
|
1834
|
+
pass # run_simple() handles cleanup
|
|
1835
|
+
```
|
|
1836
|
+
|
|
1837
|
+
**Manual Cleanup:**
|
|
1838
|
+
```python
|
|
1839
|
+
server = SMCP("My Server")
|
|
1840
|
+
try:
|
|
1841
|
+
# Custom server logic here
|
|
1842
|
+
pass
|
|
1843
|
+
finally:
|
|
1844
|
+
await server.close() # Explicit cleanup
|
|
1845
|
+
```
|
|
1846
|
+
|
|
1847
|
+
**Context Manager Pattern:**
|
|
1848
|
+
```python
|
|
1849
|
+
async with SMCP("My Server") as server:
|
|
1850
|
+
# Server operations
|
|
1851
|
+
pass
|
|
1852
|
+
# Cleanup happens automatically
|
|
1853
|
+
```
|
|
1854
|
+
|
|
1855
|
+
Performance Considerations:
|
|
1856
|
+
==========================
|
|
1857
|
+
- Cleanup operations are typically fast (< 1 second)
|
|
1858
|
+
- Thread pool shutdown may take longer if tasks are running
|
|
1859
|
+
- Network connections are closed immediately
|
|
1860
|
+
- Memory cleanup depends on garbage collection
|
|
1861
|
+
|
|
1862
|
+
Error Recovery:
|
|
1863
|
+
==============
|
|
1864
|
+
- Individual cleanup failures don't stop the overall process
|
|
1865
|
+
- Critical errors are logged but don't raise exceptions
|
|
1866
|
+
- Cleanup is idempotent - safe to call multiple times
|
|
1867
|
+
- System resources are guaranteed to be released
|
|
1868
|
+
|
|
1869
|
+
Notes:
|
|
1870
|
+
======
|
|
1871
|
+
- This method is called automatically by run_simple() on shutdown
|
|
1872
|
+
- Can be called manually for custom server lifecycle management
|
|
1873
|
+
- Async method to properly handle async resource cleanup
|
|
1874
|
+
- Safe to call even if server hasn't been fully initialized
|
|
1875
|
+
"""
|
|
1876
|
+
try:
|
|
1877
|
+
# Shutdown thread pool
|
|
1878
|
+
self.executor.shutdown(wait=True)
|
|
1879
|
+
except Exception:
|
|
1880
|
+
pass
|
|
1881
|
+
|
|
1882
|
+
def run_simple(
|
|
1883
|
+
self,
|
|
1884
|
+
transport: Literal["stdio", "http", "sse"] = "http",
|
|
1885
|
+
host: str = "0.0.0.0",
|
|
1886
|
+
port: int = 7000,
|
|
1887
|
+
**kwargs,
|
|
1888
|
+
):
|
|
1889
|
+
"""
|
|
1890
|
+
Start the SMCP server with simplified configuration and automatic setup.
|
|
1891
|
+
|
|
1892
|
+
This method provides a convenient way to launch the SMCP server with sensible
|
|
1893
|
+
defaults for different deployment scenarios. It handles transport configuration,
|
|
1894
|
+
logging setup, and graceful shutdown automatically.
|
|
1895
|
+
|
|
1896
|
+
Parameters:
|
|
1897
|
+
===========
|
|
1898
|
+
transport : {"stdio", "http", "sse"}, default "http"
|
|
1899
|
+
Communication transport protocol:
|
|
1900
|
+
|
|
1901
|
+
- "stdio": Standard input/output communication
|
|
1902
|
+
* Best for: Command-line tools, subprocess integration
|
|
1903
|
+
* Pros: Low overhead, simple integration
|
|
1904
|
+
* Cons: Single client, no network access
|
|
1905
|
+
|
|
1906
|
+
- "http": HTTP-based communication (streamable-http)
|
|
1907
|
+
* Best for: Web applications, REST API integration
|
|
1908
|
+
* Pros: Wide compatibility, stateless, scalable
|
|
1909
|
+
* Cons: Higher overhead than stdio
|
|
1910
|
+
|
|
1911
|
+
- "sse": Server-Sent Events over HTTP
|
|
1912
|
+
* Best for: Real-time applications, streaming responses
|
|
1913
|
+
* Pros: Real-time communication, web-compatible
|
|
1914
|
+
* Cons: Browser limitations, more complex
|
|
1915
|
+
|
|
1916
|
+
host : str, default "0.0.0.0"
|
|
1917
|
+
Server bind address for HTTP/SSE transports:
|
|
1918
|
+
- "0.0.0.0": Listen on all network interfaces (default)
|
|
1919
|
+
- "127.0.0.1": localhost only (more secure)
|
|
1920
|
+
- Specific IP: Bind to particular interface
|
|
1921
|
+
|
|
1922
|
+
port : int, default 7000
|
|
1923
|
+
Server port for HTTP/SSE transports. Choose ports:
|
|
1924
|
+
- 7000-7999: Recommended range for SMCP servers
|
|
1925
|
+
- Above 1024: No root privileges required
|
|
1926
|
+
- Check availability: Ensure port isn't already in use
|
|
1927
|
+
|
|
1928
|
+
**kwargs
|
|
1929
|
+
Additional arguments passed to FastMCP's run() method:
|
|
1930
|
+
- debug (bool): Enable debug logging
|
|
1931
|
+
- access_log (bool): Log client requests
|
|
1932
|
+
- workers (int): Number of worker processes (HTTP only)
|
|
1933
|
+
|
|
1934
|
+
Server Startup Process:
|
|
1935
|
+
======================
|
|
1936
|
+
1. **Initialization Summary**: Displays server configuration and capabilities
|
|
1937
|
+
2. **Transport Setup**: Configures selected communication method
|
|
1938
|
+
3. **Service Start**: Begins listening for client connections
|
|
1939
|
+
4. **Graceful Shutdown**: Handles interrupts and cleanup
|
|
1940
|
+
|
|
1941
|
+
Deployment Scenarios:
|
|
1942
|
+
====================
|
|
1943
|
+
|
|
1944
|
+
Development & Testing:
|
|
1945
|
+
```python
|
|
1946
|
+
server = SMCP(name="Dev Server")
|
|
1947
|
+
server.run_simple(transport="stdio") # For CLI testing
|
|
1948
|
+
```
|
|
1949
|
+
|
|
1950
|
+
Local Web Service:
|
|
1951
|
+
```python
|
|
1952
|
+
server = SMCP(name="Local API")
|
|
1953
|
+
server.run_simple(transport="http", host="127.0.0.1", port=8000)
|
|
1954
|
+
```
|
|
1955
|
+
|
|
1956
|
+
Production Service:
|
|
1957
|
+
```python
|
|
1958
|
+
server = SMCP(
|
|
1959
|
+
name="Production SMCP",
|
|
1960
|
+
tool_categories=["ChEMBL", "uniprot", "opentarget"],
|
|
1961
|
+
max_workers=20
|
|
1962
|
+
)
|
|
1963
|
+
server.run_simple(
|
|
1964
|
+
transport="http",
|
|
1965
|
+
host="0.0.0.0",
|
|
1966
|
+
port=7000,
|
|
1967
|
+
workers=4
|
|
1968
|
+
)
|
|
1969
|
+
```
|
|
1970
|
+
|
|
1971
|
+
Real-time Applications:
|
|
1972
|
+
```python
|
|
1973
|
+
server = SMCP(name="Streaming API")
|
|
1974
|
+
server.run_simple(transport="sse", port=7001)
|
|
1975
|
+
```
|
|
1976
|
+
|
|
1977
|
+
Error Handling:
|
|
1978
|
+
==============
|
|
1979
|
+
- **KeyboardInterrupt**: Graceful shutdown on Ctrl+C
|
|
1980
|
+
- **Port in Use**: Clear error message with suggestions
|
|
1981
|
+
- **Transport Errors**: Detailed debugging information
|
|
1982
|
+
- **Cleanup**: Automatic resource cleanup on exit
|
|
1983
|
+
|
|
1984
|
+
Logging Output:
|
|
1985
|
+
==============
|
|
1986
|
+
Provides informative startup messages:
|
|
1987
|
+
```
|
|
1988
|
+
🚀 Starting SMCP server 'My Server'...
|
|
1989
|
+
📊 Loaded 356 tools from ToolUniverse
|
|
1990
|
+
🔍 Search enabled: True
|
|
1991
|
+
🌐 Server running on http://0.0.0.0:7000
|
|
1992
|
+
```
|
|
1993
|
+
|
|
1994
|
+
Security Considerations:
|
|
1995
|
+
=======================
|
|
1996
|
+
- Use host="127.0.0.1" for local-only access
|
|
1997
|
+
- Configure firewall rules for production deployment
|
|
1998
|
+
- Consider HTTPS termination with reverse proxy
|
|
1999
|
+
- Validate all client inputs through MCP protocol
|
|
2000
|
+
|
|
2001
|
+
Performance Notes:
|
|
2002
|
+
=================
|
|
2003
|
+
- HTTP transport supports multiple concurrent clients
|
|
2004
|
+
- stdio transport is single-client but lower latency
|
|
2005
|
+
- SSE transport enables real-time bidirectional communication
|
|
2006
|
+
- Thread pool size affects concurrent tool execution capacity
|
|
2007
|
+
"""
|
|
2008
|
+
self.logger.info(f"🚀 Starting SMCP server '{self.name}'...")
|
|
2009
|
+
self.logger.info(
|
|
2010
|
+
f"📊 Loaded {len(self._exposed_tools)} tools from ToolUniverse"
|
|
2011
|
+
)
|
|
2012
|
+
self.logger.info(f"🔍 Search enabled: {self.search_enabled}")
|
|
2013
|
+
|
|
2014
|
+
# Log hook configuration
|
|
2015
|
+
if self.hooks_enabled or self.hook_type:
|
|
2016
|
+
if self.hook_type:
|
|
2017
|
+
self.logger.info(f"🔗 Hooks enabled: {self.hook_type}")
|
|
2018
|
+
elif self.hook_config:
|
|
2019
|
+
hook_count = len(self.hook_config.get("hooks", []))
|
|
2020
|
+
self.logger.info(f"🔗 Hooks enabled: {hook_count} custom hooks")
|
|
2021
|
+
else:
|
|
2022
|
+
self.logger.info("🔗 Hooks enabled: default configuration")
|
|
2023
|
+
else:
|
|
2024
|
+
self.logger.info("🔗 Hooks disabled")
|
|
2025
|
+
|
|
2026
|
+
try:
|
|
2027
|
+
if transport == "stdio":
|
|
2028
|
+
self.run(transport="stdio", **kwargs)
|
|
2029
|
+
elif transport == "http":
|
|
2030
|
+
self.run(transport="streamable-http", host=host, port=port, **kwargs)
|
|
2031
|
+
elif transport == "sse":
|
|
2032
|
+
self.run(transport="sse", host=host, port=port, **kwargs)
|
|
2033
|
+
else:
|
|
2034
|
+
raise ValueError(f"Unsupported transport: {transport}")
|
|
2035
|
+
|
|
2036
|
+
except KeyboardInterrupt:
|
|
2037
|
+
self.logger.info("\n🛑 Server stopped by user")
|
|
2038
|
+
except Exception as e:
|
|
2039
|
+
self.logger.error(f"❌ Server error: {e}")
|
|
2040
|
+
finally:
|
|
2041
|
+
# Cleanup
|
|
2042
|
+
asyncio.run(self.close())
|
|
2043
|
+
|
|
2044
|
+
def _create_mcp_tool_from_tooluniverse(self, tool_config: Dict[str, Any]):
|
|
2045
|
+
"""Create an MCP tool from a ToolUniverse tool configuration.
|
|
2046
|
+
|
|
2047
|
+
This method creates a function with proper parameter signatures that match
|
|
2048
|
+
the ToolUniverse tool schema, enabling FastMCP's automatic parameter validation.
|
|
2049
|
+
"""
|
|
2050
|
+
try:
|
|
2051
|
+
# Debug: Ensure tool_config is a dictionary
|
|
2052
|
+
if not isinstance(tool_config, dict):
|
|
2053
|
+
raise ValueError(
|
|
2054
|
+
f"tool_config must be a dictionary, got {type(tool_config)}: {tool_config}"
|
|
2055
|
+
)
|
|
2056
|
+
|
|
2057
|
+
tool_name = tool_config["name"]
|
|
2058
|
+
description = tool_config.get(
|
|
2059
|
+
"description", f"ToolUniverse tool: {tool_name}"
|
|
2060
|
+
)
|
|
2061
|
+
parameters = tool_config.get("parameter", {})
|
|
2062
|
+
|
|
2063
|
+
# Extract parameter information from the schema
|
|
2064
|
+
# Handle case where properties might be None (like in Finish tool)
|
|
2065
|
+
properties = parameters.get("properties")
|
|
2066
|
+
if properties is None:
|
|
2067
|
+
properties = {}
|
|
2068
|
+
required_params = parameters.get("required", [])
|
|
2069
|
+
|
|
2070
|
+
# Handle non-standard schema format where 'required' is set on individual properties
|
|
2071
|
+
# instead of at the object level (common in ToolUniverse schemas)
|
|
2072
|
+
if not required_params and properties:
|
|
2073
|
+
required_params = [
|
|
2074
|
+
param_name
|
|
2075
|
+
for param_name, param_info in properties.items()
|
|
2076
|
+
if param_info.get("required", False)
|
|
2077
|
+
]
|
|
2078
|
+
|
|
2079
|
+
# Build function signature dynamically with Pydantic Field support
|
|
2080
|
+
import inspect
|
|
2081
|
+
from typing import Annotated
|
|
2082
|
+
from pydantic import Field
|
|
2083
|
+
|
|
2084
|
+
# Create parameter signature for the function
|
|
2085
|
+
func_params = []
|
|
2086
|
+
param_annotations = {}
|
|
2087
|
+
|
|
2088
|
+
for param_name, param_info in properties.items():
|
|
2089
|
+
param_type = param_info.get("type", "string")
|
|
2090
|
+
param_description = param_info.get(
|
|
2091
|
+
"description", f"{param_name} parameter"
|
|
2092
|
+
)
|
|
2093
|
+
is_required = param_name in required_params
|
|
2094
|
+
|
|
2095
|
+
# Map JSON schema types to Python types and create appropriate Field
|
|
2096
|
+
field_kwargs = {"description": param_description}
|
|
2097
|
+
|
|
2098
|
+
if param_type == "string":
|
|
2099
|
+
python_type = str
|
|
2100
|
+
# For string type, don't add json_schema_extra - let Pydantic handle it
|
|
2101
|
+
elif param_type == "integer":
|
|
2102
|
+
python_type = int
|
|
2103
|
+
# For integer type, don't add json_schema_extra - let Pydantic handle it
|
|
2104
|
+
elif param_type == "number":
|
|
2105
|
+
python_type = float
|
|
2106
|
+
# For number type, don't add json_schema_extra - let Pydantic handle it
|
|
2107
|
+
elif param_type == "boolean":
|
|
2108
|
+
python_type = bool
|
|
2109
|
+
# For boolean type, don't add json_schema_extra - let Pydantic handle it
|
|
2110
|
+
elif param_type == "array":
|
|
2111
|
+
python_type = list
|
|
2112
|
+
# Add array-specific schema information only for complex cases
|
|
2113
|
+
items_info = param_info.get("items", {})
|
|
2114
|
+
if items_info:
|
|
2115
|
+
# Clean up items definition - remove invalid fields
|
|
2116
|
+
cleaned_items = items_info.copy()
|
|
2117
|
+
|
|
2118
|
+
# Remove 'required' field from items (not valid in JSON Schema for array items)
|
|
2119
|
+
if "required" in cleaned_items:
|
|
2120
|
+
cleaned_items.pop("required")
|
|
2121
|
+
|
|
2122
|
+
field_kwargs["json_schema_extra"] = {
|
|
2123
|
+
"type": "array",
|
|
2124
|
+
"items": cleaned_items,
|
|
2125
|
+
}
|
|
2126
|
+
else:
|
|
2127
|
+
# If no items specified, default to string items
|
|
2128
|
+
field_kwargs["json_schema_extra"] = {
|
|
2129
|
+
"type": "array",
|
|
2130
|
+
"items": {"type": "string"},
|
|
2131
|
+
}
|
|
2132
|
+
elif param_type == "object":
|
|
2133
|
+
python_type = dict
|
|
2134
|
+
# Add object-specific schema information
|
|
2135
|
+
object_props = param_info.get("properties", {})
|
|
2136
|
+
if object_props:
|
|
2137
|
+
# Clean up the nested object properties - fix common schema issues
|
|
2138
|
+
cleaned_props = {}
|
|
2139
|
+
nested_required = []
|
|
2140
|
+
|
|
2141
|
+
for prop_name, prop_info in object_props.items():
|
|
2142
|
+
cleaned_prop = prop_info.copy()
|
|
2143
|
+
|
|
2144
|
+
# Fix string "True"/"False" in required field (common ToolUniverse issue)
|
|
2145
|
+
if "required" in cleaned_prop:
|
|
2146
|
+
req_value = cleaned_prop.pop("required")
|
|
2147
|
+
if req_value in ["True", "true", True]:
|
|
2148
|
+
nested_required.append(prop_name)
|
|
2149
|
+
# Remove the individual required field as it should be at object level
|
|
2150
|
+
|
|
2151
|
+
cleaned_props[prop_name] = cleaned_prop
|
|
2152
|
+
|
|
2153
|
+
# Create proper JSON schema for nested object
|
|
2154
|
+
object_schema = {"type": "object", "properties": cleaned_props}
|
|
2155
|
+
|
|
2156
|
+
# Add required array at object level if there are required fields
|
|
2157
|
+
if nested_required:
|
|
2158
|
+
object_schema["required"] = nested_required
|
|
2159
|
+
|
|
2160
|
+
field_kwargs["json_schema_extra"] = object_schema
|
|
2161
|
+
else:
|
|
2162
|
+
# For unknown types, default to string and only add type info if it's truly unknown
|
|
2163
|
+
python_type = str
|
|
2164
|
+
if param_type not in [
|
|
2165
|
+
"string",
|
|
2166
|
+
"integer",
|
|
2167
|
+
"number",
|
|
2168
|
+
"boolean",
|
|
2169
|
+
"array",
|
|
2170
|
+
"object",
|
|
2171
|
+
]:
|
|
2172
|
+
field_kwargs["json_schema_extra"] = {"type": param_type}
|
|
2173
|
+
|
|
2174
|
+
# Create Pydantic Field with enhanced schema information
|
|
2175
|
+
pydantic_field = Field(**field_kwargs)
|
|
2176
|
+
|
|
2177
|
+
if is_required:
|
|
2178
|
+
# Required parameter with description and schema info
|
|
2179
|
+
annotated_type = Annotated[python_type, pydantic_field]
|
|
2180
|
+
param_annotations[param_name] = annotated_type
|
|
2181
|
+
func_params.append(
|
|
2182
|
+
inspect.Parameter(
|
|
2183
|
+
param_name,
|
|
2184
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
2185
|
+
annotation=annotated_type,
|
|
2186
|
+
)
|
|
2187
|
+
)
|
|
2188
|
+
else:
|
|
2189
|
+
# Optional parameter with description, schema info and default value
|
|
2190
|
+
annotated_type = Annotated[
|
|
2191
|
+
Union[python_type, type(None)], pydantic_field
|
|
2192
|
+
]
|
|
2193
|
+
param_annotations[param_name] = annotated_type
|
|
2194
|
+
func_params.append(
|
|
2195
|
+
inspect.Parameter(
|
|
2196
|
+
param_name,
|
|
2197
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
2198
|
+
default=None,
|
|
2199
|
+
annotation=annotated_type,
|
|
2200
|
+
)
|
|
2201
|
+
)
|
|
2202
|
+
|
|
2203
|
+
# Create the async function with dynamic signature
|
|
2204
|
+
if not properties:
|
|
2205
|
+
# Tool has no parameters - create simple function
|
|
2206
|
+
async def dynamic_tool_function() -> str:
|
|
2207
|
+
"""Execute ToolUniverse tool with no arguments."""
|
|
2208
|
+
try:
|
|
2209
|
+
# Prepare function call with empty arguments
|
|
2210
|
+
function_call = {"name": tool_name, "arguments": {}}
|
|
2211
|
+
|
|
2212
|
+
# Execute in thread pool to avoid blocking
|
|
2213
|
+
loop = asyncio.get_event_loop()
|
|
2214
|
+
result = await loop.run_in_executor(
|
|
2215
|
+
self.executor,
|
|
2216
|
+
self.tooluniverse.run_one_function,
|
|
2217
|
+
function_call,
|
|
2218
|
+
)
|
|
2219
|
+
|
|
2220
|
+
# Format the result
|
|
2221
|
+
if isinstance(result, str):
|
|
2222
|
+
return result
|
|
2223
|
+
else:
|
|
2224
|
+
return json.dumps(result, indent=2, default=str)
|
|
2225
|
+
|
|
2226
|
+
except Exception as e:
|
|
2227
|
+
error_msg = f"Error executing {tool_name}: {str(e)}"
|
|
2228
|
+
self.logger.error(error_msg)
|
|
2229
|
+
return json.dumps({"error": error_msg}, indent=2)
|
|
2230
|
+
|
|
2231
|
+
# Set function metadata
|
|
2232
|
+
dynamic_tool_function.__name__ = tool_name
|
|
2233
|
+
dynamic_tool_function.__signature__ = inspect.Signature([])
|
|
2234
|
+
dynamic_tool_function.__annotations__ = {"return": str}
|
|
2235
|
+
|
|
2236
|
+
else:
|
|
2237
|
+
# Tool has parameters - create function with dynamic signature
|
|
2238
|
+
async def dynamic_tool_function(**kwargs) -> str:
|
|
2239
|
+
"""Execute ToolUniverse tool with provided arguments."""
|
|
2240
|
+
try:
|
|
2241
|
+
# Filter out None values for optional parameters
|
|
2242
|
+
args_dict = {k: v for k, v in kwargs.items() if v is not None}
|
|
2243
|
+
|
|
2244
|
+
# Validate required parameters
|
|
2245
|
+
missing_required = [
|
|
2246
|
+
param for param in required_params if param not in args_dict
|
|
2247
|
+
]
|
|
2248
|
+
if missing_required:
|
|
2249
|
+
return json.dumps(
|
|
2250
|
+
{
|
|
2251
|
+
"error": f"Missing required parameters: {missing_required}",
|
|
2252
|
+
"required": required_params,
|
|
2253
|
+
"provided": list(args_dict.keys()),
|
|
2254
|
+
},
|
|
2255
|
+
indent=2,
|
|
2256
|
+
)
|
|
2257
|
+
|
|
2258
|
+
# Prepare function call
|
|
2259
|
+
function_call = {"name": tool_name, "arguments": args_dict}
|
|
2260
|
+
|
|
2261
|
+
# Execute in thread pool to avoid blocking
|
|
2262
|
+
loop = asyncio.get_event_loop()
|
|
2263
|
+
result = await loop.run_in_executor(
|
|
2264
|
+
self.executor,
|
|
2265
|
+
self.tooluniverse.run_one_function,
|
|
2266
|
+
function_call,
|
|
2267
|
+
)
|
|
2268
|
+
|
|
2269
|
+
# Format the result
|
|
2270
|
+
if isinstance(result, str):
|
|
2271
|
+
return result
|
|
2272
|
+
else:
|
|
2273
|
+
return json.dumps(result, indent=2, default=str)
|
|
2274
|
+
|
|
2275
|
+
except Exception as e:
|
|
2276
|
+
error_msg = f"Error executing {tool_name}: {str(e)}"
|
|
2277
|
+
self.logger.error(error_msg)
|
|
2278
|
+
return json.dumps({"error": error_msg}, indent=2)
|
|
2279
|
+
|
|
2280
|
+
# Set function metadata
|
|
2281
|
+
dynamic_tool_function.__name__ = tool_name
|
|
2282
|
+
|
|
2283
|
+
# Set function signature dynamically for tools with parameters
|
|
2284
|
+
if func_params:
|
|
2285
|
+
dynamic_tool_function.__signature__ = inspect.Signature(func_params)
|
|
2286
|
+
|
|
2287
|
+
# Set annotations for type hints
|
|
2288
|
+
dynamic_tool_function.__annotations__ = param_annotations.copy()
|
|
2289
|
+
dynamic_tool_function.__annotations__["return"] = str
|
|
2290
|
+
|
|
2291
|
+
# Create detailed docstring for internal use, but use clean description for FastMCP
|
|
2292
|
+
param_docs = []
|
|
2293
|
+
for param_name, param_info in properties.items():
|
|
2294
|
+
param_desc = param_info.get("description", f"{param_name} parameter")
|
|
2295
|
+
param_type = param_info.get("type", "string")
|
|
2296
|
+
is_required = param_name in required_params
|
|
2297
|
+
required_text = "required" if is_required else "optional"
|
|
2298
|
+
param_docs.append(
|
|
2299
|
+
f" {param_name} ({param_type}, {required_text}): {param_desc}"
|
|
2300
|
+
)
|
|
2301
|
+
|
|
2302
|
+
# Set a simple docstring for the function (internal use)
|
|
2303
|
+
dynamic_tool_function.__doc__ = f"""{description}
|
|
2304
|
+
|
|
2305
|
+
Returns:
|
|
2306
|
+
str: Tool execution result
|
|
2307
|
+
"""
|
|
2308
|
+
|
|
2309
|
+
# Register with FastMCP using explicit description (clean, without parameter list)
|
|
2310
|
+
self.tool(description=description)(dynamic_tool_function)
|
|
2311
|
+
|
|
2312
|
+
except Exception as e:
|
|
2313
|
+
self.logger.error(f"Error creating MCP tool from config: {e}")
|
|
2314
|
+
self.logger.debug(f"Tool config: {tool_config}")
|
|
2315
|
+
# Don't raise - continue with other tools
|
|
2316
|
+
return
|
|
2317
|
+
|
|
2318
|
+
|
|
2319
|
+
# Convenience function for quick server creation
|
|
2320
|
+
def create_smcp_server(
|
|
2321
|
+
name: str = "SMCP Server",
|
|
2322
|
+
tool_categories: Optional[List[str]] = None,
|
|
2323
|
+
search_enabled: bool = True,
|
|
2324
|
+
**kwargs,
|
|
2325
|
+
) -> SMCP:
|
|
2326
|
+
"""
|
|
2327
|
+
Create a configured SMCP server with common defaults and best practices.
|
|
2328
|
+
|
|
2329
|
+
This convenience function simplifies SMCP server creation by providing
|
|
2330
|
+
sensible defaults for common use cases while still allowing full customization
|
|
2331
|
+
through additional parameters.
|
|
2332
|
+
|
|
2333
|
+
Parameters:
|
|
2334
|
+
===========
|
|
2335
|
+
name : str, default "SMCP Server"
|
|
2336
|
+
Human-readable server name used in logs and server identification.
|
|
2337
|
+
Choose descriptive names like:
|
|
2338
|
+
- "Scientific Research API"
|
|
2339
|
+
- "Drug Discovery Server"
|
|
2340
|
+
- "Proteomics Analysis Service"
|
|
2341
|
+
|
|
2342
|
+
tool_categories : list of str, optional
|
|
2343
|
+
Specific ToolUniverse categories to load. If None, loads all available
|
|
2344
|
+
tools (350+ tools). Common category combinations:
|
|
2345
|
+
|
|
2346
|
+
Scientific Research:
|
|
2347
|
+
["ChEMBL", "uniprot", "opentarget", "pubchem", "hpa"]
|
|
2348
|
+
|
|
2349
|
+
Drug Discovery:
|
|
2350
|
+
["ChEMBL", "fda_drug_label", "clinical_trials", "pubchem"]
|
|
2351
|
+
|
|
2352
|
+
Literature Analysis:
|
|
2353
|
+
["EuropePMC", "semantic_scholar", "pubtator", "agents"]
|
|
2354
|
+
|
|
2355
|
+
Minimal Setup:
|
|
2356
|
+
["tool_finder_llm", "special_tools"]
|
|
2357
|
+
|
|
2358
|
+
search_enabled : bool, default True
|
|
2359
|
+
Enable AI-powered tool discovery via tools/find method.
|
|
2360
|
+
Recommended to keep enabled unless you have specific performance
|
|
2361
|
+
requirements or want to minimize dependencies.
|
|
2362
|
+
|
|
2363
|
+
**kwargs
|
|
2364
|
+
Additional SMCP configuration options:
|
|
2365
|
+
|
|
2366
|
+
- tooluniverse_config: Pre-configured ToolUniverse instance
|
|
2367
|
+
- auto_expose_tools (bool, default True): Auto-expose ToolUniverse tools
|
|
2368
|
+
- max_workers (int, default 5): Thread pool size for tool execution
|
|
2369
|
+
- Any FastMCP server options (debug, logging, etc.)
|
|
2370
|
+
|
|
2371
|
+
Returns:
|
|
2372
|
+
========
|
|
2373
|
+
SMCP
|
|
2374
|
+
Fully configured SMCP server instance ready to run.
|
|
2375
|
+
|
|
2376
|
+
Usage Examples:
|
|
2377
|
+
==============
|
|
2378
|
+
|
|
2379
|
+
Quick Start (all tools):
|
|
2380
|
+
```python
|
|
2381
|
+
server = create_smcp_server("Research Server")
|
|
2382
|
+
server.run_simple()
|
|
2383
|
+
```
|
|
2384
|
+
|
|
2385
|
+
Focused Server (specific domains):
|
|
2386
|
+
```python
|
|
2387
|
+
server = create_smcp_server(
|
|
2388
|
+
name="Drug Discovery API",
|
|
2389
|
+
tool_categories=["ChEMBL", "fda_drug_label", "clinical_trials"],
|
|
2390
|
+
max_workers=10
|
|
2391
|
+
)
|
|
2392
|
+
server.run_simple(port=8000)
|
|
2393
|
+
```
|
|
2394
|
+
|
|
2395
|
+
Custom Configuration:
|
|
2396
|
+
```python
|
|
2397
|
+
server = create_smcp_server(
|
|
2398
|
+
name="High-Performance Server",
|
|
2399
|
+
search_enabled=True,
|
|
2400
|
+
max_workers=20,
|
|
2401
|
+
debug=True
|
|
2402
|
+
)
|
|
2403
|
+
server.run_simple(transport="http", host="0.0.0.0", port=7000)
|
|
2404
|
+
```
|
|
2405
|
+
|
|
2406
|
+
Pre-configured ToolUniverse:
|
|
2407
|
+
```python
|
|
2408
|
+
tu = ToolUniverse()
|
|
2409
|
+
tu.load_tools(tool_type=["uniprot", "ChEMBL"])
|
|
2410
|
+
server = create_smcp_server(
|
|
2411
|
+
name="Protein-Drug Server",
|
|
2412
|
+
tooluniverse_config=tu,
|
|
2413
|
+
search_enabled=True
|
|
2414
|
+
)
|
|
2415
|
+
```
|
|
2416
|
+
|
|
2417
|
+
Benefits of Using This Function:
|
|
2418
|
+
===============================
|
|
2419
|
+
|
|
2420
|
+
- **Simplified Setup**: Reduces boilerplate code for common configurations
|
|
2421
|
+
- **Best Practices**: Applies recommended settings automatically
|
|
2422
|
+
- **Consistent Naming**: Encourages good server naming conventions
|
|
2423
|
+
- **Future-Proof**: Will include new recommended defaults in future versions
|
|
2424
|
+
- **Documentation**: Provides clear examples and guidance
|
|
2425
|
+
|
|
2426
|
+
Equivalent Manual Configuration:
|
|
2427
|
+
===============================
|
|
2428
|
+
This function is equivalent to:
|
|
2429
|
+
```python
|
|
2430
|
+
server = SMCP(
|
|
2431
|
+
name=name,
|
|
2432
|
+
tool_categories=tool_categories,
|
|
2433
|
+
search_enabled=search_enabled,
|
|
2434
|
+
auto_expose_tools=True,
|
|
2435
|
+
max_workers=5,
|
|
2436
|
+
**kwargs
|
|
2437
|
+
)
|
|
2438
|
+
```
|
|
2439
|
+
|
|
2440
|
+
When to Use Manual Configuration:
|
|
2441
|
+
================================
|
|
2442
|
+
- Need precise control over all initialization parameters
|
|
2443
|
+
- Using custom ToolUniverse configurations
|
|
2444
|
+
- Implementing custom MCP methods or tools
|
|
2445
|
+
- Advanced deployment scenarios with specific requirements
|
|
2446
|
+
"""
|
|
2447
|
+
return SMCP(
|
|
2448
|
+
name=name,
|
|
2449
|
+
tool_categories=tool_categories,
|
|
2450
|
+
search_enabled=search_enabled,
|
|
2451
|
+
**kwargs,
|
|
2452
|
+
)
|