Skip to content

datarobot_genai.drmcputils.categories

categories

Pre-defined MCP tool categories for header-based filtering.

Categories allow agents and the DR platform to request logical groups of tools via the x-datarobot-mcp-tools header instead of enumerating individual tool names. Parents expand to all their leaf children; leaves expand to the set of tool function names they contain. Anything that does not match a known category is kept as-is (treated as a plain tool name).

Hierarchy

dr_connectors dr_connector_confluence dr_connector_jira dr_connector_gdrive dr_connector_microsoft_sharepoint_onedrive dr_web_search dr_web_search_perplexity dr_web_search_tavily dr_documentation (leaf — no sub-categories) dr_use_cases (leaf — no sub-categories) dr_predictive dr_catalog dr_modeling dr_predictions dr_deployments (leaf — no sub-categories) dr_development dr_workload dr_file dr_visual dr_mcpapps dr_db dr_vdb dr_user_tools (user-authored tools — outside the static taxonomy) dr_dynamic_tools (hosted tools — registered separately)

ToolAllowlist dataclass

A parsed x-datarobot-mcp-tools header, keeping how each name got here.

Three buckets, because "the client named this tool" and "this name fell out of expanding a category" must not be treated alike:

  • explicit: names the client wrote verbatim, plus unknown tokens (a typo stays a name that matches nothing). Admits any tool with that name, whatever it is.
  • derived: names produced by expanding a static category. These describe DataRobot's own built-in tools, so they must only admit built-ins — a user-authored tool that happens to share a name is not the tool the category meant. Flattening these into explicit is what made dr_db admit a USER_TOOL called vdb_query while ?category=dr_db returned nothing.
  • buckets: marker-resolved categories (dr_user_tools/dr_dynamic_tools), which name no tools at all and are matched against a tool's own marker.

Matching needs the tool object, so it lives beside the transform (drmcpbase.fastmcp_transforms.utils.is_tool_allowed); this stays a pure taxonomy type with no view of any catalog.

Source code in datarobot_genai/drmcputils/categories.py
@dataclass(frozen=True, slots=True)
class ToolAllowlist:
    """A parsed ``x-datarobot-mcp-tools`` header, keeping *how* each name got here.

    Three buckets, because "the client named this tool" and "this name fell out of
    expanding a category" must not be treated alike:

    - ``explicit``: names the client wrote verbatim, plus unknown tokens (a typo stays
      a name that matches nothing). Admits any tool with that name, whatever it is.
    - ``derived``: names produced by expanding a *static* category. These describe
      DataRobot's own built-in tools, so they must only admit built-ins — a
      user-authored tool that happens to share a name is not the tool the category
      meant. Flattening these into ``explicit`` is what made ``dr_db`` admit a
      ``USER_TOOL`` called ``vdb_query`` while ``?category=dr_db`` returned nothing.
    - ``buckets``: marker-resolved categories (``dr_user_tools``/``dr_dynamic_tools``),
      which name no tools at all and are matched against a tool's own marker.

    Matching needs the tool object, so it lives beside the transform
    (``drmcpbase.fastmcp_transforms.utils.is_tool_allowed``); this stays a pure
    taxonomy type with no view of any catalog.
    """

    explicit: frozenset[str] = frozenset()
    derived: frozenset[str] = frozenset()
    buckets: frozenset[str] = frozenset()

    def may_admit_name(self, name: str) -> bool:
        """Report whether a tool with this name could be admitted at all.

        The cheap half of the decision, for callers holding only a name (``get_tool``
        rejects before resolving). ``True`` means "keep going", not "allowed": whether
        a derived name or a bucket admits *this* tool depends on its marker, which
        needs the tool in hand.
        """
        return name in self.explicit or name in self.derived or bool(self.buckets)

    def with_explicit(self, names: frozenset[str]) -> "ToolAllowlist":
        """Union in concrete tool names, as explicitly named (Tool Sets expansion).

        A Tool Set lists tool *functions* by name, which is the client naming them —
        so a user-authored tool in a bundle is admitted like any other.
        """
        return ToolAllowlist(self.explicit | names, self.derived, self.buckets)

    def __bool__(self) -> bool:
        return bool(self.explicit or self.derived or self.buckets)

may_admit_name

may_admit_name(name: str) -> bool

Report whether a tool with this name could be admitted at all.

The cheap half of the decision, for callers holding only a name (get_tool rejects before resolving). True means "keep going", not "allowed": whether a derived name or a bucket admits this tool depends on its marker, which needs the tool in hand.

