Skip to content

Conversation

@adamoutler
Copy link
Collaborator

@adamoutler adamoutler commented Jan 31, 2026

This update focuses on making the NetAlertX API more predictable, efficient, and navigable for AI agents and developers alike. We have standardized how the API communicates errors, improved the logical grouping of tools, and added context that allows AI agents to discover workflows autonomously.

Key Improvements

🚀 Enhanced AI Workflows and Tool Selection

  • 🔗 Workflow Links: Integrated OpenAPI Links into key endpoints. This allows AI agents to understand relationships between tools—for example, automatically knowing how to use a MAC address found in a search result to trigger a Wake-on-LAN or Port Scan without manual guidance.
  • 🧠 Intelligent Deduplication: The list of tools exposed to agents has been cleaned of redundant entries. When multiple routes point to the same functionality, the system now presents a single, high-quality tool.
  • 🏷️ Semantic Naming: Replaced generic numeric suffixes (like _1, _2) with descriptive names (like _mcp or _post). This ensures that both the documentation and the agent's toolbelt are logical and easy to read.

📖 Refined Documentation and Schemas

  • 🆔 Standardized MAC Address Inputs: Parameters have been normalized to use mac across all tools. This consistency removes the friction of agents having to choose between devMac, mac, or other variations.
  • ✍️ Unambiguous Parameter Descriptions: Refined descriptions for complex inputs to prevent guesswork. This includes clear definitions of device statuses (e.g., "my" vs "new") and explicit constraints on network tools (e.g., specifying single IP vs CIDR support).
  • 🗂️ Self-Contained Schemas: Fixed an issue where dropdown menus (Enums) were missing from the UI. The API now provides fully expanded definitions for all parameters.

⚠️ Improved Error Handling and Examples

  • 💬 Contextual Error Examples: Every error response in the specification now includes realistic example messages. This helps developers and AI agents understand exactly why a request failed and how to fix it based on the returned JSON.
  • 🛠️ Standardized Validation Codes: Consolidated validation error behavior. The API now consistently returns HTTP 422 Unprocessable Entity for semantic errors across all request types.

⚡ Optimization and Efficiency

  • 📉 Reduced Spec Size: By eliminating redundant route entries and consolidating tool definitions, the generated openapi.json is now significantly smaller. This reduces token overhead for AI agents and improves documentation performance.
  • 🛡️ Resilient Messaging: Hardened the internal notification system to ensure that minor file or data issues do not cause cascading failures during API authorization checks.

Summary by CodeRabbit

  • Bug Fixes

    • Global 500 error handler with standardized JSON responses.
    • Validation responses aligned to 422 with clearer messages.
  • New Features

    • Endpoints accept payloads and support GET/POST where applicable.
    • Multi-format responses (JSON/CSV) and OpenAPI links for related operations.
    • Spec generation applies environment-driven tool disablement at startup.
  • Documentation

    • Expanded OpenAPI schemas, examples and unique operation naming.
  • Tests

    • New/updated tests for spec generation, disabled-tools behavior and schema changes.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 31, 2026

📝 Walkthrough

Walkthrough

Adds a global 500 error handler, regenerates and applies MCP_DISABLED_TOOLS at startup via get_openapi_spec, deduplicates MCP tools by original operationId with prioritization, enriches OpenAPI metadata (response content types, links, query/path params), enhances schemas/validation, and updates tests accordingly.

Changes

Cohort / File(s) Summary
API Server Startup
server/api_server/api_server_start.py
Added global 500 handler; calls get_openapi_spec(force_refresh=True, flask_app=app) at startup to regenerate spec and apply MCP_DISABLED_TOOLS with logging.
MCP Mapping & Route Resolution
server/api_server/mcp_endpoint.py
Deduplicate tools by x-original-operationId, prefer /mcp/ routes and POST when choosing candidates; annotate and strip internal flags; update find_route_for_tool to match original IDs and prioritization.
Introspection / Registry / Validation
server/api_server/openapi/introspection.py, server/api_server/openapi/registry.py, server/api_server/openapi/validation.py
Unwrap decorated metadata (_get_openapi_metadata); auto-generate GET query params from pydantic models; enforce exclude_from_spec; add/propagate response_content_types and links through register_tool and validate_request.
Schema Conversion & Spec Generation
server/api_server/openapi/schema_converter.py, server/api_server/openapi/spec_generator.py
Added resolve_schema_refs; build_responses now supports multiple content types, links, and per-code error schemas; spec generator reads MCP_DISABLED_TOOLS from settings/env (with default) and applies set_tool_disabled defensively; spec metadata expanded (terms, license, externalDocs).
OpenAPI Schemas
server/api_server/openapi/schemas.py
Introduced literals and richer examples; added ErrorResponse, GraphQLRequest, tightened/aliased fields (e.g., WakeOnLanRequest mac/devLastIP), added model-level validators and expanded response examples.
Tests
test/api_endpoints/test_mcp_openapi_spec.py, test/api_endpoints/test_mcp_disabled_tools.py, test/authoritative_fields/test_device_field_lock.py
Updated tests for WakeOnLan field rename (devMacmac), adjusted error-status/format expectations (422 + error schema), adapted MCP tool-count/assertions for deduplication, and added tests for MCP_DISABLED_TOOLS env/default fallback.

