openclaw-cascade-plugin 1.0.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 (98) hide show
  1. package/PHASE1_SUMMARY.md +191 -0
  2. package/PHASE3_SUMMARY.md +195 -0
  3. package/README.md +43 -0
  4. package/dist/a2a-client.d.ts +17 -0
  5. package/dist/a2a-client.d.ts.map +1 -0
  6. package/dist/a2a-client.js +47 -0
  7. package/dist/a2a-client.js.map +1 -0
  8. package/dist/cascade-client.d.ts +53 -0
  9. package/dist/cascade-client.d.ts.map +1 -0
  10. package/dist/cascade-client.js +179 -0
  11. package/dist/cascade-client.js.map +1 -0
  12. package/dist/config.d.ts +26 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +116 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/index.d.ts +29 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +136 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/python-manager.d.ts +59 -0
  21. package/dist/python-manager.d.ts.map +1 -0
  22. package/dist/python-manager.js +190 -0
  23. package/dist/python-manager.js.map +1 -0
  24. package/dist/test-utils/helpers.d.ts +20 -0
  25. package/dist/test-utils/helpers.d.ts.map +1 -0
  26. package/dist/test-utils/helpers.js +89 -0
  27. package/dist/test-utils/helpers.js.map +1 -0
  28. package/dist/test-utils/index.d.ts +3 -0
  29. package/dist/test-utils/index.d.ts.map +1 -0
  30. package/dist/test-utils/index.js +19 -0
  31. package/dist/test-utils/index.js.map +1 -0
  32. package/dist/test-utils/mocks.d.ts +51 -0
  33. package/dist/test-utils/mocks.d.ts.map +1 -0
  34. package/dist/test-utils/mocks.js +84 -0
  35. package/dist/test-utils/mocks.js.map +1 -0
  36. package/dist/tools/a2a-tools.d.ts +9 -0
  37. package/dist/tools/a2a-tools.d.ts.map +1 -0
  38. package/dist/tools/a2a-tools.js +147 -0
  39. package/dist/tools/a2a-tools.js.map +1 -0
  40. package/dist/tools/api-tools.d.ts +9 -0
  41. package/dist/tools/api-tools.d.ts.map +1 -0
  42. package/dist/tools/api-tools.js +102 -0
  43. package/dist/tools/api-tools.js.map +1 -0
  44. package/dist/tools/desktop-automation.d.ts +10 -0
  45. package/dist/tools/desktop-automation.d.ts.map +1 -0
  46. package/dist/tools/desktop-automation.js +330 -0
  47. package/dist/tools/desktop-automation.js.map +1 -0
  48. package/dist/tools/index.d.ts +12 -0
  49. package/dist/tools/index.d.ts.map +1 -0
  50. package/dist/tools/index.js +35 -0
  51. package/dist/tools/index.js.map +1 -0
  52. package/dist/tools/response-helpers.d.ts +25 -0
  53. package/dist/tools/response-helpers.d.ts.map +1 -0
  54. package/dist/tools/response-helpers.js +71 -0
  55. package/dist/tools/response-helpers.js.map +1 -0
  56. package/dist/tools/sandbox-tools.d.ts +9 -0
  57. package/dist/tools/sandbox-tools.d.ts.map +1 -0
  58. package/dist/tools/sandbox-tools.js +79 -0
  59. package/dist/tools/sandbox-tools.js.map +1 -0
  60. package/dist/tools/tool-registry.d.ts +34 -0
  61. package/dist/tools/tool-registry.d.ts.map +1 -0
  62. package/dist/tools/tool-registry.js +50 -0
  63. package/dist/tools/tool-registry.js.map +1 -0
  64. package/dist/tools/web-automation.d.ts +9 -0
  65. package/dist/tools/web-automation.d.ts.map +1 -0
  66. package/dist/tools/web-automation.js +471 -0
  67. package/dist/tools/web-automation.js.map +1 -0
  68. package/dist/types/index.d.ts +111 -0
  69. package/dist/types/index.d.ts.map +1 -0
  70. package/dist/types/index.js +38 -0
  71. package/dist/types/index.js.map +1 -0
  72. package/jest.setup.js +19 -0
  73. package/openclaw-cascade-plugin-1.0.0.tgz +0 -0
  74. package/openclaw.plugin.json +116 -0
  75. package/package.json +74 -0
  76. package/src/a2a-client.ts +66 -0
  77. package/src/cascade-client.test.ts +400 -0
  78. package/src/cascade-client.ts +198 -0
  79. package/src/config.test.ts +194 -0
  80. package/src/config.ts +135 -0
  81. package/src/index.ts +164 -0
  82. package/src/python-manager.test.ts +187 -0
  83. package/src/python-manager.ts +230 -0
  84. package/src/test-utils/helpers.ts +107 -0
  85. package/src/test-utils/index.ts +2 -0
  86. package/src/test-utils/mocks.ts +101 -0
  87. package/src/tools/a2a-tools.ts +162 -0
  88. package/src/tools/api-tools.ts +110 -0
  89. package/src/tools/desktop-automation.test.ts +305 -0
  90. package/src/tools/desktop-automation.ts +366 -0
  91. package/src/tools/index.ts +13 -0
  92. package/src/tools/response-helpers.ts +78 -0
  93. package/src/tools/sandbox-tools.ts +83 -0
  94. package/src/tools/tool-registry.ts +51 -0
  95. package/src/tools/web-automation.test.ts +177 -0
  96. package/src/tools/web-automation.ts +518 -0
  97. package/src/types/index.ts +132 -0
  98. package/tsconfig.json +27 -0
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Tests for CascadeMcpClient
3
+ *
4
+ * MCP (Model Context Protocol) client for communicating with Cascade
5
+ */
6
+
7
+ import { CascadeMcpClient } from './cascade-client';
8
+ import { createMockConfig } from './test-utils';
9
+ import { spawn } from 'child_process';
10
+ import { EventEmitter } from 'events';
11
+
12
+ // Mock child_process
13
+ jest.mock('child_process');
14
+
15
+ describe('CascadeMcpClient', () => {
16
+ let client: CascadeMcpClient;
17
+ let mockProcess: any;
18
+ let mockSpawn: jest.MockedFunction<typeof spawn>;
19
+
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+
23
+ // Create mock process
24
+ const stdout = new EventEmitter();
25
+ const stderr = new EventEmitter();
26
+ const stdin = { write: jest.fn() };
27
+
28
+ mockProcess = {
29
+ stdout,
30
+ stderr,
31
+ stdin,
32
+ kill: jest.fn(),
33
+ on: jest.fn(),
34
+ pid: 12345,
35
+ killed: false
36
+ };
37
+
38
+ mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
39
+ mockSpawn.mockReturnValue(mockProcess as any);
40
+
41
+ const config = createMockConfig({
42
+ cascadePythonPath: '/usr/bin/python3',
43
+ cascadeGrpcEndpoint: 'localhost:50051'
44
+ });
45
+
46
+ client = new CascadeMcpClient(
47
+ config.cascadePythonPath!,
48
+ {
49
+ CASCADE_GRPC_ENDPOINT: config.cascadeGrpcEndpoint,
50
+ CASCADE_APP_ID: 'test-app',
51
+ CASCADE_USER_ID: 'test-user'
52
+ }
53
+ );
54
+ });
55
+
56
+ describe('start', () => {
57
+ test('should spawn Python MCP server', async () => {
58
+ // Arrange
59
+ const startPromise = client.start();
60
+
61
+ // Emit initialize response
62
+ setTimeout(() => {
63
+ mockProcess.stdout.emit('data', JSON.stringify({
64
+ jsonrpc: '2.0',
65
+ id: 1,
66
+ result: {
67
+ protocolVersion: '2024-11-05',
68
+ capabilities: {},
69
+ serverInfo: { name: 'cascade-mcp', version: '1.0.0' }
70
+ }
71
+ }) + '\n');
72
+ }, 10);
73
+
74
+ // Act
75
+ await startPromise;
76
+
77
+ // Assert
78
+ expect(mockSpawn).toHaveBeenCalledWith(
79
+ '/usr/bin/python3',
80
+ ['-m', 'mcp_server.cli'],
81
+ expect.objectContaining({
82
+ env: expect.objectContaining({
83
+ CASCADE_GRPC_ENDPOINT: 'localhost:50051',
84
+ CASCADE_APP_ID: 'test-app',
85
+ CASCADE_USER_ID: 'test-user'
86
+ }),
87
+ stdio: ['pipe', 'pipe', 'pipe']
88
+ })
89
+ );
90
+ });
91
+
92
+ test('should send initialize request', async () => {
93
+ // Arrange
94
+ const startPromise = client.start();
95
+
96
+ // Emit initialize response
97
+ setTimeout(() => {
98
+ mockProcess.stdout.emit('data', JSON.stringify({
99
+ jsonrpc: '2.0',
100
+ id: 1,
101
+ result: {
102
+ protocolVersion: '2024-11-05',
103
+ capabilities: {},
104
+ serverInfo: { name: 'cascade-mcp', version: '1.0.0' }
105
+ }
106
+ }) + '\n');
107
+ }, 10);
108
+
109
+ // Act
110
+ await startPromise;
111
+
112
+ // Assert
113
+ const writeCalls = mockProcess.stdin.write.mock.calls;
114
+ const initRequest = JSON.parse(writeCalls[0][0]);
115
+
116
+ expect(initRequest).toMatchObject({
117
+ jsonrpc: '2.0',
118
+ id: 1,
119
+ method: 'initialize',
120
+ params: {
121
+ protocolVersion: '2024-11-05',
122
+ clientInfo: { name: 'openclaw-cascade', version: '1.0.0' }
123
+ }
124
+ });
125
+ });
126
+
127
+ test('should throw when spawn fails', async () => {
128
+ // Arrange
129
+ mockSpawn.mockImplementation(() => {
130
+ throw new Error('Spawn failed');
131
+ });
132
+
133
+ // Act & Assert
134
+ await expect(client.start()).rejects.toThrow('Spawn failed');
135
+ });
136
+
137
+ test('should timeout if no response', async () => {
138
+ // Arrange - don't emit any response
139
+
140
+ // Act & Assert
141
+ await expect(client.start()).rejects.toThrow('timed out');
142
+ }, 35000); // 35 second timeout for this test
143
+ });
144
+
145
+ describe('callTool', () => {
146
+ beforeEach(async () => {
147
+ // Start client first
148
+ const startPromise = client.start();
149
+
150
+ setTimeout(() => {
151
+ mockProcess.stdout.emit('data', JSON.stringify({
152
+ jsonrpc: '2.0',
153
+ id: 1,
154
+ result: {
155
+ protocolVersion: '2024-11-05',
156
+ capabilities: {},
157
+ serverInfo: { name: 'cascade-mcp', version: '1.0.0' }
158
+ }
159
+ }) + '\n');
160
+ }, 10);
161
+
162
+ await startPromise;
163
+ });
164
+
165
+ test('should send tool/call request', async () => {
166
+ // Arrange
167
+ const callPromise = client.callTool('click_element', { id: 'button1' });
168
+
169
+ setTimeout(() => {
170
+ mockProcess.stdout.emit('data', JSON.stringify({
171
+ jsonrpc: '2.0',
172
+ id: 2,
173
+ result: { success: true }
174
+ }) + '\n');
175
+ }, 10);
176
+
177
+ // Act
178
+ await callPromise;
179
+
180
+ // Assert
181
+ const writeCalls = mockProcess.stdin.write.mock.calls;
182
+ const lastCall = writeCalls[writeCalls.length - 1][0];
183
+ const request = JSON.parse(lastCall);
184
+
185
+ expect(request).toMatchObject({
186
+ jsonrpc: '2.0',
187
+ id: 2,
188
+ method: 'tools/call',
189
+ params: {
190
+ name: 'click_element',
191
+ arguments: { id: 'button1' }
192
+ }
193
+ });
194
+ });
195
+
196
+ test('should return tool result', async () => {
197
+ // Arrange
198
+ const callPromise = client.callTool('get_screenshot', {});
199
+
200
+ setTimeout(() => {
201
+ mockProcess.stdout.emit('data', JSON.stringify({
202
+ jsonrpc: '2.0',
203
+ id: 2,
204
+ result: { image: 'base64data', format: 'PNG' }
205
+ }) + '\n');
206
+ }, 10);
207
+
208
+ // Act
209
+ const result = await callPromise;
210
+
211
+ // Assert
212
+ expect(result).toEqual({ image: 'base64data', format: 'PNG' });
213
+ });
214
+
215
+ test('should handle tool errors', async () => {
216
+ // Arrange
217
+ const callPromise = client.callTool('click_element', { id: 'nonexistent' });
218
+
219
+ setTimeout(() => {
220
+ mockProcess.stdout.emit('data', JSON.stringify({
221
+ jsonrpc: '2.0',
222
+ id: 2,
223
+ error: { code: -32602, message: 'Element not found' }
224
+ }) + '\n');
225
+ }, 10);
226
+
227
+ // Act & Assert
228
+ await expect(callPromise).rejects.toThrow('Element not found');
229
+ });
230
+
231
+ test('should timeout after 30 seconds', async () => {
232
+ // Arrange - don't emit any response
233
+
234
+ // Act & Assert
235
+ await expect(client.callTool('click_element', {})).rejects.toThrow('timed out');
236
+ }, 35000); // 35 second timeout for this test
237
+ });
238
+
239
+ describe('listTools', () => {
240
+ beforeEach(async () => {
241
+ const startPromise = client.start();
242
+
243
+ setTimeout(() => {
244
+ mockProcess.stdout.emit('data', JSON.stringify({
245
+ jsonrpc: '2.0',
246
+ id: 1,
247
+ result: {
248
+ protocolVersion: '2024-11-05',
249
+ capabilities: {},
250
+ serverInfo: { name: 'cascade-mcp', version: '1.0.0' }
251
+ }
252
+ }) + '\n');
253
+ }, 10);
254
+
255
+ await startPromise;
256
+ });
257
+
258
+ test('should return list of available tools', async () => {
259
+ // Arrange
260
+ const listPromise = client.listTools();
261
+
262
+ setTimeout(() => {
263
+ mockProcess.stdout.emit('data', JSON.stringify({
264
+ jsonrpc: '2.0',
265
+ id: 2,
266
+ result: {
267
+ tools: [
268
+ { name: 'click_element', description: 'Click element' },
269
+ { name: 'get_screenshot', description: 'Get screenshot' }
270
+ ]
271
+ }
272
+ }) + '\n');
273
+ }, 10);
274
+
275
+ // Act
276
+ const result = await listPromise;
277
+
278
+ // Assert
279
+ expect(result).toHaveLength(2);
280
+ expect(result[0].name).toBe('click_element');
281
+ expect(result[1].name).toBe('get_screenshot');
282
+ });
283
+
284
+ test('should handle empty tool list', async () => {
285
+ // Arrange
286
+ const listPromise = client.listTools();
287
+
288
+ setTimeout(() => {
289
+ mockProcess.stdout.emit('data', JSON.stringify({
290
+ jsonrpc: '2.0',
291
+ id: 2,
292
+ result: { tools: [] }
293
+ }) + '\n');
294
+ }, 10);
295
+
296
+ // Act
297
+ const result = await listPromise;
298
+
299
+ // Assert
300
+ expect(result).toEqual([]);
301
+ });
302
+ });
303
+
304
+ describe('error handling', () => {
305
+ test('should handle JSON parse errors gracefully', async () => {
306
+ // Arrange
307
+ const startPromise = client.start();
308
+
309
+ setTimeout(() => {
310
+ // Emit invalid JSON first
311
+ mockProcess.stdout.emit('data', 'not valid json\n');
312
+
313
+ // Then emit valid response
314
+ mockProcess.stdout.emit('data', JSON.stringify({
315
+ jsonrpc: '2.0',
316
+ id: 1,
317
+ result: {
318
+ protocolVersion: '2024-11-05',
319
+ capabilities: {},
320
+ serverInfo: { name: 'cascade-mcp', version: '1.0.0' }
321
+ }
322
+ }) + '\n');
323
+ }, 10);
324
+
325
+ // Act - should not throw
326
+ await expect(startPromise).resolves.not.toThrow();
327
+ });
328
+
329
+ test('should handle process crash', async () => {
330
+ // Arrange
331
+ const startPromise = client.start();
332
+
333
+ setTimeout(() => {
334
+ const errorHandler = mockProcess.on.mock.calls.find(
335
+ (call: any) => call[0] === 'error'
336
+ )?.[1];
337
+ if (errorHandler) {
338
+ errorHandler(new Error('Process crashed'));
339
+ }
340
+ }, 10);
341
+
342
+ // Act & Assert
343
+ await expect(startPromise).rejects.toThrow('Process crashed');
344
+ });
345
+
346
+ test('should reject all pending on error', async () => {
347
+ // Arrange
348
+ const startPromise = client.start();
349
+
350
+ setTimeout(() => {
351
+ mockProcess.stdout.emit('data', JSON.stringify({
352
+ jsonrpc: '2.0',
353
+ id: 1,
354
+ result: { protocolVersion: '2024-11-05' }
355
+ }) + '\n');
356
+ }, 10);
357
+
358
+ await startPromise;
359
+
360
+ // Start a tool call that will hang
361
+ const toolPromise = client.callTool('click_element', {});
362
+
363
+ // Emit error
364
+ setTimeout(() => {
365
+ const errorHandler = mockProcess.on.mock.calls.find(
366
+ (call: any) => call[0] === 'error'
367
+ )?.[1];
368
+ if (errorHandler) {
369
+ errorHandler(new Error('Connection lost'));
370
+ }
371
+ }, 10);
372
+
373
+ // Act & Assert
374
+ await expect(toolPromise).rejects.toThrow('Connection lost');
375
+ });
376
+ });
377
+
378
+ describe('stop', () => {
379
+ test('should kill the process', async () => {
380
+ // Arrange
381
+ const startPromise = client.start();
382
+
383
+ setTimeout(() => {
384
+ mockProcess.stdout.emit('data', JSON.stringify({
385
+ jsonrpc: '2.0',
386
+ id: 1,
387
+ result: { protocolVersion: '2024-11-05' }
388
+ }) + '\n');
389
+ }, 10);
390
+
391
+ await startPromise;
392
+
393
+ // Act
394
+ client.stop();
395
+
396
+ // Assert
397
+ expect(mockProcess.kill).toHaveBeenCalled();
398
+ });
399
+ });
400
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Cascade MCP Client
3
+ *
4
+ * Handles communication with Cascade MCP server via stdio JSON-RPC
5
+ */
6
+
7
+ import { spawn, ChildProcess } from 'child_process';
8
+
9
+ export class CascadeMcpClient {
10
+ private process: ChildProcess | null = null;
11
+ private messageId = 0;
12
+ private pendingRequests = new Map<number, { resolve: Function; reject: Function }>();
13
+ private isInitialized = false;
14
+ private readonly REQUEST_TIMEOUT = 30000; // 30 seconds
15
+ private buffer = '';
16
+
17
+ constructor(
18
+ private pythonPath: string,
19
+ private env: NodeJS.ProcessEnv
20
+ ) {}
21
+
22
+ /**
23
+ * Start the MCP client and initialize connection
24
+ */
25
+ async start(): Promise<void> {
26
+ try {
27
+ // Spawn Cascade MCP server
28
+ this.process = spawn(this.pythonPath, ['-m', 'mcp_server.cli'], {
29
+ env: { ...process.env, ...this.env },
30
+ stdio: ['pipe', 'pipe', 'pipe']
31
+ });
32
+
33
+ // Handle stdout for JSON-RPC responses
34
+ if (this.process.stdout) {
35
+ this.process.stdout.on('data', (data: Buffer) => {
36
+ this.handleData(data.toString());
37
+ });
38
+ }
39
+
40
+ // Handle stderr
41
+ if (this.process.stderr) {
42
+ this.process.stderr.on('data', (data: Buffer) => {
43
+ console.error('Cascade MCP stderr:', data.toString());
44
+ });
45
+ }
46
+
47
+ // Handle process errors
48
+ this.process.on('error', (error: Error) => {
49
+ console.error('Cascade MCP process error:', error);
50
+ this.rejectAllPending(error);
51
+ });
52
+
53
+ // Initialize MCP connection
54
+ await this.sendRequest('initialize', {
55
+ protocolVersion: '2024-11-05',
56
+ capabilities: {},
57
+ clientInfo: { name: 'openclaw-cascade', version: '1.0.0' }
58
+ });
59
+
60
+ this.isInitialized = true;
61
+ } catch (error) {
62
+ this.stop();
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Call a tool by name with arguments
69
+ */
70
+ async callTool(name: string, args: Record<string, any>): Promise<any> {
71
+ if (!this.isInitialized) {
72
+ throw new Error('MCP client not initialized. Call start() first.');
73
+ }
74
+
75
+ return this.sendRequest('tools/call', { name, arguments: args });
76
+ }
77
+
78
+ /**
79
+ * List all available tools
80
+ */
81
+ async listTools(): Promise<any[]> {
82
+ if (!this.isInitialized) {
83
+ throw new Error('MCP client not initialized. Call start() first.');
84
+ }
85
+
86
+ const result = await this.sendRequest('tools/list', {});
87
+ return result.tools || [];
88
+ }
89
+
90
+ /**
91
+ * Check if client is connected
92
+ */
93
+ isConnected(): boolean {
94
+ return this.isInitialized && this.process !== null && !this.process.killed;
95
+ }
96
+
97
+ /**
98
+ * Stop the MCP client
99
+ */
100
+ stop(): void {
101
+ this.rejectAllPending(new Error('Client stopped'));
102
+
103
+ if (this.process && !this.process.killed) {
104
+ this.process.kill();
105
+ }
106
+
107
+ this.process = null;
108
+ this.isInitialized = false;
109
+ this.buffer = '';
110
+ }
111
+
112
+ /**
113
+ * Handle incoming data from stdout
114
+ */
115
+ private handleData(data: string): void {
116
+ this.buffer += data;
117
+
118
+ // Process complete lines
119
+ const lines = this.buffer.split('\n');
120
+ this.buffer = lines.pop() || ''; // Keep incomplete line in buffer
121
+
122
+ for (const line of lines) {
123
+ if (line.trim()) {
124
+ this.handleResponse(line);
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Send a JSON-RPC request
131
+ */
132
+ private async sendRequest(method: string, params: any): Promise<any> {
133
+ const id = ++this.messageId;
134
+ const request = { jsonrpc: '2.0', id, method, params };
135
+
136
+ return new Promise((resolve, reject) => {
137
+ // Set timeout
138
+ const timeoutId = setTimeout(() => {
139
+ this.pendingRequests.delete(id);
140
+ reject(new Error(`Request ${method} timed out after ${this.REQUEST_TIMEOUT}ms`));
141
+ }, this.REQUEST_TIMEOUT);
142
+
143
+ // Store pending request
144
+ this.pendingRequests.set(id, {
145
+ resolve: (result: any) => {
146
+ clearTimeout(timeoutId);
147
+ resolve(result);
148
+ },
149
+ reject: (error: Error) => {
150
+ clearTimeout(timeoutId);
151
+ reject(error);
152
+ }
153
+ });
154
+
155
+ // Send request
156
+ if (this.process?.stdin) {
157
+ this.process.stdin.write(JSON.stringify(request) + '\n');
158
+ } else {
159
+ this.pendingRequests.delete(id);
160
+ clearTimeout(timeoutId);
161
+ reject(new Error('Process stdin not available'));
162
+ }
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Handle incoming JSON-RPC response
168
+ */
169
+ private handleResponse(line: string): void {
170
+ try {
171
+ const response = JSON.parse(line);
172
+
173
+ if (response.id && this.pendingRequests.has(response.id)) {
174
+ const { resolve, reject } = this.pendingRequests.get(response.id)!;
175
+ this.pendingRequests.delete(response.id);
176
+
177
+ if (response.error) {
178
+ reject(new Error(response.error.message || 'Unknown error'));
179
+ } else {
180
+ resolve(response.result);
181
+ }
182
+ }
183
+ } catch (e) {
184
+ // Ignore parse errors for non-JSON lines
185
+ console.debug('Failed to parse MCP response:', line.substring(0, 100));
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Reject all pending requests
191
+ */
192
+ private rejectAllPending(error: Error): void {
193
+ for (const [_id, { reject }] of this.pendingRequests) {
194
+ reject(error);
195
+ }
196
+ this.pendingRequests.clear();
197
+ }
198
+ }