Source code in datarobot_genai/drmcputils/categories.py
def may_admit_name(self, name: str) -> bool:
    """Report whether a tool with this name could be admitted at all.

    The cheap half of the decision, for callers holding only a name (``get_tool``
    rejects before resolving). ``True`` means "keep going", not "allowed": whether
    a derived name or a bucket admits *this* tool depends on its marker, which
    needs the tool in hand.
    """
    return name in self.explicit or name in self.derived or bool(self.buckets)

with_explicit

with_explicit(names: frozenset[str]) -> ToolAllowlist

Union in concrete tool names, as explicitly named (Tool Sets expansion).

A Tool Set lists tool functions by name, which is the client naming them — so a user-authored tool in a bundle is admitted like any other.

Source code in datarobot_genai/drmcputils/categories.py
def with_explicit(self, names: frozenset[str]) -> "ToolAllowlist":
    """Union in concrete tool names, as explicitly named (Tool Sets expansion).

    A Tool Set lists tool *functions* by name, which is the client naming them —
    so a user-authored tool in a bundle is admitted like any other.
    """
    return ToolAllowlist(self.explicit | names, self.derived, self.buckets)

category_label

category_label(name: str) -> str

Display label for a category, falling back to its raw dr_* name.

The fallback exists so an unlabelled category degrades to something readable instead of a KeyError mid-response; the test that pins full coverage of MCPToolCategory is what keeps the fallback unreachable in practice.

Source code in datarobot_genai/drmcputils/categories.py
def category_label(name: str) -> str:
    """Display label for a category, falling back to its raw ``dr_*`` name.

    The fallback exists so an unlabelled category degrades to something readable
    instead of a ``KeyError`` mid-response; the test that pins full coverage of
    ``MCPToolCategory`` is what keeps the fallback unreachable in practice.
    """
    try:
        return TOOL_CATEGORY_LABELS[MCPToolCategory(name)]
    except (KeyError, ValueError):
        return name

resolve_tool_allowlist

resolve_tool_allowlist(entries: frozenset[str]) -> ToolAllowlist

Sort raw header tokens into explicit names, category-derived names and buckets.

Resolution rules (per entry): 1. Marker-resolved category → a bucket, matched against the tool's own marker 2. Parent category → its leaf children's tool names, as derived 3. Leaf category → its tool names, as derived 4. Anything else → an explicit name (a plain tool name, or a typo that will simply match nothing — never an error)

Source code in datarobot_genai/drmcputils/categories.py
def resolve_tool_allowlist(entries: frozenset[str]) -> ToolAllowlist:
    """Sort raw header tokens into explicit names, category-derived names and buckets.

    Resolution rules (per entry):
    1. Marker-resolved category  → a bucket, matched against the tool's own marker
    2. Parent category           → its leaf children's tool names, as *derived*
    3. Leaf category             → its tool names, as *derived*
    4. Anything else             → an *explicit* name (a plain tool name, or a typo
       that will simply match nothing — never an error)
    """
    explicit: set[str] = set()
    derived: set[str] = set()
    buckets: set[str] = set()
    for entry in entries:
        if entry in MARKER_RESOLVED_CATEGORIES:
            buckets.add(entry)
        elif entry in PARENT_TO_CHILDREN:
            for leaf in PARENT_TO_CHILDREN[entry]:
                derived.update(LEAF_CATEGORY_TOOLS.get(leaf, frozenset()))
        elif entry in LEAF_CATEGORY_TOOLS:
            derived.update(LEAF_CATEGORY_TOOLS[entry])
        else:
            explicit.add(entry)
    return ToolAllowlist(frozenset(explicit), frozenset(derived), frozenset(buckets))

resolve_to_tool_names

resolve_to_tool_names(entries: frozenset[str]) -> frozenset[str]

Expand category names in entries to their constituent tool names.

Resolution rules (applied per entry): 1. Parent category → expand to all leaf categories → expand each to tool names 2. Leaf category → expand to its tool names 3. Anything else → kept as-is (treated as a plain tool name)

Unknown entries (typos, future categories) are silently kept as plain strings. They will simply never match any registered tool name and the filter will ignore them — no error is raised.

Marker-resolved categories expand to nothing here: their membership is a property of each tool's marker, not of this taxonomy. Use :func:resolve_tool_allowlist for the request path, which keeps them as buckets for the matcher to settle.

Parameters:

Name Type Description Default
entries frozenset[str]

Raw strings parsed from the x-datarobot-mcp-tools header.

