import { z } from 'zod'; import type { ToolContext, ToolPrivilegeLevel } from './types.js'; import { handleSqlResponse, executeSqlWithFallback } from './utils.js'; // Output schema for constraints const ListConstraintsOutputSchema = z.array(z.object({ schema_name: z.string(), table_name: z.string(), constraint_name: z.string(), constraint_type: z.string(), // PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK, EXCLUDE columns: z.array(z.string()), definition: z.string(), is_deferrable: z.boolean(), initially_deferred: z.boolean(), })); // Input schema with optional filters const ListConstraintsInputSchema = z.object({ schema: z.string().optional().describe('Filter by schema name.'), table: z.string().optional().describe('Filter by table name.'), constraint_type: z.enum(['PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE', 'CHECK', 'EXCLUDE']).optional().describe('Filter by constraint type.'), include_system: z.boolean().optional().default(false).describe('Include constraints in system schemas.'), }); type ListConstraintsInput = z.infer; // Static JSON Schema for MCP capabilities const mcpInputSchema = { type: 'object', properties: { schema: { type: 'string', description: 'Filter by schema name.', }, table: { type: 'string', description: 'Filter by table name.', }, constraint_type: { type: 'string', enum: ['PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE', 'CHECK', 'EXCLUDE'], description: 'Filter by constraint type.', }, include_system: { type: 'boolean', default: false, description: 'Include constraints in system schemas.', }, }, required: [], }; // SQL identifier validation pattern const identifierPattern = /^[a-zA-Z_][a-zA-Z0-9_$]*$/; export const listConstraintsTool = { name: 'list_constraints', description: 'Lists all constraints (PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK, EXCLUDE) in the database. Can filter by schema, table, and type.', privilegeLevel: 'regular' as ToolPrivilegeLevel, inputSchema: ListConstraintsInputSchema, mcpInputSchema: mcpInputSchema, outputSchema: ListConstraintsOutputSchema, execute: async (input: ListConstraintsInput, context: ToolContext) => { const client = context.selfhostedClient; const { schema, table, constraint_type, include_system } = input; // Validate identifiers if provided if (schema && !identifierPattern.test(schema)) { throw new Error(`Invalid schema name: ${schema}`); } if (table && !identifierPattern.test(table)) { throw new Error(`Invalid table name: ${table}`); } // Build WHERE conditions const conditions: string[] = []; if (!include_system) { conditions.push("n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'auth', 'storage', 'extensions', 'graphql', 'graphql_public', 'pgbouncer', 'realtime', 'supabase_functions', 'supabase_migrations', '_realtime')"); } if (schema) { conditions.push(`n.nspname = '${schema}'`); } if (table) { conditions.push(`rel.relname = '${table}'`); } if (constraint_type) { // Use Map to prevent object injection attacks const typeMap = new Map([ ['PRIMARY KEY', 'p'], ['FOREIGN KEY', 'f'], ['UNIQUE', 'u'], ['CHECK', 'c'], ['EXCLUDE', 'x'], ]); const typeCode = typeMap.get(constraint_type); if (typeCode) { conditions.push(`c.contype = '${typeCode}'`); } } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const sql = ` SELECT n.nspname AS schema_name, rel.relname AS table_name, c.conname AS constraint_name, CASE c.contype WHEN 'p' THEN 'PRIMARY KEY' WHEN 'f' THEN 'FOREIGN KEY' WHEN 'u' THEN 'UNIQUE' WHEN 'c' THEN 'CHECK' WHEN 'x' THEN 'EXCLUDE' ELSE c.contype::text END AS constraint_type, ARRAY( SELECT a.attname FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord) JOIN pg_catalog.pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum ORDER BY k.ord ) AS columns, pg_get_constraintdef(c.oid) AS definition, c.condeferrable AS is_deferrable, c.condeferred AS initially_deferred FROM pg_catalog.pg_constraint c JOIN pg_catalog.pg_class rel ON rel.oid = c.conrelid JOIN pg_catalog.pg_namespace n ON n.oid = rel.relnamespace ${whereClause} ORDER BY n.nspname, rel.relname, c.conname `; const result = await executeSqlWithFallback(client, sql, true); return handleSqlResponse(result, ListConstraintsOutputSchema); }, };