@rigstate/mcp 0.7.5 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,43 +17,41 @@ export const listFeaturesTool: ToolDefinition<typeof InputSchema> = {
17
17
  Useful for understanding the strategic context and major milestones.`,
18
18
  schema: InputSchema,
19
19
  handler: async ({ projectId }, { supabase, userId }) => {
20
- // 1. Fetch project to verify access and get fallback spec
21
- const { data: project, error: projectError } = await supabase
22
- .from('projects')
23
- .select('id, functional_spec')
24
- .eq('id', projectId)
25
- .eq('owner_id', userId)
26
- .single();
20
+ // 1. Verify project access and fetch context
21
+ const { data: projectRow, error: projectError } = await supabase
22
+ .rpc('get_project_context_secure', {
23
+ p_project_id: projectId,
24
+ p_user_id: userId
25
+ })
26
+ .single() as any;
27
27
 
28
- if (projectError || !project) {
29
- throw new Error('Project not found or access denied');
28
+ if (projectError || !projectRow) {
29
+ throw new Error('Project details not found or access denied');
30
30
  }
31
31
 
32
- // 2. Primary Strategy: Fetch from 'project_features'
32
+ // 2. Primary Strategy: Fetch from 'project_features' via secure RPC
33
33
  const { data: dbFeatures, error: dbError } = await supabase
34
- .from('project_features')
35
- .select('id, name, description, status')
36
- .eq('project_id', projectId)
37
- .neq('status', 'shadow') // Exclude shadow features by default unless asked
38
- .order('created_at', { ascending: false });
34
+ .rpc('get_project_features_secure', {
35
+ p_project_id: projectId,
36
+ p_user_id: userId
37
+ });
39
38
 
40
39
  let featuresList: any[] = [];
41
40
  let source = 'DB';
42
41
 
43
42
  if (!dbError && dbFeatures && dbFeatures.length > 0) {
44
- featuresList = dbFeatures.map(f => ({
43
+ featuresList = dbFeatures.map((f: any) => ({
45
44
  ...f,
46
- title: f.name // Map back to title specifically for uniform handling below
45
+ title: f.name
47
46
  }));
48
47
  } else {
49
48
  // 3. Fallback Strategy: Extract from 'functional_spec'
50
49
  source = 'FALLBACK_SPEC';
51
- // Log warning (In a real system, use a structured logger. Here we console.error to stderr)
52
- console.error(`[WARN] Project ${projectId}: 'project_features' empty or missing. Falling back to 'functional_spec'.`);
50
+ const spec = projectRow.functional_spec as any;
51
+ const features = spec?.featureList || spec?.features;
53
52
 
54
- const spec = project.functional_spec as any;
55
- if (spec && typeof spec === 'object' && Array.isArray(spec.features)) {
56
- featuresList = spec.features.map((f: any) => ({
53
+ if (Array.isArray(features)) {
54
+ featuresList = features.map((f: any) => ({
57
55
  id: 'legacy',
58
56
  title: f.name || f.title,
59
57
  description: f.description,
@@ -79,5 +77,3 @@ Useful for understanding the strategic context and major milestones.`,
79
77
  };
80
78
 
81
79
  registry.register(listFeaturesTool);
82
-
83
-
@@ -74,7 +74,12 @@ export async function listRoadmapTasks(
74
74
  priority: t.priority,
75
75
  status: t.status,
76
76
  step_number: t.step_number,
77
- prompt_content: t.prompt_content
77
+ prompt_content: t.prompt_content,
78
+ architectural_brief: t.architectural_brief,
79
+ context_summary: t.context_summary,
80
+ metadata: t.metadata,
81
+ checklist: t.checklist,
82
+ tags: t.tags
78
83
  })),
79
84
  formatted
80
85
  };
