@rip-user/rls-debugger-mcp
Version:
AI-powered MCP server for debugging Supabase Row Level Security policies with Claude structured outputs
229 lines (223 loc) • 10.4 kB
JavaScript
import Anthropic from '@anthropic-ai/sdk';
import { getAllDocumentation } from './docs.js';
export class RLSAnalyzer {
anthropic;
model;
constructor(apiKey, model) {
this.anthropic = new Anthropic({ apiKey });
this.model = model || process.env.CLAUDE_MODEL || 'claude-sonnet-4-5-20250514';
}
/**
* Analyze RLS policies using Claude with native structured outputs (Nov 14, 2025 API)
*/
async analyzeWithStructuredOutput(scenario, policies, savedKnowledge) {
// Use native structured outputs with response_format
const response = await this.anthropic.messages.create({
model: this.model,
max_tokens: 4000,
// @ts-ignore - Beta feature, types may not be available yet
betas: ["structured-outputs-2025-11-13"],
// @ts-ignore - Beta feature, types may not be available yet
response_format: {
type: "json_schema",
json_schema: {
name: "rls_analysis",
strict: true,
schema: {
type: 'object',
properties: {
scenario_summary: {
type: 'string',
description: 'Brief summary of what the user is trying to do'
},
applicable_policies: {
type: 'array',
items: {
type: 'object',
properties: {
table: { type: 'string' },
policy_name: { type: 'string' },
operation: { type: 'string' },
condition: { type: 'string' },
will_grant_access: { type: 'boolean' },
reason: { type: 'string' },
policy_type: { type: 'string', enum: ['PERMISSIVE', 'RESTRICTIVE'] }
},
required: ['table', 'policy_name', 'operation', 'will_grant_access', 'reason', 'policy_type'],
additionalProperties: false
}
},
policy_combination_logic: {
type: 'string',
description: 'Explanation of how PERMISSIVE and RESTRICTIVE policies combine'
},
root_cause: {
type: 'object',
properties: {
issue: { type: 'string' },
missing_condition: { type: 'string' },
incorrect_policy: { type: 'string' },
conflicting_policies: {
type: 'array',
items: { type: 'string' }
}
},
required: ['issue'],
additionalProperties: false
},
required_relationships: {
type: 'array',
items: {
type: 'object',
properties: {
table: { type: 'string' },
condition: { type: 'string' },
exists: { type: 'boolean' },
verification_query: { type: 'string' }
},
required: ['table', 'condition', 'exists', 'verification_query'],
additionalProperties: false
}
},
recommendations: {
type: 'array',
items: {
type: 'object',
properties: {
action: { type: 'string' },
sql: { type: 'string' },
explanation: { type: 'string' },
risk_level: { type: 'string', enum: ['low', 'medium', 'high'] }
},
required: ['action', 'explanation', 'risk_level'],
additionalProperties: false
}
}
},
required: [
'scenario_summary',
'applicable_policies',
'policy_combination_logic',
'root_cause',
'recommendations'
],
additionalProperties: false
}
}
},
messages: [
{
role: 'user',
content: this.buildAnalysisPrompt(scenario, policies, savedKnowledge)
}
]
});
// Parse JSON from response content
const textContent = response.content.find(block => block.type === 'text');
if (!textContent || textContent.type !== 'text') {
throw new Error('No text content found in response');
}
return JSON.parse(textContent.text);
}
/**
* Build the analysis prompt
*/
buildAnalysisPrompt(scenario, policies, savedKnowledge) {
const rlsDocs = getAllDocumentation();
return `You are an expert PostgreSQL RLS (Row Level Security) policy analyzer and debugger.
SCENARIO:
${scenario}
CURRENT RLS POLICIES:
${JSON.stringify(policies, null, 2)}
SAVED KNOWLEDGE FROM PREVIOUS DEBUGGING SESSIONS:
${JSON.stringify(savedKnowledge, null, 2)}
SUPABASE RLS DOCUMENTATION REFERENCE:
${rlsDocs}
YOUR TASK:
Analyze the RLS policies to determine why access might be denied or granted incorrectly.
Provide a step-by-step breakdown of which policies would be evaluated and why.
Identify the root cause of any access issues.
Suggest specific fixes with SQL statements.
Reference the documentation above when explaining concepts.
Use the tool to provide your structured analysis.`;
}
/**
* Compile policy logic tree for a table
*/
compilePolicyLogic(policies, tableName) {
const tablePolicies = policies.filter(p => p.tablename === tableName);
const permissivePolicies = tablePolicies.filter(p => p.polpermissive === 'PERMISSIVE');
const restrictivePolicies = tablePolicies.filter(p => p.polpermissive === 'RESTRICTIVE');
return {
table: tableName,
rls_enabled: tablePolicies.length > 0,
access_logic: permissivePolicies.length > 0
? 'At least ONE PERMISSIVE policy must pass (OR logic)'
: 'No PERMISSIVE policies - all access denied by default',
additional_restrictions: restrictivePolicies.length > 0
? 'ALL RESTRICTIVE policies must also pass (AND logic)'
: 'No additional restrictions',
permissive_policies: permissivePolicies.map(p => this.parsePolicyDetails(p)),
restrictive_policies: restrictivePolicies.map(p => this.parsePolicyDetails(p)),
decision_flow: this.generateDecisionFlow(permissivePolicies, restrictivePolicies)
};
}
/**
* Parse policy details
*/
parsePolicyDetails(policy) {
const condition = policy.policyqual || policy.policywithcheck || '';
return {
name: policy.policyname,
operation: policy.polcmd,
condition,
parsed: this.parseCondition(condition)
};
}
/**
* Parse a policy condition to extract key information
*/
parseCondition(condition) {
return {
requires_auth: condition.includes('auth.uid()'),
checks_relationships: condition.includes('EXISTS'),
referenced_tables: this.extractTableReferences(condition),
joins: this.extractJoins(condition),
comparisons: condition.match(/[\w.]+\s*[=<>!]+\s*[\w().']+/g) || []
};
}
/**
* Extract table references from a condition
*/
extractTableReferences(condition) {
const matches = condition.matchAll(/FROM\s+(?:public\.)?(\w+)/gi);
return [...matches].map(m => m[1]);
}
/**
* Extract joins from a condition
*/
extractJoins(condition) {
const matches = condition.matchAll(/(\w+\.\w+)\s*=\s*(\w+\.\w+)/g);
return [...matches].map(m => `${m[1]} = ${m[2]}`);
}
/**
* Generate decision flow explanation
*/
generateDecisionFlow(permissive, restrictive) {
if (permissive.length === 0) {
return 'DENY - No PERMISSIVE policies exist, access denied by default';
}
const permissiveFlow = permissive.map((p, i) => ` ${i + 1}. Check "${p.policyname}" (${p.polcmd}): ${p.policyqual || 'true'}`).join('\n OR\n');
let flow = `PERMISSIVE POLICIES (need at least ONE to pass):\n${permissiveFlow}\n`;
if (restrictive.length > 0) {
const restrictiveFlow = restrictive.map((p, i) => ` ${i + 1}. Check "${p.policyname}" (${p.polcmd}): ${p.policyqual || 'true'}`).join('\n AND\n');
flow += `\nRESTRICTIVE POLICIES (ALL must pass):\n${restrictiveFlow}\n`;
flow += '\nFINAL DECISION: GRANT if (any PERMISSIVE passes) AND (all RESTRICTIVE pass)';
}
else {
flow += '\nFINAL DECISION: GRANT if any PERMISSIVE policy passes';
}
return flow;
}
}
//# sourceMappingURL=analysis.js.map