"""Toolset definitions and filtering utilities for MCP Atlassian.
Groups 68 tools into 21 named toolsets controlled via the TOOLSETS env var.
Supports 'all', 'default', and comma-separated toolset names.
"""
import logging
import os
from dataclasses import dataclass
logger = logging.getLogger(__name__)
TOOLSET_TAG_PREFIX = "toolset:"
@dataclass(frozen=True)
class ToolsetDefinition:
"""Metadata for a named toolset group."""
name: str
description: str
default: bool
# --- Jira toolsets (15) ---
JIRA_TOOLSETS: dict[str, ToolsetDefinition] = {
"jira_issues": ToolsetDefinition(
name="jira_issues",
description="Core issue operations: CRUD, search, batch, changelogs",
default=True,
),
"jira_fields": ToolsetDefinition(
name="jira_fields",
description="Field search and option retrieval",
default=True,
),
"jira_comments": ToolsetDefinition(
name="jira_comments",
description="Issue comment operations",
default=True,
),
"jira_transitions": ToolsetDefinition(
name="jira_transitions",
description="Workflow transition operations",
default=True,
),
"jira_projects": ToolsetDefinition(
name="jira_projects",
description="Project, version, and component management",
default=False,
),
"jira_agile": ToolsetDefinition(
name="jira_agile",
description="Agile boards, sprints, and related operations",
default=False,
),
"jira_links": ToolsetDefinition(
name="jira_links",
description="Issue links, epic links, and remote links",
default=False,
),
"jira_worklog": ToolsetDefinition(
name="jira_worklog",
description="Time tracking and worklog operations",
default=False,
),
"jira_attachments": ToolsetDefinition(
name="jira_attachments",
description="Attachment download and image retrieval",
default=False,
),
"jira_users": ToolsetDefinition(
name="jira_users",
description="User profile operations",
default=False,
),
"jira_watchers": ToolsetDefinition(
name="jira_watchers",
description="Issue watcher operations",
default=False,
),
"jira_service_desk": ToolsetDefinition(
name="jira_service_desk",
description="Jira Service Management queues and service desks",
default=False,
),
"jira_forms": ToolsetDefinition(
name="jira_forms",
description="ProForma form operations",
default=False,
),
"jira_metrics": ToolsetDefinition(
name="jira_metrics",
description="Issue dates and SLA metrics",
default=False,
),
"jira_development": ToolsetDefinition(
name="jira_development",
description="Development info (branches, PRs, commits)",
default=False,
),
}
# --- Confluence toolsets (6) ---
CONFLUENCE_TOOLSETS: dict[str, ToolsetDefinition] = {
"confluence_pages": ToolsetDefinition(
name="confluence_pages",
description="Page CRUD, search, children, and history",
default=True,
),
"confluence_comments": ToolsetDefinition(
name="confluence_comments",
description="Page comment operations",
default=True,
),
"confluence_labels": ToolsetDefinition(
name="confluence_labels",
description="Page label operations",
default=False,
),
"confluence_users": ToolsetDefinition(
name="confluence_users",
description="User search operations",
default=False,
),
"confluence_analytics": ToolsetDefinition(
name="confluence_analytics",
description="Page view analytics",
default=False,
),
"confluence_attachments": ToolsetDefinition(
name="confluence_attachments",
description="Attachment upload, download, and management",
default=False,
),
}
# --- Combined registry ---
ALL_TOOLSETS: dict[str, ToolsetDefinition] = {
**JIRA_TOOLSETS,
**CONFLUENCE_TOOLSETS,
}
DEFAULT_TOOLSETS: set[str] = {
name for name, defn in ALL_TOOLSETS.items() if defn.default
}
def get_enabled_toolsets() -> set[str]:
"""Parse the TOOLSETS env var into a set of enabled toolset names.
Supports keywords 'all' (all 21 toolsets) and 'default' (6 defaults),
plus comma-separated specific toolset names. Case-insensitive for keywords.
When TOOLSETS is unset or empty, returns all toolsets with a deprecation
warning. In v0.22.0 the default will change to DEFAULT_TOOLSETS (6 core).
Set ``TOOLSETS=all`` explicitly to preserve current behavior.
Returns:
A set of valid toolset names. Defaults to all toolsets when unset.
Unknown names are silently dropped with a warning. If only unknown
names are given, returns an empty set (fail-closed).
Examples:
TOOLSETS unset -> all 21 toolsets (with deprecation warning)
TOOLSETS="" -> all 21 toolsets (with deprecation warning)
TOOLSETS="all" -> all 21 names
TOOLSETS="default" -> 6 default names
TOOLSETS="default,jira_agile" -> defaults + jira_agile
TOOLSETS="typo_name" -> set() (fail-closed)
"""
toolsets_str = os.getenv("TOOLSETS")
if not toolsets_str:
logger.info("TOOLSETS not set — all toolsets enabled.")
logger.warning(
"TOOLSETS is not set — currently defaults to all toolsets. "
"In v0.22.0, the default will change to 6 core toolsets only. "
"Set TOOLSETS=all explicitly to preserve current behavior."
)
return set(ALL_TOOLSETS.keys())
# Split by comma and strip whitespace, filter empty tokens
tokens = [t.strip() for t in toolsets_str.split(",")]
tokens = [t for t in tokens if t]
if not tokens:
logger.info("TOOLSETS empty — all toolsets enabled.")
logger.warning(
"TOOLSETS is not set — currently defaults to all toolsets. "
"In v0.22.0, the default will change to 6 core toolsets only. "
"Set TOOLSETS=all explicitly to preserve current behavior."
)
return set(ALL_TOOLSETS.keys())
result: set[str] = set()
for token in tokens:
normalized = token.lower()
if normalized == "all":
logger.info("TOOLSETS: 'all' keyword — enabling all toolsets.")
return set(ALL_TOOLSETS.keys())
elif normalized == "default":
logger.info("TOOLSETS: 'default' keyword — adding default toolsets.")
result |= DEFAULT_TOOLSETS
elif token in ALL_TOOLSETS:
result.add(token)
else:
logger.warning(f"TOOLSETS: unknown toolset name '{token}' — ignoring.")
if result:
logger.info(f"TOOLSETS: enabled toolsets: {sorted(result)}")
else:
logger.warning(
"TOOLSETS: no valid toolset names found — all tools will be blocked (fail-closed)."
)
return result
def should_include_tool_by_toolset(
tool_tags: set[str], enabled_toolsets: set[str] | None
) -> bool:
"""Check if a tool should be included based on toolset filtering.
Args:
tool_tags: The tool's tag set (e.g. {"jira", "read", "toolset:jira_issues"}).
enabled_toolsets: Set of enabled toolset names, or None to include all tools.
Returns:
True if the tool should be included, False otherwise.
Tools without a toolset tag are always included (graceful fallback).
"""
if enabled_toolsets is None:
return True
toolset_name = get_toolset_tag(tool_tags)
if toolset_name is None:
logger.warning(
f"Tool has no toolset tag in {tool_tags} — including by default."
)
return True
return toolset_name in enabled_toolsets
def get_toolset_tag(tags: set[str]) -> str | None:
"""Extract the toolset name from a tool's tag set.
Args:
tags: The tool's tag set.
Returns:
The toolset name (without prefix) if found, None otherwise.
"""
for tag in tags:
if tag.startswith(TOOLSET_TAG_PREFIX):
return tag[len(TOOLSET_TAG_PREFIX) :]
return None