@equilateral_ai/mindmeld 3.4.0 → 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.
@@ -1,582 +1,17 @@
1
1
  /**
2
- * MindMeld MCP Server — Lambda Handler
2
+ * MindMeld MCP Server — API Gateway Lambda Handler
3
3
  *
4
- * Standards injection and governance for AI-assisted development via MCP.
5
- * Implements JSON-RPC 2.0 over HTTP (Streamable HTTP transport).
4
+ * Thin wrapper over mindmeldMcpCore for API Gateway (REST) transport.
5
+ * Business logic lives in src/handlers/helpers/mindmeldMcpCore.js.
6
6
  *
7
7
  * POST /api/mcp/mindmeld - JSON-RPC messages
8
- * GET /api/mcp/mindmeld - 405 (SSE not supported in Lambda)
8
+ * GET /api/mcp/mindmeld - 405 (use Function URL for SSE)
9
9
  * DELETE /api/mcp/mindmeld - 200 (session close, no-op)
10
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.
11
+ * Auth: X-MindMeld-Token header OR Authorization: Bearer token
14
12
  */
15
13
 
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
- // ============================================================
14
+ const { validateApiToken, handleJsonRpc, CORS_HEADERS } = require('./mindmeldMcpCore');
580
15
 
581
16
  exports.handler = async (event) => {
582
17
  const method = event.httpMethod;
@@ -590,14 +25,14 @@ exports.handler = async (event) => {
590
25
  };
591
26
  }
592
27
 
593
- // GET — SSE not supported in Lambda
28
+ // GET — direct clients to the streaming Function URL
594
29
  if (method === 'GET') {
595
30
  return {
596
31
  statusCode: 405,
597
32
  headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', Allow: 'POST, DELETE, OPTIONS' },
598
33
  body: JSON.stringify({
599
34
  jsonrpc: '2.0',
600
- error: { code: -32000, message: 'SSE streaming not supported. Use POST for JSON-RPC requests.' },
35
+ error: { code: -32000, message: 'SSE streaming not supported on this endpoint. Use POST for JSON-RPC requests.' },
601
36
  id: null
602
37
  })
603
38
  };