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.

Files changed (57) hide show
  1. tooluniverse/__init__.py +56 -5
  2. tooluniverse/agentic_tool.py +90 -14
  3. tooluniverse/arxiv_tool.py +113 -0
  4. tooluniverse/biorxiv_tool.py +97 -0
  5. tooluniverse/core_tool.py +153 -0
  6. tooluniverse/crossref_tool.py +73 -0
  7. tooluniverse/data/agentic_tools.json +2 -2
  8. tooluniverse/data/arxiv_tools.json +87 -0
  9. tooluniverse/data/biorxiv_tools.json +70 -0
  10. tooluniverse/data/core_tools.json +105 -0
  11. tooluniverse/data/crossref_tools.json +70 -0
  12. tooluniverse/data/dblp_tools.json +73 -0
  13. tooluniverse/data/doaj_tools.json +94 -0
  14. tooluniverse/data/fatcat_tools.json +72 -0
  15. tooluniverse/data/hal_tools.json +70 -0
  16. tooluniverse/data/medrxiv_tools.json +70 -0
  17. tooluniverse/data/odphp_tools.json +354 -0
  18. tooluniverse/data/openaire_tools.json +85 -0
  19. tooluniverse/data/osf_preprints_tools.json +77 -0
  20. tooluniverse/data/pmc_tools.json +109 -0
  21. tooluniverse/data/pubmed_tools.json +65 -0
  22. tooluniverse/data/unpaywall_tools.json +86 -0
  23. tooluniverse/data/wikidata_sparql_tools.json +42 -0
  24. tooluniverse/data/zenodo_tools.json +82 -0
  25. tooluniverse/dblp_tool.py +62 -0
  26. tooluniverse/default_config.py +18 -0
  27. tooluniverse/doaj_tool.py +124 -0
  28. tooluniverse/execute_function.py +70 -9
  29. tooluniverse/fatcat_tool.py +66 -0
  30. tooluniverse/hal_tool.py +77 -0
  31. tooluniverse/llm_clients.py +487 -0
  32. tooluniverse/mcp_tool_registry.py +3 -3
  33. tooluniverse/medrxiv_tool.py +97 -0
  34. tooluniverse/odphp_tool.py +226 -0
  35. tooluniverse/openaire_tool.py +145 -0
  36. tooluniverse/osf_preprints_tool.py +67 -0
  37. tooluniverse/pmc_tool.py +181 -0
  38. tooluniverse/pubmed_tool.py +110 -0
  39. tooluniverse/remote/boltz/boltz_mcp_server.py +2 -2
  40. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +2 -2
  41. tooluniverse/smcp.py +313 -191
  42. tooluniverse/smcp_server.py +4 -7
  43. tooluniverse/test/test_claude_sdk.py +93 -0
  44. tooluniverse/test/test_odphp_tool.py +166 -0
  45. tooluniverse/test/test_openrouter_client.py +288 -0
  46. tooluniverse/test/test_stdio_hooks.py +1 -1
  47. tooluniverse/test/test_tool_finder.py +1 -1
  48. tooluniverse/unpaywall_tool.py +63 -0
  49. tooluniverse/wikidata_sparql_tool.py +61 -0
  50. tooluniverse/zenodo_tool.py +74 -0
  51. {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/METADATA +101 -74
  52. {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/RECORD +56 -19
  53. {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/entry_points.txt +1 -0
  54. tooluniverse-1.0.6.dist-info/licenses/LICENSE +201 -0
  55. tooluniverse-1.0.4.dist-info/licenses/LICENSE +0 -21
  56. {tooluniverse-1.0.4.dist-info → tooluniverse-1.0.6.dist-info}/WHEEL +0 -0
  57. {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 (cost-optimized) available for advanced search"
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
- 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
- )
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
- 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,
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
- # 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,
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
- # Format the result
2221
- if isinstance(result, str):
2222
- return result
2223
- else:
2224
- return json.dumps(result, indent=2, default=str)
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
- 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)
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
- # Set function metadata
2232
- dynamic_tool_function.__name__ = tool_name
2233
- dynamic_tool_function.__signature__ = inspect.Signature([])
2234
- dynamic_tool_function.__annotations__ = {"return": str}
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
- 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}
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
- # 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,
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
- # 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
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
- # Set function signature dynamically for tools with parameters
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
- # Set annotations for type hints
2288
- dynamic_tool_function.__annotations__ = param_annotations.copy()
2289
- dynamic_tool_function.__annotations__["return"] = str
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 = []
@@ -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 ToolUniverse Server",
258
- help="Server name (default: SMCP ToolUniverse Server)",
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 ToolUniverse Server",
689
- help="Server name (default: SMCP ToolUniverse Server)",
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,