@@ -48,8 +48,19 @@ export interface UpdateTaskStatusResponse {
48
48
  */
49
49
  export async function getPendingTasks(
50
50
  supabase: SupabaseClient,
51
+ userId: string,
51
52
  projectId: string
52
53
  ): Promise<GetPendingTasksResponse> {
54
+ // 0. Verify project access
55
+ const { data: hasAccess, error: accessError } = await supabase
56
+ .rpc('check_project_access_secure', {
57
+ p_project_id: projectId,
58
+ p_user_id: userId
59
+ });
60
+
61
+ if (accessError || !hasAccess) {
62
+ throw new Error('Project not found or access denied');
63
+ }
53
64
 
54
65
  // Fetch APPROVED tasks that are ready for execution
55
66
  const { data: tasks, error } = await supabase
@@ -143,11 +154,22 @@ export async function getPendingTasks(
143
154
  */
144
155
  export async function updateTaskStatus(
145
156
  supabase: SupabaseClient,
157
+ userId: string,
146
158
  projectId: string,
147
159
  taskId: string,
148
160
  newStatus: 'EXECUTING' | 'COMPLETED' | 'FAILED',
149
161
  executionSummary?: string
150
162
  ): Promise<UpdateTaskStatusResponse> {
163
+ // 0. Verify project access
164
+ const { data: hasAccess, error: accessError } = await supabase
165
+ .rpc('check_project_access_secure', {
166
+ p_project_id: projectId,
167
+ p_user_id: userId
168
+ });
169
+
170
+ if (accessError || !hasAccess) {
171
+ throw new Error('Project not found or access denied');
172
+ }
151
173
 
152
174
  // 1. Fetch the current task state
153
175
  const { data: currentTask, error: fetchError } = await supabase
@@ -60,25 +60,21 @@ export async function saveToProjectBrain(
60
60
 
61
61
  const fullContent = `# ${title}\n\n${content}`;
62
62
 
63
- const { data, error } = await supabase
64
- .from('project_memories')
65
- .insert({
66
- project_id: projectId,
67
- content: fullContent,
68
- category: category.toLowerCase(),
69
- tags: tags,
70
- importance: (category === 'DECISION' || category === 'ARCHITECTURE') ? 9 : 5,
71
- is_active: true,
72
- source_type: 'chat_manual'
73
- })
74
- .select('id')
75
- .single();
63
+ const { data: memoryId, error } = await supabase
64
+ .rpc('save_project_memory_secure', {
65
+ p_project_id: projectId,
66
+ p_user_id: userId,
67
+ p_content: fullContent,
68
+ p_category: category.toLowerCase(),
69
+ p_tags: tags || [],
70
+ p_importance: (category === 'DECISION' || category === 'ARCHITECTURE') ? 9 : 5
71
+ });
76
72
 
77
73
  if (error) throw new Error(`Failed to save memory: ${error.message}`);
78
74
 
79
75
  return {
80
76
  success: true,
81
- memoryId: data.id,
77
+ memoryId: memoryId,
82
78
  message: `✅ Saved [${category}] "${title}" to Project Brain.`
83
79
  };
84
80
  }
@@ -75,55 +75,34 @@ export async function queryBrain(
75
75
  limit: number = 8,
76
76
  threshold: number = 0.5
77
77
  ): Promise<BrainQueryResponse> {
78
- // First, verify project ownership
79
- const { data: project, error: projectError } = await supabase
80
- .from('projects')
81
- .select('id')
82
- .eq('id', projectId)
83
- .eq('owner_id', userId)
84
- .single();
85
-
86
- if (projectError || !project) {
78
+ // First, verify project access
79
+ const { data: hasAccess, error: accessError } = await supabase
80
+ .rpc('check_project_access_secure', {
81
+ p_project_id: projectId,
82
+ p_user_id: userId
83
+ });
84
+
85
+ if (accessError || !hasAccess) {
87
86
  throw new Error('Project not found or access denied');
88
87
  }
89
88
 
90
89
  // Try semantic search first using the match_memories RPC
91
- // This requires the embedding to be generated, so we'll try
92
- // Generate embedding if possible for semantic search
93
90
  const embedding = await generateQueryEmbedding(query);
94
91
  let memories: MemoryRecord[] = [];
95
92
 
96
- // Use the hybrid search RPC
93
+ // Use the secure search RPC
97
94
  const { data: searchResults, error: searchError } = await supabase
98
- .rpc('hybrid_search_memories', {
95
+ .rpc('query_project_brain_secure', {
99
96
  p_project_id: projectId,
97
+ p_user_id: userId,
100
98
  p_query: query,
101
- p_embedding: embedding || null,
102
- p_limit: limit,
103
- p_similarity_threshold: threshold || 0.1
99
+ p_limit: limit
104
100
  });
105
101
 
106
- if (searchError) {
107
- // Fallback to basic recent fetch if RPC fails
108
- const { data: recentMemories } = await supabase
109
- .from('project_memories')
110
- .select('id, content, category, tags, importance, created_at')
111
- .eq('project_id', projectId)
112
- .eq('is_active', true)
113
- .order('created_at', { ascending: false })
114
- .limit(limit);
115
-
116
- if (recentMemories) {
117
- memories = recentMemories.map(m => ({
118
- id: m.id,
119
- content: m.content,
120
- category: m.category || 'general',
121
- tags: m.tags || [],
122
- netVotes: m.importance || 0,
123
- createdAt: m.created_at
124
- }));
125
- }
126
- } else if (searchResults) {
102
+ if (searchError || !searchResults) {
103
+ console.error('Brain query failed:', searchError);
104
+ memories = [];
105
+ } else {
127
106
  memories = searchResults.map((m: any) => ({
128
107
  id: m.id,
129
108
  content: m.content,
@@ -134,31 +113,39 @@ export async function queryBrain(
134
113
  }));
135
114
  }
136
115
 
137
- // --- NEW: FETCH RELEVANT FEATURES ---
138
- // Simple fuzzy search until we vector embedding for features.
116
+ // --- FETCH RELEVANT FEATURES FROM SPEC ---
139
117
  let relevantFeatures: any[] = [];
140
118
  try {
141
- const { data: features } = await supabase
142
- .from('project_features')
143
- .select('name, status, description')
144
- .eq('project_id', projectId)
145
- .or(`name.ilike.%${query}%,description.ilike.%${query}%`)
146
- .limit(3);
147
-
148
- if (features) relevantFeatures = features;
119
+ const { data: projectRow } = await supabase
120
+ .rpc('get_project_context_secure', {
121
+ p_project_id: projectId,
122
+ p_user_id: userId
123
+ })
124
+ .single() as any;
125
+
126
+ if (projectRow?.functional_spec) {
127
+ const spec = typeof projectRow.functional_spec === 'string'
128
+ ? JSON.parse(projectRow.functional_spec)
129
+ : projectRow.functional_spec;
130
+
131
+ const features = spec.featureList || spec.features || [];
132
+ relevantFeatures = features
133
+ .filter((f: any) => (f.name || f.title)?.toLowerCase().includes(query.toLowerCase()))
134
+ .slice(0, 3);
135
+ }
149
136
  } catch (e) {
150
137
  console.warn('Feature fetch failed in brain query', e);
151
138
  }
152
139
 
153
140
  // Format memories into a readable context block
154
- const contextLines = memories.map((m) => {
141
+ const contextLines = memories.map((m: any) => {
155
142
  const voteIndicator = m.netVotes && m.netVotes < 0 ? ` [⚠️ POORLY RATED: ${m.netVotes}]` : '';
156
143
  const tagStr = m.tags && m.tags.length > 0 ? ` [${m.tags.join(', ')}]` : '';
157
144
  const category = m.category ? m.category.toUpperCase() : 'GENERAL';
158
145
  return `- [${category}]${tagStr}${voteIndicator}: ${m.content}`;
159
146
  });
160
147
 
161
- const searchType = embedding ? 'TRIPLE-HYBRID (Vector + FTS + Fuzzy)' : 'HYBRID (FTS + Fuzzy)';
148
+ const searchType = embedding ? 'TRIPLE-HYBRID (Vector + FTS + Fuzzy)' : 'SECURE-GATEWAY (Fuzzy)';
162
149
 
163
150
  let formatted = `=== PROJECT BRAIN: RELEVANT MEMORIES ===
164
151
  Search Mode: ${searchType}
@@ -166,7 +153,7 @@ Query: "${query}"`;
166
153
 
167
154
  if (relevantFeatures.length > 0) {
168
155
  formatted += `\n\n=== RELATED FEATURES ===\n` +
169
- relevantFeatures.map((f: any) => `- ${f.name} [${f.status}]`).join('\n');
156
+ relevantFeatures.map((f: any) => `- ${f.name || f.title} [${f.status || 'Active'}]`).join('\n');
170
157
  }
171
158
 
172
159
  formatted += `\n\nFound ${memories.length} relevant memories:\n\n${contextLines.join('\n')}\n\n==========================================`;
@@ -13,14 +13,15 @@ export async function queryProjectBrain(
13
13
  const { projectId, query, limit = 5 } = input;
14
14
 
15
15
  // Verify access
16
- const { data: project, error: pError } = await supabase
17
- .from('projects')
18
- .select('id')
19
- .eq('id', projectId)
20
- .eq('owner_id', userId)
21
- .single();
22
-
23
- if (pError || !project) throw new Error('Access denied or project not found');
16
+ const { data: hasAccess, error: accessError } = await supabase
17
+ .rpc('check_project_access_secure', {
18
+ p_project_id: projectId,
19
+ p_user_id: userId
20
+ });
21
+
22
+ if (accessError || !hasAccess) {
23
+ throw new Error('Access denied or project not found');
24
+ }
24
25
 
25
26
  // Simple keyword search
26
27
  const terms = query.toLowerCase().split(/\s+/).filter(t => t.length > 2);
@@ -110,15 +110,14 @@ export async function runArchitectureAudit(
110
110
  filePath: string,
111
111
  content: string
112
112
  ): Promise<ArchitectureAuditResponse> {
113
- // First, verify project ownership
114
- const { data: project, error: projectError } = await supabase
115
- .from('projects')
116
- .select('id, name')
117
- .eq('id', projectId)
118
- .eq('owner_id', userId)
119
- .single();
120
-
121
- if (projectError || !project) {
113
+ // First, verify project access
114
+ const { data: hasAccess, error: accessError } = await supabase
115
+ .rpc('check_project_access_secure', {
116
+ p_project_id: projectId,
117
+ p_user_id: userId
118
+ });
119
+
120
+ if (accessError || !hasAccess) {
122
121
  throw new Error('Project not found or access denied');
123
122
  }
124
123
 
@@ -44,18 +44,24 @@ export async function saveDecision(
44
44
  category: string = 'decision',
45
45
  tags: string[] = []
46
46
  ): Promise<SaveDecisionResponse> {
47
- // First, verify project ownership
48
- const { data: project, error: projectError } = await supabase
49
- .from('projects')
50
- .select('id, name')
51
- .eq('id', projectId)
52
- .eq('owner_id', userId)
53
- .single();
47
+ // 1. Verify project access
48
+ const { data: hasAccess, error: accessError } = await supabase
49
+ .rpc('check_project_access_secure', {
50
+ p_project_id: projectId,
51
+ p_user_id: userId
52
+ });
54
53
 
55
- if (projectError || !project) {
54
+ if (accessError || !hasAccess) {
56
55
  throw new Error('Project not found or access denied');
57
56
  }
58
57
 
58
+ // 2. Fetch project name for the response message
59
+ const { data: project } = await supabase
60
+ .from('projects')
61
+ .select('name')
62
+ .eq('id', projectId)
63
+ .single();
64
+
59
65
  // Build the full content with title, decision, and rationale
60
66
  const contentParts = [`# ${title}`, '', decision];
61
67
  if (rationale) {
@@ -98,6 +104,6 @@ export async function saveDecision(
98
104
  return {
99
105
  success: true,
100
106
  memoryId: memory.id,
101
- message: `✅ Decision "${title}" saved to project "${project.name}" with importance 9/10`
107
+ message: `✅ Decision "${title}" saved to project "${project?.name || projectId}" with importance 9/10`
102
108
  };
103
109
  }
@@ -14,7 +14,7 @@ registry.register({
14
14
  description: `Sven's Tool: Security Shield. Audits the database to ensure Row Level Security (RLS) is enforced.`,
15
15
  schema: AuditRlsStatusInputSchema,
16
16
  handler: async (args, context) => {
17
- const result = await auditRlsStatus(context.supabase, args);
17
+ const result = await auditRlsStatus(context.supabase, context.userId, args);
18
18
  return { content: [{ type: 'text', text: result.summary || 'No summary available' }] };
19
19
  }
20
20
  });
@@ -24,7 +24,7 @@ registry.register({
24
24
  description: `Frank's Tool: Security Oracle. Performs a diagnostic security audit against the Fortress Matrix.`,
25
25
  schema: AuditSecurityIntegrityInputSchema,
26
26
  handler: async (args, context) => {
27
- const result = await auditSecurityIntegrity(context.supabase, args);
27
+ const result = await auditSecurityIntegrity(context.supabase, context.userId, args);
28
28
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
29
29
  }
30
30
  });
@@ -35,8 +35,20 @@ registry.register({
35
35
  */
36
36
  export async function auditRlsStatus(
37
37
  supabase: SupabaseClient,
38
+ userId: string,
38
39
  input: AuditRlsStatusInput
39
40
  ) {
41
+ // 0. Verify project access
42
+ const { data: hasAccess, error: accessError } = await supabase
43
+ .rpc('check_project_access_secure', {
44
+ p_project_id: input.projectId,
45
+ p_user_id: userId
46
+ });
47
+
48
+ if (accessError || !hasAccess) {
49
+ throw new Error('Project not found or access denied');
50
+ }
51
+
40
52
  try {
41
53
  const { data, error } = await supabase.rpc('execute_sql', {
42
54
  query: `
@@ -97,8 +109,20 @@ export async function auditRlsStatus(
97
109
  */
98
110
  export async function auditSecurityIntegrity(
99
111
  supabase: SupabaseClient,
112
+ userId: string,
100
113
  input: { projectId: string, filePath: string, content: string }
101
114
  ) {
115
+ // 0. Verify project access
116
+ const { data: hasAccess, error: accessError } = await supabase
117
+ .rpc('check_project_access_secure', {
118
+ p_project_id: input.projectId,
119
+ p_user_id: userId
120
+ });
121
+
122
+ if (accessError || !hasAccess) {
123
+ throw new Error('Project not found or access denied');
124
+ }
125
+
102
126
  const violations: any[] = [];
103
127
  const { content, filePath } = input;
104
128
 
@@ -42,18 +42,24 @@ export async function submitIdea(
42
42
  category: string = 'feature',
43
43
  tags: string[] = []
44
44
  ): Promise<SubmitIdeaResponse> {
45
- // First, verify project ownership
46
- const { data: project, error: projectError } = await supabase
47
- .from('projects')
48
- .select('id, name')
49
- .eq('id', projectId)
50
- .eq('owner_id', userId)
51
- .single();
45
+ // 1. Verify project access
46
+ const { data: hasAccess, error: accessError } = await supabase
47
+ .rpc('check_project_access_secure', {
48
+ p_project_id: projectId,
49
+ p_user_id: userId
50
+ });
52
51
 
53
- if (projectError || !project) {
52
+ if (accessError || !hasAccess) {
54
53
  throw new Error('Project not found or access denied');
55
54
  }
56
55
 
56
+ // 2. Fetch project name for the response message
57
+ const { data: project } = await supabase
58
+ .from('projects')
59
+ .select('name')
60
+ .eq('id', projectId)
61
+ .single();
62
+
57
63
  // Insert the idea into saved_ideas
58
64
  const { data: idea, error: insertError } = await supabase
59
65
  .from('saved_ideas')
@@ -86,6 +92,6 @@ export async function submitIdea(
86
92
  return {
87
93
  success: true,
88
94
  ideaId: idea.id,
89
- message: `💡 Idea "${title}" submitted to Idea Lab for project "${project.name}". Status: Draft (awaiting review)`
95
+ message: `💡 Idea "${title}" submitted to Idea Lab for project "${project?.name || projectId}". Status: Draft (awaiting review)`
90
96
  };
91
97
  }
@@ -22,7 +22,7 @@ registry.register({
22
22
  based on project context and user settings.`,
23
23
  schema: GenerateCursorRulesInputSchema,
24
24
  handler: async (args, context) => {
25
- const result = await syncIdeRules(context.supabase, args.projectId);
25
+ const result = await syncIdeRules(context.supabase, context.userId, args.projectId);
26
26
 
27
27
  // Format response: Main file content + information about modular files
28
28
  let responseText = `FileName: ${result.fileName}\n\nContent:\n${result.content}`;
@@ -43,14 +43,27 @@ based on project context and user settings.`,
43
43
  * Uses the centralized rules-engine for consistent generation across all consumers.
44
44
  *
45
45
  * @param supabase - Supabase client instance
46
+ * @param userId - The user ID for access verification
46
47
  * @param projectId - The project ID to generate rules for
47
48
  * @returns Object with fileName, content, and the new modular files[] array
48
49
  */
49
50
  export async function syncIdeRules(
50
51
  supabase: SupabaseClient,
52
+ userId: string,
51
53
  projectId: string
52
54
  ): Promise<{ fileName: string; content: string; files: RuleFile[] }> {
53
- // 1. Fetch Project & Preferred IDE
55
+ // 1. Verify project access
56
+ const { data: hasAccess, error: accessError } = await supabase
57
+ .rpc('check_project_access_secure', {
58
+ p_project_id: projectId,
59
+ p_user_id: userId
60
+ });
61
+
62
+ if (accessError || !hasAccess) {
63
+ throw new Error('Project not found or access denied');
64
+ }
65
+
66
+ // 2. Fetch Project & Preferred IDE
54
67
  const { data: project, error: projectError } = await supabase
55
68
  .from('projects')
56
69
  .select('*')
@@ -58,7 +71,7 @@ export async function syncIdeRules(
58
71
  .single();
59
72
 
60
73
  if (projectError || !project) {
61
- throw new Error(`Project ${projectId} not found.`);
74
+ throw new Error(`Project ${projectId} details not found.`);
62
75
  }
63
76
 
64
77
  // 2. Determine IDE (Preference -> Fallback)
@@ -65,14 +65,13 @@ export async function refineLogic(
65
65
  const traceId = uuidv4();
66
66
 
67
67
  // Verify project access
68
- const { data: project, error: projectError } = await supabase
69
- .from('projects')
70
- .select('id, name')
71
- .eq('id', projectId)
72
- .eq('owner_id', userId)
73
- .single();
68
+ const { data: hasAccess, error: accessError } = await supabase
69
+ .rpc('check_project_access_secure', {
70
+ p_project_id: projectId,
71
+ p_user_id: userId
72
+ });
74
73
 
75
- if (projectError || !project) {
74
+ if (accessError || !hasAccess) {
76
75
  throw new Error(`Project access denied or not found: ${projectId}`);
77
76
  }
78
77
 
@@ -40,18 +40,24 @@ export async function updateRoadmap(
40
40
  chunkId?: string,
41
41
  title?: string
42
42
  ): Promise<UpdateRoadmapResponse> {
43
- // First, verify project ownership
44
- const { data: project, error: projectError } = await supabase
45
- .from('projects')
46
- .select('id, name')
47
- .eq('id', projectId)
48
- .eq('owner_id', userId)
49
- .single();
43
+ // 1. Verify project access
44
+ const { data: hasAccess, error: accessError } = await supabase
45
+ .rpc('check_project_access_secure', {
46
+ p_project_id: projectId,
47
+ p_user_id: userId
48
+ });
50
49
 
51
- if (projectError || !project) {
50
+ if (accessError || !hasAccess) {
52
51
  throw new Error('Project not found or access denied');
53
52
  }
54
53
 
54
+ // 2. Fetch project details (for response message if needed)
55
+ const { data: project } = await supabase
56
+ .from('projects')
57
+ .select('name')
58
+ .eq('id', projectId)
59
+ .single();
60
+
55
61
  // Find the roadmap chunk
56
62
  let targetChunk: { id: string; title: string; status: string } | null = null;
57
63