@actiondock/mcp 2.19.0 → 2.21.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/dist/adapter.d.ts CHANGED
@@ -44,5 +44,7 @@ export type ActionDockMcpServer = McpServer & {
44
44
  };
45
45
  /**
46
46
  * 创建并配置基于 ActionDockService 的 McpServer 适配层实例。
47
+ * 在任何资源创建或包装载之前,先以 core 统一校验入口校验两份白名单声明:
48
+ * 数组结构、非法包标识、动作短名与通配符均 fail closed,违规抛 INVALID_ARGUMENT。
47
49
  */
48
50
  export declare function createActionDockMcpServer(options?: ActionDockMcpOptions): Promise<ActionDockMcpServer>;
package/dist/adapter.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ACTIONDOCK_VERSION } from "@actiondock/core";
2
2
  import { McpServer } from "@modelcontextprotocol/server";
3
+ import { assertServerPolicyValid, isPackageAllowedByPolicy, } from "@actiondock/core/server";
3
4
  import { toMcpSchema } from "./schemas.js";
4
5
  import { resolveExecutionTimeout } from "./execution-mode.js";
5
6
  import { resolveService } from "./service-resolver.js";
@@ -65,23 +66,26 @@ export function toMcpResult(result) {
65
66
  }
66
67
  /**
67
68
  * 创建并配置基于 ActionDockService 的 McpServer 适配层实例。
69
+ * 在任何资源创建或包装载之前,先以 core 统一校验入口校验两份白名单声明:
70
+ * 数组结构、非法包标识、动作短名与通配符均 fail closed,违规抛 INVALID_ARGUMENT。
68
71
  */
