Files
NetAlertX/server/api_server/openapi/introspection.py
Adam Outler ecea1d1fbd feat(api): MCP, OpenAPI & Dynamic Introspection
New Features:
- API endpoints now support comprehensive input validation with detailed error responses via Pydantic models.
- OpenAPI specification endpoint (/openapi.json) and interactive Swagger UI documentation (/docs) now available for API discovery.
- Enhanced MCP session lifecycle management with create, retrieve, and delete operations.
- Network diagnostic tools: traceroute, nslookup, NMAP scanning, and network topology viewing exposed via API.
- Device search, filtering by status (including 'offline'), and bulk operations (copy, delete, update).
- Wake-on-LAN functionality for remote device management.
- Added dynamic tool disablement and status reporting.

Bug Fixes:
- Fixed get_tools_status in registry to correctly return boolean values instead of None for enabled tools.
- Improved error handling for invalid API inputs with standardized validation responses.
- Fixed OPTIONS request handling for cross-origin requests.

Refactoring:
- Significant refactoring of api_server_start.py to use decorator-based validation (@validate_request).
2026-01-18 18:16:18 +00:00

107 lines
3.9 KiB
Python

from __future__ import annotations
import re
from typing import Any
import graphene
from .registry import register_tool, _operation_ids
def introspect_graphql_schema(schema: graphene.Schema):
"""
Introspect the GraphQL schema and register endpoints in the OpenAPI registry.
This bridges the 'living code' (GraphQL) to the OpenAPI spec.
"""
# Graphene schema introspection
graphql_schema = schema.graphql_schema
query_type = graphql_schema.query_type
if not query_type:
return
# We register the main /graphql endpoint once
register_tool(
path="/graphql",
method="POST",
operation_id="graphql_query",
summary="GraphQL Endpoint",
description="Execute arbitrary GraphQL queries against the system schema.",
tags=["graphql"]
)
def _flask_to_openapi_path(flask_path: str) -> str:
"""Convert Flask path syntax to OpenAPI path syntax."""
# Handles <converter:variable> -> {variable} and <variable> -> {variable}
return re.sub(r'<(?:\w+:)?(\w+)>', r'{\1}', flask_path)
def introspect_flask_app(app: Any):
"""
Introspect the Flask application to find routes decorated with @validate_request
and register them in the OpenAPI registry.
"""
registered_ops = set()
for rule in app.url_map.iter_rules():
view_func = app.view_functions.get(rule.endpoint)
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)
if metadata:
op_id = metadata["operation_id"]
# Register the tool with real path and method from Flask
for method in rule.methods:
if method in ("OPTIONS", "HEAD"):
continue
# Create a unique key for this path/method/op combination if needed,
# but operationId must be unique globally.
# If the same function is mounted on multiple paths, we append a suffix
path = _flask_to_openapi_path(str(rule))
# Check if this operation (path + method) is already registered
op_key = f"{method}:{path}"
if op_key in registered_ops:
continue
# 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")
# Ensure unique operationId
original_op_id = op_id
unique_op_id = op_id
count = 1
while unique_op_id in _operation_ids:
unique_op_id = f"{op_id}_{count}"
count += 1
register_tool(
path=path,
method=method,
operation_id=unique_op_id,
original_operation_id=original_op_id if unique_op_id != original_op_id else None,
summary=metadata["summary"],
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"),
tags=tags,
allow_multipart_payload=metadata.get("allow_multipart_payload", False)
)
registered_ops.add(op_key)