Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
be43ede
feat: added-mcp-filtered-tag-openapi-schema-generation
Zaimwa9 Jan 7, 2026
893ee8b
feat: added-iso-endpoints-to-current-gram
Zaimwa9 Jan 8, 2026
c887686
feat: added-list-environment-endpoint
Zaimwa9 Jan 8, 2026
8347c94
feat: try-ci-locally
Zaimwa9 Jan 8, 2026
25a58df
feat: removed-useless-comments
Zaimwa9 Jan 8, 2026
6d2deac
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 8, 2026
8e1a64d
feat: install-with-no-root
Zaimwa9 Jan 9, 2026
46efa8e
Merge branch 'feat/synchronize-openapi-schema-with-gram' of github.co…
Zaimwa9 Jan 9, 2026
7b0ecb4
feat: use-make-install
Zaimwa9 Jan 9, 2026
b5fc717
feat: changed-install-gram-step
Zaimwa9 Jan 9, 2026
fab2768
feat: added-gram-org
Zaimwa9 Jan 9, 2026
c3b8f97
feat: moved-gram-push-to-deploy-workflow
Zaimwa9 Jan 9, 2026
c27833e
add-permissions-read-to-gram-step
Zaimwa9 Jan 9, 2026
e58ea58
feat: finetuning-open-api-generation
Zaimwa9 Jan 12, 2026
9d2351a
feat: integrate-security-scheme-and-missing-endpoints
Zaimwa9 Jan 12, 2026
47b03de
feat: bumped-workflows-and-private
Zaimwa9 Jan 16, 2026
52c002f
feat: bumped-workflows-and-private
Zaimwa9 Jan 16, 2026
444a4fa
feat: moved-to-using-x-gram
Zaimwa9 Jan 19, 2026
ce1244d
feat: added-gram-push-to-pr-workflow
Zaimwa9 Jan 19, 2026
b3c5467
feat: added-permission-checks
Zaimwa9 Jan 19, 2026
8c4c7fd
feat: trigger-pipeline
Zaimwa9 Jan 19, 2026
71a4800
feat: fixed-conflicts
Zaimwa9 Jan 19, 2026
890cfb9
feat: re-added-job
Zaimwa9 Jan 19, 2026
1f89181
feat: resynced-lock
Zaimwa9 Jan 19, 2026
6ccc3c4
feat: re-added-private-module
Zaimwa9 Jan 19, 2026
4d2fb11
feat: cleanup-site-packagers
Zaimwa9 Jan 19, 2026
1d312be
feat: bumped-private-modules-versions
Zaimwa9 Jan 19, 2026
80ce3ce
feat: check-auth-prefix
Zaimwa9 Jan 19, 2026
a7188b6
feat: fixed-mypy
Zaimwa9 Jan 19, 2026
3c1fd00
feat: removed-dot
Zaimwa9 Jan 19, 2026
27bd651
feat: removed-gram-on-platform-pull-request
Zaimwa9 Jan 19, 2026
4cbf4d5
feat: clean-site-packages-before-mv
Zaimwa9 Jan 19, 2026
75b803e
feat: codecov-patch
Zaimwa9 Jan 20, 2026
f2594e4
feat: removed-unused-content-disposition
Zaimwa9 Jan 20, 2026
3b09ca2
feat: addressed-review-comments
Zaimwa9 Jan 21, 2026
442821d
feat: rebased-main
Zaimwa9 Jan 21, 2026
a01905b
feat: reverted-site-packages-clean-up
Zaimwa9 Jan 21, 2026
ecdfb84
feat: fixed-lock
Zaimwa9 Jan 21, 2026
16335c9
Merge branch 'main' of github.com:Flagsmith/flagsmith into feat/synch…
Zaimwa9 Jan 21, 2026
e06d217
feat: moved-project-and-org-to-vars
Zaimwa9 Jan 21, 2026
4b148b1
feat: wrapped-init-in-type-checking
Zaimwa9 Jan 21, 2026
a133b58
feat: copy-dictionary-instead-of-mutating
Zaimwa9 Jan 21, 2026
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
42 changes: 42 additions & 0 deletions .github/workflows/api-deploy-production-ecs.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: API Deploy to Production ECS

permissions:
contents: read

on:
push:
tags:
Expand All @@ -19,3 +22,42 @@ jobs:
with:
environment: production
secrets: inherit

mcp-schema-push:
name: Push MCP Schema to Gram
runs-on: depot-ubuntu-latest
defaults:
run:
working-directory: api
steps:
- uses: actions/checkout@v4

- name: Install Poetry
run: make install-poetry

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: poetry

- name: Install dependencies
run: |
echo "https://${{ secrets.GH_PRIVATE_ACCESS_TOKEN }}:@github.com" > ${HOME}/.git-credentials
git config --global credential.helper store
make install-packages opts="--with saml,auth-controller,workflows,release-pipelines"
make install-private-modules
rm -rf ${HOME}/.git-credentials

- name: Generate MCP schema
run: make generate-mcp-spec

- name: Install Gram CLI
run: curl -fsSL https://go.getgram.ai/cli.sh | bash

