diff --git a/packages/netx-mcp/src/netx_mcp/http_tools.py b/packages/netx-mcp/src/netx_mcp/http_tools.py index d55028c..2917b5f 100644 --- a/packages/netx-mcp/src/netx_mcp/http_tools.py +++ b/packages/netx-mcp/src/netx_mcp/http_tools.py @@ -252,10 +252,33 @@ def _list_managed_ne(args: dict[str, Any]) -> dict[str, Any]: def _get_managed_ne(args: dict[str, Any]) -> dict[str, Any]: - ne_id = str(args.get("ne_id") or "").strip() + ne_id = str( + args.get("ne_id") or args.get("managed_ne_id") or args.get("id") or "" + ).strip() if not ne_id: - return {"ok": False, "error": "ne_id_required", "error_code": "ne_id_required"} - return http_json("GET", f"/v1/managed-ne/{ne_id}", params=None) + return { + "ok": False, + "error": "ne_id_required", + "error_code": "ne_id_required", + "hint": ( + "Pass managed NE id from listManagedNe/listCliTargets (source=managed). " + "For UME inventory UUIDs use execManagedNe(ume_ne_id=...) or getUmeNe, not getManagedNe." + ), + "example": {"ne_id": ""}, + } + # UME ne_id is typically a UUID; managed NE may differ. Soft-guide when callers mix them. + out = http_json("GET", f"/v1/managed-ne/{ne_id}", params=None) + if isinstance(out, dict) and out.get("ok") is False: + detail = str(out.get("detail") or out.get("error") or "") + low = detail.lower() + if "404" in low or "not_found" in low or "not found" in low or out.get("error") == "netx_http_404": + out = dict(out) + out["hint"] = ( + "Managed NE not found for this ne_id. Call listManagedNe(keyword=...) or " + "listCliTargets(source=managed) first. If this is a UME ne_id, use " + "execManagedNe(ume_ne_id=...) / getUmeNe instead of getManagedNe." + ) + return out def _exec_managed_ne(args: dict[str, Any]) -> dict[str, Any]: @@ -513,7 +536,11 @@ HTTP_MCP_TOOLS: list[dict[str, Any]] = [ }, { "name": "sqlQueryUme", - "description": "Read-only SELECT on UME tables (ume_alarms_current/ume_inventory_ne); server enforces limits.", + "description": ( + "Read-only SELECT on UME tables (ume_alarms_current/ume_inventory_ne); server enforces limits. " + "Requires netx scope sql:query. If insufficient_scope, use aggregateUmeAlarms / " + "queryUmeAlarmsRaw / ume_alarm_xlsx_report instead." + ), "inputSchema": { "type": "object", "properties": { @@ -543,11 +570,21 @@ HTTP_MCP_TOOLS: list[dict[str, Any]] = [ }, { "name": "getManagedNe", - "description": "Get single managed NE metadata (connect_status, hop config summary).", + "description": ( + "Get one **managed** NE by managed ne_id (from listManagedNe / listCliTargets source=managed). " + "Do NOT pass UME inventory UUID here — use getUmeNe or execManagedNe(ume_ne_id=...) instead." + ), "inputSchema": { "type": "object", - "properties": {"ne_id": {"type": "string"}}, - "required": ["ne_id"], + "properties": { + "ne_id": { + "type": "string", + "description": "Managed NE id (not UME host_name / not UME ne_id unless they coincide).", + }, + "managed_ne_id": {"type": "string", "description": "Alias for ne_id."}, + "id": {"type": "string", "description": "Alias for ne_id."}, + }, + "required": [], "additionalProperties": False, }, }, diff --git a/packages/netx-mcp/src/netx_mcp/server.py b/packages/netx-mcp/src/netx_mcp/server.py index 66d9f17..e4b28de 100644 --- a/packages/netx-mcp/src/netx_mcp/server.py +++ b/packages/netx-mcp/src/netx_mcp/server.py @@ -105,7 +105,15 @@ def run_stdio_loop() -> None: need = TOOL_REQUIRED_SCOPE.get(name) granted = scopes() if need and granted is not None and need not in {str(s).lower() for s in granted}: - _err(rid, -32001, f"insufficient_scope:{need}") + _err( + rid, + -32001, + ( + f"insufficient_scope:{need}. " + "Ask a netx admin to grant this scope on the API token; " + "for sql:query prefer aggregateUmeAlarms/queryUmeAlarmsRaw/ume_alarm_xlsx_report instead." + ), + ) continue args = params.get("arguments") if isinstance(params.get("arguments"), dict) else {} _ok(rid, call_http_tool(name, args)) diff --git a/packages/netx-mcp/tests/test_mcp_http.py b/packages/netx-mcp/tests/test_mcp_http.py index c974e03..cd725b1 100644 --- a/packages/netx-mcp/tests/test_mcp_http.py +++ b/packages/netx-mcp/tests/test_mcp_http.py @@ -114,12 +114,22 @@ def test_call_exec_managed_ne_defaults_read_timeout() -> None: assert payload["ok"] is True -def test_call_get_ume_ne_requires_id() -> None: - out = call_http_tool("getUmeNe", {}) +def test_get_managed_ne_requires_id_with_hint() -> None: + out = call_http_tool("getManagedNe", {}) assert out.get("isError") is True - text = out["content"][0]["text"] - payload = json.loads(text) + payload = json.loads(out["content"][0]["text"]) assert payload["error"] == "ne_id_required" + assert "hint" in payload + + +def test_get_managed_ne_accepts_managed_ne_id_alias() -> None: + with patch("netx_mcp.http_tools.http_json") as mock_http: + mock_http.return_value = {"ok": True, "data": {"ne_id": "m1"}} + out = call_http_tool("getManagedNe", {"managed_ne_id": "m1"}) + mock_http.assert_called_once() + assert mock_http.call_args[0][1].endswith("/m1") + payload = json.loads(out["content"][0]["text"]) + assert payload["ok"] is True def test_call_exec_managed_ne_posts_body() -> None: