Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/api-tests-with-private-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ jobs:
make integrate-private-tests
rm -rf ${HOME}/.git-credentials

- name: Check MCP enterprise tool catalogue is up to date
env:
DOTENV_OVERRIDE_FILE: .env-ci
run: |
uv run python manage.py generate_mcp_tool_catalogue \
--exclude ../docs/docs/integrating-with-flagsmith/_mcp-tool-catalogue.md \
> ../docs/docs/integrating-with-flagsmith/_mcp-tool-catalogue-enterprise.md
git diff --exit-code ../docs/docs/integrating-with-flagsmith/_mcp-tool-catalogue-enterprise.md

- name: Run Tests
env:
DOTENV_OVERRIDE_FILE: .env-ci
Expand Down
1 change: 1 addition & 0 deletions api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ integrate-private-tests:
generate-docs: generate-flagsmith-sdk-openapi
uv run flagsmith docgen metrics > ../docs/docs/deployment-self-hosting/observability/_metrics-catalogue.md
uv run flagsmith docgen events > ../docs/docs/deployment-self-hosting/observability/_events-catalogue.md
uv run python manage.py generate_mcp_tool_catalogue > ../docs/docs/integrating-with-flagsmith/_mcp-tool-catalogue.md

.PHONY: add-known-sdk-version
add-known-sdk-version:
Expand Down
Empty file.
Empty file.
74 changes: 74 additions & 0 deletions api/api/management/commands/generate_mcp_tool_catalogue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import re
from argparse import ArgumentParser
from pathlib import Path
from typing import Any

from django.core.management.base import BaseCommand

from api.openapi import MCPSchemaGenerator

_TOOL_NAME_RE = re.compile(r"^\| `([^`]+)`")


class Command(BaseCommand):
help = (
"Generate a Markdown table of the MCP tool catalogue from the OpenAPI "
"schema. The set of tools reflects the apps installed in the current "
"environment, so private/enterprise tools only appear when their packages "
"are installed. Pass --exclude to omit tools already listed in an existing "
"catalogue (used to derive the enterprise-only catalogue against the core one)."
)

def add_arguments(self, parser: ArgumentParser) -> None:
parser.add_argument(
"--exclude",
type=Path,
default=None,
help="Path to an existing catalogue whose tools should be omitted.",
)

def handle(self, *args: Any, exclude: Path | None = None, **options: Any) -> None:
excluded = _read_tool_names(exclude) if exclude else set()
generator = MCPSchemaGenerator()
schema = generator.get_schema(request=None, public=True)

rows = sorted(
(operation["operationId"], _one_line(operation.get("description", "")))
for path_item in schema.get("paths", {}).values()
for operation in path_item.values()
if isinstance(operation, dict) and "operationId" in operation
if operation["operationId"] not in excluded
)

self.stdout.write(_render_table(("Tool", "Description"), rows))


def _read_tool_names(path: Path) -> set[str]:
return {
match.group(1)
for line in path.read_text().splitlines()
if (match := _TOOL_NAME_RE.match(line))
}


def _one_line(text: str) -> str:
return " ".join(text.split()).replace("|", "\\|")


def _render_table(header: tuple[str, str], rows: list[tuple[str, str]]) -> str:
# Render an aligned Markdown table matching Prettier's output so the committed
# catalogue is reproducible by `make generate-docs` and passes the docs
# Prettier check unchanged.
cells = [list(header)] + [[f"`{name}`", description] for name, description in rows]
widths = [max(len(row[col]) for row in cells) for col in range(len(header))]
lines = [
_render_row(cells[0], widths),
"| " + " | ".join("-" * width for width in widths) + " |",
*(_render_row(row, widths) for row in cells[1:]),
]
return "\n".join(lines)


def _render_row(cells: list[str], widths: list[int]) -> str:
padded = " | ".join(cell.ljust(width) for cell, width in zip(cells, widths))
return f"| {padded} |"
58 changes: 58 additions & 0 deletions api/tests/unit/api/test_generate_mcp_tool_catalogue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import io
from pathlib import Path

from django.core.management import call_command


def _tool_names(table: str) -> list[str]:
return [line.split("`")[1] for line in table.splitlines() if line.startswith("| `")]


def test_generate_mcp_tool_catalogue__no_args__renders_sorted_mcp_tool_table() -> None:
# Given
out = io.StringIO()

# When
call_command("generate_mcp_tool_catalogue", stdout=out)

# Then
table = out.getvalue()
lines = table.splitlines()
assert lines[0].startswith("| Tool ")
assert set(lines[1].replace("|", "").replace(" ", "")) == {"-"}

names = _tool_names(table)
assert "list_environments" in names
assert names == sorted(names)
assert "Lists all environments the user has access to" in table


def test_generate_mcp_tool_catalogue__exclude_file__omits_listed_tools(
tmp_path: Path,
) -> None:
# Given
exclude = tmp_path / "_mcp-tool-catalogue.md"
exclude.write_text("| Tool | Description |\n| `list_environments` | ... |\n")
out = io.StringIO()

# When
call_command("generate_mcp_tool_catalogue", exclude=exclude, stdout=out)

