@equilateral_ai/mindmeld 3.3.0 → 3.4.0

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.
Files changed (69) hide show
  1. package/README.md +1 -10
  2. package/hooks/pre-compact.js +213 -25
  3. package/hooks/session-start.js +636 -42
  4. package/hooks/subagent-start.js +150 -0
  5. package/hooks/subagent-stop.js +184 -0
  6. package/package.json +8 -7
  7. package/scripts/init-project.js +74 -33
  8. package/scripts/mcp-bridge.js +220 -0
  9. package/src/core/CorrelationAnalyzer.js +157 -0
  10. package/src/core/LLMPatternDetector.js +198 -0
  11. package/src/core/RelevanceDetector.js +123 -36
  12. package/src/core/StandardsIngestion.js +119 -18
  13. package/src/handlers/activity/activityGetMe.js +1 -1
  14. package/src/handlers/activity/activityGetTeam.js +100 -55
  15. package/src/handlers/admin/adminSetup.js +216 -0
  16. package/src/handlers/alerts/alertsAcknowledge.js +6 -6
  17. package/src/handlers/alerts/alertsGet.js +11 -11
  18. package/src/handlers/analytics/activitySummaryGet.js +34 -35
  19. package/src/handlers/analytics/coachingGet.js +11 -11
  20. package/src/handlers/analytics/convergenceGet.js +236 -0
  21. package/src/handlers/analytics/developerScoreGet.js +41 -111
  22. package/src/handlers/collaborators/collaboratorInvite.js +1 -1
  23. package/src/handlers/company/companyUsersDelete.js +141 -0
  24. package/src/handlers/company/companyUsersGet.js +90 -0
  25. package/src/handlers/company/companyUsersPost.js +267 -0
  26. package/src/handlers/company/companyUsersPut.js +76 -0
  27. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
  28. package/src/handlers/correlations/correlationsGet.js +8 -8
  29. package/src/handlers/correlations/correlationsProjectGet.js +5 -5
  30. package/src/handlers/enterprise/controlTowerGet.js +224 -0
  31. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
  32. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
  33. package/src/handlers/github/githubConnectionStatus.js +1 -1
  34. package/src/handlers/github/githubDiscoverPatterns.js +4 -2
  35. package/src/handlers/github/githubPatternsReview.js +7 -36
  36. package/src/handlers/health/healthGet.js +55 -0
  37. package/src/handlers/helpers/checkSuperAdmin.js +13 -14
  38. package/src/handlers/helpers/subscriptionTiers.js +27 -27
  39. package/src/handlers/mcp/mcpHandler.js +569 -0
  40. package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
  41. package/src/handlers/notifications/sendNotification.js +18 -18
  42. package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
  43. package/src/handlers/projects/projectCreate.js +124 -10
  44. package/src/handlers/projects/projectDelete.js +4 -4
  45. package/src/handlers/projects/projectGet.js +8 -8
  46. package/src/handlers/projects/projectUpdate.js +4 -4
  47. package/src/handlers/reports/aiLeverage.js +34 -30
  48. package/src/handlers/reports/engineeringInvestment.js +16 -16
  49. package/src/handlers/reports/riskForecast.js +41 -21
  50. package/src/handlers/reports/standardsRoi.js +101 -9
  51. package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
  52. package/src/handlers/sessions/sessionStandardsPost.js +43 -7
  53. package/src/handlers/standards/discoveriesGet.js +93 -0
  54. package/src/handlers/standards/projectStandardsGet.js +2 -2
  55. package/src/handlers/standards/projectStandardsPut.js +2 -2
  56. package/src/handlers/standards/standardsRelevantPost.js +107 -12
  57. package/src/handlers/standards/standardsTransition.js +112 -15
  58. package/src/handlers/stripe/billingPortalPost.js +1 -1
  59. package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
  60. package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
  61. package/src/handlers/stripe/webhookPost.js +42 -14
  62. package/src/handlers/user/apiTokenCreate.js +71 -0
  63. package/src/handlers/user/apiTokenList.js +64 -0
  64. package/src/handlers/user/userSplashGet.js +90 -73
  65. package/src/handlers/users/cognitoPostConfirmation.js +37 -1
  66. package/src/handlers/users/cognitoPreSignUp.js +114 -0
  67. package/src/handlers/users/userGet.js +12 -8
  68. package/src/handlers/webhooks/githubWebhook.js +117 -125
  69. package/src/index.js +46 -51
