Skip to content

feat: add query_logs tool for custom log queries - #333

Open
jordienr wants to merge 16 commits into
supabase:mainfrom
jordienr:claude/mcp-log-query-tool-8372a9
Open

feat: add query_logs tool for custom log queries#333
jordienr wants to merge 16 commits into
supabase:mainfrom
jordienr:claude/mcp-log-query-tool-8372a9

Conversation

@jordienr

@jordienr jordienr commented Jul 16, 2026

Copy link
Copy Markdown
Member

What

Adds a new query_logs tool to the debugging feature group. It runs a custom ClickHouse SQL query against a project's unified logs stream, for cases where the get_logs service presets are too coarse (filtering, aggregating, or joining across log fields).

Tracks AI-701. Builds on the ClickHouse logs endpoint work in O11Y-1813 and the get_logs ClickHouse migration (#326).

How it addresses the observability team's concerns

  1. Prompt injection / security — log content is user-controllable (same class of risk as execute_sql), so the result is wrapped in wrapWithUntrustedDataBoundary, the same best-effort guardrail execute_sql uses. Not foolproof, but consistent with the existing arbitrary-query tool.
  2. Cost / scalability — starts conservative: defaults to a 24h window, and the analytics endpoint caps the requested range at 24h server-side. No unbounded queries.
  3. ClickHouse, not BigQuery — hits GET /v1/projects/{ref}/analytics/endpoints/logs (ClickHouse / logs.all.otel) and takes raw ClickHouse-dialect SQL. Does not touch the deprecated BigQuery-backed logs.all.
  4. No POST on v1 — uses GET with sql as a query param. No new POST handler.
  5. CLI / self-hosted — reuses the exact same endpoint as the already-merged get_logs (feat: support edge function runtime logs in get_logs #326) under the same debugging group, so it inherits identical platform availability and introduces no new exposure on CLI/self-hosted.

Details

  • query_logs params: project_id, sql, optional iso_timestamp_start/iso_timestamp_end. Defaults to the last 24h window (matching get_logs).
  • Read-only enforcement is left to the backend (the analytics endpoint is read-only); no client-side SQL parsing.
  • Auto-registers under the already-enabled debugging group.

Verification

  • tsc --noEmit clean, biome ci clean.
  • All unit + integration suites pass (incl. logs.test.ts, server.test.ts). The 5 failing checks are |e2e| tests requiring live SUPABASE_ACCESS_TOKEN / ANTHROPIC_API_KEY, which fork PRs don't receive — they pass when run in the upstream context with secrets.

Note

Per CONTRIBUTING, feature PRs should track an accepted issue — this tracks AI-701.

@jordienr
jordienr marked this pull request as ready for review July 17, 2026 08:57
@jordienr
jordienr requested a review from a team as a code owner July 17, 2026 08:57

@barryroodt barryroodt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really clean addition, Jordi. It mirrors the merged get_logs sibling almost exactly (same endpoint, same untrusted-data wrapping, same 24h defaulting), and I reproduced the checks against the PR head: tsc --noEmit, biome ci ., and the server.test.ts + logs.test.ts suites (111 tests) all pass. Nothing here blocks merge. A couple of small things worth a look:

The sql description is missing function_edge_logs. The source list guides the model on what to filter by, and right now it lists function_logs (edge-function runtime console output) but not function_edge_logs (the invocation/request logs the get_logs edge-function preset queries, see logs.ts:37). Since that description is effectively the contract the model writes SQL against, a model following it can't reach invocation logs and will filter on the wrong source. Everything else in the list looks right.

query_logs has no execution test yet. The test change adds it to the tool-listing assertion, which is exactly right for registration, but the new timestamp-defaulting / passthrough / wrapping logic in execute is the bug-prone part and it's currently untested (whereas get_logs has three). server.test.ts:1567 is a ready-made template and the existing /endpoints/logs mock already covers it, so no new mock needed. Happy to leave this as a fast-follow if you'd rather not expand scope here.

Tiny nit: sql: z.string() could take .min(1) to match executeSqlOptionsSchema.query, so an empty query gives a clear validation message instead of an opaque backend error. Very much a nice-to-have.

None of this is load-bearing for shipping. The function_edge_logs line is the one I'd genuinely want fixed before merge; the rest are optional.

@jordienr

Copy link
Copy Markdown
Member Author

Thanks for the thorough review! Addressed all three in f37cfc5:

  • function_edge_logs missing from the sql description — added it to the source-hint list, so a model can now reach edge function invocation logs (not just function_logs runtime output).
  • No execution test for query_logs — added three, mirroring the get_logs template at server.test.ts: sql passthrough + timestamp defaulting, custom-window forwarding, and empty-query rejection.
  • sql nit — added .min(1) on both the tool schema and queryLogsOptionsSchema, matching executeSqlOptionsSchema.query.

tsc --noEmit, biome ci ., and the server.test.ts + logs.test.ts suites (114 tests) all pass.

🤖 Addressed by Claude Code

@barryroodt barryroodt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick turnaround @jordienr

  • function_edge_logs added to the source list
  • .min(1) on the schema
  • behavioral tests for query_logs

LGTM

@mattrossman, @Rodriguespn - since I'm still the new guy, perhaps a quick scan and thumbs-up from either of you would be advisable 😁

@mattrossman mattrossman added the publish-preview Runs `publish-preview` workflow to publish preview packages via https://pkg.pr.new/ label Jul 17, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/@supabase/mcp-server-postgrest@286e9dc
pnpm add https://pkg.pr.new/@supabase/mcp-server-supabase@286e9dc
pnpm add https://pkg.pr.new/@supabase/mcp-utils@286e9dc

commit: 286e9dc

Comment thread packages/mcp-server-supabase/src/platform/api-platform.ts
@Ziinc

Ziinc commented Jul 21, 2026

Copy link
Copy Markdown

on further thought, let's leave both tools and potentially deprecate and remove getLogs in favour of queryLogs.
Note that Clickhouse backed querying is only available for production, cli/self-hosted will not have it yet and we need to document that discrepancy as this will break if people try querying against cli. they should use the getLogs tool until full support is provided.

Comment thread packages/mcp-server-supabase/src/tools/debugging-tools.ts
@jordienr

Copy link
Copy Markdown
Member Author

Addressed the deprecation + platform-availability guidance in a4d7228:

  • get_logs is now marked deprecated on hosted (production) projects in favour of query_logs, while remaining the documented path for local (CLI) and self-hosted projects.
  • query_logs description now states it's hosted-only — ClickHouse-backed querying isn't available on CLI/self-hosted yet, and to use get_logs there — so a model won't try it against an unsupported project and break.

🤖 Addressed by Claude Code

@barryroodt

Copy link
Copy Markdown
Contributor

Following up with fresh numbers against the current head (ecc456d)

Rerun of our eval A/B against the new head: passed all 3 eval checks on the first attempt, and the a4d7228 deprecation note fully flipped tool selection, 8 query_logs calls and zero get_logs (previously the model mixed both). The first genuine ClickHouse aggregation (countIf(toInt32OrZero(...))) succeeded as-is. For contrast, the get_logs-only baseline on main had one run die on context length counting raw preset rows, so the deprecation direction looks well supported.

One finding: the 24 hour default still silently widens narrow questions. The eval asks about the last 15 minutes; all 8 query_logs calls in the fresh run passed no timestamps, inheriting the 24 hour window, even though the description now mentions iso_timestamp_start/iso_timestamp_end. Mentioning the window permissively isn't landing; in production that means wrong counts whenever older logs exist.

Suggestion: same mechanism as a4d7228 - make the line directive, something like "when the user asks about a specific time range, always pass iso_timestamp_start/iso_timestamp_end to match it".

Unrelated side effect: this exercise surfaced drift in our own evals fixture, fixed in supabase/evals#99.

@jordienr

Copy link
Copy Markdown
Member Author

Thanks for the fresh eval run — great to see the deprecation note flip selection cleanly, and good catch on the time-range default.

Fixed in ea6d163: the timestamp guidance is now a directive rather than a permissive mention, using the same mechanism as the deprecation line. Both query_logs and get_logs now say:

When the user asks about a specific time range, always pass iso_timestamp_start and iso_timestamp_end to match it; otherwise the query defaults to the last 24 hours and will return results from a wider window than intended.

Applied to get_logs too since it shares the identical 24h defaulting. Whenever you next rerun the A/B I'd be curious whether the 15-minute question now passes an explicit window.

🤖 Addressed by Claude Code

@barryroodt

Copy link
Copy Markdown
Contributor

Nice! Reran the A/B against ea6d163 and the directive line does exactly what we hoped: the model now passes an explicit 15 minute window matching the question on every analytical query_logs call (4 of 4), and the only windowless calls are orientation probes (a now() check and a quick sample of the data), which is the behaviour you'd want. Still 3/3 checks on the first attempt, still 100% query_logs over get_logs on hosted.

Comment thread packages/mcp-server-supabase/src/tools/debugging-tools.ts
Comment thread packages/mcp-server-supabase/src/tools/debugging-tools.ts
Comment thread packages/mcp-server-supabase/src/tools/debugging-tools.ts Outdated
Comment thread packages/mcp-server-supabase/src/tools/debugging-tools.ts Outdated
smeubank added a commit to supabase/supabase that referenced this pull request Jul 24, 2026
…s split

this committ assume we will have merged or should be merged to gether with cahnges on skills and MCP

- supabase/agent-skills#112
- supabase/mcp#333

 query_logs (hosted only, ClickHouse SQL) is now the preferred tool for
  production projects; get_logs remains the only option for local and
  self-hosted. Updates the MCP tools table, log-querying section, and skill
  step 3 in ai-agents.mdx, and the tip admonition in debugging.mdx.
  logs.mdx already had this right at line 321.
jeremenichelli pushed a commit to supabase/supabase that referenced this pull request Jul 30, 2026
…s split

this committ assume we will have merged or should be merged to gether with cahnges on skills and MCP

- supabase/agent-skills#112
- supabase/mcp#333

 query_logs (hosted only, ClickHouse SQL) is now the preferred tool for
  production projects; get_logs remains the only option for local and
  self-hosted. Updates the MCP tools table, log-querying section, and skill
  step 3 in ai-agents.mdx, and the tip admonition in debugging.mdx.
  logs.mdx already had this right at line 321.
jeremenichelli pushed a commit to supabase/supabase that referenced this pull request Jul 31, 2026
…s split

this committ assume we will have merged or should be merged to gether with cahnges on skills and MCP

- supabase/agent-skills#112
- supabase/mcp#333

 query_logs (hosted only, ClickHouse SQL) is now the preferred tool for
  production projects; get_logs remains the only option for local and
  self-hosted. Updates the MCP tools table, log-querying section, and skill
  step 3 in ai-agents.mdx, and the tip admonition in debugging.mdx.
  logs.mdx already had this right at line 321.
Adds a query_logs debugging tool that runs a custom read-only ClickHouse
SQL query against a project's unified logs stream, for cases where the
get_logs service presets are too coarse. Reuses the existing analytics
logs endpoint and validates that queries are SELECT/WITH only.
- add function_edge_logs to the sql source-hint list so models can reach
  edge function invocation logs
- require a non-empty sql query (.min(1)), matching execute_sql
- add execution tests for query_logs: sql passthrough + timestamp
  defaulting, custom window forwarding, and empty-query rejection
- mark get_logs as deprecated on hosted projects in favour of query_logs,
  while keeping it as the path for CLI/self-hosted
- document that query_logs (ClickHouse) is hosted-only and will not work
  on CLI/self-hosted yet
The permissive mention of iso_timestamp_start/iso_timestamp_end wasn't
steering model behaviour, so narrow time-range questions silently inherited
the 24h default and over-counted. Make it a directive instruction in both
get_logs and query_logs, matching the mechanism that flipped tool selection.
The description promises iso_timestamp_start defaults to 24h before the end,
but the handler always computed start from now(), so supplying only
iso_timestamp_end produced an inverted/empty window. Derive the end first
(supplied or now), then default start to end - 24h, shared by get_logs and
query_logs.
resolveLogWindow now rejects a malformed iso_timestamp_start/end with a
clear error instead of throwing a raw "Invalid time value", and rejects a
start at or after the end. Also rebases onto main to pick up the
regenerated management API types.
@jordienr
jordienr force-pushed the claude/mcp-log-query-tool-8372a9 branch from c1bdc7f to 09e521e Compare August 3, 2026 14:52
Both tools currently ship an identical description to every client
regardless of platform (hosted vs local/self-hosted), so labeling get_logs
"Deprecated" risked a client universally hiding or deprioritizing it, which
would break self-hosted users since get_logs is their only working logs
tool. Reframe as environment-scoped preference (prefer query_logs on
hosted, use get_logs on local/self-hosted) instead of an unqualified
deprecation.

export type DebuggingOperations = {
getLogs(projectId: string, options: GetLogsOptions): Promise<unknown>;
queryLogs(projectId: string, options: QueryLogsOptions): Promise<unknown>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making queryLogs a required in DebuggingOperations breaks every external SupabasePlatform implementer on upgrade (this type ships via the ./platform subpath export and the CLI/studio and mcp.supabase.com controller implement it out-of-repo), and a stale JS implementer would still pass the if (debugging) group check, so query_logs gets listed in tools/list and then crashes with TypeError: debugging.queryLogs is not a function at call time.

We agreed here that this tool should be platform-API-only, and Matt proposed exactly this mechanism: make it queryLogs?(…) and register the tool only when the platform provides it. Then self-hosted/CLI need no changes and the tool is simply absent instead of erroring.

This is also a breaking change to a published type, so can you please change the title of this PR to include feat!: and also make sure that the merged commit that lands on main starts with feat!: please?
Check CONTRIBUTING's expand/contract rule for more details.

return { result: wrapWithUntrustedDataBoundary(result) };
},
}),
query_logs: injectableTool({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the types.ts comment: this registration is unconditional, so every platform with a debugging group advertises query_logs, including BigQuery-dialect (CLI/self-hosted) backends where the description itself says the query will fail.

Guarding with if (debugging.queryLogs) (once the member is optional) hides the tool cleanly on unsupported platforms.

throw new Error('iso_timestamp_start must be before iso_timestamp_end.');
}

return { iso_timestamp_start: start, iso_timestamp_end: end };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveLogWindow validates with Date.parse but forwards the original strings verbatim.

Two consequences: (a) Date.parse accepts non-ISO strings like "Feb 1 2024" even though the error message promises ISO 8601 rejection and (b) offset-less timestamps are parsed as server-local time while the management API treats them as UTC.

I propose normalizing both with new Date(ms).toISOString() before returning fixes both, or move format enforcement into the schema with z.iso.datetime() so the constraint also shows up in the JSON Schema the model sees.

…registration

DebuggingOperations.queryLogs is used by external SupabasePlatform
implementers (CLI, studio, mcp.supabase.com controller) outside this repo.
Making it required would break them on upgrade: a stale implementer still
passes the existing `if (debugging)` group check, so query_logs gets listed
in tools/list and then crashes with "debugging.queryLogs is not a function"
at call time.

Make queryLogs optional and only register the query_logs tool when the
platform actually implements it, so an implementer without ClickHouse
support (self-hosted/CLI today) simply doesn't get the tool listed instead
of erroring. This makes the DebuggingOperations change purely additive.
endTimestamp.getTime() - 24 * 60 * 60 * 1000
); // Last 24 hours

