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