Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/_experimental/mcp_server/tool_registry.py: 29%
63 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1import json
2from collections.abc import Callable
3from typing import TYPE_CHECKING, Any, Final
5from litellm._logging import verbose_logger
6from litellm.proxy.types_utils.utils import get_instance_fn
7from litellm.types.mcp_server.tool_registry import MCPTool
9if TYPE_CHECKING: 9 ↛ 10line 9 didn't jump to line 10 because the condition on line 9 was never true
10 from mcp.types import Tool as MCPToolSDKTool
11else:
12 try:
13 from mcp.types import Tool as MCPToolSDKTool
14 except ImportError:
15 MCPToolSDKTool = None
18class MCPToolRegistry:
19 """
20 A registry for managing MCP tools
21 """
23 def __init__(self):
24 # Registry to store all registered tools
25 self.tools: dict[str, MCPTool] = {}
27 def register_tool(
28 self,
29 name: str,
30 description: str,
31 input_schema: dict[str, Any],
32 handler: Callable,
33 ) -> None:
34 """
35 Register a new tool in the registry
36 """
37 self.tools[name] = MCPTool(
38 name=name,
39 description=description,
40 input_schema=input_schema,
41 handler=handler,
42 )
43 verbose_logger.debug("Registered tool: %s", name)
45 def get_tool(self, name: str) -> MCPTool | None:
46 """
47 Get a tool from the registry by name
48 """
49 return self.tools.get(name)
51 def list_tools(self, tool_prefix: str | None = None) -> list[MCPTool]:
52 """
53 List all registered tools
54 """
55 if tool_prefix: 55 ↛ 57line 55 didn't jump to line 57 because the condition on line 55 was always true
56 return [tool for tool in self.tools.values() if tool.name.startswith(tool_prefix)]
57 return list(self.tools.values())
59 def unregister_tools_with_prefix(self, prefix: str) -> int:
60 """Remove tools whose registered name starts with ``prefix``.
62 Used when an OpenAPI-backed MCP server leaves the runtime registry so
63 stale tool handlers cannot be invoked after eviction.
64 """
65 if not prefix:
66 return 0
67 removed = 0
68 for name in list(self.tools.keys()):
69 if name.startswith(prefix):
70 del self.tools[name]
71 removed += 1
72 verbose_logger.debug("Unregistered MCP tool %s", name)
73 return removed
75 def convert_tools_to_mcp_sdk_tool_type(self, tools: list[MCPTool]) -> list["MCPToolSDKTool"]:
76 if MCPToolSDKTool is None: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 raise ImportError("MCP SDK is not installed. Please install it with: pip install 'litellm[proxy]'")
78 return [
79 MCPToolSDKTool(
80 name=tool.name,
81 description=tool.description,
82 inputSchema=tool.input_schema,
83 )
84 for tool in tools
85 ]
87 def load_tools_from_config(
88 self,
89 mcp_tools_config: dict[str, Any] | None = None,
90 config_file_path: str | None = None,
91 ) -> None:
92 """
93 Load and register tools from the proxy config
95 Args:
96 mcp_tools_config: The mcp_tools config from the proxy config
97 config_file_path: Path to the operator's config.yaml. Threaded
98 through to ``get_instance_fn`` so an ``s3://``/``gcs://``
99 ``handler`` declared in the YAML resolves; callers from a
100 non-YAML path must leave this ``None`` so the runtime gate
101 fires.
102 """
103 if mcp_tools_config is None:
104 raise ValueError("mcp_tools_config is required, please set `mcp_tools` in your proxy config")
106 for tool_config in mcp_tools_config:
107 if not isinstance(tool_config, dict):
108 raise ValueError("mcp_tools_config must be a list of dictionaries")
110 name = tool_config.get("name")
111 description = tool_config.get("description")
112 input_schema = tool_config.get("input_schema", {})
113 handler_name = tool_config.get("handler")
115 if not all([name, description, handler_name]):
116 continue
118 # Try to resolve the handler
119 # First check if it's a module path (e.g., "module.submodule.function")
120 if handler_name is None:
121 raise ValueError(f"handler is required for tool {name}")
122 handler = get_instance_fn(handler_name, config_file_path)
124 if handler is None:
125 verbose_logger.warning("Warning: Could not find handler %s for tool %s", handler_name, name)
126 continue
128 # Register the tool
129 if name is None:
130 raise ValueError(f"name is required for tool {name}")
131 if description is None:
132 raise ValueError(f"description is required for tool {name}")
134 self.register_tool(
135 name=name,
136 description=description,
137 input_schema=input_schema,
138 handler=handler,
139 )
140 verbose_logger.debug("all registered tools: %s", json.dumps(self.tools, indent=4, default=str))
143global_mcp_tool_registry: Final = MCPToolRegistry()