mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-31 07:12:23 -07:00
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).
This commit is contained in:
191
server/api_server/openapi/spec_generator.py
Normal file
191
server/api_server/openapi/spec_generator.py
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
NetAlertX OpenAPI Specification Generator
|
||||
|
||||
This module provides a registry-based approach to OpenAPI spec generation.
|
||||
It converts Pydantic models to JSON Schema and assembles a complete OpenAPI 3.1 spec.
|
||||
|
||||
Key Features:
|
||||
- Automatic Pydantic -> JSON Schema conversion
|
||||
- Centralized endpoint registry
|
||||
- Unique operationId enforcement
|
||||
- Complete request/response schema generation
|
||||
|
||||
Usage:
|
||||
from spec_generator import registry, generate_openapi_spec, register_tool
|
||||
|
||||
# Register endpoints (typically done at module load)
|
||||
register_tool(
|
||||
path="/devices/search",
|
||||
method="POST",
|
||||
operation_id="search_devices",
|
||||
description="Search for devices",
|
||||
request_model=DeviceSearchRequest,
|
||||
response_model=DeviceSearchResponse
|
||||
)
|
||||
|
||||
# Generate spec (called by MCP endpoint)
|
||||
spec = generate_openapi_spec()
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from .registry import (
|
||||
clear_registry,
|
||||
_registry,
|
||||
_registry_lock,
|
||||
_disabled_tools
|
||||
)
|
||||
from .introspection import introspect_flask_app, introspect_graphql_schema
|
||||
from .schema_converter import (
|
||||
build_parameters,
|
||||
build_request_body,
|
||||
build_responses
|
||||
)
|
||||
|
||||
_rebuild_lock = threading.Lock()
|
||||
|
||||
|
||||
def generate_openapi_spec(
|
||||
title: str = "NetAlertX API",
|
||||
version: str = "2.0.0",
|
||||
description: str = "NetAlertX Network Monitoring API - MCP Compatible",
|
||||
servers: Optional[List[Dict[str, str]]] = None,
|
||||
flask_app: Optional[Any] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Assemble a complete OpenAPI specification from the registered endpoints."""
|
||||
|
||||
with _rebuild_lock:
|
||||
# If no app provided and registry is empty, try to use the one from api_server_start
|
||||
if not flask_app and not _registry:
|
||||
try:
|
||||
from ..api_server_start import app as start_app
|
||||
flask_app = start_app
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
# If we are in "dynamic mode", we rebuild the registry from code
|
||||
if flask_app:
|
||||
from ..graphql_endpoint import devicesSchema
|
||||
clear_registry()
|
||||
introspect_graphql_schema(devicesSchema)
|
||||
introspect_flask_app(flask_app)
|
||||
|
||||
spec = {
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": title,
|
||||
"version": version,
|
||||
"description": description,
|
||||
"contact": {
|
||||
"name": "NetAlertX",
|
||||
"url": "https://github.com/jokob-sk/NetAlertX"
|
||||
}
|
||||
},
|
||||
"servers": servers or [{"url": "/", "description": "Local server"}],
|
||||
"security": [
|
||||
{"BearerAuth": []}
|
||||
],
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"BearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"description": "API token from NetAlertX settings (API_TOKEN)"
|
||||
}
|
||||
},
|
||||
"schemas": {}
|
||||
},
|
||||
"paths": {},
|
||||
"tags": []
|
||||
}
|
||||
|
||||
definitions = {}
|
||||
|
||||
# Collect unique tags
|
||||
tag_set = set()
|
||||
|
||||
with _registry_lock:
|
||||
disabled_snapshot = _disabled_tools.copy()
|
||||
for entry in _registry:
|
||||
path = entry["path"]
|
||||
method = entry["method"].lower()
|
||||
|
||||
# Initialize path if not exists
|
||||
if path not in spec["paths"]:
|
||||
spec["paths"][path] = {}
|
||||
|
||||
# Build operation object
|
||||
operation = {
|
||||
"operationId": entry["operation_id"],
|
||||
"summary": entry["summary"],
|
||||
"description": entry["description"],
|
||||
"tags": entry["tags"],
|
||||
"deprecated": entry["deprecated"]
|
||||
}
|
||||
|
||||
# Inject disabled status if applicable
|
||||
if entry["operation_id"] in disabled_snapshot:
|
||||
operation["x-mcp-disabled"] = True
|
||||
|
||||
# Inject original ID if suffixed (Coderabbit fix)
|
||||
if entry.get("original_operation_id"):
|
||||
operation["x-original-operationId"] = entry["original_operation_id"]
|
||||
|
||||
# Add parameters (path + query)
|
||||
parameters = build_parameters(entry)
|
||||
if parameters:
|
||||
operation["parameters"] = parameters
|
||||
|
||||
# Add request body for POST/PUT/PATCH/DELETE
|
||||
if method in ("post", "put", "patch", "delete") and entry.get("request_model"):
|
||||
request_body = build_request_body(
|
||||
entry["request_model"],
|
||||
definitions,
|
||||
allow_multipart_payload=entry.get("allow_multipart_payload", False)
|
||||
)
|
||||
if request_body:
|
||||
operation["requestBody"] = request_body
|
||||
|
||||
# Add responses
|
||||
operation["responses"] = build_responses(
|
||||
entry.get("response_model"), definitions
|
||||
)
|
||||
|
||||
spec["paths"][path][method] = operation
|
||||
|
||||
# Collect tags
|
||||
for tag in entry["tags"]:
|
||||
tag_set.add(tag)
|
||||
|
||||
spec["components"]["schemas"] = definitions
|
||||
|
||||
# Build tags array with descriptions
|
||||
tag_descriptions = {
|
||||
"devices": "Device management and queries",
|
||||
"nettools": "Network diagnostic tools",
|
||||
"events": "Event and alert management",
|
||||
"sessions": "Session history tracking",
|
||||
"messaging": "In-app notifications",
|
||||
"settings": "Configuration management",
|
||||
"sync": "Data synchronization",
|
||||
"logs": "Log file access",
|
||||
"dbquery": "Direct database queries"
|
||||
}
|
||||
|
||||
spec["tags"] = [
|
||||
{"name": tag, "description": tag_descriptions.get(tag, f"{tag.title()} operations")}
|
||||
for tag in sorted(tag_set)
|
||||
]
|
||||
|
||||
return spec
|
||||
|
||||
|
||||
# Initialize registry on module load
|
||||
# Registry is now populated dynamically via introspection in generate_openapi_spec
|
||||
def _register_all_endpoints():
|
||||
"""Dummy function for compatibility with legacy tests."""
|
||||
pass
|
||||
Reference in New Issue
Block a user