@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.
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MindMeld MCP Streaming Handler — Lambda Function URL (RESPONSE_STREAM)
|
|
3
|
+
*
|
|
4
|
+
* Implements MCP Streamable HTTP transport (protocol version 2025-03-26):
|
|
5
|
+
* - POST: JSON-RPC messages, responds with JSON or SSE stream
|
|
6
|
+
* - GET: Legacy SSE handshake (endpoint event + close)
|
|
7
|
+
* - DELETE: Session close
|
|
8
|
+
* - OPTIONS: CORS preflight
|
|
9
|
+
*
|
|
10
|
+
* Auth: X-MindMeld-Token header OR Authorization: Bearer token
|
|
11
|
+
*
|
|
12
|
+
* Uses awslambda.streamifyResponse() for Lambda Function URL streaming.
|
|
13
|
+
* The `awslambda` global is provided by the Lambda Node.js runtime.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { validateApiToken, handleJsonRpc, CORS_HEADERS } = require('./mindmeldMcpCore');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Write an SSE event to the response stream
|
|
21
|
+
*/
|
|
22
|
+
function writeSSE(stream, data, eventType) {
|
|
23
|
+
if (eventType) {
|
|
24
|
+
stream.write(`event: ${eventType}\n`);
|
|
25
|
+
}
|
|
26
|
+
stream.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse Lambda Function URL event (different format from API Gateway)
|
|
31
|
+
*/
|
|
32
|
+
function parseRequest(event) {
|
|
33
|
+
const http = event.requestContext?.http || {};
|
|
34
|
+
return {
|
|
35
|
+
method: http.method || 'GET',
|
|
36
|
+
path: http.path || '/',
|
|
37
|
+
headers: event.headers || {},
|
|
38
|
+
body: event.body || '',
|
|
39
|
+
isBase64Encoded: event.isBase64Encoded || false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a response stream with headers
|
|
45
|
+
*/
|
|
46
|
+
function createStream(responseStream, statusCode, headers) {
|
|
47
|
+
return awslambda.HttpResponseStream.from(responseStream, {
|
|
48
|
+
statusCode,
|
|
49
|
+
headers: { ...CORS_HEADERS, ...headers },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Lambda Function URL streaming handler
|
|
54
|
+
const streamHandler = async (event, responseStream, _context) => {
|
|
55
|
+
const { method, headers, body, isBase64Encoded } = parseRequest(event);
|
|
56
|
+
|
|
57
|
+
// === OPTIONS: CORS preflight ===
|
|
58
|
+
if (method === 'OPTIONS') {
|
|
59
|
+
const stream = createStream(responseStream, 200, {
|
|
60
|
+
'Access-Control-Max-Age': '86400',
|
|
61
|
+
});
|
|
62
|
+
stream.end();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// === DELETE: Session close ===
|
|
67
|
+
if (method === 'DELETE') {
|
|
68
|
+
const stream = createStream(responseStream, 200, {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
});
|
|
71
|
+
stream.end();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// === GET: Legacy SSE handshake ===
|
|
76
|
+
if (method === 'GET') {
|
|
77
|
+
const authResult = await validateApiToken(headers);
|
|
78
|
+
if (authResult.error) {
|
|
79
|
+
const stream = createStream(responseStream, 401, {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
});
|
|
82
|
+
stream.write(JSON.stringify({ error: authResult.message }));
|
|
83
|
+
stream.end();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sessionId = crypto.randomUUID();
|
|
88
|
+
const stream = createStream(responseStream, 200, {
|
|
89
|
+
'Content-Type': 'text/event-stream',
|
|
90
|
+
'Cache-Control': 'no-cache',
|
|
91
|
+
'Connection': 'keep-alive',
|
|
92
|
+
'Mcp-Session-Id': sessionId,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Send endpoint event — legacy SSE clients expect this
|
|
96
|
+
stream.write(`event: endpoint\ndata: ${parseRequest(event).path}\n\n`);
|
|
97
|
+
|
|
98
|
+
// For Streamable HTTP clients probing via GET: close after endpoint event.
|
|
99
|
+
// The client will then POST to us for actual JSON-RPC messages.
|
|
100
|
+
stream.end();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// === POST: Streamable HTTP JSON-RPC ===
|
|
105
|
+
if (method !== 'POST') {
|
|
106
|
+
const stream = createStream(responseStream, 405, {
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
});
|
|
109
|
+
stream.write(JSON.stringify({ error: 'Method not allowed' }));
|
|
110
|
+
stream.end();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Auth
|
|
115
|
+
const authResult = await validateApiToken(headers);
|
|
116
|
+
if (authResult.error) {
|
|
117
|
+
const stream = createStream(responseStream, 200, {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
});
|
|
120
|
+
stream.write(JSON.stringify({
|
|
121
|
+
jsonrpc: '2.0',
|
|
122
|
+
error: { code: -32000, message: authResult.message },
|
|
123
|
+
id: null,
|
|
124
|
+
}));
|
|
125
|
+
stream.end();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Parse body (Function URL may base64-encode it)
|
|
130
|
+
let parsedBody;
|
|
131
|
+
try {
|
|
132
|
+
const raw = isBase64Encoded ? Buffer.from(body, 'base64').toString() : body;
|
|
133
|
+
parsedBody = JSON.parse(raw);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
const stream = createStream(responseStream, 400, {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
});
|
|
138
|
+
stream.write(JSON.stringify({
|
|
139
|
+
jsonrpc: '2.0',
|
|
140
|
+
error: { code: -32700, message: 'Parse error: invalid JSON' },
|
|
141
|
+
id: null,
|
|
142
|
+
}));
|
|
143
|
+
stream.end();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Session ID management
|
|
148
|
+
const sessionId = headers['mcp-session-id'] || crypto.randomUUID();
|
|
149
|
+
|
|
150
|
+
// Check if client wants SSE response
|
|
151
|
+
const acceptHeader = headers['accept'] || '';
|
|
152
|
+
const wantsSSE = acceptHeader.includes('text/event-stream');
|
|
153
|
+
|
|
154
|
+
if (wantsSSE) {
|
|
155
|
+
// === SSE streaming response ===
|
|
156
|
+
const stream = createStream(responseStream, 200, {
|
|
157
|
+
'Content-Type': 'text/event-stream',
|
|
158
|
+
'Cache-Control': 'no-cache',
|
|
159
|
+
'Mcp-Session-Id': sessionId,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(parsedBody)) {
|
|
163
|
+
for (const msg of parsedBody) {
|
|
164
|
+
const response = await handleJsonRpc(msg, authResult.user);
|
|
165
|
+
if (response) {
|
|
166
|
+
writeSSE(stream, response, 'message');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
const response = await handleJsonRpc(parsedBody, authResult.user);
|
|
171
|
+
if (response) {
|
|
172
|
+
writeSSE(stream, response, 'message');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
stream.end();
|
|
177
|
+
} else {
|
|
178
|
+
// === Standard JSON response ===
|
|
179
|
+
const responseHeaders = {
|
|
180
|
+
'Content-Type': 'application/json',
|
|
181
|
+
'Mcp-Session-Id': sessionId,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (Array.isArray(parsedBody)) {
|
|
185
|
+
const responses = [];
|
|
186
|
+
for (const msg of parsedBody) {
|
|
187
|
+
const response = await handleJsonRpc(msg, authResult.user);
|
|
188
|
+
if (response) responses.push(response);
|
|
189
|
+
}
|
|
190
|
+
const stream = createStream(responseStream,
|
|
191
|
+
responses.length > 0 ? 200 : 202, responseHeaders);
|
|
192
|
+
if (responses.length > 0) {
|
|
193
|
+
stream.write(JSON.stringify(responses));
|
|
194
|
+
}
|
|
195
|
+
stream.end();
|
|
196
|
+
} else {
|
|
197
|
+
const response = await handleJsonRpc(parsedBody, authResult.user);
|
|
198
|
+
if (!response) {
|
|
199
|
+
const stream = createStream(responseStream, 202, responseHeaders);
|
|
200
|
+
stream.end();
|
|
201
|
+
} else {
|
|
202
|
+
const stream = createStream(responseStream, 200, responseHeaders);
|
|
203
|
+
stream.write(JSON.stringify(response));
|
|
204
|
+
stream.end();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// awslambda is a global provided by the Lambda Node.js runtime.
|
|
211
|
+
// In local/test environments it won't exist — export the raw handler for testing.
|
|
212
|
+
if (typeof awslambda !== 'undefined') {
|
|
213
|
+
exports.handler = awslambda.streamifyResponse(streamHandler);
|
|
214
|
+
} else {
|
|
215
|
+
// Fallback for local testing: wrap as standard async handler
|
|
216
|
+
exports.handler = async (event) => {
|
|
217
|
+
let result = { statusCode: 200, headers: {}, body: '' };
|
|
218
|
+
const mockStream = {
|
|
219
|
+
_headers: {},
|
|
220
|
+
_statusCode: 200,
|
|
221
|
+
_chunks: [],
|
|
222
|
+
write(chunk) { this._chunks.push(chunk); },
|
|
223
|
+
end() {},
|
|
224
|
+
};
|
|
225
|
+
// Mock awslambda.HttpResponseStream.from
|
|
226
|
+
const originalFrom = mockStream;
|
|
227
|
+
const createMockStream = (_rs, meta) => {
|
|
228
|
+
mockStream._statusCode = meta.statusCode;
|
|
229
|
+
mockStream._headers = meta.headers;
|
|
230
|
+
return mockStream;
|
|
231
|
+
};
|
|
232
|
+
global.awslambda = {
|
|
233
|
+
HttpResponseStream: { from: createMockStream },
|
|
234
|
+
};
|
|
235
|
+
await streamHandler(event, mockStream, {});
|
|
236
|
+
delete global.awslambda;
|
|
237
|
+
return {
|
|
238
|
+
statusCode: mockStream._statusCode,
|
|
239
|
+
headers: mockStream._headers,
|
|
240
|
+
body: mockStream._chunks.join(''),
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -95,9 +95,9 @@ async function getUser({ requestContext }) {
|
|
|
95
95
|
invariants: parseInt(usage.invariants) || 0
|
|
96
96
|
},
|
|
97
97
|
limits: {
|
|
98
|
-
max_collaborators: tierConfig?.maxCollaborators
|
|
99
|
-
max_projects: tierConfig?.maxProjects
|
|
100
|
-
max_invariants: tierConfig?.maxInvariants
|
|
98
|
+
max_collaborators: tierConfig?.maxCollaborators ?? null,
|
|
99
|
+
max_projects: tierConfig?.maxProjects ?? null,
|
|
100
|
+
max_invariants: tierConfig?.maxInvariants ?? null
|
|
101
101
|
}
|
|
102
102
|
}]
|
|
103
103
|
},
|