@actiondock/mcp 2.7.0 → 2.9.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 CHANGED
@@ -1,13 +1,13 @@
1
1
  # @actiondock/mcp
2
2
 
3
- ActionDock 2.0 模型上下文协议适配器。
3
+ ActionDock 2.x 模型上下文协议适配器。
4
4
 
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D24.12.0-green?logo=node.js)](https://nodejs.org/)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?logo=typescript)](https://www.typescriptlang.org/)
7
7
  [![MCP](https://img.shields.io/badge/MCP-Protocol%20Compliant-purple)](https://modelcontextprotocol.io/)
8
8
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
9
9
 
10
- `@actiondock/mcp` 将 ActionDock 中的原子 Action 映射为标准的模型上下文协议工具,无缝接入各类主流 AI 智能体与开发环境。
10
+ `@actiondock/mcp` 将 ActionDock 中的原子 Action 映射为标准的模型上下文协议工具,无缝接入各类主流 AI 智能体与开发环境。在 ActionDock 2.x 中,MCP 适配层全面对接 `@actiondock/core` 导出的标准服务端口体系(`DiscoveryPort`、`ExecutionPort` 与 `RunsPort`),彻底消除对底层私有实体的直接穿透。
11
11
 
12
12
  ---
13
13
 
@@ -28,7 +28,7 @@ ActionDock 2.0 模型上下文协议适配器。
28
28
  ### 协同取消信号向下传播
29
29
 
30
30
  - 当 MCP 客户端发起取消请求时,适配层自动捕获中断事件。
31
- - 取消信号直接传递至底层的 ActionRunner 调度器,并联动激活当前任务上下文中的 `ctx.signal`。
31
+ - 取消信号通过服务端口直接传递至底层的执行服务,并联动激活当前任务上下文中的 `ctx.signal`。
32
32
  - 业务代码可通过监听 AbortSignal 安全释放资源或提前终止执行。
33
33
 
34
34
  ### Tasks 异步任务映射扩展
@@ -39,6 +39,14 @@ ActionDock 2.0 模型上下文协议适配器。
39
39
  - 任务主动取消:通过 `tasks/cancel` 端点向正在后台执行的长周期任务发出中止指令。
40
40
  - 任务清单列举:通过 `tasks/list` 端点批量查询当前会话及宿主下的活跃与历史任务列表。
41
41
 
42
+ ### 细粒度权限白名单与安全过滤
43
+
44
+ MCP 适配层原生支持基于包与动作维度的双层白名单控制,通过 `ActionDockMcpOptions` 进行配置:
45
+
46
+ - 包白名单 `packageAllowlist`(别名 `packageIds`):限制仅暴露指定包内的工具。
47
+ - 动作白名单 `actionAllowlist`:限制仅暴露指定的动作,支持动作短名 `actionId` 与全限定名 `packageId/actionId`。
48
+ - 联动过滤机制:同时配置包白名单与动作白名单时自动计算权限交集。未获授权的动作不会出现在 `tools/list` 工具列表中;在 `tasks/get`、`tasks/cancel` 与 `tasks/list` 任务扩展中,涉及未授权动作的任务同样予以严格隔离与隐藏。
49
+
42
50
  ---
43
51
 
44
52
  ## 快速使用
@@ -60,6 +68,8 @@ import { startMcpStdioServer } from "@actiondock/mcp";
60
68
 
61
69
  await startMcpStdioServer({
62
70
  projectRoot: process.cwd(),
71
+ packageAllowlist: ["system-tools"],
72
+ actionAllowlist: ["system-tools/status", "ping"],
63
73
  });
64
74
  ```
65
75
 
package/dist/adapter.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { ActionDockApp, ActionDockHost, ActionDockTarget } from "@actiondock/core";
1
+ import { type ActionDockService } from "@actiondock/core";
2
+ import { type ActionDockHost } from "@actiondock/core/server";
3
+ import type { PackageRuntime } from "@actiondock/core/package";
2
4
  import type { ExecutionResult } from "@actiondock/sdk";
3
5
  import { McpServer } from "@modelcontextprotocol/server";
4
6
  import type { ActionDockMcpOptions } from "./types.js";
@@ -29,23 +31,23 @@ export declare function toMcpResult(result: ExecutionResult): {
29
31
  structuredContent?: undefined;
30
32
  };
31
33
  /**
32
- * 解析或基于选项创建底层 ActionDockTarget 统一门面。
34
+ * 解析或基于选项创建底层 ActionDockService 统一门面。
33
35
  */
34
- export declare function resolveTarget(options: ActionDockMcpOptions): Promise<{
35
- target: ActionDockTarget;
36
- ownsTarget: boolean;
36
+ export declare function resolveService(options: ActionDockMcpOptions): Promise<{
37
+ service: ActionDockService;
38
+ ownsService: boolean;
37
39
  }>;
38
40
  export type ActionDockMcpServer = McpServer & {
39
41
  close: () => Promise<void>;
40
- target: ActionDockTarget;
42
+ service: ActionDockService;
41
43
  host?: ActionDockHost;
42
- app?: ActionDockApp;
44
+ runtime?: PackageRuntime;
43
45
  events?: (runId: string, options?: {
44
46
  after?: number;
45
47
  signal?: AbortSignal;
46
48
  }) => AsyncIterable<Record<string, unknown>>;
47
49
  };
48
50
  /**
49
- * 创建并配置基于 ActionDockTarget 的 McpServer 适配层实例。
51
+ * 创建并配置基于 ActionDockService 的 McpServer 适配层实例。
50
52
  */
51
53
  export declare function createActionDockMcpServer(options?: ActionDockMcpOptions): Promise<ActionDockMcpServer>;
package/dist/adapter.js CHANGED
@@ -1,11 +1,46 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
- import { ACTIONDOCK_VERSION, ActionResolver, createActionDockTarget, findProjectRoot, resolvePackageRoot, } from "@actiondock/core";
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";
5
9
  import { McpServer } from "@modelcontextprotocol/server";
6
10
  import { registerTasksExtension } from "./register-tasks-extension.js";
7
11
  import { toMcpSchema } from "./schemas.js";
8
12
  import { extractExecutionTimeoutMs, isAsyncExecutionRequested, stripExecutionWrapper, } from "./execution-mode.js";
13
+ /** 形状缺失告警是否已输出过(每个进程仅告警一次,避免逐请求噪声) */
14
+ let cancelSignalDegradationWarned = false;
15
+ /**
16
+ * 从 MCP 工具回调上下文提取请求级取消信号。
17
+ *
18
+ * @param ctx MCP SDK 回调传入的工具执行上下文
19
+ * @returns 可用的 AbortSignal;形状缺失时返回 undefined 并输出一次降级警告
20
+ */
21
+ function extractCancelSignal(ctx) {
22
+ const signal = ctx?.mcpReq?.signal;
23
+ if (!signal && !cancelSignalDegradationWarned) {
24
+ cancelSignalDegradationWarned = true;
25
+ process.stderr.write("[actiondock-mcp] Tool cancel signal unavailable: MCP SDK callback context does not expose mcpReq.signal; per-request cancellation is degraded for this server instance.\n");
26
+ }
27
+ return signal;
28
+ }
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
+ }
9
44
  /**
10
45
  * 判断目标值是否为普通对象(Plain Object)。
11
46
  *
@@ -48,28 +83,6 @@ export function toMcpResult(result) {
48
83
  ],
49
84
  };
50
85
  }
51
- /** 适配层统一结构化错误码:MCP 工具名冲突。 */
52
- const MCP_TOOL_NAME_COLLISION = "MCP_TOOL_NAME_COLLISION";
53
- /**
54
- * 构造外部注入 storage 的非接管视图。
55
- *
56
- * 默认(ownStorageLifecycle 为 false)时外部 storage 生命周期由注入方管理,
57
- * 适配层仅委托读写而不接管关闭:所有成员函数与属性原样转发到原始实例并绑定原 this,
58
- * 仅 close 收敛为显式声明的无操作边界,确保 target.close() 级联关闭时不会误伤外部实例。
59
- *
60
- * @param storage 外部注入的存储实例
61
- */
62
- function createExternalStorageView(storage) {
63
- return new Proxy(storage, {
64
- get(target, prop) {
65
- if (prop === "close") {
66
- return () => undefined;
67
- }
68
- const value = Reflect.get(target, prop, target);
69
- return typeof value === "function" ? value.bind(target) : value;
70
- },
71
- });
72
- }
73
86
  /**
74
87
  * 构造携带结构化错误码的工具名冲突异常。
75
88
  *
@@ -81,42 +94,51 @@ function toolNameCollisionError(toolName) {
81
94
  return err;
82
95
  }
83
96
  /**
84
- * 解析或基于选项创建底层 ActionDockTarget 统一门面。
97
+ * 解析或基于选项创建底层 ActionDockService 统一门面。
85
98
  */
86
- export async function resolveTarget(options) {
87
- if (options.target) {
88
- return { target: options.target, ownsTarget: false };
99
+ export async function resolveService(options) {
100
+ if (options.service) {
101
+ return { service: options.service, ownsService: false };
89
102
  }
90
103
  if (options.host) {
91
- const target = await createActionDockTarget({ type: "local", host: options.host });
92
- return { target, ownsTarget: false };
104
+ const service = new LocalActionDockService(options.host);
105
+ return { service, ownsService: false };
93
106
  }
94
- if (options.app) {
95
- const target = await createActionDockTarget({ type: "local", app: options.app });
96
- return { target, ownsTarget: false };
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 };
97
115
  }
98
116
  const packages = [];
99
- // 外部注入的 storage 生命周期默认由注入方管理,适配层不伪造 close 语义;
100
- // 仅当显式声明 ownStorageLifecycle 时才向包配置透传原始实例(随 target.close() 级联关闭)
101
- // 外部注入的 storage 生命周期默认由注入方管理,适配层仅委托读写不接管关闭;
102
- // 显式声明 ownStorageLifecycle 时透传原始实例,随 target.close() 级联关闭
117
+ // 外部注入的 storage 生命周期默认由注入方管理,适配层仅委托读写不接管关闭
118
+ // (非接管视图复用 core 单一事实源);显式声明 ownStorageLifecycle 时透传
119
+ // 原始实例,随 target.close() 级联关闭
103
120
  const appStorage = options.storage
104
121
  ? options.ownStorageLifecycle
105
122
  ? options.storage
106
- : createExternalStorageView(options.storage)
123
+ : createNonClosingStorageView(options.storage)
107
124
  : undefined;
125
+ // 多分支共享的包选项基底单点构造:storage 视图、家目录与配置覆写
126
+ // 在此收敛一次,各 push 分支仅叠加自身特有字段
127
+ const basePkgOpts = {
128
+ ...(appStorage ? { storage: appStorage } : {}),
129
+ customHome: options.customHome,
130
+ configOverrides: options.configOverrides,
131
+ };
108
132
  if (options.actions) {
109
133
  packages.push({
134
+ ...basePkgOpts,
110
135
  projectConfig: {
111
136
  id: options.packageId || "default",
112
137
  name: options.packageId || "default",
113
138
  version: ACTIONDOCK_VERSION,
114
139
  },
115
140
  actions: options.actions,
116
- storage: appStorage,
117
141
  inMemory: true,
118
- customHome: options.customHome,
119
- configOverrides: options.configOverrides,
120
142
  });
121
143
  }
122
144
  if (options.projectRoots && options.projectRoots.length > 0) {
@@ -127,24 +149,21 @@ export async function resolveTarget(options) {
127
149
  throw new Error(`Project root '${root}' is not a valid ActionDock package (actiondock.json not found)`);
128
150
  }
129
151
  packages.push({
152
+ ...basePkgOpts,
130
153
  packageRoot: detected,
131
- storage: appStorage,
132
- customHome: options.customHome,
133
- configOverrides: options.configOverrides,
134
154
  });
135
155
  }
136
156
  }
137
- if (options.packageIds && options.packageIds.length > 0) {
138
- for (const pkgId of options.packageIds) {
157
+ const targetPackageIds = options.packageIds || options.packageAllowlist;
158
+ if (targetPackageIds && targetPackageIds.length > 0) {
159
+ for (const pkgId of targetPackageIds) {
139
160
  const root = resolvePackageRoot(pkgId, undefined, options.customHome);
140
161
  if (!root || !existsSync(root)) {
141
162
  throw new Error(`Package '${pkgId}' not found in registry`);
142
163
  }
143
164
  packages.push({
165
+ ...basePkgOpts,
144
166
  packageRoot: root,
145
- storage: appStorage,
146
- customHome: options.customHome,
147
- configOverrides: options.configOverrides,
148
167
  });
149
168
  }
150
169
  }
@@ -173,32 +192,18 @@ export async function resolveTarget(options) {
173
192
  throw new Error(`Package '${options.packageId}' not found in registry`);
174
193
  }
175
194
  packages.push({
195
+ ...basePkgOpts,
176
196
  packageRoot: root,
177
- storage: appStorage,
178
- customHome: options.customHome,
179
- configOverrides: options.configOverrides,
180
197
  dataDir: options.dataDir,
181
198
  });
182
199
  }
183
200
  let platform = options.platform;
184
201
  if (!platform && typeof process !== "undefined" && process.versions?.node) {
185
- try {
186
- const { createNodePlatform } = await import("@actiondock/runtime-node");
187
- platform = createNodePlatform({
188
- customHome: options.customHome,
189
- dataDir: options.dataDir,
190
- rootDir: projectRoot,
191
- });
192
- }
193
- catch (err) {
194
- // 仅「模块不存在」类错误允许降级回退 core 默认平台;
195
- // 其余异常(加载后初始化失败等)原样上抛,避免吞没真实故障
196
- const isModuleMissing = err?.code === "ERR_MODULE_NOT_FOUND" ||
197
- /Cannot find (?:package|module)/i.test(String(err?.message || ""));
198
- if (!isModuleMissing) {
199
- throw err;
200
- }
201
- }
202
+ platform = createNodePlatform({
203
+ customHome: options.customHome,
204
+ dataDir: options.dataDir,
205
+ rootDir: projectRoot,
206
+ });
202
207
  }
203
208
  for (const pkg of packages) {
204
209
  if (typeof pkg === "object" && pkg !== null && !("info" in pkg)) {
@@ -210,7 +215,7 @@ export async function resolveTarget(options) {
210
215
  }
211
216
  }
212
217
  }
213
- const target = await createActionDockTarget({
218
+ const service = await createActionDock({
214
219
  type: "local",
215
220
  projectRoot,
216
221
  packages: packages.length > 0 ? packages : undefined,
@@ -225,24 +230,30 @@ export async function resolveTarget(options) {
225
230
  dataDir: options.dataDir,
226
231
  platform,
227
232
  });
228
- return { target, ownsTarget: true };
233
+ return { service, ownsService: true };
229
234
  }
230
235
  /**
231
- * 创建并配置基于 ActionDockTarget 的 McpServer 适配层实例。
236
+ * 创建并配置基于 ActionDockService 的 McpServer 适配层实例。
232
237
  */
233
238
  export async function createActionDockMcpServer(options = {}) {
234
- const { target } = await resolveTarget(options);
239
+ const { service } = await resolveService(options);
240
+ const allowedPackageIds = options.packageAllowlist && options.packageAllowlist.length > 0
241
+ ? options.packageAllowlist
242
+ : options.packageIds && options.packageIds.length > 0
243
+ ? options.packageIds
244
+ : options.packageId
245
+ ? [options.packageId]
246
+ : undefined;
235
247
  let serverName = "actiondock";
236
248
  let serverVersion = ACTIONDOCK_VERSION;
237
249
  try {
238
- const info = await target.info();
239
- if (info) {
240
- serverName = info.name || info.id || "actiondock";
241
- serverVersion = info.protocolVersion || ACTIONDOCK_VERSION;
242
- if (info.packages && info.packages.length === 1) {
243
- serverName = info.packages[0].name || info.packages[0].id || serverName;
244
- serverVersion = info.packages[0].version || serverVersion;
245
- }
250
+ let packages = await service.info();
251
+ if (allowedPackageIds && allowedPackageIds.length > 0) {
252
+ packages = packages.filter((p) => p.id && allowedPackageIds.includes(p.id));
253
+ }
254
+ if (packages && packages.length === 1) {
255
+ serverName = packages[0].name || packages[0].id || serverName;
256
+ serverVersion = packages[0].version || serverVersion;
246
257
  }
247
258
  }
248
259
  catch {
@@ -253,26 +264,46 @@ export async function createActionDockMcpServer(options = {}) {
253
264
  version: serverVersion,
254
265
  });
255
266
  // 任务规范扩展注册集中隔离在独立模块,本层不再直接操作 SDK 内层实例
256
- registerTasksExtension(server, target);
257
- // 工具注册与模式映射:tools/list 纯粹委托 target.listActions()
258
- const rawActions = await target.listActions();
259
- const seenActionKeys = new Map();
267
+ registerTasksExtension(server, service, allowedPackageIds, options.actionAllowlist);
268
+ // 工具注册与模式映射:tools/list 纯粹委托 service.discovery.listActions()
269
+ let rawActions = await service.discovery.listActions();
270
+ // 引用解析单点归一:每项仅解析一次,后续各轮(白名单过滤、去重、频次统计、
271
+ // 工具命名)复用同一结果,避免同一列表对 splitActionRef 的三轮重复调用;
272
+ // 显式 packageId 与解析所得 packageId 分别留存,各轮按既有优先级消费
273
+ const parsedRefs = new Map();
260
274
  for (const act of rawActions) {
261
- let pkgId = act.packageId || "";
262
- let actId = act.id;
263
275
  if (act.id.includes("/")) {
264
- try {
265
- const parsed = ActionResolver.parseRef(act.id);
266
- pkgId = pkgId || parsed.packageId || "";
267
- actId = parsed.actionId;
268
- }
269
- catch {
270
- const idx = act.id.lastIndexOf("/");
271
- pkgId = pkgId || act.id.slice(0, idx);
272
- actId = act.id.slice(idx + 1);
273
- }
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 });
274
285
  }
275
- const key = `${pkgId}:${actId}`;
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}`;
276
307
  const existing = seenActionKeys.get(key);
277
308
  if (existing) {
278
309
  // 若已存在的项是全限定名(含 /),而当前项是短名(不含 /),优先保留短名项
@@ -288,35 +319,24 @@ export async function createActionDockMcpServer(options = {}) {
288
319
  // 统计 Action 基础 ID 出现频次,用于同名冲突命名空间隔离
289
320
  const baseCounts = new Map();
290
321
  for (const act of actions) {
291
- let baseId = act.id;
292
- if (act.id.includes("/")) {
293
- try {
294
- const parsed = ActionResolver.parseRef(act.id);
295
- baseId = parsed.actionId;
296
- }
297
- catch {
298
- const idx = act.id.lastIndexOf("/");
299
- baseId = act.id.slice(idx + 1);
300
- }
301
- }
322
+ const baseId = parsedRefs.get(act).baseId;
302
323
  baseCounts.set(baseId, (baseCounts.get(baseId) || 0) + 1);
303
324
  }
304
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("/"));
305
336
  for (const action of actions) {
306
- let baseId = action.id;
307
- let packageId = action.packageId;
308
- if (action.id.includes("/")) {
309
- try {
310
- const parsed = ActionResolver.parseRef(action.id);
311
- packageId = parsed.packageId || packageId;
312
- baseId = parsed.actionId;
313
- }
314
- catch {
315
- const idx = action.id.lastIndexOf("/");
316
- packageId = packageId || action.id.slice(0, idx);
317
- baseId = action.id.slice(idx + 1);
318
- }
319
- }
337
+ const ref = parsedRefs.get(action);
338
+ const baseId = ref.baseId;
339
+ const packageId = ref.parsedPkgId || action.packageId;
320
340
  const count = baseCounts.get(baseId) || 1;
321
341
  let toolName = baseId;
322
342
  if (count > 1 && packageId) {
@@ -335,11 +355,10 @@ export async function createActionDockMcpServer(options = {}) {
335
355
  throw toolNameCollisionError(toolName);
336
356
  }
337
357
  registeredToolNames.add(toolName);
338
- const isMultiPackage = actions.some((a) => a.id.includes("/") || (a.packageId && a.packageId !== actions[0].packageId));
339
358
  const description = isMultiPackage
340
359
  ? `[${action.id}] ${action.description || ""}`.trim()
341
360
  : action.description;
342
- // 工具执行:tools/call 委托 target.runAction() 或 target.startAction()
361
+ // 工具执行:tools/call 委托 service.execution.run() 或 service.execution.start()
343
362
  server.registerTool(toolName, {
344
363
  description,
345
364
  inputSchema: toMcpSchema(action.inputSchema),
@@ -350,7 +369,7 @@ export async function createActionDockMcpServer(options = {}) {
350
369
  }, async (input, ctx) => {
351
370
  // 异步执行模式只认显式约定字段 execution.mode,旧版 __async 仅作只读兼容探测
352
371
  const isAsync = isAsyncExecutionRequested(input);
353
- const signal = ctx.mcpReq?.signal;
372
+ const signal = extractCancelSignal(ctx);
354
373
  // 分发前仅剥离适配层注入的包装字段,业务自有字段(含名为 async 的入参)原样透传
355
374
  const cleanInput = stripExecutionWrapper(input);
356
375
  // 超时组合策略:客户端声明与服务端配置同时存在时取较小值(更防御),
@@ -360,7 +379,7 @@ export async function createActionDockMcpServer(options = {}) {
360
379
  ? Math.min(options.timeoutMs, clientTimeoutMs)
361
380
  : clientTimeoutMs ?? options.timeoutMs;
362
381
  if (isAsync) {
363
- const ticket = await target.startAction(action.id, cleanInput, {
382
+ const ticket = await service.execution.start(action.id, cleanInput, {
364
383
  signal,
365
384
  timeoutMs: effectiveTimeoutMs,
366
385
  });
@@ -378,7 +397,7 @@ export async function createActionDockMcpServer(options = {}) {
378
397
  ],
379
398
  };
380
399
  }
381
- const result = await target.runAction(action.id, cleanInput, {
400
+ const result = await service.execution.run(action.id, cleanInput, {
382
401
  signal,
383
402
  timeoutMs: effectiveTimeoutMs,
384
403
  });
@@ -388,14 +407,17 @@ export async function createActionDockMcpServer(options = {}) {
388
407
  // 任务规范映射(tasks/get、tasks/cancel、tasks/list)已收敛至 registerTasksExtension 隔离模块
389
408
  // 资源与规程映射:规程映射为只读 MCP Resource 与 Prompt
390
409
  try {
391
- const playbooks = await target.listPlaybooks();
410
+ let playbooks = await service.discovery.listPlaybooks();
411
+ if (allowedPackageIds && allowedPackageIds.length > 0) {
412
+ playbooks = playbooks.filter((pb) => pb.packageId && allowedPackageIds.includes(pb.packageId));
413
+ }
392
414
  for (const pb of playbooks) {
393
415
  server.registerResource(pb.id, `playbook://${pb.id}`, {
394
416
  title: pb.id,
395
417
  description: pb.description,
396
418
  mimeType: "text/markdown",
397
419
  }, async (uri) => {
398
- const spec = await target.describePlaybook(pb.id);
420
+ const spec = await service.discovery.describePlaybook(pb.id);
399
421
  return {
400
422
  contents: [
401
423
  {
@@ -410,7 +432,7 @@ export async function createActionDockMcpServer(options = {}) {
410
432
  title: pb.id,
411
433
  description: pb.description,
412
434
  }, async () => {
413
- const spec = await target.describePlaybook(pb.id);
435
+ const spec = await service.discovery.describePlaybook(pb.id);
414
436
  return {
415
437
  messages: [
416
438
  {
@@ -429,16 +451,16 @@ export async function createActionDockMcpServer(options = {}) {
429
451
  // 规程获取或注册失败不中断适配器启动,但输出诊断行保留排障线索
430
452
  console.error(`[actiondock-mcp] Failed to register playbook resources/prompts: ${err?.message || String(err)}`);
431
453
  }
432
- // 服务生命周期:默认 close 仅关闭 MCP 服务本身,不级联 target——
433
- // SDK 传输层(HTTP 每请求 / stdio 探测回落)会销毁工厂产物,若 close 级联会误杀共享 target;
434
- // 需要「一次 close 同时释放 target」的独立持有方显式传 cascadeTargetClose
454
+ // 服务生命周期:默认 close 仅关闭 MCP 服务本身,不级联 service——
455
+ // SDK 传输层(HTTP 每请求 / stdio 探测回落)会销毁工厂产物,若 close 级联会误杀共享 service;
456
+ // 需要「一次 close 同时释放 service」的独立持有方显式传 cascadeServiceClose
435
457
  const originalClose = server.close.bind(server);
436
458
  let isClosed = false;
437
459
  const closeFn = async () => {
438
460
  if (isClosed)
439
461
  return;
440
462
  isClosed = true;
441
- // 清理异常不吞没:先关服务再级联目标,server 关闭失败也继续释放 target 并聚合上抛
463
+ // 清理异常不吞没:先关服务再级联目标,server 关闭失败也继续释放 service 并聚合上抛
442
464
  let closeError;
443
465
  try {
444
466
  await originalClose();
@@ -446,13 +468,13 @@ export async function createActionDockMcpServer(options = {}) {
446
468
  catch (err) {
447
469
  closeError = err;
448
470
  }
449
- if (options.cascadeTargetClose) {
471
+ if (options.cascadeServiceClose) {
450
472
  try {
451
- await target.close();
473
+ await service.close();
452
474
  }
453
475
  catch (err) {
454
476
  if (closeError !== undefined) {
455
- throw new AggregateError([closeError, err], "Failed to close MCP server and target");
477
+ throw new AggregateError([closeError, err], "Failed to close MCP server and service");
456
478
  }
457
479
  throw err;
458
480
  }
@@ -463,9 +485,10 @@ export async function createActionDockMcpServer(options = {}) {
463
485
  };
464
486
  const decorated = server;
465
487
  decorated.close = closeFn;
466
- decorated.target = target;
488
+ decorated.service = service;
467
489
  decorated.host = options.host;
468
- decorated.app = options.app;
469
- decorated.events = (runId, opts) => target.events(runId, opts);
490
+ if (service.events) {
491
+ decorated.events = (runId, opts) => service.events.events(runId, opts);
492
+ }
470
493
  return decorated;
471
494
  }
package/dist/http.js CHANGED
@@ -1,6 +1,7 @@
1
- import { ACTIONDOCK_VERSION, DEFAULT_MAX_BODY_BYTES, formatHostForUrl, isLoopbackHost, launchHttpServer, resolveCorsHeaders, verifyBearerToken, UNAUTHORIZED, } from "@actiondock/core";
1
+ import { ACTIONDOCK_VERSION, UNAUTHORIZED } from "@actiondock/core";
2
+ import { createMcpEndpointHandler, formatHostForUrl, isLoopbackHost, launchHttpServer, resolveCorsHeaders, verifyBearerToken, } from "@actiondock/core/server";
2
3
  import { createMcpHandler } from "@modelcontextprotocol/server";
3
- import { createActionDockMcpServer, resolveTarget } from "./adapter.js";
4
+ import { createActionDockMcpServer, resolveService } from "./adapter.js";
4
5
  /**
5
6
  * Starts an ActionDock MCP server over HTTP transport.
6
7
  */
@@ -16,23 +17,52 @@ export function startMcpHttpServer(options = {}) {
16
17
  // 由 allowInsecureNoAuth 与 token 策略约束,传入该字段不会产生任何效果,
17
18
  // 检测到时输出警告避免调用方误以为已生效
18
19
  if (options.allowInsecureHttp === true) {
19
- process.stderr.write("[MCP HTTP Warning] allowInsecureHttp has no effect on the server side and is deprecated; it only applies to remote ActionDockTarget clients.\n");
20
+ process.stderr.write("[MCP HTTP Warning] allowInsecureHttp has no effect on the server side and is deprecated; it only applies to remote ActionDock clients.\n");
20
21
  }
21
22
  // Non-loopback address requires token authentication by default
22
23
  if (!isLoopbackHost(host) && !token && !options.allowInsecureNoAuth) {
23
24
  throw new Error("Authentication token is required when binding to a non-loopback address. Use --allow-insecure-no-auth to override.");
24
25
  }
25
26
  return (async () => {
26
- // 先一次性解析 target,工厂闭包直接捕获已解析产物,消除选项重复展开
27
- const { target } = await resolveTarget({
27
+ // 先一次性解析 service,工厂闭包直接捕获已解析产物,消除选项重复展开
28
+ const { service } = await resolveService({
28
29
  ...options,
29
30
  host: hostInstance ?? (typeof options.host === "object" ? options.host : undefined),
30
31
  });
31
- const handler = createMcpHandler(() => createActionDockMcpServer({ target }), {
32
+ const resolvedHost = hostInstance ?? (typeof options.host === "object" ? options.host : undefined);
33
+ const handler = createMcpHandler(() => createActionDockMcpServer({ ...options, host: resolvedHost, service }), {
32
34
  onerror: (err) => {
33
35
  process.stderr.write(`[MCP HTTP Error] ${err?.message || String(err)}\n`);
34
36
  },
35
37
  });
38
+ // JSON-RPC 形态的 401 响应构造:mcp 客户端按 JSON-RPC 错误信封消费鉴权失败
39
+ // (错误码 -32000),区别于 core 网关的标准 JSON 信封
40
+ const unauthorizedJsonRpcResponse = (corsHeaders) => new Response(JSON.stringify({
41
+ jsonrpc: "2.0",
42
+ error: {
43
+ code: -32000,
44
+ message: "Unauthorized: Invalid or missing Bearer token",
45
+ },
46
+ id: null,
47
+ }), {
48
+ status: 401,
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ ...corsHeaders,
52
+ },
53
+ });
54
+ // MCP 端点共享处理器:Bearer 鉴权(401 JSON-RPC 形态)→ 请求体有界读取
55
+ // (413 拦截)→ 重建 Request 委托 SDK 处理器 → CORS merge 回写。
56
+ // 序列收敛为 core 单一事实源,此处仅以定制钩子表达 mcp 独立服务的差异
57
+ const mcpEndpoint = createMcpEndpointHandler((req) => handler.fetch(req), {
58
+ token,
59
+ allowQueryToken: options.allowQueryToken,
60
+ corsOrigins: options.corsOrigins,
61
+ maxBodyBytes: options.maxBodyBytes,
62
+ // mcp 独立服务将解析出的 CORS 头合并覆盖到 MCP 响应(core 网关为 inherit 透传)
63
+ corsApplyMode: "merge",
64
+ unauthorizedResponse: unauthorizedJsonRpcResponse,
65
+ });
36
66
  const server = await launchHttpServer(port, host, async (req) => {
37
67
  const origin = req.headers.get("origin");
38
68
  const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
@@ -45,8 +75,8 @@ export function startMcpHttpServer(options = {}) {
45
75
  const url = new URL(req.url);
46
76
  const pathname = url.pathname;
47
77
  const verifyOptions = { allowQueryToken: options.allowQueryToken };
48
- // 1. Health check
49
- if (pathname === "/health" || pathname === "/api/v1/health") {
78
+ // 健康检查路由
79
+ if (pathname === "/health") {
50
80
  if (!verifyBearerToken(req, token, verifyOptions)) {
51
81
  return new Response(JSON.stringify({
52
82
  ok: false,
@@ -75,111 +105,13 @@ export function startMcpHttpServer(options = {}) {
75
105
  },
76
106
  });
77
107
  }
78
- // 2. Authentication check
79
- if (!verifyBearerToken(req, token, verifyOptions)) {
80
- return new Response(JSON.stringify({
81
- jsonrpc: "2.0",
82
- error: {
83
- code: -32000,
84
- message: "Unauthorized: Invalid or missing Bearer token",
85
- },
86
- id: null,
87
- }), {
88
- status: 401,
89
- headers: {
90
- "Content-Type": "application/json",
91
- ...corsHeaders,
92
- },
93
- });
94
- }
95
- // 3. Delegate MCP endpoint
108
+ // 委托 MCP 端点(/mcp 与 / 双路径,鉴权与限流由共享端点处理器承担)
96
109
  if (pathname === "/mcp" || pathname === "/") {
97
- const maxBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
98
- const contentLengthHeader = req.headers.get("content-length");
99
- if (contentLengthHeader) {
100
- const parsedLength = parseInt(contentLengthHeader, 10);
101
- if (!isNaN(parsedLength) && parsedLength > maxBytes) {
102
- return new Response(JSON.stringify({
103
- ok: false,
104
- error: {
105
- code: "REQUEST_TOO_LARGE",
106
- message: "Request body exceeds maximum allowed size",
107
- },
108
- }), {
109
- status: 413,
110
- headers: {
111
- "Content-Type": "application/json",
112
- ...corsHeaders,
113
- },
114
- });
115
- }
116
- }
117
- let currentReq = req;
118
- if (req.body && req.method !== "GET" && req.method !== "HEAD") {
119
- const reader = req.body.getReader();
120
- const chunks = [];
121
- let totalBytes = 0;
122
- let tooLarge = false;
123
- try {
124
- while (true) {
125
- const { done, value } = await reader.read();
126
- if (done)
127
- break;
128
- if (value) {
129
- totalBytes += value.byteLength;
130
- if (totalBytes > maxBytes) {
131
- tooLarge = true;
132
- await reader.cancel();
133
- break;
134
- }
135
- chunks.push(value);
136
- }
137
- }
138
- }
139
- finally {
140
- reader.releaseLock();
141
- }
142
- if (tooLarge) {
143
- return new Response(JSON.stringify({
144
- ok: false,
145
- error: {
146
- code: "REQUEST_TOO_LARGE",
147
- message: "Request body exceeds maximum allowed size",
148
- },
149
- }), {
150
- status: 413,
151
- headers: {
152
- "Content-Type": "application/json",
153
- ...corsHeaders,
154
- },
155
- });
156
- }
157
- const merged = new Uint8Array(totalBytes);
158
- let offset = 0;
159
- for (const chunk of chunks) {
160
- merged.set(chunk, offset);
161
- offset += chunk.byteLength;
162
- }
163
- currentReq = new Request(req.url, {
164
- method: req.method,
165
- headers: req.headers,
166
- body: merged,
167
- signal: req.signal,
168
- });
169
- }
170
- const mcpResponse = await handler.fetch(currentReq);
171
- if (Object.keys(corsHeaders).length > 0) {
172
- const newHeaders = new Headers(mcpResponse.headers);
173
- for (const [k, v] of Object.entries(corsHeaders)) {
174
- newHeaders.set(k, String(v));
175
- }
176
- return new Response(mcpResponse.body, {
177
- status: mcpResponse.status,
178
- statusText: mcpResponse.statusText,
179
- headers: newHeaders,
180
- });
181
- }
182
- return mcpResponse;
110
+ return (await mcpEndpoint(req));
111
+ }
112
+ // 其余路径统一鉴权拦截:未认证访问任意未知路径返回 401 而非 404
113
+ if (!verifyBearerToken(req, token, verifyOptions)) {
114
+ return unauthorizedJsonRpcResponse(corsHeaders);
183
115
  }
184
116
  return new Response(JSON.stringify({
185
117
  error: "Not Found",
@@ -201,10 +133,10 @@ export function startMcpHttpServer(options = {}) {
201
133
  port: server.port ?? port,
202
134
  host,
203
135
  url,
204
- target,
136
+ service,
205
137
  stop: async () => {
206
- // 先停 HTTP 服务(等待在途请求收尾)再释放 target:
207
- // 若先关 target,在途请求的后续 Action 调用会全部异常
138
+ // 先停 HTTP 服务(等待在途请求收尾)再释放 service:
139
+ // 若先关 service,在途请求的后续 Action 调用会全部异常
208
140
  let serverError;
209
141
  try {
210
142
  await server.stop(true);
@@ -213,13 +145,13 @@ export function startMcpHttpServer(options = {}) {
213
145
  serverError = err;
214
146
  }
215
147
  try {
216
- await target.close();
148
+ await service.close();
217
149
  }
218
- catch (targetErr) {
150
+ catch (serviceErr) {
219
151
  if (serverError) {
220
- throw new AggregateError([serverError, targetErr], "Failed to stop MCP HTTP server and target");
152
+ throw new AggregateError([serverError, serviceErr], "Failed to stop MCP HTTP server and service");
221
153
  }
222
- throw targetErr;
154
+ throw serviceErr;
223
155
  }
224
156
  if (serverError) {
225
157
  throw serverError;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
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";
4
5
  export * from "./adapter.js";
5
6
  export * from "./stdio.js";
6
7
  export * from "./http.js";
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
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";
4
6
  export * from "./adapter.js";
5
7
  export * from "./stdio.js";
6
8
  export * from "./http.js";
@@ -1,9 +1,34 @@
1
- import type { ActionDockTarget } from "@actiondock/core";
1
+ import type { ActionDockService } from "@actiondock/core";
2
+ import type { RunRecord, RunStatus } from "@actiondock/sdk";
2
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;
3
28
  /**
4
29
  * 在低层 Server 实例上注册 tasks 能力声明与三个请求处理器。
5
30
  *
6
31
  * @param server MCP 高层服务实例
7
- * @param target ActionDockTarget 公共契约门面
32
+ * @param service ActionDockService 公共服务实例
8
33
  */
9
- export declare function registerTasksExtension(server: McpServer, target: ActionDockTarget): void;
34
+ export declare function registerTasksExtension(server: McpServer, service: ActionDockService, allowedPackageIds?: string[], actionAllowlist?: string[]): void;
@@ -1,5 +1,38 @@
1
+ import { isActionAllowed } from "@actiondock/core/server";
1
2
  import { fromJsonSchema } from "@modelcontextprotocol/server";
2
- import { toMcpTaskPayload, toMcpTaskStatus } from "./types.js";
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
+ }
3
36
  /** JSON-RPC 保留码段:协议保留区为 -32000 至 -32099,服务端自定义错误码必须落在该区段内。 */
4
37
  const TASK_NOT_FOUND_CODE = -32001;
5
38
  /** 无效的 limit 参数错误码,同样落在服务端自定义码段。 */
@@ -53,9 +86,9 @@ function toTimeMillis(value) {
53
86
  * 在低层 Server 实例上注册 tasks 能力声明与三个请求处理器。
54
87
  *
55
88
  * @param server MCP 高层服务实例
56
- * @param target ActionDockTarget 公共契约门面
89
+ * @param service ActionDockService 公共服务实例
57
90
  */
58
- export function registerTasksExtension(server, target) {
91
+ export function registerTasksExtension(server, service, allowedPackageIds, actionAllowlist) {
59
92
  server.server.registerCapabilities({
60
93
  tasks: {
61
94
  list: {},
@@ -63,23 +96,51 @@ export function registerTasksExtension(server, target) {
63
96
  },
64
97
  });
65
98
  server.server.setRequestHandler("tasks/get", { params: TASK_ID_PARAMS }, async (params) => {
66
- const run = await target.getRun(params.taskId);
99
+ const run = await service.runs.get(params.taskId);
67
100
  if (!run) {
68
101
  // 携带语义码透传,避免被 SDK 映射为内部错误
69
102
  throw taskNotFoundError(params.taskId);
70
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
+ }
71
114
  return { task: toMcpTaskPayload(run) };
72
115
  });
73
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
+ }
74
135
  const reason = params.reason || "Cancelled via MCP tasks/cancel";
75
- const cancelRes = await target.cancelRun(params.taskId, reason);
136
+ const cancelRes = await service.runs.cancel(params.taskId, reason);
76
137
  if (cancelRes.outcome === "requested") {
77
138
  return { taskId: params.taskId, status: "cancelled" };
78
139
  }
79
140
  if (cancelRes.outcome === "already_terminal") {
80
141
  return { taskId: params.taskId, status: toMcpTaskStatus(cancelRes.status) };
81
142
  }
82
- const run = await target.getRun(params.taskId);
143
+ const run = await service.runs.get(params.taskId);
83
144
  if (!run) {
84
145
  // 携带语义码透传,避免被 SDK 映射为内部错误
85
146
  throw taskNotFoundError(params.taskId);
@@ -96,8 +157,14 @@ export function registerTasksExtension(server, target) {
96
157
  limit = Math.min(Math.max(params.limit, 1), MAX_TASK_LIST_LIMIT);
97
158
  }
98
159
  const actionId = params.actionId;
99
- // limit 单处截断:下推到 target.listRuns 的 limit 参数,此处不再重复 slice
100
- const runs = await target.listRuns({ limit, 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
+ }
101
168
  const ordered = [...runs].sort((a, b) => toTimeMillis(b.startedAt) - toTimeMillis(a.startedAt));
102
169
  return { tasks: ordered.map(toMcpTaskPayload) };
103
170
  });
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { serveParentIpc } from "@actiondock/core";
3
- import { resolveTarget } from "./adapter.js";
2
+ import { serveParentIpc } from "@actiondock/core/server";
3
+ import { resolveService } from "./adapter.js";
4
4
  try {
5
5
  const rawOptions = process.env.ACTIONDOCK_MCP_OPTIONS;
6
6
  const options = rawOptions ? JSON.parse(rawOptions) : {};
7
- const { target } = await resolveTarget(options);
8
- await serveParentIpc(target);
7
+ const { service } = await resolveService(options);
8
+ await serveParentIpc(service);
9
9
  }
10
10
  catch (err) {
11
11
  process.stderr.write(`[MCP Host Error] ${err?.stack || err?.message || String(err)}\n`);
package/dist/stdio.js CHANGED
@@ -2,9 +2,9 @@ import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { IpcActionDockTarget } from "@actiondock/core";
5
+ import { IpcActionDockService } from "@actiondock/core/server";
6
6
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
7
- import { createActionDockMcpServer, resolveTarget } from "./adapter.js";
7
+ import { createActionDockMcpServer, resolveService } from "./adapter.js";
8
8
  /**
9
9
  * 启动 ActionDock MCP STDIO 协议服务。
10
10
  * 建立监督进程物理边界:
@@ -15,11 +15,17 @@ import { createActionDockMcpServer, resolveTarget } from "./adapter.js";
15
15
  */
16
16
  export async function startMcpStdio(options = {}) {
17
17
  let activeServer;
18
- let targetToUse;
18
+ let serviceToUse;
19
+ let ownsService = false;
19
20
  let childProcess;
20
- // 若显式传入了已构造的 target 或不可序列化的内存对象,直接复用或解析目标实例
21
- if (options.target || options.actions || options.storage || options.app || options.host || options.platform) {
22
- targetToUse = options.target ?? (await resolveTarget(options)).target;
21
+ // 若显式传入了已构造的 service 或不可序列化的内存对象,直接复用或解析目标实例;
22
+ // ownsService 标记与 adapter 的所有权契约对齐:仅自建实例由本入口的 cleanup 收敛生命周期
23
+ if (options.service || options.actions || options.storage || options.runtime || options.host || options.platform) {
24
+ const resolved = options.service
25
+ ? { service: options.service, ownsService: false }
26
+ : await resolveService(options);
27
+ serviceToUse = resolved.service;
28
+ ownsService = resolved.ownsService;
23
29
  }
24
30
  else {
25
31
  // 建立隔离的监督子进程
@@ -44,20 +50,22 @@ export async function startMcpStdio(options = {}) {
44
50
  stdio: ["pipe", "pipe", "pipe", "ipc"],
45
51
  });
46
52
  // 标准输出物理隔离与受控限流排空转入 stderr 诊断流
47
- targetToUse = new IpcActionDockTarget({
53
+ serviceToUse = new IpcActionDockService({
48
54
  childProcess,
49
55
  maxDiagnosticBytes: 512 * 1024,
50
56
  maxDiagnosticRate: 64 * 1024,
51
57
  diagnosticTarget: process.stderr,
52
58
  });
59
+ // IPC 代理实例由本入口创建,生命周期归本入口的 cleanup 所有
60
+ ownsService = true;
53
61
  }
54
62
  const stdioHandler = serveStdio(async () => {
55
63
  const server = await createActionDockMcpServer({
56
64
  ...options,
57
- target: targetToUse,
65
+ service: serviceToUse,
58
66
  });
59
67
  activeServer = server;
60
- targetToUse = targetToUse || server.target;
68
+ serviceToUse = serviceToUse || server.service;
61
69
  return server;
62
70
  }, {
63
71
  onerror: (err) => {
@@ -80,9 +88,11 @@ export async function startMcpStdio(options = {}) {
80
88
  swallow(err);
81
89
  }
82
90
  }
83
- if (targetToUse) {
91
+ if (serviceToUse && ownsService) {
92
+ // 外部注入的 service(ownsService 为 false)生命周期归调用方,这里不越权关闭;
93
+ // 自建实例(含 IPC 子进程代理)由本入口统一收敛
84
94
  try {
85
- await targetToUse.close();
95
+ await serviceToUse.close();
86
96
  }
87
97
  catch (err) {
88
98
  swallow(err);
package/dist/types.d.ts CHANGED
@@ -1,40 +1,18 @@
1
- import type { ActionDockApp, ActionDockHost, ActionDockTarget, RuntimeStorage, ServerTlsOptions } from "@actiondock/core";
2
- import type { ActionDefinition, RunRecord, RunStatus } from "@actiondock/sdk";
3
- /**
4
- * MCP 任务状态枚举(兼容 Model Context Protocol Task 规范)。
5
- */
6
- export type McpTaskStatus = "working" | "completed" | "failed" | "cancelled";
7
- /**
8
- * MCP 任务状态数据载荷结构体。
9
- */
10
- export interface McpTaskPayload {
11
- taskId: string;
12
- status: McpTaskStatus;
13
- createdAt: string;
14
- finishedAt?: string;
15
- input?: unknown;
16
- output?: unknown;
17
- error?: unknown;
18
- }
19
- /**
20
- * 将 ActionDock 内部的 RunStatus 转换为 MCP 标准的 TaskStatus。
21
- */
22
- export declare function toMcpTaskStatus(status: RunStatus): McpTaskStatus;
23
- /**
24
- * 将内部 RunRecord 实体转换为向 MCP 客户端暴露的 McpTaskPayload。
25
- */
26
- export declare function toMcpTaskPayload(run: RunRecord): McpTaskPayload;
1
+ import type { ActionDockService } from "@actiondock/core";
2
+ import type { ActionDockHost, ServerTlsOptions } from "@actiondock/core/server";
3
+ import type { PackageRuntime, RuntimePlatform, RuntimeStorage } from "@actiondock/core/package";
4
+ import type { ActionDefinition } from "@actiondock/sdk";
27
5
  /**
28
6
  * ActionDock MCP 适配层初始化选项。
29
- * 统一以 target(及可选 host, app)为核心。
7
+ * 统一以 service(及可选 host, runtime)为核心。
30
8
  */
31
9
  export interface ActionDockMcpOptions {
32
- /** 目标 ActionDockTarget 门面实例(最高优先级) */
33
- target?: ActionDockTarget;
10
+ /** 目标 ActionDockService 标准服务端口实例 */
11
+ service?: ActionDockService;
34
12
  /** 目标 ActionDockHost 宿主实例 */
35
13
  host?: ActionDockHost;
36
- /** 目标 ActionDockApp 应用实例 */
37
- app?: ActionDockApp;
14
+ /** 目标 PackageRuntime 应用运行时实例 */
15
+ runtime?: PackageRuntime;
38
16
  /** 单个目标项目根目录 */
39
17
  projectRoot?: string;
40
18
  /** 多个项目根目录(用于多包聚合提供) */
@@ -43,6 +21,10 @@ export interface ActionDockMcpOptions {
43
21
  packageId?: string;
44
22
  /** 多个 Package ID 列表 */
45
23
  packageIds?: string[];
24
+ /** 允许访问执行的 Package ID 白名单列表(为空允许全部) */
25
+ packageAllowlist?: string[];
26
+ /** 允许暴露执行的 Action 标识白名单列表(为空允许全部,支持短名或 packageId/actionId) */
27
+ actionAllowlist?: string[];
46
28
  /** 是否聚合暴露全局 Registry 中的所有 Package */
47
29
  all?: boolean;
48
30
  /** 自定义家目录路径 */
@@ -50,7 +32,7 @@ export interface ActionDockMcpOptions {
50
32
  /** 自定义全局数据存储目录 */
51
33
  dataDir?: string;
52
34
  /** 运行时底层平台适配(如提供 NodeProcessDriver 的平台实例) */
53
- platform?: import("@actiondock/core").RuntimePlatform;
35
+ platform?: RuntimePlatform;
54
36
  /** 配置动态覆盖项 */
55
37
  configOverrides?: Record<string, unknown>;
56
38
  /** 单个 Tool 执行超时时间(毫秒) */
@@ -66,18 +48,20 @@ export interface ActionDockMcpOptions {
66
48
  storage?: RuntimeStorage;
67
49
  /**
68
50
  * 是否由适配层接管外部注入 storage 的生命周期。
69
- * 默认 false:外部注入的 storage 由注入方自行管理,适配层不具各 close 语义;
70
- * 置为 true 时透传原始实例,随 target.close() 级联关闭。
51
+ * 默认 false:外部注入的 storage 由注入方自行管理,适配层不具备 close 语义;
52
+ * 置为 true 时透传原始实例,随 service.close() 级联关闭。
71
53
  */
72
54
  ownStorageLifecycle?: boolean;
73
55
  /**
74
- * 实例 close 是否级联关闭 target 门面。
56
+ * 实例 close 是否级联关闭 service 门面。
75
57
  * 默认 false:适配层产物可能被 SDK 传输层按请求(或按连接)创建与销毁,
76
- * 级联语义会误杀共享 target,生命周期统一由外层入口的 stop/cleanup 收敛;
77
- * 仅在调用方自行持有实例并需要一次 close 同时释放 target 时置 true。
58
+ * 级联语义会误杀共享 service,生命周期统一由外层入口的 stop/cleanup 收敛;
59
+ * 仅在调用方自行持有实例并需要一次 close 同时释放 service 时置 true。
78
60
  */
79
- cascadeTargetClose?: boolean;
61
+ cascadeServiceClose?: boolean;
80
62
  }
63
+ /** ActionDock MCP 适配层初始化选项别名 */
64
+ export type ActionDockMcpServerOptions = ActionDockMcpOptions;
81
65
  /**
82
66
  * HTTP 传输协议安全配置项。
83
67
  */
@@ -87,11 +71,6 @@ export interface HttpSecurityOptions {
87
71
  token?: string;
88
72
  allowInsecureNoAuth?: boolean;
89
73
  allowQueryToken?: boolean;
90
- /**
91
- * 已废弃:该选项仅适用于远程 ActionDockTarget 客户端侧的明文 HTTP 豁免,
92
- * 服务端监听安全性由 allowInsecureNoAuth 与 token 策略约束,传入不会产生任何效果;
93
- * 服务端检测到该字段时仅输出警告日志
94
- */
95
74
  allowInsecureHttp?: boolean;
96
75
  corsOrigins?: string[];
97
76
  maxBodyBytes?: number;
@@ -105,6 +84,6 @@ export interface ActionDockMcpHttpServerInstance {
105
84
  port: number;
106
85
  host: string;
107
86
  url: string;
108
- target?: ActionDockTarget;
87
+ service: ActionDockService;
109
88
  stop: () => Promise<void>;
110
89
  }
package/dist/types.js CHANGED
@@ -1,33 +1 @@
1
- /**
2
- * 将 ActionDock 内部的 RunStatus 转换为 MCP 标准的 TaskStatus。
3
- */
4
- export function toMcpTaskStatus(status) {
5
- switch (status) {
6
- case "running":
7
- return "working";
8
- case "success":
9
- return "completed";
10
- case "failed":
11
- case "timed_out":
12
- case "interrupted":
13
- return "failed";
14
- case "cancelled":
15
- return "cancelled";
16
- default:
17
- return "failed";
18
- }
19
- }
20
- /**
21
- * 将内部 RunRecord 实体转换为向 MCP 客户端暴露的 McpTaskPayload。
22
- */
23
- export function toMcpTaskPayload(run) {
24
- return {
25
- taskId: run.id,
26
- status: toMcpTaskStatus(run.status),
27
- createdAt: run.startedAt,
28
- finishedAt: run.finishedAt,
29
- input: run.input,
30
- output: run.output,
31
- error: run.error,
32
- };
33
- }
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actiondock/mcp",
3
- "version": "2.7.0",
3
+ "version": "2.9.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,18 +29,10 @@
29
29
  "test": "node ../../scripts/run-tests.ts"
30
30
  },
31
31
  "dependencies": {
32
- "@actiondock/core": "^2.7.0",
33
- "@actiondock/sdk": "^2.7.0",
32
+ "@actiondock/core": "^2.9.0",
33
+ "@actiondock/sdk": "^2.9.0",
34
34
  "@modelcontextprotocol/server": "^2.0.0"
35
35
  },
36
- "peerDependencies": {
37
- "@actiondock/runtime-node": "^2.7.0"
38
- },
39
- "peerDependenciesMeta": {
40
- "@actiondock/runtime-node": {
41
- "optional": true
42
- }
43
- },
44
36
  "devDependencies": {
45
37
  "typescript": "^5.7.0"
46
38
  },