Skip to content

Commit 6295a4b

Browse files
committed
docs(MCP): Rewrite MCP server page and generate the tool catalogue
Rewrite the user-facing MCP server page for the self-built server, replacing the Gram-hosted page: - Cloud (`https://mcp.flagsmith.com`) and self-hosted (`https://<host>/mcp/`) URLs - OAuth (auto-discovered) and `Authorization: Api-Key` authentication - Per-client install tabs: Claude Code, Cursor, Claude Desktop, Codex CLI, Windsurf, Gemini CLI, VS Code, and a generic fallback - Gram deprecation / migration callout, example prompts Generate the tool catalogue from the OpenAPI schema rather than hand-maintaining it, so descriptions stay in sync with the tools: - `manage.py generate_mcp_tool_catalogue` renders an aligned Markdown table of the MCP-tagged operations (verbatim `operationId` + `description`); `--exclude` omits tools already listed in another catalogue. - `make generate-docs` writes the core catalogue (`_mcp-tool-catalogue.md`); `api-tests-with-private-packages` derives the enterprise-only catalogue (`_mcp-tool-catalogue-enterprise.md`) against it. The page composes both via MDX imports (hence the `.md` -> `.mdx` rename). Contributes to Flagsmith/flagsmith-private#144. beep boop
1 parent 0483dd6 commit 6295a4b

10 files changed

Lines changed: 431 additions & 106 deletions

File tree

.github/workflows/api-tests-with-private-packages.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ jobs:
6060
make integrate-private-tests
6161
rm -rf ${HOME}/.git-credentials
6262
63+
- name: Check MCP enterprise tool catalogue is up to date
64+
env:
65+
DOTENV_OVERRIDE_FILE: .env-ci
66+
run: |
67+
uv run python manage.py generate_mcp_tool_catalogue \
68+
--exclude ../docs/docs/integrating-with-flagsmith/_mcp-tool-catalogue.md \
69+
> ../docs/docs/integrating-with-flagsmith/_mcp-tool-catalogue-enterprise.md
70+
git diff --exit-code ../docs/docs/integrating-with-flagsmith/_mcp-tool-catalogue-enterprise.md
71+
6372
- name: Run Tests
6473
env:
6574
DOTENV_OVERRIDE_FILE: .env-ci

api/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ integrate-private-tests:
164164
generate-docs: generate-flagsmith-sdk-openapi
165165
uv run flagsmith docgen metrics > ../docs/docs/deployment-self-hosting/observability/_metrics-catalogue.md
166166
uv run flagsmith docgen events > ../docs/docs/deployment-self-hosting/observability/_events-catalogue.md
167+
uv run python manage.py generate_mcp_tool_catalogue > ../docs/docs/integrating-with-flagsmith/_mcp-tool-catalogue.md
167168

168169
.PHONY: add-known-sdk-version
169170
add-known-sdk-version:

api/api/management/__init__.py

Whitespace-only changes.

