Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/mcp-server-supabase/src/logs.test.ts
Original file line number Diff line number Diff line change
@@ -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%'");
});
});
89 changes: 81 additions & 8 deletions packages/mcp-server-supabase/src/logs.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp-server-supabase/src/platform/api-platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions packages/mcp-server-supabase/src/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
36 changes: 33 additions & 3 deletions packages/mcp-server-supabase/src/tools/debugging-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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: {
Expand Down Expand Up @@ -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 };
},
Expand Down