# Then
names = _tool_names(out.getvalue())
assert "list_environments" not in names
assert "get_project" in names


def test_generate_mcp_tool_catalogue__description_with_pipe__escapes_pipe() -> None:
# Given / When
out = io.StringIO()
call_command("generate_mcp_tool_catalogue", stdout=out)

# Then
# No raw pipe should appear inside a description cell (only as a column
# separator), so every table row has exactly two unescaped delimiters plus
# the leading and trailing ones.
for line in out.getvalue().splitlines():
if line.startswith("| `"):
assert line.replace("\\|", "").count("|") == 3
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
| Tool | Description |
| ------------------------------------------- | ------------------------------------------------------------------------------ |
| `add_feature_to_release_pipeline` | Adds a feature flag to a release pipeline for staged rollout. |
| `create_environment_feature_change_request` | Creates a new change request for feature flag modifications in an environment. |
| `get_release_pipeline` | Retrieves detailed information about a specific release pipeline. |
| `list_environment_change_requests` | Retrieves all change requests for an environment. |
| `list_organization_roles` | Retrieves all custom roles defined within the organisation. |
| `list_project_change_requests` | Retrieves all change requests for a project. |
| `list_project_release_pipelines` | Retrieves all release pipelines configured for the specified project. |
38 changes: 38 additions & 0 deletions docs/docs/integrating-with-flagsmith/_mcp-tool-catalogue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
| Tool | Description |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create_environment_feature_version` | Creates a new version for a feature flag in a specific environment. Applies to environments with v2 feature versioning (use_v2_feature_versioning: true). |
| `create_environment_feature_version_state` | Creates a new feature state for a specific version in an environment. Applies to environments with v2 feature versioning (use_v2_feature_versioning: true). |
| `create_feature` | Creates a new feature flag in the specified project with default settings. |
| `create_feature_multivariate_option` | Creates a new multivariate option for a feature flag. |
| `create_organization_invite` | Send an invitation to join the organisation with specified role and permissions. |
| `create_project_segment` | Creates a new user segment for audience targeting within the project. |
| `create_segment_override` | Creates a segment override for a feature in an environment in a single call, setting both the segment binding and its value. Applies to environments without v2 feature versioning (use_v2_feature_versioning: false). |
| `delete_feature_multivariate_option` | Deletes a multivariate option. |
| `delete_feature_segment` | Deletes a segment override. Applies to environments without v2 feature versioning (use_v2_feature_versioning: false). |
| `get_environment_feature_version_states` | Retrieves feature state information for a specific version in an environment. Applies to environments with v2 feature versioning (use_v2_feature_versioning: true). |
| `get_environment_feature_versions` | Retrieves version information for a feature flag in a specific environment. Applies to environments with v2 feature versioning (use_v2_feature_versioning: true). |
| `get_feature_code_references` | Retrieves code references and usage information for the feature flag. |
| `get_feature_evaluation_data` | Retrieves evaluation data and analytics for a specific feature flag. |
| `get_feature_external_resources` | Retrieves external resources linked to the feature flag. |
| `get_feature_flag` | Retrieves detailed information about a specific feature flag. |
| `get_feature_health_events` | Retrieves feature health monitoring events and metrics for the project. |
| `get_project` | Retrieves comprehensive information about a specific project including configuration and statistics. |
| `get_project_segment` | Retrieves detailed information about a specific user segment. |
| `list_environments` | Lists all environments the user has access to |
| `list_feature_multivariate_options` | Retrieves all multivariate options for a feature flag. |
| `list_feature_segments` | Lists segment overrides for a feature in an environment. |
| `list_organization_groups` | Retrieves all permission groups within the organisation. |
| `list_organization_invites` | Retrieves all pending invitations for the organisation. |
| `list_organizations` | Lists all organisations accessible with the provided user API key. |
| `list_project_environments` | Retrieves all environments configured for the specified project. |
| `list_project_features` | Lists a project's feature flags (paginated). Pass `environment=<id>` to also get each feature's live state for that environment in `environment_feature_state`, along with override counts. Works for both v1 and v2 versioned environments. |
| `list_project_segments` | Retrieves all user segments defined for audience targeting within the project. |
| `list_projects_in_organization` | Retrieves all projects within a specified organisation. |
| `publish_environment_feature_version` | Publishes a feature version to make it live in the environment. Applies to environments with v2 feature versioning (use_v2_feature_versioning: true). |
| `update_environment_feature_state` | Updates a feature state in an environment, including enabled status and value. Applies to environments without v2 feature versioning (use_v2_feature_versioning: false). |
| `update_environment_feature_version_state` | Updates an existing feature state for a specific version in an environment. Applies to environments with v2 feature versioning (use_v2_feature_versioning: true). |
| `update_feature` | Updates feature flag properties such as name and description. |
| `update_feature_multivariate_option` | Updates an existing multivariate option. |
| `update_feature_state` | Updates a feature state, including its enabled status and value. Also updates a segment override's value for environments without v2 feature versioning (use_v2_feature_versioning: false). |
| `update_project` | Updates project configuration settings such as the project name and feature visibility. |
| `update_project_segment` | Updates an existing user segment's properties and rules. |
Loading
Loading