@harmonyos-arkts/opencode-acp 0.0.1 → 0.0.3
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 +91 -0
- package/README.md +100 -38
- package/README.zh.md +41 -39
- package/dist/index.cjs +1420 -215
- package/dist/index.cjs.map +4 -4
- package/docs/codebase-overview.md +191 -0
- package/docs/mcp-extmethod-design.md +366 -0
- package/docs/provider-config-flow.md +312 -0
- package/docs/question-asked.md +35 -18
- package/docs/session-stats-to-vscode-design.md +217 -0
- package/docs/subagent-visibility.md +26 -25
- package/docs/tui-vs-acp-analysis.md +36 -36
- package/package.json +10 -4
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Harmony-ACP 代码详解
|
|
2
|
+
|
|
3
|
+
## 各文件详细介绍
|
|
4
|
+
|
|
5
|
+
### `src/index.ts` — 入口 (114 行)
|
|
6
|
+
|
|
7
|
+
程序入口,负责三件事:
|
|
8
|
+
|
|
9
|
+
1. **CLI 参数解析** (`parseArgs`):支持 `--server`、`--cwd`、`--log`、`--help`
|
|
10
|
+
2. **服务器连接**:通过 `@opencode-ai/sdk` 创建客户端,调用 `health()` 验证 OpenCode 服务可达
|
|
11
|
+
3. **ACP 连接建立**:将 stdin/stdout 包装为 `ReadableStream`/`WritableStream`,通过 `ndJsonStream` 创建 NDJSON 双向流,然后实例化 `AgentSideConnection` 启动 ACP 协议
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### `src/agent.ts` — ACP Agent (934 行,最大文件)
|
|
16
|
+
|
|
17
|
+
实现 ACP 协议的 `Agent` 接口,是整个代理的核心业务逻辑。主要职责:
|
|
18
|
+
|
|
19
|
+
#### 协议生命周期
|
|
20
|
+
|
|
21
|
+
- `initialize()` — 声明能力(loadSession、MCP、fork、resume 等)
|
|
22
|
+
- `authenticate()` — 占位,未实现
|
|
23
|
+
|
|
24
|
+
#### 会话管理
|
|
25
|
+
|
|
26
|
+
- `newSession()` — 在 OpenCode 创建会话,加载模型和模式
|
|
27
|
+
- `loadSession()` — 加载已有会话,**回放历史消息**给客户端
|
|
28
|
+
- `listSessions()` — 分页列出会话
|
|
29
|
+
- `unstable_forkSession()` / `unstable_resumeSession()` — 分叉和恢复会话
|
|
30
|
+
|
|
31
|
+
#### 提示执行
|
|
32
|
+
|
|
33
|
+
- `prompt()` — 将客户端 prompt 转换为 OpenCode SDK 调用,支持普通 prompt 和 `/command` 格式
|
|
34
|
+
- `cancel()` — 中止正在进行的会话
|
|
35
|
+
|
|
36
|
+
#### 配置
|
|
37
|
+
|
|
38
|
+
- `setSessionMode()` / `unstable_setSessionModel()` / `setSessionConfigOption()` — 运行时切换模型和模式
|
|
39
|
+
|
|
40
|
+
#### 内部辅助方法
|
|
41
|
+
|
|
42
|
+
| 方法 | 职责 |
|
|
43
|
+
| ---------------------- | --------------------------------------------------------------------------------- |
|
|
44
|
+
| `defaultModel()` | 多级模型发现:配置 → opencode config → providers → fallback `opencode/big-pickle` |
|
|
45
|
+
| `loadSessionMode()` | 加载可用模型列表,注册 MCP 服务器,推送 `available_commands_update` |
|
|
46
|
+
| `processMessage()` | 消息回放(文本、工具调用、文件、推理等) |
|
|
47
|
+
| `replayToolPart()` | 工具调用回放(completed/error 状态) |
|
|
48
|
+
| `convertPromptParts()` | ACP prompt 格式 → OpenCode parts 格式 |
|
|
49
|
+
| `parseUri()` | 解析 `file://` 和 `zed://` URI |
|
|
50
|
+
| `logMessageContent()` | prompt 完成后抓取完整消息内容记录日志 |
|
|
51
|
+
| `buildUsage()` | 从 `AssistantMessage.tokens` 构建 ACP Usage 对象 |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### `src/session-manager.ts` — 会话管理器 (182 行)
|
|
56
|
+
|
|
57
|
+
增强版会话跟踪,核心区别在于**自动发现子会话**。
|
|
58
|
+
|
|
59
|
+
#### 数据结构
|
|
60
|
+
|
|
61
|
+
- `sessions: Map<string, SessionState>` — 所有会话(含子会话)
|
|
62
|
+
- `children: Map<string, Set<string>>` — 父→子的索引
|
|
63
|
+
|
|
64
|
+
#### 关键方法
|
|
65
|
+
|
|
66
|
+
| 方法 | 职责 |
|
|
67
|
+
| ----------------------- | ---------------------------------------------------------------------------- |
|
|
68
|
+
| `create()` / `load()` | 注册顶层会话(由 agent 调用) |
|
|
69
|
+
| `registerDiscovered()` | **核心增强**:通过 SSE 事件自动注册子会话,继承父会话的 cwd/mcpServers/model |
|
|
70
|
+
| `findRootSession()` | 向上遍历 parentID 链找到根会话 |
|
|
71
|
+
| `getRelatedSessions()` | 递归获取子树所有会话 |
|
|
72
|
+
| `getChildren()` | 获取直接子会话 ID 列表 |
|
|
73
|
+
| `getModel/setModel` | 会话模型状态管理 |
|
|
74
|
+
| `getVariant/setVariant` | 会话变体管理 |
|
|
75
|
+
| `getMode/setMode` | 会话模式管理 |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### `src/event-handler.ts` — SSE 事件处理 (618 行)
|
|
80
|
+
|
|
81
|
+
订阅 OpenCode SSE 事件流,路由到 ACP 客户端。实现"独立虚拟会话"策略。
|
|
82
|
+
|
|
83
|
+
#### 核心状态
|
|
84
|
+
|
|
85
|
+
| 字段 | 用途 |
|
|
86
|
+
| ------------------ | --------------------------------------------- |
|
|
87
|
+
| `messageBuffers` | 累积 text/thought chunk,边界时刷新为完整日志 |
|
|
88
|
+
| `bashSnapshots` | bash 输出去重(用简单哈希避免重复推送) |
|
|
89
|
+
| `toolStarts` | 跟踪已发送 `tool_call` 开始事件的 callID |
|
|
90
|
+
| `permissionQueues` | 按 sessionID 串行化权限请求 |
|
|
91
|
+
| `questionQueues` | 按 sessionID 串行化问题请求 |
|
|
92
|
+
|
|
93
|
+
#### 事件处理
|
|
94
|
+
|
|
95
|
+
| 事件类型 | 处理逻辑 |
|
|
96
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
97
|
+
| `session.created` / `session.updated` | 自动发现子会话,通过 `announceChildSession()` 向客户端广播虚拟会话(带 `_meta.parentSessionId`) |
|
|
98
|
+
| `permission.asked` | 转发给 ACP 客户端请求权限,支持 edit 的 diff 应用(用 `diff` 库的 `applyPatch`) |
|
|
99
|
+
| `question.asked` | 通过 `extMethod("questionAsked")` 转发给客户端,60 秒超时 |
|
|
100
|
+
| `message.part.delta` | 实时推送 `agent_message_chunk` / `agent_thought_chunk`,同时累积到 buffer |
|
|
101
|
+
| `message.part.updated` | 处理工具调用状态转换:pending → running → completed/error |
|
|
102
|
+
|
|
103
|
+
#### 消息缓冲机制
|
|
104
|
+
|
|
105
|
+
- `accumulateDelta()` — 累积 delta 到 buffer
|
|
106
|
+
- `flushMessageBuffer()` — 在边界事件(tool_call、新消息、stop)时刷新为 `agent_message_complete` 日志
|
|
107
|
+
- `flushAllBuffers()` — 停止时刷新所有剩余 buffer
|
|
108
|
+
|
|
109
|
+
#### 辅助
|
|
110
|
+
|
|
111
|
+
- `sendToClient()` — `connection.sessionUpdate()` 的包装,在发送前记录日志并刷新 buffer
|
|
112
|
+
- `resolveSession()` — 根据 sessionId 查找已注册会话或其祖先
|
|
113
|
+
- `bashOutput()` — 从工具 metadata 中提取 bash 输出
|
|
114
|
+
- `simpleHash()` — 简单字符串哈希,用于 bash 输出去重
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### `src/logger.ts` — 结构化日志 (139 行)
|
|
119
|
+
|
|
120
|
+
JSONL 格式日志系统,写入 `~/.harmony-acp/logs/`,每次启动一个文件。
|
|
121
|
+
|
|
122
|
+
#### 5 个分类 API
|
|
123
|
+
|
|
124
|
+
| 函数 | 分类 | 用途 |
|
|
125
|
+
| ----------- | ---------- | ---------------------------------------------- |
|
|
126
|
+
| `acpIn()` | `acp.in` | 客户端请求(editor → harmony-acp) |
|
|
127
|
+
| `acpOut()` | `acp.out` | 返回客户端的响应(harmony-acp → editor) |
|
|
128
|
+
| `ocCall()` | `oc.call` | 调用 OpenCode 的请求(harmony-acp → opencode) |
|
|
129
|
+
| `ocEvent()` | `oc.event` | OpenCode 的事件(opencode → harmony-acp) |
|
|
130
|
+
| `sysLog()` | `system` | 生命周期事件 |
|
|
131
|
+
|
|
132
|
+
#### 特性
|
|
133
|
+
|
|
134
|
+
- `initLogger()` — 创建日志文件,自动清理超过 30 个的旧日志
|
|
135
|
+
- `setLogEnabled()` — 默认关闭,需 `--log` 启用
|
|
136
|
+
- `sanitize()` — 常规日志截断到 2K 字符
|
|
137
|
+
- `acpAssembled()` / `sanitizeAssembled()` — 组装消息保留到 10K 字符
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### `src/types.ts` — 类型定义 (53 行)
|
|
142
|
+
|
|
143
|
+
定义核心接口:
|
|
144
|
+
|
|
145
|
+
| 接口 | 用途 |
|
|
146
|
+
| -------------- | ------------------------------------------------- |
|
|
147
|
+
| `SessionState` | 增强会话状态(含 parentID、title、model、modeId) |
|
|
148
|
+
| `ACPConfig` | 服务器配置(sdk、serverUrl、cwd、defaultModel) |
|
|
149
|
+
| `ModeOption` | 会话模式选项 |
|
|
150
|
+
| `ModelOption` | 模型选项 |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### `src/utils.ts` — 工具函数 (53 行)
|
|
155
|
+
|
|
156
|
+
两个纯函数:
|
|
157
|
+
|
|
158
|
+
| 函数 | 用途 |
|
|
159
|
+
| --------------- | ----------------------------------------------------------------------------------------- |
|
|
160
|
+
| `toToolKind()` | OpenCode 工具名 → ACP ToolKind 映射(bash→execute, edit/write→edit, grep/glob→search 等) |
|
|
161
|
+
| `toLocations()` | 从工具输入元数据中提取文件路径位置信息 |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 数据流总览
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
编辑器 Harmony-ACP OpenCode Server
|
|
169
|
+
│ │ │
|
|
170
|
+
│── JSON-RPC (stdio) ────→ │ │
|
|
171
|
+
│ initialize/newSession │ ── HTTP POST (session.create) ────→ │
|
|
172
|
+
│ │ ←── JSON response ───────────────── │
|
|
173
|
+
│ │ │
|
|
174
|
+
│ prompt │ ── HTTP POST (session.prompt) ────→ │
|
|
175
|
+
│ │ │
|
|
176
|
+
│ │ ←── SSE stream (events) ─────────── │
|
|
177
|
+
│ │ message.part.delta │
|
|
178
|
+
│ │ message.part.updated │
|
|
179
|
+
│ │ permission.asked │
|
|
180
|
+
│ │ question.asked │
|
|
181
|
+
│ │ session.created (子会话发现) │
|
|
182
|
+
│ │ │
|
|
183
|
+
│←── sessionUpdate ─────── │ │
|
|
184
|
+
│ agent_message_chunk │ │
|
|
185
|
+
│ tool_call / update │ │
|
|
186
|
+
│ session_info_update │ │
|
|
187
|
+
│ usage_update │ │
|
|
188
|
+
│ │ │
|
|
189
|
+
│── requestPermission ──→ │ ── HTTP POST (permission.reply) ──→ │
|
|
190
|
+
│── extMethod(question) → │ ── HTTP POST (question.reply) ────→ │
|
|
191
|
+
```
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# MCP API 暴露方案:通过 ACP extMethod
|
|
2
|
+
|
|
3
|
+
## 背景
|
|
4
|
+
|
|
5
|
+
当前 harmony-acp 在 session 创建时通过 `sdk.mcp.add()` 注册 MCP 服务器,但 ACP client 无法在运行时动态查看/管理 MCP 服务器。需要将 OpenCode 服务端的 MCP API 通过 ACP 协议的 extMethod 机制暴露给 ACP client,使其可以动态查看状态、添加/删除/连接/断开 MCP 服务器。
|
|
6
|
+
|
|
7
|
+
## 方案:ACP extMethod
|
|
8
|
+
|
|
9
|
+
利用 ACP 协议已有的 `extMethod` 机制(当前已用于 `questionAsked`),定义 MCP 相关的 extension method。ACP client 通过 JSON-RPC 调用,harmony-acp 代理到 OpenCode SDK。
|
|
10
|
+
|
|
11
|
+
### 架构
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
ACP Client (Editor) ← stdio/JSON-RPC → Harmony-ACP ← HTTP/SSE → OpenCode Server
|
|
15
|
+
│ │ │
|
|
16
|
+
│ extMethod("mcp/status") │ │
|
|
17
|
+
│──────────────────────────────────>│ │
|
|
18
|
+
│ │ sdk.mcp.status() │
|
|
19
|
+
│ │─────────────────────────>│
|
|
20
|
+
│ │ Record<string, Status> │
|
|
21
|
+
│ │<─────────────────────────│
|
|
22
|
+
│ { status: {...} } │ │
|
|
23
|
+
│<──────────────────────────────────│ │
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 暴露的 MCP 操作
|
|
29
|
+
|
|
30
|
+
| extMethod 名 | 对应 SDK 调用 | 说明 |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `mcp/status` | `sdk.mcp.status()` | 获取所有 MCP 服务器状态 |
|
|
33
|
+
| `mcp/add` | `sdk.mcp.add()` | 添加 MCP 服务器 |
|
|
34
|
+
| `mcp/connect` | `sdk.mcp.connect()` | 连接已禁用的服务器 |
|
|
35
|
+
| `mcp/disconnect` | `sdk.mcp.disconnect()` | 断开服务器 |
|
|
36
|
+
| `mcp/auth/start` | `sdk.mcp.auth.start()` | 启动 OAuth 认证 |
|
|
37
|
+
| `mcp/auth/callback` | `sdk.mcp.auth.callback()` | 完成 OAuth 认证 |
|
|
38
|
+
| `mcp/auth/remove` | `sdk.mcp.auth.remove()` | 删除 OAuth 凭据 |
|
|
39
|
+
|
|
40
|
+
### 各操作详细签名
|
|
41
|
+
|
|
42
|
+
#### `mcp/status`
|
|
43
|
+
|
|
44
|
+
获取所有已配置 MCP 服务器的连接状态。
|
|
45
|
+
|
|
46
|
+
**请求参数:**
|
|
47
|
+
```typescript
|
|
48
|
+
{
|
|
49
|
+
sessionId: string // 用于获取 cwd
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**响应:**
|
|
54
|
+
```typescript
|
|
55
|
+
{
|
|
56
|
+
[serverName: string]: {
|
|
57
|
+
status: "connected" | "disabled" | "failed" | "needs_auth" | "needs_client_registration"
|
|
58
|
+
error?: string // 仅 failed 和 needs_client_registration 状态
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### `mcp/add`
|
|
64
|
+
|
|
65
|
+
动态添加 MCP 服务器。
|
|
66
|
+
|
|
67
|
+
**请求参数:**
|
|
68
|
+
```typescript
|
|
69
|
+
{
|
|
70
|
+
sessionId: string
|
|
71
|
+
name: string
|
|
72
|
+
config: {
|
|
73
|
+
// Local 类型
|
|
74
|
+
type: "local"
|
|
75
|
+
command: string[]
|
|
76
|
+
environment?: Record<string, string>
|
|
77
|
+
enabled?: boolean
|
|
78
|
+
timeout?: number
|
|
79
|
+
} | {
|
|
80
|
+
// Remote 类型
|
|
81
|
+
type: "remote"
|
|
82
|
+
url: string
|
|
83
|
+
headers?: Record<string, string>
|
|
84
|
+
enabled?: boolean
|
|
85
|
+
oauth?: { clientId?: string, clientSecret?: string, scope?: string, redirectUri?: string } | false
|
|
86
|
+
timeout?: number
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**响应:**
|
|
92
|
+
```typescript
|
|
93
|
+
{
|
|
94
|
+
[serverName: string]: McpStatus // 添加后所有服务器状态
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### `mcp/connect`
|
|
99
|
+
|
|
100
|
+
连接一个已禁用/断开的 MCP 服务器。
|
|
101
|
+
|
|
102
|
+
**请求参数:**
|
|
103
|
+
```typescript
|
|
104
|
+
{
|
|
105
|
+
sessionId: string
|
|
106
|
+
name: string
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**响应:**
|
|
111
|
+
```typescript
|
|
112
|
+
{
|
|
113
|
+
success: true
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### `mcp/disconnect`
|
|
118
|
+
|
|
119
|
+
断开一个 MCP 服务器。
|
|
120
|
+
|
|
121
|
+
**请求参数:**
|
|
122
|
+
```typescript
|
|
123
|
+
{
|
|
124
|
+
sessionId: string
|
|
125
|
+
name: string
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**响应:**
|
|
130
|
+
```typescript
|
|
131
|
+
{
|
|
132
|
+
success: true
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### `mcp/auth/start`
|
|
137
|
+
|
|
138
|
+
启动 OAuth 认证流程,返回授权 URL。
|
|
139
|
+
|
|
140
|
+
**请求参数:**
|
|
141
|
+
```typescript
|
|
142
|
+
{
|
|
143
|
+
sessionId: string
|
|
144
|
+
name: string
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**响应:**
|
|
149
|
+
```typescript
|
|
150
|
+
{
|
|
151
|
+
authorizationUrl: string
|
|
152
|
+
oauthState: string
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**错误:** 如果服务器不支持 OAuth,返回错误。
|
|
157
|
+
|
|
158
|
+
#### `mcp/auth/callback`
|
|
159
|
+
|
|
160
|
+
用授权码完成 OAuth 认证。
|
|
161
|
+
|
|
162
|
+
**请求参数:**
|
|
163
|
+
```typescript
|
|
164
|
+
{
|
|
165
|
+
sessionId: string
|
|
166
|
+
name: string
|
|
167
|
+
code: string // OAuth 授权码
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**响应:**
|
|
172
|
+
```typescript
|
|
173
|
+
McpStatus // 认证后的服务器状态
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### `mcp/auth/remove`
|
|
177
|
+
|
|
178
|
+
删除 MCP 服务器的 OAuth 凭据。
|
|
179
|
+
|
|
180
|
+
**请求参数:**
|
|
181
|
+
```typescript
|
|
182
|
+
{
|
|
183
|
+
sessionId: string
|
|
184
|
+
name: string
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**响应:**
|
|
189
|
+
```typescript
|
|
190
|
+
{
|
|
191
|
+
success: true
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 实现步骤
|
|
198
|
+
|
|
199
|
+
### Step 1: 新建 `src/mcp-manager.ts` — MCP 操作封装
|
|
200
|
+
|
|
201
|
+
创建 MCP 管理模块,封装所有 MCP 相关的 SDK 调用,统一处理错误和日志。
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// src/mcp-manager.ts
|
|
205
|
+
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
|
206
|
+
import { ocCall } from "./logger.js"
|
|
207
|
+
|
|
208
|
+
export class McpManager {
|
|
209
|
+
constructor(private sdk: OpencodeClient) {}
|
|
210
|
+
|
|
211
|
+
async status(directory: string) {
|
|
212
|
+
ocCall("mcp.status", { directory })
|
|
213
|
+
const result = await this.sdk.mcp.status({ directory })
|
|
214
|
+
return result.data ?? {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async add(directory: string, name: string, config: any) {
|
|
218
|
+
ocCall("mcp.add", { directory, name })
|
|
219
|
+
const result = await this.sdk.mcp.add({ directory, name, config })
|
|
220
|
+
return result.data ?? {}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async connect(directory: string, name: string) {
|
|
224
|
+
ocCall("mcp.connect", { directory, name })
|
|
225
|
+
await this.sdk.mcp.connect({ directory, name })
|
|
226
|
+
return { success: true }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async disconnect(directory: string, name: string) {
|
|
230
|
+
ocCall("mcp.disconnect", { directory, name })
|
|
231
|
+
await this.sdk.mcp.disconnect({ directory, name })
|
|
232
|
+
return { success: true }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async startAuth(directory: string, name: string) {
|
|
236
|
+
ocCall("mcp.auth.start", { directory, name })
|
|
237
|
+
const result = await this.sdk.mcp.auth.start({ directory, name })
|
|
238
|
+
return result.data
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async callbackAuth(directory: string, name: string, code: string) {
|
|
242
|
+
ocCall("mcp.auth.callback", { directory, name })
|
|
243
|
+
const result = await this.sdk.mcp.auth.callback({ directory, name, code })
|
|
244
|
+
return result.data
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async removeAuth(directory: string, name: string) {
|
|
248
|
+
ocCall("mcp.auth.remove", { directory, name })
|
|
249
|
+
await this.sdk.mcp.auth.remove({ directory, name })
|
|
250
|
+
return { success: true }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Step 2: 修改 `src/agent.ts` — 注册 extMethod 处理器
|
|
256
|
+
|
|
257
|
+
在 Agent 类中添加 `extMethod` 方法(ACP SDK 的 Agent 接口支持可选的 `extMethod?` 回调),路由 MCP 方法到 `McpManager`。
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// agent.ts 中添加
|
|
261
|
+
|
|
262
|
+
import { McpManager } from "./mcp-manager.js"
|
|
263
|
+
|
|
264
|
+
export class Agent implements ACPAgent {
|
|
265
|
+
private mcpManager: McpManager
|
|
266
|
+
|
|
267
|
+
constructor(config: ACPConfig) {
|
|
268
|
+
// ...
|
|
269
|
+
this.mcpManager = new McpManager(config.sdk)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async extMethod(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
273
|
+
if (method.startsWith("mcp/")) {
|
|
274
|
+
return this.handleMcpMethod(method, params)
|
|
275
|
+
}
|
|
276
|
+
throw new Error(`Unknown extMethod: ${method}`)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async handleMcpMethod(method: string, params: Record<string, unknown>) {
|
|
280
|
+
const sessionId = params.sessionId as string
|
|
281
|
+
const session = this.sessionManager.get(sessionId)
|
|
282
|
+
const directory = session.cwd
|
|
283
|
+
|
|
284
|
+
switch (method) {
|
|
285
|
+
case "mcp/status":
|
|
286
|
+
return this.mcpManager.status(directory) as Promise<Record<string, unknown>>
|
|
287
|
+
case "mcp/add":
|
|
288
|
+
return this.mcpManager.add(directory, params.name as string, params.config) as Promise<Record<string, unknown>>
|
|
289
|
+
case "mcp/connect":
|
|
290
|
+
return this.mcpManager.connect(directory, params.name as string)
|
|
291
|
+
case "mcp/disconnect":
|
|
292
|
+
return this.mcpManager.disconnect(directory, params.name as string)
|
|
293
|
+
case "mcp/auth/start":
|
|
294
|
+
return this.mcpManager.startAuth(directory, params.name as string) as Promise<Record<string, unknown>>
|
|
295
|
+
case "mcp/auth/callback":
|
|
296
|
+
return this.mcpManager.callbackAuth(directory, params.name as string, params.code as string) as Promise<Record<string, unknown>>
|
|
297
|
+
case "mcp/auth/remove":
|
|
298
|
+
return this.mcpManager.removeAuth(directory, params.name as string)
|
|
299
|
+
default:
|
|
300
|
+
throw new Error(`Unknown MCP method: ${method}`)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Step 3: 迁移现有 MCP 注册逻辑
|
|
307
|
+
|
|
308
|
+
将 `agent.ts:loadSessionMode()` 中 727-757 行的 MCP 注册代码使用 `McpManager.add()` 替代,消除重复代码。
|
|
309
|
+
|
|
310
|
+
### Step 4: 更新 `src/__tests__/helpers/mock-sdk.ts`
|
|
311
|
+
|
|
312
|
+
在 mock SDK 中补全 MCP 相关方法:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
mcp: {
|
|
316
|
+
add: vi.fn().mockResolvedValue({}),
|
|
317
|
+
status: vi.fn().mockResolvedValue({ data: {} }),
|
|
318
|
+
connect: vi.fn().mockResolvedValue({ data: true }),
|
|
319
|
+
disconnect: vi.fn().mockResolvedValue({ data: true }),
|
|
320
|
+
auth: {
|
|
321
|
+
start: vi.fn().mockResolvedValue({ data: { authorizationUrl: "", oauthState: "" } }),
|
|
322
|
+
callback: vi.fn().mockResolvedValue({ data: { status: "connected" } }),
|
|
323
|
+
remove: vi.fn().mockResolvedValue({ data: { success: true } }),
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Step 5: 新建 `src/__tests__/mcp-manager.test.ts`
|
|
329
|
+
|
|
330
|
+
为 `McpManager` 编写单元测试,覆盖所有 MCP 操作。
|
|
331
|
+
|
|
332
|
+
### Step 6: 更新 `src/__tests__/agent.test.ts`
|
|
333
|
+
|
|
334
|
+
添加 extMethod MCP 路由的测试用例,验证方法分发和参数传递。
|
|
335
|
+
|
|
336
|
+
### Step 7: 更新文档
|
|
337
|
+
|
|
338
|
+
- 更新 `README.md` 说明 MCP extMethod 接口
|
|
339
|
+
- 更新 `CHANGELOG.md`
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## 关键文件清单
|
|
344
|
+
|
|
345
|
+
| 文件 | 操作 | 说明 |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| `src/mcp-manager.ts` | **新建** | MCP 操作封装,代理 SDK 调用 |
|
|
348
|
+
| `src/agent.ts` | **修改** | 添加 extMethod 路由、集成 McpManager、迁移 MCP 注册逻辑 |
|
|
349
|
+
| `src/types.ts` | **修改** | 添加 MCP 相关类型(可选) |
|
|
350
|
+
| `src/__tests__/mcp-manager.test.ts` | **新建** | McpManager 单元测试 |
|
|
351
|
+
| `src/__tests__/agent.test.ts` | **修改** | 添加 extMethod 测试 |
|
|
352
|
+
| `src/__tests__/helpers/mock-sdk.ts` | **修改** | 补全 MCP mock 方法 |
|
|
353
|
+
| `README.md` | **修改** | 文档更新 |
|
|
354
|
+
| `CHANGELOG.md` | **修改** | 记录变更 |
|
|
355
|
+
|
|
356
|
+
## 复用
|
|
357
|
+
|
|
358
|
+
- `@opencode-ai/sdk` 的 `Mcp` 类(`sdk.mcp.*`):所有 MCP 操作直接代理
|
|
359
|
+
- ACP SDK 的 `extMethod` 机制:已有协议支持,无需修改
|
|
360
|
+
- `SessionManager`:复用 `get()` 获取 session 的 `cwd` 用于 SDK 调用
|
|
361
|
+
- 现有日志工具:`ocCall` 记录 OpenCode 调用
|
|
362
|
+
|
|
363
|
+
## 验证方式
|
|
364
|
+
|
|
365
|
+
1. `cd /Users/liwenwei/Documents/Code/harmony-acp && pnpm test` — 所有测试通过
|
|
366
|
+
2. 手动测试:启动 opencode server → 启动 harmony-acp → 通过 ACP client 发送 extMethod 调用验证 MCP 操作
|