Skip to content

Catalog

catalog

In-memory catalog backing the four MCP facade meta-tools.

ToolCatalog wraps the discover_tools() dict and provides the four operations the facade exposes:

  • :meth:search — keyword/tag lookup over name + summary + tags + domain;
  • :meth:describe — the full invocation contract (typed params + docstring);
  • :meth:call — execute a tool by name and return its ToolResult.text (falling back to the flattened data dict when there is no text);
  • :meth:capabilities — compact per-domain grouping.

Discovery metadata (expose_directly / domain / tags) is read via :func:axm.tools.base.tool_metadata, so both AXMTool subclasses and structural tools work unchanged. Typed parameters reuse :func:axm_mcp.schema.signature_params — the exact introspection FastMCP itself uses — so axm_describe and the per-tool MCP schema agree.

ToolCatalog

Searchable index over discovered axm.tools entries.

Parameters:

Name Type Description Default
tools dict[str, ToolEntry]

The discover_tools() mapping (name -> tool entry). The catalog stores it by reference; it does not re-discover.

required
Source code in packages/axm-mcp/src/axm_mcp/facade/catalog.py
Python
class ToolCatalog:
    """Searchable index over discovered ``axm.tools`` entries.

    Args:
        tools: The ``discover_tools()`` mapping (name -> tool entry).  The
            catalog stores it by reference; it does not re-discover.
    """

    def __init__(self, tools: dict[str, ToolEntry]) -> None:
        self._entries = tools

    # ── introspection ────────────────────────────────────────────────────

    def names(self) -> list[str]:
        """All tool names, sorted."""
        return sorted(self._entries)

    def hot_path(self) -> list[str]:
        """Names of tools opting into direct MCP exposure (``expose_directly``)."""
        return sorted(
            name
            for name, tool in self._entries.items()
            if tool_metadata(tool).expose_directly
        )

    # ── facade operations ────────────────────────────────────────────────

    def search(
        self,
        query: str,
        domain: str | None = None,
        limit: int = _DEFAULT_LIMIT,
    ) -> list[dict[str, object]]:
        """Find tools whose name, summary, tags or domain match *query*.

        Matching is case-insensitive substring (the deliberately-simple v1;
        tags carry discovery quality).  An empty *query* lists everything
        (optionally scoped by *domain*), which makes the facade browsable.

        Args:
            query: Substring to look for. Empty matches all.
            domain: Optional exact-match domain filter.
            limit: Maximum number of results.

        Returns:
            A list of ``{name, summary, domain, tags}`` dicts, name-sorted.
        """
        q = query.lower().strip()
        results: list[dict[str, object]] = []
        for name in sorted(self._entries):
            tool = self._entries[name]
            meta = tool_metadata(tool)
            if domain is not None and meta.domain != domain:
                continue
            summary = _summary(tool)
            haystack = f"{name}\n{summary}\n{' '.join(meta.tags)}\n{meta.domain or ''}"
            if q and q not in haystack.lower():
                continue
            results.append(
                {
                    "name": name,
                    "summary": summary,
                    "domain": meta.domain,
                    "tags": sorted(meta.tags),
                }
            )
            if len(results) >= limit:
                break
        return results

    def describe(self, name: str) -> dict[str, object]:
        """Return the full invocation contract for *name*.

        Args:
            name: Tool name.

        Returns:
            ``{name, summary, domain, tags, docstring, params}`` where each
            param is ``{name, annotation, required, default}``.

        Raises:
            UnknownToolError: If *name* is not in the catalog.
        """
        tool = self._get(name)
        meta = tool_metadata(tool)
        return {
            "name": name,
            "summary": _summary(tool),
            "domain": meta.domain,
            "tags": sorted(meta.tags),
            "docstring": _doc(tool),
            "params": [dataclasses.asdict(p) for p in self._params(name)],
        }

    def call(self, name: str, arguments: dict[str, object] | None = None) -> str:
        """Execute *name* with *arguments* and return its text output.

        Args:
            name: Tool name.
            arguments: Keyword arguments for the tool.

        Returns:
            The tool's ``ToolResult.text`` if present, else a readable
            rendering of the flattened ``data`` dict.

        Raises:
            UnknownToolError: If *name* is not in the catalog.
        """
        tool = self._get(name)
        result = _exec_fn(tool)(**(arguments or {}))
        text = getattr(result, "text", None)
        if isinstance(text, str):
            return text
        success = getattr(result, "success", True)
        data = getattr(result, "data", None)
        if not isinstance(data, dict):
            return str(result)
        flat: dict[str, object] = {"success": success, **data}
        error = getattr(result, "error", None)
        if error:
            flat["error"] = error
        return "\n".join(f"{k}: {v}" for k, v in flat.items())

    def param_hint(self, name: str) -> str:
        """Human-readable list of accepted params for *name* (for error text)."""
        try:
            params = self._params(name)
        except UnknownToolError:
            return ""
        parts = [
            f"{p.name}: {p.annotation}" + ("" if p.required else f" = {p.default}")
            for p in params
        ]
        return ", ".join(parts)

    def _params(self, name: str) -> list[_ParamSpec]:
        """Typed parameter specs for *name* (shared by describe/param_hint)."""
        return [
            _ParamSpec(
                name=p.name,
                annotation=_annotation_str(p.annotation),
                required=p.default is inspect.Parameter.empty,
                default=(
                    None if p.default is inspect.Parameter.empty else repr(p.default)
                ),
            )
            for p in signature_params(_exec_fn(self._get(name)))
        ]

    def capabilities(self, domain: str | None = None) -> dict[str, list[str]]:
        """Group tool names by domain.

        Args:
            domain: When given, return only that domain's tools.

        Returns:
            ``{domain: [tool names]}``; tools without a domain are grouped
            under the ``"(ungrouped)"`` key.
        """
        groups: dict[str, list[str]] = {}
        for name in sorted(self._entries):
            d = tool_metadata(self._entries[name]).domain or "(ungrouped)"
            if domain is not None and d != domain:
                continue
            groups.setdefault(d, []).append(name)
        return groups

    # ── internals ────────────────────────────────────────────────────────

    def _get(self, name: str) -> ToolEntry:
        try:
            return self._entries[name]
        except KeyError as exc:
            known = ", ".join(sorted(self._entries))
            msg = f"Unknown tool {name!r}. Known tools: {known or '<none>'}"
            raise UnknownToolError(msg) from exc
