@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 || 1,
99
- max_projects: tierConfig?.maxProjects || 3,
100
- max_invariants: tierConfig?.maxInvariants || 10
98
+ max_collaborators: tierConfig?.maxCollaborators ?? null,
99
+ max_projects: tierConfig?.maxProjects ?? null,
100
+ max_invariants: tierConfig?.maxInvariants ?? null
101
101
  }
102
102
  }]
103
103
  },