tooluniverse 0.1.4__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tooluniverse might be problematic. Click here for more details.

Files changed (187) hide show
  1. tooluniverse/__init__.py +340 -4
  2. tooluniverse/admetai_tool.py +84 -0
  3. tooluniverse/agentic_tool.py +563 -0
  4. tooluniverse/alphafold_tool.py +96 -0
  5. tooluniverse/base_tool.py +129 -6
  6. tooluniverse/boltz_tool.py +207 -0
  7. tooluniverse/chem_tool.py +192 -0
  8. tooluniverse/compose_scripts/__init__.py +1 -0
  9. tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
  10. tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
  11. tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
  12. tooluniverse/compose_scripts/literature_tool.py +34 -0
  13. tooluniverse/compose_scripts/output_summarizer.py +279 -0
  14. tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
  15. tooluniverse/compose_scripts/tool_discover.py +705 -0
  16. tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
  17. tooluniverse/compose_tool.py +371 -0
  18. tooluniverse/ctg_tool.py +1002 -0
  19. tooluniverse/custom_tool.py +81 -0
  20. tooluniverse/dailymed_tool.py +108 -0
  21. tooluniverse/data/admetai_tools.json +155 -0
  22. tooluniverse/data/agentic_tools.json +1156 -0
  23. tooluniverse/data/alphafold_tools.json +87 -0
  24. tooluniverse/data/boltz_tools.json +9 -0
  25. tooluniverse/data/chembl_tools.json +16 -0
  26. tooluniverse/data/clait_tools.json +108 -0
  27. tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
  28. tooluniverse/data/compose_tools.json +202 -0
  29. tooluniverse/data/dailymed_tools.json +70 -0
  30. tooluniverse/data/dataset_tools.json +646 -0
  31. tooluniverse/data/disease_target_score_tools.json +712 -0
  32. tooluniverse/data/efo_tools.json +17 -0
  33. tooluniverse/data/embedding_tools.json +319 -0
  34. tooluniverse/data/enrichr_tools.json +31 -0
  35. tooluniverse/data/europe_pmc_tools.json +22 -0
  36. tooluniverse/data/expert_feedback_tools.json +10 -0
  37. tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
  38. tooluniverse/data/fda_drug_labeling_tools.json +544 -168
  39. tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
  40. tooluniverse/data/finder_tools.json +209 -0
  41. tooluniverse/data/gene_ontology_tools.json +113 -0
  42. tooluniverse/data/gwas_tools.json +1082 -0
  43. tooluniverse/data/hpa_tools.json +333 -0
  44. tooluniverse/data/humanbase_tools.json +47 -0
  45. tooluniverse/data/idmap_tools.json +74 -0
  46. tooluniverse/data/mcp_client_tools_example.json +113 -0
  47. tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
  48. tooluniverse/data/medlineplus_tools.json +141 -0
  49. tooluniverse/data/monarch_tools.json +1 -1
  50. tooluniverse/data/openalex_tools.json +36 -0
  51. tooluniverse/data/opentarget_tools.json +82 -58
  52. tooluniverse/data/output_summarization_tools.json +101 -0
  53. tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
  54. tooluniverse/data/packages/categorized_tools.txt +206 -0
  55. tooluniverse/data/packages/cheminformatics_tools.json +347 -0
  56. tooluniverse/data/packages/earth_sciences_tools.json +74 -0
  57. tooluniverse/data/packages/genomics_tools.json +776 -0
  58. tooluniverse/data/packages/image_processing_tools.json +38 -0
  59. tooluniverse/data/packages/machine_learning_tools.json +789 -0
  60. tooluniverse/data/packages/neuroscience_tools.json +62 -0
  61. tooluniverse/data/packages/original_tools.txt +0 -0
  62. tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
  63. tooluniverse/data/packages/scientific_computing_tools.json +560 -0
  64. tooluniverse/data/packages/single_cell_tools.json +453 -0
  65. tooluniverse/data/packages/software_tools.json +4954 -0
  66. tooluniverse/data/packages/structural_biology_tools.json +396 -0
  67. tooluniverse/data/packages/visualization_tools.json +399 -0
  68. tooluniverse/data/pubchem_tools.json +215 -0
  69. tooluniverse/data/pubtator_tools.json +68 -0
  70. tooluniverse/data/rcsb_pdb_tools.json +1332 -0
  71. tooluniverse/data/reactome_tools.json +19 -0
  72. tooluniverse/data/semantic_scholar_tools.json +26 -0
  73. tooluniverse/data/special_tools.json +2 -25
  74. tooluniverse/data/tool_composition_tools.json +88 -0
  75. tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
  76. tooluniverse/data/txagent_client_tools.json +9 -0
  77. tooluniverse/data/uniprot_tools.json +211 -0
  78. tooluniverse/data/url_fetch_tools.json +94 -0
  79. tooluniverse/data/uspto_downloader_tools.json +9 -0
  80. tooluniverse/data/uspto_tools.json +811 -0
  81. tooluniverse/data/xml_tools.json +3275 -0
  82. tooluniverse/dataset_tool.py +296 -0
  83. tooluniverse/default_config.py +165 -0
  84. tooluniverse/efo_tool.py +42 -0
  85. tooluniverse/embedding_database.py +630 -0
  86. tooluniverse/embedding_sync.py +396 -0
  87. tooluniverse/enrichr_tool.py +266 -0
  88. tooluniverse/europe_pmc_tool.py +52 -0
  89. tooluniverse/execute_function.py +1775 -95
  90. tooluniverse/extended_hooks.py +444 -0
  91. tooluniverse/gene_ontology_tool.py +194 -0
  92. tooluniverse/graphql_tool.py +158 -36
  93. tooluniverse/gwas_tool.py +358 -0
  94. tooluniverse/hpa_tool.py +1645 -0
  95. tooluniverse/humanbase_tool.py +389 -0
  96. tooluniverse/logging_config.py +254 -0
  97. tooluniverse/mcp_client_tool.py +764 -0
  98. tooluniverse/mcp_integration.py +413 -0
  99. tooluniverse/mcp_tool_registry.py +925 -0
  100. tooluniverse/medlineplus_tool.py +337 -0
  101. tooluniverse/openalex_tool.py +228 -0
  102. tooluniverse/openfda_adv_tool.py +283 -0
  103. tooluniverse/openfda_tool.py +393 -160
  104. tooluniverse/output_hook.py +1122 -0
  105. tooluniverse/package_tool.py +195 -0
  106. tooluniverse/pubchem_tool.py +158 -0
  107. tooluniverse/pubtator_tool.py +168 -0
  108. tooluniverse/rcsb_pdb_tool.py +38 -0
  109. tooluniverse/reactome_tool.py +108 -0
  110. tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
  111. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
  112. tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
  113. tooluniverse/remote/expert_feedback/simple_test.py +23 -0
  114. tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
  115. tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
  116. tooluniverse/remote/expert_feedback_mcp/human_expert_mcp_server.py +1611 -0
  117. tooluniverse/remote/expert_feedback_mcp/simple_test.py +34 -0
  118. tooluniverse/remote/expert_feedback_mcp/start_web_interface.py +91 -0
  119. tooluniverse/remote/immune_compass/compass_tool.py +327 -0
  120. tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
  121. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
  122. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
  123. tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
  124. tooluniverse/remote_tool.py +99 -0
  125. tooluniverse/restful_tool.py +53 -30
  126. tooluniverse/scripts/generate_tool_graph.py +408 -0
  127. tooluniverse/scripts/visualize_tool_graph.py +829 -0
  128. tooluniverse/semantic_scholar_tool.py +62 -0
  129. tooluniverse/smcp.py +2452 -0
  130. tooluniverse/smcp_server.py +975 -0
  131. tooluniverse/test/mcp_server_test.py +0 -0
  132. tooluniverse/test/test_admetai_tool.py +370 -0
  133. tooluniverse/test/test_agentic_tool.py +129 -0
  134. tooluniverse/test/test_alphafold_tool.py +71 -0
  135. tooluniverse/test/test_chem_tool.py +37 -0
  136. tooluniverse/test/test_compose_lieraturereview.py +63 -0
  137. tooluniverse/test/test_compose_tool.py +448 -0
  138. tooluniverse/test/test_dailymed.py +69 -0
  139. tooluniverse/test/test_dataset_tool.py +200 -0
  140. tooluniverse/test/test_disease_target_score.py +56 -0
  141. tooluniverse/test/test_drugbank_filter_examples.py +179 -0
  142. tooluniverse/test/test_efo.py +31 -0
  143. tooluniverse/test/test_enrichr_tool.py +21 -0
  144. tooluniverse/test/test_europe_pmc_tool.py +20 -0
  145. tooluniverse/test/test_fda_adv.py +95 -0
  146. tooluniverse/test/test_fda_drug_labeling.py +91 -0
  147. tooluniverse/test/test_gene_ontology_tools.py +66 -0
  148. tooluniverse/test/test_gwas_tool.py +139 -0
  149. tooluniverse/test/test_hpa.py +625 -0
  150. tooluniverse/test/test_humanbase_tool.py +20 -0
  151. tooluniverse/test/test_idmap_tools.py +61 -0
  152. tooluniverse/test/test_mcp_server.py +211 -0
  153. tooluniverse/test/test_mcp_tool.py +247 -0
  154. tooluniverse/test/test_medlineplus.py +220 -0
  155. tooluniverse/test/test_openalex_tool.py +32 -0
  156. tooluniverse/test/test_opentargets.py +28 -0
  157. tooluniverse/test/test_pubchem_tool.py +116 -0
  158. tooluniverse/test/test_pubtator_tool.py +37 -0
  159. tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
  160. tooluniverse/test/test_reactome.py +54 -0
  161. tooluniverse/test/test_semantic_scholar_tool.py +24 -0
  162. tooluniverse/test/test_software_tools.py +147 -0
  163. tooluniverse/test/test_tool_description_optimizer.py +49 -0
  164. tooluniverse/test/test_tool_finder.py +26 -0
  165. tooluniverse/test/test_tool_finder_llm.py +252 -0
  166. tooluniverse/test/test_tools_find.py +195 -0
  167. tooluniverse/test/test_uniprot_tools.py +74 -0
  168. tooluniverse/test/test_uspto_tool.py +72 -0
  169. tooluniverse/test/test_xml_tool.py +113 -0
  170. tooluniverse/tool_finder_embedding.py +267 -0
  171. tooluniverse/tool_finder_keyword.py +693 -0
  172. tooluniverse/tool_finder_llm.py +699 -0
  173. tooluniverse/tool_graph_web_ui.py +955 -0
  174. tooluniverse/tool_registry.py +416 -0
  175. tooluniverse/uniprot_tool.py +155 -0
  176. tooluniverse/url_tool.py +253 -0
  177. tooluniverse/uspto_tool.py +240 -0
  178. tooluniverse/utils.py +369 -41
  179. tooluniverse/xml_tool.py +369 -0
  180. tooluniverse-1.0.0.dist-info/METADATA +377 -0
  181. tooluniverse-1.0.0.dist-info/RECORD +186 -0
  182. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/WHEEL +1 -1
  183. tooluniverse-1.0.0.dist-info/entry_points.txt +9 -0
  184. tooluniverse-0.1.4.dist-info/METADATA +0 -141
  185. tooluniverse-0.1.4.dist-info/RECORD +0 -18
  186. {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/licenses/LICENSE +0 -0
  187. {tooluniverse-0.1.4.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
+ )