@equilateral_ai/mindmeld 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +1 -10
  2. package/hooks/pre-compact.js +213 -25
  3. package/hooks/session-start.js +636 -42
  4. package/hooks/subagent-start.js +150 -0
  5. package/hooks/subagent-stop.js +184 -0
  6. package/package.json +8 -7
  7. package/scripts/init-project.js +74 -33
  8. package/scripts/mcp-bridge.js +220 -0
  9. package/src/core/CorrelationAnalyzer.js +157 -0
  10. package/src/core/LLMPatternDetector.js +198 -0
  11. package/src/core/RelevanceDetector.js +123 -36
  12. package/src/core/StandardsIngestion.js +119 -18
  13. package/src/handlers/activity/activityGetMe.js +1 -1
  14. package/src/handlers/activity/activityGetTeam.js +100 -55
  15. package/src/handlers/admin/adminSetup.js +216 -0
  16. package/src/handlers/alerts/alertsAcknowledge.js +6 -6
  17. package/src/handlers/alerts/alertsGet.js +11 -11
  18. package/src/handlers/analytics/activitySummaryGet.js +34 -35
  19. package/src/handlers/analytics/coachingGet.js +11 -11
  20. package/src/handlers/analytics/convergenceGet.js +236 -0
  21. package/src/handlers/analytics/developerScoreGet.js +41 -111
  22. package/src/handlers/collaborators/collaboratorInvite.js +1 -1
  23. package/src/handlers/company/companyUsersDelete.js +141 -0
  24. package/src/handlers/company/companyUsersGet.js +90 -0
  25. package/src/handlers/company/companyUsersPost.js +267 -0
  26. package/src/handlers/company/companyUsersPut.js +76 -0
  27. package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
  28. package/src/handlers/correlations/correlationsGet.js +8 -8
  29. package/src/handlers/correlations/correlationsProjectGet.js +5 -5
  30. package/src/handlers/enterprise/controlTowerGet.js +224 -0
  31. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
  32. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
  33. package/src/handlers/github/githubConnectionStatus.js +1 -1
  34. package/src/handlers/github/githubDiscoverPatterns.js +4 -2
  35. package/src/handlers/github/githubPatternsReview.js +7 -36
  36. package/src/handlers/health/healthGet.js +55 -0
  37. package/src/handlers/helpers/checkSuperAdmin.js +13 -14
  38. package/src/handlers/helpers/subscriptionTiers.js +27 -27
  39. package/src/handlers/mcp/mcpHandler.js +569 -0
  40. package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
  41. package/src/handlers/notifications/sendNotification.js +18 -18
  42. package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
  43. package/src/handlers/projects/projectCreate.js +124 -10
  44. package/src/handlers/projects/projectDelete.js +4 -4
  45. package/src/handlers/projects/projectGet.js +8 -8
  46. package/src/handlers/projects/projectUpdate.js +4 -4
  47. package/src/handlers/reports/aiLeverage.js +34 -30
  48. package/src/handlers/reports/engineeringInvestment.js +16 -16
  49. package/src/handlers/reports/riskForecast.js +41 -21
  50. package/src/handlers/reports/standardsRoi.js +101 -9
  51. package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
  52. package/src/handlers/sessions/sessionStandardsPost.js +43 -7
  53. package/src/handlers/standards/discoveriesGet.js +93 -0
  54. package/src/handlers/standards/projectStandardsGet.js +2 -2
  55. package/src/handlers/standards/projectStandardsPut.js +2 -2
  56. package/src/handlers/standards/standardsRelevantPost.js +107 -12
  57. package/src/handlers/standards/standardsTransition.js +112 -15
  58. package/src/handlers/stripe/billingPortalPost.js +1 -1
  59. package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
  60. package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
  61. package/src/handlers/stripe/webhookPost.js +42 -14
  62. package/src/handlers/user/apiTokenCreate.js +71 -0
  63. package/src/handlers/user/apiTokenList.js +64 -0
  64. package/src/handlers/user/userSplashGet.js +90 -73
  65. package/src/handlers/users/cognitoPostConfirmation.js +37 -1
  66. package/src/handlers/users/cognitoPreSignUp.js +114 -0
  67. package/src/handlers/users/userGet.js +12 -8
  68. package/src/handlers/webhooks/githubWebhook.js +117 -125
  69. package/src/index.js +46 -51
