Cap response size on route_filters and route_devices_using_css

Two MCP tools blew the per-response token cap when run against a real
medium-sized cluster (Bingham Memorial, ~1500 patterns in Internal-PT,
20 route filters with hundreds of member rules each):

  route_devices_using_css("Internal-CSS")  -> 103,590 chars
  route_filters()                          -> 304,639 chars

Both responses are now compact-by-default with opt-in detail:

route_filters(include_members=False, default):
  - returns name, clause, dial_plan, and member_count per filter
  - 304,639 -> 17,354 chars (94% reduction)
  - member_count is the audit-relevant signal anyway: filters with
    100+ rules are complex; the count tells you that without paying
    for the full rule listing
  - include_members=True scopes detail to a single named filter
    (BLK-ALWAYS-RF with 432 rules: 40K chars; tractable per-filter)

route_devices_using_css(max_per_category=50, default):
  - each category returns at most max_per_category rows
  - truncated: bool flag set when underlying count exceeds the cap
  - 103,590 -> 13,855 chars (87% reduction)
  - implementation uses SELECT FIRST max+1, so no extra COUNT query
    per category — single round-trip with accurate truncation flag
  - LLM can drill in via higher max_per_category or axl_sql when
    truncated=true

Both changes are backward-compatible defaults; existing callers continue
to work and just get smaller, structured responses.
This commit is contained in:
Ryan Malloy 2026-04-25 20:43:13 -06:00
parent 9340e7385a
commit e3fb10cb4b
2 changed files with 114 additions and 53 deletions

View File

