JMESPath Filtering on List Tools

Every *_list_* tool across every service accepts an optional query parameter that takes a JMESPath expression. The expression is applied client-side, after the API call returns — so it filters and projects the results without changing what the Zscaler API sees.

The use case

Tenants with thousands of locations, hundreds of policy rules, or large user populations return paginated results that an agent has to walk. JMESPath lets the operator (or the agent acting on their behalf) narrow the result set before it lands in the model’s context.

A concrete example. Without filtering, listing every ZIA location returns the full page:

zia_list_locations(page=1, page_size=100)
→ [{id: ..., name: "HQ", country: "USA", ...}, {id: ..., name: "Frankfurt", ...}, …]

With a JMESPath expression:

zia_list_locations(query="[?country=='USA'].{id: id, name: name}")
→ [{id: 123, name: "HQ"}, {id: 124, name: "Boston"}, …]

The agent gets exactly the fields it needs, scoped to exactly the rows that match.

Expression syntax

Standard JMESPath syntax applies. Field names use snake_case because the Zscaler SDK converts camelCase API responses to snake_case before the expression runs.

Common patterns:

Goal

Expression

Service example

Filter rows

[?field=='value']

zia_list_locations(query="[?country=='USA']")

Project fields

[*].{a: a, b: b}

zpa_list_application_segments(query="[*].{id: id, name: name}")

Filter + project

[?enabled==`true`].{name: name, id: id}

zpa_list_segment_groups(query="[?enabled==\`true\`].{name: name, id: id}")

Count rows

length(@)

zia_list_url_filtering_rules(query="length(@)")

Contains substring

[?contains(name, 'prod')]

zpa_list_application_segments(query="[?contains(name, 'prod')]")

First match

[?name=='HQ'] | [0]

zia_list_locations(query="[?name=='HQ'] | [0]")

Note

Booleans in JMESPath are backtick-quoted — write [?enabled==\`true\`] and [?enabled==\`false\`]. That’s a JMESPath syntax quirk, not a mistake.

Service-specific examples

Service

Expression

ZIA

[?name=='HQ'].{name: name, id: id} — find location named “HQ”, project name + id

ZPA

[?enabled==\`true\`] — filter to enabled application segments

ZDX

[?platform=='Windows'].{user_name: user_name} — Windows devices only, just usernames

ZCC

[*].{name: name, os_type: os_type} — name + OS for all devices

ZMS

nodes[?cloud_provider=='AWS'] — AWS workloads only (note the nodes envelope)

EASM

results[?severity=='critical'] — filter findings to critical severity

For ZMS specifically, the GraphQL response is wrapped in a nodes[] + page_info envelope. The JMESPath expression starts inside that envelope, so use nodes[?...] instead of [?...].

How the agent should use this

The user is asking a business question. The JMESPath plumbing is internal optimization. Never narrate it.

Plain-language answers only. Translate tool output into the answer the admin actually wanted.

  • User: “How many ZIA DNS rules exist?”

    Agent: “There are 19 ZIA DNS firewall rules in the tenant.”

    ❌ Do not say “The JMESPath ``length(@)`` returned 19.”

  • User: “List the names of my SSL inspection rules”

    Agent lists the names directly.

    ❌ Do not mention “I projected ``[].name``.”*

The query parameter is a tool, not a presentation device.

Empty results are authoritative

If a filter returns an empty list, that is the answer. The agent should not fan out retries with different filters, broader projections, or larger page sizes “to double-check”. Each retry costs a round trip and adds zero information.

  • ❌ Five calls in sequence: search="DataCenter Switches SSH" → empty → query="[?contains(name,'DataCenter') || contains(name,'SSH')]"query="[*].{id,name}", page_size=200 → unfiltered list → “let me drop the projection in case it’s too aggressive”.

  • ✅ One call: search="DataCenter Switches SSH" → empty → “I can’t find an application segment named ‘DataCenter Switches SSH’. Want me to use a different name?”

This pairs naturally with the search parameter (server-side substring match on the name field). The two are complementary, not redundant.

Invalid expressions

A malformed JMESPath expression returns a structured error response instead of crashing:

zia_list_locations(query="not a valid expression")
→ [{"error": "Invalid JMESPath expression: ..."}]

The agent should treat this like any other tool error — surface a plain-language version to the user and stop, don’t retry.

Implementation

The shared helper lives at zscaler_mcp/common/jmespath_utils.py:

  • apply_jmespath(data, expression) — used by non-ZMS list tools (where the API result is a list of dicts).

  • ZMS list tools use a dedicated wrapper in zscaler_mcp/tools/zms/__init__.py that preserves the nodes[] + page_info envelope so expressions can target either the envelope or the data.

When query is None, results pass through unchanged — full backward compatibility with callers that don’t use the parameter.

Return types

JMESPath expressions can produce scalars (length(@)), differently-shaped lists ([*].name), or filtered subsets of the original shape. Tools that accept query declare a permissive return type (Any) so the MCP / Pydantic output validator accepts whichever shape the expression produces.

If you write a tool that accepts query, never declare its return type as List[dict] / List[str] — that causes the validator to reject expressions like length(@) (which returns an int).

See also