required
Returns
Resolved set of tool function names (plain strings only).
Source code in datarobot_genai/drmcputils/categories.py
def resolve_to_tool_names(entries: frozenset[str]) -> frozenset[str]:
    """Expand category names in *entries* to their constituent tool names.

    Resolution rules (applied per entry):
    1. Parent category  → expand to all leaf categories → expand each to tool names
    2. Leaf category    → expand to its tool names
    3. Anything else    → kept as-is (treated as a plain tool name)

    Unknown entries (typos, future categories) are silently kept as plain
    strings.  They will simply never match any registered tool name and the
    filter will ignore them — no error is raised.

    Marker-resolved categories expand to nothing here: their membership is a property
    of each tool's marker, not of this taxonomy. Use :func:`resolve_tool_allowlist`
    for the request path, which keeps them as buckets for the matcher to settle.

    Args:
        entries: Raw strings parsed from the ``x-datarobot-mcp-tools`` header.

    Returns
    -------
        Resolved set of tool function names (plain strings only).
    """
    allowlist = resolve_tool_allowlist(entries)
    return allowlist.explicit | allowlist.derived

parse_toolset_names_header

parse_toolset_names_header(raw: str | None) -> frozenset[str] | None

Parse x-datarobot-mcp-toolsets to toolset bundle names (not tool names).

Uses the same comma-separated token format as x-datarobot-mcp-tools. Returns None when the header is absent or blank (no toolset filter).

Source code in datarobot_genai/drmcputils/categories.py
def parse_toolset_names_header(raw: str | None) -> frozenset[str] | None:
    """Parse ``x-datarobot-mcp-toolsets`` to toolset bundle names (not tool names).

    Uses the same comma-separated token format as ``x-datarobot-mcp-tools``. Returns
    ``None`` when the header is absent or blank (no toolset filter).
    """
    return _parse_header_entries(raw)

parse_tool_allowlist_header

parse_tool_allowlist_header(raw: str | None) -> ToolAllowlist | None

Parse the x-datarobot-mcp-tools header into a :class:ToolAllowlist.

Static category names (e.g. dr_connectors, dr_connector_jira) expand to the tool names they contain; marker-resolved categories become buckets; plain tool names and unknown entries are kept as explicit names.

Returns None when the header is absent or blank — no filtering. That is a different answer from an empty allowlist, which denies everything; see effective_tool_allowlist.

Source code in datarobot_genai/drmcputils/categories.py
def parse_tool_allowlist_header(raw: str | None) -> ToolAllowlist | None:
    """Parse the x-datarobot-mcp-tools header into a :class:`ToolAllowlist`.

    Static category names (e.g. ``dr_connectors``, ``dr_connector_jira``) expand to
    the tool names they contain; marker-resolved categories become buckets; plain
    tool names and unknown entries are kept as explicit names.

    Returns ``None`` when the header is absent or blank — *no filtering*. That is a
    different answer from an empty allowlist, which denies everything; see
    ``effective_tool_allowlist``.
    """
    entries = _parse_header_entries(raw)
    if entries is None:
        return None
    return resolve_tool_allowlist(entries)

category_kind

category_kind(name: str) -> str

Return "parent" for a category that owns leaf children, "leaf" otherwise.

Source code in datarobot_genai/drmcputils/categories.py
def category_kind(name: str) -> str:
    """Return ``"parent"`` for a category that owns leaf children, ``"leaf"`` otherwise."""
    return "parent" if name in PARENT_TO_CHILDREN else "leaf"

category_entry

category_entry(name: str) -> dict[str, str]

Serialise one category as the {name, label, kind} dict the REST responses carry.

Source code in datarobot_genai/drmcputils/categories.py
def category_entry(name: str) -> dict[str, str]:
    """Serialise one category as the ``{name, label, kind}`` dict the REST responses carry."""
    return {"name": str(name), "label": category_label(name), "kind": category_kind(name)}

categories_for_tool

categories_for_tool(tool_name: str) -> list[dict[str, str]]

Return the categories (leaf + parent) for tool_name as {name, label, kind} dicts.

Sorted by category name. Empty list for hosted/dynamic tools and any name not in the static taxonomy.

Source code in datarobot_genai/drmcputils/categories.py
def categories_for_tool(tool_name: str) -> list[dict[str, str]]:
    """Return the categories (leaf + parent) for *tool_name* as ``{name, label, kind}`` dicts.

    Sorted by category name. Empty list for hosted/dynamic tools and any name not in
    the static taxonomy.
    """
    return [category_entry(name) for name in sorted(TOOL_TO_CATEGORIES.get(tool_name, frozenset()))]