@actiondock/mcp 2.9.1 → 2.11.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/README.md +18 -21
- package/dist/adapter.d.ts +3 -8
- package/dist/adapter.js +11 -301
- package/dist/execution-mode.d.ts +12 -31
- package/dist/execution-mode.js +9 -74
- package/dist/http.js +2 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/dist/schemas.d.ts +1 -4
- package/dist/schemas.js +1 -16
- package/dist/service-resolver.d.ts +9 -0
- package/dist/service-resolver.js +142 -0
- package/dist/stdio.js +0 -2
- package/dist/tool-mapper.d.ts +33 -0
- package/dist/tool-mapper.js +129 -0
- package/dist/types.d.ts +0 -2
- package/package.json +3 -3
- package/dist/register-tasks-extension.d.ts +0 -34
- package/dist/register-tasks-extension.js +0 -171
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ ActionDock 2.x 模型上下文协议适配器。
|
|
|
7
7
|
[](https://modelcontextprotocol.io/)
|
|
8
8
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
9
9
|
|
|
10
|
-
`@actiondock/mcp` 将 ActionDock 中的原子 Action
|
|
10
|
+
`@actiondock/mcp` 将 ActionDock 中的原子 Action 映射为标准的模型上下文协议工具,无缝接入各类主流智能体与宿主环境。MCP 适配层直接对接 `@actiondock/core` 导出的标准服务端口体系,忠实保持工具模式与业务入参契约。
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -15,15 +15,20 @@ ActionDock 2.x 模型上下文协议适配器。
|
|
|
15
15
|
|
|
16
16
|
### 双协议传输通道
|
|
17
17
|
|
|
18
|
-
- STDIO
|
|
18
|
+
- STDIO 标准输入输出通道:专为本地客户端设计,直接通过子进程标准流双向通信,具备受控诊断流隔离与进程退出协同机制。
|
|
19
19
|
- HTTP 传输通道:专为远程服务与容器化部署设计,基于 Web 标准请求与响应实现流式传输,支持跨域策略配置与鉴权令牌校验。
|
|
20
20
|
|
|
21
|
-
###
|
|
21
|
+
### 纯净模式映射与契约忠实
|
|
22
22
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
23
|
+
- 原始模式完全保真:工具入参和出参模式完全忠实于 Action 定义,不在模式中注入非业务执行包装字段。
|
|
24
|
+
- 业务入参纯净传递:工具入参直接透明传递至底层执行引擎,不修改或过滤业务自有的入参字段。
|
|
25
|
+
- 命名空间冲突隔离:智能处理多包聚合场景下的动作标识冲突,自动使用包命名空间前缀消除歧义。
|
|
26
|
+
- 结构化结果转换:执行结果自动封装为包含文本内容块与结构化数据的 MCP 标准信封。
|
|
27
|
+
|
|
28
|
+
### 规程映射为资源与提示词
|
|
29
|
+
|
|
30
|
+
- 只读资源映射:将 Playbook 操作规程自动注册为只读 MCP Resource,支持客户端直接读取 Markdown 规程文本。
|
|
31
|
+
- 提示词模版映射:将 Playbook 映射为 MCP Prompt,方便智能体快速装配规程指令。
|
|
27
32
|
|
|
28
33
|
### 协同取消信号向下传播
|
|
29
34
|
|
|
@@ -31,27 +36,19 @@ ActionDock 2.x 模型上下文协议适配器。
|
|
|
31
36
|
- 取消信号通过服务端口直接传递至底层的执行服务,并联动激活当前任务上下文中的 `ctx.signal`。
|
|
32
37
|
- 业务代码可通过监听 AbortSignal 安全释放资源或提前终止执行。
|
|
33
38
|
|
|
34
|
-
### Tasks 异步任务映射扩展
|
|
35
|
-
|
|
36
|
-
针对长周期、重资源消耗的复杂任务,完整适配 MCP Tasks 异步协议扩展:
|
|
37
|
-
|
|
38
|
-
- 任务状态查询:通过 `tasks/get` 端点根据任务标识检索当前运行状态、执行进度与部分输出快照。
|
|
39
|
-
- 任务主动取消:通过 `tasks/cancel` 端点向正在后台执行的长周期任务发出中止指令。
|
|
40
|
-
- 任务清单列举:通过 `tasks/list` 端点批量查询当前会话及宿主下的活跃与历史任务列表。
|
|
41
|
-
|
|
42
39
|
### 细粒度权限白名单与安全过滤
|
|
43
40
|
|
|
44
41
|
MCP 适配层原生支持基于包与动作维度的双层白名单控制,通过 `ActionDockMcpOptions` 进行配置:
|
|
45
42
|
|
|
46
|
-
- 包白名单 `packageAllowlist`(别名 `packageIds
|
|
43
|
+
- 包白名单 `packageAllowlist`(别名 `packageIds`):限制仅暴露指定包内的工具与规程。
|
|
47
44
|
- 动作白名单 `actionAllowlist`:限制仅暴露指定的动作,支持动作短名 `actionId` 与全限定名 `packageId/actionId`。
|
|
48
|
-
-
|
|
45
|
+
- 联动过滤机制:同时配置包白名单与动作白名单时自动计算权限交集,未获授权的动作不会出现在 `tools/list` 工具列表中。
|
|
49
46
|
|
|
50
47
|
---
|
|
51
48
|
|
|
52
49
|
## 快速使用
|
|
53
50
|
|
|
54
|
-
|
|
51
|
+
通过命令行启动 MCP 服务:
|
|
55
52
|
|
|
56
53
|
```bash
|
|
57
54
|
# 以 STDIO 协议启动
|
|
@@ -61,12 +58,12 @@ ad mcp
|
|
|
61
58
|
ad mcp serve --port 8080
|
|
62
59
|
```
|
|
63
60
|
|
|
64
|
-
|
|
61
|
+
在代码中通过编程方式创建适配器服务:
|
|
65
62
|
|
|
66
63
|
```ts
|
|
67
|
-
import {
|
|
64
|
+
import { startMcpStdio } from "@actiondock/mcp";
|
|
68
65
|
|
|
69
|
-
await
|
|
66
|
+
await startMcpStdio({
|
|
70
67
|
projectRoot: process.cwd(),
|
|
71
68
|
packageAllowlist: ["system-tools"],
|
|
72
69
|
actionAllowlist: ["system-tools/status", "ping"],
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type ActionDockService } from "@actiondock/core";
|
|
2
|
-
import {
|
|
2
|
+
import type { ActionDockHost } from "@actiondock/core/server";
|
|
3
3
|
import type { PackageRuntime } from "@actiondock/core/package";
|
|
4
4
|
import type { ExecutionResult } from "@actiondock/sdk";
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
6
6
|
import type { ActionDockMcpOptions } from "./types.js";
|
|
7
|
+
import { resolveService } from "./service-resolver.js";
|
|
8
|
+
export { resolveService };
|
|
7
9
|
/**
|
|
8
10
|
* 判断目标值是否为普通对象(Plain Object)。
|
|
9
11
|
*
|
|
@@ -30,13 +32,6 @@ export declare function toMcpResult(result: ExecutionResult): {
|
|
|
30
32
|
}[];
|
|
31
33
|
structuredContent?: undefined;
|
|
32
34
|
};
|
|
33
|
-
/**
|
|
34
|
-
* 解析或基于选项创建底层 ActionDockService 统一门面。
|
|
35
|
-
*/
|
|
36
|
-
export declare function resolveService(options: ActionDockMcpOptions): Promise<{
|
|
37
|
-
service: ActionDockService;
|
|
38
|
-
ownsService: boolean;
|
|
39
|
-
}>;
|
|
40
35
|
export type ActionDockMcpServer = McpServer & {
|
|
41
36
|
close: () => Promise<void>;
|
|
42
37
|
service: ActionDockService;
|
package/dist/adapter.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { ACTIONDOCK_VERSION, createActionDock, createNodePlatform, findProjectRoot, MCP_TOOL_NAME_COLLISION, } from "@actiondock/core";
|
|
5
|
-
import { createActionDockHost, isActionAllowed, LocalActionDockService, } from "@actiondock/core/server";
|
|
6
|
-
import { resolvePackageRoot } from "@actiondock/core/registry";
|
|
7
|
-
import { createNonClosingStorageView } from "@actiondock/core/package";
|
|
8
|
-
import { parseActionRef } from "@actiondock/core/graph";
|
|
1
|
+
import { ACTIONDOCK_VERSION } from "@actiondock/core";
|
|
9
2
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
10
|
-
import { registerTasksExtension } from "./register-tasks-extension.js";
|
|
11
3
|
import { toMcpSchema } from "./schemas.js";
|
|
12
|
-
import {
|
|
4
|
+
import { resolveExecutionTimeout } from "./execution-mode.js";
|
|
5
|
+
import { resolveService } from "./service-resolver.js";
|
|
6
|
+
import { mapAndFilterActions } from "./tool-mapper.js";
|
|
7
|
+
export { resolveService };
|
|
13
8
|
/** 形状缺失告警是否已输出过(每个进程仅告警一次,避免逐请求噪声) */
|
|
14
9
|
let cancelSignalDegradationWarned = false;
|
|
15
10
|
/**
|
|
@@ -26,21 +21,6 @@ function extractCancelSignal(ctx) {
|
|
|
26
21
|
}
|
|
27
22
|
return signal;
|
|
28
23
|
}
|
|
29
|
-
/**
|
|
30
|
-
* 解析 Action 引用字符串为包标识与动作标识(容错语义)。
|
|
31
|
-
*
|
|
32
|
-
* 委派 core 的 parseActionRef 单一事实源完成解析;非法形态(如尾部斜杠、
|
|
33
|
-
* 含冒号等)回退为整体短名处理,与适配层既有的注册容错行为保持一致,
|
|
34
|
-
* 不让目录聚合阶段的脏数据中断服务启动。
|
|
35
|
-
*/
|
|
36
|
-
function splitActionRef(ref) {
|
|
37
|
-
try {
|
|
38
|
-
return parseActionRef(ref);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return { actionId: ref };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
24
|
/**
|
|
45
25
|
* 判断目标值是否为普通对象(Plain Object)。
|
|
46
26
|
*
|
|
@@ -83,155 +63,6 @@ export function toMcpResult(result) {
|
|
|
83
63
|
],
|
|
84
64
|
};
|
|
85
65
|
}
|
|
86
|
-
/**
|
|
87
|
-
* 构造携带结构化错误码的工具名冲突异常。
|
|
88
|
-
*
|
|
89
|
-
* @param toolName 冲突的工具名
|
|
90
|
-
*/
|
|
91
|
-
function toolNameCollisionError(toolName) {
|
|
92
|
-
const err = new Error(`MCP tool name collision detected for tool '${toolName}'`);
|
|
93
|
-
err.code = MCP_TOOL_NAME_COLLISION;
|
|
94
|
-
return err;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* 解析或基于选项创建底层 ActionDockService 统一门面。
|
|
98
|
-
*/
|
|
99
|
-
export async function resolveService(options) {
|
|
100
|
-
if (options.service) {
|
|
101
|
-
return { service: options.service, ownsService: false };
|
|
102
|
-
}
|
|
103
|
-
if (options.host) {
|
|
104
|
-
const service = new LocalActionDockService(options.host);
|
|
105
|
-
return { service, ownsService: false };
|
|
106
|
-
}
|
|
107
|
-
if (options.runtime) {
|
|
108
|
-
const host = await createActionDockHost({
|
|
109
|
-
autoLoadCurrentProject: false,
|
|
110
|
-
scanLinkedPackages: false,
|
|
111
|
-
});
|
|
112
|
-
host.registerRuntime(options.runtime);
|
|
113
|
-
const service = new LocalActionDockService(host);
|
|
114
|
-
return { service, ownsService: false };
|
|
115
|
-
}
|
|
116
|
-
const packages = [];
|
|
117
|
-
// 外部注入的 storage 生命周期默认由注入方管理,适配层仅委托读写不接管关闭
|
|
118
|
-
// (非接管视图复用 core 单一事实源);显式声明 ownStorageLifecycle 时透传
|
|
119
|
-
// 原始实例,随 target.close() 级联关闭
|
|
120
|
-
const appStorage = options.storage
|
|
121
|
-
? options.ownStorageLifecycle
|
|
122
|
-
? options.storage
|
|
123
|
-
: createNonClosingStorageView(options.storage)
|
|
124
|
-
: undefined;
|
|
125
|
-
// 多分支共享的包选项基底单点构造:storage 视图、家目录与配置覆写
|
|
126
|
-
// 在此收敛一次,各 push 分支仅叠加自身特有字段
|
|
127
|
-
const basePkgOpts = {
|
|
128
|
-
...(appStorage ? { storage: appStorage } : {}),
|
|
129
|
-
customHome: options.customHome,
|
|
130
|
-
configOverrides: options.configOverrides,
|
|
131
|
-
};
|
|
132
|
-
if (options.actions) {
|
|
133
|
-
packages.push({
|
|
134
|
-
...basePkgOpts,
|
|
135
|
-
projectConfig: {
|
|
136
|
-
id: options.packageId || "default",
|
|
137
|
-
name: options.packageId || "default",
|
|
138
|
-
version: ACTIONDOCK_VERSION,
|
|
139
|
-
},
|
|
140
|
-
actions: options.actions,
|
|
141
|
-
inMemory: true,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
if (options.projectRoots && options.projectRoots.length > 0) {
|
|
145
|
-
for (const root of options.projectRoots) {
|
|
146
|
-
const abs = resolve(root);
|
|
147
|
-
const detected = findProjectRoot(abs);
|
|
148
|
-
if (!detected) {
|
|
149
|
-
throw new Error(`Project root '${root}' is not a valid ActionDock package (actiondock.json not found)`);
|
|
150
|
-
}
|
|
151
|
-
packages.push({
|
|
152
|
-
...basePkgOpts,
|
|
153
|
-
packageRoot: detected,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const targetPackageIds = options.packageIds || options.packageAllowlist;
|
|
158
|
-
if (targetPackageIds && targetPackageIds.length > 0) {
|
|
159
|
-
for (const pkgId of targetPackageIds) {
|
|
160
|
-
const root = resolvePackageRoot(pkgId, undefined, options.customHome);
|
|
161
|
-
if (!root || !existsSync(root)) {
|
|
162
|
-
throw new Error(`Package '${pkgId}' not found in registry`);
|
|
163
|
-
}
|
|
164
|
-
packages.push({
|
|
165
|
-
...basePkgOpts,
|
|
166
|
-
packageRoot: root,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
let projectRoot = options.projectRoot;
|
|
171
|
-
if (options.projectRoot) {
|
|
172
|
-
const abs = resolve(options.projectRoot);
|
|
173
|
-
const detected = findProjectRoot(abs);
|
|
174
|
-
if (!detected) {
|
|
175
|
-
throw new Error(`Project root '${options.projectRoot}' is not a valid ActionDock package (actiondock.json not found)`);
|
|
176
|
-
}
|
|
177
|
-
projectRoot = detected;
|
|
178
|
-
}
|
|
179
|
-
if (!projectRoot &&
|
|
180
|
-
packages.length === 0 &&
|
|
181
|
-
!options.all &&
|
|
182
|
-
!options.packageId) {
|
|
183
|
-
const currentRoot = findProjectRoot(process.cwd());
|
|
184
|
-
if (!currentRoot) {
|
|
185
|
-
throw new Error("No ActionDock project root found. Run inside an ActionDock package or specify --dir / --package / --all.");
|
|
186
|
-
}
|
|
187
|
-
projectRoot = currentRoot;
|
|
188
|
-
}
|
|
189
|
-
if (options.packageId && !options.actions && packages.length === 0) {
|
|
190
|
-
const root = resolvePackageRoot(options.packageId, undefined, options.customHome);
|
|
191
|
-
if (!root || !existsSync(root)) {
|
|
192
|
-
throw new Error(`Package '${options.packageId}' not found in registry`);
|
|
193
|
-
}
|
|
194
|
-
packages.push({
|
|
195
|
-
...basePkgOpts,
|
|
196
|
-
packageRoot: root,
|
|
197
|
-
dataDir: options.dataDir,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
let platform = options.platform;
|
|
201
|
-
if (!platform && typeof process !== "undefined" && process.versions?.node) {
|
|
202
|
-
platform = createNodePlatform({
|
|
203
|
-
customHome: options.customHome,
|
|
204
|
-
dataDir: options.dataDir,
|
|
205
|
-
rootDir: projectRoot,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
for (const pkg of packages) {
|
|
209
|
-
if (typeof pkg === "object" && pkg !== null && !("info" in pkg)) {
|
|
210
|
-
if (platform && !pkg.platform) {
|
|
211
|
-
pkg.platform = platform;
|
|
212
|
-
}
|
|
213
|
-
if (!pkg.dataDir && options.dataDir) {
|
|
214
|
-
pkg.dataDir = options.dataDir;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
const service = await createActionDock({
|
|
219
|
-
type: "local",
|
|
220
|
-
projectRoot,
|
|
221
|
-
packages: packages.length > 0 ? packages : undefined,
|
|
222
|
-
scanLinkedPackages: Boolean(options.all),
|
|
223
|
-
hostOptions: {
|
|
224
|
-
autoLoadCurrentProject: packages.length === 0,
|
|
225
|
-
},
|
|
226
|
-
// MCP 服务进程是长驻执行宿主,声明数据目录持有者身份,
|
|
227
|
-
// 打开时收割遗留孤儿运行记录
|
|
228
|
-
recoverOrphans: true,
|
|
229
|
-
customHome: options.customHome,
|
|
230
|
-
dataDir: options.dataDir,
|
|
231
|
-
platform,
|
|
232
|
-
});
|
|
233
|
-
return { service, ownsService: true };
|
|
234
|
-
}
|
|
235
66
|
/**
|
|
236
67
|
* 创建并配置基于 ActionDockService 的 McpServer 适配层实例。
|
|
237
68
|
*/
|
|
@@ -263,148 +94,27 @@ export async function createActionDockMcpServer(options = {}) {
|
|
|
263
94
|
name: serverName,
|
|
264
95
|
version: serverVersion,
|
|
265
96
|
});
|
|
266
|
-
// 任务规范扩展注册集中隔离在独立模块,本层不再直接操作 SDK 内层实例
|
|
267
|
-
registerTasksExtension(server, service, allowedPackageIds, options.actionAllowlist);
|
|
268
97
|
// 工具注册与模式映射:tools/list 纯粹委托 service.discovery.listActions()
|
|
269
98
|
let rawActions = await service.discovery.listActions();
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const parsedRefs = new Map();
|
|
274
|
-
for (const act of rawActions) {
|
|
275
|
-
if (act.id.includes("/")) {
|
|
276
|
-
const parsed = splitActionRef(act.id);
|
|
277
|
-
parsedRefs.set(act, {
|
|
278
|
-
explicitPkgId: act.packageId || "",
|
|
279
|
-
parsedPkgId: parsed.packageId,
|
|
280
|
-
baseId: parsed.actionId,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
parsedRefs.set(act, { explicitPkgId: act.packageId || "", baseId: act.id });
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (allowedPackageIds && allowedPackageIds.length > 0) {
|
|
288
|
-
// 白名单轮:显式 packageId 优先,缺失时回退解析所得
|
|
289
|
-
rawActions = rawActions.filter((act) => {
|
|
290
|
-
const ref = parsedRefs.get(act);
|
|
291
|
-
const pkgId = ref.explicitPkgId || ref.parsedPkgId || "";
|
|
292
|
-
return pkgId ? allowedPackageIds.includes(pkgId) : false;
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
if (options.actionAllowlist && options.actionAllowlist.length > 0) {
|
|
296
|
-
rawActions = rawActions.filter((act) => {
|
|
297
|
-
const ref = parsedRefs.get(act);
|
|
298
|
-
const pkgId = ref.explicitPkgId || ref.parsedPkgId || undefined;
|
|
299
|
-
return isActionAllowed({ packageId: pkgId, actionId: ref.baseId }, options.actionAllowlist);
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
const seenActionKeys = new Map();
|
|
303
|
-
for (const act of rawActions) {
|
|
304
|
-
const ref = parsedRefs.get(act);
|
|
305
|
-
const pkgId = ref.explicitPkgId || ref.parsedPkgId || "";
|
|
306
|
-
const key = `${pkgId}:${ref.baseId}`;
|
|
307
|
-
const existing = seenActionKeys.get(key);
|
|
308
|
-
if (existing) {
|
|
309
|
-
// 若已存在的项是全限定名(含 /),而当前项是短名(不含 /),优先保留短名项
|
|
310
|
-
if (existing.id.includes("/") && !act.id.includes("/")) {
|
|
311
|
-
seenActionKeys.set(key, act);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
seenActionKeys.set(key, act);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
const actions = Array.from(seenActionKeys.values());
|
|
319
|
-
// 统计 Action 基础 ID 出现频次,用于同名冲突命名空间隔离
|
|
320
|
-
const baseCounts = new Map();
|
|
321
|
-
for (const act of actions) {
|
|
322
|
-
const baseId = parsedRefs.get(act).baseId;
|
|
323
|
-
baseCounts.set(baseId, (baseCounts.get(baseId) || 0) + 1);
|
|
324
|
-
}
|
|
325
|
-
const registeredToolNames = new Set();
|
|
326
|
-
// 多包判定(循环外一次算清):不同 packageId 去重计数大于 1,
|
|
327
|
-
// 或任一 id 含斜杠(跨包限定名形态),则工具描述需附全限定 id 锚点;
|
|
328
|
-
// 本轮与工具命名轮一致:解析所得 packageId 优先,缺失时回退显式声明
|
|
329
|
-
const distinctPackageIds = new Set(actions
|
|
330
|
-
.map((a) => {
|
|
331
|
-
const ref = parsedRefs.get(a);
|
|
332
|
-
return ref.parsedPkgId || a.packageId;
|
|
333
|
-
})
|
|
334
|
-
.filter((pkg) => Boolean(pkg)));
|
|
335
|
-
const isMultiPackage = distinctPackageIds.size > 1 || actions.some((a) => a.id.includes("/"));
|
|
336
|
-
for (const action of actions) {
|
|
337
|
-
const ref = parsedRefs.get(action);
|
|
338
|
-
const baseId = ref.baseId;
|
|
339
|
-
const packageId = ref.parsedPkgId || action.packageId;
|
|
340
|
-
const count = baseCounts.get(baseId) || 1;
|
|
341
|
-
let toolName = baseId;
|
|
342
|
-
if (count > 1 && packageId) {
|
|
343
|
-
const cleanPkgId = packageId.replace(/^@/, "").replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
344
|
-
toolName = `${cleanPkgId}_${baseId}`;
|
|
345
|
-
}
|
|
346
|
-
else if (toolName.includes("/") && packageId) {
|
|
347
|
-
const cleanPkgId = packageId.replace(/^@/, "").replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
348
|
-
toolName = `${cleanPkgId}_${baseId}`;
|
|
349
|
-
}
|
|
350
|
-
if (toolName.length > 64) {
|
|
351
|
-
const hash = createHash("sha256").update(toolName).digest("hex").slice(0, 8);
|
|
352
|
-
toolName = `${toolName.slice(0, 55)}_${hash}`;
|
|
353
|
-
}
|
|
354
|
-
if (registeredToolNames.has(toolName)) {
|
|
355
|
-
throw toolNameCollisionError(toolName);
|
|
356
|
-
}
|
|
357
|
-
registeredToolNames.add(toolName);
|
|
358
|
-
const description = isMultiPackage
|
|
359
|
-
? `[${action.id}] ${action.description || ""}`.trim()
|
|
360
|
-
: action.description;
|
|
361
|
-
// 工具执行:tools/call 委托 service.execution.run() 或 service.execution.start()
|
|
99
|
+
const mappedActions = mapAndFilterActions(rawActions, allowedPackageIds, options.actionAllowlist);
|
|
100
|
+
for (const { toolName, action, description } of mappedActions) {
|
|
101
|
+
// 工具执行:tools/call 委托 service.execution.run()
|
|
362
102
|
server.registerTool(toolName, {
|
|
363
103
|
description,
|
|
364
104
|
inputSchema: toMcpSchema(action.inputSchema),
|
|
365
|
-
// 出参 schema 不注入 execution 包装字段:实际 structuredContent 不含该字段,注入会与真实返回结构不符
|
|
366
105
|
outputSchema: action.outputSchema
|
|
367
|
-
? toMcpSchema(action.outputSchema
|
|
106
|
+
? toMcpSchema(action.outputSchema)
|
|
368
107
|
: undefined,
|
|
369
108
|
}, async (input, ctx) => {
|
|
370
|
-
// 异步执行模式只认显式约定字段 execution.mode,旧版 __async 仅作只读兼容探测
|
|
371
|
-
const isAsync = isAsyncExecutionRequested(input);
|
|
372
109
|
const signal = extractCancelSignal(ctx);
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
// 超时组合策略:客户端声明与服务端配置同时存在时取较小值(更防御),
|
|
376
|
-
// 任一方单独存在则直接生效,双方均缺省时不设置超时
|
|
377
|
-
const clientTimeoutMs = extractExecutionTimeoutMs(input);
|
|
378
|
-
const effectiveTimeoutMs = typeof options.timeoutMs === "number" && typeof clientTimeoutMs === "number"
|
|
379
|
-
? Math.min(options.timeoutMs, clientTimeoutMs)
|
|
380
|
-
: clientTimeoutMs ?? options.timeoutMs;
|
|
381
|
-
if (isAsync) {
|
|
382
|
-
const ticket = await service.execution.start(action.id, cleanInput, {
|
|
383
|
-
signal,
|
|
384
|
-
timeoutMs: effectiveTimeoutMs,
|
|
385
|
-
});
|
|
386
|
-
return {
|
|
387
|
-
content: [
|
|
388
|
-
{
|
|
389
|
-
type: "text",
|
|
390
|
-
text: JSON.stringify({
|
|
391
|
-
ok: true,
|
|
392
|
-
runId: ticket.runId,
|
|
393
|
-
taskId: ticket.runId,
|
|
394
|
-
status: "running",
|
|
395
|
-
}),
|
|
396
|
-
},
|
|
397
|
-
],
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
const result = await service.execution.run(action.id, cleanInput, {
|
|
110
|
+
const effectiveTimeoutMs = resolveExecutionTimeout(options, ctx);
|
|
111
|
+
const result = await service.execution.run(action.id, input, {
|
|
401
112
|
signal,
|
|
402
113
|
timeoutMs: effectiveTimeoutMs,
|
|
403
114
|
});
|
|
404
115
|
return toMcpResult(result);
|
|
405
116
|
});
|
|
406
117
|
}
|
|
407
|
-
// 任务规范映射(tasks/get、tasks/cancel、tasks/list)已收敛至 registerTasksExtension 隔离模块
|
|
408
118
|
// 资源与规程映射:规程映射为只读 MCP Resource 与 Prompt
|
|
409
119
|
try {
|
|
410
120
|
let playbooks = await service.discovery.listPlaybooks();
|
package/dist/execution-mode.d.ts
CHANGED
|
@@ -1,37 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP
|
|
3
|
-
*
|
|
4
|
-
* 单一显式约定:异步执行模式只认 input.execution.mode === "async"。
|
|
5
|
-
* 旧版魔法字段(__async 布尔)保留只读兼容探测,但不再参与 schema 注入,
|
|
6
|
-
* 也不再吞没业务自有的 async 入参字段:
|
|
7
|
-
* - 探测阶段仅读取 execution.mode 与 __async,语义不变;
|
|
8
|
-
* - 剥离阶段仅删除适配层实际注入的包装字段 execution 与 __async,
|
|
9
|
-
* 名为 async 的业务字段原样透传给 Action。
|
|
10
|
-
*
|
|
11
|
-
* 保留命名空间权衡:`execution` 与 `__async` 是适配层声明的前缀保留字段;
|
|
12
|
-
* 业务 Action 入参若恰好撞名 `__async: true`,会被误判为异步意图并在转发前剥离。
|
|
13
|
-
* 这是旧版兼容通道的已知代价,新代码统一使用 execution.mode 声明异步,
|
|
14
|
-
* 业务字段命名应避开该前缀保留命名空间。
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* 从工具调用入参中提取显式声明的异步执行模式。
|
|
18
|
-
*
|
|
19
|
-
* @param input 工具调用入参
|
|
20
|
-
* @returns 是否显式声明了异步执行(execution.mode 为 async,或旧版 __async 为 true)
|
|
2
|
+
* MCP 工具执行选项与调用上下文中的超时控制解析。
|
|
21
3
|
*/
|
|
22
|
-
export declare function isAsyncExecutionRequested(input: unknown): boolean;
|
|
23
4
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* 仅删除 WRAPPER_KEYS 中声明的包装字段;业务自有字段(含名为 async 的入参)原样保留。
|
|
5
|
+
* 解析有效的执行超时毫秒数。
|
|
27
6
|
*
|
|
28
|
-
*
|
|
29
|
-
*/
|
|
30
|
-
export declare function stripExecutionWrapper<T>(input: T): T;
|
|
31
|
-
/**
|
|
32
|
-
* 从工具调用入参的 execution 包装字段中提取客户端显式声明的超时时间。
|
|
7
|
+
* 优先从调用上下文中读取,缺省时回退到选项中的全局配置。
|
|
33
8
|
*
|
|
34
|
-
* @param
|
|
35
|
-
* @
|
|
9
|
+
* @param options MCP 初始化配置选项
|
|
10
|
+
* @param context 可选的调用上下文
|
|
11
|
+
* @returns 超时毫秒数,未指定或非法时返回 undefined
|
|
36
12
|
*/
|
|
37
|
-
export declare function
|
|
13
|
+
export declare function resolveExecutionTimeout(options?: {
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}, context?: {
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}): number | undefined;
|
|
18
|
+
export declare const resolveExecutionTimeoutMs: typeof resolveExecutionTimeout;
|
package/dist/execution-mode.js
CHANGED
|
@@ -1,84 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP
|
|
3
|
-
*
|
|
4
|
-
* 单一显式约定:异步执行模式只认 input.execution.mode === "async"。
|
|
5
|
-
* 旧版魔法字段(__async 布尔)保留只读兼容探测,但不再参与 schema 注入,
|
|
6
|
-
* 也不再吞没业务自有的 async 入参字段:
|
|
7
|
-
* - 探测阶段仅读取 execution.mode 与 __async,语义不变;
|
|
8
|
-
* - 剥离阶段仅删除适配层实际注入的包装字段 execution 与 __async,
|
|
9
|
-
* 名为 async 的业务字段原样透传给 Action。
|
|
10
|
-
*
|
|
11
|
-
* 保留命名空间权衡:`execution` 与 `__async` 是适配层声明的前缀保留字段;
|
|
12
|
-
* 业务 Action 入参若恰好撞名 `__async: true`,会被误判为异步意图并在转发前剥离。
|
|
13
|
-
* 这是旧版兼容通道的已知代价,新代码统一使用 execution.mode 声明异步,
|
|
14
|
-
* 业务字段命名应避开该前缀保留命名空间。
|
|
15
|
-
*/
|
|
16
|
-
/** 适配层注入到工具 schema 的执行控制包装字段名集合。 */
|
|
17
|
-
const WRAPPER_KEYS = ["execution", "__async"];
|
|
18
|
-
/**
|
|
19
|
-
* 从工具调用入参中提取显式声明的异步执行模式。
|
|
20
|
-
*
|
|
21
|
-
* @param input 工具调用入参
|
|
22
|
-
* @returns 是否显式声明了异步执行(execution.mode 为 async,或旧版 __async 为 true)
|
|
2
|
+
* MCP 工具执行选项与调用上下文中的超时控制解析。
|
|
23
3
|
*/
|
|
24
|
-
export function isAsyncExecutionRequested(input) {
|
|
25
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
const record = input;
|
|
29
|
-
const execution = record.execution;
|
|
30
|
-
if (typeof execution === "object" &&
|
|
31
|
-
execution !== null &&
|
|
32
|
-
!Array.isArray(execution) &&
|
|
33
|
-
execution.mode === "async") {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
// 旧版魔法字段兼容探测:仅只读,不再注入 schema 也不再参与剥离以外的影响
|
|
37
|
-
if (record.__async === true)
|
|
38
|
-
return true;
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
4
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* 仅删除 WRAPPER_KEYS 中声明的包装字段;业务自有字段(含名为 async 的入参)原样保留。
|
|
5
|
+
* 解析有效的执行超时毫秒数。
|
|
45
6
|
*
|
|
46
|
-
*
|
|
47
|
-
*/
|
|
48
|
-
export function stripExecutionWrapper(input) {
|
|
49
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
50
|
-
return input;
|
|
51
|
-
}
|
|
52
|
-
const record = input;
|
|
53
|
-
const hasWrapper = WRAPPER_KEYS.some((key) => key in record);
|
|
54
|
-
if (!hasWrapper)
|
|
55
|
-
return input;
|
|
56
|
-
const rest = {};
|
|
57
|
-
for (const [key, value] of Object.entries(record)) {
|
|
58
|
-
if (WRAPPER_KEYS.includes(key))
|
|
59
|
-
continue;
|
|
60
|
-
rest[key] = value;
|
|
61
|
-
}
|
|
62
|
-
return rest;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* 从工具调用入参的 execution 包装字段中提取客户端显式声明的超时时间。
|
|
7
|
+
* 优先从调用上下文中读取,缺省时回退到选项中的全局配置。
|
|
66
8
|
*
|
|
67
|
-
* @param
|
|
68
|
-
* @
|
|
9
|
+
* @param options MCP 初始化配置选项
|
|
10
|
+
* @param context 可选的调用上下文
|
|
11
|
+
* @returns 超时毫秒数,未指定或非法时返回 undefined
|
|
69
12
|
*/
|
|
70
|
-
export function
|
|
71
|
-
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
const execution = input.execution;
|
|
75
|
-
if (typeof execution !== "object" ||
|
|
76
|
-
execution === null ||
|
|
77
|
-
Array.isArray(execution)) {
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
const timeoutMs = execution.timeoutMs;
|
|
13
|
+
export function resolveExecutionTimeout(options, context) {
|
|
14
|
+
const timeoutMs = context?.timeoutMs ?? options?.timeoutMs;
|
|
81
15
|
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
82
16
|
? timeoutMs
|
|
83
17
|
: undefined;
|
|
84
18
|
}
|
|
19
|
+
export const resolveExecutionTimeoutMs = resolveExecutionTimeout;
|
package/dist/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ACTIONDOCK_VERSION, UNAUTHORIZED } from "@actiondock/core";
|
|
1
|
+
import { ACCESS_DENIED, ACTIONDOCK_VERSION, ActionDockError, UNAUTHORIZED } from "@actiondock/core";
|
|
2
2
|
import { createMcpEndpointHandler, formatHostForUrl, isLoopbackHost, launchHttpServer, resolveCorsHeaders, verifyBearerToken, } from "@actiondock/core/server";
|
|
3
3
|
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
4
4
|
import { createActionDockMcpServer, resolveService } from "./adapter.js";
|
|
@@ -21,7 +21,7 @@ export function startMcpHttpServer(options = {}) {
|
|
|
21
21
|
}
|
|
22
22
|
// Non-loopback address requires token authentication by default
|
|
23
23
|
if (!isLoopbackHost(host) && !token && !options.allowInsecureNoAuth) {
|
|
24
|
-
throw new
|
|
24
|
+
throw new ActionDockError(ACCESS_DENIED, "Authentication token is required when binding to a non-loopback address. Use --allow-insecure-no-auth to override.");
|
|
25
25
|
}
|
|
26
26
|
return (async () => {
|
|
27
27
|
// 先一次性解析 service,工厂闭包直接捕获已解析产物,消除选项重复展开
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
2
|
export * from "./schemas.js";
|
|
3
3
|
export * from "./execution-mode.js";
|
|
4
|
-
export { toMcpTaskStatus, toMcpTaskPayload, type McpTaskStatus, type McpTaskPayload } from "./register-tasks-extension.js";
|
|
5
4
|
export * from "./adapter.js";
|
|
6
5
|
export * from "./stdio.js";
|
|
7
6
|
export * from "./http.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
export * from "./types.js";
|
|
2
2
|
export * from "./schemas.js";
|
|
3
3
|
export * from "./execution-mode.js";
|
|
4
|
-
// 任务规范纯函数迁入 register-tasks-extension 后,从此处转引保持包根导出面不变
|
|
5
|
-
export { toMcpTaskStatus, toMcpTaskPayload } from "./register-tasks-extension.js";
|
|
6
4
|
export * from "./adapter.js";
|
|
7
5
|
export * from "./stdio.js";
|
|
8
6
|
export * from "./http.js";
|
package/dist/schemas.d.ts
CHANGED
|
@@ -4,8 +4,5 @@ import type { JsonSchema } from "@actiondock/sdk";
|
|
|
4
4
|
* 布尔分支已穷尽(true / false / 对象),无其余可达路径。
|
|
5
5
|
*
|
|
6
6
|
* @param schema 原始 JSON Schema 定义
|
|
7
|
-
* @param injectExecution 是否向 schema 注入适配层的 execution 执行控制包装字段:
|
|
8
|
-
* 入参 schema 需要注入(客户端借此声明执行模式与超时),
|
|
9
|
-
* 出参 schema 不注入(实际 structuredContent 不含该字段,注入会与真实返回结构不符)
|
|
10
7
|
*/
|
|
11
|
-
export declare function toMcpSchema(schema?: JsonSchema
|
|
8
|
+
export declare function toMcpSchema(schema?: JsonSchema): import("@modelcontextprotocol/server").StandardSchemaWithJSON<unknown, unknown>;
|
package/dist/schemas.js
CHANGED
|
@@ -4,11 +4,8 @@ import { fromJsonSchema } from "@modelcontextprotocol/server";
|
|
|
4
4
|
* 布尔分支已穷尽(true / false / 对象),无其余可达路径。
|
|
5
5
|
*
|
|
6
6
|
* @param schema 原始 JSON Schema 定义
|
|
7
|
-
* @param injectExecution 是否向 schema 注入适配层的 execution 执行控制包装字段:
|
|
8
|
-
* 入参 schema 需要注入(客户端借此声明执行模式与超时),
|
|
9
|
-
* 出参 schema 不注入(实际 structuredContent 不含该字段,注入会与真实返回结构不符)
|
|
10
7
|
*/
|
|
11
|
-
export function toMcpSchema(schema
|
|
8
|
+
export function toMcpSchema(schema) {
|
|
12
9
|
if (schema === undefined || schema === true) {
|
|
13
10
|
return fromJsonSchema({});
|
|
14
11
|
}
|
|
@@ -19,17 +16,5 @@ export function toMcpSchema(schema, injectExecution = true) {
|
|
|
19
16
|
}
|
|
20
17
|
// JSON Schema 为纯数据结构,structuredClone 深拷贝避免污染调用方原始定义;布尔分支已穷尽,收窄为对象模式
|
|
21
18
|
const cloned = structuredClone(schema);
|
|
22
|
-
const properties = (cloned.properties ?? {});
|
|
23
|
-
if ((cloned.type === "object" || cloned.properties) && injectExecution) {
|
|
24
|
-
cloned.properties = properties;
|
|
25
|
-
properties.execution = {
|
|
26
|
-
type: "object",
|
|
27
|
-
description: "Execution options",
|
|
28
|
-
properties: {
|
|
29
|
-
mode: { type: "string", enum: ["sync", "async"] },
|
|
30
|
-
timeoutMs: { type: "number" },
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
19
|
return fromJsonSchema(cloned);
|
|
35
20
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ActionDockService } from "@actiondock/core";
|
|
2
|
+
import type { ActionDockMcpOptions } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* 解析或基于选项创建底层 ActionDockService 统一门面。
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveService(options: ActionDockMcpOptions): Promise<{
|
|
7
|
+
service: ActionDockService;
|
|
8
|
+
ownsService: boolean;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { ACTIONDOCK_VERSION, ActionDockError, createActionDock, createNodePlatform, findProjectRoot, PACKAGE_NOT_FOUND, PROJECT_ROOT_NOT_FOUND, } from "@actiondock/core";
|
|
4
|
+
import { createActionDockHost } from "@actiondock/core/server";
|
|
5
|
+
import { resolvePackageRoot } from "@actiondock/core/registry";
|
|
6
|
+
import { createNonClosingStorageView } from "@actiondock/core/package";
|
|
7
|
+
/**
|
|
8
|
+
* 解析或基于选项创建底层 ActionDockService 统一门面。
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveService(options) {
|
|
11
|
+
if (options.service) {
|
|
12
|
+
return { service: options.service, ownsService: false };
|
|
13
|
+
}
|
|
14
|
+
if (options.host) {
|
|
15
|
+
return { service: options.host, ownsService: false };
|
|
16
|
+
}
|
|
17
|
+
if (options.runtime) {
|
|
18
|
+
const host = await createActionDockHost({
|
|
19
|
+
autoLoadCurrentProject: false,
|
|
20
|
+
scanLinkedPackages: false,
|
|
21
|
+
});
|
|
22
|
+
host.registerRuntime(options.runtime);
|
|
23
|
+
return { service: host, ownsService: false };
|
|
24
|
+
}
|
|
25
|
+
const packages = [];
|
|
26
|
+
// 外部注入的 storage 生命周期默认由注入方管理,适配层仅委托读写不接管关闭
|
|
27
|
+
// (非接管视图复用 core 单一事实源);显式声明 ownStorageLifecycle 时透传
|
|
28
|
+
// 原始实例,随 target.close() 级联关闭
|
|
29
|
+
const appStorage = options.storage
|
|
30
|
+
? options.ownStorageLifecycle
|
|
31
|
+
? options.storage
|
|
32
|
+
: createNonClosingStorageView(options.storage)
|
|
33
|
+
: undefined;
|
|
34
|
+
// 多分支共享的包选项基底单点构造:storage 视图、家目录与配置覆写
|
|
35
|
+
// 在此收敛一次,各 push 分支仅叠加自身特有字段
|
|
36
|
+
const basePkgOpts = {
|
|
37
|
+
...(appStorage ? { storage: appStorage } : {}),
|
|
38
|
+
customHome: options.customHome,
|
|
39
|
+
configOverrides: options.configOverrides,
|
|
40
|
+
};
|
|
41
|
+
if (options.actions) {
|
|
42
|
+
packages.push({
|
|
43
|
+
...basePkgOpts,
|
|
44
|
+
projectConfig: {
|
|
45
|
+
id: options.packageId || "default",
|
|
46
|
+
name: options.packageId || "default",
|
|
47
|
+
version: ACTIONDOCK_VERSION,
|
|
48
|
+
},
|
|
49
|
+
actions: options.actions,
|
|
50
|
+
inMemory: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (options.projectRoots && options.projectRoots.length > 0) {
|
|
54
|
+
for (const root of options.projectRoots) {
|
|
55
|
+
const abs = resolve(root);
|
|
56
|
+
const detected = findProjectRoot(abs);
|
|
57
|
+
if (!detected) {
|
|
58
|
+
throw new ActionDockError(PROJECT_ROOT_NOT_FOUND, `Project root '${root}' is not a valid ActionDock package (actiondock.json not found)`);
|
|
59
|
+
}
|
|
60
|
+
packages.push({
|
|
61
|
+
...basePkgOpts,
|
|
62
|
+
packageRoot: detected,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const targetPackageIds = options.packageIds || options.packageAllowlist;
|
|
67
|
+
if (targetPackageIds && targetPackageIds.length > 0) {
|
|
68
|
+
for (const pkgId of targetPackageIds) {
|
|
69
|
+
const root = resolvePackageRoot(pkgId, undefined, options.customHome);
|
|
70
|
+
if (!root || !existsSync(root)) {
|
|
71
|
+
throw new ActionDockError(PACKAGE_NOT_FOUND, `Package '${pkgId}' not found in registry`);
|
|
72
|
+
}
|
|
73
|
+
packages.push({
|
|
74
|
+
...basePkgOpts,
|
|
75
|
+
packageRoot: root,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let projectRoot = options.projectRoot;
|
|
80
|
+
if (options.projectRoot) {
|
|
81
|
+
const abs = resolve(options.projectRoot);
|
|
82
|
+
const detected = findProjectRoot(abs);
|
|
83
|
+
if (!detected) {
|
|
84
|
+
throw new ActionDockError(PROJECT_ROOT_NOT_FOUND, `Project root '${options.projectRoot}' is not a valid ActionDock package (actiondock.json not found)`);
|
|
85
|
+
}
|
|
86
|
+
projectRoot = detected;
|
|
87
|
+
}
|
|
88
|
+
if (!projectRoot &&
|
|
89
|
+
packages.length === 0 &&
|
|
90
|
+
!options.all &&
|
|
91
|
+
!options.packageId) {
|
|
92
|
+
const currentRoot = findProjectRoot(process.cwd());
|
|
93
|
+
if (!currentRoot) {
|
|
94
|
+
throw new ActionDockError(PROJECT_ROOT_NOT_FOUND, "No ActionDock project root found. Run inside an ActionDock package or specify --dir / --package / --all.");
|
|
95
|
+
}
|
|
96
|
+
projectRoot = currentRoot;
|
|
97
|
+
}
|
|
98
|
+
if (options.packageId && !options.actions && packages.length === 0) {
|
|
99
|
+
const root = resolvePackageRoot(options.packageId, undefined, options.customHome);
|
|
100
|
+
if (!root || !existsSync(root)) {
|
|
101
|
+
throw new ActionDockError(PACKAGE_NOT_FOUND, `Package '${options.packageId}' not found in registry`);
|
|
102
|
+
}
|
|
103
|
+
packages.push({
|
|
104
|
+
...basePkgOpts,
|
|
105
|
+
packageRoot: root,
|
|
106
|
+
dataDir: options.dataDir,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
let platform = options.platform;
|
|
110
|
+
if (!platform && typeof process !== "undefined" && process.versions?.node) {
|
|
111
|
+
platform = createNodePlatform({
|
|
112
|
+
customHome: options.customHome,
|
|
113
|
+
dataDir: options.dataDir,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
for (const pkg of packages) {
|
|
117
|
+
if (typeof pkg === "object" && pkg !== null && !("info" in pkg)) {
|
|
118
|
+
if (platform && !pkg.platform) {
|
|
119
|
+
pkg.platform = platform;
|
|
120
|
+
}
|
|
121
|
+
if (!pkg.dataDir && options.dataDir) {
|
|
122
|
+
pkg.dataDir = options.dataDir;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const service = await createActionDock({
|
|
127
|
+
type: "local",
|
|
128
|
+
projectRoot,
|
|
129
|
+
packages: packages.length > 0 ? packages : undefined,
|
|
130
|
+
scanLinkedPackages: Boolean(options.all),
|
|
131
|
+
hostOptions: {
|
|
132
|
+
autoLoadCurrentProject: packages.length === 0,
|
|
133
|
+
},
|
|
134
|
+
// MCP 服务进程是长驻执行宿主,声明数据目录持有者身份,
|
|
135
|
+
// 打开时收割遗留孤儿运行记录
|
|
136
|
+
recoverOrphans: true,
|
|
137
|
+
customHome: options.customHome,
|
|
138
|
+
dataDir: options.dataDir,
|
|
139
|
+
platform,
|
|
140
|
+
});
|
|
141
|
+
return { service, ownsService: true };
|
|
142
|
+
}
|
package/dist/stdio.js
CHANGED
|
@@ -52,8 +52,6 @@ export async function startMcpStdio(options = {}) {
|
|
|
52
52
|
// 标准输出物理隔离与受控限流排空转入 stderr 诊断流
|
|
53
53
|
serviceToUse = new IpcActionDockService({
|
|
54
54
|
childProcess,
|
|
55
|
-
maxDiagnosticBytes: 512 * 1024,
|
|
56
|
-
maxDiagnosticRate: 64 * 1024,
|
|
57
55
|
diagnosticTarget: process.stderr,
|
|
58
56
|
});
|
|
59
57
|
// IPC 代理实例由本入口创建,生命周期归本入口的 cleanup 所有
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 构造携带结构化错误码的工具名冲突异常。
|
|
3
|
+
*
|
|
4
|
+
* @param toolName 冲突的工具名
|
|
5
|
+
*/
|
|
6
|
+
export declare function toolNameCollisionError(toolName: string): Error & {
|
|
7
|
+
code: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* 解析 Action 引用字符串为包标识与动作标识(容错语义)。
|
|
11
|
+
*
|
|
12
|
+
* 委派 core 的 parseActionRef 单一事实源完成解析;非法形态(如尾部斜杠、
|
|
13
|
+
* 含冒号等)回退为整体短名处理,与适配层既有的注册容错行为保持一致,
|
|
14
|
+
* 不让目录聚合阶段的脏数据中断服务启动。
|
|
15
|
+
*/
|
|
16
|
+
export declare function splitActionRef(ref: string): {
|
|
17
|
+
packageId?: string;
|
|
18
|
+
actionId: string;
|
|
19
|
+
};
|
|
20
|
+
export interface MappedAction<T> {
|
|
21
|
+
toolName: string;
|
|
22
|
+
action: T;
|
|
23
|
+
isMultiPackage: boolean;
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 映射与过滤工具,处理冲突检测、去重、白名单过滤及 SHA-256 截断散列逻辑。
|
|
28
|
+
*/
|
|
29
|
+
export declare function mapAndFilterActions<T extends {
|
|
30
|
+
id: string;
|
|
31
|
+
packageId?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
}>(rawActions: T[], allowedPackageIds?: string[], actionAllowlist?: string[]): MappedAction<T>[];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { MCP_TOOL_NAME_COLLISION } from "@actiondock/core";
|
|
3
|
+
import { parseActionRef } from "@actiondock/core/graph";
|
|
4
|
+
import { isActionAllowed } from "@actiondock/core/server";
|
|
5
|
+
/**
|
|
6
|
+
* 构造携带结构化错误码的工具名冲突异常。
|
|
7
|
+
*
|
|
8
|
+
* @param toolName 冲突的工具名
|
|
9
|
+
*/
|
|
10
|
+
export function toolNameCollisionError(toolName) {
|
|
11
|
+
const err = new Error(`MCP tool name collision detected for tool '${toolName}'`);
|
|
12
|
+
err.code = MCP_TOOL_NAME_COLLISION;
|
|
13
|
+
return err;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 解析 Action 引用字符串为包标识与动作标识(容错语义)。
|
|
17
|
+
*
|
|
18
|
+
* 委派 core 的 parseActionRef 单一事实源完成解析;非法形态(如尾部斜杠、
|
|
19
|
+
* 含冒号等)回退为整体短名处理,与适配层既有的注册容错行为保持一致,
|
|
20
|
+
* 不让目录聚合阶段的脏数据中断服务启动。
|
|
21
|
+
*/
|
|
22
|
+
export function splitActionRef(ref) {
|
|
23
|
+
try {
|
|
24
|
+
return parseActionRef(ref);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { actionId: ref };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 映射与过滤工具,处理冲突检测、去重、白名单过滤及 SHA-256 截断散列逻辑。
|
|
32
|
+
*/
|
|
33
|
+
export function mapAndFilterActions(rawActions, allowedPackageIds, actionAllowlist) {
|
|
34
|
+
// 引用解析单点归一:每项仅解析一次,后续各轮(白名单过滤、去重、频次统计、
|
|
35
|
+
// 工具命名)复用同一结果,避免同一列表对 splitActionRef 的三轮重复调用;
|
|
36
|
+
// 显式 packageId 与解析所得 packageId 分别留存,各轮按既有优先级消费
|
|
37
|
+
const parsedRefs = new Map();
|
|
38
|
+
for (const act of rawActions) {
|
|
39
|
+
if (act.id.includes("/")) {
|
|
40
|
+
const parsed = splitActionRef(act.id);
|
|
41
|
+
parsedRefs.set(act, {
|
|
42
|
+
explicitPkgId: act.packageId || "",
|
|
43
|
+
parsedPkgId: parsed.packageId,
|
|
44
|
+
baseId: parsed.actionId,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
parsedRefs.set(act, { explicitPkgId: act.packageId || "", baseId: act.id });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (allowedPackageIds && allowedPackageIds.length > 0) {
|
|
52
|
+
// 白名单轮:显式 packageId 优先,缺失时回退解析所得
|
|
53
|
+
rawActions = rawActions.filter((act) => {
|
|
54
|
+
const ref = parsedRefs.get(act);
|
|
55
|
+
const pkgId = ref.explicitPkgId || ref.parsedPkgId || "";
|
|
56
|
+
return pkgId ? allowedPackageIds.includes(pkgId) : false;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (actionAllowlist && actionAllowlist.length > 0) {
|
|
60
|
+
rawActions = rawActions.filter((act) => {
|
|
61
|
+
const ref = parsedRefs.get(act);
|
|
62
|
+
const pkgId = ref.explicitPkgId || ref.parsedPkgId || undefined;
|
|
63
|
+
return isActionAllowed({ packageId: pkgId, actionId: ref.baseId }, actionAllowlist);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const seenActionKeys = new Map();
|
|
67
|
+
for (const act of rawActions) {
|
|
68
|
+
const ref = parsedRefs.get(act);
|
|
69
|
+
const pkgId = ref.explicitPkgId || ref.parsedPkgId || "";
|
|
70
|
+
const key = `${pkgId}:${ref.baseId}`;
|
|
71
|
+
const existing = seenActionKeys.get(key);
|
|
72
|
+
if (existing) {
|
|
73
|
+
// 若已存在的项是全限定名(含 /),而当前项是短名(不含 /),优先保留短名项
|
|
74
|
+
if (existing.id.includes("/") && !act.id.includes("/")) {
|
|
75
|
+
seenActionKeys.set(key, act);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
seenActionKeys.set(key, act);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const actions = Array.from(seenActionKeys.values());
|
|
83
|
+
// 统计 Action 基础 ID 出现频次,用于同名冲突命名空间隔离
|
|
84
|
+
const baseCounts = new Map();
|
|
85
|
+
for (const act of actions) {
|
|
86
|
+
const baseId = parsedRefs.get(act).baseId;
|
|
87
|
+
baseCounts.set(baseId, (baseCounts.get(baseId) || 0) + 1);
|
|
88
|
+
}
|
|
89
|
+
const registeredToolNames = new Set();
|
|
90
|
+
// 多包判定(循环外一次算清):不同 packageId 去重计数大于 1,
|
|
91
|
+
// 或任一 id 含斜杠(跨包限定名形态),则工具描述需附全限定 id 锚点;
|
|
92
|
+
// 本轮与工具命名轮一致:解析所得 packageId 优先,缺失时回退显式声明
|
|
93
|
+
const distinctPackageIds = new Set(actions
|
|
94
|
+
.map((a) => {
|
|
95
|
+
const ref = parsedRefs.get(a);
|
|
96
|
+
return ref.parsedPkgId || a.packageId;
|
|
97
|
+
})
|
|
98
|
+
.filter((pkg) => Boolean(pkg)));
|
|
99
|
+
const isMultiPackage = distinctPackageIds.size > 1 || actions.some((a) => a.id.includes("/"));
|
|
100
|
+
const mappedActions = [];
|
|
101
|
+
for (const action of actions) {
|
|
102
|
+
const ref = parsedRefs.get(action);
|
|
103
|
+
const baseId = ref.baseId;
|
|
104
|
+
const packageId = ref.parsedPkgId || action.packageId;
|
|
105
|
+
const count = baseCounts.get(baseId) || 1;
|
|
106
|
+
let toolName = baseId;
|
|
107
|
+
if (count > 1 && packageId) {
|
|
108
|
+
const cleanPkgId = packageId.replace(/^@/, "").replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
109
|
+
toolName = `${cleanPkgId}_${baseId}`;
|
|
110
|
+
}
|
|
111
|
+
else if (toolName.includes("/") && packageId) {
|
|
112
|
+
const cleanPkgId = packageId.replace(/^@/, "").replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
113
|
+
toolName = `${cleanPkgId}_${baseId}`;
|
|
114
|
+
}
|
|
115
|
+
if (toolName.length > 64) {
|
|
116
|
+
const hash = createHash("sha256").update(toolName).digest("hex").slice(0, 8);
|
|
117
|
+
toolName = `${toolName.slice(0, 55)}_${hash}`;
|
|
118
|
+
}
|
|
119
|
+
if (registeredToolNames.has(toolName)) {
|
|
120
|
+
throw toolNameCollisionError(toolName);
|
|
121
|
+
}
|
|
122
|
+
registeredToolNames.add(toolName);
|
|
123
|
+
const description = isMultiPackage
|
|
124
|
+
? `[${action.id}] ${action.description || ""}`.trim()
|
|
125
|
+
: action.description;
|
|
126
|
+
mappedActions.push({ toolName, action, isMultiPackage, description });
|
|
127
|
+
}
|
|
128
|
+
return mappedActions;
|
|
129
|
+
}
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actiondock/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "ActionDock MCP Adapter for exposing Actions as Model Context Protocol (MCP) Tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"test": "node ../../scripts/run-tests.ts"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@actiondock/core": "^2.
|
|
33
|
-
"@actiondock/sdk": "^2.
|
|
32
|
+
"@actiondock/core": "^2.11.0",
|
|
33
|
+
"@actiondock/sdk": "^2.11.0",
|
|
34
34
|
"@modelcontextprotocol/server": "^2.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { ActionDockService } from "@actiondock/core";
|
|
2
|
-
import type { RunRecord, RunStatus } from "@actiondock/sdk";
|
|
3
|
-
import { McpServer } from "@modelcontextprotocol/server";
|
|
4
|
-
/**
|
|
5
|
-
* MCP 任务状态枚举(兼容 Model Context Protocol Task 规范)。
|
|
6
|
-
*/
|
|
7
|
-
export type McpTaskStatus = "working" | "completed" | "failed" | "cancelled";
|
|
8
|
-
/**
|
|
9
|
-
* MCP 任务状态数据载荷结构体。
|
|
10
|
-
*/
|
|
11
|
-
export interface McpTaskPayload {
|
|
12
|
-
taskId: string;
|
|
13
|
-
status: McpTaskStatus;
|
|
14
|
-
createdAt: string;
|
|
15
|
-
finishedAt?: string;
|
|
16
|
-
input?: unknown;
|
|
17
|
-
output?: unknown;
|
|
18
|
-
error?: unknown;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* 将 ActionDock 内部的 RunStatus 转换为 MCP 标准的 TaskStatus。
|
|
22
|
-
*/
|
|
23
|
-
export declare function toMcpTaskStatus(status: RunStatus): McpTaskStatus;
|
|
24
|
-
/**
|
|
25
|
-
* 将内部 RunRecord 实体转换为向 MCP 客户端暴露的 McpTaskPayload。
|
|
26
|
-
*/
|
|
27
|
-
export declare function toMcpTaskPayload(run: RunRecord): McpTaskPayload;
|
|
28
|
-
/**
|
|
29
|
-
* 在低层 Server 实例上注册 tasks 能力声明与三个请求处理器。
|
|
30
|
-
*
|
|
31
|
-
* @param server MCP 高层服务实例
|
|
32
|
-
* @param service ActionDockService 公共服务实例
|
|
33
|
-
*/
|
|
34
|
-
export declare function registerTasksExtension(server: McpServer, service: ActionDockService, allowedPackageIds?: string[], actionAllowlist?: string[]): void;
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { isActionAllowed } from "@actiondock/core/server";
|
|
2
|
-
import { fromJsonSchema } from "@modelcontextprotocol/server";
|
|
3
|
-
/**
|
|
4
|
-
* 将 ActionDock 内部的 RunStatus 转换为 MCP 标准的 TaskStatus。
|
|
5
|
-
*/
|
|
6
|
-
export function toMcpTaskStatus(status) {
|
|
7
|
-
switch (status) {
|
|
8
|
-
case "running":
|
|
9
|
-
return "working";
|
|
10
|
-
case "success":
|
|
11
|
-
return "completed";
|
|
12
|
-
case "failed":
|
|
13
|
-
case "timed_out":
|
|
14
|
-
case "interrupted":
|
|
15
|
-
return "failed";
|
|
16
|
-
case "cancelled":
|
|
17
|
-
return "cancelled";
|
|
18
|
-
default:
|
|
19
|
-
return "failed";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* 将内部 RunRecord 实体转换为向 MCP 客户端暴露的 McpTaskPayload。
|
|
24
|
-
*/
|
|
25
|
-
export function toMcpTaskPayload(run) {
|
|
26
|
-
return {
|
|
27
|
-
taskId: run.id,
|
|
28
|
-
status: toMcpTaskStatus(run.status),
|
|
29
|
-
createdAt: run.startedAt,
|
|
30
|
-
finishedAt: run.finishedAt,
|
|
31
|
-
input: run.input,
|
|
32
|
-
output: run.output,
|
|
33
|
-
error: run.error,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/** JSON-RPC 保留码段:协议保留区为 -32000 至 -32099,服务端自定义错误码必须落在该区段内。 */
|
|
37
|
-
const TASK_NOT_FOUND_CODE = -32001;
|
|
38
|
-
/** 无效的 limit 参数错误码,同样落在服务端自定义码段。 */
|
|
39
|
-
const INVALID_LIMIT_CODE = -32002;
|
|
40
|
-
/** tasks/list 允许的最大单页数量上限。 */
|
|
41
|
-
const MAX_TASK_LIST_LIMIT = 500;
|
|
42
|
-
/**
|
|
43
|
-
* 构造携带 JSON-RPC 语义码的任务不存在异常。
|
|
44
|
-
*
|
|
45
|
-
* MCP SDK 对处理器抛出的异常会读取数字型 code 字段并原样透传到错误响应,
|
|
46
|
-
* 裸 Error 缺少 code 会被映射为内部错误(-32603),丢失调用方可分辨的语义。
|
|
47
|
-
*
|
|
48
|
-
* @param taskId 未找到的任务标识
|
|
49
|
-
*/
|
|
50
|
-
function taskNotFoundError(taskId) {
|
|
51
|
-
return Object.assign(new Error(`Task '${taskId}' not found`), { code: TASK_NOT_FOUND_CODE });
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* 构造携带 JSON-RPC 语义码的非法 limit 参数异常。
|
|
55
|
-
*
|
|
56
|
-
* @param limit 非法的 limit 入参
|
|
57
|
-
*/
|
|
58
|
-
function invalidLimitError(limit) {
|
|
59
|
-
return Object.assign(new Error(`Invalid tasks/list limit: expected an integer between 1 and ${MAX_TASK_LIST_LIMIT}, got ${JSON.stringify(limit)}`), { code: INVALID_LIMIT_CODE });
|
|
60
|
-
}
|
|
61
|
-
const TASK_ID_PARAMS = fromJsonSchema({
|
|
62
|
-
type: "object",
|
|
63
|
-
properties: {
|
|
64
|
-
taskId: { type: "string" },
|
|
65
|
-
reason: { type: "string" },
|
|
66
|
-
},
|
|
67
|
-
required: ["taskId"],
|
|
68
|
-
});
|
|
69
|
-
const TASK_LIST_PARAMS = fromJsonSchema({
|
|
70
|
-
type: "object",
|
|
71
|
-
properties: {
|
|
72
|
-
limit: { type: "number" },
|
|
73
|
-
actionId: { type: "string" },
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
/**
|
|
77
|
-
* 将内部 RunRecord 的 ISO 时间字符串转为毫秒时间戳,用于任务排序比较。
|
|
78
|
-
*
|
|
79
|
-
* @param value 待解析的时间字符串
|
|
80
|
-
*/
|
|
81
|
-
function toTimeMillis(value) {
|
|
82
|
-
const parsed = Date.parse(value);
|
|
83
|
-
return Number.isNaN(parsed) ? 0 : parsed;
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* 在低层 Server 实例上注册 tasks 能力声明与三个请求处理器。
|
|
87
|
-
*
|
|
88
|
-
* @param server MCP 高层服务实例
|
|
89
|
-
* @param service ActionDockService 公共服务实例
|
|
90
|
-
*/
|
|
91
|
-
export function registerTasksExtension(server, service, allowedPackageIds, actionAllowlist) {
|
|
92
|
-
server.server.registerCapabilities({
|
|
93
|
-
tasks: {
|
|
94
|
-
list: {},
|
|
95
|
-
cancel: {},
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
server.server.setRequestHandler("tasks/get", { params: TASK_ID_PARAMS }, async (params) => {
|
|
99
|
-
const run = await service.runs.get(params.taskId);
|
|
100
|
-
if (!run) {
|
|
101
|
-
// 携带语义码透传,避免被 SDK 映射为内部错误
|
|
102
|
-
throw taskNotFoundError(params.taskId);
|
|
103
|
-
}
|
|
104
|
-
if (allowedPackageIds &&
|
|
105
|
-
allowedPackageIds.length > 0 &&
|
|
106
|
-
(!run.packageId || !allowedPackageIds.includes(run.packageId))) {
|
|
107
|
-
throw taskNotFoundError(params.taskId);
|
|
108
|
-
}
|
|
109
|
-
if (actionAllowlist &&
|
|
110
|
-
actionAllowlist.length > 0 &&
|
|
111
|
-
!isActionAllowed({ packageId: run.packageId, actionId: run.actionId }, actionAllowlist)) {
|
|
112
|
-
throw taskNotFoundError(params.taskId);
|
|
113
|
-
}
|
|
114
|
-
return { task: toMcpTaskPayload(run) };
|
|
115
|
-
});
|
|
116
|
-
server.server.setRequestHandler("tasks/cancel", { params: TASK_ID_PARAMS }, async (params) => {
|
|
117
|
-
const hasPackageFilter = Boolean(allowedPackageIds && allowedPackageIds.length > 0);
|
|
118
|
-
const hasActionFilter = Boolean(actionAllowlist && actionAllowlist.length > 0);
|
|
119
|
-
if (hasPackageFilter || hasActionFilter) {
|
|
120
|
-
const existingRun = await service.runs.get(params.taskId);
|
|
121
|
-
if (!existingRun) {
|
|
122
|
-
throw taskNotFoundError(params.taskId);
|
|
123
|
-
}
|
|
124
|
-
if (hasPackageFilter) {
|
|
125
|
-
if (!existingRun.packageId || !allowedPackageIds.includes(existingRun.packageId)) {
|
|
126
|
-
throw taskNotFoundError(params.taskId);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (hasActionFilter) {
|
|
130
|
-
if (!isActionAllowed({ packageId: existingRun.packageId, actionId: existingRun.actionId }, actionAllowlist)) {
|
|
131
|
-
throw taskNotFoundError(params.taskId);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
const reason = params.reason || "Cancelled via MCP tasks/cancel";
|
|
136
|
-
const cancelRes = await service.runs.cancel(params.taskId, reason);
|
|
137
|
-
if (cancelRes.outcome === "requested") {
|
|
138
|
-
return { taskId: params.taskId, status: "cancelled" };
|
|
139
|
-
}
|
|
140
|
-
if (cancelRes.outcome === "already_terminal") {
|
|
141
|
-
return { taskId: params.taskId, status: toMcpTaskStatus(cancelRes.status) };
|
|
142
|
-
}
|
|
143
|
-
const run = await service.runs.get(params.taskId);
|
|
144
|
-
if (!run) {
|
|
145
|
-
// 携带语义码透传,避免被 SDK 映射为内部错误
|
|
146
|
-
throw taskNotFoundError(params.taskId);
|
|
147
|
-
}
|
|
148
|
-
return { taskId: params.taskId, status: toMcpTaskStatus(run.status) };
|
|
149
|
-
});
|
|
150
|
-
server.server.setRequestHandler("tasks/list", { params: TASK_LIST_PARAMS }, async (params) => {
|
|
151
|
-
// 边界防御:limit 必须为整数,缺省取 50;超限钳制到 [1, 500],非法值抛带语义码的参数错误
|
|
152
|
-
let limit = 50;
|
|
153
|
-
if (params.limit !== undefined) {
|
|
154
|
-
if (typeof params.limit !== "number" || !Number.isInteger(params.limit)) {
|
|
155
|
-
throw invalidLimitError(params.limit);
|
|
156
|
-
}
|
|
157
|
-
limit = Math.min(Math.max(params.limit, 1), MAX_TASK_LIST_LIMIT);
|
|
158
|
-
}
|
|
159
|
-
const actionId = params.actionId;
|
|
160
|
-
// limit 单处截断:下推到 service.runs.list 的 limit 参数,此处不再重复 slice
|
|
161
|
-
let runs = await service.runs.list({ limit, actionId });
|
|
162
|
-
if (allowedPackageIds && allowedPackageIds.length > 0) {
|
|
163
|
-
runs = runs.filter((r) => Boolean(r.packageId && allowedPackageIds.includes(r.packageId)));
|
|
164
|
-
}
|
|
165
|
-
if (actionAllowlist && actionAllowlist.length > 0) {
|
|
166
|
-
runs = runs.filter((r) => isActionAllowed({ packageId: r.packageId, actionId: r.actionId }, actionAllowlist));
|
|
167
|
-
}
|
|
168
|
-
const ordered = [...runs].sort((a, b) => toTimeMillis(b.startedAt) - toTimeMillis(a.startedAt));
|
|
169
|
-
return { tasks: ordered.map(toMcpTaskPayload) };
|
|
170
|
-
});
|
|
171
|
-
}
|