69
72
  export async function createActionDockMcpServer(options = {}) {
73
+ // 授权声明前置校验(单一事实源:core assertServerPolicyValid),
74
+ // 覆盖 service/host/runtime/projectRoot/actions 等全部注入路径
75
+ assertServerPolicyValid(options, "mcp options");
70
76
  const { service } = await resolveService(options);
71
- const allowedPackageIds = options.packageAllowlist && options.packageAllowlist.length > 0
72
- ? options.packageAllowlist
73
- : options.packageIds && options.packageIds.length > 0
74
- ? options.packageIds
75
- : options.packageId
76
- ? [options.packageId]
77
- : undefined;
77
+ // 统一授权策略单一事实源:以 core EffectiveServerPolicy 表达两份白名单并集,
78
+ // 复用 core 策略函数判定,适配层不复制授权逻辑
79
+ const policy = {
80
+ packageAllowlist: options.packageAllowlist,
81
+ actionAllowlist: options.actionAllowlist,
82
+ };
78
83
  let serverName = "actiondock";
79
84
  let serverVersion = ACTIONDOCK_VERSION;
80
85
  try {
81
86
  let packages = await service.info();
82
- if (allowedPackageIds && allowedPackageIds.length > 0) {
83
- packages = packages.filter((p) => p.id && allowedPackageIds.includes(p.id));
84
- }
87
+ // 包元数据按整包授权可见性收敛(含 * 全授权),单动作授权不推导整包身份
88
+ packages = packages.filter((p) => isPackageAllowedByPolicy(p.id, policy));
85
89
  if (packages && packages.length === 1) {
86
90
  serverName = packages[0].name || packages[0].id || serverName;
87
91
  serverVersion = packages[0].version || serverVersion;
@@ -90,13 +94,15 @@ export async function createActionDockMcpServer(options = {}) {
90
94
  catch {
91
95
  // 忽略元数据读取失败,使用默认值
92
96
  }
97
+ // 即使当前授权集合为空,tools/list 也应明确返回空列表而非未注册方法。
93
98
  const server = new McpServer({
94
99
  name: serverName,
95
100
  version: serverVersion,
96
- });
97
- // 工具注册与模式映射:tools/list 纯粹委托 service.discovery.listActions()
101
+ }, { capabilities: { tools: {} } });
102
+ // 工具注册与模式映射:tools/list 纯粹委托 service.discovery.listActions();
103
+ // 过滤仅用单一 policy 并集判定,不先按包选择截断单动作授权目标
98
104
  let rawActions = await service.discovery.listActions();
99
- const mappedActions = mapAndFilterActions(rawActions, allowedPackageIds, options.actionAllowlist);
105
+ const mappedActions = mapAndFilterActions(rawActions, undefined, undefined, policy);
100
106
  for (const { toolName, action, description } of mappedActions) {
101
107
  // 工具执行:tools/call 委托 service.execution.run()
102
108
  server.registerTool(toolName, {
@@ -116,12 +122,12 @@ export async function createActionDockMcpServer(options = {}) {
116
122
  return toMcpResult(result);
117
123
  });
118
124
  }
119
- // 资源与规程映射:规程映射为只读 MCP Resource 与 Prompt
125
+ // 资源与规程映射:规程映射为只读 MCP Resource 与 Prompt。
126
+ // 仅整包授权(或 * 全授权)的包开放对应规程;
127
+ // 单动作授权(如 B/read)不得借资源或提示获取该包完整规程(防泄露)
120
128
  try {
121
129
  let playbooks = await service.discovery.listPlaybooks();
122
- if (allowedPackageIds && allowedPackageIds.length > 0) {
123
- playbooks = playbooks.filter((pb) => pb.packageId && allowedPackageIds.includes(pb.packageId));
124
- }
130
+ playbooks = playbooks.filter((pb) => isPackageAllowedByPolicy(pb.packageId, policy));
125
131
  for (const pb of playbooks) {
126
132
  server.registerResource(pb.id, `playbook://${pb.id}`, {
127
133
  title: pb.id,
@@ -1,13 +1,18 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { ACTIONDOCK_VERSION, ActionDockError, createActionDock, createNodePlatform, findProjectRoot, PACKAGE_NOT_FOUND, PROJECT_ROOT_NOT_FOUND, } from "@actiondock/core";
4
- import { createActionDockHost } from "@actiondock/core/server";
3
+ import { ACTIONDOCK_VERSION, ActionDockError, createActionDock, createNodePlatform, findProjectRoot, loadProjectConfig, PACKAGE_NOT_FOUND, PROJECT_ROOT_NOT_FOUND, } from "@actiondock/core";
4
+ import { assertServerPolicyValid, createActionDockHost } from "@actiondock/core/server";
5
+ import { parseActionRef } from "@actiondock/core/graph";
5
6
  import { resolvePackageRoot } from "@actiondock/core/registry";
6
7
  import { createNonClosingStorageView } from "@actiondock/core/package";
7
8
  /**
8
9
  * 解析或基于选项创建底层 ActionDockService 统一门面。
9
10
  */
10
11
  export async function resolveService(options) {
12
+ // 授权声明前置校验(单一事实源:core assertServerPolicyValid):
13
+ // 在任何资源创建、包装载或注入实例复用之前执行,
14
+ // 覆盖 service/host/runtime 直接调用 resolveService 的入口
15
+ assertServerPolicyValid(options, "mcp options");
11
16
  if (options.service) {
12
17
  return { service: options.service, ownsService: false };
13
18
  }
@@ -63,9 +68,65 @@ export async function resolveService(options) {
63
68
  });
64
69
  }
65
70
  }
66
- const targetPackageIds = options.packageIds || options.packageAllowlist;
67
- if (targetPackageIds && targetPackageIds.length > 0) {
68
- for (const pkgId of targetPackageIds) {
71
+ // 单一 projectRoot 分支提前解析并统一登记已声明包身份,
72
+ // 与 projectRoots/内存 actions 包共同构成“无需额外 registry 解析”的身份集合
73
+ let projectRoot = options.projectRoot;
74
+ if (projectRoot) {
75
+ const abs = resolve(projectRoot);
76
+ const detected = findProjectRoot(abs);
77
+ if (!detected) {
78
+ throw new ActionDockError(PROJECT_ROOT_NOT_FOUND, `Project root '${options.projectRoot}' is not a valid ActionDock package (actiondock.json not found)`);
79
+ }
80
+ projectRoot = detected;
81
+ packages.push({
82
+ ...basePkgOpts,
83
+ packageRoot: detected,
84
+ });
85
+ }
86
+ // 统一收集全部已声明来源的包身份(projectRoot、projectRoots、内存 actions 包)。
87
+ // 已声明来源(projectRoot、projectRoots、内存 actions 包)身份优先登记,
88
+ // 额外授权目标在查 registry 前先比对身份集合:
89
+ // 已声明身份无需重复装载,避免本地项目未 link 时误走 registry 抛 PACKAGE_NOT_FOUND
90
+ const declaredIds = new Set();
91
+ const collectDeclaredId = (packageRoot) => {
92
+ if (!packageRoot)
93
+ return;
94
+ const id = loadProjectConfig(packageRoot).id;
95
+ if (id)
96
+ declaredIds.add(id);
97
+ };
98
+ if (projectRoot) {
99
+ collectDeclaredId(projectRoot);
100
+ }
101
+ for (const pkg of packages) {
102
+ if (typeof pkg === "object" && pkg !== null && "packageRoot" in pkg) {
103
+ collectDeclaredId(pkg.packageRoot);
104
+ }
105
+ }
106
+ if (options.actions) {
107
+ declaredIds.add(options.packageId || "default");
108
+ }
109
+ // 附加装载目标为两份白名单的并集:整包授权包与单动作授权引用的所属包均需装载,
110
+ // 仅装载整包授权包会导致单动作授权目标缺失而不可用。
111
+ // * 是全授权标记而非真实包,必须跳过;动作引用用 core parseActionRef 单一事实源解析。
112
+ // packageId/packageIds 是装载选择,不与授权目标混同:显式授权 B/read 必须装载 B。
113
+ const allowedPackageIds = [
114
+ ...(options.packageId ? [options.packageId] : []),
115
+ ...(options.packageIds || []),
116
+ ...(options.packageAllowlist || []).filter((id) => id !== "*"),
117
+ ...(options.actionAllowlist || []).map((ref) => parseActionRef(ref).packageId),
118
+ ];
119
+ const dedupedTargetPackageIds = [];
120
+ for (const pkgId of allowedPackageIds) {
121
+ if (declaredIds.has(pkgId)) {
122
+ continue;
123
+ }
124
+ if (!dedupedTargetPackageIds.includes(pkgId)) {
125
+ dedupedTargetPackageIds.push(pkgId);
126
+ }
127
+ }
128
+ if (dedupedTargetPackageIds.length > 0) {
129
+ for (const pkgId of dedupedTargetPackageIds) {
69
130
  const root = resolvePackageRoot(pkgId, undefined, options.customHome);
70
131
  if (!root || !existsSync(root)) {
71
132
  throw new ActionDockError(PACKAGE_NOT_FOUND, `Package '${pkgId}' not found in registry`);
@@ -76,15 +137,6 @@ export async function resolveService(options) {
76
137
  });
77
138
  }
78
139
  }
79
- let projectRoot = options.projectRoot;
80
- if (options.projectRoot) {
81
- const abs = resolve(options.projectRoot);
82
- const detected = findProjectRoot(abs);
83
- if (!detected) {
84
- throw new ActionDockError(PROJECT_ROOT_NOT_FOUND, `Project root '${options.projectRoot}' is not a valid ActionDock package (actiondock.json not found)`);
85
- }
86
- projectRoot = detected;
87
- }
88
140
  if (!projectRoot &&
89
141
  packages.length === 0 &&
90
142
  !options.all &&
@@ -95,17 +147,6 @@ export async function resolveService(options) {
95
147
  }
96
148
  projectRoot = currentRoot;
97
149
  }
98
- if (options.packageId && !options.actions && packages.length === 0) {
99
- const root = resolvePackageRoot(options.packageId, undefined, options.customHome);
100
- if (!root || !existsSync(root)) {
101
- throw new ActionDockError(PACKAGE_NOT_FOUND, `Package '${options.packageId}' not found in registry`);
102
- }
103
- packages.push({
104
- ...basePkgOpts,
105
- packageRoot: root,
106
- dataDir: options.dataDir,
107
- });
108
- }
109
150
  let platform = options.platform;
110
151
  if (!platform && typeof process !== "undefined" && process.versions?.node) {
111
152
  platform = createNodePlatform({
package/dist/stdio.js CHANGED
@@ -41,6 +41,8 @@ export async function startMcpStdio(options = {}) {
41
41
  packageId: options.packageId,
42
42
  packageIds: options.packageIds,
43
43
  all: options.all,
44
+ packageAllowlist: options.packageAllowlist,
45
+ actionAllowlist: options.actionAllowlist,
44
46
  customHome: options.customHome,
45
47
  dataDir: options.dataDir,
46
48
  configOverrides: options.configOverrides,
@@ -1,3 +1,4 @@
1
+ import { type EffectiveServerPolicy } from "@actiondock/core/server";
1
2
  /**
2
3
  * 构造携带结构化错误码的工具名冲突异常。
3
4
  *
@@ -25,9 +26,11 @@ export interface MappedAction<T> {
25
26
  }
26
27
  /**
27
28
  * 映射与过滤工具,处理冲突检测、去重、白名单过滤及 SHA-256 截断散列逻辑。
29
+ * 过滤统一委托 core 策略函数做并集判定(整包 ∪ 全限定单动作),适配层不复制授权逻辑;
30
+ * 未传 policy 时从两份 allowlist 构造同一 policy(空授权仍为空,不回退全访问)。
28
31
  */
29
32
  export declare function mapAndFilterActions<T extends {
30
33
  id: string;
31
34
  packageId?: string;
32
35
  description?: string;
33
- }>(rawActions: T[], allowedPackageIds?: string[], actionAllowlist?: string[]): MappedAction<T>[];
36
+ }>(rawActions: T[], allowedPackageIds?: string[], actionAllowlist?: string[], policy?: EffectiveServerPolicy): MappedAction<T>[];
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { MCP_TOOL_NAME_COLLISION } from "@actiondock/core";
3
3
  import { parseActionRef } from "@actiondock/core/graph";
4
- import { isActionAllowed } from "@actiondock/core/server";
4
+ import { isActionAllowedByPolicy } from "@actiondock/core/server";
5
5
  /**
6
6
  * 构造携带结构化错误码的工具名冲突异常。
7
7
  *
@@ -29,8 +29,15 @@ export function splitActionRef(ref) {
29
29
  }
30
30
  /**
31
31
  * 映射与过滤工具,处理冲突检测、去重、白名单过滤及 SHA-256 截断散列逻辑。
32
+ * 过滤统一委托 core 策略函数做并集判定(整包 ∪ 全限定单动作),适配层不复制授权逻辑;
33
+ * 未传 policy 时从两份 allowlist 构造同一 policy(空授权仍为空,不回退全访问)。
32
34
  */
33
- export function mapAndFilterActions(rawActions, allowedPackageIds, actionAllowlist) {
35
+ export function mapAndFilterActions(rawActions, allowedPackageIds, actionAllowlist, policy) {
36
+ // 策略单点归一:调用方未传 policy 时按同两份 allowlist 构造,保证与 adapter 主链路同一并集语义
37
+ const effectivePolicy = policy || {
38
+ packageAllowlist: allowedPackageIds,
39
+ actionAllowlist,
40
+ };
34
41
  // 引用解析单点归一:每项仅解析一次,后续各轮(白名单过滤、去重、频次统计、
35
42
  // 工具命名)复用同一结果,避免同一列表对 splitActionRef 的三轮重复调用;
36
43
  // 显式 packageId 与解析所得 packageId 分别留存,各轮按既有优先级消费
@@ -48,21 +55,12 @@ export function mapAndFilterActions(rawActions, allowedPackageIds, actionAllowli
48
55
  parsedRefs.set(act, { explicitPkgId: act.packageId || "", baseId: act.id });
49
56
  }
50
57
  }
51
- if (allowedPackageIds && allowedPackageIds.length > 0) {
52
- // 白名单轮:显式 packageId 优先,缺失时回退解析所得
53
- rawActions = rawActions.filter((act) => {
54
- const ref = parsedRefs.get(act);
55
- const pkgId = ref.explicitPkgId || ref.parsedPkgId || "";
56
- return pkgId ? allowedPackageIds.includes(pkgId) : false;
57
- });
58
- }
59
- if (actionAllowlist && actionAllowlist.length > 0) {
60
- rawActions = rawActions.filter((act) => {
61
- const ref = parsedRefs.get(act);
62
- const pkgId = ref.explicitPkgId || ref.parsedPkgId || undefined;
63
- return isActionAllowed({ packageId: pkgId, actionId: ref.baseId }, actionAllowlist);
64
- });
65
- }
58
+ // 授权过滤单轮:仅用 core 策略函数并集判定,不先按包选择截断单动作授权目标
59
+ rawActions = rawActions.filter((act) => {
60
+ const ref = parsedRefs.get(act);
61
+ const pkgId = ref.explicitPkgId || ref.parsedPkgId || undefined;
62
+ return isActionAllowedByPolicy({ packageId: pkgId, actionId: ref.baseId }, effectivePolicy);
63
+ });
66
64
  const seenActionKeys = new Map();
67
65
  for (const act of rawActions) {
68
66
  const ref = parsedRefs.get(act);
package/dist/types.d.ts CHANGED
@@ -21,9 +21,9 @@ export interface ActionDockMcpOptions {
21
21
  packageId?: string;
22
22
  /** 多个 Package ID 列表 */
23
23
  packageIds?: string[];
24
- /** 允许访问执行的 Package ID 白名单列表(为空允许全部) */
24
+ /** 整包授权白名单列表;与动作授权取并集,独立 * 显式授予全部,省略或空数组不增加授权 */
25
25
  packageAllowlist?: string[];
26
- /** 允许暴露执行的 Action 标识白名单列表(为空允许全部,支持短名或 packageId/actionId) */
26
+ /** 全限定 packageId/actionId 精确动作授权白名单列表;与整包授权取并集,省略或空数组不增加授权 */
27
27
  actionAllowlist?: string[];
28
28
  /** 是否聚合暴露全局 Registry 中的所有 Package */
29
29
  all?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actiondock/mcp",
3
- "version": "2.19.0",
3
+ "version": "2.21.0",
4
4
  "description": "ActionDock MCP Adapter for exposing Actions as Model Context Protocol (MCP) Tools",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "test": "node ../../scripts/run-tests.ts"
30
30
  },
31
31
  "dependencies": {
32
- "@actiondock/core": "^2.19.0",
33
- "@actiondock/sdk": "^2.19.0",
32
+ "@actiondock/core": "^2.21.0",
33
+ "@actiondock/sdk": "^2.21.0",
34
34
  "@modelcontextprotocol/server": "^2.0.0"
35
35
  },
36
36
  "devDependencies": {