@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
package/agent/tasks/milestone-4-progress-streaming-server/task-7-implement-progress-streaming.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Task 7: Implement Progress Streaming in Execute Command
|
|
2
|
+
|
|
3
|
+
**Milestone**: M4 - Progress Streaming - Server Implementation
|
|
4
|
+
**Estimated Time**: 4-5 hours
|
|
5
|
+
**Dependencies**: Task 6 (SSH stream execution)
|
|
6
|
+
**Status**: Not Started
|
|
7
|
+
**Priority**: High
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Objective
|
|
12
|
+
|
|
13
|
+
Update `acp_remote_execute_command` tool to support progress streaming when client provides a `progressToken`. Implement graceful fallback to timeout mode when no progress token is provided.
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
**Design Reference**: [`agent/design/local.progress-streaming.md`](../../design/local.progress-streaming.md)
|
|
18
|
+
|
|
19
|
+
**Current State**: Tool uses `execWithTimeout()` and returns all output at once
|
|
20
|
+
|
|
21
|
+
**Desired State**: Tool detects `progressToken`, streams output with progress notifications, maintains backward compatibility
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
### 1. Update Handler Signature
|
|
28
|
+
|
|
29
|
+
**File**: `src/tools/acp-remote-execute-command.ts`
|
|
30
|
+
|
|
31
|
+
**Actions**:
|
|
32
|
+
- Add `extra?: { progressToken?: string | number }` parameter
|
|
33
|
+
- Import server instance for sending notifications
|
|
34
|
+
- Add type definitions for progress params
|
|
35
|
+
|
|
36
|
+
### 2. Add Progress Detection Logic
|
|
37
|
+
|
|
38
|
+
**Actions**:
|
|
39
|
+
- Check if `extra?.progressToken` exists
|
|
40
|
+
- If yes: call `executeWithProgress()`
|
|
41
|
+
- If no: use existing `execWithTimeout()` (fallback)
|
|
42
|
+
|
|
43
|
+
### 3. Implement executeWithProgress() Function
|
|
44
|
+
|
|
45
|
+
**Actions**:
|
|
46
|
+
- Call `sshConnection.execStream()`
|
|
47
|
+
- Set up stdout data handler
|
|
48
|
+
- Send progress notification for each chunk
|
|
49
|
+
- Collect full output for final result
|
|
50
|
+
- Handle stderr separately (no progress)
|
|
51
|
+
- Wait for exit code
|
|
52
|
+
- Return structured result
|
|
53
|
+
|
|
54
|
+
**Code Pattern**:
|
|
55
|
+
```typescript
|
|
56
|
+
async function executeWithProgress(
|
|
57
|
+
command: string,
|
|
58
|
+
cwd: string | undefined,
|
|
59
|
+
sshConnection: SSHConnectionManager,
|
|
60
|
+
progressToken: string | number
|
|
61
|
+
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
62
|
+
const { stream, stderr: stderrStream, exitCode } = await sshConnection.execStream(command, cwd);
|
|
63
|
+
|
|
64
|
+
let stdout = '';
|
|
65
|
+
let stderr = '';
|
|
66
|
+
let bytesReceived = 0;
|
|
67
|
+
|
|
68
|
+
stream.on('data', (chunk: Buffer) => {
|
|
69
|
+
const text = chunk.toString();
|
|
70
|
+
stdout += text;
|
|
71
|
+
bytesReceived += chunk.length;
|
|
72
|
+
|
|
73
|
+
// Send progress notification
|
|
74
|
+
server.notification({
|
|
75
|
+
method: 'notifications/progress',
|
|
76
|
+
params: {
|
|
77
|
+
progressToken,
|
|
78
|
+
progress: bytesReceived,
|
|
79
|
+
total: undefined,
|
|
80
|
+
message: text,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
stderrStream.on('data', (chunk: Buffer) => {
|
|
86
|
+
stderr += chunk.toString();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const finalExitCode = await exitCode;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
content: [{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: JSON.stringify({
|
|
95
|
+
stdout,
|
|
96
|
+
stderr,
|
|
97
|
+
exitCode: finalExitCode,
|
|
98
|
+
timedOut: false,
|
|
99
|
+
streamed: true,
|
|
100
|
+
}, null, 2),
|
|
101
|
+
}],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 4. Add Rate Limiting
|
|
107
|
+
|
|
108
|
+
**Actions**:
|
|
109
|
+
- Limit progress notifications to max 10/second
|
|
110
|
+
- Batch small chunks
|
|
111
|
+
- Track last notification time
|
|
112
|
+
- Only send if enough time elapsed
|
|
113
|
+
|
|
114
|
+
**Code Pattern**:
|
|
115
|
+
```typescript
|
|
116
|
+
let lastProgressTime = 0;
|
|
117
|
+
const MIN_PROGRESS_INTERVAL = 100; // 100ms
|
|
118
|
+
|
|
119
|
+
stream.on('data', (chunk: Buffer) => {
|
|
120
|
+
stdout += chunk.toString();
|
|
121
|
+
bytesReceived += chunk.length;
|
|
122
|
+
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
if (now - lastProgressTime >= MIN_PROGRESS_INTERVAL) {
|
|
125
|
+
server.notification({ /* ... */ });
|
|
126
|
+
lastProgressTime = now;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 5. Add Error Handling
|
|
132
|
+
|
|
133
|
+
**Actions**:
|
|
134
|
+
- Handle stream errors
|
|
135
|
+
- Handle connection loss
|
|
136
|
+
- Send error via progress notification
|
|
137
|
+
- Return error in final result
|
|
138
|
+
|
|
139
|
+
### 6. Update Tool Schema
|
|
140
|
+
|
|
141
|
+
**Actions**:
|
|
142
|
+
- Update description to mention progress support
|
|
143
|
+
- Note that timeout is ignored when streaming
|
|
144
|
+
- Add examples of progress usage
|
|
145
|
+
|
|
146
|
+
### 7. Test Implementation
|
|
147
|
+
|
|
148
|
+
**Actions**:
|
|
149
|
+
- Build project
|
|
150
|
+
- Test with progressToken (streaming mode)
|
|
151
|
+
- Test without progressToken (fallback mode)
|
|
152
|
+
- Test with long-running command
|
|
153
|
+
- Test with command that fails
|
|
154
|
+
- Test error scenarios
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Verification
|
|
159
|
+
|
|
160
|
+
- [ ] Handler accepts `extra` parameter
|
|
161
|
+
- [ ] Detects `progressToken` correctly
|
|
162
|
+
- [ ] Calls `executeWithProgress()` when token provided
|
|
163
|
+
- [ ] Falls back to `execWithTimeout()` when no token
|
|
164
|
+
- [ ] Sends progress notifications for stdout chunks
|
|
165
|
+
- [ ] Rate limiting prevents notification spam
|
|
166
|
+
- [ ] Collects full output for final result
|
|
167
|
+
- [ ] Handles errors gracefully
|
|
168
|
+
- [ ] TypeScript compiles without errors
|
|
169
|
+
- [ ] Build completes successfully
|
|
170
|
+
- [ ] Both modes tested and working
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Expected Output
|
|
175
|
+
|
|
176
|
+
### Files Modified
|
|
177
|
+
- `src/tools/acp-remote-execute-command.ts` - Progress streaming implementation
|
|
178
|
+
|
|
179
|
+
### New Function
|
|
180
|
+
```typescript
|
|
181
|
+
async function executeWithProgress(
|
|
182
|
+
command: string,
|
|
183
|
+
cwd: string | undefined,
|
|
184
|
+
sshConnection: SSHConnectionManager,
|
|
185
|
+
progressToken: string | number
|
|
186
|
+
): Promise<{ content: Array<{ type: string; text: string }> }>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
**Next Task**: [Task 8: Update Server Request Handlers](task-8-update-server-handlers.md)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Task 8: Update Server Request Handlers
|
|
2
|
+
|
|
3
|
+
**Milestone**: M4 - Progress Streaming - Server Implementation
|
|
4
|
+
**Estimated Time**: 1-2 hours
|
|
5
|
+
**Dependencies**: Task 7 (Progress streaming implementation)
|
|
6
|
+
**Status**: Not Started
|
|
7
|
+
**Priority**: Medium
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Objective
|
|
12
|
+
|
|
13
|
+
Update server request handlers in both `server.ts` and `server-factory.ts` to pass the `extra` parameter (containing `progressToken`) to tool handlers.
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
**Design Reference**: [`agent/design/local.progress-streaming.md`](../../design/local.progress-streaming.md)
|
|
18
|
+
|
|
19
|
+
**Current State**: Handlers don't pass `extra` parameter to tools
|
|
20
|
+
|
|
21
|
+
**Desired State**: Handlers pass `extra` to `acp_remote_execute_command` handler
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
### 1. Update server.ts Handler
|
|
28
|
+
|
|
29
|
+
**File**: `src/server.ts`
|
|
30
|
+
|
|
31
|
+
**Actions**:
|
|
32
|
+
- Modify `CallToolRequestSchema` handler signature to accept `extra` parameter
|
|
33
|
+
- Pass `extra` to `handleAcpRemoteExecuteCommand()`
|
|
34
|
+
- Keep other tool handlers unchanged (they don't need progress)
|
|
35
|
+
|
|
36
|
+
**Code Pattern**:
|
|
37
|
+
```typescript
|
|
38
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
logger.toolInvoked(request.params.name, request.params.arguments);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
let result;
|
|
44
|
+
|
|
45
|
+
if (request.params.name === 'acp_remote_execute_command') {
|
|
46
|
+
result = await handleAcpRemoteExecuteCommand(
|
|
47
|
+
request.params.arguments,
|
|
48
|
+
sshConnection,
|
|
49
|
+
extra // Pass extra with progressToken
|
|
50
|
+
);
|
|
51
|
+
} else if (request.params.name === 'acp_remote_list_files') {
|
|
52
|
+
result = await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
|
|
53
|
+
}
|
|
54
|
+
// ... other tools
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logger.toolFailed(request.params.name, error as Error, request.params.arguments);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 2. Update server-factory.ts Handler
|
|
65
|
+
|
|
66
|
+
**File**: `src/server-factory.ts`
|
|
67
|
+
|
|
68
|
+
**Actions**:
|
|
69
|
+
- Same changes as server.ts
|
|
70
|
+
- Ensure consistency between both files
|
|
71
|
+
|
|
72
|
+
### 3. Verify No Breaking Changes
|
|
73
|
+
|
|
74
|
+
**Actions**:
|
|
75
|
+
- Confirm other tools still work
|
|
76
|
+
- Verify `extra` parameter is optional
|
|
77
|
+
- Test that undefined `extra` doesn't break anything
|
|
78
|
+
|
|
79
|
+
### 4. Test Both Server Modes
|
|
80
|
+
|
|
81
|
+
**Actions**:
|
|
82
|
+
- Build project
|
|
83
|
+
- Test standalone server (server.ts)
|
|
84
|
+
- Test factory server (server-factory.ts)
|
|
85
|
+
- Verify both pass `extra` correctly
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Verification
|
|
90
|
+
|
|
91
|
+
- [ ] server.ts handler updated
|
|
92
|
+
- [ ] server-factory.ts handler updated
|
|
93
|
+
- [ ] `extra` parameter passed to execute_command handler
|
|
94
|
+
- [ ] Other tools unaffected
|
|
95
|
+
- [ ] TypeScript compiles without errors
|
|
96
|
+
- [ ] Build completes successfully
|
|
97
|
+
- [ ] Both server modes tested
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Expected Output
|
|
102
|
+
|
|
103
|
+
### Files Modified
|
|
104
|
+
- `src/server.ts` - Pass `extra` to handler
|
|
105
|
+
- `src/server-factory.ts` - Pass `extra` to handler
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
**Next Task**: [Task 9: Testing and Documentation](task-9-testing-documentation.md)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Task 9: Testing and Documentation
|
|
2
|
+
|
|
3
|
+
**Milestone**: M4 - Progress Streaming - Server Implementation
|
|
4
|
+
**Estimated Time**: 3-4 hours
|
|
5
|
+
**Dependencies**: Tasks 6, 7, 8 (All implementation complete)
|
|
6
|
+
**Status**: Not Started
|
|
7
|
+
**Priority**: High
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Objective
|
|
12
|
+
|
|
13
|
+
Add comprehensive tests for progress streaming functionality and update all documentation for v0.7.0 release.
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
**Design Reference**: [`agent/design/local.progress-streaming.md`](../../design/local.progress-streaming.md)
|
|
18
|
+
|
|
19
|
+
**Current State**: Implementation complete, needs testing and documentation
|
|
20
|
+
|
|
21
|
+
**Desired State**: Full test coverage, updated documentation, ready for v0.7.0 release
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
### 1. Write Unit Tests
|
|
28
|
+
|
|
29
|
+
**File**: Create `src/utils/ssh-connection.test.ts` (if doesn't exist)
|
|
30
|
+
|
|
31
|
+
**Actions**:
|
|
32
|
+
- Test `execStream()` returns correct types
|
|
33
|
+
- Test stream emits data events
|
|
34
|
+
- Test exit code promise resolves
|
|
35
|
+
- Test error handling
|
|
36
|
+
- Mock SSH client for tests
|
|
37
|
+
|
|
38
|
+
### 2. Write Integration Tests
|
|
39
|
+
|
|
40
|
+
**File**: Create `src/tools/acp-remote-execute-command.test.ts`
|
|
41
|
+
|
|
42
|
+
**Actions**:
|
|
43
|
+
- Test with `progressToken` (streaming mode)
|
|
44
|
+
- Test without `progressToken` (fallback mode)
|
|
45
|
+
- Test progress notifications are sent
|
|
46
|
+
- Test rate limiting works
|
|
47
|
+
- Test error scenarios
|
|
48
|
+
- Use real SSH connection (or mock)
|
|
49
|
+
|
|
50
|
+
### 3. Manual Testing
|
|
51
|
+
|
|
52
|
+
**Actions**:
|
|
53
|
+
- Test with simple command: `echo "test"`
|
|
54
|
+
- Test with long command: `for i in 1 2 3 4 5; do echo "Line $i"; sleep 0.5; done`
|
|
55
|
+
- Test with failing command
|
|
56
|
+
- Test with very long output
|
|
57
|
+
- Test connection loss scenario
|
|
58
|
+
|
|
59
|
+
### 4. Update README.md
|
|
60
|
+
|
|
61
|
+
**File**: `README.md`
|
|
62
|
+
|
|
63
|
+
**Actions**:
|
|
64
|
+
- Add section on progress streaming support
|
|
65
|
+
- Document that progress requires MCP SDK v1.26.0+
|
|
66
|
+
- Add examples of progress usage
|
|
67
|
+
- Note client requirements
|
|
68
|
+
- Update tool description for `acp_remote_execute_command`
|
|
69
|
+
|
|
70
|
+
**Example Addition**:
|
|
71
|
+
```markdown
|
|
72
|
+
### Progress Streaming (v0.7.0+)
|
|
73
|
+
|
|
74
|
+
`acp_remote_execute_command` supports real-time progress streaming for long-running commands. When a client provides a `progressToken`, the tool will send progress notifications with live output.
|
|
75
|
+
|
|
76
|
+
**Requirements**:
|
|
77
|
+
- MCP SDK v1.26.0+ (server and client)
|
|
78
|
+
- Client must provide `progressToken` in request
|
|
79
|
+
- Client must handle `onprogress` callback
|
|
80
|
+
|
|
81
|
+
**Example** (client-side):
|
|
82
|
+
\`\`\`typescript
|
|
83
|
+
const result = await client.request({
|
|
84
|
+
method: 'tools/call',
|
|
85
|
+
params: {
|
|
86
|
+
name: 'acp_remote_execute_command',
|
|
87
|
+
arguments: { command: 'npm run build' }
|
|
88
|
+
}
|
|
89
|
+
}, {
|
|
90
|
+
progressToken: 'build-123',
|
|
91
|
+
onprogress: (progress) => {
|
|
92
|
+
console.log(progress.message); // Live output
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
97
|
+
**Fallback**: If no `progressToken` provided, uses timeout-based execution (backward compatible).
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5. Update CHANGELOG.md
|
|
101
|
+
|
|
102
|
+
**File**: `CHANGELOG.md`
|
|
103
|
+
|
|
104
|
+
**Actions**:
|
|
105
|
+
- Add v0.7.0 section
|
|
106
|
+
- Document progress streaming feature
|
|
107
|
+
- Note backward compatibility
|
|
108
|
+
- List all changes
|
|
109
|
+
|
|
110
|
+
**Example Entry**:
|
|
111
|
+
```markdown
|
|
112
|
+
## [0.7.0] - 2026-02-24
|
|
113
|
+
|
|
114
|
+
### Added
|
|
115
|
+
- **Progress Streaming** for `acp_remote_execute_command` tool
|
|
116
|
+
- Real-time output streaming for long-running commands
|
|
117
|
+
- Uses MCP SDK's native progress notification system
|
|
118
|
+
- Graceful fallback to timeout mode for clients without progress support
|
|
119
|
+
- Rate limiting prevents notification spam (max 10/second)
|
|
120
|
+
- Supports commands like `npm run build`, `npm run dev`, `npm test`
|
|
121
|
+
|
|
122
|
+
### Changed
|
|
123
|
+
- `acp_remote_execute_command` now accepts optional `progressToken` parameter
|
|
124
|
+
- Added `execStream()` method to SSHConnectionManager
|
|
125
|
+
- Server handlers now pass `extra` parameter to tool handlers
|
|
126
|
+
|
|
127
|
+
### Technical Details
|
|
128
|
+
- Requires MCP SDK v1.26.0+ for progress support
|
|
129
|
+
- Progress notifications sent via `notifications/progress` method
|
|
130
|
+
- Backward compatible - existing clients unaffected
|
|
131
|
+
- No breaking changes to API
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 6. Update package.json
|
|
135
|
+
|
|
136
|
+
**File**: `package.json`
|
|
137
|
+
|
|
138
|
+
**Actions**:
|
|
139
|
+
- Bump version to 0.7.0
|
|
140
|
+
- Verify dependencies are correct
|
|
141
|
+
|
|
142
|
+
### 7. Build and Verify
|
|
143
|
+
|
|
144
|
+
**Actions**:
|
|
145
|
+
- Run `npm run build`
|
|
146
|
+
- Verify TypeScript compiles
|
|
147
|
+
- Run tests: `npm test`
|
|
148
|
+
- Verify all tests pass
|
|
149
|
+
- Check for any warnings
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Verification
|
|
154
|
+
|
|
155
|
+
- [ ] Unit tests written and passing
|
|
156
|
+
- [ ] Integration tests written and passing
|
|
157
|
+
- [ ] Manual testing completed successfully
|
|
158
|
+
- [ ] README.md updated with progress documentation
|
|
159
|
+
- [ ] CHANGELOG.md updated for v0.7.0
|
|
160
|
+
- [ ] package.json version bumped to 0.7.0
|
|
161
|
+
- [ ] TypeScript compiles without errors
|
|
162
|
+
- [ ] Build completes successfully
|
|
163
|
+
- [ ] All tests pass
|
|
164
|
+
- [ ] No warnings or errors
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Expected Output
|
|
169
|
+
|
|
170
|
+
### Files Created
|
|
171
|
+
- `src/utils/ssh-connection.test.ts` - Unit tests (if doesn't exist)
|
|
172
|
+
- `src/tools/acp-remote-execute-command.test.ts` - Integration tests (if doesn't exist)
|
|
173
|
+
|
|
174
|
+
### Files Modified
|
|
175
|
+
- `README.md` - Progress streaming documentation
|
|
176
|
+
- `CHANGELOG.md` - v0.7.0 entry
|
|
177
|
+
- `package.json` - Version bump to 0.7.0
|
|
178
|
+
|
|
179
|
+
### Test Results
|
|
180
|
+
```
|
|
181
|
+
✓ SSH stream execution tests (5 passed)
|
|
182
|
+
✓ Progress streaming tests (8 passed)
|
|
183
|
+
✓ Fallback mode tests (3 passed)
|
|
184
|
+
✓ Error handling tests (4 passed)
|
|
185
|
+
|
|
186
|
+
Total: 20 tests passed
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
**Next Milestone**: M5 - Progress Streaming - Wrapper Integration (mcp-auth)
|
|
192
|
+
**Related Design**: [`agent/design/local.progress-streaming.md`](../../design/local.progress-streaming.md)
|
package/dist/server-factory.js
CHANGED
|
@@ -179,7 +179,7 @@ var logger = new Logger();
|
|
|
179
179
|
// src/tools/acp-remote-execute-command.ts
|
|
180
180
|
var acpRemoteExecuteCommandTool = {
|
|
181
181
|
name: "acp_remote_execute_command",
|
|
182
|
-
description: "Execute a shell command on the remote machine via SSH",
|
|
182
|
+
description: "Execute a shell command on the remote machine via SSH. Supports real-time progress streaming if client provides progressToken.",
|
|
183
183
|
inputSchema: {
|
|
184
184
|
type: "object",
|
|
185
185
|
properties: {
|
|
@@ -193,17 +193,21 @@ var acpRemoteExecuteCommandTool = {
|
|
|
193
193
|
},
|
|
194
194
|
timeout: {
|
|
195
195
|
type: "number",
|
|
196
|
-
description: "Timeout in seconds (default: 30)",
|
|
196
|
+
description: "Timeout in seconds (default: 30). Ignored if progress streaming is used.",
|
|
197
197
|
default: 30
|
|
198
198
|
}
|
|
199
199
|
},
|
|
200
200
|
required: ["command"]
|
|
201
201
|
}
|
|
202
202
|
};
|
|
203
|
-
async function handleAcpRemoteExecuteCommand(args, sshConnection) {
|
|
203
|
+
async function handleAcpRemoteExecuteCommand(args, sshConnection, extra, server) {
|
|
204
204
|
const { command, cwd, timeout = 30 } = args;
|
|
205
|
-
|
|
205
|
+
const progressToken = extra?._meta?.progressToken;
|
|
206
|
+
logger.debug("Executing remote command", { command, cwd, timeout, hasProgressToken: !!progressToken });
|
|
206
207
|
try {
|
|
208
|
+
if (progressToken && server) {
|
|
209
|
+
return await executeWithProgress(command, cwd, sshConnection, progressToken, server);
|
|
210
|
+
}
|
|
207
211
|
const fullCommand = cwd ? `cd ${cwd} && ${command}` : command;
|
|
208
212
|
const result = await sshConnection.execWithTimeout(fullCommand, timeout);
|
|
209
213
|
logger.debug("Command execution result", {
|
|
@@ -244,6 +248,75 @@ async function handleAcpRemoteExecuteCommand(args, sshConnection) {
|
|
|
244
248
|
};
|
|
245
249
|
}
|
|
246
250
|
}
|
|
251
|
+
async function executeWithProgress(command, cwd, sshConnection, progressToken, server) {
|
|
252
|
+
logger.debug("Starting streaming execution", { command, cwd, progressToken });
|
|
253
|
+
const { stream, stderr: stderrStream, exitCode } = await sshConnection.execStream(command, cwd);
|
|
254
|
+
let stdout = "";
|
|
255
|
+
let stderr = "";
|
|
256
|
+
let bytesReceived = 0;
|
|
257
|
+
let lastProgressTime = 0;
|
|
258
|
+
const MIN_PROGRESS_INTERVAL = 100;
|
|
259
|
+
stream.on("data", (chunk) => {
|
|
260
|
+
const text = chunk.toString();
|
|
261
|
+
stdout += text;
|
|
262
|
+
bytesReceived += chunk.length;
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
if (now - lastProgressTime >= MIN_PROGRESS_INTERVAL) {
|
|
265
|
+
try {
|
|
266
|
+
server.notification({
|
|
267
|
+
method: "notifications/progress",
|
|
268
|
+
params: {
|
|
269
|
+
progressToken,
|
|
270
|
+
progress: bytesReceived,
|
|
271
|
+
total: void 0,
|
|
272
|
+
// Unknown total for streaming
|
|
273
|
+
message: text
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
lastProgressTime = now;
|
|
277
|
+
logger.debug("Progress notification sent", {
|
|
278
|
+
progressToken,
|
|
279
|
+
bytes: bytesReceived,
|
|
280
|
+
chunkSize: chunk.length
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
logger.warn("Failed to send progress notification", {
|
|
284
|
+
error: error instanceof Error ? error.message : String(error)
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
stderrStream.on("data", (chunk) => {
|
|
290
|
+
stderr += chunk.toString();
|
|
291
|
+
});
|
|
292
|
+
stream.on("error", (error) => {
|
|
293
|
+
logger.error("Stream error during execution", {
|
|
294
|
+
command,
|
|
295
|
+
error: error.message
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
const finalExitCode = await exitCode;
|
|
299
|
+
logger.debug("Streaming execution completed", {
|
|
300
|
+
command,
|
|
301
|
+
exitCode: finalExitCode,
|
|
302
|
+
stdoutBytes: stdout.length,
|
|
303
|
+
stderrBytes: stderr.length
|
|
304
|
+
});
|
|
305
|
+
const output = {
|
|
306
|
+
stdout,
|
|
307
|
+
stderr,
|
|
308
|
+
exitCode: finalExitCode,
|
|
309
|
+
timedOut: false,
|
|
310
|
+
streamed: true
|
|
311
|
+
// Indicate this was streamed
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
content: [{
|
|
315
|
+
type: "text",
|
|
316
|
+
text: JSON.stringify(output, null, 2)
|
|
317
|
+
}]
|
|
318
|
+
};
|
|
319
|
+
}
|
|
247
320
|
|
|
248
321
|
// src/tools/acp-remote-read-file.ts
|
|
249
322
|
var acpRemoteReadFileTool = {
|
|
@@ -559,6 +632,57 @@ var SSHConnectionManager = class {
|
|
|
559
632
|
throw error;
|
|
560
633
|
}
|
|
561
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Execute a command on the remote server with streaming output
|
|
637
|
+
* Returns streams instead of buffered output for real-time progress
|
|
638
|
+
*
|
|
639
|
+
* @param command - Shell command to execute
|
|
640
|
+
* @param cwd - Optional working directory
|
|
641
|
+
* @returns Object with stdout stream, stderr stream, and exit code promise
|
|
642
|
+
*/
|
|
643
|
+
async execStream(command, cwd) {
|
|
644
|
+
if (!this.connected) {
|
|
645
|
+
await this.connect();
|
|
646
|
+
}
|
|
647
|
+
const fullCommand = cwd ? `cd "${cwd}" && ${command}` : command;
|
|
648
|
+
const startTime = Date.now();
|
|
649
|
+
logger.sshCommand(fullCommand, cwd);
|
|
650
|
+
return new Promise((resolve, reject) => {
|
|
651
|
+
this.client.exec(fullCommand, (err, stream) => {
|
|
652
|
+
if (err) {
|
|
653
|
+
logger.error("SSH exec failed", {
|
|
654
|
+
command: fullCommand,
|
|
655
|
+
error: err.message
|
|
656
|
+
});
|
|
657
|
+
reject(err);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
logger.debug("SSH stream started", { command: fullCommand });
|
|
661
|
+
const exitCodePromise = new Promise((resolveExit) => {
|
|
662
|
+
stream.on("close", (code) => {
|
|
663
|
+
const duration = Date.now() - startTime;
|
|
664
|
+
logger.debug("SSH stream closed", {
|
|
665
|
+
command: fullCommand,
|
|
666
|
+
exitCode: code,
|
|
667
|
+
duration: `${duration}ms`
|
|
668
|
+
});
|
|
669
|
+
resolveExit(code);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
stream.on("error", (error) => {
|
|
673
|
+
logger.error("SSH stream error", {
|
|
674
|
+
command: fullCommand,
|
|
675
|
+
error: error.message
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
resolve({
|
|
679
|
+
stream,
|
|
680
|
+
stderr: stream.stderr,
|
|
681
|
+
exitCode: exitCodePromise
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
}
|
|
562
686
|
/**
|
|
563
687
|
* Get SFTP wrapper for file operations
|
|
564
688
|
*/
|
|
@@ -845,7 +969,7 @@ async function createServer(serverConfig) {
|
|
|
845
969
|
logger.debug(`Returning ${tools.length} tools`, { tools: tools.map((t) => t.name), userId: serverConfig.userId });
|
|
846
970
|
return { tools };
|
|
847
971
|
});
|
|
848
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
972
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
849
973
|
const startTime = Date.now();
|
|
850
974
|
logger.toolInvoked(request.params.name, request.params.arguments, serverConfig.userId);
|
|
851
975
|
try {
|
|
@@ -853,7 +977,7 @@ async function createServer(serverConfig) {
|
|
|
853
977
|
if (request.params.name === "acp_remote_list_files") {
|
|
854
978
|
result = await handleAcpRemoteListFiles(request.params.arguments, sshConnection);
|
|
855
979
|
} else if (request.params.name === "acp_remote_execute_command") {
|
|
856
|
-
result = await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection);
|
|
980
|
+
result = await handleAcpRemoteExecuteCommand(request.params.arguments, sshConnection, extra, server);
|
|
857
981
|
} else if (request.params.name === "acp_remote_read_file") {
|
|
858
982
|
result = await handleAcpRemoteReadFile(request.params.arguments, sshConnection);
|
|
859
983
|
} else if (request.params.name === "acp_remote_write_file") {
|