@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.
- package/README.md +1 -10
- package/hooks/pre-compact.js +213 -25
- package/hooks/session-end.js +112 -3
- package/hooks/session-start.js +635 -41
- package/hooks/subagent-start.js +150 -0
- package/hooks/subagent-stop.js +184 -0
- package/package.json +8 -7
- package/scripts/init-project.js +74 -33
- package/scripts/mcp-bridge.js +220 -0
- package/src/core/CorrelationAnalyzer.js +157 -0
- package/src/core/LLMPatternDetector.js +198 -0
- package/src/core/RelevanceDetector.js +123 -36
- package/src/core/StandardsIngestion.js +119 -18
- package/src/handlers/activity/activityGetMe.js +1 -1
- package/src/handlers/activity/activityGetTeam.js +100 -55
- package/src/handlers/admin/adminSetup.js +216 -0
- package/src/handlers/alerts/alertsAcknowledge.js +6 -6
- package/src/handlers/alerts/alertsGet.js +11 -11
- package/src/handlers/analytics/activitySummaryGet.js +34 -35
- package/src/handlers/analytics/coachingGet.js +11 -11
- package/src/handlers/analytics/convergenceGet.js +236 -0
- package/src/handlers/analytics/developerScoreGet.js +41 -111
- package/src/handlers/collaborators/collaboratorInvite.js +1 -1
- package/src/handlers/company/companyUsersDelete.js +141 -0
- package/src/handlers/company/companyUsersGet.js +90 -0
- package/src/handlers/company/companyUsersPost.js +267 -0
- package/src/handlers/company/companyUsersPut.js +76 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
- package/src/handlers/correlations/correlationsGet.js +8 -8
- package/src/handlers/correlations/correlationsProjectGet.js +5 -5
- package/src/handlers/enterprise/controlTowerGet.js +224 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +4 -2
- package/src/handlers/github/githubPatternsReview.js +7 -36
- package/src/handlers/health/healthGet.js +55 -0
- package/src/handlers/helpers/checkSuperAdmin.js +13 -14
- package/src/handlers/helpers/mindmeldMcpCore.js +594 -0
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +124 -0
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +243 -0
- package/src/handlers/notifications/sendNotification.js +18 -18
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
- package/src/handlers/projects/projectCreate.js +124 -10
- package/src/handlers/projects/projectDelete.js +4 -4
- package/src/handlers/projects/projectGet.js +8 -8
- package/src/handlers/projects/projectUpdate.js +4 -4
- package/src/handlers/reports/aiLeverage.js +34 -30
- package/src/handlers/reports/engineeringInvestment.js +16 -16
- package/src/handlers/reports/riskForecast.js +41 -21
- package/src/handlers/reports/standardsRoi.js +101 -9
- package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
- package/src/handlers/sessions/sessionStandardsPost.js +43 -7
- package/src/handlers/standards/discoveriesGet.js +93 -0
- package/src/handlers/standards/projectStandardsGet.js +2 -2
- package/src/handlers/standards/projectStandardsPut.js +2 -2
- package/src/handlers/standards/standardsRelevantPost.js +107 -12
- package/src/handlers/standards/standardsTransition.js +112 -15
- package/src/handlers/stripe/billingPortalPost.js +1 -1
- package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
- package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
- package/src/handlers/stripe/webhookPost.js +42 -14
- package/src/handlers/user/apiTokenCreate.js +71 -0
- package/src/handlers/user/apiTokenList.js +64 -0
- package/src/handlers/user/userSplashGet.js +90 -73
- package/src/handlers/users/cognitoPostConfirmation.js +37 -1
- package/src/handlers/users/cognitoPreSignUp.js +114 -0
- package/src/handlers/users/userGet.js +15 -11
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- 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
|
+
};
|