@ -520,18 +520,32 @@ _CSS_REFERENCE_QUERIES: dict[str, dict] = {
} }
def find_devices_using_css(client: "AxlClient", css_name: str) -> dict: def find_devices_using_css(
client: "AxlClient",
css_name: str,
max_per_category: int = 50,
) -> dict:
"""Impact analysis: enumerate every reference to this CSS across the schema. """Impact analysis: enumerate every reference to this CSS across the schema.
Useful before changing or removing a CSS answers "what would break if Useful before changing or removing a CSS answers "what would break if
I touched this CSS?" Cross-references the major schema tables that hold I touched this CSS?" Cross-references the major schema tables that hold
fkcallingsearchspace* columns. fkcallingsearchspace* columns.
Returns groups of references by category. Each reference has: Args:
- name: the object identifier (pattern text, device name, or DN) css_name: The exact CSS name (case-sensitive).
- context: partition (for patterns) or device class (for devices) max_per_category: Max rows to return per reference category. Default 50.
- description: free-text description On heavily-referenced CSSs (e.g., Internal-CSS with hundreds of
- _table, _column: which fk-column referenced this CSS line-CFA references), the un-paginated response can exceed MCP
response limits. Each truncated category includes a `truncated:
true` flag so the LLM knows to drill in (call again with higher
max, or use `axl_sql` directly for that specific table+column).
Returns:
references_by_category: each entry has:
- rows: list of references, capped at max_per_category
- returned_count: number of rows in this response
- truncated: True if the underlying result has more rows than returned
- table, column: which fk-column was queried
""" """
safe = _esc(css_name) safe = _esc(css_name)
css_lookup = client.execute_sql_query( css_lookup = client.execute_sql_query(
@ -542,29 +556,51 @@ def find_devices_using_css(client: "AxlClient", css_name: str) -> dict:
css_pkid = css_lookup["rows"][0]["pkid"] css_pkid = css_lookup["rows"][0]["pkid"]
safe_pkid = _esc(css_pkid) safe_pkid = _esc(css_pkid)
grouped: dict[str, list[dict]] = {} # Trick: ask Informix for max+1 rows. If we get max+1 back, we know the
for label, spec in _CSS_REFERENCE_QUERIES.items(): # underlying result has MORE than max — set truncated=True and trim.
sql = spec["sql"].format(pkid=safe_pkid) # This avoids the cost of a separate COUNT(*) per category.
try: fetch_n = max_per_category + 1
result = client.execute_sql_query(sql)
if result["rows"]:
grouped[label] = [
{**r, "_table": spec["table"], "_column": spec["column"]}
for r in result["rows"]
]
except Exception as e:
# Don't let one failed reference point block the whole audit
grouped[label] = [{
"_error": str(e)[:200],
"_table": spec["table"],
"_column": spec["column"],
}]
total = sum(len(v) for v in grouped.values()) grouped: dict[str, dict] = {}
for label, spec in _CSS_REFERENCE_QUERIES.items():
# Inject a FIRST limit into the SELECT — the queries all start with
# `SELECT ` so a literal replacement is reliable here.
base_sql = spec["sql"].format(pkid=safe_pkid).strip()
if base_sql.upper().startswith("SELECT "):
limited_sql = "SELECT FIRST " + str(fetch_n) + " " + base_sql[7:]
else:
limited_sql = base_sql
try:
result = client.execute_sql_query(limited_sql)
rows = result["rows"]
if not rows:
continue
truncated = len(rows) > max_per_category
if truncated:
rows = rows[:max_per_category]
grouped[label] = {
"table": spec["table"],
"column": spec["column"],
"returned_count": len(rows),
"truncated": truncated,
"rows": rows,
}
except Exception as e:
grouped[label] = {
"table": spec["table"],
"column": spec["column"],
"error": str(e)[:200],
}
total_returned = sum(c.get("returned_count", 0) for c in grouped.values())
any_truncated = any(c.get("truncated") for c in grouped.values())
return { return {
"css_name": css_name, "css_name": css_name,
"css_pkid": css_pkid, "css_pkid": css_pkid,
"total_references": total, "total_returned": total_returned,
"any_truncated": any_truncated,
"max_per_category": max_per_category,
"references_by_category": grouped, "references_by_category": grouped,
} }
@ -573,21 +609,38 @@ def find_devices_using_css(client: "AxlClient", css_name: str) -> dict:
# Route Filters # Route Filters
# ==================================================================== # ====================================================================
def list_route_filters(client: "AxlClient", name: str | None = None) -> dict: def list_route_filters(
"""List route filters with their member clauses. client: "AxlClient",
name: str | None = None,
include_members: bool = False,
) -> dict:
"""List route filters with their composition rules.
Route filters compose with @-pattern (NANPA) route patterns to constrain Route filters compose with @-pattern (NANPA) route patterns to constrain
matches e.g., a filter that matches only when AREA-CODE == 208 narrows matches e.g., a filter that matches only when AREA-CODE == 208 narrows
a `9.@` pattern to in-state calls. Members specify (digit, operator, tag) a `9.@` pattern to in-state calls. The `clause` field contains the
triples; the clause column shows the assembled expression. pre-rendered expression, which is usually enough for audit work.
Args:
name: If given, return only the named filter.
include_members: If True, also fetch the (digit, operator, tag) member
rules for each filter. Default False the response can be very
large (hundreds of KB on clusters with many international filters)
and the `clause` field already shows the human-readable rule. Set
True only when you need to programmatically reconstruct or modify
the filter logic.
""" """
where = f"WHERE rf.name = '{_esc(name)}'" if name else "" where = f"WHERE rf.name = '{_esc(name)}'" if name else ""
# Member count is cheap to compute server-side and gives auditors a quick
# signal for which filters are complex without paying for the full member
# listing.
sql = f""" sql = f"""
SELECT SELECT
rf.name AS filter_name, rf.name AS filter_name,
rf.clause AS clause, rf.clause AS clause,
dp.name AS dial_plan, dp.name AS dial_plan,
rf.pkid AS pkid rf.pkid AS pkid,
(SELECT COUNT(*) FROM routefiltermember rfm WHERE rfm.fkroutefilter = rf.pkid) AS member_count
FROM routefilter rf FROM routefilter rf
LEFT OUTER JOIN dialplan dp ON rf.fkdialplan = dp.pkid LEFT OUTER JOIN dialplan dp ON rf.fkdialplan = dp.pkid
{where} {where}
@ -597,9 +650,15 @@ def list_route_filters(client: "AxlClient", name: str | None = None) -> dict:
if not result["rows"]: if not result["rows"]:
return {"filter_count": 0, "route_filters": []} return {"filter_count": 0, "route_filters": []}
# Fetch members per filter
out = [] out = []
for filt in result["rows"]: for filt in result["rows"]:
entry = {
"name": filt.get("filter_name"),
"clause": filt.get("clause"),
"dial_plan": filt.get("dial_plan"),
"member_count": _to_int(filt.get("member_count")),
}
if include_members:
member_sql = f""" member_sql = f"""
SELECT SELECT
dpt.tag AS tag, dpt.tag AS tag,
@ -613,12 +672,8 @@ def list_route_filters(client: "AxlClient", name: str | None = None) -> dict:
ORDER BY rfm.precedence ORDER BY rfm.precedence
""" """
members = client.execute_sql_query(member_sql) members = client.execute_sql_query(member_sql)
out.append({ entry["members"] = members["rows"]
"name": filt.get("filter_name"), out.append(entry)
"clause": filt.get("clause"),
"dial_plan": filt.get("dial_plan"),
"members": members["rows"],
})
return {"filter_count": len(out), "route_filters": out} return {"filter_count": len(out), "route_filters": out}

View File

@ -263,7 +263,7 @@ def route_device_pool_route_groups(device_pool_name: str | None = None) -> dict:
@mcp.tool @mcp.tool
def route_devices_using_css(css_name: str) -> dict: def route_devices_using_css(css_name: str, max_per_category: int = 50) -> dict:
"""Impact analysis: every reference to a CSS across the schema. """Impact analysis: every reference to a CSS across the schema.
Use before changing or removing a CSS to find all dependent devices, lines, Use before changing or removing a CSS to find all dependent devices, lines,
@ -272,23 +272,29 @@ def route_devices_using_css(css_name: str) -> dict:
Args: Args:
css_name: The exact CSS name (case-sensitive). css_name: The exact CSS name (case-sensitive).
max_per_category: Max rows per category. Default 50. Each category
with more than `max_per_category` references gets `truncated: true`
so you know to either raise the limit or drill in via `axl_sql`.
""" """
return route_plan.find_devices_using_css(_client(), css_name) return route_plan.find_devices_using_css(_client(), css_name, max_per_category)
@mcp.tool @mcp.tool
def route_filters(name: str | None = None) -> dict: def route_filters(name: str | None = None, include_members: bool = False) -> dict:
"""List route filters with their composition rules. """List route filters with their composition rules.
Route filters compose with @-pattern (NANPA) route patterns to constrain Route filters compose with @-pattern (NANPA) route patterns to constrain
which calls match e.g., "AREA-CODE == 208" narrows a `9.@` pattern to which calls match e.g., "AREA-CODE == 208" narrows a `9.@` pattern to
in-state calls. Each filter has an ordered list of (digit, operator, tag) in-state calls.
member clauses.
Args: Args:
name: Optional. If given, return only the named filter. name: Optional. If given, return only the named filter.
include_members: If True, include the (digit, operator, tag) member
rules. Default False the `clause` field already shows the
human-readable rule, and member listings can be very large
(hundreds of KB across all filters on a complex cluster).
""" """
return route_plan.list_route_filters(_client(), name) return route_plan.list_route_filters(_client(), name, include_members)
# ==================================================================== # ====================================================================