@actiondock/mcp 2.7.0 → 2.8.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 异步任务映射扩展
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, 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,39 @@ 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);
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
+ });
274
282
  }
275
- const key = `${pkgId}:${actId}`;
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
+ const seenActionKeys = new Map();
296
+ for (const act of rawActions) {
297
+ const ref = parsedRefs.get(act);
298
+ const pkgId = ref.explicitPkgId || ref.parsedPkgId || "";
299
+ const key = `${pkgId}:${ref.baseId}`;
276
300
  const existing = seenActionKeys.get(key);
277
301
  if (existing) {
278
302
  // 若已存在的项是全限定名(含 /),而当前项是短名(不含 /),优先保留短名项
@@ -288,35 +312,24 @@ export async function createActionDockMcpServer(options = {}) {
288
312
  // 统计 Action 基础 ID 出现频次,用于同名冲突命名空间隔离
289
313
  const baseCounts = new Map();
290
314
  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
- }
315
+ const baseId = parsedRefs.get(act).baseId;
302
316
  baseCounts.set(baseId, (baseCounts.get(baseId) || 0) + 1);
303
317
  }
304
318
  const registeredToolNames = new Set();
319
+ // 多包判定(循环外一次算清):不同 packageId 去重计数大于 1,
320
+ // 或任一 id 含斜杠(跨包限定名形态),则工具描述需附全限定 id 锚点;
321
+ // 本轮与工具命名轮一致:解析所得 packageId 优先,缺失时回退显式声明
322
+ const distinctPackageIds = new Set(actions
323
+ .map((a) => {
324
+ const ref = parsedRefs.get(a);
325
+ return ref.parsedPkgId || a.packageId;
326
+ })
327
+ .filter((pkg) => Boolean(pkg)));
328
+ const isMultiPackage = distinctPackageIds.size > 1 || actions.some((a) => a.id.includes("/"));
305
329
  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
- }
330
+ const ref = parsedRefs.get(action);
331
+ const baseId = ref.baseId;
332
+ const packageId = ref.parsedPkgId || action.packageId;
320
333
  const count = baseCounts.get(baseId) || 1;
321
334
  let toolName = baseId;
