Skip to main content
Glama
MisterSandFR

Supabase MCP Server - Self-Hosted Edition

by MisterSandFR
auto_create_indexes.ts18 kB
import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { ToolContext } from "./types.js"; import { executeSqlWithFallback } from "./utils.js"; const AutoCreateIndexesInputSchema = z.object({ action: z.enum(['analyze', 'suggest', 'create', 'drop_unused', 'rebuild']).describe("Action to perform"), minUsage: z.number().optional().default(100).describe("Minimum query usage for index suggestion"), minSize: z.number().optional().default(1000).describe("Minimum table size for indexing"), autoApply: z.boolean().optional().default(false).describe("Automatically apply suggestions"), includePartial: z.boolean().optional().default(true).describe("Include partial indexes"), analyzeOnly: z.boolean().optional().default(false).describe("Only analyze, don't create") }); type AutoCreateIndexesInput = z.infer<typeof AutoCreateIndexesInputSchema>; const AutoCreateIndexesOutputSchema = z.object({ content: z.array(z.object({ type: z.literal("text"), text: z.string() })) }); export const autoCreateIndexesTool = { name: "auto_create_indexes", description: "Automatically analyze queries and create optimal indexes for performance", inputSchema: AutoCreateIndexesInputSchema, mcpInputSchema: { type: "object", properties: { action: { type: "string", enum: ["analyze", "suggest", "create", "drop_unused", "rebuild"], description: "Action to perform" }, minUsage: { type: "number", description: "Minimum query usage" }, minSize: { type: "number", description: "Minimum table size" }, autoApply: { type: "boolean", description: "Auto apply suggestions" }, includePartial: { type: "boolean", description: "Include partial indexes" }, analyzeOnly: { type: "boolean", description: "Only analyze" } }, required: ["action"] }, outputSchema: AutoCreateIndexesOutputSchema, execute: async (input: unknown, context: ToolContext) => { const validatedInput = AutoCreateIndexesInputSchema.parse(input || {}); switch (validatedInput.action) { case 'analyze': case 'suggest': { // Find missing indexes from pg_stat_user_tables const missingIndexesSql = ` WITH table_stats AS ( SELECT schemaname, tablename, n_tup_ins + n_tup_upd + n_tup_del as write_activity, seq_scan, seq_tup_read, idx_scan, n_live_tup, pg_relation_size(schemaname||'.'||tablename) as table_size FROM pg_stat_user_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema') ), index_usage AS ( SELECT schemaname, tablename, indexrelname, idx_scan as index_scans, pg_relation_size(indexrelid) as index_size FROM pg_stat_user_indexes ) SELECT t.schemaname, t.tablename, t.seq_scan, t.seq_tup_read, t.idx_scan, t.n_live_tup, t.table_size, CASE WHEN t.seq_scan > 0 THEN ROUND((t.seq_tup_read::numeric / t.seq_scan), 2) ELSE 0 END as avg_tuples_per_seq_scan, COUNT(i.indexrelname) as index_count FROM table_stats t LEFT JOIN index_usage i ON t.schemaname = i.schemaname AND t.tablename = i.tablename WHERE t.n_live_tup > ${validatedInput.minSize} AND t.seq_scan > ${validatedInput.minUsage} GROUP BY t.schemaname, t.tablename, t.seq_scan, t.seq_tup_read, t.idx_scan, t.n_live_tup, t.table_size HAVING COUNT(i.indexrelname) < 5 ORDER BY t.seq_scan DESC, t.table_size DESC `; const missingIndexes = await executeSqlWithFallback(missingIndexesSql, context); // Analyze slow queries from pg_stat_statements if available let slowQueries: any = { data: [] }; try { const slowQueriesSql = ` SELECT query, calls, total_exec_time, mean_exec_time, stddev_exec_time, rows FROM pg_stat_statements WHERE query NOT LIKE '%pg_%' AND mean_exec_time > 10 ORDER BY mean_exec_time DESC LIMIT 20 `; slowQueries = await executeSqlWithFallback(slowQueriesSql, context); } catch (error) { // pg_stat_statements might not be enabled } // Find foreign key columns without indexes const fkWithoutIndexesSql = ` SELECT DISTINCT tc.table_schema, tc.table_name, kcu.column_name, ccu.table_schema AS foreign_table_schema, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name FROM information_schema.table_constraints AS tc JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = tc.table_schema AND tablename = tc.table_name AND indexdef LIKE '%' || kcu.column_name || '%' ) `; const fkWithoutIndexes = await executeSqlWithFallback(fkWithoutIndexesSql, context); // Generate index suggestions const suggestions = []; // Suggest indexes for high seq scan tables for (const table of missingIndexes.data) { if (table.seq_scan > validatedInput.minUsage && table.index_count < 3) { // Get columns that could benefit from indexing const columnsSql = ` SELECT a.attname AS column_name, t.typname AS data_type, a.attnum AS column_order FROM pg_attribute a JOIN pg_type t ON a.atttypid = t.oid JOIN pg_class c ON a.attrelid = c.oid JOIN pg_namespace n ON c.relnamespace = n.oid WHERE n.nspname = $1 AND c.relname = $2 AND a.attnum > 0 AND NOT a.attisdropped AND a.attname NOT IN ('created_at', 'updated_at') ORDER BY a.attnum LIMIT 3 `; const columns = await executeSqlWithFallback( columnsSql, context, [table.schemaname, table.tablename] ); if (columns.data.length > 0) { const primaryColumns = columns.data.slice(0, 2).map((c: any) => c.column_name); suggestions.push({ type: 'btree', table: `${table.schemaname}.${table.tablename}`, columns: primaryColumns, name: `idx_${table.tablename}_${primaryColumns.join('_')}`, reason: `High sequential scans (${table.seq_scan}) on large table`, impact: 'HIGH', sql: `CREATE INDEX CONCURRENTLY idx_${table.tablename}_${primaryColumns.join('_')} ON ${table.schemaname}.${table.tablename} (${primaryColumns.join(', ')})` }); } } } // Suggest indexes for foreign keys for (const fk of fkWithoutIndexes.data) { suggestions.push({ type: 'btree', table: `${fk.table_schema}.${fk.table_name}`, columns: [fk.column_name], name: `idx_${fk.table_name}_${fk.column_name}_fk`, reason: `Foreign key without index (references ${fk.foreign_table_name}.${fk.foreign_column_name})`, impact: 'MEDIUM', sql: `CREATE INDEX CONCURRENTLY idx_${fk.table_name}_${fk.column_name}_fk ON ${fk.table_schema}.${fk.table_name} (${fk.column_name})` }); } // Apply suggestions if requested const applied = []; if (validatedInput.autoApply && suggestions.length > 0) { for (const suggestion of suggestions) { try { await executeSqlWithFallback(suggestion.sql, context); applied.push(suggestion.name); } catch (error) { // Index might already exist or creation failed } } } return { content: [{ type: "text", text: JSON.stringify({ analysis: { tablesAnalyzed: missingIndexes.data.length, slowQueries: slowQueries.data.length, foreignKeysWithoutIndexes: fkWithoutIndexes.data.length }, suggestions, applied: validatedInput.autoApply ? applied : [], summary: { totalSuggestions: suggestions.length, highImpact: suggestions.filter(s => s.impact === 'HIGH').length, mediumImpact: suggestions.filter(s => s.impact === 'MEDIUM').length, appliedCount: applied.length } }, null, 2) }] }; } case 'drop_unused': { // Find unused indexes const unusedIndexesSql = ` SELECT schemaname, tablename, indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size, pg_relation_size(indexrelid) as size_bytes FROM pg_stat_user_indexes WHERE idx_scan = 0 AND indexrelname NOT LIKE '%_pkey' AND indexrelname NOT LIKE '%_key' AND pg_relation_size(indexrelid) > 1024 * 1024 ORDER BY pg_relation_size(indexrelid) DESC `; const unusedIndexes = await executeSqlWithFallback(unusedIndexesSql, context); const dropped = []; if (validatedInput.autoApply) { for (const index of unusedIndexes.data) { try { const sql = `DROP INDEX CONCURRENTLY IF EXISTS ${index.schemaname}.${index.indexrelname}`; await executeSqlWithFallback(sql, context); dropped.push({ index: `${index.schemaname}.${index.indexrelname}`, size: index.index_size }); } catch (error) { // Might be a system index or drop failed } } } return { content: [{ type: "text", text: JSON.stringify({ unusedIndexes: unusedIndexes.data, dropped, summary: { totalUnused: unusedIndexes.data.length, totalSize: unusedIndexes.data.reduce((acc: number, idx: any) => acc + idx.size_bytes, 0), droppedCount: dropped.length } }, null, 2) }] }; } case 'rebuild': { // Find bloated indexes const bloatedIndexesSql = ` WITH index_bloat AS ( SELECT schemaname, tablename, indexname, pg_relation_size(schemaname||'.'||indexname) AS index_size, CASE WHEN pg_relation_size(schemaname||'.'||tablename) > 0 THEN (pg_relation_size(schemaname||'.'||indexname)::float / pg_relation_size(schemaname||'.'||tablename)::float) ELSE 0 END AS index_ratio FROM pg_indexes WHERE schemaname NOT IN ('pg_catalog', 'information_schema') ) SELECT schemaname, tablename, indexname, pg_size_pretty(index_size) AS size, ROUND(index_ratio * 100, 2) AS index_table_ratio_pct FROM index_bloat WHERE index_size > 10 * 1024 * 1024 AND index_ratio > 0.5 ORDER BY index_size DESC LIMIT 10 `; const bloatedIndexes = await executeSqlWithFallback(bloatedIndexesSql, context); const rebuilt = []; if (validatedInput.autoApply) { for (const index of bloatedIndexes.data) { try { const sql = `REINDEX INDEX CONCURRENTLY ${index.schemaname}.${index.indexname}`; await executeSqlWithFallback(sql, context); rebuilt.push({ index: `${index.schemaname}.${index.indexname}`, size: index.size }); } catch (error) { // REINDEX CONCURRENTLY might not be available try { const sql = `REINDEX INDEX ${index.schemaname}.${index.indexname}`; await executeSqlWithFallback(sql, context); rebuilt.push({ index: `${index.schemaname}.${index.indexname}`, size: index.size, warning: 'Rebuilt with table lock' }); } catch (e) { // Index rebuild failed } } } } return { content: [{ type: "text", text: JSON.stringify({ bloatedIndexes: bloatedIndexes.data, rebuilt, summary: { totalBloated: bloatedIndexes.data.length, rebuiltCount: rebuilt.length } }, null, 2) }] }; } default: throw new Error(`Unknown action: ${validatedInput.action}`); } } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MisterSandFR/Supabase-MCP-SelfHosted'

If you have feedback or need assistance with the MCP directory API, please join our Discord server