From 0ac0dccba19903d938bd9f267fba8c1b7ac1bc23 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Sun, 1 Feb 2026 16:55:45 +0000 Subject: [PATCH 01/13] enhance agent workflow for testing --- .gemini/skills/testing-workflow/SKILL.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.gemini/skills/testing-workflow/SKILL.md b/.gemini/skills/testing-workflow/SKILL.md index a81c8bb4..a496bfd7 100644 --- a/.gemini/skills/testing-workflow/SKILL.md +++ b/.gemini/skills/testing-workflow/SKILL.md @@ -1,6 +1,6 @@ --- name: testing-workflow -description: Read before running tests. Detailed instructions for single, astandard unit tests (fast), full suites (slow), and handling authentication. Tests must be run when a job is complete. +description: Read before running tests. Detailed instructions for single, standard unit tests (fast), full suites (slow), handling authentication, and obtaining the API Token. Tests must be run when a job is complete. --- # Testing Workflow @@ -8,6 +8,19 @@ After code is developed, tests must be run to ensure the integrity of the final **Crucial:** Tests MUST be run inside the container to access the correct runtime environment (DB, Config, Dependencies). +## 0. Pre-requisites: Environment Check + +Before running any tests, verify you are inside the development container: + +```bash +ls -d /workspaces/NetAlertX +``` + +**IF** this directory does not exist, you are likely on the host machine. You **MUST** immediately activate the `devcontainer-management` skill to enter the container or run commands inside it. + +```text +activate_skill("devcontainer-management") +``` ## 1. Full Test Suite (MANDATORY DEFAULT) From b2501d98a5475b8077326bd3fb92cbc0b74e0e04 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Mon, 2 Feb 2026 23:08:39 +0100 Subject: [PATCH 02/13] fix(mcp): Handle non-JSON responses and correct JSON errors --- server/api_server/mcp_endpoint.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/api_server/mcp_endpoint.py b/server/api_server/mcp_endpoint.py index 1aa3ba49..db65ebe3 100644 --- a/server/api_server/mcp_endpoint.py +++ b/server/api_server/mcp_endpoint.py @@ -749,7 +749,8 @@ def _execute_tool(route: Dict[str, Any], args: Dict[str, Any]) -> Dict[str, Any] "type": "text", "text": json.dumps(json_content, indent=2) }) - except json.JSONDecodeError: + except (json.JSONDecodeError, ValueError): + # Fallback for endpoints that return plain text instead of JSON (e.g., /metrics) content.append({ "type": "text", "text": api_response.text From dc6b57a58178ef97a3216aa3d78776ecf78af91e Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Mon, 2 Feb 2026 23:08:45 +0100 Subject: [PATCH 03/13] feat(devcontainer): Auto-configure MCP settings for VSCode and Gemini --- .devcontainer/scripts/generate-configs.sh | 13 +++++++++++++ .gitignore | 2 ++ 2 files changed, 15 insertions(+) diff --git a/.devcontainer/scripts/generate-configs.sh b/.devcontainer/scripts/generate-configs.sh index 745f9633..987137ed 100755 --- a/.devcontainer/scripts/generate-configs.sh +++ b/.devcontainer/scripts/generate-configs.sh @@ -31,4 +31,17 @@ cat "${DEVCONTAINER_DIR}/resources/devcontainer-Dockerfile" echo "Generated $OUT_FILE using root dir $ROOT_DIR" +# Passive Gemini MCP config +TOKEN=$(grep '^API_TOKEN=' /data/config/app.conf 2>/dev/null | cut -d"'" -f2) +if [ -n "${TOKEN}" ]; then + mkdir -p "${ROOT_DIR}/.gemini" + [ -f "${ROOT_DIR}/.gemini/settings.json" ] || echo "{}" > "${ROOT_DIR}/.gemini/settings.json" + jq --arg t "$TOKEN" '.mcpServers["netalertx-devcontainer"] = {url: "http://127.0.0.1:20212/mcp/sse", headers: {Authorization: ("Bearer " + $t)}}' "${ROOT_DIR}/.gemini/settings.json" > "${ROOT_DIR}/.gemini/settings.json.tmp" && mv "${ROOT_DIR}/.gemini/settings.json.tmp" "${ROOT_DIR}/.gemini/settings.json" + + # VS Code MCP config + mkdir -p "${ROOT_DIR}/.vscode" + [ -f "${ROOT_DIR}/.vscode/mcp.json" ] || echo "{}" > "${ROOT_DIR}/.vscode/mcp.json" + jq --arg t "$TOKEN" '.servers["netalertx-devcontainer"] = {type: "sse", url: "http://127.0.0.1:20212/mcp/sse", headers: {Authorization: ("Bearer " + $t)}}' "${ROOT_DIR}/.vscode/mcp.json" > "${ROOT_DIR}/.vscode/mcp.json.tmp" && mv "${ROOT_DIR}/.vscode/mcp.json.tmp" "${ROOT_DIR}/.vscode/mcp.json" +fi + echo "Done." \ No newline at end of file diff --git a/.gitignore b/.gitignore index 760bb78f..dea40523 100755 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ docker-compose.yml.ffsb42 .env.omada.ffsb42 .venv test_mounts/ +.gemini/settings.json +.vscode/mcp.json From 5095edd5d8b9a592b766a9026c1bcf33576bf847 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Mon, 2 Feb 2026 23:14:41 +0100 Subject: [PATCH 04/13] 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") From f43517b9a51f36e16d1df290fb42d9e1178c1be2 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Mon, 2 Feb 2026 23:14:45 +0100 Subject: [PATCH 05/13] chore(api): Cleanup schemas and update skills docs --- .gemini/skills/testing-workflow/SKILL.md | 21 ++++++++++++++++----- server/api_server/openapi/schemas.py | 21 +++++++++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/.gemini/skills/testing-workflow/SKILL.md b/.gemini/skills/testing-workflow/SKILL.md index a496bfd7..6a49b6fa 100644 --- a/.gemini/skills/testing-workflow/SKILL.md +++ b/.gemini/skills/testing-workflow/SKILL.md @@ -51,13 +51,24 @@ cd /workspaces/NetAlertX; pytest test/ cd /workspaces/NetAlertX; pytest test/api_endpoints/test_mcp_extended_endpoints.py ``` -## Authentication in Tests +## Authentication & Environment Reset -The test environment uses `API_TOKEN`. The most reliable way to retrieve the current token from a running container is: +Authentication tokens are required to perform certain operations such as manual testing or crafting expressions to work with the web APIs.After making code changes, you MUST reset the environment to ensure the new code is running and verify you have the latest `API_TOKEN`. -```bash -python3 -c "from helper import get_setting_value; print(get_setting_value('API_TOKEN'))" -``` +1. **Reset Environment:** Run the setup script inside the container. + ```bash + bash /workspaces/NetAlertX/.devcontainer/scripts/setup.sh + ``` +2. **Wait for Stabilization:** Wait at least 5 seconds for services (nginx, python server, etc.) to start. + ```bash + sleep 5 + ``` +3. **Obtain Token:** Retrieve the current token from the container. + ```bash + python3 -c "from helper import get_setting_value; print(get_setting_value('API_TOKEN'))" + ``` + +The retrieved token MUST be used in all subsequent API or test calls requiring authentication. ### Troubleshooting diff --git a/server/api_server/openapi/schemas.py b/server/api_server/openapi/schemas.py index 96f862de..5181a4b7 100644 --- a/server/api_server/openapi/schemas.py +++ b/server/api_server/openapi/schemas.py @@ -39,8 +39,7 @@ ALLOWED_DEVICE_COLUMNS = Literal[ ] ALLOWED_NMAP_MODES = Literal[ - "quick", "intense", "ping", "comprehensive", "fast", "normal", "detail", "skipdiscovery", - "-sS", "-sT", "-sU", "-sV", "-O" + "fast", "normal", "detail", "skipdiscovery" ] NOTIFICATION_LEVELS = Literal["info", "warning", "error", "alert", "interrupt"] @@ -301,6 +300,24 @@ class DeviceTotalsResponse(RootModel): root: List[int] = Field(default_factory=list, description="List of counts: [all, online, favorites, new, offline, archived]") +class DeviceTotalsNamedResponse(BaseResponse): + """Response with named device statistics.""" + totals: Dict[str, int] = Field( + ..., + description="Dictionary of counts", + json_schema_extra={ + "examples": [{ + "devices": 10, + "connected": 5, + "favorites": 2, + "new": 1, + "down": 0, + "archived": 2 + }] + } + ) + + class DeviceExportRequest(BaseModel): """Request for exporting devices.""" format: Literal["csv", "json"] = Field( From 9ca5375652a19b067ab557bebb86302a06f29e07 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Mon, 2 Feb 2026 23:19:14 +0100 Subject: [PATCH 06/13] fix(schema): Enhance MAC validation for sessions and events --- server/api_server/api_server_start.py | 2 +- server/api_server/openapi/schemas.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/api_server/api_server_start.py b/server/api_server/api_server_start.py index 29bdcd92..17c68a1b 100755 --- a/server/api_server/api_server_start.py +++ b/server/api_server/api_server_start.py @@ -1385,7 +1385,7 @@ def api_add_to_execution_queue(payload=None): path_params=[{ "name": "mac", "description": "Device MAC address", - "schema": {"type": "string"} + "schema": {"type": "string", "pattern": "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"} }], request_model=CreateEventRequest, response_model=BaseResponse, diff --git a/server/api_server/openapi/schemas.py b/server/api_server/openapi/schemas.py index 5181a4b7..b711e8ea 100644 --- a/server/api_server/openapi/schemas.py +++ b/server/api_server/openapi/schemas.py @@ -719,7 +719,7 @@ class SessionInfo(BaseModel): class CreateSessionRequest(BaseModel): """Request to create a session.""" - mac: str = Field(..., description="Device MAC") + mac: str = Field(..., description="Device MAC", pattern=MAC_PATTERN) ip: str = Field(..., description="Device IP") start_time: str = Field(..., description="Start time") end_time: Optional[str] = Field(None, description="End time") From a70354997d8f25546200c56044865538df4dbfff Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Mon, 2 Feb 2026 23:22:26 +0100 Subject: [PATCH 07/13] test(mcp): Update endpoints to remove obsolete /mcp/sse prefixes --- test/api_endpoints/test_mcp_tools_endpoints.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/api_endpoints/test_mcp_tools_endpoints.py b/test/api_endpoints/test_mcp_tools_endpoints.py index 55362bbf..c18c0195 100644 --- a/test/api_endpoints/test_mcp_tools_endpoints.py +++ b/test/api_endpoints/test_mcp_tools_endpoints.py @@ -54,7 +54,7 @@ def test_trigger_scan_ARPSCAN(mock_queue_class, client, api_token): mock_queue_class.return_value = mock_queue payload = {"type": "ARPSCAN"} - response = client.post("/mcp/sse/nettools/trigger-scan", json=payload, headers=auth_headers(api_token)) + response = client.post("/nettools/trigger-scan", json=payload, headers=auth_headers(api_token)) assert response.status_code == 200 data = response.get_json() @@ -71,7 +71,7 @@ def test_trigger_scan_invalid_type(mock_queue_class, client, api_token): mock_queue_class.return_value = mock_queue payload = {"type": "invalid_type", "target": "192.168.1.0/24"} - response = client.post("/mcp/sse/nettools/trigger-scan", json=payload, headers=auth_headers(api_token)) + response = client.post("/nettools/trigger-scan", json=payload, headers=auth_headers(api_token)) assert response.status_code == 400 data = response.get_json() @@ -267,7 +267,7 @@ def test_get_latest_device(mock_db_conn, client, api_token): def test_openapi_spec(client, api_token): """Test openapi_spec endpoint contains MCP tool paths.""" - response = client.get("/mcp/sse/openapi.json", headers=auth_headers(api_token)) + response = client.get("/openapi.json", headers=auth_headers(api_token)) assert response.status_code == 200 spec = response.get_json() @@ -297,7 +297,7 @@ def test_mcp_devices_export_csv(mock_db_conn, client, api_token): mock_conn.execute.return_value = mock_execute_result mock_db_conn.return_value = mock_conn - response = client.get("/mcp/sse/devices/export", headers=auth_headers(api_token)) + response = client.get("/devices/export", headers=auth_headers(api_token)) assert response.status_code == 200 # CSV response should have content-type header @@ -314,7 +314,7 @@ def test_mcp_devices_export_json(mock_export, client, api_token): "columns": ["devMac", "devName", "devLastIP"], } - response = client.get("/mcp/sse/devices/export?format=json", headers=auth_headers(api_token)) + response = client.get("/devices/export?format=json", headers=auth_headers(api_token)) assert response.status_code == 200 data = response.get_json() @@ -339,7 +339,7 @@ def test_mcp_devices_import_json(mock_db_conn, client, api_token): mock_import.return_value = {"success": True, "message": "Imported 2 devices"} payload = {"content": "bW9ja2VkIGNvbnRlbnQ="} # base64 encoded content - response = client.post("/mcp/sse/devices/import", json=payload, headers=auth_headers(api_token)) + response = client.post("/devices/import", json=payload, headers=auth_headers(api_token)) assert response.status_code == 200 data = response.get_json() @@ -362,7 +362,7 @@ def test_mcp_devices_totals(mock_db_conn, client, api_token): mock_conn.cursor.return_value = mock_sql mock_db_conn.return_value = mock_conn - response = client.get("/mcp/sse/devices/totals", headers=auth_headers(api_token)) + response = client.get("/devices/totals", headers=auth_headers(api_token)) assert response.status_code == 200 data = response.get_json() @@ -380,7 +380,7 @@ def test_mcp_traceroute(mock_traceroute, client, api_token): mock_traceroute.return_value = ({"success": True, "output": "traceroute output"}, 200) payload = {"devLastIP": "8.8.8.8"} - response = client.post("/mcp/sse/nettools/traceroute", json=payload, headers=auth_headers(api_token)) + response = client.post("/nettools/traceroute", json=payload, headers=auth_headers(api_token)) assert response.status_code == 200 data = response.get_json() @@ -395,7 +395,7 @@ def test_mcp_traceroute_missing_ip(mock_traceroute, client, api_token): mock_traceroute.return_value = ({"success": False, "error": "Invalid IP: None"}, 400) payload = {} # Missing devLastIP - response = client.post("/mcp/sse/nettools/traceroute", json=payload, headers=auth_headers(api_token)) + response = client.post("/nettools/traceroute", json=payload, headers=auth_headers(api_token)) assert response.status_code == 422 data = response.get_json() From 36d5f5b434ffc6dedf454d870ffe0f9974a9a555 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Mon, 2 Feb 2026 23:08:41 +0000 Subject: [PATCH 08/13] Fix for space after period. --- .gemini/skills/testing-workflow/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gemini/skills/testing-workflow/SKILL.md b/.gemini/skills/testing-workflow/SKILL.md index 6a49b6fa..debf7983 100644 --- a/.gemini/skills/testing-workflow/SKILL.md +++ b/.gemini/skills/testing-workflow/SKILL.md @@ -53,7 +53,7 @@ cd /workspaces/NetAlertX; pytest test/api_endpoints/test_mcp_extended_endpoints. ## Authentication & Environment Reset -Authentication tokens are required to perform certain operations such as manual testing or crafting expressions to work with the web APIs.After making code changes, you MUST reset the environment to ensure the new code is running and verify you have the latest `API_TOKEN`. +Authentication tokens are required to perform certain operations such as manual testing or crafting expressions to work with the web APIs. After making code changes, you MUST reset the environment to ensure the new code is running and verify you have the latest `API_TOKEN`. 1. **Reset Environment:** Run the setup script inside the container. ```bash From 7b22c0a5dd0c473c2db51462c8ef0f6ead13ecaf Mon Sep 17 00:00:00 2001 From: Massimo Pissarello Date: Tue, 3 Feb 2026 05:50:26 +0100 Subject: [PATCH 09/13] Translated using Weblate (Italian) Currently translated at 100.0% (790 of 790 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/ --- front/php/templates/language/it_it.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/front/php/templates/language/it_it.json b/front/php/templates/language/it_it.json index c3edccb7..f4cc17bb 100644 --- a/front/php/templates/language/it_it.json +++ b/front/php/templates/language/it_it.json @@ -414,8 +414,8 @@ "Maintenance_Tool_ImportPastedConfig": "Importa impostazioni (incolla)", "Maintenance_Tool_ImportPastedConfig_noti_text": "Vuoi davvero importare le impostazioni di configurazione incollate? Questo sovrascriverà completamente il file app.conf.", "Maintenance_Tool_ImportPastedConfig_text": "Importa il file app.conf contenente tutte le impostazioni dell'applicazione. Potresti voler scaricare prima il file app.conf corrente con Esporta impostazioni.", - "Maintenance_Tool_UnlockFields": "Cancella tutte le sorgenti del dispositivo", - "Maintenance_Tool_UnlockFields_noti": "Cancella tutte le sorgenti del dispositivo", + "Maintenance_Tool_UnlockFields": "Sblocca campi del dispositivo", + "Maintenance_Tool_UnlockFields_noti": "Sblocca campi del dispositivo", "Maintenance_Tool_UnlockFields_noti_text": "Vuoi davvero cancellare tutti i valori sorgente (BLOCCATO/UTENTE) per tutti i campi dispositivo su tutti i dispositivi? Questa azione non può essere annullata.", "Maintenance_Tool_UnlockFields_text": "Questo strumento rimuoverà tutti i valori sorgente da ogni campo tracciato per tutti i dispositivi, sbloccando di fatto tutti i campi per plugin e utenti. Usalo con cautela, poiché influirà sull'intero inventario dei dispositivi.", "Maintenance_Tool_arpscansw": "Attiva/disattiva arp-Scan", @@ -427,9 +427,9 @@ "Maintenance_Tool_backup_noti_text": "Sei sicuro di voler eseguire il backup del DB? Assicurati che nessuna scansione sia attualmente in esecuzione.", "Maintenance_Tool_backup_text": "I backup del database si trovano nella directory del database come archivio zip, denominato con la data di creazione. Non esiste un numero massimo di backup.", "Maintenance_Tool_check_visible": "Deseleziona per nascondere la colonna.", - "Maintenance_Tool_clearSourceFields_selected": "", - "Maintenance_Tool_clearSourceFields_selected_noti": "", - "Maintenance_Tool_clearSourceFields_selected_text": "", + "Maintenance_Tool_clearSourceFields_selected": "Cancella campi sorgente", + "Maintenance_Tool_clearSourceFields_selected_noti": "Cancella sorgenti", + "Maintenance_Tool_clearSourceFields_selected_text": "Questa operazione cancellerà tutti i campi sorgente dei dispositivi selezionati. Questa azione non può essere annullata.", "Maintenance_Tool_darkmode": "Alterna modalità (Scuro/Chiaro)", "Maintenance_Tool_darkmode_noti": "Alterna modalità", "Maintenance_Tool_darkmode_noti_text": "Dopo il cambio di tema, la pagina tenta di ricaricarsi per attivare la modifica. Potrebbe essere necessaria la cancellazione della cache.", @@ -789,4 +789,4 @@ "settings_system_label": "Sistema", "settings_update_item_warning": "Aggiorna il valore qui sotto. Fai attenzione a seguire il formato precedente. La convalida non viene eseguita.", "test_event_tooltip": "Salva le modifiche prima di provare le nuove impostazioni." -} \ No newline at end of file +} From 1a9ae626e5e41f4c779cd170441fde83d46837e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=93=D0=BE=D1=80?= =?UTF-8?q?=D0=BF=D0=B8=D0=BD=D1=96=D1=87?= Date: Mon, 2 Feb 2026 06:16:47 +0100 Subject: [PATCH 10/13] Translated using Weblate (Ukrainian) Currently translated at 98.9% (782 of 790 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/uk/ --- front/php/templates/language/uk_ua.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/php/templates/language/uk_ua.json b/front/php/templates/language/uk_ua.json index 8c5efe4f..fffbddc8 100644 --- a/front/php/templates/language/uk_ua.json +++ b/front/php/templates/language/uk_ua.json @@ -789,4 +789,4 @@ "settings_system_label": "Система", "settings_update_item_warning": "Оновіть значення нижче. Слідкуйте за попереднім форматом. Перевірка не виконана.", "test_event_tooltip": "Перш ніж перевіряти налаштування, збережіть зміни." -} \ No newline at end of file +} From f7fa857cae9456394d20bf6a142bcf8914f6d758 Mon Sep 17 00:00:00 2001 From: Sylvain Pichon Date: Mon, 2 Feb 2026 20:11:17 +0100 Subject: [PATCH 11/13] Translated using Weblate (French) Currently translated at 100.0% (790 of 790 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/fr/ --- front/php/templates/language/fr_fr.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/front/php/templates/language/fr_fr.json b/front/php/templates/language/fr_fr.json index 56ce651b..38c4c009 100644 --- a/front/php/templates/language/fr_fr.json +++ b/front/php/templates/language/fr_fr.json @@ -414,8 +414,8 @@ "Maintenance_Tool_ImportPastedConfig": "Import des paramètres (coller)", "Maintenance_Tool_ImportPastedConfig_noti_text": "Êtes-vous sûr de vouloir importer les paramètres de configuration copiés ? Cela va complètement remplacer le fichier app.conf.", "Maintenance_Tool_ImportPastedConfig_text": "Importe le fichier app.conf, qui contient tous les paramètres de l'application. Vous devriez commencer par télécharger le fichier actuelapp.conf avec la fonctionnalité Export des paramètres.", - "Maintenance_Tool_UnlockFields": "Supprimer toutes les sources des appareils", - "Maintenance_Tool_UnlockFields_noti": "Supprimer toutes les sources des appareils", + "Maintenance_Tool_UnlockFields": "Déverrouiller les champs de l'appareil", + "Maintenance_Tool_UnlockFields_noti": "Déverrouiller les champs de l'appareil", "Maintenance_Tool_UnlockFields_noti_text": "Êtes-vous sûr de vouloir supprimer toutes les valeurs de source (verrouillés par l'utilisateur LOCKED/USER) pour tous les champs d'appareil de tous les appareils ? Cette action ne peut pas être annulée.", "Maintenance_Tool_UnlockFields_text": "Cet outil va supprimer toutes les valeurs de source pour chaque champ suivi de tous les appareils, ce qui déverrouillera tous les champs pour les plugins et les utilisateurs. Utilisez-lebm avec précaution, cela impactera l'ensemble de l'inventaire des appareils.", "Maintenance_Tool_arpscansw": "Basculer l'arp-Scan (activé/désactivé)", @@ -427,9 +427,9 @@ "Maintenance_Tool_backup_noti_text": "Êtes-vous sûr de vouloir lancer la sauvegarde de la base de données ? Assurez-vous de ne pas avoir de scan en cours.", "Maintenance_Tool_backup_text": "Les sauvegardes de base de données sont situées dans le répertoire de la base de données, soir forme d'archive ZIP, nommé selon la date de création. Il n'y a pas de limite de nombre de sauvegarde.", "Maintenance_Tool_check_visible": "Décocher pour masquer la colonne.", - "Maintenance_Tool_clearSourceFields_selected": "", - "Maintenance_Tool_clearSourceFields_selected_noti": "", - "Maintenance_Tool_clearSourceFields_selected_text": "", + "Maintenance_Tool_clearSourceFields_selected": "Supprimer les champs source", + "Maintenance_Tool_clearSourceFields_selected_noti": "Supprimer les sources", + "Maintenance_Tool_clearSourceFields_selected_text": "Cela va supprimer tous les champs de sources des appareils sélectionnés. Cette action est irréversible.", "Maintenance_Tool_darkmode": "Basculer de mode (clair/sombre)", "Maintenance_Tool_darkmode_noti": "Basculer de mode", "Maintenance_Tool_darkmode_noti_text": "Après le changement de thème, la page tente de se rafraîchir pour activer le changement. Si besoin, le cache doit être supprimé.", @@ -789,4 +789,4 @@ "settings_system_label": "Système", "settings_update_item_warning": "Mettre à jour la valeur ci-dessous. Veillez à bien suivre le même format qu'auparavant. Il n'y a pas de pas de contrôle.", "test_event_tooltip": "Enregistrer d'abord vos modifications avant de tester vôtre paramétrage." -} \ No newline at end of file +} From fe7e91c5156b57c3048c916fb6b55988cc67e54b Mon Sep 17 00:00:00 2001 From: batman Date: Mon, 2 Feb 2026 06:16:48 +0100 Subject: [PATCH 12/13] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 99.3% (785 of 790 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/zh_Hans/ --- front/php/templates/language/zh_cn.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/php/templates/language/zh_cn.json b/front/php/templates/language/zh_cn.json index 0d46621b..4d3b2683 100644 --- a/front/php/templates/language/zh_cn.json +++ b/front/php/templates/language/zh_cn.json @@ -789,4 +789,4 @@ "settings_system_label": "系统", "settings_update_item_warning": "更新下面的值。请注意遵循先前的格式。未执行验证。", "test_event_tooltip": "在测试设置之前,请先保存更改。" -} \ No newline at end of file +} From 034ee688fb500cc749a6a42caf394d2245b7b7d3 Mon Sep 17 00:00:00 2001 From: Artyom Rybakov Date: Mon, 2 Feb 2026 06:16:42 +0100 Subject: [PATCH 13/13] Translated using Weblate (Russian) Currently translated at 98.4% (778 of 790 strings) Translation: NetAlertX/core Translate-URL: https://hosted.weblate.org/projects/pialert/core/ru/ --- front/php/templates/language/ru_ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/php/templates/language/ru_ru.json b/front/php/templates/language/ru_ru.json index 493c55ca..e644cfe1 100644 --- a/front/php/templates/language/ru_ru.json +++ b/front/php/templates/language/ru_ru.json @@ -789,4 +789,4 @@ "settings_system_label": "Система", "settings_update_item_warning": "Обновить значение ниже. Будьте осторожны, следуя предыдущему формату. Проверка не выполняется.", "test_event_tooltip": "Сначала сохраните изменения, прежде чем проверять настройки." -} \ No newline at end of file +}