call(name, arguments=None)

Execute name with arguments and return its text output.

Parameters:

Name Type Description Default
name str

Tool name.

required
arguments dict[str, object] | None

Keyword arguments for the tool.

None

Returns:

Type Description
str

The tool's ToolResult.text if present, else a readable

str

rendering of the flattened data dict.

Raises:

Type Description
UnknownToolError

If name is not in the catalog.

Source code in packages/axm-mcp/src/axm_mcp/facade/catalog.py
Python
def call(self, name: str, arguments: dict[str, object] | None = None) -> str:
    """Execute *name* with *arguments* and return its text output.

    Args:
        name: Tool name.
        arguments: Keyword arguments for the tool.

    Returns:
        The tool's ``ToolResult.text`` if present, else a readable
        rendering of the flattened ``data`` dict.

    Raises:
        UnknownToolError: If *name* is not in the catalog.
    """
    tool = self._get(name)
    result = _exec_fn(tool)(**(arguments or {}))
    text = getattr(result, "text", None)
    if isinstance(text, str):
        return text
    success = getattr(result, "success", True)
    data = getattr(result, "data", None)
    if not isinstance(data, dict):
        return str(result)
    flat: dict[str, object] = {"success": success, **data}
    error = getattr(result, "error", None)
    if error:
        flat["error"] = error
    return "\n".join(f"{k}: {v}" for k, v in flat.items())
capabilities(domain=None)

Group tool names by domain.

Parameters:

Name Type Description Default
domain str | None

When given, return only that domain's tools.

None

Returns:

Type Description
dict[str, list[str]]

{domain: [tool names]}; tools without a domain are grouped

dict[str, list[str]]

under the "(ungrouped)" key.

Source code in packages/axm-mcp/src/axm_mcp/facade/catalog.py
Python
def capabilities(self, domain: str | None = None) -> dict[str, list[str]]:
    """Group tool names by domain.

    Args:
        domain: When given, return only that domain's tools.

    Returns:
        ``{domain: [tool names]}``; tools without a domain are grouped
        under the ``"(ungrouped)"`` key.
    """
    groups: dict[str, list[str]] = {}
    for name in sorted(self._entries):
        d = tool_metadata(self._entries[name]).domain or "(ungrouped)"
        if domain is not None and d != domain:
            continue
        groups.setdefault(d, []).append(name)
    return groups