Sequence Diagram(s)

sequenceDiagram
  participant FlaskApp as Flask App
  participant SpecGen as OpenAPI Spec Generator
  participant Env as Env/Settings
  participant Registry as Tool Registry
  participant MCP as MCP Mapper

  rect rgba(135,206,250,0.5)
  FlaskApp->>SpecGen: get_openapi_spec(force_refresh=True, flask_app)
  SpecGen->>Env: read MCP_DISABLED_TOOLS (settings or env or default)
  SpecGen->>Registry: build OpenAPI paths & operations
  SpecGen->>MCP: map_openapi_to_mcp_tools(spec)
  MCP->>MCP: deduplicate by x-original-operationId (prefer /mcp/ and POST)
  MCP->>SpecGen: return mapped tools (with x-mcp-disabled flags)
  SpecGen->>FlaskApp: return final OpenAPI spec
  end
Loading

Possibly related PRs

Poem

🐰 I hopped the startup log and nudged the spec to play,

Grouped tools by origin so duplicates ran away.
I stitched in links and schemas, caught errors with a wink,
Whispered env-based disables—neat, not on the blink.
I munched a carrot, tidy docs—now onward to deploy!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main objective of the changeset—standardizing and optimizing OpenAPI and MCP specifications for AI agents.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@server/api_server/api_server_start.py`:
- Around line 104-113: The global exception handler handle_500_error is catching
HTTPException instances (e.g., abort(400/403/405)) and turning them into 500s;
modify handle_500_error to early-return the original exception when
isinstance(e, HTTPException) so Flask preserves its status/code, and add the
required import from werkzeug.exceptions import HTTPException; keep the existing
logging and 500 JSON response for non-HTTP exceptions.

In `@server/api_server/openapi/schemas.py`:
- Around line 679-693: Add the missing Pydantic model declaration by wrapping
the orphaned docstring, model_config and fields in a class named SessionInfo
that inherits from BaseModel; specifically create class SessionInfo(BaseModel):
above the existing docstring so the docstring "Session information.", the
model_config ConfigDict (with extra="allow" and json_schema_extra examples) and
any ses*-prefixed field definitions become members of SessionInfo, ensuring
imports for BaseModel/ConfigDict are present if not already.

In `@server/api_server/openapi/spec_generator.py`:
- Around line 77-106: The code never reads the MCP_DISABLED_TOOLS environment
variable because disabled_env is initialized to "" and later checked with "is
None"; change the control flow in the disabled-tools block so the env var is
consulted when the helper setting is absent or empty: e.g., initialize
disabled_env as None or treat empty helper setting as missing, call
os.getenv("MCP_DISABLED_TOOLS") when setting_val is falsy (or disabled_env is
None/empty) before falling back to the hard-coded default; keep calling
set_tool_disabled(op, True) for each parsed op and preserve the outer try/except
behavior. Reference symbols: disabled_env, get_setting_value,
MCP_DISABLED_TOOLS, os.getenv, and set_tool_disabled.
🧹 Nitpick comments (3)
server/api_server/mcp_endpoint.py (1)

413-424: Remove unused _tool_name_to_operation_id variable.

The _tool_name_to_operation_id dictionary is populated on line 418 but never used or returned. This appears to be leftover from development or an incomplete feature.

🧹 Proposed cleanup
     final_tools = []
-    _tool_name_to_operation_id: Dict[str, str] = {}
     for tool in tools_map.values():
-        actual_operation_id = tool["name"]  # Save before overwriting
         tool["name"] = tool["_original_op_id"]
-        _tool_name_to_operation_id[tool["name"]] = actual_operation_id
         del tool["_original_op_id"]
         del tool["_is_mcp"]
         del tool["_is_post"]
         final_tools.append(tool)
server/api_server/openapi/schema_converter.py (1)

200-206: Remove or use the unused method parameter.

The method parameter is declared but never used in the function body, as flagged by static analysis (ARG001). Either remove it or add a comment indicating it's reserved for future use.

🧹 Option 1: Remove unused parameter
 def build_responses(
     response_model: Optional[Type[BaseModel]],
     definitions: Dict[str, Any],
     response_content_types: Optional[List[str]] = None,
-    links: Optional[Dict[str, Any]] = None,
-    method: str = "post"
+    links: Optional[Dict[str, Any]] = None
 ) -> Dict[str, Any]:
🧹 Option 2: Document as reserved
 def build_responses(
     response_model: Optional[Type[BaseModel]],
     definitions: Dict[str, Any],
     response_content_types: Optional[List[str]] = None,
     links: Optional[Dict[str, Any]] = None,
-    method: str = "post"
+    method: str = "post"  # Reserved for future method-specific response handling
 ) -> Dict[str, Any]:
+    _ = method  # Silence unused parameter warning
server/api_server/openapi/schemas.py (1)

472-509: Consider extracting the validation error message to a constant.

The model validator at line 508 uses a detailed inline message. While the static analysis hint (TRY003) is pedantic, extracting this to a constant would improve maintainability if the message needs to change.

💡 Optional: Extract error message
+_WOL_MAC_OR_IP_REQUIRED = "Either devMac (aka mac) or devLastIP (aka ip) must be provided"
+
 class WakeOnLanRequest(BaseModel):
     # ... fields ...
 
     `@model_validator`(mode="after")
     def require_mac_or_ip(self) -> "WakeOnLanRequest":
         """Ensure at least one of mac or devLastIP is provided."""
         if self.mac is None and self.devLastIP is None:
-            raise ValueError("Either devMac (aka mac) or devLastIP (aka ip) must be provided")
+            raise ValueError(_WOL_MAC_OR_IP_REQUIRED)
         return self

@adamoutler adamoutler marked this pull request as draft January 31, 2026 01:36
@adamoutler adamoutler marked this pull request as ready for review January 31, 2026 02:54
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@server/api_server/openapi/schemas.py`:
- Around line 60-63: ALLOWED_EVENT_TYPES currently includes "Device Up", which
is not a valid NetAlertX event; update the Literal definition named
ALLOWED_EVENT_TYPES to remove "Device Up" and ensure it exactly matches the
canonical NetAlertX event list ("Device Down", "New Device", "Connected",
"Disconnected", "IP Changed", "Down Reconnected" plus the actual missing event
name); locate ALLOWED_EVENT_TYPES in this module and either remove the "Device
Up" entry or replace the list with the authoritative set from NetAlertX (fill in
the correct missing event string from the source of truth).
🧹 Nitpick comments (1)
test/api_endpoints/test_mcp_disabled_tools.py (1)

33-40: Consider renaming unused loop variables to indicate intent.

The loop variables path and method are not used within the loop body. Rename them to _path and _method to signal they are intentionally unused.

♻️ Proposed fix
             found = False
-            for path, methods in spec["paths"].items():
-                for method, op in methods.items():
+            for _path, methods in spec["paths"].items():
+                for _method, op in methods.items():
                     if op["operationId"] == "search_devices_api":
                         assert op.get("x-mcp-disabled") is True
                         found = True

And similarly for the second test:

             found_read = False
-            for path, methods in spec["paths"].items():
-                for method, op in methods.items():
+            for _path, methods in spec["paths"].items():
+                for _method, op in methods.items():
                     if op["operationId"] == "dbquery_read":
                         assert op.get("x-mcp-disabled") is True
                         found_read = True

Also applies to: 55-62

description="Present in last scan (0 or 1)",
json_schema_extra={"enum": [0, 1]}
)
devStatus: Optional[Literal["online", "offline"]] = Field(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a read-only field (= dynamically evaluated - there is no corresponding DB coilumn) - not sure if this causes issues

@jokob-sk jokob-sk merged commit 0c08659 into netalertx:main Jan 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants