@prmichaelsen/acp-mcp 0.6.0 → 0.7.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,940 @@
1
+ # Design: Progress Streaming for Long-Running Commands
2
+
3
+ **Concept**: Real-time output streaming for long-running SSH commands using MCP progress notifications
4
+ **Created**: 2026-02-23
5
+ **Status**: Design Specification
6
+ **Version**: 1.0.0
7
+
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ This design document specifies how to implement real-time progress streaming for long-running commands in acp-mcp using the MCP SDK's built-in progress notification system. This enables users to see live output from commands like `npm run dev`, `npm run build`, or `npm test` as they execute on remote machines.
13
+
14
+ ## Problem Statement
15
+
16
+ Currently, `acp_remote_execute_command` uses a timeout-based approach where:
17
+ - Command executes completely before returning any output
18
+ - Users wait without feedback for long-running operations
19
+ - Commands that exceed timeout fail, even if still running
20
+ - No way to see incremental progress or output
21
+
22
+ **User Impact**:
23
+ - Poor experience for long-running builds (5+ minutes)
24
+ - Cannot monitor development servers in real-time
25
+ - Difficult to debug failing commands (no intermediate output)
26
+ - Timeout errors for legitimate long operations
27
+
28
+ **Example Scenarios**:
29
+ ```bash
30
+ # Build that takes 10 minutes - user sees nothing until complete or timeout
31
+ npm run build
32
+
33
+ # Dev server - runs indefinitely, would timeout
34
+ npm run dev
35
+
36
+ # Test suite - want to see tests as they run
37
+ npm test --verbose
38
+ ```
39
+
40
+ ## Solution
41
+
42
+ Implement **progress streaming** using MCP SDK's native progress notification system:
43
+
44
+ 1. **Detect progress support** - Check if client provided `progressToken`
45
+ 2. **Stream SSH output** - Use SSH stream instead of buffered execution
46
+ 3. **Send progress notifications** - Forward output chunks to client in real-time
47
+ 4. **Graceful fallback** - Use existing timeout approach if no progress support
48
+
49
+ ### Architecture
50
+
51
+ ```
52
+ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
53
+ │ Client │ │ acp-mcp │ │ Remote │
54
+ │ (Claude/UI) │ │ Server │ │ Machine │
55
+ └─────────────┘ └──────────────┘ └─────────────┘
56
+ │ │ │
57
+ │ CallTool(progressToken=123) │ │
58
+ ├──────────────────────────────────>│ │
59
+ │ │ SSH exec stream │
60
+ │ ├──────────────────────────────────>│
61
+ │ │ │
62
+ │ │<──────────────────────────────────┤
63
+ │ │ stdout chunk 1 │
64
+ │<──────────────────────────────────┤ │
65
+ │ Progress(token=123, msg=chunk1) │ │
66
+ │ │ │
67
+ │ │<──────────────────────────────────┤
68
+ │ │ stdout chunk 2 │
69
+ │<──────────────────────────────────┤ │
70
+ │ Progress(token=123, msg=chunk2) │ │
71
+ │ │ │
72
+ │ │<──────────────────────────────────┤
73
+ │ │ exit code 0 │
74
+ │<──────────────────────────────────┤ │
75
+ │ Result(stdout=full, exitCode=0) │ │
76
+ │ │ │
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Implementation
82
+
83
+ ### 1. Update SSHConnectionManager
84
+
85
+ Add streaming execution method:
86
+
87
+ ```typescript
88
+ // src/utils/ssh-connection.ts
89
+
90
+ /**
91
+ * Execute command with streaming output
92
+ * Returns a stream that emits data chunks as they arrive
93
+ */
94
+ async execStream(
95
+ command: string,
96
+ cwd?: string
97
+ ): Promise<{
98
+ stream: NodeJS.ReadableStream;
99
+ stderr: NodeJS.ReadableStream;
100
+ exitCode: Promise<number>;
101
+ }> {
102
+ if (!this.connected) {
103
+ await this.connect();
104
+ }
105
+
106
+ const fullCommand = cwd ? `cd "${cwd}" && ${command}` : command;
107
+ logger.sshCommand(fullCommand, cwd);
108
+
109
+ return new Promise((resolve, reject) => {
110
+ this.client.exec(fullCommand, (err, stream) => {
111
+ if (err) {
112
+ logger.error('SSH exec failed', { command: fullCommand, error: err.message });
113
+ reject(err);
114
+ return;
115
+ }
116
+
117
+ const exitCodePromise = new Promise<number>((resolveExit) => {
118
+ stream.on('close', (code: number) => {
119
+ logger.debug('SSH stream closed', { command: fullCommand, exitCode: code });
120
+ resolveExit(code);
121
+ });
122
+ });
123
+
124
+ resolve({
125
+ stream: stream,
126
+ stderr: stream.stderr,
127
+ exitCode: exitCodePromise,
128
+ });
129
+ });
130
+ });
131
+ }
132
+ ```
133
+
134
+ ### 2. Update Tool Handler
135
+
136
+ Modify `acp_remote_execute_command` to support progress:
137
+
138
+ ```typescript
139
+ // src/tools/acp-remote-execute-command.ts
140
+
141
+ export async function handleAcpRemoteExecuteCommand(
142
+ args: any,
143
+ sshConnection: SSHConnectionManager,
144
+ extra?: { progressToken?: string | number }
145
+ ): Promise<{ content: Array<{ type: string; text: string }> }> {
146
+ const { command, cwd, timeout = 30 } = args;
147
+ const progressToken = extra?.progressToken;
148
+
149
+ try {
150
+ // If progress token provided, use streaming
151
+ if (progressToken) {
152
+ return await executeWithProgress(command, cwd, sshConnection, progressToken);
153
+ }
154
+
155
+ // Otherwise, use existing timeout-based execution
156
+ const result = await sshConnection.execWithTimeout(command, timeout);
157
+ return {
158
+ content: [{
159
+ type: 'text',
160
+ text: JSON.stringify({
161
+ stdout: result.stdout,
162
+ stderr: result.stderr,
163
+ exitCode: result.exitCode,
164
+ timedOut: result.timedOut,
165
+ }, null, 2),
166
+ }],
167
+ };
168
+ } catch (error) {
169
+ const errorMessage = error instanceof Error ? error.message : String(error);
170
+ return {
171
+ content: [{
172
+ type: 'text',
173
+ text: `Error executing command: ${errorMessage}`,
174
+ }],
175
+ };
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Execute command with progress streaming
181
+ */
182
+ async function executeWithProgress(
183
+ command: string,
184
+ cwd: string | undefined,
185
+ sshConnection: SSHConnectionManager,
186
+ progressToken: string | number
187
+ ): Promise<{ content: Array<{ type: string; text: string }> }> {
188
+ const { stream, stderr: stderrStream, exitCode } = await sshConnection.execStream(command, cwd);
189
+
190
+ let stdout = '';
191
+ let stderr = '';
192
+ let bytesReceived = 0;
193
+
194
+ // Stream stdout with progress notifications
195
+ stream.on('data', (chunk: Buffer) => {
196
+ const text = chunk.toString();
197
+ stdout += text;
198
+ bytesReceived += chunk.length;
199
+
200
+ // Send progress notification
201
+ server.notification({
202
+ method: 'notifications/progress',
203
+ params: {
204
+ progressToken,
205
+ progress: bytesReceived,
206
+ total: undefined, // Unknown total for streaming
207
+ message: text, // Send chunk as message
208
+ },
209
+ });
210
+
211
+ logger.debug('Progress sent', { progressToken, bytes: bytesReceived });
212
+ });
213
+
214
+ // Collect stderr (no progress for errors)
215
+ stderrStream.on('data', (chunk: Buffer) => {
216
+ stderr += chunk.toString();
217
+ });
218
+
219
+ // Wait for completion
220
+ const finalExitCode = await exitCode;
221
+
222
+ logger.debug('Command completed', {
223
+ command,
224
+ exitCode: finalExitCode,
225
+ stdoutBytes: stdout.length,
226
+ stderrBytes: stderr.length,
227
+ });
228
+
229
+ return {
230
+ content: [{
231
+ type: 'text',
232
+ text: JSON.stringify({
233
+ stdout,
234
+ stderr,
235
+ exitCode: finalExitCode,
236
+ timedOut: false,
237
+ streamed: true, // Indicate this was streamed
238
+ }, null, 2),
239
+ }],
240
+ };
241
+ }
242
+ ```
243
+
244
+ ### 3. Update Server Request Handler
245
+
246
+ Pass `extra` parameter to tool handlers:
247
+
248
+ ```typescript
249
+ // src/server.ts and src/server-factory.ts
250
+
251
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
252
+ const startTime = Date.now();
253
+ logger.toolInvoked(request.params.name, request.params.arguments);
254
+
255
+ try {
256
+ let result;
257
+
258
+ if (request.params.name === 'acp_remote_execute_command') {
259
+ // Pass extra parameter for progress token
260
+ result = await handleAcpRemoteExecuteCommand(
261
+ request.params.arguments,
262
+ sshConnection,
263
+ extra // Contains progressToken if provided
264
+ );
265
+ } else if (request.params.name === 'acp_remote_list_files') {
266
+ result = await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
267
+ } else if (request.params.name === 'acp_remote_read_file') {
268
+ result = await handleAcpRemoteReadFile(request.params.arguments, sshConnection);
269
+ } else if (request.params.name === 'acp_remote_write_file') {
270
+ result = await handleAcpRemoteWriteFile(request.params.arguments, sshConnection);
271
+ } else {
272
+ throw new Error(`Unknown tool: ${request.params.name}`);
273
+ }
274
+
275
+ const duration = Date.now() - startTime;
276
+ const resultSize = JSON.stringify(result).length;
277
+ logger.toolCompleted(request.params.name, duration, resultSize);
278
+
279
+ return result;
280
+ } catch (error) {
281
+ logger.toolFailed(request.params.name, error as Error, request.params.arguments);
282
+ throw error;
283
+ }
284
+ });
285
+ ```
286
+
287
+ ### 4. Update Tool Schema (Optional)
288
+
289
+ Document progress support in tool description:
290
+
291
+ ```typescript
292
+ // src/tools/acp-remote-execute-command.ts
293
+
294
+ export const acpRemoteExecuteCommandTool: Tool = {
295
+ name: 'acp_remote_execute_command',
296
+ description: 'Execute a shell command on the remote machine via SSH. Supports real-time progress streaming if client provides progressToken.',
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ command: {
301
+ type: 'string',
302
+ description: 'Shell command to execute',
303
+ },
304
+ cwd: {
305
+ type: 'string',
306
+ description: 'Working directory (optional)',
307
+ },
308
+ timeout: {
309
+ type: 'number',
310
+ description: 'Timeout in seconds (default: 30). Ignored if progress streaming is used.',
311
+ default: 30,
312
+ },
313
+ },
314
+ required: ['command'],
315
+ },
316
+ };
317
+ ```
318
+
319
+ ---
320
+
321
+ ## Benefits
322
+
323
+ ### For Users
324
+
325
+ 1. **Real-time feedback** - See output as it happens
326
+ 2. **Better debugging** - See where commands fail
327
+ 3. **No artificial timeouts** - Long operations can run indefinitely
328
+ 4. **Progress visibility** - Know command is still running
329
+ 5. **Better UX** - More responsive feel
330
+
331
+ ### For Developers
332
+
333
+ 1. **Native MCP feature** - No custom protocol needed
334
+ 2. **Graceful degradation** - Works with or without client support
335
+ 3. **No breaking changes** - Existing API still works
336
+ 4. **Simple implementation** - SDK handles complexity
337
+ 5. **Reliable** - Built on proven SSH streaming
338
+
339
+ ---
340
+
341
+ ## Trade-offs
342
+
343
+ ### Advantages
344
+
345
+ ✅ **Real-time output** - Users see progress immediately
346
+ ✅ **No timeout issues** - Progress resets timeout automatically
347
+ ✅ **Standard protocol** - Uses MCP SDK features
348
+ ✅ **Backward compatible** - Falls back to timeout mode
349
+ ✅ **Better UX** - More responsive and informative
350
+
351
+ ### Disadvantages
352
+
353
+ ⚠️ **Client dependency** - Requires client support for progress
354
+ ⚠️ **Complexity** - Two execution paths (streaming vs timeout)
355
+ ⚠️ **Memory usage** - Must buffer full output for final result
356
+ ⚠️ **Network overhead** - More messages sent (progress notifications)
357
+ ⚠️ **Testing complexity** - Need to test both modes
358
+
359
+ ### Decision
360
+
361
+ **Implement with graceful fallback**: Use streaming when `progressToken` provided, otherwise use existing timeout approach. This gives best of both worlds with no breaking changes.
362
+
363
+ ---
364
+
365
+ ## Client Support
366
+
367
+ ### MCP SDK Support
368
+
369
+ **Confirmed**: `@modelcontextprotocol/sdk` v1.26.0 has full support:
370
+ - `progressToken` parameter in request params
371
+ - `onprogress` callback for client-side handling
372
+ - Progress notifications reset request timeout
373
+ - Task-augmented requests for long operations
374
+
375
+ ### Client Implementation
376
+
377
+ **Client Side** (Claude Desktop, mcp-auth, etc.):
378
+ ```typescript
379
+ const result = await client.request({
380
+ method: 'tools/call',
381
+ params: {
382
+ name: 'acp_remote_execute_command',
383
+ arguments: { command: 'npm run build' }
384
+ }
385
+ }, {
386
+ progressToken: 'build-123', // Request progress
387
+ onprogress: (progress) => {
388
+ // Display progress to user
389
+ console.log(progress.message);
390
+ updateUI(progress);
391
+ }
392
+ });
393
+ ```
394
+
395
+ ### Compatibility
396
+
397
+ **Known Support**:
398
+ - ✅ MCP SDK v1.26.0+ (confirmed)
399
+ - ❓ Claude Desktop (unknown version support)
400
+ - ❓ mcp-auth wrapper (needs testing)
401
+ - ❓ Other MCP clients (varies)
402
+
403
+ **Fallback**: If client doesn't provide `progressToken`, uses existing timeout-based execution. No functionality lost.
404
+
405
+ ---
406
+
407
+ ## Use Cases
408
+
409
+ ### Use Case 1: Long Build Process
410
+
411
+ **Scenario**: Building a large TypeScript project (10 minutes)
412
+
413
+ **Without Progress**:
414
+ ```
415
+ User: Run npm run build
416
+ [10 minutes of silence]
417
+ Result: Build complete (or timeout error)
418
+ ```
419
+
420
+ **With Progress**:
421
+ ```
422
+ User: Run npm run build
423
+ [Immediate feedback]
424
+ > Building...
425
+ > Compiling src/index.ts
426
+ > Compiling src/utils.ts
427
+ > [100 more files...]
428
+ > Build complete!
429
+ Result: Build complete with full output
430
+ ```
431
+
432
+ ### Use Case 2: Development Server
433
+
434
+ **Scenario**: Starting a dev server that runs indefinitely
435
+
436
+ **Without Progress**:
437
+ ```
438
+ User: Run npm run dev
439
+ [30 seconds]
440
+ Error: Command timed out
441
+ ```
442
+
443
+ **With Progress**:
444
+ ```
445
+ User: Run npm run dev
446
+ > Starting dev server...
447
+ > Webpack compiled successfully
448
+ > Server running on http://localhost:3000
449
+ [Server continues running, user sees logs in real-time]
450
+ ```
451
+
452
+ ### Use Case 3: Test Suite
453
+
454
+ **Scenario**: Running comprehensive test suite (5 minutes)
455
+
456
+ **Without Progress**:
457
+ ```
458
+ User: Run npm test
459
+ [5 minutes of silence]
460
+ Result: 150 tests passed
461
+ ```
462
+
463
+ **With Progress**:
464
+ ```
465
+ User: Run npm test
466
+ > Running test suite...
467
+ > ✓ Auth tests (15 passed)
468
+ > ✓ API tests (42 passed)
469
+ > ✓ Integration tests (93 passed)
470
+ > All tests passed!
471
+ Result: 150 tests passed with details
472
+ ```
473
+
474
+ ---
475
+
476
+ ## Error Handling
477
+
478
+ ### SSH Stream Errors
479
+
480
+ ```typescript
481
+ stream.on('error', (error) => {
482
+ logger.error('SSH stream error', { error: error.message });
483
+
484
+ // Send error via progress
485
+ server.notification({
486
+ method: 'notifications/progress',
487
+ params: {
488
+ progressToken,
489
+ progress: bytesReceived,
490
+ message: `Error: ${error.message}`,
491
+ },
492
+ });
493
+
494
+ // Return error in final result
495
+ return {
496
+ content: [{
497
+ type: 'text',
498
+ text: JSON.stringify({
499
+ stdout,
500
+ stderr: stderr + `\nStream error: ${error.message}`,
501
+ exitCode: -1,
502
+ error: error.message,
503
+ }),
504
+ }],
505
+ };
506
+ });
507
+ ```
508
+
509
+ ### Connection Loss
510
+
511
+ ```typescript
512
+ // Detect connection loss
513
+ this.client.on('close', () => {
514
+ if (streamActive) {
515
+ logger.error('SSH connection closed during streaming');
516
+ // Notify client
517
+ server.notification({
518
+ method: 'notifications/progress',
519
+ params: {
520
+ progressToken,
521
+ message: 'Connection lost',
522
+ },
523
+ });
524
+ }
525
+ });
526
+ ```
527
+
528
+ ### Timeout Management
529
+
530
+ ```typescript
531
+ // Progress notifications reset timeout automatically (MCP SDK feature)
532
+ // But we can add explicit timeout for safety:
533
+
534
+ const maxDuration = 3600; // 1 hour max
535
+ const startTime = Date.now();
536
+
537
+ stream.on('data', (chunk) => {
538
+ const elapsed = (Date.now() - startTime) / 1000;
539
+
540
+ if (elapsed > maxDuration) {
541
+ stream.destroy();
542
+ throw new Error('Command exceeded maximum duration (1 hour)');
543
+ }
544
+
545
+ // Send progress...
546
+ });
547
+ ```
548
+
549
+ ---
550
+
551
+ ## Testing Strategy
552
+
553
+ ### Unit Tests
554
+
555
+ ```typescript
556
+ describe('executeWithProgress', () => {
557
+ it('should stream output chunks', async () => {
558
+ const mockStream = new EventEmitter();
559
+ const progressNotifications: any[] = [];
560
+
561
+ // Mock server.notification
562
+ server.notification = (params) => {
563
+ progressNotifications.push(params);
564
+ };
565
+
566
+ // Simulate streaming
567
+ mockStream.emit('data', Buffer.from('chunk 1\n'));
568
+ mockStream.emit('data', Buffer.from('chunk 2\n'));
569
+ mockStream.emit('close', 0);
570
+
571
+ expect(progressNotifications).toHaveLength(2);
572
+ expect(progressNotifications[0].params.message).toBe('chunk 1\n');
573
+ });
574
+
575
+ it('should fall back to timeout mode without progressToken', async () => {
576
+ const result = await handleAcpRemoteExecuteCommand(
577
+ { command: 'echo test' },
578
+ sshConnection,
579
+ undefined // No progressToken
580
+ );
581
+
582
+ expect(result.content[0].text).toContain('test');
583
+ });
584
+ });
585
+ ```
586
+
587
+ ### Integration Tests
588
+
589
+ ```typescript
590
+ describe('Progress Streaming Integration', () => {
591
+ it('should stream real SSH command output', async () => {
592
+ const ssh = new SSHConnectionManager(testConfig);
593
+ await ssh.connect();
594
+
595
+ const progressMessages: string[] = [];
596
+ const progressToken = 'test-123';
597
+
598
+ // Mock notification handler
599
+ server.notification = (params) => {
600
+ if (params.params.progressToken === progressToken) {
601
+ progressMessages.push(params.params.message);
602
+ }
603
+ };
604
+
605
+ await handleAcpRemoteExecuteCommand(
606
+ { command: 'for i in 1 2 3; do echo "Line $i"; sleep 0.1; done' },
607
+ ssh,
608
+ { progressToken }
609
+ );
610
+
611
+ expect(progressMessages.length).toBeGreaterThan(0);
612
+ expect(progressMessages.join('')).toContain('Line 1');
613
+ });
614
+ });
615
+ ```
616
+
617
+ ### Manual Testing
618
+
619
+ 1. **Test with Claude Desktop** (if supports progress)
620
+ 2. **Test with mcp-auth wrapper**
621
+ 3. **Test long-running commands** (`npm run build`)
622
+ 4. **Test infinite commands** (`npm run dev`)
623
+ 5. **Test error scenarios** (command fails mid-stream)
624
+ 6. **Test connection loss** (kill SSH during stream)
625
+
626
+ ---
627
+
628
+ ## Migration Path
629
+
630
+ ### Phase 1: Implementation (v0.7.0)
631
+
632
+ - Add `execStream()` to SSHConnectionManager
633
+ - Update `acp_remote_execute_command` handler
634
+ - Add progress notification logic
635
+ - Maintain backward compatibility
636
+
637
+ ### Phase 2: Testing (v0.7.0)
638
+
639
+ - Unit tests for streaming
640
+ - Integration tests with real SSH
641
+ - Manual testing with various commands
642
+ - Performance testing (memory, network)
643
+
644
+ ### Phase 3: Documentation (v0.7.0)
645
+
646
+ - Update README with progress support
647
+ - Add examples of streaming usage
648
+ - Document client requirements
649
+ - Update CHANGELOG
650
+
651
+ ### Phase 4: Deployment (v0.7.0)
652
+
653
+ - Deploy to npm
654
+ - Test with mcp-auth wrapper
655
+ - Test with agentbase.me platform
656
+ - Monitor for issues
657
+
658
+ ### Phase 5: Optimization (v0.8.0+)
659
+
660
+ - Add progress percentage calculation
661
+ - Implement smart buffering
662
+ - Add progress rate limiting (avoid spam)
663
+ - Add configurable chunk sizes
664
+
665
+ ---
666
+
667
+ ## Future Enhancements
668
+
669
+ ### 1. Background Process Management
670
+
671
+ Combine progress streaming with background processes:
672
+
673
+ ```typescript
674
+ // Start process in background with log streaming
675
+ {
676
+ name: 'acp_remote_start_process_with_logs',
677
+ handler: async (args, ssh, extra) => {
678
+ const { command, logFile } = args;
679
+
680
+ // Start in background
681
+ await ssh.exec(`nohup ${command} > ${logFile} 2>&1 &`);
682
+
683
+ // Stream log file if progressToken provided
684
+ if (extra?.progressToken) {
685
+ await streamLogFile(logFile, ssh, extra.progressToken);
686
+ }
687
+ }
688
+ }
689
+ ```
690
+
691
+ ### 2. Interactive Commands
692
+
693
+ Support for interactive commands (requires stdin):
694
+
695
+ ```typescript
696
+ // Send input to running command
697
+ {
698
+ name: 'acp_remote_send_input',
699
+ handler: async (args, ssh) => {
700
+ const { processId, input } = args;
701
+ // Send input to stdin of running process
702
+ }
703
+ }
704
+ ```
705
+
706
+ ### 3. Progress Percentage
707
+
708
+ Calculate progress for known operations:
709
+
710
+ ```typescript
711
+ // For npm install, parse package count
712
+ stream.on('data', (chunk) => {
713
+ const match = chunk.toString().match(/(\d+)\/(\d+)/);
714
+ if (match) {
715
+ const [_, current, total] = match;
716
+ server.notification({
717
+ method: 'notifications/progress',
718
+ params: {
719
+ progressToken,
720
+ progress: parseInt(current),
721
+ total: parseInt(total),
722
+ message: chunk.toString(),
723
+ },
724
+ });
725
+ }
726
+ });
727
+ ```
728
+
729
+ ### 4. Multi-Command Streaming
730
+
731
+ Stream output from multiple commands in sequence:
732
+
733
+ ```typescript
734
+ const commands = ['npm install', 'npm run build', 'npm test'];
735
+ let overallProgress = 0;
736
+
737
+ for (const command of commands) {
738
+ await executeWithProgress(command, ...);
739
+ overallProgress += 33; // Each command is 33% of total
740
+
741
+ server.notification({
742
+ method: 'notifications/progress',
743
+ params: {
744
+ progressToken,
745
+ progress: overallProgress,
746
+ total: 100,
747
+ },
748
+ });
749
+ }
750
+ ```
751
+
752
+ ---
753
+
754
+ ## Performance Considerations
755
+
756
+ ### Memory Usage
757
+
758
+ **Issue**: Buffering full output for final result
759
+
760
+ **Solution**:
761
+ - Limit buffer size (e.g., 10MB max)
762
+ - Truncate if exceeded
763
+ - Notify user of truncation
764
+
765
+ ```typescript
766
+ const MAX_BUFFER = 10 * 1024 * 1024; // 10MB
767
+
768
+ stream.on('data', (chunk) => {
769
+ if (stdout.length + chunk.length > MAX_BUFFER) {
770
+ stdout += '\n[Output truncated - exceeded 10MB limit]';
771
+ stream.destroy();
772
+ return;
773
+ }
774
+ stdout += chunk.toString();
775
+ // Send progress...
776
+ });
777
+ ```
778
+
779
+ ### Network Overhead
780
+
781
+ **Issue**: Many progress notifications increase network traffic
782
+
783
+ **Solution**:
784
+ - Rate limit notifications (e.g., max 10/second)
785
+ - Batch small chunks
786
+ - Only send on newlines for text output
787
+
788
+ ```typescript
789
+ let lastProgressTime = 0;
790
+ const MIN_PROGRESS_INTERVAL = 100; // 100ms between notifications
791
+
792
+ stream.on('data', (chunk) => {
793
+ stdout += chunk.toString();
794
+
795
+ const now = Date.now();
796
+ if (now - lastProgressTime >= MIN_PROGRESS_INTERVAL) {
797
+ server.notification({ /* ... */ });
798
+ lastProgressTime = now;
799
+ }
800
+ });
801
+ ```
802
+
803
+ ### CPU Usage
804
+
805
+ **Issue**: Parsing and formatting progress messages
806
+
807
+ **Solution**:
808
+ - Minimal processing in hot path
809
+ - Defer complex parsing to final result
810
+ - Use efficient string operations
811
+
812
+ ---
813
+
814
+ ## Security Considerations
815
+
816
+ ### Output Sanitization
817
+
818
+ **Risk**: Sensitive data in command output (passwords, keys)
819
+
820
+ **Mitigation**:
821
+ - Warn users about streaming sensitive commands
822
+ - Consider adding output filtering
823
+ - Log warnings for commands with sensitive patterns
824
+
825
+ ```typescript
826
+ const SENSITIVE_PATTERNS = [
827
+ /password[=:]\s*\S+/i,
828
+ /api[_-]?key[=:]\s*\S+/i,
829
+ /secret[=:]\s*\S+/i,
830
+ ];
831
+
832
+ function sanitizeOutput(text: string): string {
833
+ let sanitized = text;
834
+ for (const pattern of SENSITIVE_PATTERNS) {
835
+ sanitized = sanitized.replace(pattern, '[REDACTED]');
836
+ }
837
+ return sanitized;
838
+ }
839
+ ```
840
+
841
+ ### Resource Limits
842
+
843
+ **Risk**: Malicious commands consuming resources
844
+
845
+ **Mitigation**:
846
+ - Maximum duration limit (1 hour)
847
+ - Maximum output size (10MB)
848
+ - Rate limiting on progress notifications
849
+ - Monitor CPU/memory usage
850
+
851
+ ### Command Injection
852
+
853
+ **Risk**: Same as existing execute_command
854
+
855
+ **Mitigation**:
856
+ - SSH handles command escaping
857
+ - No additional risk from streaming
858
+ - Existing security measures apply
859
+
860
+ ---
861
+
862
+ ## Alternatives Considered
863
+
864
+ ### Alternative 1: Polling-Based Progress
865
+
866
+ **Approach**: Return job ID, poll for updates
867
+
868
+ **Pros**:
869
+ - Works with all clients
870
+ - Simple to implement
871
+ - No protocol changes
872
+
873
+ **Cons**:
874
+ - Not real-time (polling delay)
875
+ - More complex state management
876
+ - Higher latency
877
+
878
+ **Decision**: Rejected - MCP SDK has native progress support
879
+
880
+ ### Alternative 2: WebSocket Transport
881
+
882
+ **Approach**: Use WebSocket instead of stdio
883
+
884
+ **Pros**:
885
+ - True bidirectional streaming
886
+ - Lower latency
887
+ - More flexible
888
+
889
+ **Cons**:
890
+ - Requires transport change
891
+ - Not compatible with stdio clients
892
+ - More complex deployment
893
+
894
+ **Decision**: Rejected - stdio is standard for MCP
895
+
896
+ ### Alternative 3: Server-Sent Events (SSE)
897
+
898
+ **Approach**: Use SSE transport for streaming
899
+
900
+ **Pros**:
901
+ - Native streaming support
902
+ - HTTP-based (firewall friendly)
903
+ - Good browser support
904
+
905
+ **Cons**:
906
+ - Requires SSE transport
907
+ - Not compatible with stdio
908
+ - More complex setup
909
+
910
+ **Decision**: Rejected - progress notifications work with stdio
911
+
912
+ ---
913
+
914
+ ## Recommendation
915
+
916
+ **Implement progress streaming in v0.7.0** with:
917
+
918
+ 1. ✅ Use MCP SDK progress notifications (native support)
919
+ 2. ✅ Graceful fallback to timeout mode (backward compatible)
920
+ 3. ✅ Start with `acp_remote_execute_command` (highest value)
921
+ 4. ✅ Add comprehensive testing (unit + integration)
922
+ 5. ✅ Document client requirements (README)
923
+ 6. ✅ Monitor performance (memory, network)
924
+
925
+ **Timeline**:
926
+ - Implementation: 1-2 days
927
+ - Testing: 1 day
928
+ - Documentation: 0.5 days
929
+ - Total: 2-3 days
930
+
931
+ **Priority**: Medium (nice-to-have, not critical)
932
+
933
+ **Dependencies**: None (SDK already supports it)
934
+
935
+ ---
936
+
937
+ **Status**: Design Specification
938
+ **Next Steps**: Create milestone and tasks for implementation
939
+ **Version**: 1.0.0
940
+ **Compatibility**: Requires MCP SDK v1.26.0+