api/api/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import re
2+
from argparse import ArgumentParser
3+
from pathlib import Path
4+
from typing import Any
5+
6+
from django.core.management.base import BaseCommand
7+
8+
from api.openapi import MCPSchemaGenerator
9+
10+
_TOOL_NAME_RE = re.compile(r"^\| `([^`]+)`")
11+
12+
13+
class Command(BaseCommand):
14+
help = (
15+
"Generate a Markdown table of the MCP tool catalogue from the OpenAPI "
16+
"schema. The set of tools reflects the apps installed in the current "
17+
"environment, so private/enterprise tools only appear when their packages "
18+
"are installed. Pass --exclude to omit tools already listed in an existing "
19+
"catalogue (used to derive the enterprise-only catalogue against the core one)."
20+
)
21+
22+
def add_arguments(self, parser: ArgumentParser) -> None:
23+
parser.add_argument(
24+
"--exclude",
25+
type=Path,
26+
default=None,
27+
help="Path to an existing catalogue whose tools should be omitted.",
28+
)
29+
30+
def handle(self, *args: Any, exclude: Path | None = None, **options: Any) -> None:
31+
excluded = _read_tool_names(exclude) if exclude else set()
32+
generator = MCPSchemaGenerator()
33+
schema = generator.get_schema(request=None, public=True)
34+
35+
rows = sorted(
36+
(operation["operationId"], _one_line(operation.get("description", "")))
37+
for path_item in schema.get("paths", {}).values()
38+
for operation in path_item.values()
39+
if isinstance(operation, dict) and "operationId" in operation
40+
if operation["operationId"] not in excluded
41+
)
42+
43+
self.stdout.write(_render_table(("Tool", "Description"), rows))
44+
45+
46+
def _read_tool_names(path: Path) -> set[str]:
47+
return {
48+
match.group(1)
49+
for line in path.read_text().splitlines()
50+
if (match := _TOOL_NAME_RE.match(line))
51+
}
52+
53+
54+
def _one_line(text: str) -> str:
55+
return " ".join(text.split()).replace("|", "\\|")
56+
57+
58+
def _render_table(header: tuple[str, str], rows: list[tuple[str, str]]) -> str:
59+
# Render an aligned Markdown table matching Prettier's output so the committed
60+
# catalogue is reproducible by `make generate-docs` and passes the docs
61+
# Prettier check unchanged.
62+
cells = [list(header)] + [[f"`{name}`", description] for name, description in rows]
63+
widths = [max(len(row[col]) for row in cells) for col in range(len(header))]
64+
lines = [
65+
_render_row(cells[0], widths),
66+
"| " + " | ".join("-" * width for width in widths) + " |",
67+
*(_render_row(row, widths) for row in cells[1:]),
68+
]
69+
return "\n".join(lines)
70+
71+
72+
def _render_row(cells: list[str], widths: list[int]) -> str:
73+
padded = " | ".join(cell.ljust(width) for cell, width in zip(cells, widths))
74+
return f"| {padded} |"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import io
2+
from pathlib import Path
3+
4+
from django.core.management import call_command
5+
6+
7+
def _tool_names(table: str) -> list[str]:
8+
return [line.split("`")[1] for line in table.splitlines() if line.startswith("| `")]
9+
10+
11+
def test_generate_mcp_tool_catalogue__no_args__renders_sorted_mcp_tool_table() -> None:
12+
# Given
13+
out = io.StringIO()
14+
15+
# When
16+
call_command("generate_mcp_tool_catalogue", stdout=out)
17+
18+
# Then
19+
table = out.getvalue()
20+
lines = table.splitlines()
21+
assert lines[0].startswith("| Tool ")
22+
assert set(lines[1].replace("|", "").replace(" ", "")) == {"-"}
23+
24+
names = _tool_names(table)
25+
assert "list_environments" in names
26+
assert names == sorted(names)
27+
assert "Lists all environments the user has access to" in table
28+
29+
30+
def test_generate_mcp_tool_catalogue__exclude_file__omits_listed_tools(
31+
tmp_path: Path,
32+
) -> None:
33+
# Given
34+
exclude = tmp_path / "_mcp-tool-catalogue.md"
35+
exclude.write_text("| Tool | Description |\n| `list_environments` | ... |\n")
36+
out = io.StringIO()
37+
38+
# When
39+
call_command("generate_mcp_tool_catalogue", exclude=exclude, stdout=out)
40+
41+
# Then
42+
names = _tool_names(out.getvalue())
43+
assert "list_environments" not in names
44+
assert "get_project" in names
45+
46+
47+
def test_generate_mcp_tool_catalogue__description_with_pipe__escapes_pipe() -> None:
48+
# Given / When
49+
out = io.StringIO()
50+
call_command("generate_mcp_tool_catalogue", stdout=out)
51+
52+
# Then
53+
# No raw pipe should appear inside a description cell (only as a column
54+
# separator), so every table row has exactly two unescaped delimiters plus
55+
# the leading and trailing ones.
56+
for line in out.getvalue().splitlines():
57+
if line.startswith("| `"):
58+
assert line.replace("\\|", "").count("|") == 3
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
| Tool | Description |
2+
| ------------------------------------------- | ------------------------------------------------------------------------------ |
3+
| `add_feature_to_release_pipeline` | Adds a feature flag to a release pipeline for staged rollout. |
4+
| `create_environment_feature_change_request` | Creates a new change request for feature flag modifications in an environment. |
5+
| `get_release_pipeline` | Retrieves detailed information about a specific release pipeline. |
6+
| `list_environment_change_requests` | Retrieves all change requests for an environment. |
7+
| `list_organization_roles` | Retrieves all custom roles defined within the organisation. |
8+
| `list_project_change_requests` | Retrieves all change requests for a project. |
9+
| `list_project_release_pipelines` | Retrieves all release pipelines configured for the specified project. |
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
| Tool | Description |
2+
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
3+
| `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). |
4+
| `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). |
5+
| `create_feature` | Creates a new feature flag in the specified project with default settings. |
6+
| `create_feature_multivariate_option` | Creates a new multivariate option for a feature flag. |
7+
| `create_organization_invite` | Send an invitation to join the organisation with specified role and permissions. |
8+
| `create_project_segment` | Creates a new user segment for audience targeting within the project. |
9+
| `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). |
10+
| `delete_feature_multivariate_option` | Deletes a multivariate option. |
11+
| `delete_feature_segment` | Deletes a segment override. Applies to environments without v2 feature versioning (use_v2_feature_versioning: false). |
12+
| `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). |
13+
| `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). |
14+
| `get_feature_code_references` | Retrieves code references and usage information for the feature flag. |
15+
| `get_feature_evaluation_data` | Retrieves evaluation data and analytics for a specific feature flag. |
16+
| `get_feature_external_resources` | Retrieves external resources linked to the feature flag. |
17+
| `get_feature_flag` | Retrieves detailed information about a specific feature flag. |
18+
| `get_feature_health_events` | Retrieves feature health monitoring events and metrics for the project. |
19+
| `get_project` | Retrieves comprehensive information about a specific project including configuration and statistics. |
20+
| `get_project_segment` | Retrieves detailed information about a specific user segment. |
21+
| `list_environments` | Lists all environments the user has access to |
22+
| `list_feature_multivariate_options` | Retrieves all multivariate options for a feature flag. |
23+
| `list_feature_segments` | Lists segment overrides for a feature in an environment. |
24+
| `list_organization_groups` | Retrieves all permission groups within the organisation. |
25+
| `list_organization_invites` | Retrieves all pending invitations for the organisation. |
26+
| `list_organizations` | Lists all organisations accessible with the provided user API key. |
27+
| `list_project_environments` | Retrieves all environments configured for the specified project. |
28+
| `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. |
29+
| `list_project_segments` | Retrieves all user segments defined for audience targeting within the project. |
30+
| `list_projects_in_organization` | Retrieves all projects within a specified organisation. |
31+
| `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). |
32+
| `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). |
33+
| `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). |
34+
| `update_feature` | Updates feature flag properties such as name and description. |
35+
| `update_feature_multivariate_option` | Updates an existing multivariate option. |
36+
| `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). |
37+
| `update_project` | Updates project configuration settings such as the project name and feature visibility. |
38+
| `update_project_segment` | Updates an existing user segment's properties and rules. |

0 commit comments

Comments
 (0)