From 905dd19817aaea9f2c99a2168e9f791a24e071a8 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Thu, 16 Jul 2026 16:24:10 +0200 Subject: [PATCH 01/12] feat: add query_logs tool for custom log queries 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. --- packages/mcp-server-supabase/src/logs.test.ts | 33 +++++++++- packages/mcp-server-supabase/src/logs.ts | 18 ++++++ .../src/platform/api-platform.ts | 30 ++++++++- .../mcp-server-supabase/src/platform/types.ts | 8 +++ .../src/tools/debugging-tools.ts | 61 +++++++++++++++++++ 5 files changed, 148 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server-supabase/src/logs.test.ts b/packages/mcp-server-supabase/src/logs.test.ts index 9855e20..3daad94 100644 --- a/packages/mcp-server-supabase/src/logs.test.ts +++ b/packages/mcp-server-supabase/src/logs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { getClickHouseLogQuery } from './logs.js'; +import { assertReadOnlyLogQuery, getClickHouseLogQuery } from './logs.js'; import type { LogsService } from './platform/types.js'; const serviceSources = { @@ -38,3 +38,34 @@ describe('getClickHouseLogQuery', () => { expect(query).not.toContain("log_attributes['response.status_code']"); }); }); + +describe('assertReadOnlyLogQuery', () => { + test('allows SELECT and WITH queries, ignoring comments and casing', () => { + expect(() => + assertReadOnlyLogQuery('select id from logs limit 10') + ).not.toThrow(); + expect(() => + assertReadOnlyLogQuery(' WITH x as (select 1) select * from x ') + ).not.toThrow(); + expect(() => + assertReadOnlyLogQuery('-- comment\nselect id from logs;') + ).not.toThrow(); + }); + + test('rejects non-read statements', () => { + for (const sql of [ + "insert into logs values ('x')", + 'delete from logs', + 'drop table logs', + '/* select */ update logs set id = 1', + ]) { + expect(() => assertReadOnlyLogQuery(sql)).toThrow(/read-only/); + } + }); + + test('rejects multiple statements', () => { + expect(() => + assertReadOnlyLogQuery('select 1; drop table logs') + ).toThrow(/single/); + }); +}); diff --git a/packages/mcp-server-supabase/src/logs.ts b/packages/mcp-server-supabase/src/logs.ts index 8d449e6..25c576a 100644 --- a/packages/mcp-server-supabase/src/logs.ts +++ b/packages/mcp-server-supabase/src/logs.ts @@ -1,6 +1,24 @@ import { stripIndent } from 'common-tags'; import type { LogsService } from './platform/types.js'; +export function assertReadOnlyLogQuery(sql: string) { + const stripped = sql + .replace(/--[^\n]*/g, '') + .replace(/\/\*[\s\S]*?\*\//g, '') + .trim() + .replace(/;\s*$/, ''); + + if (stripped.includes(';')) { + throw new Error('Only a single log query statement is allowed.'); + } + + if (!/^(select|with)\b/i.test(stripped)) { + throw new Error( + 'Only read-only log queries are allowed. The query must start with SELECT or WITH.' + ); + } +} + export function getClickHouseLogQuery( service: LogsService, limit: number = 100 diff --git a/packages/mcp-server-supabase/src/platform/api-platform.ts b/packages/mcp-server-supabase/src/platform/api-platform.ts index 0862ac2..abf5aa2 100644 --- a/packages/mcp-server-supabase/src/platform/api-platform.ts +++ b/packages/mcp-server-supabase/src/platform/api-platform.ts @@ -6,7 +6,7 @@ import type { InitData } from '@supabase/mcp-utils'; import { fileURLToPath } from 'node:url'; import packageJson from '../../package.json' with { type: 'json' }; import { getDeploymentId, normalizeFilename } from '../edge-function.js'; -import { getClickHouseLogQuery } from '../logs.js'; +import { assertReadOnlyLogQuery, getClickHouseLogQuery } from '../logs.js'; import { assertProjectScopedSuccess, assertSuccess, @@ -20,6 +20,7 @@ import { deployEdgeFunctionOptionsSchema, executeSqlOptionsSchema, getLogsOptionsSchema, + queryLogsOptionsSchema, resetBranchOptionsSchema, type AccountOperations, type ApiKey, @@ -38,6 +39,7 @@ import { type EdgeFunctionWithBody, type ExecuteSqlOptions, type GetLogsOptions, + type QueryLogsOptions, type ResetBranchOptions, type StorageConfig, type StorageOperations, @@ -277,6 +279,32 @@ export function createSupabaseApiPlatform( return response.data; }, + async queryLogs(projectId: string, options: QueryLogsOptions) { + const { sql, iso_timestamp_start, iso_timestamp_end } = + queryLogsOptionsSchema.parse(options); + + assertReadOnlyLogQuery(sql); + + const response = await managementApiClient.GET( + '/v1/projects/{ref}/analytics/endpoints/logs', + { + params: { + path: { + ref: projectId, + }, + query: { + sql, + iso_timestamp_start, + iso_timestamp_end, + }, + }, + } + ); + + assertSuccess(response, 'Failed to query logs'); + + return response.data; + }, async getSecurityAdvisors(projectId: string) { const response = await managementApiClient.GET( '/v1/projects/{ref}/advisors/security', diff --git a/packages/mcp-server-supabase/src/platform/types.ts b/packages/mcp-server-supabase/src/platform/types.ts index 0021a9a..49142ed 100644 --- a/packages/mcp-server-supabase/src/platform/types.ts +++ b/packages/mcp-server-supabase/src/platform/types.ts @@ -148,6 +148,12 @@ export const getLogsOptionsSchema = z.object({ iso_timestamp_end: z.string().optional(), }); +export const queryLogsOptionsSchema = z.object({ + sql: z.string(), + iso_timestamp_start: z.string().optional(), + iso_timestamp_end: z.string().optional(), +}); + export const generateTypescriptTypesResultSchema = z.object({ types: z.string(), }); @@ -172,6 +178,7 @@ export type ListMigrationsResult = z.infer; export type LogsService = z.infer; export type GetLogsOptions = z.infer; +export type QueryLogsOptions = z.infer; export type GenerateTypescriptTypesResult = z.infer< typeof generateTypescriptTypesResultSchema >; @@ -212,6 +219,7 @@ export type EdgeFunctionsOperations = { export type DebuggingOperations = { getLogs(projectId: string, options: GetLogsOptions): Promise; + queryLogs(projectId: string, options: QueryLogsOptions): Promise; getSecurityAdvisors(projectId: string): Promise; getPerformanceAdvisors(projectId: string): Promise; }; diff --git a/packages/mcp-server-supabase/src/tools/debugging-tools.ts b/packages/mcp-server-supabase/src/tools/debugging-tools.ts index b620bc3..0f9cf35 100644 --- a/packages/mcp-server-supabase/src/tools/debugging-tools.ts +++ b/packages/mcp-server-supabase/src/tools/debugging-tools.ts @@ -35,6 +35,31 @@ const getLogsOutputSchema = z.object({ result: z.unknown(), }); +const queryLogsInputSchema = z.object({ + project_id: z.string(), + sql: z + .string() + .describe( + "A read-only ClickHouse SQL query to run against the project's unified logs stream. Must start with SELECT or WITH. Logs are exposed through a `logs` table; filter by `source` (e.g. 'edge_logs', 'postgres_logs', 'function_logs', 'auth_logs', 'storage_logs', 'realtime_logs', 'workflow_run_logs') and read nested fields via `log_attributes['']`." + ), + iso_timestamp_start: z + .string() + .optional() + .describe( + 'The start of the log window as an ISO 8601 timestamp. The API caps the requested range at 24 hours.' + ), + iso_timestamp_end: z + .string() + .optional() + .describe( + 'The end of the log window as an ISO 8601 timestamp. The API caps the requested range at 24 hours.' + ), +}); + +const queryLogsOutputSchema = z.object({ + result: z.unknown(), +}); + const getAdvisorsInputSchema = z.object({ project_id: z.string(), type: z @@ -60,6 +85,19 @@ export const debuggingToolDefs = { openWorldHint: false, }, }, + query_logs: { + description: + "Runs a custom read-only ClickHouse SQL query against a Supabase project's unified logs stream. Use this when `get_logs` (service presets) is too coarse and you need to filter, aggregate, or join across log fields. Queries the last 24 hours by default; provide a custom iso_timestamp_start/iso_timestamp_end window up to 24 hours. Only SELECT/WITH queries are allowed. Do not poll this tool in a loop.", + parameters: queryLogsInputSchema, + outputSchema: queryLogsOutputSchema, + annotations: { + title: 'Query project logs', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, get_advisors: { description: "Gets a list of advisory notices for the Supabase project. Use this to check for security vulnerabilities or performance improvements. Include the remediation URL as a clickable link so that the user can reference the issue themselves. It's recommended to run this tool regularly, especially after making DDL changes to the database since it will catch things like missing RLS policies.", @@ -105,6 +143,29 @@ export function getDebuggingTools({ return { result: wrapWithUntrustedDataBoundary(result) }; }, }), + query_logs: injectableTool({ + ...debuggingToolDefs.query_logs, + inject: { project_id }, + execute: async ({ + project_id, + sql, + iso_timestamp_start, + iso_timestamp_end, + }) => { + const endTimestamp = new Date(); + const startTimestamp = new Date( + endTimestamp.getTime() - 24 * 60 * 60 * 1000 + ); // Last 24 hours + + const result = await debugging.queryLogs(project_id, { + sql, + iso_timestamp_start: + iso_timestamp_start ?? startTimestamp.toISOString(), + iso_timestamp_end: iso_timestamp_end ?? endTimestamp.toISOString(), + }); + return { result: wrapWithUntrustedDataBoundary(result) }; + }, + }), get_advisors: injectableTool({ ...debuggingToolDefs.get_advisors, inject: { project_id }, From 9bfe581c549395865115ecac8bb487bcaa807e1b Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Thu, 16 Jul 2026 16:26:06 +0200 Subject: [PATCH 02/12] chore: format logs.test.ts with biome --- packages/mcp-server-supabase/src/logs.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mcp-server-supabase/src/logs.test.ts b/packages/mcp-server-supabase/src/logs.test.ts index 3daad94..4aab675 100644 --- a/packages/mcp-server-supabase/src/logs.test.ts +++ b/packages/mcp-server-supabase/src/logs.test.ts @@ -64,8 +64,8 @@ describe('assertReadOnlyLogQuery', () => { }); test('rejects multiple statements', () => { - expect(() => - assertReadOnlyLogQuery('select 1; drop table logs') - ).toThrow(/single/); + expect(() => assertReadOnlyLogQuery('select 1; drop table logs')).toThrow( + /single/ + ); }); }); From 8de5d6fa18d792f60d522d271e9c212e327e25d7 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Thu, 16 Jul 2026 16:28:12 +0200 Subject: [PATCH 03/12] test: include query_logs in debugging feature group assertion --- packages/mcp-server-supabase/src/server.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server-supabase/src/server.test.ts b/packages/mcp-server-supabase/src/server.test.ts index d08564d..63cc838 100644 --- a/packages/mcp-server-supabase/src/server.test.ts +++ b/packages/mcp-server-supabase/src/server.test.ts @@ -3267,7 +3267,7 @@ describe('feature groups', () => { const { tools } = await client.listTools(); const toolNames = tools.map((tool) => tool.name); - expect(toolNames).toEqual(['get_logs', 'get_advisors']); + expect(toolNames).toEqual(['get_logs', 'query_logs', 'get_advisors']); }); test('development tools', async () => { From 52456a30c7c181ee7213eef78cd19a064e0f9412 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Thu, 16 Jul 2026 16:31:50 +0200 Subject: [PATCH 04/12] refactor: drop SELECT-only guard, backend enforces read-only --- packages/mcp-server-supabase/src/logs.test.ts | 33 +------------------ packages/mcp-server-supabase/src/logs.ts | 18 ---------- .../src/platform/api-platform.ts | 4 +-- .../src/tools/debugging-tools.ts | 4 +-- 4 files changed, 4 insertions(+), 55 deletions(-) diff --git a/packages/mcp-server-supabase/src/logs.test.ts b/packages/mcp-server-supabase/src/logs.test.ts index 4aab675..9855e20 100644 --- a/packages/mcp-server-supabase/src/logs.test.ts +++ b/packages/mcp-server-supabase/src/logs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { assertReadOnlyLogQuery, getClickHouseLogQuery } from './logs.js'; +import { getClickHouseLogQuery } from './logs.js'; import type { LogsService } from './platform/types.js'; const serviceSources = { @@ -38,34 +38,3 @@ describe('getClickHouseLogQuery', () => { expect(query).not.toContain("log_attributes['response.status_code']"); }); }); - -describe('assertReadOnlyLogQuery', () => { - test('allows SELECT and WITH queries, ignoring comments and casing', () => { - expect(() => - assertReadOnlyLogQuery('select id from logs limit 10') - ).not.toThrow(); - expect(() => - assertReadOnlyLogQuery(' WITH x as (select 1) select * from x ') - ).not.toThrow(); - expect(() => - assertReadOnlyLogQuery('-- comment\nselect id from logs;') - ).not.toThrow(); - }); - - test('rejects non-read statements', () => { - for (const sql of [ - "insert into logs values ('x')", - 'delete from logs', - 'drop table logs', - '/* select */ update logs set id = 1', - ]) { - expect(() => assertReadOnlyLogQuery(sql)).toThrow(/read-only/); - } - }); - - test('rejects multiple statements', () => { - expect(() => assertReadOnlyLogQuery('select 1; drop table logs')).toThrow( - /single/ - ); - }); -}); diff --git a/packages/mcp-server-supabase/src/logs.ts b/packages/mcp-server-supabase/src/logs.ts index 25c576a..8d449e6 100644 --- a/packages/mcp-server-supabase/src/logs.ts +++ b/packages/mcp-server-supabase/src/logs.ts @@ -1,24 +1,6 @@ import { stripIndent } from 'common-tags'; import type { LogsService } from './platform/types.js'; -export function assertReadOnlyLogQuery(sql: string) { - const stripped = sql - .replace(/--[^\n]*/g, '') - .replace(/\/\*[\s\S]*?\*\//g, '') - .trim() - .replace(/;\s*$/, ''); - - if (stripped.includes(';')) { - throw new Error('Only a single log query statement is allowed.'); - } - - if (!/^(select|with)\b/i.test(stripped)) { - throw new Error( - 'Only read-only log queries are allowed. The query must start with SELECT or WITH.' - ); - } -} - export function getClickHouseLogQuery( service: LogsService, limit: number = 100 diff --git a/packages/mcp-server-supabase/src/platform/api-platform.ts b/packages/mcp-server-supabase/src/platform/api-platform.ts index abf5aa2..628978a 100644 --- a/packages/mcp-server-supabase/src/platform/api-platform.ts +++ b/packages/mcp-server-supabase/src/platform/api-platform.ts @@ -6,7 +6,7 @@ import type { InitData } from '@supabase/mcp-utils'; import { fileURLToPath } from 'node:url'; import packageJson from '../../package.json' with { type: 'json' }; import { getDeploymentId, normalizeFilename } from '../edge-function.js'; -import { assertReadOnlyLogQuery, getClickHouseLogQuery } from '../logs.js'; +import { getClickHouseLogQuery } from '../logs.js'; import { assertProjectScopedSuccess, assertSuccess, @@ -283,8 +283,6 @@ export function createSupabaseApiPlatform( const { sql, iso_timestamp_start, iso_timestamp_end } = queryLogsOptionsSchema.parse(options); - assertReadOnlyLogQuery(sql); - const response = await managementApiClient.GET( '/v1/projects/{ref}/analytics/endpoints/logs', { diff --git a/packages/mcp-server-supabase/src/tools/debugging-tools.ts b/packages/mcp-server-supabase/src/tools/debugging-tools.ts index 0f9cf35..9b4e2ae 100644 --- a/packages/mcp-server-supabase/src/tools/debugging-tools.ts +++ b/packages/mcp-server-supabase/src/tools/debugging-tools.ts @@ -40,7 +40,7 @@ const queryLogsInputSchema = z.object({ sql: z .string() .describe( - "A read-only ClickHouse SQL query to run against the project's unified logs stream. Must start with SELECT or WITH. Logs are exposed through a `logs` table; filter by `source` (e.g. 'edge_logs', 'postgres_logs', 'function_logs', 'auth_logs', 'storage_logs', 'realtime_logs', 'workflow_run_logs') and read nested fields via `log_attributes['']`." + "A read-only ClickHouse SQL query to run against the project's unified logs stream. Logs are exposed through a `logs` table; filter by `source` (e.g. 'edge_logs', 'postgres_logs', 'function_logs', 'auth_logs', 'storage_logs', 'realtime_logs', 'workflow_run_logs') and read nested fields via `log_attributes['']`." ), iso_timestamp_start: z .string() @@ -87,7 +87,7 @@ export const debuggingToolDefs = { }, query_logs: { description: - "Runs a custom read-only ClickHouse SQL query against a Supabase project's unified logs stream. Use this when `get_logs` (service presets) is too coarse and you need to filter, aggregate, or join across log fields. Queries the last 24 hours by default; provide a custom iso_timestamp_start/iso_timestamp_end window up to 24 hours. Only SELECT/WITH queries are allowed. Do not poll this tool in a loop.", + "Runs a custom read-only ClickHouse SQL query against a Supabase project's unified logs stream. Use this when `get_logs` (service presets) is too coarse and you need to filter, aggregate, or join across log fields. Queries the last 24 hours by default; provide a custom iso_timestamp_start/iso_timestamp_end window up to 24 hours. Do not poll this tool in a loop.", parameters: queryLogsInputSchema, outputSchema: queryLogsOutputSchema, annotations: { From f37cfc5dd5119d9b3d116689fc27f3150895f593 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Fri, 17 Jul 2026 11:26:06 +0200 Subject: [PATCH 05/12] feat: address query_logs review feedback - 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 --- .../mcp-server-supabase/src/platform/types.ts | 2 +- .../mcp-server-supabase/src/server.test.ts | 125 ++++++++++++++++++ .../src/tools/debugging-tools.ts | 3 +- 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server-supabase/src/platform/types.ts b/packages/mcp-server-supabase/src/platform/types.ts index 49142ed..276e68a 100644 --- a/packages/mcp-server-supabase/src/platform/types.ts +++ b/packages/mcp-server-supabase/src/platform/types.ts @@ -149,7 +149,7 @@ export const getLogsOptionsSchema = z.object({ }); export const queryLogsOptionsSchema = z.object({ - sql: z.string(), + sql: z.string().min(1), iso_timestamp_start: z.string().optional(), iso_timestamp_end: z.string().optional(), }); diff --git a/packages/mcp-server-supabase/src/server.test.ts b/packages/mcp-server-supabase/src/server.test.ts index 63cc838..c26f6f9 100644 --- a/packages/mcp-server-supabase/src/server.test.ts +++ b/packages/mcp-server-supabase/src/server.test.ts @@ -1617,6 +1617,131 @@ describe('tools', () => { ); }); + test('query logs forwards custom sql and defaults the timestamp window', async () => { + const { callTool } = await setup(); + + const org = await createOrganization({ + name: 'My Org', + plan: 'free', + allowed_release_channels: ['ga'], + }); + + const project = await createProject({ + name: 'Project 1', + region: 'us-east-1', + organization_id: org.id, + }); + project.status = 'ACTIVE_HEALTHY'; + + const capturedSearchParams: URLSearchParams[] = []; + + mockServer?.use( + http.get<{ projectId: string }>( + `${API_URL}/v1/projects/:projectId/analytics/endpoints/logs`, + ({ params, request }) => { + expect(params.projectId).toBe(project.id); + capturedSearchParams.push(new URL(request.url).searchParams); + + return HttpResponse.json([]); + } + ) + ); + + const sql = + "select id, timestamp, event_message from logs where source = 'postgres_logs' order by timestamp desc limit 10"; + + const { result } = await callTool({ + name: 'query_logs', + arguments: { + project_id: project.id, + sql, + }, + }); + + expect(result).toContain('untrusted-data'); + expect(capturedSearchParams).toHaveLength(1); + expect(capturedSearchParams[0]?.get('sql')).toBe(sql); + expect(capturedSearchParams[0]?.get('iso_timestamp_start')).toBeTruthy(); + expect(capturedSearchParams[0]?.get('iso_timestamp_end')).toBeTruthy(); + }); + + test('query logs forwards a custom timestamp window', async () => { + const { callTool } = await setup(); + + const org = await createOrganization({ + name: 'My Org', + plan: 'free', + allowed_release_channels: ['ga'], + }); + + const project = await createProject({ + name: 'Project 1', + region: 'us-east-1', + organization_id: org.id, + }); + project.status = 'ACTIVE_HEALTHY'; + + const capturedSearchParams: URLSearchParams[] = []; + + mockServer?.use( + http.get<{ projectId: string }>( + `${API_URL}/v1/projects/:projectId/analytics/endpoints/logs`, + ({ request }) => { + capturedSearchParams.push(new URL(request.url).searchParams); + return HttpResponse.json([]); + } + ) + ); + + const isoTimestampStart = '2024-02-01T10:00:00.000Z'; + const isoTimestampEnd = '2024-02-01T11:00:00.000Z'; + + await callTool({ + name: 'query_logs', + arguments: { + project_id: project.id, + sql: 'select id from logs limit 1', + iso_timestamp_start: isoTimestampStart, + iso_timestamp_end: isoTimestampEnd, + }, + }); + + expect(capturedSearchParams).toHaveLength(1); + expect(capturedSearchParams[0]?.get('iso_timestamp_start')).toBe( + isoTimestampStart + ); + expect(capturedSearchParams[0]?.get('iso_timestamp_end')).toBe( + isoTimestampEnd + ); + }); + + test('query logs rejects an empty sql query', async () => { + const { callTool } = await setup(); + + const org = await createOrganization({ + name: 'My Org', + plan: 'free', + allowed_release_channels: ['ga'], + }); + + const project = await createProject({ + name: 'Project 1', + region: 'us-east-1', + organization_id: org.id, + }); + project.status = 'ACTIVE_HEALTHY'; + + await expect( + callTool({ + name: 'query_logs', + arguments: { + project_id: project.id, + sql: '', + }, + }) + ).rejects.toThrow(); + }); + test('get security advisors', async () => { const { callTool } = await setup(); diff --git a/packages/mcp-server-supabase/src/tools/debugging-tools.ts b/packages/mcp-server-supabase/src/tools/debugging-tools.ts index 9b4e2ae..c9f9194 100644 --- a/packages/mcp-server-supabase/src/tools/debugging-tools.ts +++ b/packages/mcp-server-supabase/src/tools/debugging-tools.ts @@ -39,8 +39,9 @@ const queryLogsInputSchema = z.object({ project_id: z.string(), sql: z .string() + .min(1) .describe( - "A read-only ClickHouse SQL query to run against the project's unified logs stream. Logs are exposed through a `logs` table; filter by `source` (e.g. 'edge_logs', 'postgres_logs', 'function_logs', 'auth_logs', 'storage_logs', 'realtime_logs', 'workflow_run_logs') and read nested fields via `log_attributes['']`." + "A read-only ClickHouse SQL query to run against the project's unified logs stream. Logs are exposed through a `logs` table; filter by `source` (e.g. 'edge_logs', 'postgres_logs', 'function_edge_logs', 'function_logs', 'auth_logs', 'storage_logs', 'realtime_logs', 'workflow_run_logs') and read nested fields via `log_attributes['']`." ), iso_timestamp_start: z .string() From aec7c4b4da6e25bca3f68c0f239f9bf3ac92e3e2 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Fri, 17 Jul 2026 11:27:35 +0200 Subject: [PATCH 06/12] chore: regenerate management API types --- .../src/management-api/types.ts | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/mcp-server-supabase/src/management-api/types.ts b/packages/mcp-server-supabase/src/management-api/types.ts index e27d76b..71c52b2 100644 --- a/packages/mcp-server-supabase/src/management-api/types.ts +++ b/packages/mcp-server-supabase/src/management-api/types.ts @@ -2940,13 +2940,18 @@ export interface components { /** @enum {string} */ status: "stored" | "applied"; }; + /** @example { + * "root_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + * } */ PgsodiumConfigResponse: { + /** @description The pgsodium root key: 32 bytes, hex-encoded (64 characters). */ root_key: string; }; /** @example { - * "root_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=" + * "root_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" * } */ UpdatePgsodiumConfigBody: { + /** @description The pgsodium root key: 32 bytes, hex-encoded (64 characters). */ root_key: string; }; PostgrestConfigWithJWTSecretResponse: { @@ -3039,6 +3044,22 @@ export interface components { status: "not-used" | "custom-domain-used" | "active"; custom_domain?: string; }; + PlanGateErrorBody: { + /** @description Human-readable explanation of the plan gate */ + message: string; + /** @description Present on entitlement denials. Other errors with this status code (validation, billing state) carry only message. */ + error?: { + /** + * @description Machine-readable marker for plan-gated denials + * @enum {string} + */ + code: "entitlement_required"; + /** @description Entitlement feature key that failed the check */ + feature: string; + /** @description Billing page URL for the organization, present when the org is resolvable */ + upgrade_url?: string; + }; + }; /** @example { * "vanity_subdomain": "acme-prod" * } */ @@ -7940,7 +7961,9 @@ export interface operations { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["PlanGateErrorBody"]; + }; }; /** @description Unauthorized */ 401: { @@ -8049,7 +8072,9 @@ export interface operations { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["PlanGateErrorBody"]; + }; }; /** @description Unauthorized */ 401: { @@ -8110,7 +8135,9 @@ export interface operations { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["PlanGateErrorBody"]; + }; }; /** @description Unauthorized */ 401: { @@ -8430,7 +8457,9 @@ export interface operations { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["PlanGateErrorBody"]; + }; }; /** @description Forbidden action */ 403: { @@ -12909,7 +12938,9 @@ export interface operations { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["PlanGateErrorBody"]; + }; }; /** @description Forbidden action */ 403: { @@ -12984,7 +13015,9 @@ export interface operations { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["PlanGateErrorBody"]; + }; }; /** @description Forbidden action */ 403: { From a4d72283104e9a9675c65372ad32510733a47966 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Tue, 21 Jul 2026 11:15:38 +0200 Subject: [PATCH 07/12] docs: deprecate get_logs for query_logs, note ClickHouse availability - 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 --- packages/mcp-server-supabase/src/tools/debugging-tools.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server-supabase/src/tools/debugging-tools.ts b/packages/mcp-server-supabase/src/tools/debugging-tools.ts index c9f9194..766246b 100644 --- a/packages/mcp-server-supabase/src/tools/debugging-tools.ts +++ b/packages/mcp-server-supabase/src/tools/debugging-tools.ts @@ -75,7 +75,7 @@ const getAdvisorsOutputSchema = z.object({ export const debuggingToolDefs = { get_logs: { description: - 'Gets logs for a Supabase project by service type. Each call returns logs from the last 24 hours by default. Provide a custom iso_timestamp_start/iso_timestamp_end window up to 24 hours. Edge Function logs are split by kind: `edge-function` returns invocation/request logs, while `edge-function-runtime` returns console output from inside the function. Query one service first, then correlate with other services by timestamp or error anchors. Do not poll get_logs in a loop.', + 'Gets logs for a Supabase project by service type. Each call returns logs from the last 24 hours by default. Provide a custom iso_timestamp_start/iso_timestamp_end window up to 24 hours. Edge Function logs are split by kind: `edge-function` returns invocation/request logs, while `edge-function-runtime` returns console output from inside the function. Query one service first, then correlate with other services by timestamp or error anchors. Do not poll get_logs in a loop. Deprecated on hosted (production) projects in favour of `query_logs`, which supports custom ClickHouse queries; prefer `query_logs` there. Continue using `get_logs` on local (CLI) and self-hosted projects until ClickHouse-backed querying is available for them.', parameters: getLogsInputSchema, outputSchema: getLogsOutputSchema, annotations: { @@ -88,7 +88,7 @@ export const debuggingToolDefs = { }, query_logs: { description: - "Runs a custom read-only ClickHouse SQL query against a Supabase project's unified logs stream. Use this when `get_logs` (service presets) is too coarse and you need to filter, aggregate, or join across log fields. Queries the last 24 hours by default; provide a custom iso_timestamp_start/iso_timestamp_end window up to 24 hours. Do not poll this tool in a loop.", + "Runs a custom read-only ClickHouse SQL query against a Supabase project's unified logs stream. Use this when `get_logs` (service presets) is too coarse and you need to filter, aggregate, or join across log fields. Queries the last 24 hours by default; provide a custom iso_timestamp_start/iso_timestamp_end window up to 24 hours. Do not poll this tool in a loop. Only available on hosted (production) Supabase projects; ClickHouse-backed querying is not yet available for local (CLI) or self-hosted projects, where you should use `get_logs` instead.", parameters: queryLogsInputSchema, outputSchema: queryLogsOutputSchema, annotations: { From ecc456dd1cdb96b1d364b6511002d0c3809210a7 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Tue, 21 Jul 2026 11:17:51 +0200 Subject: [PATCH 08/12] chore: regenerate management API types --- .../mcp-server-supabase/src/management-api/types.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/mcp-server-supabase/src/management-api/types.ts b/packages/mcp-server-supabase/src/management-api/types.ts index 71c52b2..2103670 100644 --- a/packages/mcp-server-supabase/src/management-api/types.ts +++ b/packages/mcp-server-supabase/src/management-api/types.ts @@ -4801,7 +4801,6 @@ export interface components { CreateProviderResponse: { id: string; saml?: { - id: string; entity_id: string; metadata_url?: string; metadata_xml?: string; @@ -4819,7 +4818,6 @@ export interface components { name_id_format?: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" | "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" | "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" | "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"; }; domains?: { - id: string; domain?: string; created_at?: string; updated_at?: string; @@ -4831,7 +4829,6 @@ export interface components { items: { id: string; saml?: { - id: string; entity_id: string; metadata_url?: string; metadata_xml?: string; @@ -4849,7 +4846,6 @@ export interface components { name_id_format?: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" | "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" | "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" | "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"; }; domains?: { - id: string; domain?: string; created_at?: string; updated_at?: string; @@ -4861,7 +4857,6 @@ export interface components { GetProviderResponse: { id: string; saml?: { - id: string; entity_id: string; metadata_url?: string; metadata_xml?: string; @@ -4879,7 +4874,6 @@ export interface components { name_id_format?: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" | "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" | "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" | "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"; }; domains?: { - id: string; domain?: string; created_at?: string; updated_at?: string; @@ -4914,7 +4908,6 @@ export interface components { UpdateProviderResponse: { id: string; saml?: { - id: string; entity_id: string; metadata_url?: string; metadata_xml?: string; @@ -4932,7 +4925,6 @@ export interface components { name_id_format?: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" | "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" | "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" | "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"; }; domains?: { - id: string; domain?: string; created_at?: string; updated_at?: string; @@ -4943,7 +4935,6 @@ export interface components { DeleteProviderResponse: { id: string; saml?: { - id: string; entity_id: string; metadata_url?: string; metadata_xml?: string; @@ -4961,7 +4952,6 @@ export interface components { name_id_format?: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" | "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" | "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" | "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"; }; domains?: { - id: string; domain?: string; created_at?: string; updated_at?: string; From ea6d1639d4992965bfb35f9b2d220f271ecc67a0 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Tue, 21 Jul 2026 12:02:52 +0200 Subject: [PATCH 09/12] fix: make time-range guidance directive in log tool descriptions 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. --- packages/mcp-server-supabase/src/tools/debugging-tools.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server-supabase/src/tools/debugging-tools.ts b/packages/mcp-server-supabase/src/tools/debugging-tools.ts index 766246b..230df35 100644 --- a/packages/mcp-server-supabase/src/tools/debugging-tools.ts +++ b/packages/mcp-server-supabase/src/tools/debugging-tools.ts @@ -75,7 +75,7 @@ const getAdvisorsOutputSchema = z.object({ export const debuggingToolDefs = { get_logs: { description: - 'Gets logs for a Supabase project by service type. Each call returns logs from the last 24 hours by default. Provide a custom iso_timestamp_start/iso_timestamp_end window up to 24 hours. Edge Function logs are split by kind: `edge-function` returns invocation/request logs, while `edge-function-runtime` returns console output from inside the function. Query one service first, then correlate with other services by timestamp or error anchors. Do not poll get_logs in a loop. Deprecated on hosted (production) projects in favour of `query_logs`, which supports custom ClickHouse queries; prefer `query_logs` there. Continue using `get_logs` on local (CLI) and self-hosted projects until ClickHouse-backed querying is available for them.', + 'Gets logs for a Supabase project by service type. When the user asks about a specific time range, always pass iso_timestamp_start and iso_timestamp_end to match it; otherwise each call defaults to the last 24 hours and will return logs from a wider window than intended. The window can be up to 24 hours. Edge Function logs are split by kind: `edge-function` returns invocation/request logs, while `edge-function-runtime` returns console output from inside the function. Query one service first, then correlate with other services by timestamp or error anchors. Do not poll get_logs in a loop. Deprecated on hosted (production) projects in favour of `query_logs`, which supports custom ClickHouse queries; prefer `query_logs` there. Continue using `get_logs` on local (CLI) and self-hosted projects until ClickHouse-backed querying is available for them.', parameters: getLogsInputSchema, outputSchema: getLogsOutputSchema, annotations: { @@ -88,7 +88,7 @@ export const debuggingToolDefs = { }, query_logs: { description: - "Runs a custom read-only ClickHouse SQL query against a Supabase project's unified logs stream. Use this when `get_logs` (service presets) is too coarse and you need to filter, aggregate, or join across log fields. Queries the last 24 hours by default; provide a custom iso_timestamp_start/iso_timestamp_end window up to 24 hours. Do not poll this tool in a loop. Only available on hosted (production) Supabase projects; ClickHouse-backed querying is not yet available for local (CLI) or self-hosted projects, where you should use `get_logs` instead.", + "Runs a custom read-only ClickHouse SQL query against a Supabase project's unified logs stream. Use this when `get_logs` (service presets) is too coarse and you need to filter, aggregate, or join across log fields. 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. The window can be up to 24 hours. Do not poll this tool in a loop. Only available on hosted (production) Supabase projects; ClickHouse-backed querying is not yet available for local (CLI) or self-hosted projects, where you should use `get_logs` instead.", parameters: queryLogsInputSchema, outputSchema: queryLogsOutputSchema, annotations: { From b36a7541fd622bb8a4f9aa02f0533f00c6cffe2f Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Tue, 21 Jul 2026 12:21:59 +0200 Subject: [PATCH 10/12] docs: state timestamp defaults in log tool descriptions --- packages/mcp-server-supabase/src/tools/debugging-tools.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/mcp-server-supabase/src/tools/debugging-tools.ts b/packages/mcp-server-supabase/src/tools/debugging-tools.ts index 230df35..1397b73 100644 --- a/packages/mcp-server-supabase/src/tools/debugging-tools.ts +++ b/packages/mcp-server-supabase/src/tools/debugging-tools.ts @@ -21,13 +21,13 @@ const getLogsInputSchema = z.object({ .string() .optional() .describe( - 'The start of the log window as an ISO 8601 timestamp. The API caps the requested range at 24 hours.' + 'The start of the log window as an ISO 8601 timestamp. Defaults to 24 hours before the end of the window. The API caps the requested range at 24 hours.' ), iso_timestamp_end: z .string() .optional() .describe( - 'The end of the log window as an ISO 8601 timestamp. The API caps the requested range at 24 hours.' + 'The end of the log window as an ISO 8601 timestamp. Defaults to the current time. The API caps the requested range at 24 hours.' ), }); @@ -47,13 +47,13 @@ const queryLogsInputSchema = z.object({ .string() .optional() .describe( - 'The start of the log window as an ISO 8601 timestamp. The API caps the requested range at 24 hours.' + 'The start of the log window as an ISO 8601 timestamp. Defaults to 24 hours before the end of the window. The API caps the requested range at 24 hours.' ), iso_timestamp_end: z .string() .optional() .describe( - 'The end of the log window as an ISO 8601 timestamp. The API caps the requested range at 24 hours.' + 'The end of the log window as an ISO 8601 timestamp. Defaults to the current time. The API caps the requested range at 24 hours.' ), }); From d9ddd0b7a7689cb6a9e4fb1fc3dd5de7e5492b89 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Tue, 21 Jul 2026 12:56:22 +0200 Subject: [PATCH 11/12] docs: clarify query_logs vs get_logs availability in description --- packages/mcp-server-supabase/src/tools/debugging-tools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server-supabase/src/tools/debugging-tools.ts b/packages/mcp-server-supabase/src/tools/debugging-tools.ts index 1397b73..f7ee1b4 100644 --- a/packages/mcp-server-supabase/src/tools/debugging-tools.ts +++ b/packages/mcp-server-supabase/src/tools/debugging-tools.ts @@ -88,7 +88,7 @@ export const debuggingToolDefs = { }, query_logs: { description: - "Runs a custom read-only ClickHouse SQL query against a Supabase project's unified logs stream. Use this when `get_logs` (service presets) is too coarse and you need to filter, aggregate, or join across log fields. 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. The window can be up to 24 hours. Do not poll this tool in a loop. Only available on hosted (production) Supabase projects; ClickHouse-backed querying is not yet available for local (CLI) or self-hosted projects, where you should use `get_logs` instead.", + "Runs a custom read-only ClickHouse SQL query against a Supabase project's unified logs stream, for filtering, aggregating, or joining across log fields more precisely than the `get_logs` service presets allow. Only works on hosted (production) Supabase projects: on hosted projects, prefer this over `get_logs` whenever you need more than a simple per-service log dump. On local (CLI) and self-hosted projects this query will fail because ClickHouse-backed querying is not yet supported there, so use `get_logs` instead even when its presets are coarser — it is the only logs tool that works on those projects. 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. The window can be up to 24 hours. Do not poll this tool in a loop.", parameters: queryLogsInputSchema, outputSchema: queryLogsOutputSchema, annotations: { From c1bdc7f07e2ce81618fc8e1c5252e3ac2a53fa64 Mon Sep 17 00:00:00 2001 From: Jordi Enric Date: Tue, 21 Jul 2026 15:44:06 +0200 Subject: [PATCH 12/12] fix: anchor default log window start to the supplied end 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. --- .../mcp-server-supabase/src/server.test.ts | 51 +++++++++++++++++++ .../src/tools/debugging-tools.ts | 31 ++++++----- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/packages/mcp-server-supabase/src/server.test.ts b/packages/mcp-server-supabase/src/server.test.ts index c26f6f9..026ee5b 100644 --- a/packages/mcp-server-supabase/src/server.test.ts +++ b/packages/mcp-server-supabase/src/server.test.ts @@ -1715,6 +1715,57 @@ describe('tools', () => { ); }); + test('query logs anchors the default start to a supplied end', async () => { + const { callTool } = await setup(); + + const org = await createOrganization({ + name: 'My Org', + plan: 'free', + allowed_release_channels: ['ga'], + }); + + const project = await createProject({ + name: 'Project 1', + region: 'us-east-1', + organization_id: org.id, + }); + project.status = 'ACTIVE_HEALTHY'; + + const capturedSearchParams: URLSearchParams[] = []; + + mockServer?.use( + http.get<{ projectId: string }>( + `${API_URL}/v1/projects/:projectId/analytics/endpoints/logs`, + ({ request }) => { + capturedSearchParams.push(new URL(request.url).searchParams); + return HttpResponse.json([]); + } + ) + ); + + const isoTimestampEnd = '2024-02-01T11:00:00.000Z'; + + await callTool({ + name: 'query_logs', + arguments: { + project_id: project.id, + sql: 'select id from logs limit 1', + iso_timestamp_end: isoTimestampEnd, + }, + }); + + expect(capturedSearchParams).toHaveLength(1); + expect(capturedSearchParams[0]?.get('iso_timestamp_end')).toBe( + isoTimestampEnd + ); + const expectedStart = new Date( + new Date(isoTimestampEnd).getTime() - 24 * 60 * 60 * 1000 + ).toISOString(); + expect(capturedSearchParams[0]?.get('iso_timestamp_start')).toBe( + expectedStart + ); + }); + test('query logs rejects an empty sql query', async () => { const { callTool } = await setup(); diff --git a/packages/mcp-server-supabase/src/tools/debugging-tools.ts b/packages/mcp-server-supabase/src/tools/debugging-tools.ts index f7ee1b4..8db4e98 100644 --- a/packages/mcp-server-supabase/src/tools/debugging-tools.ts +++ b/packages/mcp-server-supabase/src/tools/debugging-tools.ts @@ -114,6 +114,19 @@ export const debuggingToolDefs = { }, } as const satisfies ToolDefs; +const DAY_MS = 24 * 60 * 60 * 1000; + +function resolveLogWindow( + iso_timestamp_start?: string, + iso_timestamp_end?: string +) { + const end = iso_timestamp_end ?? new Date().toISOString(); + const start = + iso_timestamp_start ?? + new Date(new Date(end).getTime() - DAY_MS).toISOString(); + return { iso_timestamp_start: start, iso_timestamp_end: end }; +} + export function getDebuggingTools({ debugging, projectId, @@ -130,16 +143,9 @@ export function getDebuggingTools({ iso_timestamp_start, iso_timestamp_end, }) => { - const endTimestamp = new Date(); - const startTimestamp = new Date( - endTimestamp.getTime() - 24 * 60 * 60 * 1000 - ); // Last 24 hours - const result = await debugging.getLogs(project_id, { service, - iso_timestamp_start: - iso_timestamp_start ?? startTimestamp.toISOString(), - iso_timestamp_end: iso_timestamp_end ?? endTimestamp.toISOString(), + ...resolveLogWindow(iso_timestamp_start, iso_timestamp_end), }); return { result: wrapWithUntrustedDataBoundary(result) }; }, @@ -153,16 +159,9 @@ export function getDebuggingTools({ iso_timestamp_start, iso_timestamp_end, }) => { - const endTimestamp = new Date(); - const startTimestamp = new Date( - endTimestamp.getTime() - 24 * 60 * 60 * 1000 - ); // Last 24 hours - const result = await debugging.queryLogs(project_id, { sql, - iso_timestamp_start: - iso_timestamp_start ?? startTimestamp.toISOString(), - iso_timestamp_end: iso_timestamp_end ?? endTimestamp.toISOString(), + ...resolveLogWindow(iso_timestamp_start, iso_timestamp_end), }); return { result: wrapWithUntrustedDataBoundary(result) }; },