tooluniverse 1.0.4__py3-none-any.whl → 1.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tooluniverse might be problematic. Click here for more details.
- tooluniverse/__init__.py +56 -5
- tooluniverse/agentic_tool.py +90 -14
- tooluniverse/arxiv_tool.py +113 -0
- tooluniverse/biorxiv_tool.py +97 -0
- tooluniverse/core_tool.py +153 -0
- tooluniverse/crossref_tool.py +73 -0
- tooluniverse/data/agentic_tools.json +2 -2
- tooluniverse/data/arxiv_tools.json +87 -0
- tooluniverse/data/biorxiv_tools.json +70 -0
- tooluniverse/data/core_tools.json +105 -0
- tooluniverse/data/crossref_tools.json +70 -0
- tooluniverse/data/dblp_tools.json +73 -0
- tooluniverse/data/doaj_tools.json +94 -0
- tooluniverse/data/fatcat_tools.json +72 -0
- tooluniverse/data/hal_tools.json +70 -0
- tooluniverse/data/medrxiv_tools.json +70 -0
- tooluniverse/data/odphp_tools.json +354 -0
- tooluniverse/data/openaire_tools.json +85 -0
- tooluniverse/data/osf_preprints_tools.json +77 -0
- tooluniverse/data/pmc_tools.json +109 -0
- tooluniverse/data/pubmed_tools.json +65 -0
- tooluniverse/data/unpaywall_tools.json +86 -0
- tooluniverse/data/wikidata_sparql_tools.json +42 -0
- tooluniverse/data/zenodo_tools.json +82 -0
- tooluniverse/dblp_tool.py +62 -0
- tooluniverse/default_config.py +18 -0
- tooluniverse/doaj_tool.py +124 -0
- tooluniverse/execute_function.py +70 -9
- tooluniverse/fatcat_tool.py +66 -0
- tooluniverse/hal_tool.py +77 -0
- tooluniverse/llm_clients.py +487 -0
- tooluniverse/mcp_tool_registry.py +3 -3
- tooluniverse/medrxiv_tool.py +97 -0
- tooluniverse/odphp_tool.py +226 -0
- tooluniverse/openaire_tool.py +145 -0
- tooluniverse/osf_preprints_tool.py +67 -0
- tooluniverse/pmc_tool.py +181 -0
- tooluniverse/pubmed_tool.py +110 -0
- tooluniverse/remote/boltz/boltz_mcp_server.py +2 -2
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +2 -2
- tooluniverse/smcp.py +313 -191
- tooluniverse/smcp_server.py +4 -7
- tooluniverse/test/test_claude_sdk.py +93 -0
- tooluniverse/test/test_odphp_tool.py +166 -0
- tooluniverse/test/test_openrouter_client.py +288 -0
- tooluniverse/test/test_stdio_hooks.py +1 -1
- tooluniverse/test/test_tool_finder.py +1 -1
- tooluniverse/unpaywall_tool.py +63 -0
- tooluniverse/wikidata_sparql_tool.py +61 -0
- tooluniverse/zenodo_tool.py +74 -0
- {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/METADATA +101 -74
- {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/RECORD +56 -19
- {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/entry_points.txt +1 -0
- tooluniverse-1.0.6.dist-info/licenses/LICENSE +201 -0
- tooluniverse-1.0.4.dist-info/licenses/LICENSE +0 -21
- {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/WHEEL +0 -0
- {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/top_level.txt +0 -0
tooluniverse/smcp.py
CHANGED
|
@@ -92,6 +92,7 @@ AI Agent Interface:
|
|
|
92
92
|
"""
|
|
93
93
|
|
|
94
94
|
import asyncio
|
|
95
|
+
import functools
|
|
95
96
|
import json
|
|
96
97
|
from concurrent.futures import ThreadPoolExecutor
|
|
97
98
|
from typing import Any, Dict, List, Optional, Union, Callable, Literal
|
|
@@ -1389,7 +1390,7 @@ class SMCP(FastMCP):
|
|
|
1389
1390
|
self.tool_finder_available = True
|
|
1390
1391
|
self.tool_finder_type = "Tool_Finder_LLM"
|
|
1391
1392
|
self.logger.info(
|
|
1392
|
-
"✅ Tool_Finder_LLM
|
|
1393
|
+
"✅ Tool_Finder_LLM available for advanced search"
|
|
1393
1394
|
)
|
|
1394
1395
|
return
|
|
1395
1396
|
|
|
@@ -1879,6 +1880,91 @@ class SMCP(FastMCP):
|
|
|
1879
1880
|
except Exception:
|
|
1880
1881
|
pass
|
|
1881
1882
|
|
|
1883
|
+
def _print_tooluniverse_banner(self):
|
|
1884
|
+
"""Print ToolUniverse branding banner after FastMCP banner with dynamic information."""
|
|
1885
|
+
# Get transport info if available
|
|
1886
|
+
transport_display = getattr(self, '_transport_type', 'Unknown')
|
|
1887
|
+
server_url = getattr(self, '_server_url', 'N/A')
|
|
1888
|
+
tools_count = len(self._exposed_tools)
|
|
1889
|
+
|
|
1890
|
+
# Map transport types to display names
|
|
1891
|
+
transport_map = {
|
|
1892
|
+
'stdio': 'STDIO',
|
|
1893
|
+
'streamable-http': 'Streamable-HTTP',
|
|
1894
|
+
'http': 'HTTP',
|
|
1895
|
+
'sse': 'SSE'
|
|
1896
|
+
}
|
|
1897
|
+
transport_name = transport_map.get(transport_display, transport_display)
|
|
1898
|
+
|
|
1899
|
+
# Format lines with proper alignment (matching FastMCP style)
|
|
1900
|
+
# Each line should be exactly 75 characters (emoji takes 2 display widths but counts as 1 in len())
|
|
1901
|
+
transport_line = f" 📦 Transport: {transport_name}"
|
|
1902
|
+
server_line = f" 🔗 Server URL: {server_url}"
|
|
1903
|
+
tools_line = f" 🧰 Loaded Tools: {tools_count}"
|
|
1904
|
+
|
|
1905
|
+
# Pad to exactly 75 characters (emoji counts as 1 in len() but displays as 2)
|
|
1906
|
+
transport_line = transport_line + " " * (75 - len(transport_line))
|
|
1907
|
+
server_line = server_line + " " * (75 - len(server_line))
|
|
1908
|
+
tools_line = tools_line + " " * (75 - len(tools_line))
|
|
1909
|
+
|
|
1910
|
+
banner = f"""
|
|
1911
|
+
╭────────────────────────────────────────────────────────────────────────────╮
|
|
1912
|
+
│ │
|
|
1913
|
+
│ 🧬 ToolUniverse SMCP Server 🧬 │
|
|
1914
|
+
│ │
|
|
1915
|
+
│ Bridging AI Agents with Scientific Computing Tools │
|
|
1916
|
+
│ │
|
|
1917
|
+
│{transport_line}│
|
|
1918
|
+
│{server_line}│
|
|
1919
|
+
│{tools_line}│
|
|
1920
|
+
│ │
|
|
1921
|
+
│ 🌐 Website: https://aiscientist.tools/ │
|
|
1922
|
+
│ 💻 GitHub: https://github.com/mims-harvard/ToolUniverse │
|
|
1923
|
+
│ │
|
|
1924
|
+
╰────────────────────────────────────────────────────────────────────────────╯
|
|
1925
|
+
"""
|
|
1926
|
+
print(banner)
|
|
1927
|
+
|
|
1928
|
+
def run(self, *args, **kwargs):
|
|
1929
|
+
"""
|
|
1930
|
+
Override run method to display ToolUniverse banner after FastMCP banner.
|
|
1931
|
+
|
|
1932
|
+
This method intercepts the parent's run() call to inject our custom banner
|
|
1933
|
+
immediately after FastMCP displays its startup banner.
|
|
1934
|
+
"""
|
|
1935
|
+
# Save transport information for banner display
|
|
1936
|
+
transport = kwargs.get('transport', args[0] if args else 'unknown')
|
|
1937
|
+
host = kwargs.get('host', '0.0.0.0')
|
|
1938
|
+
port = kwargs.get('port', 7000)
|
|
1939
|
+
|
|
1940
|
+
self._transport_type = transport
|
|
1941
|
+
|
|
1942
|
+
# Build server URL based on transport
|
|
1943
|
+
if transport == 'streamable-http' or transport == 'http':
|
|
1944
|
+
self._server_url = f"http://{host}:{port}/mcp"
|
|
1945
|
+
elif transport == 'sse':
|
|
1946
|
+
self._server_url = f"http://{host}:{port}"
|
|
1947
|
+
else:
|
|
1948
|
+
self._server_url = "N/A (stdio mode)"
|
|
1949
|
+
|
|
1950
|
+
# Use threading to print our banner shortly after FastMCP's banner
|
|
1951
|
+
import threading
|
|
1952
|
+
import time
|
|
1953
|
+
|
|
1954
|
+
def delayed_banner():
|
|
1955
|
+
"""Print ToolUniverse banner with a small delay to appear after FastMCP banner."""
|
|
1956
|
+
time.sleep(1.0) # Delay to ensure FastMCP banner displays first
|
|
1957
|
+
self._print_tooluniverse_banner()
|
|
1958
|
+
|
|
1959
|
+
# Start banner thread only on first run
|
|
1960
|
+
if not hasattr(self, '_tooluniverse_banner_shown'):
|
|
1961
|
+
self._tooluniverse_banner_shown = True
|
|
1962
|
+
banner_thread = threading.Thread(target=delayed_banner, daemon=True)
|
|
1963
|
+
banner_thread.start()
|
|
1964
|
+
|
|
1965
|
+
# Call parent's run method (blocking call)
|
|
1966
|
+
return super().run(*args, **kwargs)
|
|
1967
|
+
|
|
1882
1968
|
def run_simple(
|
|
1883
1969
|
self,
|
|
1884
1970
|
transport: Literal["stdio", "http", "sse"] = "http",
|
|
@@ -2085,208 +2171,244 @@ class SMCP(FastMCP):
|
|
|
2085
2171
|
func_params = []
|
|
2086
2172
|
param_annotations = {}
|
|
2087
2173
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
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
|
-
)
|
|
2174
|
+
# Process parameters in two phases: required first, then optional
|
|
2175
|
+
# This ensures Python function signature validity (no default args before non-default)
|
|
2176
|
+
for is_required_phase in [True, False]:
|
|
2177
|
+
for param_name, param_info in properties.items():
|
|
2178
|
+
param_type = param_info.get("type", "string")
|
|
2179
|
+
param_description = param_info.get(
|
|
2180
|
+
"description", f"{param_name} parameter"
|
|
2187
2181
|
)
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2182
|
+
is_required = param_name in required_params
|
|
2183
|
+
|
|
2184
|
+
# Skip if not in current phase
|
|
2185
|
+
if is_required != is_required_phase:
|
|
2186
|
+
continue
|
|
2187
|
+
|
|
2188
|
+
# Map JSON schema types to Python types and create appropriate Field
|
|
2189
|
+
field_kwargs = {"description": param_description}
|
|
2190
|
+
|
|
2191
|
+
if param_type == "string":
|
|
2192
|
+
python_type = str
|
|
2193
|
+
# For string type, don't add json_schema_extra - let Pydantic handle it
|
|
2194
|
+
elif param_type == "integer":
|
|
2195
|
+
python_type = int
|
|
2196
|
+
# For integer type, don't add json_schema_extra - let Pydantic handle it
|
|
2197
|
+
elif param_type == "number":
|
|
2198
|
+
python_type = float
|
|
2199
|
+
# For number type, don't add json_schema_extra - let Pydantic handle it
|
|
2200
|
+
elif param_type == "boolean":
|
|
2201
|
+
python_type = bool
|
|
2202
|
+
# For boolean type, don't add json_schema_extra - let Pydantic handle it
|
|
2203
|
+
elif param_type == "array":
|
|
2204
|
+
python_type = list
|
|
2205
|
+
# Add array-specific schema information only for complex cases
|
|
2206
|
+
items_info = param_info.get("items", {})
|
|
2207
|
+
if items_info:
|
|
2208
|
+
# Clean up items definition - remove invalid fields
|
|
2209
|
+
cleaned_items = items_info.copy()
|
|
2210
|
+
|
|
2211
|
+
# Remove 'required' field from items (not valid in JSON Schema for array items)
|
|
2212
|
+
if "required" in cleaned_items:
|
|
2213
|
+
cleaned_items.pop("required")
|
|
2214
|
+
|
|
2215
|
+
field_kwargs["json_schema_extra"] = {
|
|
2216
|
+
"type": "array",
|
|
2217
|
+
"items": cleaned_items,
|
|
2218
|
+
}
|
|
2219
|
+
else:
|
|
2220
|
+
# If no items specified, default to string items
|
|
2221
|
+
field_kwargs["json_schema_extra"] = {
|
|
2222
|
+
"type": "array",
|
|
2223
|
+
"items": {"type": "string"},
|
|
2224
|
+
}
|
|
2225
|
+
elif param_type == "object":
|
|
2226
|
+
python_type = dict
|
|
2227
|
+
# Add object-specific schema information
|
|
2228
|
+
object_props = param_info.get("properties", {})
|
|
2229
|
+
if object_props:
|
|
2230
|
+
# Clean up the nested object properties - fix common schema issues
|
|
2231
|
+
cleaned_props = {}
|
|
2232
|
+
nested_required = []
|
|
2233
|
+
|
|
2234
|
+
for prop_name, prop_info in object_props.items():
|
|
2235
|
+
cleaned_prop = prop_info.copy()
|
|
2236
|
+
|
|
2237
|
+
# Fix string "True"/"False" in required field (common ToolUniverse issue)
|
|
2238
|
+
if "required" in cleaned_prop:
|
|
2239
|
+
req_value = cleaned_prop.pop("required")
|
|
2240
|
+
if req_value in ["True", "true", True]:
|
|
2241
|
+
nested_required.append(prop_name)
|
|
2242
|
+
# Remove the individual required field as it should be at object level
|
|
2243
|
+
|
|
2244
|
+
cleaned_props[prop_name] = cleaned_prop
|
|
2245
|
+
|
|
2246
|
+
# Create proper JSON schema for nested object
|
|
2247
|
+
object_schema = {"type": "object", "properties": cleaned_props}
|
|
2248
|
+
|
|
2249
|
+
# Add required array at object level if there are required fields
|
|
2250
|
+
if nested_required:
|
|
2251
|
+
object_schema["required"] = nested_required
|
|
2252
|
+
|
|
2253
|
+
field_kwargs["json_schema_extra"] = object_schema
|
|
2254
|
+
else:
|
|
2255
|
+
# For unknown types, default to string and only add type info if it's truly unknown
|
|
2256
|
+
python_type = str
|
|
2257
|
+
if param_type not in [
|
|
2258
|
+
"string",
|
|
2259
|
+
"integer",
|
|
2260
|
+
"number",
|
|
2261
|
+
"boolean",
|
|
2262
|
+
"array",
|
|
2263
|
+
"object",
|
|
2264
|
+
]:
|
|
2265
|
+
field_kwargs["json_schema_extra"] = {"type": param_type}
|
|
2266
|
+
|
|
2267
|
+
# Create Pydantic Field with enhanced schema information
|
|
2268
|
+
pydantic_field = Field(**field_kwargs)
|
|
2269
|
+
|
|
2270
|
+
if is_required:
|
|
2271
|
+
# Required parameter with description and schema info
|
|
2272
|
+
annotated_type = Annotated[python_type, pydantic_field]
|
|
2273
|
+
param_annotations[param_name] = annotated_type
|
|
2274
|
+
func_params.append(
|
|
2275
|
+
inspect.Parameter(
|
|
2276
|
+
param_name,
|
|
2277
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
2278
|
+
annotation=annotated_type,
|
|
2279
|
+
)
|
|
2200
2280
|
)
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
result = await loop.run_in_executor(
|
|
2215
|
-
self.executor,
|
|
2216
|
-
self.tooluniverse.run_one_function,
|
|
2217
|
-
function_call,
|
|
2281
|
+
else:
|
|
2282
|
+
# Optional parameter with description, schema info and default value
|
|
2283
|
+
annotated_type = Annotated[
|
|
2284
|
+
Union[python_type, type(None)], pydantic_field
|
|
2285
|
+
]
|
|
2286
|
+
param_annotations[param_name] = annotated_type
|
|
2287
|
+
func_params.append(
|
|
2288
|
+
inspect.Parameter(
|
|
2289
|
+
param_name,
|
|
2290
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
2291
|
+
default=None,
|
|
2292
|
+
annotation=annotated_type,
|
|
2293
|
+
)
|
|
2218
2294
|
)
|
|
2219
2295
|
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2296
|
+
# Add optional streaming parameter to signature
|
|
2297
|
+
stream_field = Field(
|
|
2298
|
+
description="Set to true to receive incremental streaming output (experimental)."
|
|
2299
|
+
)
|
|
2300
|
+
stream_annotation = Annotated[Union[bool, type(None)], stream_field]
|
|
2301
|
+
param_annotations["_tooluniverse_stream"] = stream_annotation
|
|
2302
|
+
func_params.append(
|
|
2303
|
+
inspect.Parameter(
|
|
2304
|
+
"_tooluniverse_stream",
|
|
2305
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
2306
|
+
default=None,
|
|
2307
|
+
annotation=stream_annotation,
|
|
2308
|
+
)
|
|
2309
|
+
)
|
|
2225
2310
|
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2311
|
+
# Optional FastMCP context injection for streaming callbacks
|
|
2312
|
+
try:
|
|
2313
|
+
from fastmcp.server.context import Context as MCPContext # type: ignore
|
|
2314
|
+
except Exception: # pragma: no cover - context unavailable
|
|
2315
|
+
MCPContext = None # type: ignore
|
|
2316
|
+
|
|
2317
|
+
if MCPContext is not None:
|
|
2318
|
+
func_params.append(
|
|
2319
|
+
inspect.Parameter(
|
|
2320
|
+
"ctx",
|
|
2321
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
2322
|
+
default=None,
|
|
2323
|
+
annotation=MCPContext,
|
|
2324
|
+
)
|
|
2325
|
+
)
|
|
2230
2326
|
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2327
|
+
async def dynamic_tool_function(**kwargs) -> str:
|
|
2328
|
+
"""Execute ToolUniverse tool with provided arguments."""
|
|
2329
|
+
try:
|
|
2330
|
+
ctx = kwargs.pop("ctx", None)
|
|
2331
|
+
stream_flag = bool(kwargs.get("_tooluniverse_stream"))
|
|
2235
2332
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
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}
|
|
2333
|
+
# Filter out None values for optional parameters (preserve streaming flag)
|
|
2334
|
+
args_dict = {
|
|
2335
|
+
k: v for k, v in kwargs.items() if v is not None
|
|
2336
|
+
}
|
|
2337
|
+
filtered_args = {
|
|
2338
|
+
k: v
|
|
2339
|
+
for k, v in args_dict.items()
|
|
2340
|
+
if k != "_tooluniverse_stream"
|
|
2341
|
+
}
|
|
2260
2342
|
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2343
|
+
# Validate required parameters
|
|
2344
|
+
missing_required = [
|
|
2345
|
+
param for param in required_params if param not in filtered_args
|
|
2346
|
+
]
|
|
2347
|
+
if missing_required:
|
|
2348
|
+
return json.dumps(
|
|
2349
|
+
{
|
|
2350
|
+
"error": f"Missing required parameters: {missing_required}",
|
|
2351
|
+
"required": required_params,
|
|
2352
|
+
"provided": list(filtered_args.keys()),
|
|
2353
|
+
},
|
|
2354
|
+
indent=2,
|
|
2267
2355
|
)
|
|
2268
2356
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2357
|
+
function_call = {"name": tool_name, "arguments": args_dict}
|
|
2358
|
+
|
|
2359
|
+
loop = asyncio.get_event_loop()
|
|
2360
|
+
stream_callback = None
|
|
2361
|
+
|
|
2362
|
+
if stream_flag and ctx is not None and MCPContext is not None:
|
|
2363
|
+
def stream_callback(chunk: str) -> None:
|
|
2364
|
+
if not chunk:
|
|
2365
|
+
return
|
|
2366
|
+
try:
|
|
2367
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
2368
|
+
ctx.info(chunk), loop
|
|
2369
|
+
)
|
|
2370
|
+
|
|
2371
|
+
def _log_future_result(fut) -> None:
|
|
2372
|
+
exc = fut.exception()
|
|
2373
|
+
if exc:
|
|
2374
|
+
self.logger.debug(
|
|
2375
|
+
f"Streaming callback error for {tool_name}: {exc}"
|
|
2376
|
+
)
|
|
2377
|
+
|
|
2378
|
+
future.add_done_callback(_log_future_result)
|
|
2379
|
+
except Exception as cb_error: # noqa: BLE001
|
|
2380
|
+
self.logger.debug(
|
|
2381
|
+
f"Failed to dispatch stream chunk for {tool_name}: {cb_error}"
|
|
2382
|
+
)
|
|
2383
|
+
|
|
2384
|
+
# Ensure downstream tools see the streaming flag
|
|
2385
|
+
if "_tooluniverse_stream" not in args_dict:
|
|
2386
|
+
args_dict["_tooluniverse_stream"] = True
|
|
2387
|
+
|
|
2388
|
+
run_callable = functools.partial(
|
|
2389
|
+
self.tooluniverse.run_one_function,
|
|
2390
|
+
function_call,
|
|
2391
|
+
stream_callback=stream_callback,
|
|
2392
|
+
)
|
|
2282
2393
|
|
|
2283
|
-
|
|
2284
|
-
if func_params:
|
|
2285
|
-
dynamic_tool_function.__signature__ = inspect.Signature(func_params)
|
|
2394
|
+
result = await loop.run_in_executor(self.executor, run_callable)
|
|
2286
2395
|
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2396
|
+
if isinstance(result, str):
|
|
2397
|
+
return result
|
|
2398
|
+
else:
|
|
2399
|
+
return json.dumps(result, indent=2, default=str)
|
|
2400
|
+
|
|
2401
|
+
except Exception as e:
|
|
2402
|
+
error_msg = f"Error executing {tool_name}: {str(e)}"
|
|
2403
|
+
self.logger.error(error_msg)
|
|
2404
|
+
return json.dumps({"error": error_msg}, indent=2)
|
|
2405
|
+
|
|
2406
|
+
# Set function metadata
|
|
2407
|
+
dynamic_tool_function.__name__ = tool_name
|
|
2408
|
+
dynamic_tool_function.__signature__ = inspect.Signature(func_params)
|
|
2409
|
+
annotations = param_annotations.copy()
|
|
2410
|
+
annotations["return"] = str
|
|
2411
|
+
dynamic_tool_function.__annotations__ = annotations
|
|
2290
2412
|
|
|
2291
2413
|
# Create detailed docstring for internal use, but use clean description for FastMCP
|
|
2292
2414
|
param_docs = []
|
tooluniverse/smcp_server.py
CHANGED
|
@@ -111,7 +111,6 @@ Examples:
|
|
|
111
111
|
auto_expose_tools=True,
|
|
112
112
|
search_enabled=True,
|
|
113
113
|
max_workers=5,
|
|
114
|
-
stateless_http=True, # Enable stateless mode for MCPAutoLoaderTool compatibility
|
|
115
114
|
hooks_enabled=hooks_enabled,
|
|
116
115
|
hook_config=hook_config,
|
|
117
116
|
hook_type=args.hook_type,
|
|
@@ -254,8 +253,8 @@ Examples:
|
|
|
254
253
|
# Server configuration (stdio-specific)
|
|
255
254
|
parser.add_argument(
|
|
256
255
|
"--name",
|
|
257
|
-
default="SMCP
|
|
258
|
-
help="Server name (default: SMCP
|
|
256
|
+
default="ToolUniverse SMCP Server",
|
|
257
|
+
help="Server name (default: ToolUniverse SMCP Server)",
|
|
259
258
|
)
|
|
260
259
|
parser.add_argument(
|
|
261
260
|
"--no-search",
|
|
@@ -528,7 +527,6 @@ Examples:
|
|
|
528
527
|
exclude_tool_types=exclude_tool_types,
|
|
529
528
|
search_enabled=not args.no_search,
|
|
530
529
|
max_workers=args.max_workers,
|
|
531
|
-
stateless_http=True, # Enable stateless mode for MCPAutoLoaderTool compatibility
|
|
532
530
|
hooks_enabled=hooks_enabled,
|
|
533
531
|
hook_config=hook_config,
|
|
534
532
|
hook_type=hook_type,
|
|
@@ -685,8 +683,8 @@ Examples:
|
|
|
685
683
|
)
|
|
686
684
|
parser.add_argument(
|
|
687
685
|
"--name",
|
|
688
|
-
default="SMCP
|
|
689
|
-
help="Server name (default: SMCP
|
|
686
|
+
default="ToolUniverse SMCP Server",
|
|
687
|
+
help="Server name (default: ToolUniverse SMCP Server)",
|
|
690
688
|
)
|
|
691
689
|
parser.add_argument(
|
|
692
690
|
"--no-search",
|
|
@@ -956,7 +954,6 @@ Examples:
|
|
|
956
954
|
exclude_tool_types=exclude_tool_types,
|
|
957
955
|
search_enabled=not args.no_search,
|
|
958
956
|
max_workers=args.max_workers,
|
|
959
|
-
stateless_http=True, # Enable stateless mode for MCPAutoLoaderTool compatibility
|
|
960
957
|
hooks_enabled=hooks_enabled,
|
|
961
958
|
hook_config=hook_config,
|
|
962
959
|
hook_type=args.hook_type,
|