- name: Push to Gram
env:
GRAM_API_KEY: ${{ secrets.GRAM_API_KEY }}
GRAM_ORG: ${{ vars.GRAM_ORG }}
GRAM_PROJECT: ${{ vars.GRAM_PROJECT }}
run: gram push --api-key "$GRAM_API_KEY" --org "$GRAM_ORG" --project "$GRAM_PROJECT" --config gram.json
6 changes: 5 additions & 1 deletion api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ DOTENV_OVERRIDE_FILE ?= .env
POETRY_VERSION ?= 2.2.1

SAML_REVISION ?= v1.6.6
RBAC_REVISION ?= v0.12.1
RBAC_REVISION ?= v0.13.0

-include .env-local
-include $(DOTENV_OVERRIDE_FILE)
Expand Down Expand Up @@ -159,3 +159,7 @@ generate-docs:
.PHONY: add-known-sdk-version
add-known-sdk-version:
poetry run python scripts/add-known-sdk-version.py $(opts)

.PHONY: generate-mcp-spec
generate-mcp-spec:
poetry run python manage.py spectacular --generator-class api.openapi.MCPSchemaGenerator --file mcp_openapi.yaml
84 changes: 82 additions & 2 deletions api/api/openapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Literal
from typing import TYPE_CHECKING, Any, Literal

