diff --git a/packages/mcp-server-supabase/src/logs.test.ts b/packages/mcp-server-supabase/src/logs.test.ts new file mode 100644 index 0000000..899a13d --- /dev/null +++ b/packages/mcp-server-supabase/src/logs.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from 'vitest'; +import { getLogQuery } from './logs.js'; + +describe('getLogQuery', () => { + test('keeps the existing numeric limit overload', () => { + const query = getLogQuery('edge-function', 25); + + expect(query).toContain('from function_edge_logs'); + expect(query).toContain('limit 25'); + }); + + test('adds a service-specific text filter', () => { + const query = getLogQuery('edge-function', { + limit: 50, + search: 'planner', + }); + + expect(query).toContain('where ('); + expect(query).toContain("event_message ilike '%planner%'"); + expect(query).toContain("m.function_id ilike '%planner%'"); + expect(query).toContain("request.path ilike '%planner%'"); + expect(query).toContain('limit 50'); + }); + + test('escapes single quotes in text filters', () => { + const query = getLogQuery('api', { + search: "worker's path", + }); + + expect(query).toContain("event_message ilike '%worker''s path%'"); + }); +}); diff --git a/packages/mcp-server-supabase/src/logs.ts b/packages/mcp-server-supabase/src/logs.ts index a9ea852..60495ec 100644 --- a/packages/mcp-server-supabase/src/logs.ts +++ b/packages/mcp-server-supabase/src/logs.ts @@ -1,60 +1,133 @@ import { stripIndent } from 'common-tags'; import type { LogsService } from './platform/types.js'; -export function getLogQuery(service: LogsService, limit: number = 100) { +type LogQueryOptions = { + limit?: number; + search?: string; +}; + +function sqlString(value: string) { + return value.replaceAll("'", "''"); +} + +function buildSearchFilter(search: string | undefined, columns: string[]) { + if (!search) return ''; + + const pattern = sqlString(`%${search}%`); + return `where (${columns + .map((column) => `${column} ilike '${pattern}'`) + .join(' or ')})`; +} + +export function getLogQuery( + service: LogsService, + options: LogQueryOptions | number = {} +) { + const { limit = 100, search } = + typeof options === 'number' ? { limit: options } : options; + switch (service) { - case 'api': + case 'api': { + const searchFilter = buildSearchFilter(search, [ + 'event_message', + 'identifier', + 'request.method', + 'request.path', + 'cast(response.status_code as text)', + ]); return stripIndent` select id, identifier, timestamp, event_message, request.method, request.path, response.status_code from edge_logs cross join unnest(metadata) as m cross join unnest(m.request) as request cross join unnest(m.response) as response + ${searchFilter} order by timestamp desc limit ${limit} `; - case 'branch-action': + } + case 'branch-action': { + const searchFilter = buildSearchFilter(search, [ + 'event_message', + 'workflow_run', + ]); return stripIndent` select workflow_run, workflow_run_logs.timestamp, id, event_message from workflow_run_logs + ${searchFilter} order by timestamp desc limit ${limit} `; - case 'postgres': + } + case 'postgres': { + const searchFilter = buildSearchFilter(search, [ + 'event_message', + 'identifier', + 'parsed.error_severity', + ]); return stripIndent` select identifier, postgres_logs.timestamp, id, event_message, parsed.error_severity from postgres_logs cross join unnest(metadata) as m cross join unnest(m.parsed) as parsed + ${searchFilter} order by timestamp desc limit ${limit} `; - case 'edge-function': + } + case 'edge-function': { + const searchFilter = buildSearchFilter(search, [ + 'event_message', + 'm.function_id', + 'request.method', + 'request.path', + 'm.deployment_id', + 'm.version', + 'cast(response.status_code as text)', + ]); return stripIndent` select id, function_edge_logs.timestamp, event_message, response.status_code, request.method, m.function_id, m.execution_time_ms, m.deployment_id, m.version from function_edge_logs cross join unnest(metadata) as m cross join unnest(m.response) as response cross join unnest(m.request) as request + ${searchFilter} order by timestamp desc limit ${limit} `; - case 'auth': + } + case 'auth': { + const searchFilter = buildSearchFilter(search, [ + 'event_message', + 'metadata.level', + 'metadata.status', + 'metadata.path', + 'metadata.msg', + 'metadata.error', + ]); return stripIndent` select id, auth_logs.timestamp, event_message, metadata.level, metadata.status, metadata.path, metadata.msg as msg, metadata.error from auth_logs cross join unnest(metadata) as metadata + ${searchFilter} order by timestamp desc limit ${limit} `; - case 'storage': + } + case 'storage': { + const searchFilter = buildSearchFilter(search, ['event_message', 'id']); return stripIndent` select id, storage_logs.timestamp, event_message from storage_logs + ${searchFilter} order by timestamp desc limit ${limit} `; - case 'realtime': + } + case 'realtime': { + const searchFilter = buildSearchFilter(search, ['event_message', 'id']); return stripIndent` select id, realtime_logs.timestamp, event_message from realtime_logs + ${searchFilter} order by timestamp desc limit ${limit} `; + } default: throw new Error(`unsupported log service type: ${service}`); } diff --git a/packages/mcp-server-supabase/src/platform/api-platform.ts b/packages/mcp-server-supabase/src/platform/api-platform.ts index fef901c..0bdaba8 100644 --- a/packages/mcp-server-supabase/src/platform/api-platform.ts +++ b/packages/mcp-server-supabase/src/platform/api-platform.ts @@ -239,10 +239,10 @@ export function createSupabaseApiPlatform( const debugging: DebuggingOperations = { async getLogs(projectId: string, options: GetLogsOptions) { - const { service, iso_timestamp_start, iso_timestamp_end } = + const { service, iso_timestamp_start, iso_timestamp_end, limit, search } = getLogsOptionsSchema.parse(options); - const sql = getLogQuery(service); + const sql = getLogQuery(service, { limit, search }); const response = await managementApiClient.GET( '/v1/projects/{ref}/analytics/endpoints/logs.all', diff --git a/packages/mcp-server-supabase/src/platform/types.ts b/packages/mcp-server-supabase/src/platform/types.ts index 0c36d1f..a1caf29 100644 --- a/packages/mcp-server-supabase/src/platform/types.ts +++ b/packages/mcp-server-supabase/src/platform/types.ts @@ -145,6 +145,8 @@ export const getLogsOptionsSchema = z.object({ service: logsServiceSchema, iso_timestamp_start: z.string().optional(), iso_timestamp_end: z.string().optional(), + limit: z.number().int().min(1).max(1000).optional(), + search: z.string().min(1).max(200).optional(), }); export const generateTypescriptTypesResultSchema = z.object({ diff --git a/packages/mcp-server-supabase/src/tools/debugging-tools.ts b/packages/mcp-server-supabase/src/tools/debugging-tools.ts index ac0b142..819c13c 100644 --- a/packages/mcp-server-supabase/src/tools/debugging-tools.ts +++ b/packages/mcp-server-supabase/src/tools/debugging-tools.ts @@ -13,6 +13,32 @@ type DebuggingToolsOptions = { const getLogsInputSchema = z.object({ project_id: z.string(), service: logsServiceSchema.describe('The service to fetch logs for'), + minutes: z + .number() + .int() + .min(1) + .max(24 * 60) + .optional() + .describe( + 'Optional lookback window in minutes. Defaults to 1440 minutes (24 hours).' + ), + limit: z + .number() + .int() + .min(1) + .max(1000) + .optional() + .describe( + 'Optional maximum number of log rows to return. Defaults to 100.' + ), + search: z + .string() + .min(1) + .max(200) + .optional() + .describe( + 'Optional case-insensitive text filter matched against log messages and service-specific fields.' + ), }); const getLogsOutputSchema = z.object({ @@ -33,7 +59,7 @@ const getAdvisorsOutputSchema = z.object({ export const debuggingToolDefs = { get_logs: { description: - 'Gets logs for a Supabase project by service type. Use this to help debug problems with your app. This will return logs within the last 24 hours.', + 'Gets logs for a Supabase project by service type. Use this to help debug problems with your app. Supports narrowing the lookback window, row limit, and text search to reduce noisy log output.', parameters: getLogsInputSchema, outputSchema: getLogsOutputSchema, annotations: { @@ -69,14 +95,18 @@ export function getDebuggingTools({ get_logs: injectableTool({ ...debuggingToolDefs.get_logs, inject: { project_id }, - execute: async ({ project_id, service }) => { - const startTimestamp = new Date(Date.now() - 24 * 60 * 60 * 1000); // Last 24 hours + execute: async ({ project_id, service, minutes, limit, search }) => { + const startTimestamp = new Date( + Date.now() - (minutes ?? 24 * 60) * 60 * 1000 + ); const endTimestamp = new Date(); const result = await debugging.getLogs(project_id, { service, iso_timestamp_start: startTimestamp.toISOString(), iso_timestamp_end: endTimestamp.toISOString(), + limit, + search, }); return { result }; },