-
Notifications
You must be signed in to change notification settings - Fork 567
docs(MCP): Rewrite MCP server page and generate the tool catalogue #7668
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
Comment on lines
+63
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add a TODO mentioning #7669? |
||
| - name: Run Tests | ||
| env: | ||
| DOTENV_OVERRIDE_FILE: .env-ci | ||
|
|
||
| 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.", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| ) | ||||||
|
|
||||||
| 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) | ||||||
|
Comment on lines
+58
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: Not a fan of this, seems fragile. Feels like this code should give place to something that should already exist.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah full disclosure I didn't ask Claude to generate prettier-compatible markdown. :/ |
||||||
|
|
||||||
|
|
||||||
| 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} |" | ||||||
| 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. | |
| 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. | |
Uh oh!
There was an error while loading. Please reload this page.