mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
Accept group_by on aggregateUmeAlarms and clarify CLI tool guidance.
Agents frequently passed group_by to the fixed aggregate tool; route those calls to raw aggregation and discourage list/exec retry loops. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
78d174232d
commit
9457ac9353
2 changed files with 73 additions and 7 deletions
|
|
@ -132,6 +132,10 @@ def _query_ume_alarms(args: dict[str, Any]) -> dict[str, Any]:
|
|||
|
||||
|
||||
def _aggregate_ume_alarms(args: dict[str, Any]) -> dict[str, Any]:
|
||||
# Agents often pass group_by here (docs historically mixed tools). Route to raw aggregate.
|
||||
group_by = str(args.get("group_by") or "").strip()
|
||||
if group_by:
|
||||
return _aggregate_ume_alarms_raw(args)
|
||||
# Default top 50 named NEs — missing host buckets reported separately.
|
||||
top_ne = max(0, min(500, int(args.get("top_ne") if args.get("top_ne") is not None else 50)))
|
||||
params: dict[str, Any] = {"top_ne": top_ne}
|
||||
|
|
@ -360,7 +364,8 @@ HTTP_MCP_TOOLS: list[dict[str, Any]] = [
|
|||
"Optional severity filter (e.g. critical) for risk Top-N. "
|
||||
"by_ne is capped by top_ne (default 50) and excludes missing host_name by default "
|
||||
"(see by_ne_missing). meta.last_seen_min/max show data freshness. "
|
||||
"For custom grouping use aggregateUmeAlarmsRaw."
|
||||
"If group_by is set (e.g. alarm_host_name), automatically routes to the same "
|
||||
"behavior as aggregateUmeAlarmsRaw — preferred for custom dimensions."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
|
|
@ -374,15 +379,39 @@ HTTP_MCP_TOOLS: list[dict[str, Any]] = [
|
|||
"minimum": 0,
|
||||
"maximum": 500,
|
||||
"default": 50,
|
||||
"description": "Max NE buckets to return (0 = all, capped at API 5000).",
|
||||
"description": "Max NE buckets to return (0 = all, capped at API 5000). Ignored when group_by is set.",
|
||||
},
|
||||
"exclude_missing_host": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Omit (host_name missing) from by_ne ranking.",
|
||||
"description": "Omit (host_name missing) from by_ne / host rankings.",
|
||||
},
|
||||
"time_from": {"type": "string", "description": "ISO time; filters last_seen_at >="},
|
||||
"time_to": {"type": "string", "description": "ISO time; filters last_seen_at <="},
|
||||
"group_by": {
|
||||
"type": "string",
|
||||
"enum": UME_RAW_GROUP_FIELDS,
|
||||
"description": (
|
||||
"Optional. When set, routes to dynamic raw aggregation "
|
||||
"(same as aggregateUmeAlarmsRaw). Prefer alarm_host_name for NE Top."
|
||||
),
|
||||
},
|
||||
"group_by2": {
|
||||
"type": "string",
|
||||
"enum": UME_RAW_GROUP_FIELDS,
|
||||
"description": "Optional second group field when group_by is set.",
|
||||
},
|
||||
"is_cleared": {"type": "string", "description": "Only used when group_by is set."},
|
||||
"ne_id": {"type": "string", "description": "Only used when group_by is set."},
|
||||
"event_type": {"type": "string", "description": "Only used when group_by is set."},
|
||||
"keyword": {"type": "string", "description": "Only used when group_by is set."},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 2000,
|
||||
"default": 200,
|
||||
"description": "Bucket limit when group_by is set (default 200).",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
"additionalProperties": False,
|
||||
|
|
@ -526,8 +555,10 @@ HTTP_MCP_TOOLS: list[dict[str, Any]] = [
|
|||
"name": "execManagedNe",
|
||||
"description": (
|
||||
f"Run read-only CLI via netx (show/display/ping/traceroute; "
|
||||
f"max {exec_max_commands()} commands, NETX_NE_EXEC_MAX_COMMANDS). "
|
||||
"Use ne_id (managed NE) OR ume_ne_id (UME inventory)."
|
||||
f"max {exec_max_commands()} commands per call, NETX_NE_EXEC_MAX_COMMANDS). "
|
||||
"Use ne_id (managed NE) OR ume_ne_id (UME inventory). "
|
||||
"Batch multiple show commands in one call instead of looping. "
|
||||
"On timeout, raise read_timeout_sec (max 120) or shrink commands — do not blind-retry."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
|
|
@ -540,7 +571,12 @@ HTTP_MCP_TOOLS: list[dict[str, Any]] = [
|
|||
"minItems": 1,
|
||||
"maxItems": exec_max_commands(),
|
||||
},
|
||||
"read_timeout_sec": {"type": "integer", "minimum": 10, "maximum": 120},
|
||||
"read_timeout_sec": {
|
||||
"type": "integer",
|
||||
"minimum": 10,
|
||||
"maximum": 120,
|
||||
"description": "Per-command read timeout; use 60–120 for slow show commands.",
|
||||
},
|
||||
},
|
||||
"required": ["commands"],
|
||||
"additionalProperties": False,
|
||||
|
|
@ -548,7 +584,11 @@ HTTP_MCP_TOOLS: list[dict[str, Any]] = [
|
|||
},
|
||||
{
|
||||
"name": "listCliTargets",
|
||||
"description": "List CLI-capable targets (managed NE and/or UME inventory); use before execManagedNe.",
|
||||
"description": (
|
||||
"List CLI-capable targets (managed NE and/or UME inventory). "
|
||||
"Call once per session with keyword/source, cache ume_ne_id/ne_id, "
|
||||
"then execManagedNe — do not re-list before every command."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,32 @@ def test_call_aggregate_ume_alarms_forwards_top_ne() -> None:
|
|||
assert payload["data"]["top_ne"] == 20
|
||||
|
||||
|
||||
def test_call_aggregate_ume_alarms_group_by_routes_to_raw() -> None:
|
||||
with patch("netx_mcp.http_tools.http_json") as mock_http:
|
||||
mock_http.return_value = {"ok": True, "data": {"buckets": []}}
|
||||
out = call_http_tool(
|
||||
"aggregateUmeAlarms",
|
||||
{"group_by": "alarm_host_name", "severity": "critical", "limit": 20},
|
||||
)
|
||||
mock_http.assert_called_once()
|
||||
assert mock_http.call_args[0][0] == "GET"
|
||||
assert mock_http.call_args[0][1] == "/v1/ume/alarms/aggregate/raw"
|
||||
params = mock_http.call_args[1]["params"]
|
||||
assert params["group_by"] == "alarm_host_name"
|
||||
assert params["severity"] == "critical"
|
||||
assert params["limit"] == "20"
|
||||
payload = json.loads(out["content"][0]["text"])
|
||||
assert payload["ok"] is True
|
||||
|
||||
|
||||
def test_aggregate_ume_alarms_schema_accepts_group_by() -> None:
|
||||
tool = next(t for t in HTTP_MCP_TOOLS if t.get("name") == "aggregateUmeAlarms")
|
||||
props = tool["inputSchema"]["properties"]
|
||||
assert "group_by" in props
|
||||
assert "group_by2" in props
|
||||
assert "alarm_host_name" in props["group_by"]["enum"]
|
||||
|
||||
|
||||
def test_call_find_topology_paths_defaults_summary_detail() -> None:
|
||||
with patch("netx_mcp.http_tools.http_post_json") as mock_post:
|
||||
mock_post.return_value = {"ok": True, "data": {"path_count": 1, "detail": "summary", "paths": []}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue