@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
JavaScript
#!/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