@equilateral_ai/mindmeld 3.4.0 → 3.5.1
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.
- package/hooks/pre-compact.js +269 -21
- package/hooks/session-end.js +112 -3
- package/hooks/session-start.js +139 -34
- package/package.json +1 -1
- package/scripts/auth-login.js +45 -8
- package/src/core/StandardsIngestion.js +3 -1
- package/src/handlers/collaborators/collaboratorList.js +4 -10
- package/src/handlers/correlations/correlationsProjectGet.js +4 -13
- package/src/handlers/github/githubDiscoverPatterns.js +4 -8
- package/src/handlers/github/githubPatternsReview.js +4 -8
- package/src/handlers/helpers/decisionFrames.js +29 -0
- package/src/handlers/helpers/index.js +14 -0
- package/src/handlers/helpers/mindmeldMcpCore.js +1103 -0
- package/src/handlers/helpers/predictiveCache.js +51 -0
- package/src/handlers/helpers/projectAccess.js +88 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +8 -573
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +342 -0
- package/src/handlers/standards/discoveriesGet.js +4 -8
- package/src/handlers/standards/projectStandardsGet.js +5 -11
- package/src/handlers/standards/projectStandardsPut.js +19 -14
- package/src/handlers/standards/standardsParseUpload.js +4 -8
- package/src/handlers/standards/standardsRelevantPost.js +126 -29
- package/src/handlers/users/userGet.js +3 -3
|
@@ -1,582 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MindMeld MCP Server — Lambda Handler
|
|
2
|
+
* MindMeld MCP Server — API Gateway Lambda Handler
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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 (
|
|
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
|
|
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 {
|
|
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 —
|
|
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
|
};
|