From 5095edd5d8b9a592b766a9026c1bcf33576bf847 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Mon, 2 Feb 2026 23:14:41 +0100 Subject: [PATCH] docs(mcp): Update tool descriptions, links, and standardize path parameters --- docs/API_MCP.md | 31 +++++----- server/api_server/api_server_start.py | 85 ++++++++++++++++++--------- 2 files changed, 72 insertions(+), 44 deletions(-) diff --git a/docs/API_MCP.md b/docs/API_MCP.md index 344f163a..387facef 100644 --- a/docs/API_MCP.md +++ b/docs/API_MCP.md @@ -49,7 +49,7 @@ sequenceDiagram API-->>MCP: 5. Available tools spec MCP-->>AI: 6. Tool definitions AI->>MCP: 7. tools/call: search_devices - MCP->>API: 8. POST /mcp/sse/devices/search + MCP->>API: 8. POST /devices/search API->>DB: 9. Query devices DB-->>API: 10. Device data API-->>MCP: 11. JSON response @@ -72,9 +72,9 @@ graph LR end subgraph "NetAlertX API Server (:20211)" - F[Device APIs
/mcp/sse/devices/*] - G[Network Tools
/mcp/sse/nettools/*] - H[Events API
/mcp/sse/events/*] + F[Device APIs
/devices/*] + G[Network Tools
/nettools/*] + H[Events API
/events/*] end subgraph "Backend" @@ -182,27 +182,28 @@ eventSource.onmessage = function(event) { | Tool | Endpoint | Description | |------|----------|-------------| -| `list_devices` | `/mcp/sse/devices/by-status` | List devices by online status | -| `get_device_info` | `/mcp/sse/device/` | Get detailed device information | -| `search_devices` | `/mcp/sse/devices/search` | Search devices by MAC, name, or IP | -| `get_latest_device` | `/mcp/sse/devices/latest` | Get most recently connected device | -| `set_device_alias` | `/mcp/sse/device//set-alias` | Set device friendly name | +| `list_devices` | `/devices/by-status` | List devices by online status | +| `get_device_info` | `/device/{mac}` | Get detailed device information | +| `search_devices` | `/devices/search` | Search devices by MAC, name, or IP | +| `get_latest_device` | `/devices/latest` | Get most recently connected device | +| `set_device_alias` | `/device/{mac}/set-alias` | Set device friendly name | ### Network Tools | Tool | Endpoint | Description | |------|----------|-------------| -| `trigger_scan` | `/mcp/sse/nettools/trigger-scan` | Trigger network discovery scan | -| `get_open_ports` | `/mcp/sse/device/open_ports` | Get stored NMAP open ports for device | -| `wol_wake_device` | `/mcp/sse/nettools/wakeonlan` | Wake device using Wake-on-LAN | -| `get_network_topology` | `/mcp/sse/devices/network/topology` | Get network topology map | +| `trigger_scan` | `/nettools/trigger-scan` | Trigger network discovery scan to find new devices. | +| `run_nmap_scan` | `/nettools/nmap` | Perform NMAP scan on a target to identify open ports. | +| `get_open_ports` | `/device/open_ports` | Get stored NMAP open ports. Use `run_nmap_scan` first if empty. | +| `wol_wake_device` | `/nettools/wakeonlan` | Wake device using Wake-on-LAN | +| `get_network_topology` | `/devices/network/topology` | Get network topology map | ### Event & Monitoring Tools | Tool | Endpoint | Description | |------|----------|-------------| -| `get_recent_alerts` | `/mcp/sse/events/recent` | Get events from last 24 hours | -| `get_last_events` | `/mcp/sse/events/last` | Get 10 most recent events | +| `get_recent_alerts` | `/events/recent` | Get events from last 24 hours | +| `get_last_events` | `/events/last` | Get 10 most recent events | --- diff --git a/server/api_server/api_server_start.py b/server/api_server/api_server_start.py index 221d57be..29bdcd92 100755 --- a/server/api_server/api_server_start.py +++ b/server/api_server/api_server_start.py @@ -72,6 +72,7 @@ from .openapi.schemas import ( # noqa: E402 [flake8 lint suppression] DeviceUpdateRequest, DeviceInfo, BaseResponse, DeviceTotalsResponse, + DeviceTotalsNamedResponse, DeleteDevicesRequest, DeviceImportRequest, DeviceImportResponse, UpdateDeviceColumnRequest, LockDeviceFieldRequest, UnlockDeviceFieldsRequest, @@ -289,7 +290,6 @@ def api_get_setting(setKey): # -------------------------- # Device Endpoints # -------------------------- -@app.route('/mcp/sse/device/', methods=['GET', 'POST']) @app.route("/device/", methods=["GET"]) @validate_request( operation_id="get_device_info", @@ -432,7 +432,7 @@ def api_device_copy(payload=None): @validate_request( operation_id="update_device_column", summary="Update Device Column", - description="Update a specific database column for a device.", + description="Update a specific database column for a device. Use this to mark devices as favorites (columnName='devFavorite', columnValue=1). See `get_favorite_devices` to retrieve them.", path_params=[{ "name": "mac", "description": "Device MAC address", @@ -554,7 +554,6 @@ def api_device_fields_unlock(payload=None): # Devices Collections # -------------------------- -@app.route('/mcp/sse/device//set-alias', methods=['POST']) @app.route('/device//set-alias', methods=['POST']) @validate_request( operation_id="set_device_alias", @@ -582,16 +581,25 @@ def api_device_set_alias(mac, payload=None): return jsonify(result) -@app.route('/mcp/sse/device/open_ports', methods=['POST']) @app.route('/device/open_ports', methods=['POST']) @validate_request( operation_id="get_open_ports", summary="Get Open Ports", - description="Retrieve open ports for a target IP or MAC address. Returns cached NMAP scan results.", + description="Retrieve open ports for a target IP or MAC address. Returns cached NMAP scan results. If no ports are found, run a scan first using `run_nmap_scan`.", request_model=OpenPortsRequest, response_model=OpenPortsResponse, tags=["nettools"], - auth_callable=is_authorized + auth_callable=is_authorized, + links={ + "RunNmapScan": { + "operationId": "run_nmap_scan", + "parameters": { + "scan": "$response.body#/target", + "mode": "fast" + }, + "description": "Refresh the open ports data by running a new NMAP scan on this target." + } + } ) def api_device_open_ports(payload=None): """Get stored NMAP open ports for a target IP or MAC.""" @@ -606,7 +614,7 @@ def api_device_open_ports(payload=None): open_ports = device_handler.getOpenPorts(target) if not open_ports: - return jsonify({"success": False, "error": f"No stored open ports for {target}. Run a scan with `/nettools/trigger-scan`"}), 404 + return jsonify({"success": False, "error": f"No stored open ports for {target}. Run a scan with the 'run_nmap_scan' tool (or /nettools/nmap)."}), 404 return jsonify({"success": True, "target": target, "open_ports": open_ports}) @@ -674,7 +682,6 @@ def api_delete_unknown_devices(payload=None): return jsonify(device_handler.deleteUnknownDevices()) -@app.route('/mcp/sse/devices/export', methods=['GET']) @app.route("/devices/export", methods=["GET"]) @app.route("/devices/export/", methods=["GET"]) @validate_request( @@ -716,7 +723,6 @@ def api_export_devices(format=None, payload=None): ) -@app.route('/mcp/sse/devices/import', methods=['POST']) @app.route("/devices/import", methods=["POST"]) @validate_request( operation_id="import_devices", @@ -746,12 +752,11 @@ def api_import_csv(payload=None): return jsonify(result) -@app.route('/mcp/sse/devices/totals', methods=['GET']) @app.route("/devices/totals", methods=["GET"]) @validate_request( operation_id="get_device_totals", - summary="Get Device Totals", - description="Get device statistics including total count, online/offline counts, new devices, and archived devices.", + summary="Get Device Totals (Deprecated)", + description="Get device statistics including total count, online/offline counts, new devices, and archived devices. Deprecated: use /devices/totals/named instead.", response_model=DeviceTotalsResponse, tags=["devices"], auth_callable=is_authorized @@ -761,7 +766,30 @@ def api_devices_totals(payload=None): return jsonify(device_handler.getTotals()) -@app.route('/mcp/sse/devices/by-status', methods=['GET', 'POST']) +@app.route("/devices/totals/named", methods=["GET"]) +@validate_request( + operation_id="get_device_totals_named", + summary="Get Named Device Totals", + description="Get device statistics with named fields including total count, online/offline counts, new devices, and archived devices.", + response_model=DeviceTotalsNamedResponse, + tags=["devices"], + auth_callable=is_authorized +) +def api_devices_totals_named(payload=None): + device_handler = DeviceInstance() + totals_list = device_handler.getTotals() + # totals_list order: [devices, connected, favorites, new, down, archived] + totals_dict = { + "devices": totals_list[0] if len(totals_list) > 0 else 0, + "connected": totals_list[1] if len(totals_list) > 1 else 0, + "favorites": totals_list[2] if len(totals_list) > 2 else 0, + "new": totals_list[3] if len(totals_list) > 3 else 0, + "down": totals_list[4] if len(totals_list) > 4 else 0, + "archived": totals_list[5] if len(totals_list) > 5 else 0 + } + return jsonify({"success": True, "totals": totals_dict}) + + @app.route("/devices/by-status", methods=["GET", "POST"]) @validate_request( operation_id="list_devices_by_status_api", @@ -811,12 +839,11 @@ def api_devices_by_status(payload: DeviceListRequest = None): return jsonify(device_handler.getByStatus(status)) -@app.route('/mcp/sse/devices/search', methods=['POST']) @app.route('/devices/search', methods=['POST']) @validate_request( operation_id="search_devices_api", summary="Search Devices", - description="Search for devices based on various criteria like name, IP, MAC, or vendor.", + description="Search for devices based on various criteria like name, IP, MAC, or vendor. Use this to find MAC addresses for other tools.", request_model=DeviceSearchRequest, response_model=DeviceSearchResponse, tags=["devices"], @@ -878,7 +905,6 @@ def api_devices_search(payload=None): return jsonify({"success": True, "devices": matches}) -@app.route('/mcp/sse/devices/latest', methods=['GET']) @app.route('/devices/latest', methods=['GET']) @validate_request( operation_id="get_latest_device", @@ -899,12 +925,11 @@ def api_devices_latest(payload=None): return jsonify([latest]) -@app.route('/mcp/sse/devices/favorite', methods=['GET']) @app.route('/devices/favorite', methods=['GET']) @validate_request( operation_id="get_favorite_devices", summary="Get Favorite Devices", - description="Get list of devices marked as favorites.", + description="Get list of devices marked as favorites. Use `update_device_column` with 'devFavorite' to add devices.", response_model=DeviceListResponse, tags=["devices"], auth_callable=is_authorized @@ -916,11 +941,10 @@ def api_devices_favorite(payload=None): favorite = device_handler.getFavorite() if not favorite: - return jsonify({"success": False, "message": "No devices found", "error": "No devices found"}), 404 + return jsonify({"success": False, "message": "No devices found", "error": "No favorite devices found. Mark devices using `update_device_column`."}), 404 return jsonify([favorite]) -@app.route('/mcp/sse/devices/network/topology', methods=['GET']) @app.route('/devices/network/topology', methods=['GET']) @validate_request( operation_id="get_network_topology", @@ -942,7 +966,6 @@ def api_devices_network_topology(payload=None): # -------------------------- # Net tools # -------------------------- -@app.route('/mcp/sse/nettools/wakeonlan', methods=['POST']) @app.route("/nettools/wakeonlan", methods=["POST"]) @validate_request( operation_id="wake_on_lan", @@ -979,7 +1002,6 @@ def api_wakeonlan(payload=None): return wakeonlan(mac) -@app.route('/mcp/sse/nettools/traceroute', methods=['POST']) @app.route("/nettools/traceroute", methods=["POST"]) @validate_request( operation_id="perform_traceroute", @@ -1036,11 +1058,20 @@ def api_nslookup(payload: NslookupRequest = None): @validate_request( operation_id="run_nmap_scan", summary="NMAP Scan", - description="Perform an NMAP scan on a target IP.", + description="Perform an NMAP scan on a target IP to identify open ports. This data is used by `get_open_ports`.", request_model=NmapScanRequest, response_model=NmapScanResponse, tags=["nettools"], - auth_callable=is_authorized + auth_callable=is_authorized, + links={ + "GetOpenPorts": { + "operationId": "get_open_ports", + "parameters": { + "target": "$response.body#/ip" + }, + "description": "View the open ports discovered by this scan." + } + } ) def api_nmap(payload: NmapScanRequest = None): """ @@ -1084,7 +1115,6 @@ def api_network_interfaces(payload=None): return network_interfaces() -@app.route('/mcp/sse/nettools/trigger-scan', methods=['POST']) @app.route("/nettools/trigger-scan", methods=["GET", "POST"]) @validate_request( operation_id="trigger_network_scan", @@ -1139,7 +1169,6 @@ def api_trigger_scan(payload=None): # MCP Server # -------------------------- @app.route('/openapi.json', methods=['GET']) -@app.route('/mcp/sse/openapi.json', methods=['GET']) def serve_openapi_spec(): # Allow unauthenticated access to the spec itself so Swagger UI can load. # The actual API endpoints remain protected. @@ -1498,7 +1527,6 @@ def api_get_events_totals(payload=None): return jsonify(totals) -@app.route('/mcp/sse/events/recent', methods=['GET', 'POST']) @app.route('/events/recent', methods=['GET', 'POST']) @validate_request( operation_id="get_recent_events", @@ -1518,7 +1546,6 @@ def api_events_default_24h(payload=None): return api_events_recent(hours) -@app.route('/mcp/sse/events/last', methods=['GET', 'POST']) @app.route('/events/last', methods=['GET', 'POST']) @validate_request( operation_id="get_last_events", @@ -1707,7 +1734,7 @@ def api_get_session_events(payload=None): auth_callable=is_authorized ) def metrics(payload=None): - # Return Prometheus metrics as plain text + # Return Prometheus metrics as plain text (not JSON) return Response(get_metric_stats(), mimetype="text/plain")