@natomalabs/natoma-mcp-gateway 1.0.4 → 1.0.5
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/build/constants.js +3 -0
- package/build/gateway.js +439 -60
- package/build/main.js +58 -0
- package/build/types.js +1 -0
- package/package.json +8 -7
- package/build/base.js +0 -57
- package/build/ent-gateway.js +0 -407
package/build/gateway.js
CHANGED
|
@@ -1,70 +1,449 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (!args.includes("--enterprise")) {
|
|
7
|
-
console.error("Error: --enterprise flag is required");
|
|
8
|
-
process.exit(1);
|
|
1
|
+
import { JSON_RPC_VERSION, MCP_SESSION_ID_HEADER, NATOMA_ENTERPRISE_SERVER_URL } from './constants.js';
|
|
2
|
+
// Handle EPIPE errors gracefully
|
|
3
|
+
process.stdout.on('error', (err) => {
|
|
4
|
+
if (err.code === 'EPIPE') {
|
|
5
|
+
process.exit(0);
|
|
9
6
|
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
7
|
+
});
|
|
8
|
+
process.stderr.on('error', (err) => {
|
|
9
|
+
if (err.code === 'EPIPE') {
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
// Enterprise Gateway - Fetch based with enhanced features
|
|
14
|
+
export class Gateway {
|
|
15
|
+
isReady = false;
|
|
16
|
+
messageQueue = [];
|
|
17
|
+
reconnectAttempts = 0;
|
|
18
|
+
baseUrl;
|
|
19
|
+
maxReconnectAttempts;
|
|
20
|
+
reconnectDelay;
|
|
21
|
+
apiKey;
|
|
22
|
+
sessionHeaders = {};
|
|
23
|
+
endpointUrl;
|
|
24
|
+
timeout;
|
|
25
|
+
sseAbortController = null;
|
|
26
|
+
constructor(config) {
|
|
27
|
+
// Validate that config is provided with required isEnterprise flag
|
|
28
|
+
if (!config || config.isEnterprise !== true) {
|
|
29
|
+
throw new Error('Configuration with isEnterprise: true is required for MCP Gateway');
|
|
30
|
+
}
|
|
31
|
+
// Use custom URL if provided, otherwise use enterprise URL
|
|
32
|
+
this.baseUrl = config.customUrl || NATOMA_ENTERPRISE_SERVER_URL;
|
|
33
|
+
this.maxReconnectAttempts = 3;
|
|
34
|
+
this.reconnectDelay = 1000;
|
|
35
|
+
this.apiKey = config.apiKey;
|
|
36
|
+
// Validate that API key is provided
|
|
37
|
+
if (!this.apiKey) {
|
|
38
|
+
throw new Error('API key is required for MCP Gateway');
|
|
39
|
+
}
|
|
40
|
+
// Debug logging
|
|
41
|
+
console.error(`[BaseMCPGateway] Base URL set to: ${this.baseUrl}`);
|
|
42
|
+
if (config.customUrl) {
|
|
43
|
+
console.error(`[BaseMCPGateway] Using custom URL`);
|
|
44
|
+
}
|
|
45
|
+
console.error(`[BaseMCPGateway] API Key: ${this.apiKey ? 'PROVIDED' : 'NOT PROVIDED'}`);
|
|
46
|
+
console.error(`[BaseMCPGateway] Max reconnect attempts: ${this.maxReconnectAttempts}`);
|
|
47
|
+
// Debug the inherited baseUrl
|
|
48
|
+
console.error(`[Gateway] Inherited baseUrl: ${this.baseUrl}`);
|
|
49
|
+
const slug = config?.slug;
|
|
50
|
+
this.endpointUrl = slug ? `${this.baseUrl}/${slug}` : `${this.baseUrl}`;
|
|
51
|
+
this.timeout = config?.timeout ?? 30000;
|
|
52
|
+
// Debug final endpoint URL
|
|
53
|
+
console.error(`[Gateway] Final endpoint URL: ${this.endpointUrl}`);
|
|
54
|
+
console.error(`[Gateway] Timeout: ${this.timeout}ms`);
|
|
55
|
+
}
|
|
56
|
+
// Simplified validation for production - only log errors
|
|
57
|
+
validateResponse(response, originalRequest) {
|
|
58
|
+
if (!response || typeof response !== 'object') {
|
|
59
|
+
console.error('❌ Invalid response format');
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(response)) {
|
|
63
|
+
response.forEach((elem, idx) => {
|
|
64
|
+
this.validateSingleResponse(elem, idx);
|
|
39
65
|
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.validateSingleResponse(response);
|
|
69
|
+
}
|
|
70
|
+
return response;
|
|
71
|
+
}
|
|
72
|
+
validateSingleResponse(resp, index) {
|
|
73
|
+
const prefix = index !== undefined ? `#${index}: ` : '';
|
|
74
|
+
if (resp.jsonrpc !== JSON_RPC_VERSION) {
|
|
75
|
+
console.error(`❌ ${prefix}Invalid jsonrpc version`);
|
|
76
|
+
}
|
|
77
|
+
const hasResult = resp.hasOwnProperty('result');
|
|
78
|
+
const hasError = resp.hasOwnProperty('error');
|
|
79
|
+
if (!hasResult && !hasError) {
|
|
80
|
+
console.error(`❌ ${prefix}Response missing both 'result' and 'error' fields`);
|
|
81
|
+
}
|
|
82
|
+
if (hasError && resp.error) {
|
|
83
|
+
console.error(`❌ ${prefix}Server error: ${resp.error.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async connect() {
|
|
87
|
+
try {
|
|
88
|
+
console.error('--- Connecting to MCP server');
|
|
89
|
+
console.error(`--- Connection URL: ${this.endpointUrl}`);
|
|
90
|
+
const initializeRequest = {
|
|
91
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
92
|
+
method: 'initialize',
|
|
93
|
+
params: {
|
|
94
|
+
capabilities: {
|
|
95
|
+
tools: {},
|
|
96
|
+
prompts: {},
|
|
97
|
+
resources: {},
|
|
98
|
+
logging: {}
|
|
99
|
+
},
|
|
100
|
+
clientInfo: {
|
|
101
|
+
name: 'claude-desktop-gateway',
|
|
102
|
+
version: '1.0.0'
|
|
103
|
+
},
|
|
104
|
+
protocolVersion: '2025-03-26'
|
|
105
|
+
},
|
|
106
|
+
id: 1
|
|
107
|
+
};
|
|
108
|
+
const response = await this.makeRequest(initializeRequest);
|
|
109
|
+
if (response && response.result) {
|
|
110
|
+
this.isReady = true;
|
|
111
|
+
this.reconnectAttempts = 0;
|
|
112
|
+
console.error('--- MCP server connected successfully');
|
|
113
|
+
await this.processQueuedMessages();
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
throw new Error(`Initialize failed: ${JSON.stringify(response.error)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
121
|
+
console.error(`--- Connection failed: ${errorMessage}`);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async makeRequest(messageBody) {
|
|
126
|
+
const headers = {
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
Accept: 'application/json, text/event-stream',
|
|
129
|
+
...this.sessionHeaders
|
|
45
130
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
131
|
+
if (this.apiKey) {
|
|
132
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
133
|
+
}
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
136
|
+
let resp;
|
|
137
|
+
try {
|
|
138
|
+
resp = await fetch(this.endpointUrl, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers,
|
|
141
|
+
body: JSON.stringify(messageBody),
|
|
142
|
+
signal: controller.signal
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
147
|
+
throw new Error(`Request timeout after ${this.timeout}ms`);
|
|
148
|
+
}
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
clearTimeout(timeoutId);
|
|
153
|
+
}
|
|
154
|
+
// Update session headers
|
|
155
|
+
resp.headers.forEach((value, key) => {
|
|
156
|
+
if (key.toLowerCase() === MCP_SESSION_ID_HEADER.toLowerCase()) {
|
|
157
|
+
this.sessionHeaders[MCP_SESSION_ID_HEADER] = value;
|
|
158
|
+
}
|
|
51
159
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
160
|
+
if (!resp.ok) {
|
|
161
|
+
const text = await resp.text();
|
|
162
|
+
console.error(`❌ HTTP Error ${resp.status}: ${text}`);
|
|
163
|
+
throw new Error(`HTTP ${resp.status}: ${text}`);
|
|
164
|
+
}
|
|
165
|
+
const contentType = resp.headers.get('content-type') || '';
|
|
166
|
+
// Handle JSON response
|
|
167
|
+
if (contentType.includes('application/json')) {
|
|
168
|
+
const raw = await resp.text();
|
|
169
|
+
if (!raw || raw.trim() === '') {
|
|
170
|
+
throw new Error('Empty JSON response');
|
|
171
|
+
}
|
|
172
|
+
let parsed;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(raw);
|
|
175
|
+
}
|
|
176
|
+
catch (parseErr) {
|
|
177
|
+
console.error('❌ Failed to parse JSON response');
|
|
178
|
+
throw new Error(`Invalid JSON: ${parseErr.message}`);
|
|
179
|
+
}
|
|
180
|
+
this.validateResponse(parsed, messageBody);
|
|
181
|
+
return parsed;
|
|
182
|
+
}
|
|
183
|
+
// Handle SSE stream
|
|
184
|
+
if (contentType.includes('text/event-stream')) {
|
|
185
|
+
if (this.sseAbortController) {
|
|
186
|
+
this.sseAbortController.abort();
|
|
187
|
+
}
|
|
188
|
+
this.sseAbortController = new AbortController();
|
|
189
|
+
await this.readSSEStream(resp, messageBody);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
// Handle 202 Accepted
|
|
193
|
+
if (resp.status === 202) {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
const txt = await resp.text();
|
|
197
|
+
console.error(`❌ Unexpected Content-Type ${contentType}`);
|
|
198
|
+
throw new Error(`Unexpected Content-Type: ${contentType}`);
|
|
199
|
+
}
|
|
200
|
+
async readSSEStream(response, originalRequest) {
|
|
201
|
+
if (!response.body) {
|
|
202
|
+
console.error('❌ No response body for SSE');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const reader = response.body.getReader();
|
|
206
|
+
const decoder = new TextDecoder();
|
|
207
|
+
let buffer = '';
|
|
208
|
+
try {
|
|
209
|
+
while (true) {
|
|
210
|
+
const { done, value } = await reader.read();
|
|
211
|
+
if (done)
|
|
212
|
+
break;
|
|
213
|
+
buffer += decoder.decode(value, { stream: true });
|
|
214
|
+
let parts = buffer.split(/\r?\n\r?\n/);
|
|
215
|
+
buffer = parts.pop() || '';
|
|
216
|
+
for (const chunk of parts) {
|
|
217
|
+
const lines = chunk.split(/\r?\n/);
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
if (line.startsWith('data:')) {
|
|
220
|
+
const payload = line.slice(5).trim();
|
|
221
|
+
// Handle session establishment
|
|
222
|
+
const sidMatch = payload.match(/sessionId=([^&]+)/);
|
|
223
|
+
if (sidMatch) {
|
|
224
|
+
this.sessionHeaders[MCP_SESSION_ID_HEADER] = sidMatch[1];
|
|
225
|
+
this.isReady = true;
|
|
226
|
+
console.error(`Session established: ${sidMatch[1]}`);
|
|
227
|
+
await this.processQueuedMessages();
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
// Handle JSON-RPC messages
|
|
231
|
+
try {
|
|
232
|
+
const obj = JSON.parse(payload);
|
|
233
|
+
this.validateResponse(obj, originalRequest);
|
|
234
|
+
console.log(JSON.stringify(obj));
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
console.log(payload);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
60
240
|
}
|
|
61
241
|
}
|
|
62
|
-
}
|
|
242
|
+
}
|
|
243
|
+
console.error('⚠️ SSE stream closed by server');
|
|
244
|
+
await this.handleConnectionError(new Error('SSE closed'));
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
if (err.name !== 'AbortError') {
|
|
248
|
+
console.error('❌ Error reading SSE:', err);
|
|
249
|
+
await this.handleConnectionError(err);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
reader.releaseLock();
|
|
63
254
|
}
|
|
64
255
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
256
|
+
async handleConnectionError(error) {
|
|
257
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
258
|
+
console.error(`Connection error: ${errorMessage}`);
|
|
259
|
+
if (this.sseAbortController) {
|
|
260
|
+
this.sseAbortController.abort();
|
|
261
|
+
this.sseAbortController = null;
|
|
262
|
+
}
|
|
263
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
264
|
+
console.error(`Max reconnect attempts reached. Exiting.`);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
this.reconnectAttempts++;
|
|
268
|
+
this.isReady = false;
|
|
269
|
+
this.sessionHeaders = {};
|
|
270
|
+
console.error(`Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
|
|
271
|
+
await new Promise((r) => setTimeout(r, this.reconnectDelay));
|
|
272
|
+
await this.connect();
|
|
273
|
+
}
|
|
274
|
+
parseMultipleJSON(input) {
|
|
275
|
+
const messages = [];
|
|
276
|
+
let braceCount = 0;
|
|
277
|
+
let inString = false;
|
|
278
|
+
let escapeNext = false;
|
|
279
|
+
let startIndex = -1;
|
|
280
|
+
for (let i = 0; i < input.length; i++) {
|
|
281
|
+
const char = input[i];
|
|
282
|
+
if (escapeNext) {
|
|
283
|
+
escapeNext = false;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (char === '\\') {
|
|
287
|
+
escapeNext = true;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (char === '"' && !escapeNext) {
|
|
291
|
+
inString = !inString;
|
|
292
|
+
}
|
|
293
|
+
if (!inString) {
|
|
294
|
+
if (char === '{') {
|
|
295
|
+
if (braceCount === 0)
|
|
296
|
+
startIndex = i;
|
|
297
|
+
braceCount++;
|
|
298
|
+
}
|
|
299
|
+
else if (char === '}') {
|
|
300
|
+
braceCount--;
|
|
301
|
+
if (braceCount === 0 && startIndex >= 0) {
|
|
302
|
+
const jsonObj = input.substring(startIndex, i + 1).trim();
|
|
303
|
+
if (jsonObj)
|
|
304
|
+
messages.push(jsonObj);
|
|
305
|
+
startIndex = -1;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return messages;
|
|
311
|
+
}
|
|
312
|
+
async processMessage(input) {
|
|
313
|
+
if (!this.isReady) {
|
|
314
|
+
this.messageQueue.push(input);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const rawInput = input.toString().trim();
|
|
318
|
+
try {
|
|
319
|
+
const fragments = this.parseMultipleJSON(rawInput);
|
|
320
|
+
if (fragments.length === 0) {
|
|
321
|
+
console.error('⚠️ No JSON found in input');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
for (const raw of fragments) {
|
|
325
|
+
console.error(`--> ${raw}`);
|
|
326
|
+
const body = JSON.parse(raw);
|
|
327
|
+
const isNotification = !body.hasOwnProperty('id');
|
|
328
|
+
if (isNotification) {
|
|
329
|
+
await this.makeNotificationRequest(body);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const responseObj = await this.makeRequest(body);
|
|
333
|
+
if (responseObj) {
|
|
334
|
+
const respStr = JSON.stringify(responseObj);
|
|
335
|
+
console.log(respStr);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
341
|
+
console.error(`❌ Request error: ${errMsg}`);
|
|
342
|
+
if (errMsg.includes('fetch') ||
|
|
343
|
+
errMsg.includes('timeout') ||
|
|
344
|
+
errMsg.includes('503') ||
|
|
345
|
+
errMsg.includes('502')) {
|
|
346
|
+
await this.handleConnectionError(err);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// Send error response if we can determine the request ID
|
|
350
|
+
let originalId = null;
|
|
351
|
+
try {
|
|
352
|
+
const fragments = this.parseMultipleJSON(rawInput);
|
|
353
|
+
if (fragments.length > 0) {
|
|
354
|
+
const parsed = JSON.parse(fragments[0]);
|
|
355
|
+
if (parsed.hasOwnProperty('id')) {
|
|
356
|
+
originalId = parsed.id;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch (parseErr) {
|
|
361
|
+
console.error(`⚠️ Failed to extract request ID for error response: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
|
|
362
|
+
}
|
|
363
|
+
if (originalId !== null) {
|
|
364
|
+
const errorResponse = {
|
|
365
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
366
|
+
error: {
|
|
367
|
+
code: -32603,
|
|
368
|
+
message: `Gateway error: ${errMsg}`
|
|
369
|
+
},
|
|
370
|
+
id: originalId
|
|
371
|
+
};
|
|
372
|
+
console.log(JSON.stringify(errorResponse));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async makeNotificationRequest(messageBody) {
|
|
378
|
+
const headers = {
|
|
379
|
+
'Content-Type': 'application/json',
|
|
380
|
+
Accept: 'application/json, text/event-stream',
|
|
381
|
+
...this.sessionHeaders
|
|
382
|
+
};
|
|
383
|
+
if (this.apiKey) {
|
|
384
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
385
|
+
}
|
|
386
|
+
const controller = new AbortController();
|
|
387
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
388
|
+
try {
|
|
389
|
+
const resp = await fetch(this.endpointUrl, {
|
|
390
|
+
method: 'POST',
|
|
391
|
+
headers,
|
|
392
|
+
body: JSON.stringify(messageBody),
|
|
393
|
+
signal: controller.signal
|
|
394
|
+
});
|
|
395
|
+
resp.headers.forEach((value, key) => {
|
|
396
|
+
if (key.toLowerCase() === MCP_SESSION_ID_HEADER.toLowerCase()) {
|
|
397
|
+
this.sessionHeaders[MCP_SESSION_ID_HEADER] = value;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
if (!resp.ok && resp.status !== 202) {
|
|
401
|
+
const text = await resp.text();
|
|
402
|
+
console.error(`❌ Notification error ${resp.status}: ${text}`);
|
|
403
|
+
throw new Error(`HTTP ${resp.status}: ${text}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
408
|
+
throw new Error(`Notification timeout after ${this.timeout}ms`);
|
|
409
|
+
}
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
clearTimeout(timeoutId);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
cleanup() {
|
|
417
|
+
this.isReady = false;
|
|
418
|
+
this.sessionHeaders = {};
|
|
419
|
+
if (this.sseAbortController) {
|
|
420
|
+
this.sseAbortController.abort();
|
|
421
|
+
this.sseAbortController = null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async healthCheck() {
|
|
425
|
+
try {
|
|
426
|
+
const pingRequest = {
|
|
427
|
+
jsonrpc: JSON_RPC_VERSION,
|
|
428
|
+
method: 'ping',
|
|
429
|
+
id: 'health-check'
|
|
430
|
+
};
|
|
431
|
+
const resp = await this.makeRequest(pingRequest);
|
|
432
|
+
return resp !== null && !resp.hasOwnProperty('error');
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
get ready() {
|
|
439
|
+
return this.isReady;
|
|
440
|
+
}
|
|
441
|
+
async processQueuedMessages() {
|
|
442
|
+
while (this.messageQueue.length > 0) {
|
|
443
|
+
const message = this.messageQueue.shift();
|
|
444
|
+
if (message) {
|
|
445
|
+
await this.processMessage(message);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
68
448
|
}
|
|
69
449
|
}
|
|
70
|
-
main();
|
package/build/main.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Gateway } from './gateway.js';
|
|
3
|
+
// Main function
|
|
4
|
+
async function main() {
|
|
5
|
+
const slug = `${process.env.NATOMA_MCP_SERVER_INSTALLATION_ID}/mcp`;
|
|
6
|
+
if (!process.env.NATOMA_MCP_SERVER_INSTALLATION_ID) {
|
|
7
|
+
console.error('Please set NATOMA_MCP_SERVER_INSTALLATION_ID env var');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const config = {
|
|
11
|
+
slug,
|
|
12
|
+
apiKey: process.env.NATOMA_MCP_API_KEY,
|
|
13
|
+
maxReconnectAttempts: 5,
|
|
14
|
+
reconnectDelay: 2000,
|
|
15
|
+
timeout: 60000,
|
|
16
|
+
isEnterprise: true, // Mandatory flag
|
|
17
|
+
customUrl: process.env.NATOMA_MCP_CUSTOM_URL // Optional custom URL from env
|
|
18
|
+
};
|
|
19
|
+
// Always create Enterprise gateway
|
|
20
|
+
const gateway = new Gateway(config);
|
|
21
|
+
console.error(`--- Starting Enterprise Gateway ---`);
|
|
22
|
+
if (config.customUrl) {
|
|
23
|
+
console.error(`--- Using custom URL: ${config.customUrl} ---`);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await gateway.connect();
|
|
27
|
+
process.stdin.on('data', (data) => {
|
|
28
|
+
gateway.processMessage(data).catch((err) => {
|
|
29
|
+
console.error('Error in processMessage:', err);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
const shutdown = () => {
|
|
33
|
+
console.error('Shutting down...');
|
|
34
|
+
gateway.cleanup();
|
|
35
|
+
process.exit(0);
|
|
36
|
+
};
|
|
37
|
+
process.on('SIGINT', shutdown);
|
|
38
|
+
process.on('SIGTERM', shutdown);
|
|
39
|
+
process.on('uncaughtException', (err) => {
|
|
40
|
+
console.error('Uncaught exception:', err);
|
|
41
|
+
shutdown();
|
|
42
|
+
});
|
|
43
|
+
setInterval(async () => {
|
|
44
|
+
if (gateway.ready) {
|
|
45
|
+
const ok = await gateway.healthCheck();
|
|
46
|
+
if (!ok) {
|
|
47
|
+
console.error('Health check failed; reconnecting');
|
|
48
|
+
await gateway.handleConnectionError(new Error('Health check failed'));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, 5 * 60 * 1000); // Every 5 minutes
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error('Fatal error:', err);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
main();
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@natomalabs/natoma-mcp-gateway",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Natoma MCP Gateway",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"
|
|
8
|
+
"main": "./build/main.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"build"
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"prepare": "npm run build",
|
|
16
|
-
"inspector": "node build/
|
|
17
|
-
"inspector:enterprise": "node build/
|
|
18
|
-
"dev": "tsx watch src/
|
|
19
|
-
"dev:enterprise": "tsx watch src/
|
|
16
|
+
"inspector": "node build/main.js",
|
|
17
|
+
"inspector:enterprise": "node build/main.js --enterprise",
|
|
18
|
+
"dev": "tsx watch src/main.ts",
|
|
19
|
+
"dev:enterprise": "tsx watch src/main.ts --enterprise"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.0.3",
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/eventsource": "^1.1.15",
|
|
28
28
|
"@types/express": "^5.0.0",
|
|
29
|
-
"@types/node": "^20.
|
|
29
|
+
"@types/node": "^20.19.27",
|
|
30
|
+
"prettier": "^3.2.5",
|
|
30
31
|
"tsx": "^4.0.0",
|
|
31
32
|
"typescript": "^5.3.3"
|
|
32
33
|
},
|
package/build/base.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// base.ts
|
|
2
|
-
export const NATOMA_ENTERPRISE_SERVER_URL = "https://api.natoma.app/mcp";
|
|
3
|
-
export const MCP_SESSION_ID_HEADER = "Mcp-Session-Id";
|
|
4
|
-
// Handle EPIPE errors gracefully
|
|
5
|
-
process.stdout.on("error", (err) => {
|
|
6
|
-
if (err.code === "EPIPE") {
|
|
7
|
-
process.exit(0);
|
|
8
|
-
}
|
|
9
|
-
});
|
|
10
|
-
process.stderr.on("error", (err) => {
|
|
11
|
-
if (err.code === "EPIPE") {
|
|
12
|
-
process.exit(0);
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
// Base abstract gateway class
|
|
16
|
-
export class BaseMCPGateway {
|
|
17
|
-
isReady = false;
|
|
18
|
-
messageQueue = [];
|
|
19
|
-
reconnectAttempts = 0;
|
|
20
|
-
baseUrl;
|
|
21
|
-
maxReconnectAttempts;
|
|
22
|
-
reconnectDelay;
|
|
23
|
-
apiKey;
|
|
24
|
-
constructor(config) {
|
|
25
|
-
// Validate that config is provided with required isEnterprise flag
|
|
26
|
-
if (!config || config.isEnterprise !== true) {
|
|
27
|
-
throw new Error("Configuration with isEnterprise: true is required for MCP Gateway");
|
|
28
|
-
}
|
|
29
|
-
// Use custom URL if provided, otherwise use enterprise URL
|
|
30
|
-
this.baseUrl = config.customUrl || NATOMA_ENTERPRISE_SERVER_URL;
|
|
31
|
-
this.maxReconnectAttempts = 3;
|
|
32
|
-
this.reconnectDelay = 1000;
|
|
33
|
-
this.apiKey = config.apiKey;
|
|
34
|
-
// Validate that API key is provided
|
|
35
|
-
if (!this.apiKey) {
|
|
36
|
-
throw new Error("API key is required for MCP Gateway");
|
|
37
|
-
}
|
|
38
|
-
// Debug logging
|
|
39
|
-
console.error(`[BaseMCPGateway] Base URL set to: ${this.baseUrl}`);
|
|
40
|
-
if (config.customUrl) {
|
|
41
|
-
console.error(`[BaseMCPGateway] Using custom URL`);
|
|
42
|
-
}
|
|
43
|
-
console.error(`[BaseMCPGateway] API Key: ${this.apiKey ? "PROVIDED" : "NOT PROVIDED"}`);
|
|
44
|
-
console.error(`[BaseMCPGateway] Max reconnect attempts: ${this.maxReconnectAttempts}`);
|
|
45
|
-
}
|
|
46
|
-
get ready() {
|
|
47
|
-
return this.isReady;
|
|
48
|
-
}
|
|
49
|
-
async processQueuedMessages() {
|
|
50
|
-
while (this.messageQueue.length > 0) {
|
|
51
|
-
const message = this.messageQueue.shift();
|
|
52
|
-
if (message) {
|
|
53
|
-
await this.processMessage(message);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
package/build/ent-gateway.js
DELETED
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
// ent-gateway.ts
|
|
2
|
-
import { BaseMCPGateway, MCP_SESSION_ID_HEADER, } from "./base.js";
|
|
3
|
-
const JSON_RPC_VERSION = "2.0";
|
|
4
|
-
// Enterprise Gateway - Fetch based with enhanced features
|
|
5
|
-
export class EnterpriseGateway extends BaseMCPGateway {
|
|
6
|
-
sessionHeaders = {};
|
|
7
|
-
endpointUrl;
|
|
8
|
-
timeout;
|
|
9
|
-
sseAbortController = null;
|
|
10
|
-
constructor(config) {
|
|
11
|
-
// Set isEnterprise to true for Enterprise Gateway
|
|
12
|
-
super({ ...config, isEnterprise: true });
|
|
13
|
-
// Debug the inherited baseUrl
|
|
14
|
-
console.error(`[EnterpriseGateway] Inherited baseUrl: ${this.baseUrl}`);
|
|
15
|
-
const slug = config?.slug;
|
|
16
|
-
this.endpointUrl = slug ? `${this.baseUrl}/${slug}` : `${this.baseUrl}`;
|
|
17
|
-
this.timeout = config?.timeout ?? 30000;
|
|
18
|
-
// Debug final endpoint URL
|
|
19
|
-
console.error(`[EnterpriseGateway] Final endpoint URL: ${this.endpointUrl}`);
|
|
20
|
-
console.error(`[EnterpriseGateway] Timeout: ${this.timeout}ms`);
|
|
21
|
-
}
|
|
22
|
-
// Simplified validation for production - only log errors
|
|
23
|
-
validateResponse(response, originalRequest) {
|
|
24
|
-
if (!response || typeof response !== "object") {
|
|
25
|
-
console.error("❌ Invalid response format");
|
|
26
|
-
return response;
|
|
27
|
-
}
|
|
28
|
-
if (Array.isArray(response)) {
|
|
29
|
-
response.forEach((elem, idx) => {
|
|
30
|
-
this.validateSingleResponse(elem, idx);
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
this.validateSingleResponse(response);
|
|
35
|
-
}
|
|
36
|
-
return response;
|
|
37
|
-
}
|
|
38
|
-
validateSingleResponse(resp, index) {
|
|
39
|
-
const prefix = index !== undefined ? `#${index}: ` : "";
|
|
40
|
-
if (resp.jsonrpc !== JSON_RPC_VERSION) {
|
|
41
|
-
console.error(`❌ ${prefix}Invalid jsonrpc version`);
|
|
42
|
-
}
|
|
43
|
-
const hasResult = resp.hasOwnProperty("result");
|
|
44
|
-
const hasError = resp.hasOwnProperty("error");
|
|
45
|
-
if (!hasResult && !hasError) {
|
|
46
|
-
console.error(`❌ ${prefix}Response missing both 'result' and 'error' fields`);
|
|
47
|
-
}
|
|
48
|
-
if (hasError && resp.error) {
|
|
49
|
-
console.error(`❌ ${prefix}Server error: ${resp.error.message}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
async connect() {
|
|
53
|
-
try {
|
|
54
|
-
console.error("--- Connecting to MCP server");
|
|
55
|
-
console.error(`--- Connection URL: ${this.endpointUrl}`);
|
|
56
|
-
const initializeRequest = {
|
|
57
|
-
jsonrpc: JSON_RPC_VERSION,
|
|
58
|
-
method: "initialize",
|
|
59
|
-
params: {
|
|
60
|
-
capabilities: {
|
|
61
|
-
tools: {},
|
|
62
|
-
prompts: {},
|
|
63
|
-
resources: {},
|
|
64
|
-
logging: {},
|
|
65
|
-
},
|
|
66
|
-
clientInfo: {
|
|
67
|
-
name: "claude-desktop-gateway",
|
|
68
|
-
version: "1.0.0",
|
|
69
|
-
},
|
|
70
|
-
protocolVersion: "2025-03-26",
|
|
71
|
-
},
|
|
72
|
-
id: 1,
|
|
73
|
-
};
|
|
74
|
-
const response = await this.makeRequest(initializeRequest);
|
|
75
|
-
if (response && response.result) {
|
|
76
|
-
this.isReady = true;
|
|
77
|
-
this.reconnectAttempts = 0;
|
|
78
|
-
console.error("--- MCP server connected successfully");
|
|
79
|
-
await this.processQueuedMessages();
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
throw new Error(`Initialize failed: ${JSON.stringify(response.error)}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
87
|
-
console.error(`--- Connection failed: ${errorMessage}`);
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
async makeRequest(messageBody) {
|
|
92
|
-
const isInitialize = messageBody?.method === "initialize";
|
|
93
|
-
const headers = {
|
|
94
|
-
"Content-Type": "application/json",
|
|
95
|
-
Accept: isInitialize
|
|
96
|
-
? "application/json"
|
|
97
|
-
: "application/json, text/event-stream",
|
|
98
|
-
...this.sessionHeaders,
|
|
99
|
-
};
|
|
100
|
-
if (this.apiKey) {
|
|
101
|
-
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
102
|
-
}
|
|
103
|
-
const controller = new AbortController();
|
|
104
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
105
|
-
let resp;
|
|
106
|
-
try {
|
|
107
|
-
resp = await fetch(this.endpointUrl, {
|
|
108
|
-
method: "POST",
|
|
109
|
-
headers,
|
|
110
|
-
body: JSON.stringify(messageBody),
|
|
111
|
-
signal: controller.signal,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
116
|
-
throw new Error(`Request timeout after ${this.timeout}ms`);
|
|
117
|
-
}
|
|
118
|
-
throw err;
|
|
119
|
-
}
|
|
120
|
-
finally {
|
|
121
|
-
clearTimeout(timeoutId);
|
|
122
|
-
}
|
|
123
|
-
// Update session headers
|
|
124
|
-
resp.headers.forEach((value, key) => {
|
|
125
|
-
if (key.toLowerCase() === MCP_SESSION_ID_HEADER.toLowerCase()) {
|
|
126
|
-
this.sessionHeaders[MCP_SESSION_ID_HEADER] = value;
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
if (!resp.ok) {
|
|
130
|
-
const text = await resp.text();
|
|
131
|
-
console.error(`❌ HTTP Error ${resp.status}: ${text}`);
|
|
132
|
-
throw new Error(`HTTP ${resp.status}: ${text}`);
|
|
133
|
-
}
|
|
134
|
-
const contentType = resp.headers.get("content-type") || "";
|
|
135
|
-
// Handle JSON response
|
|
136
|
-
if (contentType.includes("application/json")) {
|
|
137
|
-
const raw = await resp.text();
|
|
138
|
-
if (!raw || raw.trim() === "") {
|
|
139
|
-
throw new Error("Empty JSON response");
|
|
140
|
-
}
|
|
141
|
-
let parsed;
|
|
142
|
-
try {
|
|
143
|
-
parsed = JSON.parse(raw);
|
|
144
|
-
}
|
|
145
|
-
catch (parseErr) {
|
|
146
|
-
console.error("❌ Failed to parse JSON response");
|
|
147
|
-
throw new Error(`Invalid JSON: ${parseErr.message}`);
|
|
148
|
-
}
|
|
149
|
-
this.validateResponse(parsed, messageBody);
|
|
150
|
-
return parsed;
|
|
151
|
-
}
|
|
152
|
-
// Handle SSE stream
|
|
153
|
-
if (contentType.includes("text/event-stream")) {
|
|
154
|
-
if (this.sseAbortController) {
|
|
155
|
-
this.sseAbortController.abort();
|
|
156
|
-
}
|
|
157
|
-
this.sseAbortController = new AbortController();
|
|
158
|
-
await this.readSSEStream(resp, messageBody);
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
// Handle 202 Accepted
|
|
162
|
-
if (resp.status === 202) {
|
|
163
|
-
return {};
|
|
164
|
-
}
|
|
165
|
-
const txt = await resp.text();
|
|
166
|
-
console.error(`❌ Unexpected Content-Type ${contentType}`);
|
|
167
|
-
throw new Error(`Unexpected Content-Type: ${contentType}`);
|
|
168
|
-
}
|
|
169
|
-
async readSSEStream(response, originalRequest) {
|
|
170
|
-
if (!response.body) {
|
|
171
|
-
console.error("❌ No response body for SSE");
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const reader = response.body.getReader();
|
|
175
|
-
const decoder = new TextDecoder();
|
|
176
|
-
let buffer = "";
|
|
177
|
-
try {
|
|
178
|
-
while (true) {
|
|
179
|
-
const { done, value } = await reader.read();
|
|
180
|
-
if (done)
|
|
181
|
-
break;
|
|
182
|
-
buffer += decoder.decode(value, { stream: true });
|
|
183
|
-
let parts = buffer.split(/\r?\n\r?\n/);
|
|
184
|
-
buffer = parts.pop() || "";
|
|
185
|
-
for (const chunk of parts) {
|
|
186
|
-
const lines = chunk.split(/\r?\n/);
|
|
187
|
-
for (const line of lines) {
|
|
188
|
-
if (line.startsWith("data:")) {
|
|
189
|
-
const payload = line.slice(5).trim();
|
|
190
|
-
// Handle session establishment
|
|
191
|
-
const sidMatch = payload.match(/sessionId=([^&]+)/);
|
|
192
|
-
if (sidMatch) {
|
|
193
|
-
this.sessionHeaders[MCP_SESSION_ID_HEADER] = sidMatch[1];
|
|
194
|
-
this.isReady = true;
|
|
195
|
-
console.error(`Session established: ${sidMatch[1]}`);
|
|
196
|
-
await this.processQueuedMessages();
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
// Handle JSON-RPC messages
|
|
200
|
-
try {
|
|
201
|
-
const obj = JSON.parse(payload);
|
|
202
|
-
this.validateResponse(obj, originalRequest);
|
|
203
|
-
console.log(JSON.stringify(obj));
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
console.log(payload);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
console.error("⚠️ SSE stream closed by server");
|
|
213
|
-
await this.handleConnectionError(new Error("SSE closed"));
|
|
214
|
-
}
|
|
215
|
-
catch (err) {
|
|
216
|
-
if (err.name !== "AbortError") {
|
|
217
|
-
console.error("❌ Error reading SSE:", err);
|
|
218
|
-
await this.handleConnectionError(err);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
finally {
|
|
222
|
-
reader.releaseLock();
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
async handleConnectionError(error) {
|
|
226
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
227
|
-
console.error(`Connection error: ${errorMessage}`);
|
|
228
|
-
if (this.sseAbortController) {
|
|
229
|
-
this.sseAbortController.abort();
|
|
230
|
-
this.sseAbortController = null;
|
|
231
|
-
}
|
|
232
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
233
|
-
console.error(`Max reconnect attempts reached. Exiting.`);
|
|
234
|
-
process.exit(1);
|
|
235
|
-
}
|
|
236
|
-
this.reconnectAttempts++;
|
|
237
|
-
this.isReady = false;
|
|
238
|
-
this.sessionHeaders = {};
|
|
239
|
-
console.error(`Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
|
|
240
|
-
await new Promise((r) => setTimeout(r, this.reconnectDelay));
|
|
241
|
-
await this.connect();
|
|
242
|
-
}
|
|
243
|
-
parseMultipleJSON(input) {
|
|
244
|
-
const messages = [];
|
|
245
|
-
let braceCount = 0;
|
|
246
|
-
let inString = false;
|
|
247
|
-
let escapeNext = false;
|
|
248
|
-
let startIndex = -1;
|
|
249
|
-
for (let i = 0; i < input.length; i++) {
|
|
250
|
-
const char = input[i];
|
|
251
|
-
if (escapeNext) {
|
|
252
|
-
escapeNext = false;
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
if (char === "\\") {
|
|
256
|
-
escapeNext = true;
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
if (char === '"' && !escapeNext) {
|
|
260
|
-
inString = !inString;
|
|
261
|
-
}
|
|
262
|
-
if (!inString) {
|
|
263
|
-
if (char === "{") {
|
|
264
|
-
if (braceCount === 0)
|
|
265
|
-
startIndex = i;
|
|
266
|
-
braceCount++;
|
|
267
|
-
}
|
|
268
|
-
else if (char === "}") {
|
|
269
|
-
braceCount--;
|
|
270
|
-
if (braceCount === 0 && startIndex >= 0) {
|
|
271
|
-
const jsonObj = input.substring(startIndex, i + 1).trim();
|
|
272
|
-
if (jsonObj)
|
|
273
|
-
messages.push(jsonObj);
|
|
274
|
-
startIndex = -1;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return messages;
|
|
280
|
-
}
|
|
281
|
-
async processMessage(input) {
|
|
282
|
-
if (!this.isReady) {
|
|
283
|
-
this.messageQueue.push(input);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const rawInput = input.toString().trim();
|
|
287
|
-
try {
|
|
288
|
-
const fragments = this.parseMultipleJSON(rawInput);
|
|
289
|
-
if (fragments.length === 0) {
|
|
290
|
-
console.error("⚠️ No JSON found in input");
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
for (const raw of fragments) {
|
|
294
|
-
console.error(`--> ${raw}`);
|
|
295
|
-
const body = JSON.parse(raw);
|
|
296
|
-
const isNotification = !body.hasOwnProperty("id");
|
|
297
|
-
if (isNotification) {
|
|
298
|
-
await this.makeNotificationRequest(body);
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
const responseObj = await this.makeRequest(body);
|
|
302
|
-
if (responseObj) {
|
|
303
|
-
const respStr = JSON.stringify(responseObj);
|
|
304
|
-
console.log(respStr);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
catch (err) {
|
|
309
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
310
|
-
console.error(`❌ Request error: ${errMsg}`);
|
|
311
|
-
if (errMsg.includes("fetch") ||
|
|
312
|
-
errMsg.includes("timeout") ||
|
|
313
|
-
errMsg.includes("503") ||
|
|
314
|
-
errMsg.includes("502")) {
|
|
315
|
-
await this.handleConnectionError(err);
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
// Send error response if we can determine the request ID
|
|
319
|
-
let originalId = null;
|
|
320
|
-
try {
|
|
321
|
-
const fragments = this.parseMultipleJSON(rawInput);
|
|
322
|
-
if (fragments.length > 0) {
|
|
323
|
-
const parsed = JSON.parse(fragments[0]);
|
|
324
|
-
if (parsed.hasOwnProperty("id")) {
|
|
325
|
-
originalId = parsed.id;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
catch (parseErr) {
|
|
330
|
-
console.error(`⚠️ Failed to extract request ID for error response: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
|
|
331
|
-
}
|
|
332
|
-
if (originalId !== null) {
|
|
333
|
-
const errorResponse = {
|
|
334
|
-
jsonrpc: JSON_RPC_VERSION,
|
|
335
|
-
error: {
|
|
336
|
-
code: -32603,
|
|
337
|
-
message: `Gateway error: ${errMsg}`,
|
|
338
|
-
},
|
|
339
|
-
id: originalId,
|
|
340
|
-
};
|
|
341
|
-
console.log(JSON.stringify(errorResponse));
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
async makeNotificationRequest(messageBody) {
|
|
347
|
-
const headers = {
|
|
348
|
-
"Content-Type": "application/json",
|
|
349
|
-
Accept: "application/json, text/event-stream",
|
|
350
|
-
...this.sessionHeaders,
|
|
351
|
-
};
|
|
352
|
-
if (this.apiKey) {
|
|
353
|
-
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
354
|
-
}
|
|
355
|
-
const controller = new AbortController();
|
|
356
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
357
|
-
try {
|
|
358
|
-
const resp = await fetch(this.endpointUrl, {
|
|
359
|
-
method: "POST",
|
|
360
|
-
headers,
|
|
361
|
-
body: JSON.stringify(messageBody),
|
|
362
|
-
signal: controller.signal,
|
|
363
|
-
});
|
|
364
|
-
resp.headers.forEach((value, key) => {
|
|
365
|
-
if (key.toLowerCase() === MCP_SESSION_ID_HEADER.toLowerCase()) {
|
|
366
|
-
this.sessionHeaders[MCP_SESSION_ID_HEADER] = value;
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
if (!resp.ok && resp.status !== 202) {
|
|
370
|
-
const text = await resp.text();
|
|
371
|
-
console.error(`❌ Notification error ${resp.status}: ${text}`);
|
|
372
|
-
throw new Error(`HTTP ${resp.status}: ${text}`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
catch (err) {
|
|
376
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
377
|
-
throw new Error(`Notification timeout after ${this.timeout}ms`);
|
|
378
|
-
}
|
|
379
|
-
throw err;
|
|
380
|
-
}
|
|
381
|
-
finally {
|
|
382
|
-
clearTimeout(timeoutId);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
cleanup() {
|
|
386
|
-
this.isReady = false;
|
|
387
|
-
this.sessionHeaders = {};
|
|
388
|
-
if (this.sseAbortController) {
|
|
389
|
-
this.sseAbortController.abort();
|
|
390
|
-
this.sseAbortController = null;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
async healthCheck() {
|
|
394
|
-
try {
|
|
395
|
-
const pingRequest = {
|
|
396
|
-
jsonrpc: JSON_RPC_VERSION,
|
|
397
|
-
method: "ping",
|
|
398
|
-
id: "health-check",
|
|
399
|
-
};
|
|
400
|
-
const resp = await this.makeRequest(pingRequest);
|
|
401
|
-
return resp !== null && !resp.hasOwnProperty("error");
|
|
402
|
-
}
|
|
403
|
-
catch {
|
|
404
|
-
return false;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|