Source code for ralph.mcp.upstream.tool_catalog_cache

"""Workspace-scoped cache of validated upstream tool catalogs."""

from __future__ import annotations

from collections import OrderedDict
from typing import TYPE_CHECKING

from ralph.mcp.upstream.client import make_upstream_client
from ralph.mcp.upstream.config import (
    UPSTREAM_MCP_TOOL_CATALOG_ENV,
    UpstreamMcpServer,
    serialize_upstream_tool_catalog,
)

if TYPE_CHECKING:
    from collections.abc import Iterable
    from pathlib import Path

    from ralph.mcp.upstream.models import UpstreamTool

# wt-024 M5 (AC-02): bound the module-level cache so a session that
# touches many distinct workspace roots cannot grow the cache
# unboundedly. FIFO eviction mirrors ProcessManager._terminal_records
# (ralph/process/manager.py). 32 entries covers typical build/test
# concurrency without dropping recently-used workspaces.
_MAX_CACHE_ENTRIES: int = 32

# bounded-accumulator-ok: FIFO LRU cap _MAX_CACHE_ENTRIES=32
# (OrderedDict.popitem(last=False) eviction in cache_tool_catalog)
_CACHE: OrderedDict[str, dict[str, list[UpstreamTool]]] = OrderedDict()  # bounded-accumulator-ok


def _cache_key(workspace_root: Path | None) -> str | None:
    if workspace_root is None:
        return None
    return str(workspace_root.expanduser().resolve())


[docs] def cache_tool_catalog( workspace_root: Path | None, catalog: dict[str, list[UpstreamTool]], ) -> None: """Store a copy of the validated upstream tool catalog for one workspace.""" key = _cache_key(workspace_root) if key is None: return _CACHE[key] = {name: list(tools) for name, tools in catalog.items()} _CACHE.move_to_end(key) while len(_CACHE) > _MAX_CACHE_ENTRIES: _CACHE.popitem(last=False)
[docs] def get_tool_catalog(workspace_root: Path | None) -> dict[str, list[UpstreamTool]]: """Return a defensive copy of the cached tool catalog for one workspace.""" key = _cache_key(workspace_root) if key is None: return {} return {name: list(tools) for name, tools in _CACHE.get(key, {}).items()}
[docs] def clear_tool_catalog(workspace_root: Path | None) -> None: """Drop any cached upstream tool catalog associated with one workspace.""" key = _cache_key(workspace_root) if key is None: return _CACHE.pop(key, None)
[docs] def collect_tool_catalog( servers: Iterable[UpstreamMcpServer], ) -> dict[str, list[UpstreamTool]]: """Probe configured upstream servers and return their advertised tool catalogs.""" return {server.name: list(make_upstream_client(server).list_tools()) for server in servers}
[docs] def apply_tool_catalog_env( runtime_env: dict[str, str], catalog: dict[str, list[UpstreamTool]], ) -> None: """Materialize the upstream tool catalog into runtime environment variables.""" if catalog: runtime_env[UPSTREAM_MCP_TOOL_CATALOG_ENV] = serialize_upstream_tool_catalog(catalog) return runtime_env.pop(UPSTREAM_MCP_TOOL_CATALOG_ENV, None)
__all__ = [ "apply_tool_catalog_env", "cache_tool_catalog", "clear_tool_catalog", "collect_tool_catalog", "get_tool_catalog", ]