Skip to main content
Glama
MisterSandFR

Supabase MCP Server - Self-Hosted Edition

by MisterSandFR
manage_extensions.ts34.6 kB
import { Tool } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { ToolContext } from "./types.js"; import { executeSqlWithFallback } from "./utils.js"; const ManageExtensionsInputSchema = z.object({ action: z.enum(['list', 'install', 'uninstall', 'update', 'available', 'configure', 'dependencies', 'security_audit', 'bulk_install', 'backup_config']).describe("Action to perform"), extensionName: z.string().optional().describe("Extension name"), schema: z.string().optional().describe("Schema to install extension in"), version: z.string().optional().describe("Specific version to install"), cascade: z.boolean().optional().default(false).describe("Use CASCADE when dropping"), ifExists: z.boolean().optional().default(true).describe("Use IF EXISTS when dropping"), force: z.boolean().optional().default(false).describe("Force installation/update"), extensions: z.array(z.string()).optional().describe("List of extensions for bulk operations"), configParams: z.record(z.any()).optional().describe("Configuration parameters"), category: z.enum(['all', 'contrib', 'third_party', 'supabase', 'security', 'performance', 'data_types', 'foreign_data']).optional().default('all').describe("Extension category filter"), environment: z.enum(['development', 'staging', 'production']).optional().describe("Target environment"), autoConfig: z.boolean().optional().default(false).describe("Apply recommended configuration"), skipDependencies: z.boolean().optional().default(false).describe("Skip dependency checking") }); type ManageExtensionsInput = z.infer<typeof ManageExtensionsInputSchema>; const manageExtensionsOutputSchema = z.object({ content: z.array(z.object({ type: z.literal("text"), text: z.string() })) }); export const manageExtensionsTool = { name: "manage_extensions", description: "Comprehensive PostgreSQL extension management with dependency handling, security auditing, and environment-specific configurations", inputSchema: ManageExtensionsInputSchema, mcpInputSchema: { type: "object", properties: { action: { type: "string", enum: ["list", "install", "uninstall", "update", "available", "configure", "dependencies", "security_audit", "bulk_install", "backup_config"], description: "Action to perform" }, extensionName: { type: "string", description: "Extension name" }, schema: { type: "string", description: "Schema to install in" }, version: { type: "string", description: "Specific version" }, cascade: { type: "boolean", description: "Use CASCADE when dropping" }, ifExists: { type: "boolean", description: "Use IF EXISTS" }, force: { type: "boolean", description: "Force operation" }, extensions: { type: "array", items: { type: "string" }, description: "Extensions for bulk operations" }, configParams: { type: "object", description: "Configuration parameters" }, category: { type: "string", enum: ["all", "contrib", "third_party", "supabase", "security", "performance", "data_types", "foreign_data"], description: "Extension category" }, environment: { type: "string", enum: ["development", "staging", "production"], description: "Target environment" }, autoConfig: { type: "boolean", description: "Apply recommended config" }, skipDependencies: { type: "boolean", description: "Skip dependency checking" } }, required: ["action"] }, outputSchema: manageExtensionsOutputSchema, execute: async (input: unknown, context: ToolContext) => { const validatedInput = ManageExtensionsInputSchema.parse(input); switch (validatedInput.action) { case 'list': { const sql = ` SELECT e.extname AS extension_name, e.extversion AS version, n.nspname AS schema_name, e.extrelocatable AS relocatable, CASE WHEN e.extname = ANY(ARRAY['plpgsql', 'adminpack']) THEN 'Built-in' WHEN e.extname LIKE 'supabase_%' THEN 'Supabase' WHEN e.extname = ANY(ARRAY['uuid-ossp', 'pgcrypto', 'btree_gist', 'citext', 'cube', 'dblink', 'dict_int', 'earthdistance', 'fuzzystrmatch', 'hstore', 'intarray', 'isn', 'lo', 'ltree', 'pg_buffercache', 'pg_freespacemap', 'pg_stat_statements', 'pg_trgm', 'pgrowlocks', 'sslinfo', 'tablefunc', 'tcn', 'tsearch2', 'unaccent', 'xml2']) THEN 'Contrib' ELSE 'Third-party' END as category, CASE WHEN e.extname = ANY(ARRAY['uuid-ossp', 'pgcrypto', 'citext', 'hstore', 'ltree', 'unaccent']) THEN 'Data Types' WHEN e.extname = ANY(ARRAY['pg_stat_statements', 'pg_buffercache', 'pg_freespacemap', 'pgrowlocks']) THEN 'Performance' WHEN e.extname = ANY(ARRAY['dblink', 'postgres_fdw']) THEN 'Foreign Data' WHEN e.extname = ANY(ARRAY['pgcrypto', 'sslinfo']) THEN 'Security' ELSE 'General' END as functionality, d.description, COALESCE(dep_count.count, 0) as dependency_count FROM pg_extension e LEFT JOIN pg_namespace n ON e.extnamespace = n.oid LEFT JOIN pg_description d ON d.objoid = e.oid AND d.classoid = 'pg_extension'::regclass LEFT JOIN ( SELECT refobjid, COUNT(*) as count FROM pg_depend dep JOIN pg_extension ext ON dep.objid = ext.oid WHERE dep.classid = 'pg_extension'::regclass AND dep.deptype = 'n' GROUP BY refobjid ) dep_count ON dep_count.refobjid = e.oid ${validatedInput.category !== 'all' ? ` WHERE CASE WHEN '${validatedInput.category}' = 'contrib' AND e.extname = ANY(ARRAY['uuid-ossp', 'pgcrypto', 'btree_gist', 'citext', 'cube', 'dblink', 'dict_int', 'earthdistance', 'fuzzystrmatch', 'hstore', 'intarray', 'isn', 'lo', 'ltree', 'pg_buffercache', 'pg_freespacemap', 'pg_stat_statements', 'pg_trgm', 'pgrowlocks', 'sslinfo', 'tablefunc', 'tcn', 'tsearch2', 'unaccent', 'xml2']) THEN true WHEN '${validatedInput.category}' = 'supabase' AND e.extname LIKE 'supabase_%' THEN true WHEN '${validatedInput.category}' = 'security' AND e.extname = ANY(ARRAY['pgcrypto', 'sslinfo']) THEN true WHEN '${validatedInput.category}' = 'performance' AND e.extname = ANY(ARRAY['pg_stat_statements', 'pg_buffercache', 'pg_freespacemap', 'pgrowlocks']) THEN true WHEN '${validatedInput.category}' = 'data_types' AND e.extname = ANY(ARRAY['uuid-ossp', 'pgcrypto', 'citext', 'hstore', 'ltree', 'unaccent']) THEN true WHEN '${validatedInput.category}' = 'foreign_data' AND e.extname = ANY(ARRAY['dblink', 'postgres_fdw']) THEN true ELSE false END ` : ''} ${validatedInput.extensionName ? `AND e.extname LIKE '%${validatedInput.extensionName}%'` : ''} ORDER BY category, e.extname `; const result = await executeSqlWithFallback(sql, context); const summary = { total_extensions: result.data.length, by_category: result.data.reduce((acc: any, ext: any) => { acc[ext.category] = (acc[ext.category] || 0) + 1; return acc; }, {}), by_functionality: result.data.reduce((acc: any, ext: any) => { acc[ext.functionality] = (acc[ext.functionality] || 0) + 1; return acc; }, {}) }; return { content: [{ type: "text", text: JSON.stringify({ installed_extensions: result.data, summary, recommendations: generateExtensionRecommendations(result.data, validatedInput.environment) }, null, 2) }] }; } case 'available': { const sql = ` SELECT DISTINCT name, default_version, comment, CASE WHEN name = ANY(ARRAY['plpgsql', 'adminpack']) THEN 'Built-in' WHEN name LIKE 'supabase_%' THEN 'Supabase' WHEN name = ANY(ARRAY['uuid-ossp', 'pgcrypto', 'btree_gist', 'citext', 'cube', 'dblink', 'dict_int', 'earthdistance', 'fuzzystrmatch', 'hstore', 'intarray', 'isn', 'lo', 'ltree', 'pg_buffercache', 'pg_freespacemap', 'pg_stat_statements', 'pg_trgm', 'pgrowlocks', 'sslinfo', 'tablefunc', 'tcn', 'tsearch2', 'unaccent', 'xml2']) THEN 'Contrib' ELSE 'Third-party' END as category, CASE WHEN EXISTS(SELECT 1 FROM pg_extension WHERE extname = av.name) THEN 'Installed' ELSE 'Available' END as status FROM pg_available_extensions av ${validatedInput.category !== 'all' ? ` WHERE CASE WHEN '${validatedInput.category}' = 'contrib' AND name = ANY(ARRAY['uuid-ossp', 'pgcrypto', 'btree_gist', 'citext', 'cube', 'dblink', 'dict_int', 'earthdistance', 'fuzzystrmatch', 'hstore', 'intarray', 'isn', 'lo', 'ltree', 'pg_buffercache', 'pg_freespacemap', 'pg_stat_statements', 'pg_trgm', 'pgrowlocks', 'sslinfo', 'tablefunc', 'tcn', 'tsearch2', 'unaccent', 'xml2']) THEN true WHEN '${validatedInput.category}' = 'supabase' AND name LIKE 'supabase_%' THEN true WHEN '${validatedInput.category}' = 'security' AND name = ANY(ARRAY['pgcrypto', 'sslinfo']) THEN true WHEN '${validatedInput.category}' = 'performance' AND name = ANY(ARRAY['pg_stat_statements', 'pg_buffercache', 'pg_freespacemap', 'pgrowlocks']) THEN true WHEN '${validatedInput.category}' = 'data_types' AND name = ANY(ARRAY['uuid-ossp', 'pgcrypto', 'citext', 'hstore', 'ltree', 'unaccent']) THEN true WHEN '${validatedInput.category}' = 'foreign_data' AND name = ANY(ARRAY['dblink', 'postgres_fdw']) THEN true ELSE false END ` : ''} ${validatedInput.extensionName ? `AND name LIKE '%${validatedInput.extensionName}%'` : ''} ORDER BY category, status, name `; const result = await executeSqlWithFallback(sql, context); return { content: [{ type: "text", text: JSON.stringify({ available_extensions: result.data, summary: { total_available: result.data.length, installed: result.data.filter((e: any) => e.status === 'Installed').length, available: result.data.filter((e: any) => e.status === 'Available').length, by_category: result.data.reduce((acc: any, ext: any) => { acc[ext.category] = (acc[ext.category] || 0) + 1; return acc; }, {}) }, recommended_for_supabase: getSupabaseRecommendedExtensions() }, null, 2) }] }; } case 'install': { if (!validatedInput.extensionName) { throw new Error("Extension name is required"); } // Check if extension is already installed const checkSql = `SELECT extname FROM pg_extension WHERE extname = $1`; const existing = await executeSqlWithFallback(checkSql, context, [validatedInput.extensionName]); if (existing.data.length > 0 && !validatedInput.force) { return { content: [{ type: "text", text: JSON.stringify({ success: false, message: "Extension already installed", extension: validatedInput.extensionName, suggestion: "Use force: true to reinstall or action: 'update' to update" }, null, 2) }] }; } // Check dependencies if not skipping let dependencyInfo = {}; if (!validatedInput.skipDependencies) { dependencyInfo = await checkExtensionDependencies(validatedInput.extensionName, context); } // Build installation command let installCmd = `CREATE EXTENSION`; if (existing.data.length > 0) installCmd += ` IF NOT EXISTS`; installCmd += ` "${validatedInput.extensionName}"`; if (validatedInput.schema) installCmd += ` WITH SCHEMA ${validatedInput.schema}`; if (validatedInput.version) installCmd += ` VERSION '${validatedInput.version}'`; const result = await executeSqlWithFallback(installCmd, context); // Apply auto-configuration if requested let configResult = null; if (validatedInput.autoConfig) { configResult = await applyExtensionAutoConfig(validatedInput.extensionName, context, validatedInput.environment); } return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Extension installed successfully", extension: validatedInput.extensionName, version: validatedInput.version || 'default', schema: validatedInput.schema || 'default', dependencies: dependencyInfo, auto_config_applied: !!configResult, configuration: configResult }, null, 2) }] }; } case 'bulk_install': { if (!validatedInput.extensions || validatedInput.extensions.length === 0) { throw new Error("Extensions list is required for bulk install"); } const results = []; for (const extensionName of validatedInput.extensions) { try { const installResult = await manageExtensionsTool.execute({ action: 'install', extensionName, schema: validatedInput.schema, force: validatedInput.force, autoConfig: validatedInput.autoConfig, skipDependencies: validatedInput.skipDependencies, environment: validatedInput.environment }, context); results.push({ extension: extensionName, success: true, result: installResult }); } catch (error: any) { results.push({ extension: extensionName, success: false, error: error.message }); } } return { content: [{ type: "text", text: JSON.stringify({ bulk_install_results: results, summary: { total_attempted: validatedInput.extensions.length, successful: results.filter(r => r.success).length, failed: results.filter(r => !r.success).length, success_rate: Math.round((results.filter(r => r.success).length / validatedInput.extensions.length) * 100) } }, null, 2) }] }; } case 'dependencies': { if (!validatedInput.extensionName) { throw new Error("Extension name is required"); } const dependencySql = ` WITH RECURSIVE extension_deps AS ( -- Base case: direct dependencies SELECT e.extname as extension, dep_ext.extname as depends_on, 1 as level FROM pg_extension e JOIN pg_depend d ON e.oid = d.objid JOIN pg_extension dep_ext ON d.refobjid = dep_ext.oid WHERE e.extname = $1 AND d.classid = 'pg_extension'::regclass AND d.deptype = 'n' UNION ALL -- Recursive case: dependencies of dependencies SELECT ed.extension, dep_ext.extname as depends_on, ed.level + 1 FROM extension_deps ed JOIN pg_extension e ON ed.depends_on = e.extname JOIN pg_depend d ON e.oid = d.objid JOIN pg_extension dep_ext ON d.refobjid = dep_ext.oid WHERE d.classid = 'pg_extension'::regclass AND d.deptype = 'n' AND ed.level < 5 -- Prevent infinite recursion ) SELECT DISTINCT * FROM extension_deps ORDER BY level, depends_on `; const dependents_sql = ` SELECT e.extname as extension, 'depends_on' as relationship FROM pg_extension e JOIN pg_depend d ON e.oid = d.refobjid JOIN pg_extension target ON d.objid = target.oid WHERE target.extname = $1 AND d.classid = 'pg_extension'::regclass AND d.deptype = 'n' ORDER BY e.extname `; const [dependencies, dependents] = await Promise.all([ executeSqlWithFallback(dependencySql, context, [validatedInput.extensionName]), executeSqlWithFallback(dependents_sql, context, [validatedInput.extensionName]) ]); return { content: [{ type: "text", text: JSON.stringify({ extension: validatedInput.extensionName, dependencies: dependencies.data, dependents: dependents.data, analysis: { has_dependencies: dependencies.data.length > 0, dependency_count: dependencies.data.length, has_dependents: dependents.data.length > 0, dependent_count: dependents.data.length, max_dependency_level: dependencies.data.length > 0 ? Math.max(...dependencies.data.map((d: any) => d.level)) : 0, safe_to_remove: dependents.data.length === 0 } }, null, 2) }] }; } case 'configure': { if (!validatedInput.extensionName) { throw new Error("Extension name is required"); } const config = await applyExtensionAutoConfig( validatedInput.extensionName, context, validatedInput.environment, validatedInput.configParams ); return { content: [{ type: "text", text: JSON.stringify({ success: true, extension: validatedInput.extensionName, environment: validatedInput.environment, configuration: config }, null, 2) }] }; } case 'security_audit': { const securitySql = ` SELECT e.extname, e.extversion, n.nspname as schema_name, CASE WHEN e.extname = ANY(ARRAY['pgcrypto', 'sslinfo', 'uuid-ossp']) THEN 'Security' WHEN e.extname = ANY(ARRAY['dblink', 'postgres_fdw', 'file_fdw']) THEN 'Data Access' WHEN e.extname LIKE '%plpython%' OR e.extname LIKE '%plperl%' THEN 'Scripting' ELSE 'Standard' END as security_category, CASE WHEN e.extname = ANY(ARRAY['dblink', 'postgres_fdw', 'file_fdw']) THEN 'HIGH' WHEN e.extname LIKE '%plpython%' OR e.extname LIKE '%plperl%' THEN 'HIGH' WHEN e.extname = ANY(ARRAY['pgcrypto', 'sslinfo']) THEN 'MEDIUM' ELSE 'LOW' END as risk_level, CASE WHEN e.extname = 'dblink' THEN 'Can execute queries on remote databases' WHEN e.extname = 'postgres_fdw' THEN 'Foreign data wrapper for PostgreSQL' WHEN e.extname = 'file_fdw' THEN 'Can read files from file system' WHEN e.extname LIKE '%plpython%' THEN 'Allows Python code execution' WHEN e.extname LIKE '%plperl%' THEN 'Allows Perl code execution' WHEN e.extname = 'pgcrypto' THEN 'Cryptographic functions' ELSE 'Standard extension functionality' END as security_implications, EXISTS( SELECT 1 FROM pg_proc p JOIN pg_namespace pn ON p.pronamespace = pn.oid WHERE p.prolang = (SELECT oid FROM pg_language WHERE lanname = 'c') AND p.proname LIKE e.extname || '%' ) as has_c_functions FROM pg_extension e LEFT JOIN pg_namespace n ON e.extnamespace = n.oid ORDER BY CASE risk_level WHEN 'HIGH' THEN 1 WHEN 'MEDIUM' THEN 2 ELSE 3 END, e.extname `; const result = await executeSqlWithFallback(securitySql, context); const securityAssessment = { overall_risk: 'LOW', high_risk_extensions: result.data.filter((e: any) => e.risk_level === 'HIGH').length, recommendations: [] as string[] }; if (securityAssessment.high_risk_extensions > 0) { securityAssessment.overall_risk = 'HIGH'; securityAssessment.recommendations.push('Review high-risk extensions and their usage'); } const scriptingExtensions = result.data.filter((e: any) => e.security_category === 'Scripting'); if (scriptingExtensions.length > 0) { securityAssessment.recommendations.push('Monitor scripting extensions for code injection risks'); } const dataAccessExtensions = result.data.filter((e: any) => e.security_category === 'Data Access'); if (dataAccessExtensions.length > 0) { securityAssessment.recommendations.push('Audit data access extensions for proper access controls'); } return { content: [{ type: "text", text: JSON.stringify({ security_audit: result.data, assessment: securityAssessment, compliance_notes: { production_ready: securityAssessment.overall_risk !== 'HIGH', requires_review: securityAssessment.high_risk_extensions > 0, security_score: Math.max(0, 100 - (securityAssessment.high_risk_extensions * 25)) } }, null, 2) }] }; } case 'uninstall': { if (!validatedInput.extensionName) { throw new Error("Extension name is required"); } // Check dependencies before uninstalling const dependentsCheck = await manageExtensionsTool.execute({ action: 'dependencies', extensionName: validatedInput.extensionName }, context); const dependentsData = JSON.parse(dependentsCheck.content[0].text); if (dependentsData.dependents.length > 0 && !validatedInput.cascade) { return { content: [{ type: "text", text: JSON.stringify({ success: false, message: "Extension has dependents", extension: validatedInput.extensionName, dependents: dependentsData.dependents, suggestion: "Use cascade: true to force removal or remove dependents first" }, null, 2) }] }; } let dropCmd = `DROP EXTENSION`; if (validatedInput.ifExists) dropCmd += ` IF EXISTS`; dropCmd += ` "${validatedInput.extensionName}"`; if (validatedInput.cascade) dropCmd += ` CASCADE`; await executeSqlWithFallback(dropCmd, context); return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Extension uninstalled successfully", extension: validatedInput.extensionName, cascade_used: validatedInput.cascade }, null, 2) }] }; } default: throw new Error(`Unknown action: ${validatedInput.action}`); } } }; async function checkExtensionDependencies(extensionName: string, context: ToolContext): Promise<any> { // This would contain logic to check extension dependencies // For now, return a simple structure return { checked: true, dependencies: [], warnings: [] }; } async function applyExtensionAutoConfig(extensionName: string, context: ToolContext, environment?: string, customParams?: any): Promise<any> { const configurations = getExtensionConfigurations(); const config = configurations[extensionName]; if (!config) { return { message: "No auto-configuration available for this extension", extension: extensionName }; } const appliedSettings = []; const envConfig = environment ? config.environments?.[environment] || config.default : config.default; // Apply configuration settings for (const [setting, value] of Object.entries(envConfig)) { try { const sql = `SET ${setting} = '${value}'`; await executeSqlWithFallback(sql, context); appliedSettings.push({ setting, value, success: true }); } catch (error: any) { appliedSettings.push({ setting, value, success: false, error: error.message }); } } // Apply custom parameters if provided if (customParams) { for (const [setting, value] of Object.entries(customParams)) { try { const sql = `SET ${setting} = '${value}'`; await executeSqlWithFallback(sql, context); appliedSettings.push({ setting, value, success: true, source: 'custom' }); } catch (error: any) { appliedSettings.push({ setting, value, success: false, error: error.message, source: 'custom' }); } } } return { extension: extensionName, environment: environment || 'default', settings_applied: appliedSettings, success: appliedSettings.every(s => s.success) }; } function getExtensionConfigurations(): any { return { 'pg_stat_statements': { default: { 'pg_stat_statements.max': '10000', 'pg_stat_statements.track': 'all' }, environments: { development: { 'pg_stat_statements.max': '5000', 'pg_stat_statements.track': 'all' }, production: { 'pg_stat_statements.max': '10000', 'pg_stat_statements.track': 'top' } } }, 'pg_buffercache': { default: {}, environments: { development: {}, production: {} } }, 'pgcrypto': { default: {}, environments: {} } }; } function generateExtensionRecommendations(extensions: any[], environment?: string): string[] { const recommendations = []; const installedNames = extensions.map(e => e.extension_name); // Essential extensions recommendations if (!installedNames.includes('uuid-ossp')) { recommendations.push('Consider installing uuid-ossp for UUID generation'); } if (!installedNames.includes('pgcrypto')) { recommendations.push('Install pgcrypto for cryptographic functions'); } if (!installedNames.includes('pg_stat_statements') && environment === 'production') { recommendations.push('Install pg_stat_statements for query performance monitoring'); } if (!installedNames.includes('pg_trgm')) { recommendations.push('Consider pg_trgm for full-text search improvements'); } // Environment-specific recommendations if (environment === 'development') { if (!installedNames.includes('pg_buffercache')) { recommendations.push('Install pg_buffercache for development debugging'); } } if (environment === 'production') { if (installedNames.includes('dblink') || installedNames.includes('postgres_fdw')) { recommendations.push('Review foreign data access extensions for security'); } } return recommendations; } function getSupabaseRecommendedExtensions(): any { return { essential: [ 'uuid-ossp', 'pgcrypto', 'pg_stat_statements', 'pg_trgm' ], recommended: [ 'citext', 'hstore', 'ltree', 'unaccent', 'btree_gist' ], performance: [ 'pg_buffercache', 'pg_freespacemap' ], development: [ 'pgrowlocks', 'sslinfo', 'tablefunc' ] }; }

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