@natomalabs/natoma-mcp-gateway 1.0.3 → 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/README.md CHANGED
@@ -4,12 +4,13 @@ A robust, production-ready gateway that bridges stdio-based MCP clients (like Cl
4
4
 
5
5
  ## Overview
6
6
 
7
- The Natoma MCP Gateway acts as a translation layer that enables seamless communication between different MCP (Model Context Protocol) implementations. It supports both standard and enterprise deployment modes, with automatic reconnection, health monitoring, and robust error handling.
7
+ The Natoma MCP Gateway acts as a translation layer that enables seamless communication between different MCP (Model Context Protocol) implementations. It operates exclusively in enterprise mode, providing production-grade features including automatic reconnection, health monitoring, and robust error handling.
8
8
 
9
9
  ## Features
10
10
 
11
- - **Dual Gateway Modes**: NMS (standard) and Enterprise gateways for different deployment scenarios
11
+ - **Enterprise Gateway**: Production-ready gateway with enhanced reliability and monitoring
12
12
  - **Protocol Translation**: Converts stdio JSON-RPC messages to HTTP/SSE format and vice versa
13
+ - **Custom URL Support**: Configure custom endpoints for private deployments
13
14
 
14
15
  ## Installation
15
16
 
@@ -26,13 +27,28 @@ Required environment variables:
26
27
  - `NATOMA_MCP_SERVER_INSTALLATION_ID`: Your MCP server installation ID/slug
27
28
  - `NATOMA_MCP_API_KEY`: Your Natoma API key for authentication
28
29
 
29
- ### Command Line Usage
30
+ Optional environment variables:
31
+
32
+ - `NATOMA_MCP_CUSTOM_URL`: Custom endpoint URL (overrides default api.natoma.app endpoint)
33
+
34
+ ### Corporate Proxy/Firewall Support
35
+
36
+ If you're running in a corporate environment with tools like ZScaler that perform TLS interception, you may encounter certificate validation errors. You can resolve this using Node.js's built-in support for custom CA certificates:
30
37
 
31
38
  ```bash
32
- # Standard NMS Gateway mode
39
+ # Export your corporate CA bundle
40
+ export NODE_EXTRA_CA_CERTS="/path/to/corporate-ca-bundle.pem"
41
+
42
+ # Run the gateway
33
43
  npx @natomalabs/natoma-mcp-gateway
44
+ ```
34
45
 
35
- # Enterprise Gateway mode
46
+ This approach is the standard Node.js solution for corporate environments.
47
+
48
+ ### Command Line Usage
49
+
50
+ ```bash
51
+ # Run the gateway (--enterprise flag is required)
36
52
  npx @natomalabs/natoma-mcp-gateway --enterprise
37
53
  ```
38
54
 
@@ -46,7 +62,8 @@ Add to your Claude Desktop MCP configuration:
46
62
  "your-server-name": {
47
63
  "command": "npx",
48
64
  "args": [
49
- "@natomalabs/natoma-mcp-gateway"
65
+ "@natomalabs/natoma-mcp-gateway",
66
+ "--enterprise"
50
67
  ],
51
68
  "env": {
52
69
  "NATOMA_MCP_SERVER_INSTALLATION_ID": "your-installation-id",
@@ -57,26 +74,60 @@ Add to your Claude Desktop MCP configuration:
57
74
  }
58
75
  ```
59
76
 
60
- For enterprise deployments, add `--enterprise` to the args array.
77
+ For custom endpoint URLs:
61
78
 
62
- ## Architecture
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "your-server-name": {
83
+ "command": "npx",
84
+ "args": [
85
+ "@natomalabs/natoma-mcp-gateway",
86
+ "--enterprise"
87
+ ],
88
+ "env": {
89
+ "NATOMA_MCP_SERVER_INSTALLATION_ID": "your-installation-id",
90
+ "NATOMA_MCP_API_KEY": "your-api-key",
91
+ "NATOMA_MCP_CUSTOM_URL": "https://your-custom-endpoint.com/mcp"
92
+ }
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ For corporate environments with custom certificates:
63
99
 
64
- ### Gateway Modes
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "your-server-name": {
104
+ "command": "npx",
105
+ "args": [
106
+ "@natomalabs/natoma-mcp-gateway",
107
+ "--enterprise"
108
+ ],
109
+ "env": {
110
+ "NATOMA_MCP_SERVER_INSTALLATION_ID": "your-installation-id",
111
+ "NATOMA_MCP_API_KEY": "your-api-key",
112
+ "NODE_EXTRA_CA_CERTS": "/path/to/corporate-ca-bundle.pem"
113
+ }
114
+ }
115
+ }
116
+ }
117
+ ```
65
118
 
66
- #### NMS Gateway (Standard)
119
+ ## Architecture
67
120
 
68
- - Uses EventSource for Server-Sent Events
69
- - Optimized for standard MCP deployments
70
- - Faster reconnection times (1 second)
71
- - HTTP-based message delivery
121
+ ### Enterprise Gateway
72
122
 
73
- #### Enterprise Gateway
123
+ The gateway operates in enterprise mode with the following features:
74
124
 
75
- - Uses fetch API with enhanced features
125
+ - Uses fetch API with enhanced reliability
76
126
  - Support for both JSON and SSE responses
77
127
  - Health check monitoring (every 5 minutes)
78
128
  - Extended timeouts (60 seconds)
79
129
  - Enhanced error recovery
130
+ - Custom endpoint URL support
80
131
 
81
132
  ### Protocol Flow
82
133
 
@@ -92,9 +143,11 @@ The gateway accepts several configuration options through the `MCPConfig` interf
92
143
 
93
144
  - `slug`: Server installation ID (required)
94
145
  - `apiKey`: Authentication API key (required)
146
+ - `isEnterprise`: Must be set to `true` (mandatory for enterprise mode)
147
+ - `customUrl`: Optional custom endpoint URL (overrides default api.natoma.app)
95
148
  - `maxReconnectAttempts`: Maximum reconnection attempts (default: 5)
96
- - `reconnectDelay`: Delay between reconnection attempts (default: 1000ms for NMS, 2000ms for Enterprise)
97
- - `timeout`: Request timeout (default: 30000ms for NMS, 60000ms for Enterprise)
149
+ - `reconnectDelay`: Delay between reconnection attempts (default: 2000ms)
150
+ - `timeout`: Request timeout (default: 60000ms)
98
151
 
99
152
  ## What is MCP?
100
153
 
@@ -0,0 +1,3 @@
1
+ export const NATOMA_ENTERPRISE_SERVER_URL = 'https://mcp.natoma.app';
2
+ export const MCP_SESSION_ID_HEADER = 'Mcp-Session-Id';
3
+ export const JSON_RPC_VERSION = '2.0';
package/build/gateway.js CHANGED
@@ -1,67 +1,449 @@
1
- #!/usr/bin/env node
2
- import { NMSGateway } from "./nms-gateway.js";
3
- import { EnterpriseGateway } from "./ent-gateway.js";
4
- // Parse command line arguments
5
- function parseArgs() {
6
- const args = process.argv.slice(2);
7
- return {
8
- enterprise: args.includes("--enterprise"),
9
- };
10
- }
11
- // Main function
12
- async function main() {
13
- const { enterprise } = parseArgs();
14
- const slug = process.env.NATOMA_MCP_SERVER_INSTALLATION_ID;
15
- if (!slug) {
16
- console.error("Please set NATOMA_MCP_SERVER_INSTALLATION_ID env var");
17
- process.exit(1);
18
- }
19
- const config = {
20
- slug,
21
- apiKey: process.env.NATOMA_MCP_API_KEY,
22
- maxReconnectAttempts: 5,
23
- reconnectDelay: enterprise ? 2000 : 1000,
24
- timeout: enterprise ? 60000 : undefined,
25
- };
26
- // Create appropriate gateway based on flag
27
- const gateway = enterprise
28
- ? new EnterpriseGateway(config)
29
- : new NMSGateway(config);
30
- console.error(`--- Starting ${enterprise ? "Enterprise" : "NMS"} Gateway ---`);
31
- try {
32
- await gateway.connect();
33
- process.stdin.on("data", (data) => {
34
- gateway.processMessage(data).catch((err) => {
35
- console.error("Error in processMessage:", err);
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);
6
+ }
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);
36
65
  });
37
- });
38
- const shutdown = () => {
39
- console.error("Shutting down...");
40
- gateway.cleanup();
41
- process.exit(0);
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
42
130
  };
43
- process.on("SIGINT", shutdown);
44
- process.on("SIGTERM", shutdown);
45
- process.on("uncaughtException", (err) => {
46
- console.error("Uncaught exception:", err);
47
- shutdown();
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
+ }
48
159
  });
49
- // Optional health check for enterprise gateway
50
- if (enterprise && gateway instanceof EnterpriseGateway) {
51
- setInterval(async () => {
52
- if (gateway.ready) {
53
- const ok = await gateway.healthCheck();
54
- if (!ok) {
55
- console.error("Health check failed; reconnecting");
56
- await gateway.handleConnectionError(new Error("Health check failed"));
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
+ }
240
+ }
241
+ }
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();
254
+ }
255
+ }
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;
57
306
  }
58
307
  }
59
- }, 5 * 60 * 1000); // Every 5 minutes
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
+ }
60
375
  }
61
376
  }
62
- catch (err) {
63
- console.error("Fatal error:", err);
64
- process.exit(1);
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
+ }
65
448
  }
66
449
  }
67
- main();