UNPKG

@rip-user/rls-debugger-mcp

Version:

AI-powered MCP server for debugging Supabase Row Level Security policies with Claude structured outputs

542 lines 23.1 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { createClient } from '@supabase/supabase-js'; import { RLSAnalyzer } from './analysis.js'; import { MemoryStore } from './memory.js'; import { fetchAllPolicies, fetchTablePolicies } from './rls-queries.js'; import { getDocumentation, getAllDocumentation, searchDocumentation } from './docs.js'; // Environment variables const SUPABASE_URL = process.env.SUPABASE_URL; const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY; const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; const MEMORY_DIR = process.env.MEMORY_DIR || './.rls-memory'; const CLAUDE_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-4-5-20250514'; if (!SUPABASE_URL || !SUPABASE_SERVICE_KEY) { console.error('Error: SUPABASE_URL and SUPABASE_SERVICE_KEY environment variables are required'); process.exit(1); } if (!ANTHROPIC_API_KEY) { console.error('Error: ANTHROPIC_API_KEY environment variable is required'); process.exit(1); } // Initialize clients const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY); const analyzer = new RLSAnalyzer(ANTHROPIC_API_KEY, CLAUDE_MODEL); const memory = new MemoryStore(MEMORY_DIR); // Initialize memory store await memory.initialize(); // Create MCP server const server = new Server({ name: 'rls-policy-debugger', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'analyze_rls_policies', title: 'Analyze RLS Policies', description: 'Analyze RLS policies with AI-powered structured output to debug access issues. Explains why access is granted or denied, identifies root causes, and suggests fixes.', inputSchema: { type: 'object', properties: { scenario: { type: 'string', description: 'Describe the access issue, e.g., "Why can\'t user X see their orders?"' }, table_name: { type: 'string', description: 'Optional: Focus analysis on a specific table' } }, required: ['scenario'] }, outputSchema: { type: 'object', properties: { scenario_summary: { type: 'string' }, applicable_policies: { type: 'array', items: { type: 'object', properties: { table: { type: 'string' }, policy_name: { type: 'string' }, operation: { type: 'string' }, will_grant_access: { type: 'boolean' }, reason: { type: 'string' }, policy_type: { type: 'string' } } } }, policy_combination_logic: { type: 'string' }, root_cause: { type: 'object', properties: { issue: { type: 'string' }, missing_condition: { type: 'string' }, incorrect_policy: { type: 'string' } } }, required_relationships: { type: 'array', items: { type: 'object', properties: { table: { type: 'string' }, condition: { type: 'string' }, exists: { type: 'boolean' }, verification_query: { type: 'string' } } } }, recommendations: { type: 'array', items: { type: 'object', properties: { action: { type: 'string' }, sql: { type: 'string' }, explanation: { type: 'string' }, risk_level: { type: 'string' } } } } }, required: ['scenario_summary', 'applicable_policies', 'policy_combination_logic', 'root_cause', 'recommendations'] } }, { name: 'compile_policy_logic', title: 'Compile Policy Logic', description: 'Compile all RLS policies for a table into a human-readable logic tree showing how PERMISSIVE and RESTRICTIVE policies combine', inputSchema: { type: 'object', properties: { table_name: { type: 'string', description: 'The table name to analyze' } }, required: ['table_name'] }, outputSchema: { type: 'object', properties: { table: { type: 'string' }, rls_enabled: { type: 'boolean' }, access_logic: { type: 'string' }, additional_restrictions: { type: 'string' }, permissive_policies: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, operation: { type: 'string' }, condition: { type: 'string' }, parsed: { type: 'object' } } } }, restrictive_policies: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, operation: { type: 'string' }, condition: { type: 'string' }, parsed: { type: 'object' } } } }, decision_flow: { type: 'string' } }, required: ['table', 'rls_enabled', 'access_logic', 'decision_flow'] } }, { name: 'list_all_policies', title: 'List All Policies', description: 'List all RLS policies across all tables in the database', inputSchema: { type: 'object', properties: { table_name: { type: 'string', description: 'Optional: Filter by table name' } } }, outputSchema: { type: 'array', items: { type: 'object', properties: { schemaname: { type: 'string' }, tablename: { type: 'string' }, policyname: { type: 'string' }, polpermissive: { type: 'string', enum: ['PERMISSIVE', 'RESTRICTIVE'] }, policyrole: { type: 'string' }, polcmd: { type: 'string', enum: ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'ALL'] }, policyqual: { type: ['string', 'null'] }, policywithcheck: { type: ['string', 'null'] } }, required: ['schemaname', 'tablename', 'policyname', 'polpermissive', 'polcmd'] } } }, { name: 'save_policy_knowledge', title: 'Save Policy Knowledge', description: 'Save insights about RLS policies to memory for future reference and debugging sessions', inputSchema: { type: 'object', properties: { knowledge_type: { type: 'string', enum: ['policy_intent', 'known_issue', 'architectural_decision', 'correction'], description: 'Type of knowledge being saved' }, content: { type: 'string', description: 'The knowledge content to save' }, related_tables: { type: 'array', items: { type: 'string' }, description: 'Tables this knowledge relates to' }, related_policies: { type: 'array', items: { type: 'string' }, description: 'Optional: Specific policy names this relates to' } }, required: ['knowledge_type', 'content', 'related_tables'] }, outputSchema: { type: 'object', properties: { success: { type: 'boolean' }, message: { type: 'string' } }, required: ['success', 'message'] } }, { name: 'get_policy_knowledge', title: 'Get Policy Knowledge', description: 'Retrieve saved knowledge about RLS policies from memory', inputSchema: { type: 'object', properties: { table_name: { type: 'string', description: 'Optional: Filter by table name' }, knowledge_type: { type: 'string', enum: ['policy_intent', 'known_issue', 'architectural_decision', 'correction'], description: 'Optional: Filter by knowledge type' } } }, outputSchema: { type: 'array', items: { type: 'object', properties: { type: { type: 'string', enum: ['policy_intent', 'known_issue', 'architectural_decision', 'correction'] }, content: { type: 'string' }, tables: { type: 'array', items: { type: 'string' } }, timestamp: { type: 'string' }, related_policies: { type: 'array', items: { type: 'string' } } }, required: ['type', 'content', 'tables', 'timestamp'] } } }, { name: 'get_memory_summary', title: 'Get Memory Summary', description: 'Get a summary of all saved policy knowledge', inputSchema: { type: 'object', properties: {} }, outputSchema: { type: 'object', properties: { total: { type: 'number' }, byType: { type: 'object', additionalProperties: { type: 'number' } }, byTable: { type: 'object', additionalProperties: { type: 'number' } } }, required: ['total', 'byType', 'byTable'] } }, { name: 'get_rls_documentation', title: 'Get RLS Documentation', description: 'Get Supabase RLS documentation for a specific concept (policyTypes, policyCommands, authHelpers, performanceTips, commonPatterns, troubleshooting)', inputSchema: { type: 'object', properties: { concept: { type: 'string', enum: ['policyTypes', 'policyCommands', 'authHelpers', 'performanceTips', 'commonPatterns', 'troubleshooting'], description: 'The RLS concept to get documentation for' } }, required: ['concept'] }, outputSchema: { type: 'object', properties: { concept: { type: 'string' }, documentation: { type: 'string' } }, required: ['concept', 'documentation'] } }, { name: 'search_rls_docs', title: 'Search RLS Documentation', description: 'Search Supabase RLS documentation for keywords or topics', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (e.g., "PERMISSIVE", "auth.uid", "performance")' } }, required: ['query'] }, outputSchema: { type: 'array', items: { type: 'object', properties: { title: { type: 'string' }, excerpt: { type: 'string' }, ref: { type: 'string' } }, required: ['title', 'excerpt', 'ref'] } } }, { name: 'get_all_rls_concepts', title: 'Get All RLS Concepts', description: 'Get all Supabase RLS documentation concepts and best practices in one reference', inputSchema: { type: 'object', properties: {} }, outputSchema: { type: 'object', properties: { documentation: { type: 'string' } }, required: ['documentation'] } } ] }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'analyze_rls_policies': { const { scenario, table_name } = args; // Fetch policies const policies = table_name ? await fetchTablePolicies(supabase, table_name) : await fetchAllPolicies(supabase); // Load relevant knowledge from memory const tables = table_name ? [table_name] : [...new Set(policies.map(p => p.tablename))]; const savedKnowledge = await memory.loadForTables(tables); // Analyze with AI const analysis = await analyzer.analyzeWithStructuredOutput(scenario, policies, savedKnowledge); return { content: [ { type: 'text', text: `# RLS Policy Analysis\n\n${JSON.stringify(analysis, null, 2)}` } ] }; } case 'compile_policy_logic': { const { table_name } = args; const policies = await fetchAllPolicies(supabase); const logicTree = analyzer.compilePolicyLogic(policies, table_name); return { content: [ { type: 'text', text: `# Policy Logic Tree for ${table_name}\n\n${JSON.stringify(logicTree, null, 2)}` } ] }; } case 'list_all_policies': { const { table_name } = args; const policies = table_name ? await fetchTablePolicies(supabase, table_name) : await fetchAllPolicies(supabase); return { content: [ { type: 'text', text: `# RLS Policies\n\n${JSON.stringify(policies, null, 2)}` } ] }; } case 'save_policy_knowledge': { const { knowledge_type, content, related_tables, related_policies } = args; await memory.save({ type: knowledge_type, content, tables: related_tables, related_policies }); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: `Saved ${knowledge_type} to memory for tables: ${related_tables.join(', ')}` }, null, 2) } ] }; } case 'get_policy_knowledge': { const { table_name, knowledge_type } = args; let knowledge; if (table_name) { knowledge = await memory.loadForTables([table_name]); } else if (knowledge_type) { knowledge = await memory.loadByType(knowledge_type); } else { knowledge = await memory.loadAll(); } if (knowledge_type && table_name) { knowledge = knowledge.filter(k => k.type === knowledge_type); } return { content: [ { type: 'text', text: `# Saved Policy Knowledge\n\n${JSON.stringify(knowledge, null, 2)}` } ] }; } case 'get_memory_summary': { const summary = await memory.getSummary(); return { content: [ { type: 'text', text: `# Memory Summary\n\n${JSON.stringify(summary, null, 2)}` } ] }; } case 'get_rls_documentation': { const { concept } = args; const docs = getDocumentation(concept); return { content: [ { type: 'text', text: docs } ] }; } case 'search_rls_docs': { const { query } = args; const results = searchDocumentation(query); if (results.length === 0) { return { content: [ { type: 'text', text: `No documentation found for query: "${query}"` } ] }; } const formattedResults = results.map(r => `## ${r.title}\n\n${r.excerpt}\n\n**Reference:** ${r.ref}`).join('\n\n---\n\n'); return { content: [ { type: 'text', text: `# Search Results for "${query}"\n\n${formattedResults}` } ] }; } case 'get_all_rls_concepts': { const allDocs = getAllDocumentation(); return { content: [ { type: 'text', text: `# Complete Supabase RLS Documentation Reference\n\n${allDocs}` } ] }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Error: ${errorMessage}` } ], isError: true }; } }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('RLS Policy Debugger MCP server running on stdio'); } main().catch((error) => { console.error('Fatal error in main():', error); process.exit(1); }); //# sourceMappingURL=index.js.map