Improve OpenAPI specs

This commit is contained in:
Adam Outler
2026-01-29 23:06:05 +00:00
parent f54ba4817e
commit ed4e0388cc
10 changed files with 533 additions and 133 deletions

View File

@@ -1,10 +1,12 @@
from __future__ import annotations
import re
from typing import Any
from typing import Any, Dict, Optional
import graphene
from .registry import register_tool, _operation_ids
from .schemas import GraphQLRequest
from .schema_converter import pydantic_to_json_schema, resolve_schema_refs
def introspect_graphql_schema(schema: graphene.Schema):
@@ -26,6 +28,7 @@ def introspect_graphql_schema(schema: graphene.Schema):
operation_id="graphql_query",
summary="GraphQL Endpoint",
description="Execute arbitrary GraphQL queries against the system schema.",
request_model=GraphQLRequest,
tags=["graphql"]
)
@@ -36,6 +39,20 @@ def _flask_to_openapi_path(flask_path: str) -> str:
return re.sub(r'<(?:\w+:)?(\w+)>', r'{\1}', flask_path)
def _get_openapi_metadata(func: Any) -> Optional[Dict[str, Any]]:
"""Recursively find _openapi_metadata in wrapped functions."""
# Check current function
metadata = getattr(func, "_openapi_metadata", None)
if metadata:
return metadata
# Check __wrapped__ (standard for @wraps)
if hasattr(func, "__wrapped__"):
return _get_openapi_metadata(func.__wrapped__)
return None
def introspect_flask_app(app: Any):
"""
Introspect the Flask application to find routes decorated with @validate_request
@@ -47,14 +64,13 @@ def introspect_flask_app(app: Any):
if not view_func:
continue
# Check for our decorator's metadata
metadata = getattr(view_func, "_openapi_metadata", None)
if not metadata:
# Fallback for wrapped functions
if hasattr(view_func, "__wrapped__"):
metadata = getattr(view_func.__wrapped__, "_openapi_metadata", None)
# Check for our decorator's metadata recursively
metadata = _get_openapi_metadata(view_func)
if metadata:
if metadata.get("exclude_from_spec"):
continue
op_id = metadata["operation_id"]
# Register the tool with real path and method from Flask
@@ -75,11 +91,8 @@ def introspect_flask_app(app: Any):
# Determine tags - create a copy to avoid mutating shared metadata
tags = list(metadata.get("tags") or ["rest"])
if path.startswith("/mcp/"):
# Move specific tags to secondary position or just add MCP
if "rest" in tags:
tags.remove("rest")
if "mcp" not in tags:
tags.append("mcp")
# For MCP endpoints, we want them exclusively in the 'mcp' tag section
tags = ["mcp"]
# Ensure unique operationId
original_op_id = op_id
@@ -89,6 +102,38 @@ def introspect_flask_app(app: Any):
unique_op_id = f"{op_id}_{count}"
count += 1
# Filter path_params to only include those that are actually in the path
path_params = metadata.get("path_params")
if path_params:
path_params = [
p for p in path_params
if f"{{{p['name']}}}" in path
]
# Auto-generate query_params from request_model for GET requests
query_params = metadata.get("query_params")
if method == 'GET' and not query_params and metadata.get("request_model"):
try:
schema = pydantic_to_json_schema(metadata["request_model"])
properties = schema.get("properties", {})
query_params = []
for name, prop in properties.items():
is_required = name in schema.get("required", [])
# Create param definition, preserving enum/schema
param_def = {
"name": name,
"in": "query",
"required": is_required,
"description": prop.get("description", ""),
"schema": prop
}
# Remove description from schema to avoid duplication
if "description" in param_def["schema"]:
del param_def["schema"]["description"]
query_params.append(param_def)
except Exception:
pass # Fallback to empty if schema generation fails
register_tool(
path=path,
method=method,
@@ -98,9 +143,11 @@ def introspect_flask_app(app: Any):
description=metadata["description"],
request_model=metadata.get("request_model"),
response_model=metadata.get("response_model"),
path_params=metadata.get("path_params"),
query_params=metadata.get("query_params"),
path_params=path_params,
query_params=query_params,
tags=tags,
allow_multipart_payload=metadata.get("allow_multipart_payload", False)
allow_multipart_payload=metadata.get("allow_multipart_payload", False),
response_content_types=metadata.get("response_content_types"),
links=metadata.get("links")
)
registered_ops.add(op_key)