-
Notifications
You must be signed in to change notification settings - Fork 468
feat: synchronize-openapi-schema-with-gram #6499
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
base: main
Are you sure you want to change the base?
Changes from 12 commits
be43ede
893ee8b
c887686
8347c94
25a58df
6d2deac
8e1a64d
46efa8e
7b0ecb4
b5fc717
fab2768
c3b8f97
c27833e
e58ea58
9d2351a
47b03de
52c002f
444a4fa
ce1244d
b3c5467
8c4c7fd
71a4800
890cfb9
1f89181
6ccc3c4
4d2fb11
1d312be
80ce3ce
a7188b6
3c1fd00
27bd651
4cbf4d5
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 |
|---|---|---|
|
|
@@ -51,7 +51,88 @@ 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". | ||
|
|
||
| Supports custom extensions: | ||
| - x-mcp-name: Override the operationId for MCP tools | ||
| - x-mcp-description: Override the description for MCP tools | ||
| """ | ||
|
|
||
| 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") | ||
| info["title"] = "mcp_openapi" | ||
|
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. Info dict mutated without copying causing potential state corruptionMedium Severity The |
||
| return { | ||
| "openapi": schema.pop("openapi"), | ||
| "info": info, | ||
| "servers": [{"url": self.MCP_SERVER_URL}], | ||
| **schema, | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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] = {} | ||
|
|
||
| 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) | ||
|
|
||
| if any(isinstance(op, dict) for op in filtered_operations.values()): | ||
| 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() | ||
|
|
||
| # Override operationId if x-mcp-name provided | ||
| if mcp_name := operation.pop("x-mcp-name", None): | ||
| operation["operationId"] = mcp_name | ||
|
|
||
| # Override description if x-mcp-description provided | ||
| if mcp_desc := operation.pop("x-mcp-description", None): | ||
| operation["description"] = mcp_desc | ||
|
|
||
| return operation | ||
|
|
||
| def _update_security_for_mcp(self, schema: dict[str, Any]) -> dict[str, Any]: | ||
| """Update security schemes for MCP (admin API key).""" | ||
| schema = schema.copy() | ||
| schema["components"] = schema.get("components", {}).copy() | ||
| schema["components"]["securitySchemes"] = { | ||
| "ApiKey": { | ||
| "type": "apiKey", | ||
| "in": "header", | ||
| "name": "Authorization", | ||
| "description": "API key for MCP access. Format: Api-Key <key>", | ||
| }, | ||
| } | ||
| schema["security"] = [{"ApiKey": []}] | ||
| return schema | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| 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 CustomSpectacularJSONAPIView(SpectacularJSONAPIView): # type: ignore[misc] | ||
|
Check failure on line 10 in api/api/openapi_views.py
|
||
| """ | ||
| JSON schema view that supports ?mcp=true query parameter for MCP-filtered output. | ||
| """ | ||
|
|
||
| def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: | ||
| if request.query_params.get("mcp", "").lower() == "true": | ||
| self.generator_class = MCPSchemaGenerator | ||
| else: | ||
| self.generator_class = SchemaGenerator | ||
| return super().get(request, *args, **kwargs) | ||
|
Check failure on line 20 in api/api/openapi_views.py
|
||
|
|
||
|
|
||
| class CustomSpectacularYAMLAPIView(SpectacularYAMLAPIView): # type: ignore[misc] | ||
|
Check failure on line 23 in api/api/openapi_views.py
|
||
| """ | ||
| YAML schema view that supports ?mcp=true query parameter for MCP-filtered output. | ||
| """ | ||
|
|
||
| def get(self, request: Request, *args: Any, **kwargs: Any) -> Response: | ||
| if request.query_params.get("mcp", "").lower() == "true": | ||
| self.generator_class = MCPSchemaGenerator | ||
| response = super().get(request, *args, **kwargs) | ||
|
Check failure on line 31 in api/api/openapi_views.py
|
||
| response["Content-Disposition"] = 'attachment; filename="mcp_openapi.yaml"' | ||
| return response | ||
|
Check failure on line 33 in api/api/openapi_views.py
|
||
| else: | ||
| self.generator_class = SchemaGenerator | ||
| return super().get(request, *args, **kwargs) | ||
|
Check failure on line 36 in api/api/openapi_views.py
|
||
Uh oh!
There was an error while loading. Please reload this page.