@@ -0,0 +1,569 @@
1
+ /**
2
+ * JARVIS MCP Server - Lambda Handler
3
+ *
4
+ * Implements MCP (Model Context Protocol) JSON-RPC directly over HTTP.
5
+ * No SDK transport layer needed — Lambda's request/response model maps
6
+ * cleanly to stateless JSON-RPC.
7
+ *
8
+ * POST /mcp - JSON-RPC messages (initialize, tools/list, tools/call, etc.)
9
+ * GET /mcp - 405 (SSE not supported in stateless Lambda)
10
+ * DELETE /mcp - 200 (no-op session close)
11
+ */
12
+
13
+ const { executeQuery } = require('./dbOperations');
14
+
15
+ const SERVER_INFO = {
16
+ name: 'jarvis-mcp',
17
+ version: '1.0.0'
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',
26
+ };
27
+
28
+ // ============================================================
29
+ // Tool Definitions
30
+ // ============================================================
31
+
32
+ const TOOLS = [
33
+ {
34
+ name: 'list_agents',
35
+ description: 'Discover active agents in the JARVIS mesh and their capabilities, success rates, and circuit breaker status',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ capability: { type: 'string', description: 'Filter by capability (optional)' }
40
+ }
41
+ }
42
+ },
43
+ {
44
+ name: 'dispatch_task',
45
+ description: 'Dispatch a task to the JARVIS orchestration engine for routing to the best agent',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ description: { type: 'string', description: 'Natural language task description' },
50
+ parameters: { type: 'object', description: 'Task parameters (optional)' },
51
+ constraints: { type: 'object', description: 'Constraints: minSuccessRate, maxCost, maxDuration (optional)' }
52
+ },
53
+ required: ['description']
54
+ }
55
+ },
56
+ {
57
+ name: 'query_decision_frames',
58
+ description: 'Query governance decision frames - the audit trail of every agent routing decision with full reasoning context',
59
+ inputSchema: {
60
+ type: 'object',
61
+ properties: {
62
+ limit: { type: 'number', description: 'Max results (default 20, max 100)' },
63
+ agentName: { type: 'string', description: 'Filter by agent name (optional)' },
64
+ timeWindow: { type: 'string', description: 'Time window: 1d, 7d, 30d, 90d (default 7d)' }
65
+ }
66
+ }
67
+ },
68
+ {
69
+ name: 'get_standards',
70
+ description: 'Search the Equilateral standards library for relevant rules, anti-patterns, and examples',
71
+ inputSchema: {
72
+ type: 'object',
73
+ properties: {
74
+ context: { type: 'string', description: 'Search context (e.g., "Lambda database", "CORS", "cost optimization")' },
75
+ topK: { type: 'number', description: 'Number of results (default 5, max 20)' }
76
+ },
77
+ required: ['context']
78
+ }
79
+ },
80
+ {
81
+ name: 'get_rapport_context',
82
+ description: 'Get Rapport invariants, purpose, notes, and active loops for a scope. These are the structural patterns that shape how agents interact.',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {
86
+ scope: { type: 'string', description: 'Scope name (default: jarvis)' }
87
+ }
88
+ }
89
+ },
90
+ {
91
+ name: 'get_mesh_status',
92
+ description: 'Get aggregate health status of the JARVIS agent mesh: active/inactive agents, circuit breaker states, success rates',
93
+ inputSchema: { type: 'object', properties: {} }
94
+ },
95
+ {
96
+ name: 'get_agent_stats',
97
+ description: 'Get detailed performance statistics for a specific agent: success rate, duration, cost, recent executions',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ agentName: { type: 'string', description: 'Name of the agent' },
102
+ timeWindow: { type: 'string', description: 'Time window: 1d, 7d, 30d, 90d (default 7d)' }
103
+ },
104
+ required: ['agentName']
105
+ }
106
+ }
107
+ ];
108
+
109
+ // ============================================================
110
+ // Resource Definitions
111
+ // ============================================================
112
+
113
+ const RESOURCES = [
114
+ { uri: 'agent://registry', name: 'Agent Registry', description: 'Complete JARVIS agent registry with capabilities and health status', mimeType: 'application/json' },
115
+ { uri: 'decision://frames/recent', name: 'Decision Frames', description: 'Recent governance decision frames with audit trail', mimeType: 'application/json' },
116
+ { uri: 'standards://library', name: 'Standards Library', description: 'Equilateral AI standards library - rules and anti-patterns', mimeType: 'application/json' },
117
+ { uri: 'rapport://invariants/jarvis', name: 'Rapport Invariants', description: 'Rapport structural invariants for the jarvis scope', mimeType: 'application/json' },
118
+ { uri: 'workflow://active', name: 'Active Workflows', description: 'Currently active agent workflows', mimeType: 'application/json' }
119
+ ];
120
+
121
+ // ============================================================
122
+ // Tool Implementations
123
+ // ============================================================
124
+
125
+ async function callTool(name, args = {}) {
126
+ switch (name) {
127
+ case 'list_agents': {
128
+ try {
129
+ let query = `
130
+ SELECT agent_name, agent_type, capabilities, tools, status,
131
+ circuit_breaker_status, success_rate, total_executions
132
+ FROM agent_orchestration.agent_registry
133
+ WHERE status = 'active'
134
+ `;
135
+ const params = [];
136
+ if (args.capability) {
137
+ query += ` AND $1 = ANY(capabilities)`;
138
+ params.push(args.capability);
139
+ }
140
+ query += ` ORDER BY success_rate DESC, total_executions DESC`;
141
+ const result = await executeQuery(query, params);
142
+ return { content: [{ type: 'text', text: JSON.stringify({ agents: result.rows, count: result.rows.length }, null, 2) }] };
143
+ } catch (e) {
144
+ if (e.message.includes('does not exist')) {
145
+ return { content: [{ type: 'text', text: 'Agent orchestration schema not available in this database. The agent_orchestration schema is on the JARVIS Mac Mini database. Use get_rapport_context or get_standards for data available in this environment.' }] };
146
+ }
147
+ throw e;
148
+ }
149
+ }
150
+
151
+ case 'dispatch_task': {
152
+ try {
153
+ const result = await executeQuery(`
154
+ INSERT INTO agent_orchestration.agent_routing_log
155
+ (task_description, task_parameters, constraints, status, created_at)
156
+ VALUES ($1, $2, $3, 'pending', NOW())
157
+ RETURNING routing_id, status, created_at
158
+ `, [args.description, JSON.stringify(args.parameters || {}), JSON.stringify(args.constraints || {})]);
159
+ return {
160
+ content: [{
161
+ type: 'text',
162
+ text: JSON.stringify({
163
+ dispatched: true,
164
+ routing_id: result.rows[0].routing_id,
165
+ status: result.rows[0].status,
166
+ message: `Task dispatched: "${args.description}"`
167
+ }, null, 2)
168
+ }]
169
+ };
170
+ } catch (e) {
171
+ if (e.message.includes('does not exist')) {
172
+ return { content: [{ type: 'text', text: 'Agent orchestration schema not available in this database. Task dispatch requires the JARVIS Mac Mini environment.' }] };
173
+ }
174
+ throw e;
175
+ }
176
+ }
177
+
178
+ case 'query_decision_frames': {
179
+ try {
180
+ const effectiveLimit = Math.min(parseInt(args.limit) || 20, 100);
181
+ const validWindows = { '1d': '1 day', '7d': '7 days', '30d': '30 days', '90d': '90 days' };
182
+ const effectiveWindow = validWindows[args.timeWindow] || '7 days';
183
+ const params = [effectiveWindow];
184
+ let query = `
185
+ SELECT frame_id, agent_name, decision_type, reasoning,
186
+ governance_context, outcome, created_at
187
+ FROM agent_orchestration.decision_frames
188
+ WHERE created_at > NOW() - $1::interval
189
+ `;
190
+ if (args.agentName) {
191
+ params.push(args.agentName);
192
+ query += ` AND agent_name = $${params.length}`;
193
+ }
194
+ params.push(effectiveLimit);
195
+ query += ` ORDER BY created_at DESC LIMIT $${params.length}`;
196
+ const result = await executeQuery(query, params);
197
+ return { content: [{ type: 'text', text: JSON.stringify({ frames: result.rows, count: result.rows.length }, null, 2) }] };
198
+ } catch (e) {
199
+ if (e.message.includes('does not exist')) {
200
+ return { content: [{ type: 'text', text: 'Decision frames schema not available in this database. Governance audit trails are on the JARVIS Mac Mini environment.' }] };
201
+ }
202
+ throw e;
203
+ }
204
+ }
205
+
206
+ case 'get_standards': {
207
+ const k = Math.min(args.topK || 5, 20);
208
+ // Split search into individual words for AND matching
209
+ const words = args.context.split(/\s+/).filter(w => w.length > 1);
210
+ const searchFields = `COALESCE(rule,'') || ' ' || COALESCE(category,'') || ' ' || COALESCE(element,'') || ' ' || COALESCE(title,'') || ' ' || COALESCE(keywords::text,'')`;
211
+ const conditions = words.map((_, i) => `(${searchFields}) ILIKE $${i + 1}`);
212
+ const params = words.map(w => `%${w}%`);
213
+ params.push(k);
214
+ const result = await executeQuery(`
215
+ SELECT pattern_id, element, title, rule, category,
216
+ keywords, anti_patterns, examples, cost_impact,
217
+ maturity, correlation
218
+ FROM rapport.standards_patterns
219
+ WHERE ${conditions.join(' AND ')}
220
+ ORDER BY
221
+ CASE WHEN maturity = 'enforced' THEN 1
222
+ WHEN maturity = 'validated' THEN 2
223
+ ELSE 3 END,
224
+ correlation DESC
225
+ LIMIT $${params.length}
226
+ `, params);
227
+ return { content: [{ type: 'text', text: JSON.stringify({ standards: result.rows, count: result.rows.length }, null, 2) }] };
228
+ }
229
+
230
+ case 'get_rapport_context': {
231
+ const scope = args.scope || 'jarvis';
232
+ const result = await executeQuery(
233
+ `SELECT rapport.get_user_context($1, $2) as context`,
234
+ [process.env.MCP_DEFAULT_USER || 'james.ford@happyhippo.ai', scope]
235
+ );
236
+ const context = result.rows[0]?.context || {
237
+ invariants: { agent_level: [], relationship_level: [] },
238
+ purpose: null, recent_notes: [], active_loops: []
239
+ };
240
+ return { content: [{ type: 'text', text: JSON.stringify({ scope, ...context }, null, 2) }] };
241
+ }
242
+
243
+ case 'get_mesh_status': {
244
+ try {
245
+ const result = await executeQuery(`
246
+ SELECT
247
+ COUNT(*) FILTER (WHERE status = 'active') as active_agents,
248
+ COUNT(*) FILTER (WHERE status = 'inactive') as inactive_agents,
249
+ COUNT(*) FILTER (WHERE circuit_breaker_status = 'open') as circuit_breakers_open,
250
+ AVG(success_rate) FILTER (WHERE status = 'active') as avg_success_rate,
251
+ SUM(total_executions) as total_executions
252
+ FROM agent_orchestration.agent_registry
253
+ `);
254
+ const mesh = result.rows[0];
255
+ return {
256
+ content: [{
257
+ type: 'text',
258
+ text: JSON.stringify({
259
+ mesh: {
260
+ active_agents: parseInt(mesh.active_agents) || 0,
261
+ inactive_agents: parseInt(mesh.inactive_agents) || 0,
262
+ circuit_breakers_open: parseInt(mesh.circuit_breakers_open) || 0,
263
+ avg_success_rate: parseFloat(mesh.avg_success_rate) || 0,
264
+ total_executions: parseInt(mesh.total_executions) || 0,
265
+ status: parseInt(mesh.circuit_breakers_open) > 0 ? 'degraded' : 'healthy',
266
+ timestamp: new Date().toISOString()
267
+ }
268
+ }, null, 2)
269
+ }]
270
+ };
271
+ } catch (e) {
272
+ if (e.message.includes('does not exist')) {
273
+ return { content: [{ type: 'text', text: 'Agent mesh schema not available in this database. Mesh status is on the JARVIS Mac Mini environment.' }] };
274
+ }
275
+ throw e;
276
+ }
277
+ }
278
+
279
+ case 'get_agent_stats': {
280
+ try {
281
+ const validWindows = { '1d': '1 day', '7d': '7 days', '30d': '30 days', '90d': '90 days' };
282
+ const effectiveWindow = validWindows[args.timeWindow] || '7 days';
283
+ const agentResult = await executeQuery(`
284
+ SELECT agent_name, agent_type, total_executions, total_successes,
285
+ total_failures, success_rate, avg_duration_ms, avg_cost_usd,
286
+ circuit_breaker_status
287
+ FROM agent_orchestration.agent_registry
288
+ WHERE agent_name = $1
289
+ `, [args.agentName]);
290
+ if (agentResult.rows.length === 0) {
291
+ return { content: [{ type: 'text', text: JSON.stringify({ error: `Agent '${args.agentName}' not found` }) }], isError: true };
292
+ }
293
+ const executionsResult = await executeQuery(`
294
+ SELECT status, duration_ms, cost_usd, execution_timestamp
295
+ FROM agent_orchestration.agent_executions
296
+ WHERE agent_name = $1
297
+ AND execution_timestamp > NOW() - $2::interval
298
+ ORDER BY execution_timestamp DESC
299
+ LIMIT 50
300
+ `, [args.agentName, effectiveWindow]);
301
+ return {
302
+ content: [{
303
+ type: 'text',
304
+ text: JSON.stringify({
305
+ agent: agentResult.rows[0],
306
+ recent_executions: executionsResult.rows.length,
307
+ executions: executionsResult.rows
308
+ }, null, 2)
309
+ }]
310
+ };
311
+ } catch (e) {
312
+ if (e.message.includes('does not exist')) {
313
+ return { content: [{ type: 'text', text: 'Agent stats schema not available in this database. Agent performance data is on the JARVIS Mac Mini environment.' }] };
314
+ }
315
+ throw e;
316
+ }
317
+ }
318
+
319
+ default:
320
+ throw new Error(`Unknown tool: ${name}`);
321
+ }
322
+ }
323
+
324
+ // ============================================================
325
+ // Resource Implementations
326
+ // ============================================================
327
+
328
+ async function readResource(uri) {
329
+ try {
330
+ switch (uri) {
331
+ case 'agent://registry': {
332
+ const result = await executeQuery(`
333
+ SELECT agent_name, agent_type, capabilities, status,
334
+ circuit_breaker_status, success_rate
335
+ FROM agent_orchestration.agent_registry ORDER BY agent_name
336
+ `);
337
+ return { contents: [{ uri, text: JSON.stringify(result.rows, null, 2), mimeType: 'application/json' }] };
338
+ }
339
+ case 'decision://frames/recent': {
340
+ const result = await executeQuery(`
341
+ SELECT frame_id, agent_name, decision_type, reasoning, outcome, created_at
342
+ FROM agent_orchestration.decision_frames
343
+ ORDER BY created_at DESC LIMIT 20
344
+ `);
345
+ return { contents: [{ uri, text: JSON.stringify(result.rows, null, 2), mimeType: 'application/json' }] };
346
+ }
347
+ case 'standards://library': {
348
+ const result = await executeQuery(`
349
+ SELECT pattern_id, element, title, rule, category, maturity, correlation
350
+ FROM rapport.standards_patterns
351
+ WHERE maturity IN ('enforced', 'validated', 'recommended')
352
+ ORDER BY category, correlation DESC
353
+ `);
354
+ return { contents: [{ uri, text: JSON.stringify(result.rows, null, 2), mimeType: 'application/json' }] };
355
+ }
356
+ case 'rapport://invariants/jarvis': {
357
+ const result = await executeQuery(
358
+ `SELECT rapport.get_user_context($1, $2) as context`,
359
+ [process.env.MCP_DEFAULT_USER || 'james.ford@happyhippo.ai', 'jarvis']
360
+ );
361
+ const context = result.rows[0]?.context || { invariants: {} };
362
+ return { contents: [{ uri, text: JSON.stringify(context, null, 2), mimeType: 'application/json' }] };
363
+ }
364
+ case 'workflow://active': {
365
+ const result = await executeQuery(`
366
+ SELECT workflow_name, status, workflow_config, created_at
367
+ FROM agent_orchestration.agent_workflows
368
+ WHERE status = 'active' ORDER BY created_at DESC
369
+ `);
370
+ return { contents: [{ uri, text: JSON.stringify(result.rows, null, 2), mimeType: 'application/json' }] };
371
+ }
372
+ default:
373
+ throw new Error(`Unknown resource: ${uri}`);
374
+ }
375
+ } catch (e) {
376
+ if (e.message.includes('does not exist')) {
377
+ return { contents: [{ uri, text: JSON.stringify({ error: 'Schema not available in this database environment' }), mimeType: 'application/json' }] };
378
+ }
379
+ throw e;
380
+ }
381
+ }
382
+
383
+ // ============================================================
384
+ // JSON-RPC Message Router
385
+ // ============================================================
386
+
387
+ async function handleJsonRpc(message) {
388
+ const { method, params, id } = message;
389
+
390
+ switch (method) {
391
+ case 'initialize':
392
+ return {
393
+ jsonrpc: '2.0',
394
+ id,
395
+ result: {
396
+ protocolVersion: PROTOCOL_VERSION,
397
+ capabilities: {
398
+ tools: { listChanged: false },
399
+ resources: { subscribe: false, listChanged: false }
400
+ },
401
+ serverInfo: SERVER_INFO
402
+ }
403
+ };
404
+
405
+ case 'notifications/initialized':
406
+ // Client acknowledgment — no response needed for notifications
407
+ return null;
408
+
409
+ case 'ping':
410
+ return { jsonrpc: '2.0', id, result: {} };
411
+
412
+ case 'tools/list':
413
+ return {
414
+ jsonrpc: '2.0',
415
+ id,
416
+ result: { tools: TOOLS }
417
+ };
418
+
419
+ case 'tools/call': {
420
+ const { name, arguments: args } = params;
421
+ try {
422
+ const result = await callTool(name, args || {});
423
+ return { jsonrpc: '2.0', id, result };
424
+ } catch (error) {
425
+ return {
426
+ jsonrpc: '2.0',
427
+ id,
428
+ result: {
429
+ content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }],
430
+ isError: true
431
+ }
432
+ };
433
+ }
434
+ }
435
+
436
+ case 'resources/list':
437
+ return {
438
+ jsonrpc: '2.0',
439
+ id,
440
+ result: { resources: RESOURCES }
441
+ };
442
+
443
+ case 'resources/read': {
444
+ const { uri } = params;
445
+ try {
446
+ const result = await readResource(uri);
447
+ return { jsonrpc: '2.0', id, result };
448
+ } catch (error) {
449
+ return {
450
+ jsonrpc: '2.0',
451
+ id,
452
+ error: { code: -32602, message: error.message }
453
+ };
454
+ }
455
+ }
456
+
457
+ default:
458
+ return {
459
+ jsonrpc: '2.0',
460
+ id,
461
+ error: { code: -32601, message: `Method not found: ${method}` }
462
+ };
463
+ }
464
+ }
465
+
466
+ // ============================================================
467
+ // Lambda Handler
468
+ // ============================================================
469
+
470
+ exports.handler = async (event) => {
471
+ const method = event.httpMethod;
472
+
473
+ // CORS preflight
474
+ if (method === 'OPTIONS') {
475
+ return {
476
+ statusCode: 200,
477
+ headers: { ...CORS_HEADERS, 'Access-Control-Max-Age': '86400' },
478
+ body: ''
479
+ };
480
+ }
481
+
482
+ // GET - SSE not supported in stateless Lambda
483
+ if (method === 'GET') {
484
+ return {
485
+ statusCode: 405,
486
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', Allow: 'POST, DELETE, OPTIONS' },
487
+ body: JSON.stringify({
488
+ jsonrpc: '2.0',
489
+ error: { code: -32000, message: 'SSE streaming not supported. Use POST for JSON-RPC requests.' },
490
+ id: null
491
+ })
492
+ };
493
+ }
494
+
495
+ // DELETE - Session close (no-op in stateless mode)
496
+ if (method === 'DELETE') {
497
+ return {
498
+ statusCode: 200,
499
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
500
+ body: ''
501
+ };
502
+ }
503
+
504
+ // POST - JSON-RPC message processing
505
+ if (method !== 'POST') {
506
+ return {
507
+ statusCode: 405,
508
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', Allow: 'POST, GET, DELETE, OPTIONS' },
509
+ body: JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed' }, id: null })
510
+ };
511
+ }
512
+
513
+ try {
514
+ let body;
515
+ try {
516
+ body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body;
517
+ } catch (e) {
518
+ return {
519
+ statusCode: 400,
520
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
521
+ body: JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error: invalid JSON' }, id: null })
522
+ };
523
+ }
524
+
525
+ // Handle batch requests
526
+ if (Array.isArray(body)) {
527
+ const responses = [];
528
+ for (const msg of body) {
529
+ const response = await handleJsonRpc(msg);
530
+ if (response) responses.push(response);
531
+ }
532
+ return {
533
+ statusCode: responses.length > 0 ? 200 : 202,
534
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
535
+ body: responses.length > 0 ? JSON.stringify(responses) : ''
536
+ };
537
+ }
538
+
539
+ // Handle single request
540
+ const response = await handleJsonRpc(body);
541
+
542
+ // Notifications don't get responses
543
+ if (!response) {
544
+ return {
545
+ statusCode: 202,
546
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
547
+ body: ''
548
+ };
549
+ }
550
+
551
+ return {
552
+ statusCode: 200,
553
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
554
+ body: JSON.stringify(response)
555
+ };
556
+
557
+ } catch (error) {
558
+ console.error('MCP handler error:', error);
559
+ return {
560
+ statusCode: 500,
561
+ headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
562
+ body: JSON.stringify({
563
+ jsonrpc: '2.0',
564
+ error: { code: -32603, message: 'Internal server error' },
565
+ id: null
566
+ })
567
+ };
568
+ }
569
+ };