feat: migrate to docker-compose with selfhosted-supabase-mcp
- Replace legacy directory structure (api/, db/, functions/, logs/, pooler/) with single docker-compose.yml based self-hosted setup - Add selfhosted-supabase-mcp TypeScript MCP server for database management - Add .dockerignore for Docker build context - Update .gitignore to exclude .env files, volumes/, backups, logs
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* get_cron_job_history - Gets execution history for pg_cron jobs.
|
||||
*
|
||||
* Requires the pg_cron extension to be installed.
|
||||
* Shows recent job runs with status and timing information.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { handleSqlResponse, executeSqlWithFallback } from './utils.js';
|
||||
import type { ToolContext, ToolPrivilegeLevel } from './types.js';
|
||||
|
||||
const CronJobRunSchema = z.object({
|
||||
jobid: z.number(),
|
||||
jobname: z.string().nullable(),
|
||||
schedule: z.string(),
|
||||
runid: z.number().nullable(),
|
||||
status: z.string().nullable(),
|
||||
start_time: z.string().nullable(),
|
||||
end_time: z.string().nullable(),
|
||||
return_message: z.string().nullable(),
|
||||
});
|
||||
|
||||
const GetCronJobHistoryOutputSchema = z.array(CronJobRunSchema);
|
||||
|
||||
const GetCronJobHistoryInputSchema = z.object({
|
||||
jobid: z.number().optional().describe('Filter by specific job ID.'),
|
||||
jobname: z.string().optional().describe('Filter by job name pattern.'),
|
||||
limit: z.number().optional().default(100).describe('Maximum number of records to return.'),
|
||||
status: z.enum(['succeeded', 'failed']).optional().describe('Filter by execution status.'),
|
||||
});
|
||||
|
||||
type GetCronJobHistoryInput = z.infer<typeof GetCronJobHistoryInputSchema>;
|
||||
|
||||
const mcpInputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
jobid: {
|
||||
type: 'number',
|
||||
description: 'Filter by specific job ID.',
|
||||
},
|
||||
jobname: {
|
||||
type: 'string',
|
||||
description: 'Filter by job name pattern.',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of records to return.',
|
||||
default: 100,
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['succeeded', 'failed'],
|
||||
description: 'Filter by execution status.',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
};
|
||||
|
||||
// Safe pattern for job name search - alphanumeric, underscore, hyphen, space
|
||||
const jobNamePattern = /^[a-zA-Z0-9_\-\s]+$/;
|
||||
|
||||
export const getCronJobHistoryTool = {
|
||||
name: 'get_cron_job_history',
|
||||
description: 'Gets execution history for pg_cron jobs including status and timing. Requires pg_cron to be installed.',
|
||||
privilegeLevel: 'regular' as ToolPrivilegeLevel,
|
||||
inputSchema: GetCronJobHistoryInputSchema,
|
||||
mcpInputSchema: mcpInputSchema,
|
||||
outputSchema: GetCronJobHistoryOutputSchema,
|
||||
|
||||
execute: async (input: GetCronJobHistoryInput, context: ToolContext) => {
|
||||
const client = context.selfhostedClient;
|
||||
const { jobid, jobname, limit = 100, status } = input;
|
||||
|
||||
// Validate jobname if provided
|
||||
if (jobname && !jobNamePattern.test(jobname)) {
|
||||
throw new Error(`Invalid job name pattern: ${jobname}. Use only alphanumeric, underscore, hyphen, and space characters.`);
|
||||
}
|
||||
|
||||
// First check if pg_cron extension is installed
|
||||
const checkExtensionSql = `
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM pg_extension WHERE extname = 'pg_cron'
|
||||
) as installed;
|
||||
`;
|
||||
|
||||
const extensionCheck = await executeSqlWithFallback(client, checkExtensionSql, true);
|
||||
|
||||
if (!Array.isArray(extensionCheck) || extensionCheck.length === 0) {
|
||||
throw new Error('Failed to check pg_cron extension status.');
|
||||
}
|
||||
|
||||
if (!extensionCheck[0].installed) {
|
||||
throw new Error('pg_cron extension is not installed. Install it with: CREATE EXTENSION pg_cron;');
|
||||
}
|
||||
|
||||
// Build query with filters
|
||||
const conditions: string[] = [];
|
||||
|
||||
if (jobid !== undefined) {
|
||||
// jobid is a number from Zod validation, safe to use directly
|
||||
conditions.push(`j.jobid = ${jobid}`);
|
||||
}
|
||||
|
||||
if (jobname) {
|
||||
conditions.push(`j.jobname ILIKE '%${jobname}%'`);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
// status is validated by Zod enum, safe to use directly
|
||||
conditions.push(`r.status = '${status}'`);
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
// Ensure limit is within bounds
|
||||
const safeLimit = Math.min(Math.max(1, limit), 1000);
|
||||
|
||||
const historySql = `
|
||||
SELECT
|
||||
j.jobid,
|
||||
j.jobname,
|
||||
j.schedule,
|
||||
r.runid,
|
||||
r.status,
|
||||
r.start_time::text,
|
||||
r.end_time::text,
|
||||
r.return_message
|
||||
FROM cron.job j
|
||||
LEFT JOIN cron.job_run_details r ON j.jobid = r.jobid
|
||||
${whereClause}
|
||||
ORDER BY r.start_time DESC NULLS LAST
|
||||
LIMIT ${safeLimit}
|
||||
`;
|
||||
|
||||
const result = await executeSqlWithFallback(client, historySql, true);
|
||||
return handleSqlResponse(result, GetCronJobHistoryOutputSchema);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user