fix(tools/looker): strip wrapping quotes from filter values for unquoted parameters#3273
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request enhances the Looker tool by providing more detailed documentation for the filters parameter and improving the logic for stripping wrapping quotes from filter keys and string values. It also includes a new test suite to verify these changes across various edge cases. A review comment correctly identifies a potential runtime panic when comparing non-comparable types in the filter map and suggests an idiomatic Go approach of building a new map during iteration to avoid instability.
42a9f3c to
00dba60
Compare
…ted parameters
LookML parameters declared `type: unquoted` are substituted bare into SQL
via {% parameter %}. When the LLM emits a filter value with literal
wrapping quote characters (e.g. `"first_touch"`), those characters land
in the rendered SQL and Looker rejects the query.
Extend the existing key-quote-stripping logic in ProcessQueryArgs to
also strip a single layer of wrapping `"` or `'` from string values.
Bare values are correct for all parameter types over the WriteQuery
wire format, so this is safe to apply unconditionally.
Also tighten the `filters` parameter description so the LLM is told
explicitly not to wrap values in extra quotes, addressing the upstream
cause while the normalization remains as a backstop.
Affects every tool that runs or materializes a query via the shared
helper: looker-query, looker-query-sql, looker-query-url,
looker-make-look, and looker-add-dashboard-element.
00dba60 to
8a8fc82
Compare
|
/gcbrun |
|
/gcbrun |
…or unquoted parameters (googleapis#3273) ## Description LookML `parameter` fields declared `type: unquoted` reject queries through the Looker tools because filter values arrive with a literal layer of wrapping quote characters and are substituted into SQL bare via `{% parameter %}` — producing invalid SQL like `... = "first_touch"`. `ProcessQueryArgs` in `internal/tools/looker/lookercommon/lookercommon.go` already strips a wrapping layer of quotes from filter *keys*; this extends the same handling to string *values*, stripping a single layer of wrapping `"` or `'` only when the first and last characters match. Bare values are correct for all parameter types over the `WriteQuery` wire format, so this is applied unconditionally — no need to branch on the field's declared LookML type. The `filters` parameter description is also tightened so the model is told explicitly not to wrap values in extra quotes, addressing the upstream cause while the normalization stays a backstop. One fix covers every tool built on the shared helper: `looker-query`, `looker-query-sql`, `looker-query-url`, `looker-make-look`, and `looker-add-dashboard-element`. ## Tests - **Unit:** added `TestProcessQueryArgsStripsWrappingQuotes` exercising `ProcessQueryArgs` directly — bare values (unchanged), double-quoted values, single-quoted values, quoted keys (regression), quoted-key+quoted-value, non-string values (untouched), single-character strings (no length-check footgun), and mismatched wrapping characters (left alone). `go test -race ./internal/tools/looker/lookercommon/...` passes. - **Integration:** updated the `filters` manifest assertions in `tests/looker/looker_integration_test.go` to match the tightened parameter description, keeping the live `looker-query`/`looker-query-sql`/`looker-query-url` integration suite green. ## PR Checklist - [x] Make sure you reviewed [CONTRIBUTING.md](https://github.com/googleapis/mcp-toolbox/blob/main/CONTRIBUTING.md) - [x] Make sure to open an issue as a bug/issue before writing your code! - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) - [ ] Make sure to add `!` if this involve a breaking change 🛠️ Fixes googleapis#3272 Co-authored-by: Dr. Strangelove <drstrangelove@google.com> 1e3de96
|
If a h was a h of course of course |
🤖 I have created a release *beep* *boop* --- ## [1.3.0](v1.2.0...v1.3.0) (2026-05-21) ### Features * **auth:** Implement MCP auth tool-level scopes validation ([#3049](#3049)) ([c528985](c528985)) * **looker:** Propagate client IP from incoming MCP requests to downstream SDK calls ([#3253](#3253)) ([75da6c2](75da6c2)) * Setup SQLCommenter and allow client metadata ([#3064](#3064)) ([9f1f9b3](9f1f9b3)) * **tool/cloudsqladmin:** Add `cloud-sql-admin-execute-sql-many` and `cloud-sql-admin-sql-many` ([#3083](#3083)) ([ef300a8](ef300a8)) ### Bug Fixes * **auth/generic:** Fix generic auth expiration field and integration with `authRequired` ([#3251](#3251)) ([f4d16c0](f4d16c0)) * Enforce toolset/promptset boundary on tools/call and prompts/get ([#3036](#3036)) ([c739b80](c739b80)) * **tools/http:** Prevent path traversal and base path scope escape ([#3218](#3218)) ([80a6602](80a6602)) * **tools/looker:** Return a 401 error to MCP client when Looker returns a 401 ([#3233](#3233)) ([4f409a3](4f409a3)) * **tools/looker:** Strip wrapping quotes from filter values for unquoted parameters ([#3273](#3273)) ([1e3de96](1e3de96)) * **tools:** Initialize query result slices to empty array ([#3250](#3250)) ([60ddf48](60ddf48)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
🤖 I have created a release *beep* *boop* --- ## [1.3.0](v1.2.0...v1.3.0) (2026-05-21) ### Features * **auth:** Implement MCP auth tool-level scopes validation ([#3049](#3049)) ([c528985](c528985)) * **looker:** Propagate client IP from incoming MCP requests to downstream SDK calls ([#3253](#3253)) ([75da6c2](75da6c2)) * Setup SQLCommenter and allow client metadata ([#3064](#3064)) ([9f1f9b3](9f1f9b3)) * **tool/cloudsqladmin:** Add `cloud-sql-admin-execute-sql-many` and `cloud-sql-admin-sql-many` ([#3083](#3083)) ([ef300a8](ef300a8)) ### Bug Fixes * **auth/generic:** Fix generic auth expiration field and integration with `authRequired` ([#3251](#3251)) ([f4d16c0](f4d16c0)) * Enforce toolset/promptset boundary on tools/call and prompts/get ([#3036](#3036)) ([c739b80](c739b80)) * **tools/http:** Prevent path traversal and base path scope escape ([#3218](#3218)) ([80a6602](80a6602)) * **tools/looker:** Return a 401 error to MCP client when Looker returns a 401 ([#3233](#3233)) ([4f409a3](4f409a3)) * **tools/looker:** Strip wrapping quotes from filter values for unquoted parameters ([#3273](#3273)) ([1e3de96](1e3de96)) * **tools:** Initialize query result slices to empty array ([#3250](#3250)) ([60ddf48](60ddf48)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com> b001006
🤖 I have created a release *beep* *boop* --- ## [1.3.0](googleapis/mcp-toolbox@v1.2.0...v1.3.0) (2026-05-21) ### Features * **auth:** Implement MCP auth tool-level scopes validation ([googleapis#3049](googleapis#3049)) ([c528985](googleapis@c528985)) * **looker:** Propagate client IP from incoming MCP requests to downstream SDK calls ([googleapis#3253](googleapis#3253)) ([75da6c2](googleapis@75da6c2)) * Setup SQLCommenter and allow client metadata ([googleapis#3064](googleapis#3064)) ([9f1f9b3](googleapis@9f1f9b3)) * **tool/cloudsqladmin:** Add `cloud-sql-admin-execute-sql-many` and `cloud-sql-admin-sql-many` ([googleapis#3083](googleapis#3083)) ([ef300a8](googleapis@ef300a8)) ### Bug Fixes * **auth/generic:** Fix generic auth expiration field and integration with `authRequired` ([googleapis#3251](googleapis#3251)) ([f4d16c0](googleapis@f4d16c0)) * Enforce toolset/promptset boundary on tools/call and prompts/get ([googleapis#3036](googleapis#3036)) ([c739b80](googleapis@c739b80)) * **tools/http:** Prevent path traversal and base path scope escape ([googleapis#3218](googleapis#3218)) ([80a6602](googleapis@80a6602)) * **tools/looker:** Return a 401 error to MCP client when Looker returns a 401 ([googleapis#3233](googleapis#3233)) ([4f409a3](googleapis@4f409a3)) * **tools/looker:** Strip wrapping quotes from filter values for unquoted parameters ([googleapis#3273](googleapis#3273)) ([1e3de96](googleapis@1e3de96)) * **tools:** Initialize query result slices to empty array ([googleapis#3250](googleapis#3250)) ([60ddf48](googleapis@60ddf48)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com> b001006
🤖 I have created a release *beep* *boop* --- ## [1.3.0](googleapis/mcp-toolbox@v1.2.0...v1.3.0) (2026-05-21) ### Features * **auth:** Implement MCP auth tool-level scopes validation ([googleapis#3049](googleapis#3049)) ([c528985](googleapis@c528985)) * **looker:** Propagate client IP from incoming MCP requests to downstream SDK calls ([googleapis#3253](googleapis#3253)) ([75da6c2](googleapis@75da6c2)) * Setup SQLCommenter and allow client metadata ([googleapis#3064](googleapis#3064)) ([9f1f9b3](googleapis@9f1f9b3)) * **tool/cloudsqladmin:** Add `cloud-sql-admin-execute-sql-many` and `cloud-sql-admin-sql-many` ([googleapis#3083](googleapis#3083)) ([ef300a8](googleapis@ef300a8)) ### Bug Fixes * **auth/generic:** Fix generic auth expiration field and integration with `authRequired` ([googleapis#3251](googleapis#3251)) ([f4d16c0](googleapis@f4d16c0)) * Enforce toolset/promptset boundary on tools/call and prompts/get ([googleapis#3036](googleapis#3036)) ([c739b80](googleapis@c739b80)) * **tools/http:** Prevent path traversal and base path scope escape ([googleapis#3218](googleapis#3218)) ([80a6602](googleapis@80a6602)) * **tools/looker:** Return a 401 error to MCP client when Looker returns a 401 ([googleapis#3233](googleapis#3233)) ([4f409a3](googleapis@4f409a3)) * **tools/looker:** Strip wrapping quotes from filter values for unquoted parameters ([googleapis#3273](googleapis#3273)) ([1e3de96](googleapis@1e3de96)) * **tools:** Initialize query result slices to empty array ([googleapis#3250](googleapis#3250)) ([60ddf48](googleapis@60ddf48)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com> b001006
🤖 I have created a release *beep* *boop* --- ## [1.3.0](googleapis/mcp-toolbox@v1.2.0...v1.3.0) (2026-05-21) ### Features * **auth:** Implement MCP auth tool-level scopes validation ([googleapis#3049](googleapis#3049)) ([c528985](googleapis@c528985)) * **looker:** Propagate client IP from incoming MCP requests to downstream SDK calls ([googleapis#3253](googleapis#3253)) ([75da6c2](googleapis@75da6c2)) * Setup SQLCommenter and allow client metadata ([googleapis#3064](googleapis#3064)) ([9f1f9b3](googleapis@9f1f9b3)) * **tool/cloudsqladmin:** Add `cloud-sql-admin-execute-sql-many` and `cloud-sql-admin-sql-many` ([googleapis#3083](googleapis#3083)) ([ef300a8](googleapis@ef300a8)) ### Bug Fixes * **auth/generic:** Fix generic auth expiration field and integration with `authRequired` ([googleapis#3251](googleapis#3251)) ([f4d16c0](googleapis@f4d16c0)) * Enforce toolset/promptset boundary on tools/call and prompts/get ([googleapis#3036](googleapis#3036)) ([c739b80](googleapis@c739b80)) * **tools/http:** Prevent path traversal and base path scope escape ([googleapis#3218](googleapis#3218)) ([80a6602](googleapis@80a6602)) * **tools/looker:** Return a 401 error to MCP client when Looker returns a 401 ([googleapis#3233](googleapis#3233)) ([4f409a3](googleapis@4f409a3)) * **tools/looker:** Strip wrapping quotes from filter values for unquoted parameters ([googleapis#3273](googleapis#3273)) ([1e3de96](googleapis@1e3de96)) * **tools:** Initialize query result slices to empty array ([googleapis#3250](googleapis#3250)) ([60ddf48](googleapis@60ddf48)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com> b001006
🤖 I have created a release *beep* *boop* --- ## [1.3.0](googleapis/mcp-toolbox@v1.2.0...v1.3.0) (2026-05-21) ### Features * **auth:** Implement MCP auth tool-level scopes validation ([googleapis#3049](googleapis#3049)) ([c528985](googleapis@c528985)) * **looker:** Propagate client IP from incoming MCP requests to downstream SDK calls ([googleapis#3253](googleapis#3253)) ([75da6c2](googleapis@75da6c2)) * Setup SQLCommenter and allow client metadata ([googleapis#3064](googleapis#3064)) ([9f1f9b3](googleapis@9f1f9b3)) * **tool/cloudsqladmin:** Add `cloud-sql-admin-execute-sql-many` and `cloud-sql-admin-sql-many` ([googleapis#3083](googleapis#3083)) ([ef300a8](googleapis@ef300a8)) ### Bug Fixes * **auth/generic:** Fix generic auth expiration field and integration with `authRequired` ([googleapis#3251](googleapis#3251)) ([f4d16c0](googleapis@f4d16c0)) * Enforce toolset/promptset boundary on tools/call and prompts/get ([googleapis#3036](googleapis#3036)) ([c739b80](googleapis@c739b80)) * **tools/http:** Prevent path traversal and base path scope escape ([googleapis#3218](googleapis#3218)) ([80a6602](googleapis@80a6602)) * **tools/looker:** Return a 401 error to MCP client when Looker returns a 401 ([googleapis#3233](googleapis#3233)) ([4f409a3](googleapis@4f409a3)) * **tools/looker:** Strip wrapping quotes from filter values for unquoted parameters ([googleapis#3273](googleapis#3273)) ([1e3de96](googleapis@1e3de96)) * **tools:** Initialize query result slices to empty array ([googleapis#3250](googleapis#3250)) ([60ddf48](googleapis@60ddf48)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com> b001006
🤖 I have created a release *beep* *boop* --- ## [1.3.0](googleapis/mcp-toolbox@v1.2.0...v1.3.0) (2026-05-21) ### Features * **auth:** Implement MCP auth tool-level scopes validation ([googleapis#3049](googleapis#3049)) ([c528985](googleapis@c528985)) * **looker:** Propagate client IP from incoming MCP requests to downstream SDK calls ([googleapis#3253](googleapis#3253)) ([75da6c2](googleapis@75da6c2)) * Setup SQLCommenter and allow client metadata ([googleapis#3064](googleapis#3064)) ([9f1f9b3](googleapis@9f1f9b3)) * **tool/cloudsqladmin:** Add `cloud-sql-admin-execute-sql-many` and `cloud-sql-admin-sql-many` ([googleapis#3083](googleapis#3083)) ([ef300a8](googleapis@ef300a8)) ### Bug Fixes * **auth/generic:** Fix generic auth expiration field and integration with `authRequired` ([googleapis#3251](googleapis#3251)) ([f4d16c0](googleapis@f4d16c0)) * Enforce toolset/promptset boundary on tools/call and prompts/get ([googleapis#3036](googleapis#3036)) ([c739b80](googleapis@c739b80)) * **tools/http:** Prevent path traversal and base path scope escape ([googleapis#3218](googleapis#3218)) ([80a6602](googleapis@80a6602)) * **tools/looker:** Return a 401 error to MCP client when Looker returns a 401 ([googleapis#3233](googleapis#3233)) ([4f409a3](googleapis@4f409a3)) * **tools/looker:** Strip wrapping quotes from filter values for unquoted parameters ([googleapis#3273](googleapis#3273)) ([1e3de96](googleapis@1e3de96)) * **tools:** Initialize query result slices to empty array ([googleapis#3250](googleapis#3250)) ([60ddf48](googleapis@60ddf48)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com> b001006
🤖 I have created a release *beep* *boop* --- ## [1.3.0](googleapis/mcp-toolbox@v1.2.0...v1.3.0) (2026-05-21) ### Features * **auth:** Implement MCP auth tool-level scopes validation ([googleapis#3049](googleapis#3049)) ([c528985](googleapis@c528985)) * **looker:** Propagate client IP from incoming MCP requests to downstream SDK calls ([googleapis#3253](googleapis#3253)) ([75da6c2](googleapis@75da6c2)) * Setup SQLCommenter and allow client metadata ([googleapis#3064](googleapis#3064)) ([9f1f9b3](googleapis@9f1f9b3)) * **tool/cloudsqladmin:** Add `cloud-sql-admin-execute-sql-many` and `cloud-sql-admin-sql-many` ([googleapis#3083](googleapis#3083)) ([ef300a8](googleapis@ef300a8)) ### Bug Fixes * **auth/generic:** Fix generic auth expiration field and integration with `authRequired` ([googleapis#3251](googleapis#3251)) ([f4d16c0](googleapis@f4d16c0)) * Enforce toolset/promptset boundary on tools/call and prompts/get ([googleapis#3036](googleapis#3036)) ([c739b80](googleapis@c739b80)) * **tools/http:** Prevent path traversal and base path scope escape ([googleapis#3218](googleapis#3218)) ([80a6602](googleapis@80a6602)) * **tools/looker:** Return a 401 error to MCP client when Looker returns a 401 ([googleapis#3233](googleapis#3233)) ([4f409a3](googleapis@4f409a3)) * **tools/looker:** Strip wrapping quotes from filter values for unquoted parameters ([googleapis#3273](googleapis#3273)) ([1e3de96](googleapis@1e3de96)) * **tools:** Initialize query result slices to empty array ([googleapis#3250](googleapis#3250)) ([60ddf48](googleapis@60ddf48)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com> b001006
## Description PR #3273 fixed the wrapping-quote case for `type: unquoted` Looker parameter filters, but the value still reaches Looker un-escaped, so identifiers containing `_`, `%`, or `,` are still rejected by Looker's filter-expression parser. The 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 `first_touch` is expanded into a wildcard pattern and 400s with `The filter "first_touch" is not allowed.` Looker's own `default_filter_value` is stored already escaped (`first^_touch`), confirming that filter-expression escape is the correct wire format for `/queries/run`. This adds a new helper `EscapeUnquotedParameterFilters` in `internal/tools/looker/lookercommon/lookercommon.go` that fetches the explore's parameter metadata via `sdk.LookmlModelExplore` and escapes `^`/`_`/`%`/`,` with a `^` prefix in any string filter value keyed to a `type: unquoted` parameter. `ProcessQueryArgs` stays pure and keeps the wrapping-quote stripping from #3273. The new helper is idempotent — values that already contain `^` 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 their non-parameter queries through. Wired into every WriteQuery-building tool: `looker-query`, `looker-query-sql`, `looker-query-url`, `looker-make-look`, and `looker-add-dashboard-element`. ## Tests - **Unit:** added `TestEscapeFiltersForUnquotedParameters` and `TestEscapeFiltersForUnquotedParameters_NoopGuards` in `internal/tools/looker/lookercommon/lookercommon_test.go` covering: single-metacharacter escape, multi-metacharacter escape in one value, pre-escaped pass-through, no-metacharacter pass-through, non-parameter filters untouched, non-string values untouched, mixed filters, empty unquoted set is a no-op, and nil-guard panics. `go test -race ./internal/tools/looker/...` passes. - **End-to-end:** verified against the live Looker API — posting `{"…attribution_model_selector": "first_touch"}` to `/queries/run/json` 400s with the documented error; the same body after the escape (`first^_touch` on the wire) returns rows. ## PR Checklist - [x] Make sure you reviewed [CONTRIBUTING.md](https://github.com/googleapis/mcp-toolbox/blob/main/CONTRIBUTING.md) - [x] Make sure to open an issue as a bug/issue before writing your code! - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) - [ ] Make sure to add `!` if this involve a breaking change 🛠️ Fixes #3288 --------- Co-authored-by: Dr. Strangelove <drstrangelove@google.com>
Description
LookML
parameterfields declaredtype: unquotedreject queries through the Looker tools because filter values arrive with a literal layer of wrapping quote characters and are substituted into SQL bare via{% parameter %}— producing invalid SQL like... = "first_touch".ProcessQueryArgsininternal/tools/looker/lookercommon/lookercommon.goalready strips a wrapping layer of quotes from filter keys; this extends the same handling to string values, stripping a single layer of wrapping"or'only when the first and last characters match. Bare values are correct for all parameter types over theWriteQuerywire format, so this is applied unconditionally — no need to branch on the field's declared LookML type. Thefiltersparameter description is also tightened so the model is told explicitly not to wrap values in extra quotes, addressing the upstream cause while the normalization stays a backstop.One fix covers every tool built on the shared helper:
looker-query,looker-query-sql,looker-query-url,looker-make-look, andlooker-add-dashboard-element.Tests
TestProcessQueryArgsStripsWrappingQuotesexercisingProcessQueryArgsdirectly — bare values (unchanged), double-quoted values, single-quoted values, quoted keys (regression), quoted-key+quoted-value, non-string values (untouched), single-character strings (no length-check footgun), and mismatched wrapping characters (left alone).go test -race ./internal/tools/looker/lookercommon/...passes.filtersmanifest assertions intests/looker/looker_integration_test.goto match the tightened parameter description, keeping the livelooker-query/looker-query-sql/looker-query-urlintegration suite green.PR Checklist
!if this involve a breaking change🛠️ Fixes #3272