322
335
  if (count > 1 && packageId) {
@@ -335,11 +348,10 @@ export async function createActionDockMcpServer(options = {}) {
335
348
  throw toolNameCollisionError(toolName);
336
349
  }
337
350
  registeredToolNames.add(toolName);
338
- const isMultiPackage = actions.some((a) => a.id.includes("/") || (a.packageId && a.packageId !== actions[0].packageId));
339
351
  const description = isMultiPackage
340
352
  ? `[${action.id}] ${action.description || ""}`.trim()
341
353
  : action.description;
342
- // 工具执行:tools/call 委托 target.runAction() 或 target.startAction()
354
+ // 工具执行:tools/call 委托 service.execution.run() 或 service.execution.start()
343
355
  server.registerTool(toolName, {
344
356
  description,
345
357
  inputSchema: toMcpSchema(action.inputSchema),
@@ -350,7 +362,7 @@ export async function createActionDockMcpServer(options = {}) {
350
362
  }, async (input, ctx) => {
351
363
  // 异步执行模式只认显式约定字段 execution.mode,旧版 __async 仅作只读兼容探测
352
364
  const isAsync = isAsyncExecutionRequested(input);
353
- const signal = ctx.mcpReq?.signal;
365
+ const signal = extractCancelSignal(ctx);
354
366
  // 分发前仅剥离适配层注入的包装字段,业务自有字段(含名为 async 的入参)原样透传
355
367
  const cleanInput = stripExecutionWrapper(input);
356
368
  // 超时组合策略:客户端声明与服务端配置同时存在时取较小值(更防御),
@@ -360,7 +372,7 @@ export async function createActionDockMcpServer(options = {}) {
360
372
  ? Math.min(options.timeoutMs, clientTimeoutMs)
361
373
  : clientTimeoutMs ?? options.timeoutMs;
362
374
  if (isAsync) {
363
- const ticket = await target.startAction(action.id, cleanInput, {
375
+ const ticket = await service.execution.start(action.id, cleanInput, {
364
376
  signal,
365
377
  timeoutMs: effectiveTimeoutMs,
366
378
  });
@@ -378,7 +390,7 @@ export async function createActionDockMcpServer(options = {}) {
378
390
  ],
379
391
  };
380
392
  }
381
- const result = await target.runAction(action.id, cleanInput, {
393
+ const result = await service.execution.run(action.id, cleanInput, {
382
394
  signal,
383
395
  timeoutMs: effectiveTimeoutMs,
384
396
  });
@@ -388,14 +400,17 @@ export async function createActionDockMcpServer(options = {}) {
388
400
  // 任务规范映射(tasks/get、tasks/cancel、tasks/list)已收敛至 registerTasksExtension 隔离模块
389
401
  // 资源与规程映射:规程映射为只读 MCP Resource 与 Prompt
390
402
  try {
391
- const playbooks = await target.listPlaybooks();
403
+ let playbooks = await service.discovery.listPlaybooks();
404
+ if (allowedPackageIds && allowedPackageIds.length > 0) {
405
+ playbooks = playbooks.filter((pb) => pb.packageId && allowedPackageIds.includes(pb.packageId));
406
+ }
392
407
  for (const pb of playbooks) {
393
408
  server.registerResource(pb.id, `playbook://${pb.id}`, {
394
409
  title: pb.id,
395
410
  description: pb.description,
396
411
  mimeType: "text/markdown",
397
412
  }, async (uri) => {
398
- const spec = await target.describePlaybook(pb.id);
413
+ const spec = await service.discovery.describePlaybook(pb.id);
399
414
  return {
400
415
  contents: [
401
416
  {
@@ -410,7 +425,7 @@ export async function createActionDockMcpServer(options = {}) {
410
425
  title: pb.id,
411
426
  description: pb.description,
412
427
  }, async () => {
413
- const spec = await target.describePlaybook(pb.id);
428
+ const spec = await service.discovery.describePlaybook(pb.id);
414
429
  return {
415
430
  messages: [
416
431
  {
@@ -429,16 +444,16 @@ export async function createActionDockMcpServer(options = {}) {
429
444
  // 规程获取或注册失败不中断适配器启动,但输出诊断行保留排障线索
430
445
  console.error(`[actiondock-mcp] Failed to register playbook resources/prompts: ${err?.message || String(err)}`);
431
446
  }
432
- // 服务生命周期:默认 close 仅关闭 MCP 服务本身,不级联 target——
433
- // SDK 传输层(HTTP 每请求 / stdio 探测回落)会销毁工厂产物,若 close 级联会误杀共享 target;
434
- // 需要「一次 close 同时释放 target」的独立持有方显式传 cascadeTargetClose
447
+ // 服务生命周期:默认 close 仅关闭 MCP 服务本身,不级联 service——
448
+ // SDK 传输层(HTTP 每请求 / stdio 探测回落)会销毁工厂产物,若 close 级联会误杀共享 service;
449
+ // 需要「一次 close 同时释放 service」的独立持有方显式传 cascadeServiceClose
435
450
  const originalClose = server.close.bind(server);
436
451
  let isClosed = false;
437
452
  const closeFn = async () => {
438
453
  if (isClosed)
439
454
  return;
440
455
  isClosed = true;
441
- // 清理异常不吞没:先关服务再级联目标,server 关闭失败也继续释放 target 并聚合上抛
456
+ // 清理异常不吞没:先关服务再级联目标,server 关闭失败也继续释放 service 并聚合上抛
442
457
  let closeError;
443
458
  try {
444
459
  await originalClose();
@@ -446,13 +461,13 @@ export async function createActionDockMcpServer(options = {}) {
446
461
  catch (err) {
447
462
  closeError = err;
448
463
  }
449
- if (options.cascadeTargetClose) {
464
+ if (options.cascadeServiceClose) {
450
465
  try {
451
- await target.close();
466
+ await service.close();
452
467
  }
453
468
  catch (err) {
454
469
  if (closeError !== undefined) {
455
- throw new AggregateError([closeError, err], "Failed to close MCP server and target");
470
+ throw new AggregateError([closeError, err], "Failed to close MCP server and service");
456
471
  }
457
472
  throw err;
458
473
  }
@@ -463,9 +478,10 @@ export async function createActionDockMcpServer(options = {}) {
463
478
  };
464
479
  const decorated = server;
465
480
  decorated.close = closeFn;
466
- decorated.target = target;
481
+ decorated.service = service;
467
482
  decorated.host = options.host;
468
- decorated.app = options.app;
469
- decorated.events = (runId, opts) => target.events(runId, opts);
483
+ if (service.events) {
484
+ decorated.events = (runId, opts) => service.events.events(runId, opts);
485
+ }
470
486
  return decorated;
471
487
  }
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[]): void;
@@ -1,5 +1,37 @@
1
1
  import { fromJsonSchema } from "@modelcontextprotocol/server";
2
- import { toMcpTaskPayload, toMcpTaskStatus } from "./types.js";
2
+ /**
3
+ * 将 ActionDock 内部的 RunStatus 转换为 MCP 标准的 TaskStatus。
4
+ */
5
+ export function toMcpTaskStatus(status) {
6
+ switch (status) {
7
+ case "running":
8
+ return "working";
9
+ case "success":
10
+ return "completed";
11
+ case "failed":
12
+ case "timed_out":
13
+ case "interrupted":
14
+ return "failed";
15
+ case "cancelled":
16
+ return "cancelled";
17
+ default:
18
+ return "failed";
19
+ }
20
+ }
21
+ /**
22
+ * 将内部 RunRecord 实体转换为向 MCP 客户端暴露的 McpTaskPayload。
23
+ */
24
+ export function toMcpTaskPayload(run) {
25
+ return {
26
+ taskId: run.id,
27
+ status: toMcpTaskStatus(run.status),
28
+ createdAt: run.startedAt,
29
+ finishedAt: run.finishedAt,
30
+ input: run.input,
31
+ output: run.output,
32
+ error: run.error,
33
+ };
34
+ }
3
35
  /** JSON-RPC 保留码段:协议保留区为 -32000 至 -32099,服务端自定义错误码必须落在该区段内。 */
4
36
  const TASK_NOT_FOUND_CODE = -32001;
5
37
  /** 无效的 limit 参数错误码,同样落在服务端自定义码段。 */
@@ -53,9 +85,9 @@ function toTimeMillis(value) {
53
85
  * 在低层 Server 实例上注册 tasks 能力声明与三个请求处理器。
54
86
  *
55
87
  * @param server MCP 高层服务实例
56
- * @param target ActionDockTarget 公共契约门面
88
+ * @param service ActionDockService 公共服务实例
57
89
  */
58
- export function registerTasksExtension(server, target) {
90
+ export function registerTasksExtension(server, service, allowedPackageIds) {
59
91
  server.server.registerCapabilities({
60
92
  tasks: {
61
93
  list: {},
@@ -63,23 +95,36 @@ export function registerTasksExtension(server, target) {
63
95
  },
64
96
  });
65
97
  server.server.setRequestHandler("tasks/get", { params: TASK_ID_PARAMS }, async (params) => {
66
- const run = await target.getRun(params.taskId);
98
+ const run = await service.runs.get(params.taskId);
67
99
  if (!run) {
68
100
  // 携带语义码透传,避免被 SDK 映射为内部错误
69
101
  throw taskNotFoundError(params.taskId);
70
102
  }
103
+ if (allowedPackageIds &&
104
+ allowedPackageIds.length > 0 &&
105
+ (!run.packageId || !allowedPackageIds.includes(run.packageId))) {
106
+ throw taskNotFoundError(params.taskId);
107
+ }
71
108
  return { task: toMcpTaskPayload(run) };
72
109
  });
73
110
  server.server.setRequestHandler("tasks/cancel", { params: TASK_ID_PARAMS }, async (params) => {
111
+ if (allowedPackageIds && allowedPackageIds.length > 0) {
112
+ const existingRun = await service.runs.get(params.taskId);
113
+ if (!existingRun ||
114
+ !existingRun.packageId ||
115
+ !allowedPackageIds.includes(existingRun.packageId)) {
116
+ throw taskNotFoundError(params.taskId);
117
+ }
118
+ }
74
119
  const reason = params.reason || "Cancelled via MCP tasks/cancel";
75
- const cancelRes = await target.cancelRun(params.taskId, reason);
120
+ const cancelRes = await service.runs.cancel(params.taskId, reason);
76
121
  if (cancelRes.outcome === "requested") {
77
122
  return { taskId: params.taskId, status: "cancelled" };
78
123
  }
79
124
  if (cancelRes.outcome === "already_terminal") {
80
125
  return { taskId: params.taskId, status: toMcpTaskStatus(cancelRes.status) };
81
126
  }
82
- const run = await target.getRun(params.taskId);
127
+ const run = await service.runs.get(params.taskId);
83
128
  if (!run) {
84
129
  // 携带语义码透传,避免被 SDK 映射为内部错误
85
130
  throw taskNotFoundError(params.taskId);
@@ -96,8 +141,11 @@ export function registerTasksExtension(server, target) {
96
141
  limit = Math.min(Math.max(params.limit, 1), MAX_TASK_LIST_LIMIT);
97
142
  }
98
143
  const actionId = params.actionId;
99
- // limit 单处截断:下推到 target.listRuns 的 limit 参数,此处不再重复 slice
100
- const runs = await target.listRuns({ limit, actionId });
144
+ // limit 单处截断:下推到 service.runs.list 的 limit 参数,此处不再重复 slice
145
+ let runs = await service.runs.list({ limit, actionId });
146
+ if (allowedPackageIds && allowedPackageIds.length > 0) {
147
+ runs = runs.filter((r) => Boolean(r.packageId && allowedPackageIds.includes(r.packageId)));
148
+ }
101
149
  const ordered = [...runs].sort((a, b) => toTimeMillis(b.startedAt) - toTimeMillis(a.startedAt));
102
150
  return { tasks: ordered.map(toMcpTaskPayload) };
103
151
  });
@@ -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,8 @@ export interface ActionDockMcpOptions {
43
21
  packageId?: string;
44
22
  /** 多个 Package ID 列表 */
45
23
  packageIds?: string[];
24
+ /** 允许访问执行的 Package ID 白名单列表(为空允许全部) */
25
+ packageAllowlist?: string[];
46
26
  /** 是否聚合暴露全局 Registry 中的所有 Package */
47
27
  all?: boolean;
48
28
  /** 自定义家目录路径 */
@@ -50,7 +30,7 @@ export interface ActionDockMcpOptions {
50
30
  /** 自定义全局数据存储目录 */
51
31
  dataDir?: string;
52
32
  /** 运行时底层平台适配(如提供 NodeProcessDriver 的平台实例) */
53
- platform?: import("@actiondock/core").RuntimePlatform;
33
+ platform?: RuntimePlatform;
54
34
  /** 配置动态覆盖项 */
55
35
  configOverrides?: Record<string, unknown>;
56
36
  /** 单个 Tool 执行超时时间(毫秒) */
@@ -66,17 +46,17 @@ export interface ActionDockMcpOptions {
66
46
  storage?: RuntimeStorage;
67
47
  /**
68
48
  * 是否由适配层接管外部注入 storage 的生命周期。
69
- * 默认 false:外部注入的 storage 由注入方自行管理,适配层不具各 close 语义;
70
- * 置为 true 时透传原始实例,随 target.close() 级联关闭。
49
+ * 默认 false:外部注入的 storage 由注入方自行管理,适配层不具备 close 语义;
50
+ * 置为 true 时透传原始实例,随 service.close() 级联关闭。
71
51
  */
72
52
  ownStorageLifecycle?: boolean;
73
53
  /**
74
- * 实例 close 是否级联关闭 target 门面。
54
+ * 实例 close 是否级联关闭 service 门面。
75
55
  * 默认 false:适配层产物可能被 SDK 传输层按请求(或按连接)创建与销毁,
76
- * 级联语义会误杀共享 target,生命周期统一由外层入口的 stop/cleanup 收敛;
77
- * 仅在调用方自行持有实例并需要一次 close 同时释放 target 时置 true。
56
+ * 级联语义会误杀共享 service,生命周期统一由外层入口的 stop/cleanup 收敛;
57
+ * 仅在调用方自行持有实例并需要一次 close 同时释放 service 时置 true。
78
58
  */
79
- cascadeTargetClose?: boolean;
59
+ cascadeServiceClose?: boolean;
80
60
  }
81
61
  /**
82
62
  * HTTP 传输协议安全配置项。
@@ -87,11 +67,6 @@ export interface HttpSecurityOptions {
87
67
  token?: string;
88
68
  allowInsecureNoAuth?: boolean;
89
69
  allowQueryToken?: boolean;
90
- /**
91
- * 已废弃:该选项仅适用于远程 ActionDockTarget 客户端侧的明文 HTTP 豁免,
92
- * 服务端监听安全性由 allowInsecureNoAuth 与 token 策略约束,传入不会产生任何效果;
93
- * 服务端检测到该字段时仅输出警告日志
94
- */
95
70
  allowInsecureHttp?: boolean;
96
71
  corsOrigins?: string[];
97
72
  maxBodyBytes?: number;
@@ -105,6 +80,6 @@ export interface ActionDockMcpHttpServerInstance {
105
80
  port: number;
106
81
  host: string;
107
82
  url: string;
108
- target?: ActionDockTarget;
83
+ service: ActionDockService;
109
84
  stop: () => Promise<void>;
110
85
  }
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.8.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.8.0",
33
+ "@actiondock/sdk": "^2.8.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
  },