@@ -0,0 +1,689 @@
1
+ /**
2
+ * MindMeld MCP Server — Lambda Handler
3
+ *
4
+ * Standards injection and governance for AI-assisted development via MCP.
5
+ * Implements JSON-RPC 2.0 over HTTP (Streamable HTTP transport).
6
+ *
7
+ * POST /api/mcp/mindmeld - JSON-RPC messages
8
+ * GET /api/mcp/mindmeld - 405 (SSE not supported in Lambda)
9
+ * DELETE /api/mcp/mindmeld - 200 (session close, no-op)
10
+ *
11
+ * Auth: X-MindMeld-Token header (API token, not Cognito)
12
+ * Separate from JARVIS MCP (agent orchestration) — this serves
13
+ * external MCP clients (Cline, Claude Code) for standards injection.
14
+ */
15
+
16
+ const { executeQuery } = require('./dbOperations');
17
+ const crypto = require('crypto');
18
+
19
+ const SERVER_INFO = {
20
+ name: 'mindmeld',
21
+ version: '0.1.0',
22
+ description: 'Standards injection and governance for AI-assisted development'
23
+ };
24
+
25
+ const PROTOCOL_VERSION = '2025-03-26';
26
+
27
+ const CORS_HEADERS = {
28
+ 'Access-Control-Allow-Origin': '*',
29
+ 'Access-Control-Allow-Methods': 'POST, GET, DELETE, OPTIONS',
30
+ 'Access-Control-Allow-Headers': 'Content-Type, Accept, Mcp-Session-Id, X-MindMeld-Token',
31
+ };
32
+
33
+ // ============================================================
34
+ // Category Weights (same as standardsRelevantPost.js)
35
+ // ============================================================
36
+
37
+ const CATEGORY_WEIGHTS = {
38
+ 'serverless-saas-aws': 1.0,
39
+ 'frontend-development': 1.0,
40
+ 'database': 0.9,
41
+ 'backend': 0.9,
42
+ 'compliance-security': 0.9,
43
+ 'deployment': 0.8,
44
+ 'testing': 0.7,
45
+ 'real-time-systems': 0.7,
46
+ 'well-architected': 0.7,
47
+ 'cost-optimization': 0.7,
48
+ 'multi-agent-orchestration': 0.1,
49
+ };
50
+
51
+ // ============================================================
52
+ // Tool Definitions
53
+ // ============================================================
54
+
55
+ const TOOLS = [
56
+ {
57
+ name: 'mindmeld_init_session',
58
+ description: 'Initialize a MindMeld standards injection session. Scans project context, identifies relevant standards, returns injected rules and session token.',
59
+ inputSchema: {
60
+ type: 'object',
61
+ properties: {
62
+ project_path: { type: 'string', description: 'Absolute path to the project root' },
63
+ task_description: { type: 'string', description: 'Optional: what the developer intends to work on this session' },
64
+ team_id: { type: 'string', description: 'Team identifier for corpus lookup. Uses personal corpus if omitted.' }
65
+ },
66
+ required: ['project_path']
67
+ }
68
+ },
69
+ {
70
+ name: 'mindmeld_record_correction',
71
+ description: 'Record a correction to AI output. Feeds the standards maturity pipeline. Corrections drive pattern detection and eventually promote to Provisional standards.',
72
+ inputSchema: {
73
+ type: 'object',
74
+ properties: {
75
+ session_id: { type: 'string', description: 'Session token from mindmeld_init_session' },
76
+ original_output: { type: 'string', description: 'What the AI generated' },
77
+ corrected_output: { type: 'string', description: 'What the developer changed it to' },
78
+ correction_note: { type: 'string', description: 'Optional: developer explanation of why the correction was made' },
79
+ file_context: { type: 'string', description: 'Optional: filename or path where correction occurred' }
80
+ },
81
+ required: ['session_id', 'original_output', 'corrected_output']
82
+ }
83
+ },
84
+ {
85
+ name: 'mindmeld_get_standards',
86
+ description: 'On-demand lookup for specific standards or maturity status. Use for UI display, not injection — mindmeld_init_session handles injection.',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: {
90
+ team_id: { type: 'string', description: 'Team identifier (optional, resolved from token)' },
91
+ filter: {
92
+ type: 'object',
93
+ properties: {
94
+ maturity: { type: 'array', items: { type: 'string' }, description: 'Filter by maturity: provisional, solidified, reinforced' },
95
+ standard_name: { type: 'string', description: 'Filter by standard name (partial match)' },
96
+ limit: { type: 'integer', description: 'Max results (default 20)' }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ ];
103
+
104
+ // ============================================================
105
+ // Auth: API Token Validation
106
+ // ============================================================
107
+
108
+ async function validateApiToken(headers) {
109
+ const token = headers['x-mindmeld-token'] || headers['X-MindMeld-Token'];
110
+ if (!token) {
111
+ return { error: 'auth_invalid', message: 'X-MindMeld-Token header is required' };
112
+ }
113
+
114
+ const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
115
+
116
+ const result = await executeQuery(`
117
+ SELECT t.token_id, t.email_address, t.client_id, t.company_id,
118
+ c.subscription_tier, c.subscription_status
119
+ FROM rapport.api_tokens t
120
+ JOIN rapport.clients c ON t.client_id = c.client_id
121
+ WHERE t.token_hash = $1
122
+ AND t.status = 'active'
123
+ `, [tokenHash]);
124
+
125
+ if (result.rows.length === 0) {
126
+ return { error: 'auth_invalid', message: 'Invalid or expired API token' };
127
+ }
128
+
129
+ const row = result.rows[0];
130
+
131
+ // Require active subscription (no free tier)
132
+ if (!row.subscription_tier || row.subscription_tier === 'free') {
133
+ return { error: 'auth_invalid', message: 'Active MindMeld subscription required. Subscribe at app.mindmeld.dev' };
134
+ }
135
+
136
+ // Fire-and-forget: update usage stats
137
+ executeQuery(
138
+ 'UPDATE rapport.api_tokens SET last_used_at = NOW(), request_count = request_count + 1 WHERE token_id = $1',
139
+ [row.token_id]
140
+ ).catch(() => {});
141
+
142
+ return {
143
+ user: {
144
+ email: row.email_address,
145
+ client_id: row.client_id,
146
+ company_id: row.company_id,
147
+ subscription_tier: row.subscription_tier
148
+ }
149
+ };
150
+ }
151
+
152
+ // ============================================================
153
+ // Relevance Scoring (same algorithm as standardsRelevantPost.js)
154
+ // ============================================================
155
+
156
+ function rankStandards(standards, recentCategories) {
157
+ return standards.map(standard => {
158
+ let score = 0;
159
+ score += (standard.correlation || 1.0) * 40;
160
+
161
+ const maturityScores = { enforced: 30, validated: 20, recommended: 10, provisional: 5 };
162
+ score += maturityScores[standard.maturity] || 0;
163
+
164
+ const categoryWeight = CATEGORY_WEIGHTS[standard.category] || 0.5;
165
+ score += categoryWeight * 20;
166
+
167
+ if (standard.applicable_files && standard.applicable_files.length > 0) score += 5;
168
+ if (standard.cost_impact && standard.cost_impact.severity === 'critical') score += 10;
169
+
170
+ if (standard.anti_patterns) {
171
+ const apCount = Array.isArray(standard.anti_patterns)
172
+ ? standard.anti_patterns.length
173
+ : Object.keys(standard.anti_patterns).length;
174
+ if (apCount > 0) score += 5;
175
+ }
176
+
177
+ const isWorkflow = (standard.rule && standard.rule.startsWith('WORKFLOW:'))
178
+ || (Array.isArray(standard.keywords) && standard.keywords.includes('workflow'));
179
+ if (isWorkflow) score += 10;
180
+
181
+ if (recentCategories && recentCategories[standard.category]) {
182
+ const usageCount = recentCategories[standard.category];
183
+ let rawBonus;
184
+ if (usageCount >= 8) rawBonus = 25;
185
+ else if (usageCount >= 4) rawBonus = 18;
186
+ else rawBonus = 10;
187
+ score += rawBonus * categoryWeight;
188
+ }
189
+
190
+ return { ...standard, relevance_score: Math.round(score * 10) / 10 };
191
+ }).sort((a, b) => b.relevance_score - a.relevance_score);
192
+ }
193
+
194
+ // ============================================================
195
+ // Formatted Injection (same format as hooks/session-start.js)
196
+ // ============================================================
197
+
198
+ function formatInjection(sessionId, standards) {
199
+ const sections = [];
200
+
201
+ sections.push('# MindMeld Standards Injection');
202
+ sections.push(`<!-- session:${sessionId} -->`);
203
+ sections.push('');
204
+ sections.push('\u00A9 2025 Equilateral AI (Pareidolia LLC). All rights reserved.');
205
+ sections.push('Licensed for use within MindMeld platform only. Redistribution prohibited.');
206
+ sections.push('');
207
+
208
+ if (standards.length > 0) {
209
+ sections.push('## Relevant Standards');
210
+ sections.push('');
211
+
212
+ for (const standard of standards) {
213
+ sections.push(`### ${standard.element}`);
214
+ sections.push(`**Category**: ${standard.category}`);
215
+ sections.push(`**Rule**: ${standard.rule}`);
216
+
217
+ if (standard.examples && standard.examples.length > 0) {
218
+ const example = standard.examples[0];
219
+ const exampleCode = typeof example === 'string' ? example : (example?.code || example?.description || '');
220
+ if (exampleCode) {
221
+ sections.push('');
222
+ sections.push('**Example**:');
223
+ sections.push('```javascript');
224
+ sections.push(exampleCode);
225
+ sections.push('```');
226
+ }
227
+ }
228
+
229
+ if (standard.anti_patterns && standard.anti_patterns.length > 0) {
230
+ sections.push('');
231
+ sections.push('**Anti-patterns**:');
232
+ for (const ap of standard.anti_patterns) {
233
+ const desc = typeof ap === 'string' ? ap : (ap?.description || '');
234
+ if (desc) sections.push(`- \u274C ${desc}`);
235
+ }
236
+ }
237
+
238
+ sections.push('');
239
+ }
240
+ }
241
+
242
+ sections.push('---');
243
+ sections.push('*Context provided by MindMeld - mindmeld.dev*');
244
+
245
+ return sections.join('\n');
246
+ }
247
+
248
+ // ============================================================
249
+ // Tool Implementations
250
+ // ============================================================
251
+
252
+ async function callTool(name, args, user) {
253
+ switch (name) {
254
+ case 'mindmeld_init_session':
255
+ return await toolInitSession(args, user);
256
+ case 'mindmeld_record_correction':
257
+ return await toolRecordCorrection(args, user);
258
+ case 'mindmeld_get_standards':
259
+ return await toolGetStandards(args, user);
260
+ default:
261
+ throw new Error(`Unknown tool: ${name}`);
262
+ }
263
+ }
264
+
265
+ async function toolInitSession(args, user) {
266
+ const { project_path, task_description } = args;
267
+ const sessionId = crypto.randomUUID();
268
+
269
+ // Try to match project by name for the user's company
270
+ let projectId = null;
271
+ if (project_path) {
272
+ const projectName = project_path.split('/').filter(Boolean).pop();
273
+ try {
274
+ const projectResult = await executeQuery(`
275
+ SELECT project_id FROM rapport.projects
276
+ WHERE company_id = $1 AND LOWER(project_name) = LOWER($2)
277
+ LIMIT 1
278
+ `, [user.company_id, projectName]);
279
+ if (projectResult.rows.length > 0) {
280
+ projectId = projectResult.rows[0].project_id;
281
+ }
282
+ } catch (err) {
283
+ console.error('[MCP] Project lookup failed:', err.message);
284
+ }
285
+ }
286
+
287
+ // Get recency data for scoring boost
288
+ const recentCategories = {};
289
+ try {
290
+ const recencyResult = await executeQuery(`
291
+ SELECT sp.category, COUNT(*) as usage_count
292
+ FROM rapport.session_standards ss
293
+ JOIN rapport.sessions s ON s.session_id = ss.session_id
294
+ JOIN rapport.standards_patterns sp ON sp.pattern_id = ss.standard_id
295
+ WHERE s.email_address = $1
296
+ AND s.started_at >= NOW() - INTERVAL '7 days'
297
+ GROUP BY sp.category
298
+ ORDER BY usage_count DESC LIMIT 5
299
+ `, [user.email]);
300
+ for (const row of recencyResult.rows) {
301
+ recentCategories[row.category] = parseInt(row.usage_count, 10);
302
+ }
303
+ } catch (err) {
304
+ console.error('[MCP] Recency query failed:', err.message);
305
+ }
306
+
307
+ // Default to broad categories (no filesystem scanning in Lambda)
308
+ const categories = [
309
+ 'serverless-saas-aws', 'frontend-development', 'database', 'backend',
310
+ 'compliance-security', 'well-architected', 'cost-optimization', 'deployment', 'testing'
311
+ ];
312
+
313
+ // Merge recency categories
314
+ for (const cat of Object.keys(recentCategories)) {
315
+ if (!categories.includes(cat)) categories.push(cat);
316
+ }
317
+
318
+ // Query standards
319
+ const result = await executeQuery(`
320
+ SELECT pattern_id, element, title, rule, category, keywords, correlation,
321
+ maturity, applicable_files, anti_patterns, examples, cost_impact, source
322
+ FROM rapport.standards_patterns
323
+ WHERE category = ANY($1::varchar[])
324
+ AND maturity IN ('enforced', 'validated', 'recommended')
325
+ ORDER BY CASE WHEN maturity = 'enforced' THEN 1 WHEN maturity = 'validated' THEN 2 ELSE 3 END,
326
+ correlation DESC
327
+ `, [categories]);
328
+
329
+ if (result.rows.length === 0) {
330
+ return {
331
+ content: [{ type: 'text', text: JSON.stringify({ error: 'corpus_empty', message: 'No standards found in corpus' }) }],
332
+ isError: true
333
+ };
334
+ }
335
+
336
+ // Rank, deduplicate, apply diversity caps
337
+ let ranked = rankStandards(result.rows, recentCategories);
338
+
339
+ const seenElements = new Set();
340
+ ranked = ranked.filter(s => {
341
+ if (seenElements.has(s.element)) return false;
342
+ seenElements.add(s.element);
343
+ return true;
344
+ });
345
+
346
+ const MAX_PER_CATEGORY = 2;
347
+ const MAX_PER_TITLE = 1;
348
+ const top = [];
349
+ const categoryCounts = {};
350
+ const titleCounts = {};
351
+ for (const standard of ranked) {
352
+ const cat = standard.category;
353
+ const title = standard.title || standard.element;
354
+ categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
355
+ titleCounts[title] = (titleCounts[title] || 0) + 1;
356
+ if (categoryCounts[cat] <= MAX_PER_CATEGORY && titleCounts[title] <= MAX_PER_TITLE) {
357
+ top.push(standard);
358
+ if (top.length >= 10) break;
359
+ }
360
+ }
361
+
362
+ // Format injection markdown
363
+ const formattedInjection = formatInjection(sessionId, top);
364
+
365
+ // Fire-and-forget: record session + standards
366
+ executeQuery(`
367
+ INSERT INTO rapport.sessions (session_id, project_id, email_address, started_at, session_data)
368
+ VALUES ($1, $2, $3, NOW(), $4)
369
+ ON CONFLICT (session_id) DO NOTHING
370
+ `, [sessionId, projectId, user.email, JSON.stringify({ source: 'mcp', task_description: task_description || null })])
371
+ .catch(err => console.error('[MCP] Session record failed:', err.message));
372
+
373
+ for (const standard of top) {
374
+ executeQuery(`
375
+ INSERT INTO rapport.session_standards (session_id, standard_id, standard_name, relevance_score, created_at)
376
+ VALUES ($1, $2, $3, $4, NOW())
377
+ ON CONFLICT (session_id, standard_id) DO UPDATE SET relevance_score = EXCLUDED.relevance_score
378
+ `, [sessionId, standard.pattern_id, standard.element, standard.relevance_score])
379
+ .catch(err => console.error('[MCP] Standard record failed:', err.message));
380
+ }
381
+
382
+ // Get corpus size for summary
383
+ let corpusSize = result.rows.length;
384
+ try {
385
+ const countResult = await executeQuery('SELECT COUNT(*) as cnt FROM rapport.standards_patterns');
386
+ corpusSize = parseInt(countResult.rows[0].cnt, 10);
387
+ } catch (err) {
388
+ // Use result count as fallback
389
+ }
390
+
391
+ const response = {
392
+ session_id: sessionId,
393
+ injected_rules: top.map(s => ({
394
+ rule_id: s.pattern_id,
395
+ standard: s.element,
396
+ maturity: s.maturity,
397
+ text: s.rule,
398
+ relevance_score: s.relevance_score
399
+ })),
400
+ injection_summary: {
401
+ rule_count: top.length,
402
+ token_estimate: Math.ceil(formattedInjection.length / 4),
403
+ standards_matched: ranked.length,
404
+ corpus_size: corpusSize
405
+ },
406
+ formatted_injection: formattedInjection
407
+ };
408
+
409
+ return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
410
+ }
411
+
412
+ async function toolRecordCorrection(args, user) {
413
+ const { session_id, original_output, corrected_output, correction_note, file_context } = args;
414
+ const correctionId = crypto.randomUUID();
415
+
416
+ // Simple pattern match: check if correction keywords match any existing standard rules
417
+ let matchedStandardId = null;
418
+ let patternDetected = false;
419
+ try {
420
+ // Extract significant words from the correction
421
+ const correctionWords = corrected_output.toLowerCase().split(/\s+/).filter(w => w.length > 4);
422
+ if (correctionWords.length > 0) {
423
+ const searchTerms = correctionWords.slice(0, 5).join(' | ');
424
+ const matchResult = await executeQuery(`
425
+ SELECT pattern_id, element FROM rapport.standards_patterns
426
+ WHERE to_tsvector('english', rule) @@ to_tsquery('english', $1)
427
+ LIMIT 1
428
+ `, [searchTerms]);
429
+ if (matchResult.rows.length > 0) {
430
+ matchedStandardId = matchResult.rows[0].pattern_id;
431
+ patternDetected = true;
432
+ }
433
+ }
434
+ } catch (err) {
435
+ console.error('[MCP] Pattern match failed:', err.message);
436
+ }
437
+
438
+ // Store correction
439
+ await executeQuery(`
440
+ INSERT INTO rapport.mcp_corrections
441
+ (correction_id, session_id, email_address, company_id, original_output,
442
+ corrected_output, correction_note, file_context, matched_standard_id, status)
443
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'recorded')
444
+ `, [correctionId, session_id, user.email, user.company_id,
445
+ original_output, corrected_output, correction_note || null,
446
+ file_context || null, matchedStandardId]);
447
+
448
+ const response = {
449
+ correction_id: correctionId,
450
+ pattern_detected: patternDetected,
451
+ matched_standard: matchedStandardId,
452
+ status: 'recorded'
453
+ };
454
+
455
+ return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
456
+ }
457
+
458
+ async function toolGetStandards(args, user) {
459
+ const filter = args.filter || {};
460
+ const limit = Math.min(parseInt(filter.limit) || 20, 100);
461
+ const maturityFilter = filter.maturity;
462
+ const nameFilter = filter.standard_name;
463
+
464
+ let query = `
465
+ SELECT pattern_id as standard_id, element as name, maturity,
466
+ COUNT(*) OVER() as total_count,
467
+ (SELECT COUNT(*) FROM rapport.session_standards WHERE standard_id = sp.pattern_id) as session_count
468
+ FROM rapport.standards_patterns sp
469
+ WHERE 1=1
470
+ `;
471
+ const params = [];
472
+
473
+ if (maturityFilter && Array.isArray(maturityFilter) && maturityFilter.length > 0) {
474
+ params.push(maturityFilter);
475
+ query += ` AND maturity = ANY($${params.length}::varchar[])`;
476
+ }
477
+
478
+ if (nameFilter) {
479
+ params.push(`%${nameFilter}%`);
480
+ query += ` AND (element ILIKE $${params.length} OR title ILIKE $${params.length})`;
481
+ }
482
+
483
+ query += ` ORDER BY maturity DESC, element ASC`;
484
+ params.push(limit);
485
+ query += ` LIMIT $${params.length}`;
486
+
487
+ const result = await executeQuery(query, params);
488
+
489
+ // Corpus summary
490
+ const summaryResult = await executeQuery(`
491
+ SELECT
492
+ COUNT(*) as total_standards,
493
+ COUNT(*) FILTER (WHERE maturity = 'enforced') as reinforced_count,
494
+ COUNT(*) FILTER (WHERE maturity = 'validated') as solidified_count,
495
+ COUNT(*) FILTER (WHERE maturity IN ('recommended', 'provisional')) as provisional_count
496
+ FROM rapport.standards_patterns
497
+ `);
498
+ const summary = summaryResult.rows[0] || {};
499
+
500
+ const response = {
501
+ standards: result.rows.map(r => ({
502
+ standard_id: r.standard_id,
503
+ name: r.name,
504
+ maturity: r.maturity,
505
+ rule_count: 1,
506
+ session_count: parseInt(r.session_count, 10) || 0,
507
+ })),
508
+ corpus_summary: {
509
+ total_standards: parseInt(summary.total_standards, 10) || 0,
510
+ total_rules: parseInt(summary.total_standards, 10) || 0,
511
+ reinforced_count: parseInt(summary.reinforced_count, 10) || 0,
512
+ solidified_count: parseInt(summary.solidified_count, 10) || 0,
513
+ provisional_count: parseInt(summary.provisional_count, 10) || 0,
514
+ }
515
+ };
516
+
517
+ return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
518
+ }
519
+
520
+ // ============================================================
521
+ // JSON-RPC Message Router
522
+ // ============================================================
523
+
524
+ async function handleJsonRpc(message, user) {
525
+ const { method, params, id } = message;
526
+
527
+ switch (method) {
528
+ case 'initialize':
529
+ return {
530
+ jsonrpc: '2.0',
531
+ id,
532
+ result: {
533
+ protocolVersion: PROTOCOL_VERSION,
534
+ capabilities: {
535
+ tools: { listChanged: false }
536
+ },
537
+ serverInfo: SERVER_INFO
538
+ }
539
+ };
540
+
541
+ case 'notifications/initialized':
542
+ return null;
543
+
544
+ case 'ping':
545
+ return { jsonrpc: '2.0', id, result: {} };
546
+
547
+ case 'tools/list':
548
+ return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
549
+
550
+ case 'tools/call': {
551
+ const { name, arguments: args } = params;
552
+ try {
553
+ const result = await callTool(name, args || {}, user);
554
+ return { jsonrpc: '2.0', id, result };
555
+ } catch (error) {
556
+ console.error(`[MCP] Tool ${name} error:`, error.message);
557
+ return {
558
+ jsonrpc: '2.0',
559
+ id,
560
+ result: {
561
+ content: [{ type: 'text', text: JSON.stringify({ error: 'timeout', message: error.message }) }],
562
+ isError: true
563
+ }
564
+ };
565
+ }
566
+ }
567
+
568
+ default:
569
+ return {
570
+ jsonrpc: '2.0',
571
+ id,
572
+ error: { code: -32601, message: `Method not found: ${method}` }
573
+ };
574
+ }
575
+ }
576
+
577
+ // ============================================================
578
+ // Lambda Handler
579
+ // ============================================================
580
+
581
+ exports.handler = async (event) => {
582
+ const method = event.httpMethod;
583
+
584
+ // CORS preflight
585
+ if (method === 'OPTIONS') {
586
+ return {
587
+ statusCode: 200,
588
+ headers: { ...CORS_HEADERS, 'Access-Control-Max-Age': '86400' },
589
+ body: ''
590
+ };
591
+ }
592
+
593
+ // GET — SSE not supported in Lambda
594
+ if (method === 'GET') {
595
+ return {
596
+ statusCode: 405,
597
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', Allow: 'POST, DELETE, OPTIONS' },
598
+ body: JSON.stringify({
599
+ jsonrpc: '2.0',
600
+ error: { code: -32000, message: 'SSE streaming not supported. Use POST for JSON-RPC requests.' },
601
+ id: null
602
+ })
603
+ };
604
+ }
605
+
606
+ // DELETE — Session close (no-op)
607
+ if (method === 'DELETE') {
608
+ return {
609
+ statusCode: 200,
610
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
611
+ body: ''
612
+ };
613
+ }
614
+
615
+ // POST — JSON-RPC
616
+ if (method !== 'POST') {
617
+ return {
618
+ statusCode: 405,
619
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', Allow: 'POST, GET, DELETE, OPTIONS' },
620
+ body: JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed' }, id: null })
621
+ };
622
+ }
623
+
624
+ // Validate API token
625
+ const authResult = await validateApiToken(event.headers || {});
626
+ if (authResult.error) {
627
+ return {
628
+ statusCode: 200,
629
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
630
+ body: JSON.stringify({
631
+ jsonrpc: '2.0',
632
+ error: { code: -32000, message: authResult.message },
633
+ id: null
634
+ })
635
+ };
636
+ }
637
+
638
+ try {
639
+ let body;
640
+ try {
641
+ body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body;
642
+ } catch (e) {
643
+ return {
644
+ statusCode: 400,
645
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
646
+ body: JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error: invalid JSON' }, id: null })
647
+ };
648
+ }
649
+
650
+ // Handle batch requests
651
+ if (Array.isArray(body)) {
652
+ const responses = [];
653
+ for (const msg of body) {
654
+ const response = await handleJsonRpc(msg, authResult.user);
655
+ if (response) responses.push(response);
656
+ }
657
+ return {
658
+ statusCode: responses.length > 0 ? 200 : 202,
659
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
660
+ body: responses.length > 0 ? JSON.stringify(responses) : ''
661
+ };
662
+ }
663
+
664
+ // Handle single request
665
+ const response = await handleJsonRpc(body, authResult.user);
666
+
667
+ if (!response) {
668
+ return { statusCode: 202, headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' }, body: '' };
669
+ }
670
+
671
+ return {
672
+ statusCode: 200,
673
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
674
+ body: JSON.stringify(response)
675
+ };
676
+
677
+ } catch (error) {
678
+ console.error('[MCP] Handler error:', error);
679
+ return {
680
+ statusCode: 200,
681
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
682
+ body: JSON.stringify({
683
+ jsonrpc: '2.0',
684
+ error: { code: -32603, message: 'Internal server error' },
685
+ id: null
686
+ })
687
+ };
688
+ }
689
+ };