Source code for ralph.mcp.tools.bridge._tool_bridge
"""ToolBridge registry and dispatcher."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from ralph.mcp.tools.bridge._lazy_tool_handler import LazyToolHandler
from ralph.mcp.tools.bridge._registered_tool import RegisteredTool
from ralph.mcp.tools.bridge._spec_helpers import _is_approved
from ralph.mcp.tools.bridge._tool_bridge_error import ToolBridgeError
from ralph.mcp.tools.bridge._tool_dispatch_error import ToolDispatchError
from ralph.mcp.tools.bridge._tool_registration_error import ToolRegistrationError
if TYPE_CHECKING:
from collections.abc import Callable
from ralph.mcp.tools.bridge._registration_handler import RegistrationHandler
from ralph.mcp.tools.bridge._tool_definition import ToolDefinition
from ralph.mcp.tools.bridge._tool_metadata import ToolMetadata
from ralph.mcp.tools.bridge._tool_spec import ToolSpec
from ralph.mcp.tools.bridge._types import JsonObject
[docs]
class ToolBridge:
"""Registry for MCP tools and dispatcher for tool invocations."""
def __init__(self, session: object | None = None) -> None:
self._tools: dict[str, RegisteredTool] = {}
self._session = session
self._client_capabilities: set[str] | None = None
[docs]
def set_client_capabilities(self, capabilities: set[str] | None) -> None:
"""Set the client declared capabilities from MCP initialize handshake."""
self._client_capabilities = capabilities
[docs]
def register(self, metadata: ToolMetadata, handler: RegistrationHandler) -> None:
"""Register a tool definition and handler."""
name = metadata.definition.name
if name in self._tools:
raise ToolRegistrationError(f"Tool '{name}' is already registered")
self._tools[name] = RegisteredTool(metadata=metadata, handler=handler)
[docs]
def register_spec(self, spec: ToolSpec, *, session: object, workspace: object) -> None:
"""Register a tool from a complete lazy-loading spec."""
self.register(
spec.metadata,
LazyToolHandler(
module_name=spec.module_name,
handler_name=spec.handler_name,
session=session,
workspace=workspace,
),
)
[docs]
def has_tool(self, name: str) -> bool:
"""Return whether a tool is registered."""
return name in self._tools
[docs]
def get(self, name: str) -> RegisteredTool:
"""Return a registered tool or raise if it does not exist."""
try:
return self._tools[name]
except KeyError as exc:
raise ToolDispatchError(f"Tool '{name}' is not registered") from exc
[docs]
def list_metadata(self) -> list[ToolMetadata]:
"""Return tool metadata in registration order."""
return [
tool.metadata
for tool in self._tools.values()
if self._is_tool_allowed(tool.metadata) and self._is_tool_visible(tool.metadata)
]
[docs]
def list_definitions(self) -> list[ToolDefinition]:
"""Return public tool definitions in registration order."""
return [
tool.metadata.definition
for tool in self._tools.values()
if self._is_tool_allowed(tool.metadata) and self._is_tool_visible(tool.metadata)
]
[docs]
def dispatch(
self,
name: str,
params: JsonObject | None = None,
*,
host_session: object | None = None,
workspace: object | None = None,
) -> object:
"""Dispatch a tool invocation to its registered handler."""
tool = self.get(name)
session = host_session or self._session
if not self._is_tool_allowed(tool.metadata, session=session):
capability = tool.metadata.required_capability
raise ToolDispatchError(f"Tool '{name}' requires capability '{capability}'")
tool_params = dict(params or {})
try:
return tool.handler(host_session, workspace, tool_params)
except ToolBridgeError:
raise
except Exception as exc:
raise ToolDispatchError(f"Tool '{name}' failed: {exc}") from exc
def _is_tool_visible(self, metadata: ToolMetadata) -> bool:
"""Check if a tool is visible to the client based on multimodal flags."""
if not metadata.is_multimodal:
return True
if self._client_capabilities is None:
return False
client_caps = self._client_capabilities
return (
"image" in client_caps
or "media" in client_caps
or "multimodal" in client_caps
or "MediaRead" in client_caps
or "media.read" in client_caps
)
def _is_tool_allowed(self, metadata: ToolMetadata, session: object | None = None) -> bool:
effective_session = session or self._session
if effective_session is None:
return True
checker = cast(
"Callable[[str], object] | None",
getattr(effective_session, "check_capability", None),
)
if not callable(checker):
return True
return _is_approved(checker(metadata.required_capability))