from drf_spectacular import generators, openapi
from drf_spectacular.extensions import (
Expand Down Expand Up @@ -47,11 +47,91 @@ class SchemaGenerator(generators.SchemaGenerator):
Adds a `$schema` property to the root schema object.
"""

if TYPE_CHECKING:
# Parent class has no type_hints, this is used to ignore the type error upstream
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs) # type: ignore[no-untyped-call]

def get_schema(
self, request: Request | None = None, public: bool = False
) -> dict[str, Any]:
schema: dict[str, Any] = super().get_schema(request, public) # type: ignore[no-untyped-call]
schema["$schema"] = "https://spec.openapis.org/oas/3.1/dialect/base"
return {
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
**schema,
}


class MCPSchemaGenerator(SchemaGenerator):
"""
Schema generator that filters to only include operations tagged with "mcp".

Uses x-gram extension for Gram-native tool naming and descriptions.
Gram reads x-gram directly from the spec.
"""

MCP_TAG = "mcp"
MCP_SERVER_URL = "https://api.flagsmith.com"

def get_schema(
self, request: Request | None = None, public: bool = False
) -> dict[str, Any]:
schema = super().get_schema(request, public)
schema["paths"] = self._filter_paths(schema.get("paths", {}))
schema = self._update_security_for_mcp(schema)
schema.pop("$schema", None)
info = schema.pop("info").copy()
info["title"] = "mcp_openapi"
return {
"openapi": schema.pop("openapi"),
"info": info,
"servers": [{"url": self.MCP_SERVER_URL}],
**schema,
}

def _filter_paths(self, paths: dict[str, Any]) -> dict[str, Any]:
"""Filter paths to only include operations tagged with 'mcp'."""
filtered_paths: dict[str, Any] = {}

for path, path_item in paths.items():
filtered_operations: dict[str, Any] = {}
has_any_mcp_tag = False

for method, operation in path_item.items():
if not isinstance(operation, dict):
filtered_operations[method] = operation
continue

tags = operation.get("tags", [])
if self.MCP_TAG in tags:
filtered_operations[method] = self._transform_for_mcp(operation)
has_any_mcp_tag = True

if has_any_mcp_tag:
filtered_paths[path] = filtered_operations

return filtered_paths

def _transform_for_mcp(self, operation: dict[str, Any]) -> dict[str, Any]:
"""Apply MCP-specific transformations to an operation."""
operation = operation.copy()
# Remove operation-level security (use global MCP security instead)
operation.pop("security", None)
return operation

def _update_security_for_mcp(self, schema: dict[str, Any]) -> dict[str, Any]:
"""Update security schemes for MCP (Organisation API Key)."""
schema = schema.copy()
schema["components"] = schema.get("components", {}).copy()
schema["components"]["securitySchemes"] = {
"TOKEN_AUTH": {
"type": "apiKey",
"in": "header",
"name": "Authorization",
"description": "Organisation API Key. Format: Api-Key <key>",
},
}
schema["security"] = [{"TOKEN_AUTH": []}]
return schema


Expand Down
37 changes: 37 additions & 0 deletions api/api/openapi_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import Any

from drf_spectacular.views import SpectacularJSONAPIView, SpectacularYAMLAPIView
from rest_framework.request import Request
from rest_framework.response import Response

from api.openapi import MCPSchemaGenerator, SchemaGenerator


class _MCPSchemaViewMixin:
"""
Mixin that provides MCP schema generator selection based on ?mcp=true query parameter.
"""

def get_generator_class(self) -> type:
try:
if self.request.query_params["mcp"].lower() == "true": # type: ignore[attr-defined]
return MCPSchemaGenerator
except (AttributeError, KeyError):
pass
return SchemaGenerator

def get(self, request: Request, *args: Any, **kwargs: Any) -> Response:
self.generator_class = self.get_generator_class()
return super().get(request, *args, **kwargs) # type: ignore[misc, no-any-return]


class CustomSpectacularJSONAPIView(_MCPSchemaViewMixin, SpectacularJSONAPIView):
"""
JSON schema view that supports ?mcp=true query parameter for MCP-filtered output.
"""


class CustomSpectacularYAMLAPIView(_MCPSchemaViewMixin, SpectacularYAMLAPIView):
"""
YAML schema view that supports ?mcp=true query parameter for MCP-filtered output.
"""
11 changes: 4 additions & 7 deletions api/api/urls/v1.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
from django.conf import settings
from django.urls import include, path, re_path
from drf_spectacular.views import (
SpectacularJSONAPIView,
SpectacularSwaggerView,
SpectacularYAMLAPIView,
)
from drf_spectacular.views import SpectacularSwaggerView
from rest_framework import permissions, routers

from api.openapi_views import CustomSpectacularJSONAPIView, CustomSpectacularYAMLAPIView
from app_analytics.views import SDKAnalyticsFlags, SelfHostedTelemetryAPIView
from environments.identities.traits.views import SDKTraits
from environments.identities.views import SDKIdentities
Expand Down Expand Up @@ -73,14 +70,14 @@
# API documentation
path(
"swagger.json",
SpectacularJSONAPIView.as_view(
CustomSpectacularJSONAPIView.as_view(
permission_classes=[schema_view_permission_class],
),
name="schema-json",
),
path(
"swagger.yaml",
SpectacularYAMLAPIView.as_view(
CustomSpectacularYAMLAPIView.as_view(
permission_classes=[schema_view_permission_class],
),
name="schema-yaml",
Expand Down
9 changes: 8 additions & 1 deletion api/environments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
@method_decorator(
name="list",
decorator=extend_schema(
tags=["mcp"],
parameters=[
OpenApiParameter(
name="project",
Expand All @@ -77,7 +78,13 @@
required=False,
type=int,
)
]
],
extensions={
"x-gram": {
"name": "list_environments",
"description": "Lists all environments the user has access to",
},
},
),
)
class EnvironmentViewSet(viewsets.ModelViewSet): # type: ignore[type-arg]
Expand Down
14 changes: 14 additions & 0 deletions api/features/feature_external_resources/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import re

from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from drf_spectacular.utils import extend_schema
from rest_framework import status, viewsets
from rest_framework.response import Response

Expand All @@ -17,6 +19,18 @@
from .serializers import FeatureExternalResourceSerializer


@method_decorator(
name="list",
decorator=extend_schema(
tags=["mcp"],
extensions={
"x-gram": {
"name": "get_feature_external_resources",
"description": "Retrieves external resources linked to the feature flag.",
},
},
),
)
class FeatureExternalResourceViewSet(viewsets.ModelViewSet): # type: ignore[type-arg]
serializer_class = FeatureExternalResourceSerializer
permission_classes = [FeatureExternalResourcePermissions]
Expand Down
13 changes: 13 additions & 0 deletions api/features/feature_health/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
)
from django.db.models import QuerySet
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from drf_spectacular.utils import extend_schema
from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action, api_view, permission_classes
Expand Down Expand Up @@ -34,6 +35,18 @@
from users.models import FFAdminUser


@method_decorator(
name="list",
decorator=extend_schema(
tags=["mcp"],
extensions={
"x-gram": {
"name": "get_feature_health_events",
"description": "Retrieves feature health monitoring events and metrics for the project.",
},
},
),
)
class FeatureHealthEventViewSet(
mixins.ListModelMixin,
viewsets.GenericViewSet[FeatureHealthEvent],
Expand Down
49 changes: 49 additions & 0 deletions api/features/multivariate/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
CREATE_FEATURE,
VIEW_PROJECT,
)
from django.utils.decorators import method_decorator
from drf_spectacular.utils import extend_schema
from rest_framework import viewsets
from rest_framework.decorators import api_view
Expand All @@ -15,6 +16,54 @@
from .serializers import MultivariateFeatureOptionSerializer


@method_decorator(
name="list",
decorator=extend_schema(
tags=["mcp"],
extensions={
"x-gram": {
"name": "list_feature_multivariate_options",
"description": "Retrieves all multivariate options for a feature flag.",
},
},
),
)
@method_decorator(
name="create",
decorator=extend_schema(
tags=["mcp"],
extensions={
"x-gram": {
"name": "create_feature_multivariate_option",
"description": "Creates a new multivariate option for a feature flag.",
},
},
),
)
@method_decorator(
name="update",
decorator=extend_schema(
tags=["mcp"],
extensions={
"x-gram": {
"name": "update_feature_multivariate_option",
"description": "Updates an existing multivariate option.",
},
},
),
)
@method_decorator(
name="destroy",
decorator=extend_schema(
tags=["mcp"],
extensions={
"x-gram": {
"name": "delete_feature_multivariate_option",
"description": "Deletes a multivariate option.",
},
},
),
)
class MultivariateFeatureOptionViewSet(viewsets.ModelViewSet): # type: ignore[type-arg]
serializer_class = MultivariateFeatureOptionSerializer

Expand Down
Loading
Loading