const result = await debugging.getLogs(project_id, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a heads-up that routing get_logs through resolveLogWindow silently changes existing behavior: inputs that previously flowed through to the API are now rejected client-side, and a supplied iso_timestamp_end now anchors the default start instead of using now-24h.

That's arguably all improvement, but none of the six new validation tests cover get_logs, mirroring the inverted/malformed/anchoring cases for get_logs (or exporting resolveLogWindow and unit-testing it once) would pin the shared behavior.

);
}

if (startMs >= endMs) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Both tool descriptions say "The API caps the requested range at 24 hours", but nothing pre-validates.

Cheap win: if (endMs - startMs > DAY_MS) throw new Error('The log window can be at most 24 hours.') plus the matching unit test.


return response.data;
},
async queryLogs(projectId: string, options: QueryLogsOptions) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: Does /v1/.../analytics/endpoints/logs hard-rejects non-SELECT statements?

).rejects.toThrow();
});

test('query logs rejects a start at or after the end', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Adding the equal-timestamps case, and giving these negative tests a message matcher (rejects.toThrow(/must be before/)) instead of bare rejects.toThrow(), would make them mean what they say.

Same theme at line 2075: the default-window assertions only check the params are truthy, an exact assertion would catch a wrong default or swapped bounds.

resolveLogWindow now:
- enforces the 24h API cap client-side with a clear error, instead of
  relying on an unvalidated description promise
- normalizes accepted timestamps to canonical UTC ISO strings before
  forwarding them, instead of passing the original strings through verbatim
- is exported and unit-tested directly (default anchoring, offset
  normalization, malformed/inverted/oversized-window rejection), covering
  get_logs and query_logs' shared behavior in one place

Also enforces ISO 8601 with an explicit UTC "Z" suffix or offset at the
schema level via z.iso.datetime({ offset: true }), so offset-less
timestamps (ambiguous local-time interpretation) are rejected before
reaching resolveLogWindow, and the constraint shows up in the tool's JSON
schema.
@Rodriguespn

Rodriguespn commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Since this adds a tool and rewords the get_logs description, two post-merge follow-ups from CONTRIBUTING apply and are worth tracking in the PR description:

  1. Updating the tool list at Supabase MCP docs
  2. A ChatGPT app resubmission (tool list and descriptions are frozen fields).

Also linking the eval evidence here: the pkg.pr.new preview build of this branch passed the full supabase/evals regression suite (8/8, identical to the published-version baseline - CI run) and locally against the three debugging evals that exercise the analytics logs endpoint (evals#79, with transcripts showing agents successfully using query_logs with source = 'function_edge_logs' ClickHouse SQL.

Conditions of the run: model claude-sonnet-5 (reasoning effort high)

Thanks for taking care of this @jordienr!

- add an equal-timestamps case to the start-at-or-after-end rejection test
- assert on the actual rejection message (Invalid ISO datetime, must be
  before, min-length) instead of a bare rejects.toThrow()
- assert the exact default window (end near now, start = end - 24h)
  instead of just checking the params are truthy

@Rodriguespn Rodriguespn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for taking another look @jordienr. Could you please address the nit comments I left, whether implementing them or closing them. Up to you 🙁

Important

One last thing before merging, this is also a breaking change as we changed get_logs description and inputSchema.
Can you change the title of this PR to include feat!: and also make sure that the merged commit that lands on main starts with feat!: please?

Check CONTRIBUTING's expand/contract rule for more details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

publish-preview Runs `publish-preview` workflow to publish preview packages via https://pkg.pr.new/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants