Skip to content

datarobot_genai.drmcputils.tool_gallery

Tool gallery utilities — shared between MCP servers and the ARD catalog.

This module has no fastmcp dependency and no MCP-protocol imports so it can be imported by drtools, drmcputils, and drmcpbase alike.

marked_kind

marked_kind(tool_category: str | None) -> dict[str, Any] | None

Return the gallery classification for a marker-classified tool, or None.

Source code in datarobot_genai/drmcputils/tool_gallery.py
def marked_kind(tool_category: str | None) -> dict[str, Any] | None:
    """Return the gallery classification for a marker-classified tool, or None."""
    if not tool_category:
        return None
    return _MARKED_TOOL_KINDS.get(tool_category)

is_hosted

is_hosted(tool: Any) -> bool

Return True for dynamic/proxied tools — resolved at request time, not registered.

Source code in datarobot_genai/drmcputils/tool_gallery.py
def is_hosted(tool: Any) -> bool:
    """Return True for dynamic/proxied tools — resolved at request time, not registered."""
    kind = marked_kind(_tool_category(tool))
    return bool(kind and kind["hosted"])

merge_tool_info

merge_tool_info(tool: Any, ui_metadata: dict[str, dict[str, Any]], scopes_provider: ScopesProvider | None = None) -> dict[str, Any]

Combine a FastMCP Tool with drtools UI metadata + derived categories.

Carries the raw tool_category marker and the tool's own description so the builder can classify hosted tools (provider/categories) and fall back to the MCP description when there is no curated UI copy.

Tags come from the drtools registry when available, preserving the declaration order of @tool_metadata(tags=(...)). FastMCP stores tags as a set, so tools outside the registry fall back to a sorted list — the only deterministic order a set can offer.

required_scopes is read through scopes_provider so it covers every declaration spelling and matches what the scope-validation middleware enforces — never re-derived from any single mechanism here.

Source code in datarobot_genai/drmcputils/tool_gallery.py
def merge_tool_info(
    tool: Any,
    ui_metadata: dict[str, dict[str, Any]],
    scopes_provider: ScopesProvider | None = None,
) -> dict[str, Any]:
    """Combine a FastMCP ``Tool`` with drtools UI metadata + derived categories.

    Carries the raw ``tool_category`` marker and the tool's own ``description`` so the
    builder can classify hosted tools (provider/categories) and fall back to the MCP
    description when there is no curated UI copy.

    Tags come from the drtools registry when available, preserving the declaration
    order of ``@tool_metadata(tags=(...))``. FastMCP stores tags as a set, so tools
    outside the registry fall back to a sorted list — the only deterministic order a
    set can offer.

    ``required_scopes`` is read through *scopes_provider* so it covers every
    declaration spelling and matches what the scope-validation middleware
    enforces — never re-derived from any single mechanism here.
    """
    ui = ui_metadata.get(tool.name, {})
    ordered_tags = ui.get("tags")
    return {
        "name": tool.name,
        "display_name": ui.get("display_name"),
        "description_ui": ui.get("description_ui"),
        "description": getattr(tool, "description", None),
        "auth_provider": ui.get("auth_provider"),
        "tags": list(ordered_tags) if ordered_tags else sorted(tool.tags or []),
        "categories": categories_for_tool(tool.name),
        "required_scopes": sorted(scopes_provider(tool)) if scopes_provider else [],
        "tool_category": _tool_category(tool),
        "hosted": is_hosted(tool),
    }
build_tool_gallery_items(tools: list[dict]) -> list[dict]

Build the tools-gallery JSON response items from merged tool dicts.

Each dict in tools should contain at minimum name and hosted. All other keys are optional and fall back to safe defaults.

Parameters:

Name Type Description Default
tools list[dict]

List of dicts with merged FastMCP tool attrs + drtools metadata.

required
Serialisable list of tool gallery item dicts.
Source code in datarobot_genai/drmcputils/tool_gallery.py
def build_tool_gallery_items(tools: list[dict]) -> list[dict]:
    """Build the tools-gallery JSON response items from merged tool dicts.

    Each dict in *tools* should contain at minimum ``name`` and ``hosted``.
    All other keys are optional and fall back to safe defaults.

    Args:
        tools: List of dicts with merged FastMCP tool attrs + drtools metadata.

    Returns
    -------
        Serialisable list of tool gallery item dicts.
    """
    items: list[dict] = []
    for t in tools:
        kind = marked_kind(t.get("tool_category"))
        if kind is not None:
            # Marker-classified tool (user/dynamic/proxied): classification comes from
            # its meta marker, not the static taxonomy or a drtools auth_provider.
            provider = kind["provider"]
            provider_name = kind["provider_name"]
            oauth_provider_type = None
            # Proxied tools have no category (kind["category"] is None) → [].
            categories = [kind["category"]] if kind["category"] else []
            hosted = bool(kind["hosted"])
        else:
            auth_provider = t.get("auth_provider")
            provider = _provider_for(auth_provider)
            provider_name = _provider_name_for(auth_provider)
            oauth_provider_type = _oauth_provider_type_for(auth_provider)
            categories = list(t.get("categories") or [])
            hosted = bool(t.get("hosted", False))
        display_name = t.get("display_name") or t["name"]
        items.append(
            {
                "name": t["name"],
                "display_name": display_name,
                "ui_display_name": _ui_display_name(display_name),
                # Prefer the curated UI copy (drtools ``description_ui``); fall back to the
                # tool's own MCP description (the only copy dynamic/proxied tools carry).
                "description": t.get("description_ui") or t.get("description") or "",
                # Declaration order, as merged from the drtools registry — never re-sorted.
                "tags": list(t.get("tags") or []),
                # Every OAuth scope the tool requires, across all declaration
                # spellings — what a tools/call token must cover (sorted).
                "required_scopes": sorted(t.get("required_scopes") or []),
                "categories": [_category_item(category) for category in categories],
                "provider": provider,
                "provider_name": provider_name,
                "oauth_provider_type": oauth_provider_type,
                "hosted": hosted,
            }
        )
    return items