describe(name)

Return the full invocation contract for name.

Parameters:

Name Type Description Default
name str

Tool name.

required

Returns:

Type Description
dict[str, object]

{name, summary, domain, tags, docstring, params} where each

dict[str, object]

param is {name, annotation, required, default}.

Raises:

Type Description
UnknownToolError

If name is not in the catalog.

Source code in packages/axm-mcp/src/axm_mcp/facade/catalog.py
Python
def describe(self, name: str) -> dict[str, object]:
    """Return the full invocation contract for *name*.

    Args:
        name: Tool name.

    Returns:
        ``{name, summary, domain, tags, docstring, params}`` where each
        param is ``{name, annotation, required, default}``.

    Raises:
        UnknownToolError: If *name* is not in the catalog.
    """
    tool = self._get(name)
    meta = tool_metadata(tool)
    return {
        "name": name,
        "summary": _summary(tool),
        "domain": meta.domain,
        "tags": sorted(meta.tags),
        "docstring": _doc(tool),
        "params": [dataclasses.asdict(p) for p in self._params(name)],
    }
hot_path()

Names of tools opting into direct MCP exposure (expose_directly).

Source code in packages/axm-mcp/src/axm_mcp/facade/catalog.py
Python
def hot_path(self) -> list[str]:
    """Names of tools opting into direct MCP exposure (``expose_directly``)."""
    return sorted(
        name
        for name, tool in self._entries.items()
        if tool_metadata(tool).expose_directly
    )
names()

All tool names, sorted.

Source code in packages/axm-mcp/src/axm_mcp/facade/catalog.py
Python
def names(self) -> list[str]:
    """All tool names, sorted."""
    return sorted(self._entries)
param_hint(name)

Human-readable list of accepted params for name (for error text).

Source code in packages/axm-mcp/src/axm_mcp/facade/catalog.py
Python
def param_hint(self, name: str) -> str:
    """Human-readable list of accepted params for *name* (for error text)."""
    try:
        params = self._params(name)
    except UnknownToolError:
        return ""
    parts = [
        f"{p.name}: {p.annotation}" + ("" if p.required else f" = {p.default}")
        for p in params
    ]
    return ", ".join(parts)
search(query, domain=None, limit=_DEFAULT_LIMIT)

Find tools whose name, summary, tags or domain match query.

Matching is case-insensitive substring (the deliberately-simple v1; tags carry discovery quality). An empty query lists everything (optionally scoped by domain), which makes the facade browsable.

Parameters:

Name Type Description Default
query str

Substring to look for. Empty matches all.

required
domain str | None

Optional exact-match domain filter.

None
limit int

Maximum number of results.

_DEFAULT_LIMIT

Returns:

Type Description
list[dict[str, object]]

A list of {name, summary, domain, tags} dicts, name-sorted.

Source code in packages/axm-mcp/src/axm_mcp/facade/catalog.py
Python
def search(
    self,
    query: str,
    domain: str | None = None,
    limit: int = _DEFAULT_LIMIT,
) -> list[dict[str, object]]:
    """Find tools whose name, summary, tags or domain match *query*.

    Matching is case-insensitive substring (the deliberately-simple v1;
    tags carry discovery quality).  An empty *query* lists everything
    (optionally scoped by *domain*), which makes the facade browsable.

    Args:
        query: Substring to look for. Empty matches all.
        domain: Optional exact-match domain filter.
        limit: Maximum number of results.

    Returns:
        A list of ``{name, summary, domain, tags}`` dicts, name-sorted.
    """
    q = query.lower().strip()
    results: list[dict[str, object]] = []
    for name in sorted(self._entries):
        tool = self._entries[name]
        meta = tool_metadata(tool)
        if domain is not None and meta.domain != domain:
            continue
        summary = _summary(tool)
        haystack = f"{name}\n{summary}\n{' '.join(meta.tags)}\n{meta.domain or ''}"
        if q and q not in haystack.lower():
            continue
        results.append(
            {
                "name": name,
                "summary": summary,
                "domain": meta.domain,
                "tags": sorted(meta.tags),
            }
        )
        if len(results) >= limit:
            break
    return results

UnknownToolError

Bases: KeyError

Raised when a tool name is not present in the catalog.

Source code in packages/axm-mcp/src/axm_mcp/facade/catalog.py
Python
class UnknownToolError(KeyError):
    """Raised when a tool name is not present in the catalog."""