Prerequisites
Toolbox version
Built from main (module github.com/googleapis/mcp-toolbox); the affected code path is unchanged on the latest release and the quote-stripping fix from #3273 does not address this case.
Environment
- OS type and version: macOS (Darwin); reproducible on any platform — the bug is in filter-expression encoding, not OS-specific.
- How are you running Toolbox: Compiled from source (
go build).
Client
- Client: MCP client driving the Looker tools (e.g. an LLM-backed MCP host).
- Version: n/a — any client that passes a bare
type: unquoted allowed_value containing _, %, or , triggers it.
- Example filter payload that triggers the bug:
{"cohort_marketing_performance.attribution_model_selector": "first_touch"}
Expected Behavior
When a looker-query (or any tool that goes through the shared lookercommon.ProcessQueryArgs helper) filters on a LookML parameter field declared type: unquoted, the bare allowed_value (e.g. first_touch) should reach Looker so it is substituted into SQL exactly as written. This matches the URL form that works in the Looker UI and the documented allowed_value semantics in LookML.
Current Behavior
Looker's filter-expression parser treats _ as a single-character wildcard and % as a multi-character wildcard. For type: unquoted parameters the substituted SQL must match [A-Za-z0-9_.$] literally, so the parser expands first_touch into a wildcard pattern, finds non-identifier characters in the result, and 400s with:
The filter "first_touch" is not allowed.
Filter on unquoted field <field> must contain only underscores, numbers, letters
Same failure for signup_date, paid_social, and any other allowed_value that contains _, %, or ,. PR #3273 fixed the wrapping-quote case but the value still reaches Looker un-escaped, so identifiers containing _ continue to fail.
Looker's own default_filter_value is stored already escaped (first^_touch), which confirms that filter-expression escape — not bare allowed_value — is the correct wire format for the /queries/run API. The explore URL parser used by the UI applies different escaping rules, which is why the same value works in the browser and 400s through the API.
Affects every tool built on the shared helper: looker-query, looker-query-sql, looker-query-url, looker-make-look, and looker-add-dashboard-element.
Steps to reproduce?
- Define a LookML
parameter field with type: unquoted and an allowed_value whose value contains _ (e.g. first_touch).
- Run a
looker-query against the explore with a bare filter value: {"view.field": "first_touch"}.
- Looker returns
400 The filter "first_touch" is not allowed. instead of substituting first_touch into the SQL.
Additional Details
Proposed fix: in internal/tools/looker/lookercommon/lookercommon.go, add a helper that fetches the explore's parameter metadata via sdk.LookmlModelExplore and escapes ^/_/%/, with a ^ prefix in any filter value targeting a type: unquoted parameter. Idempotent — values containing ^ are assumed pre-escaped and pass through unchanged, so callers can keep using the raw default_filter_value form. Metadata-lookup failures degrade to a no-op so users without explore-read permission still get non-parameter queries through. Wire into all five WriteQuery-building tools. Unit tests cover escape, idempotence, mixed filters, non-string values, and the nil/empty guards. End-to-end-verified against the live Looker API. Happy to send a PR.
Prerequisites
Toolbox version
Built from
main(modulegithub.com/googleapis/mcp-toolbox); the affected code path is unchanged on the latest release and the quote-stripping fix from #3273 does not address this case.Environment
go build).Client
type: unquotedallowed_value containing_,%, or,triggers it.{"cohort_marketing_performance.attribution_model_selector": "first_touch"}Expected Behavior
When a
looker-query(or any tool that goes through the sharedlookercommon.ProcessQueryArgshelper) filters on a LookMLparameterfield declaredtype: unquoted, the bareallowed_value(e.g.first_touch) should reach Looker so it is substituted into SQL exactly as written. This matches the URL form that works in the Looker UI and the documentedallowed_valuesemantics in LookML.Current Behavior
Looker's filter-expression parser treats
_as a single-character wildcard and%as a multi-character wildcard. Fortype: unquotedparameters the substituted SQL must match[A-Za-z0-9_.$]literally, so the parser expandsfirst_touchinto a wildcard pattern, finds non-identifier characters in the result, and 400s with:Same failure for
signup_date,paid_social, and any other allowed_value that contains_,%, or,. PR #3273 fixed the wrapping-quote case but the value still reaches Looker un-escaped, so identifiers containing_continue to fail.Looker's own
default_filter_valueis stored already escaped (first^_touch), which confirms that filter-expression escape — not bare allowed_value — is the correct wire format for the/queries/runAPI. The explore URL parser used by the UI applies different escaping rules, which is why the same value works in the browser and 400s through the API.Affects every tool built on the shared helper:
looker-query,looker-query-sql,looker-query-url,looker-make-look, andlooker-add-dashboard-element.Steps to reproduce?
parameterfield withtype: unquotedand anallowed_valuewhose value contains_(e.g.first_touch).looker-queryagainst the explore with a bare filter value:{"view.field": "first_touch"}.400 The filter "first_touch" is not allowed.instead of substitutingfirst_touchinto the SQL.Additional Details
Proposed fix: in
internal/tools/looker/lookercommon/lookercommon.go, add a helper that fetches the explore's parameter metadata viasdk.LookmlModelExploreand escapes^/_/%/,with a^prefix in any filter value targeting atype: unquotedparameter. Idempotent — values containing^are assumed pre-escaped and pass through unchanged, so callers can keep using the rawdefault_filter_valueform. Metadata-lookup failures degrade to a no-op so users without explore-read permission still get non-parameter queries through. Wire into all five WriteQuery-building tools. Unit tests cover escape, idempotence, mixed filters, non-string values, and the nil/empty guards. End-to-end-verified against the live Looker API. Happy to send a PR.