@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.
- package/CHANGELOG.md +31 -0
- package/README.md +10 -3
- package/agent/design/local.progress-streaming.md +940 -0
- package/agent/milestones/milestone-4-progress-streaming-server.md +84 -0
- package/agent/milestones/milestone-5-progress-streaming-wrapper.md +71 -0
- package/agent/milestones/milestone-6-progress-streaming-client.md +79 -0
- package/agent/progress.yaml +93 -13
- package/agent/tasks/milestone-4-progress-streaming-server/task-6-add-ssh-stream-execution.md +149 -0
- package/agent/tasks/milestone-4-progress-streaming-server/task-7-implement-progress-streaming.md +191 -0
- package/agent/tasks/milestone-4-progress-streaming-server/task-8-update-server-handlers.md +109 -0
- package/agent/tasks/milestone-4-progress-streaming-server/task-9-testing-documentation.md +192 -0
- package/dist/server-factory.js +130 -6
- package/dist/server-factory.js.map +2 -2
- package/dist/server.js +130 -6
- package/dist/server.js.map +2 -2
- package/dist/tools/acp-remote-execute-command.d.ts +4 -1
- package/dist/utils/ssh-connection.d.ts +13 -0
- package/package.json +1 -1
- package/src/server-factory.ts +3 -2
- package/src/server.ts +3 -2
- package/src/tools/acp-remote-execute-command.ts +116 -7
- package/src/utils/ssh-connection.ts +66 -0
|
@@ -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+
|