mirror of
https://github.com/mims-harvard/ToolUniverse.git
synced 2026-09-19 07:31:47 +08:00
dc8e9b4237
Implement a production-ready HTTP API server that exposes all ToolUniverse class methods remotely via REST endpoints. The server uses Python introspection to automatically discover methods, requiring zero manual updates when the ToolUniverse class changes. Key Features: - Auto-discovery: Server introspects ToolUniverse for all 49+ methods - Minimal client: Only requires requests + pydantic (via pip install tooluniverse[client]) - Production ready: 8 workers by default, multi-worker support via uvicorn - Stateful: Maintains ToolUniverse instance across requests - Dynamic proxying: Client uses __getattr__ to proxy any method call to server - Well tested: Comprehensive test suite with 182 lines of test code - Well documented: RST documentation integrated into Sphinx docs Server Components: - src/tooluniverse/http_api_server.py: FastAPI server with endpoints - src/tooluniverse/http_api_server_cli.py: CLI entry point - Command: tooluniverse-http-api --host 0.0.0.0 --port 8080 Client Components: - src/tooluniverse/http_client.py: Auto-proxying client - Install: pip install tooluniverse[client] - Import: from tooluniverse import ToolUniverseClient Documentation: - docs/guide/http_api.rst: Complete RST documentation - examples/http_api_usage_example.py: 7 usage examples - tests/test_http_api_server.py: Unit tests Changes: - Added [client] optional dependency to pyproject.toml - Exported ToolUniverseClient in __init__.py - Added HTTP API section to README.md - Integrated http_api.rst into documentation tree
155 lines
5.0 KiB
Python
155 lines
5.0 KiB
Python
"""
|
|
Utility to validate ToolUniverse tool name lengths.
|
|
|
|
This script loads tools via ToolUniverse and checks that every tool name
|
|
is at most MAX_LEN characters (default: 64). It prints a concise report
|
|
and returns a non-zero exit code if any violations are found.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from typing import List, Tuple
|
|
|
|
|
|
def check_tool_name_lengths(max_len: int = 64) -> Tuple[List[str], List[str]]:
|
|
"""
|
|
Load all tools via ToolUniverse and check their name lengths.
|
|
|
|
Returns a tuple of (valid_names, invalid_names) where invalid_names are
|
|
those exceeding max_len.
|
|
"""
|
|
# Import locally to avoid import overhead when used as a library
|
|
from tooluniverse import ToolUniverse
|
|
|
|
tool_universe = ToolUniverse()
|
|
# Load all built-in/configured tools
|
|
tool_universe.load_tools()
|
|
|
|
# Retrieve only names for efficient scanning
|
|
tool_names = tool_universe.get_available_tools(name_only=True)
|
|
|
|
valid: List[str] = []
|
|
invalid: List[str] = []
|
|
|
|
for name in tool_names:
|
|
if len(name) <= max_len:
|
|
valid.append(name)
|
|
else:
|
|
invalid.append(name)
|
|
|
|
return valid, invalid
|
|
|
|
|
|
def check_tool_name_lengths_with_shortening(max_len: int = 55) -> Tuple[int, int, List[Tuple[str, str, int]]]:
|
|
"""
|
|
Check tool names with automatic shortening enabled.
|
|
Validates that all shortened names fit within max_len.
|
|
|
|
Returns:
|
|
Tuple of (total_tools, shortened_count, invalid_tools)
|
|
where invalid_tools is a list of (original, shortened, length) tuples
|
|
for tools that still exceed max_len after shortening.
|
|
"""
|
|
from tooluniverse import ToolUniverse
|
|
from tooluniverse.tool_name_utils import ToolNameMapper
|
|
|
|
tu = ToolUniverse()
|
|
tu.load_tools()
|
|
|
|
# Enable name shortening
|
|
tu.enable_name_shortening(max_length=max_len)
|
|
|
|
tool_names = tu.get_available_tools(name_only=True, shortened=False) # Get original names
|
|
|
|
shortened_count = 0
|
|
invalid_tools = []
|
|
|
|
for original_name in tool_names:
|
|
shortened = tu.get_tool_name(original_name, shorten=True)
|
|
|
|
if shortened != original_name:
|
|
shortened_count += 1
|
|
|
|
if len(shortened) > max_len:
|
|
invalid_tools.append((original_name, shortened, len(shortened)))
|
|
|
|
return len(tool_names), shortened_count, invalid_tools
|
|
|
|
|
|
def _format_report(valid: List[str], invalid: List[str], max_len: int) -> str:
|
|
lines: List[str] = []
|
|
lines.append(f"Max allowed length: {max_len}")
|
|
lines.append(f"Total tools scanned: {len(valid) + len(invalid)}")
|
|
lines.append(f"Valid (≤{max_len}): {len(valid)}")
|
|
lines.append(f"Invalid (>{max_len}): {len(invalid)}")
|
|
if invalid:
|
|
lines.append("")
|
|
lines.append("Invalid tool names:")
|
|
for name in sorted(invalid):
|
|
lines.append(f" - {name} ({len(name)} chars)")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main(argv: List[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Validate ToolUniverse tool name lengths",
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
"--max-len",
|
|
type=int,
|
|
default=64,
|
|
help="Maximum allowed tool name length",
|
|
)
|
|
parser.add_argument(
|
|
"--test-shortening",
|
|
action="store_true",
|
|
help="Test with automatic name shortening enabled (for MCP compatibility)",
|
|
)
|
|
parser.add_argument(
|
|
"--shortening-max-len",
|
|
type=int,
|
|
default=55,
|
|
help="Maximum length for shortened names (default: 55 for mcp__tu__ prefix)",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
if args.test_shortening:
|
|
# Test with shortening enabled
|
|
total, shortened_count, invalid = check_tool_name_lengths_with_shortening(
|
|
max_len=args.shortening_max_len
|
|
)
|
|
|
|
print(f"Testing with automatic name shortening enabled")
|
|
print(f"Max allowed length: {args.shortening_max_len}")
|
|
print(f"Total tools scanned: {total}")
|
|
print(f"Tools shortened: {shortened_count}")
|
|
print(f"Valid after shortening: {total - len(invalid)}")
|
|
print(f"Still invalid: {len(invalid)}")
|
|
|
|
if invalid:
|
|
print("\nTools still exceeding limit after shortening:")
|
|
for orig, short, length in sorted(invalid, key=lambda x: x[2], reverse=True):
|
|
print(f" - {orig}")
|
|
print(f" → {short} ({length} chars)")
|
|
else:
|
|
print("\n✅ All tools fit within limit after shortening!")
|
|
|
|
return 1 if invalid else 0
|
|
else:
|
|
# Test without shortening (original behavior)
|
|
valid, invalid = check_tool_name_lengths(max_len=args.max_len)
|
|
report = _format_report(valid, invalid, args.max_len)
|
|
print(report)
|
|
|
|
# Non-zero exit when violations are present (useful in CI)
|
|
return 1 if invalid else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|
|
|
|
|