@actiondock/mcp 2.0.12 → 2.2.1
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 +10 -10
- package/dist/adapter.d.ts +17 -2
- package/dist/adapter.js +395 -0
- package/dist/execution-mode.d.ts +30 -0
- package/dist/execution-mode.js +63 -0
- package/dist/http.d.ts +1 -1
- package/dist/http.js +221 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +6 -574
- package/dist/register-tasks-extension.d.ts +9 -0
- package/dist/register-tasks-extension.js +69 -0
- package/dist/schemas.d.ts +2 -1
- package/dist/schemas.js +30 -0
- package/dist/stdio-host.d.ts +2 -0
- package/dist/stdio-host.js +13 -0
- package/dist/stdio.d.ts +7 -2
- package/dist/stdio.js +114 -0
- package/dist/types.d.ts +34 -9
- package/dist/types.js +33 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
ActionDock 2.0 模型上下文协议适配器。
|
|
4
4
|
|
|
5
|
-
[](https://nodejs.org/)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
[](https://modelcontextprotocol.io/)
|
|
8
8
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
@@ -15,12 +15,12 @@ ActionDock 2.0 模型上下文协议适配器。
|
|
|
15
15
|
|
|
16
16
|
### 双协议传输通道
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
18
|
+
- STDIO 标准输入输出通道:专为本地客户端设计,直接通过子进程标准流双向通信,安全且免配置端口。
|
|
19
|
+
- HTTP 传输通道:专为远程服务与容器化部署设计,基于 Web 标准请求与响应实现流式传输,支持跨域策略配置与鉴权令牌校验。
|
|
20
20
|
|
|
21
21
|
### 工具模式自动转换
|
|
22
22
|
|
|
23
|
-
- 自动解析每个 Action
|
|
23
|
+
- 自动解析每个 Action 的模式规范,转换为标准 MCP Tool 契约。
|
|
24
24
|
- 智能处理多包聚合场景下的动作标识冲突,自动使用包命名空间前缀消除歧义。
|
|
25
25
|
- 严格遵循输入格式校验,在参数违规时向模型返回结构化诊断提示。
|
|
26
26
|
- 执行结果自动封装为包含文本内容块与结构化数据的 MCP 格式信封,并在异常时正确标记错误标识。
|
|
@@ -28,16 +28,16 @@ ActionDock 2.0 模型上下文协议适配器。
|
|
|
28
28
|
### 协同取消信号向下传播
|
|
29
29
|
|
|
30
30
|
- 当 MCP 客户端发起取消请求时,适配层自动捕获中断事件。
|
|
31
|
-
- 取消信号直接传递至底层的
|
|
32
|
-
- 业务代码可通过监听
|
|
31
|
+
- 取消信号直接传递至底层的 ActionRunner 调度器,并联动激活当前任务上下文中的 `ctx.signal`。
|
|
32
|
+
- 业务代码可通过监听 AbortSignal 安全释放资源或提前终止执行。
|
|
33
33
|
|
|
34
34
|
### Tasks 异步任务映射扩展
|
|
35
35
|
|
|
36
36
|
针对长周期、重资源消耗的复杂任务,完整适配 MCP Tasks 异步协议扩展:
|
|
37
37
|
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
38
|
+
- 任务状态查询:通过 `tasks/get` 端点根据任务标识检索当前运行状态、执行进度与部分输出快照。
|
|
39
|
+
- 任务主动取消:通过 `tasks/cancel` 端点向正在后台执行的长周期任务发出中止指令。
|
|
40
|
+
- 任务清单列举:通过 `tasks/list` 端点批量查询当前会话及宿主下的活跃与历史任务列表。
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
@@ -50,7 +50,7 @@ ActionDock 2.0 模型上下文协议适配器。
|
|
|
50
50
|
ad mcp
|
|
51
51
|
|
|
52
52
|
# 以 HTTP 协议启动并在指定端口监听
|
|
53
|
-
ad mcp
|
|
53
|
+
ad mcp serve --port 8080
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
也可以在代码中通过编程方式创建适配器服务:
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { ActionDockApp, ActionDockHost, ActionDockTarget } from "@actiondock/core";
|
|
1
2
|
import type { ExecutionResult } from "@actiondock/sdk";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3
|
-
import {
|
|
4
|
+
import type { ActionDockMcpOptions } from "./types.js";
|
|
4
5
|
/**
|
|
5
6
|
* 判断目标值是否为普通对象(Plain Object)。
|
|
6
7
|
*
|
|
@@ -27,10 +28,24 @@ export declare function toMcpResult(result: ExecutionResult): {
|
|
|
27
28
|
}[];
|
|
28
29
|
structuredContent?: undefined;
|
|
29
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* 解析或基于选项创建底层 ActionDockTarget 统一门面。
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveTarget(options: ActionDockMcpOptions): Promise<{
|
|
35
|
+
target: ActionDockTarget;
|
|
36
|
+
ownsTarget: boolean;
|
|
37
|
+
}>;
|
|
30
38
|
export type ActionDockMcpServer = McpServer & {
|
|
31
39
|
close: () => Promise<void>;
|
|
40
|
+
target: ActionDockTarget;
|
|
41
|
+
host?: ActionDockHost;
|
|
42
|
+
app?: ActionDockApp;
|
|
43
|
+
events?: (runId: string, options?: {
|
|
44
|
+
after?: number;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
}) => AsyncIterable<Record<string, unknown>>;
|
|
32
47
|
};
|
|
33
48
|
/**
|
|
34
|
-
*
|
|
49
|
+
* 创建并配置基于 ActionDockTarget 的 McpServer 适配层实例。
|
|
35
50
|
*/
|
|
36
51
|
export declare function createActionDockMcpServer(options?: ActionDockMcpOptions): Promise<ActionDockMcpServer>;
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { ACTIONDOCK_VERSION, ActionResolver, createActionDockTarget, findProjectRoot, resolvePackageRoot, } from "@actiondock/core";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
6
|
+
import { registerTasksExtension } from "./register-tasks-extension.js";
|
|
7
|
+
import { toMcpSchema } from "./schemas.js";
|
|
8
|
+
import { isAsyncExecutionRequested, stripExecutionWrapper, } from "./execution-mode.js";
|
|
9
|
+
/**
|
|
10
|
+
* 判断目标值是否为普通对象(Plain Object)。
|
|
11
|
+
*
|
|
12
|
+
* @param value 待检查的值
|
|
13
|
+
*/
|
|
14
|
+
export function isPlainObject(value) {
|
|
15
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const proto = Object.getPrototypeOf(value);
|
|
19
|
+
return proto === Object.prototype || proto === null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 将 ActionDock 标准的 ExecutionResult 信封结构转换为 MCP 协议规范的 Tool Call 返回结果。
|
|
23
|
+
*
|
|
24
|
+
* @param result ExecutionResult 结果对象
|
|
25
|
+
*/
|
|
26
|
+
export function toMcpResult(result) {
|
|
27
|
+
if (result.ok) {
|
|
28
|
+
const structuredContent = isPlainObject(result.data)
|
|
29
|
+
? result.data
|
|
30
|
+
: { value: result.data };
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: JSON.stringify(result),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
structuredContent,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
isError: true,
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: JSON.stringify(result),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
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
|
+
/**
|
|
74
|
+
* 构造携带结构化错误码的工具名冲突异常。
|
|
75
|
+
*
|
|
76
|
+
* @param toolName 冲突的工具名
|
|
77
|
+
*/
|
|
78
|
+
function toolNameCollisionError(toolName) {
|
|
79
|
+
const err = new Error(`MCP tool name collision detected for tool '${toolName}'`);
|
|
80
|
+
err.code = MCP_TOOL_NAME_COLLISION;
|
|
81
|
+
return err;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 解析或基于选项创建底层 ActionDockTarget 统一门面。
|
|
85
|
+
*/
|
|
86
|
+
export async function resolveTarget(options) {
|
|
87
|
+
if (options.target) {
|
|
88
|
+
return { target: options.target, ownsTarget: false };
|
|
89
|
+
}
|
|
90
|
+
if (options.host) {
|
|
91
|
+
const target = await createActionDockTarget({ type: "local", host: options.host });
|
|
92
|
+
return { target, ownsTarget: false };
|
|
93
|
+
}
|
|
94
|
+
if (options.app) {
|
|
95
|
+
const target = await createActionDockTarget({ type: "local", app: options.app });
|
|
96
|
+
return { target, ownsTarget: false };
|
|
97
|
+
}
|
|
98
|
+
const packages = [];
|
|
99
|
+
// 外部注入的 storage 生命周期默认由注入方管理,适配层不伪造 close 语义;
|
|
100
|
+
// 仅当显式声明 ownStorageLifecycle 时才向包配置透传原始实例(随 target.close() 级联关闭)
|
|
101
|
+
// 外部注入的 storage 生命周期默认由注入方管理,适配层仅委托读写不接管关闭;
|
|
102
|
+
// 显式声明 ownStorageLifecycle 时透传原始实例,随 target.close() 级联关闭
|
|
103
|
+
const appStorage = options.storage
|
|
104
|
+
? options.ownStorageLifecycle
|
|
105
|
+
? options.storage
|
|
106
|
+
: createExternalStorageView(options.storage)
|
|
107
|
+
: undefined;
|
|
108
|
+
if (options.actions) {
|
|
109
|
+
packages.push({
|
|
110
|
+
projectConfig: {
|
|
111
|
+
id: options.packageId || "default",
|
|
112
|
+
name: options.packageId || "default",
|
|
113
|
+
version: ACTIONDOCK_VERSION,
|
|
114
|
+
},
|
|
115
|
+
actions: options.actions,
|
|
116
|
+
storage: appStorage,
|
|
117
|
+
inMemory: true,
|
|
118
|
+
customHome: options.customHome,
|
|
119
|
+
configOverrides: options.configOverrides,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (options.projectRoots && options.projectRoots.length > 0) {
|
|
123
|
+
for (const root of options.projectRoots) {
|
|
124
|
+
const abs = resolve(root);
|
|
125
|
+
const detected = findProjectRoot(abs);
|
|
126
|
+
if (!detected) {
|
|
127
|
+
throw new Error(`Project root '${root}' is not a valid ActionDock package (actiondock.json not found)`);
|
|
128
|
+
}
|
|
129
|
+
packages.push({
|
|
130
|
+
packageRoot: detected,
|
|
131
|
+
storage: appStorage,
|
|
132
|
+
customHome: options.customHome,
|
|
133
|
+
configOverrides: options.configOverrides,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (options.packageIds && options.packageIds.length > 0) {
|
|
138
|
+
for (const pkgId of options.packageIds) {
|
|
139
|
+
const root = resolvePackageRoot(pkgId, undefined, options.customHome);
|
|
140
|
+
if (!root || !existsSync(root)) {
|
|
141
|
+
throw new Error(`Package '${pkgId}' not found in registry`);
|
|
142
|
+
}
|
|
143
|
+
packages.push({
|
|
144
|
+
packageRoot: root,
|
|
145
|
+
storage: appStorage,
|
|
146
|
+
customHome: options.customHome,
|
|
147
|
+
configOverrides: options.configOverrides,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let projectRoot = options.projectRoot;
|
|
152
|
+
if (options.projectRoot) {
|
|
153
|
+
const abs = resolve(options.projectRoot);
|
|
154
|
+
const detected = findProjectRoot(abs);
|
|
155
|
+
if (!detected) {
|
|
156
|
+
throw new Error(`Project root '${options.projectRoot}' is not a valid ActionDock package (actiondock.json not found)`);
|
|
157
|
+
}
|
|
158
|
+
projectRoot = detected;
|
|
159
|
+
}
|
|
160
|
+
if (!projectRoot &&
|
|
161
|
+
packages.length === 0 &&
|
|
162
|
+
!options.all &&
|
|
163
|
+
!options.packageId) {
|
|
164
|
+
const currentRoot = findProjectRoot(process.cwd());
|
|
165
|
+
if (!currentRoot) {
|
|
166
|
+
throw new Error("No ActionDock project root found. Run inside an ActionDock package or specify --dir / --package / --all.");
|
|
167
|
+
}
|
|
168
|
+
projectRoot = currentRoot;
|
|
169
|
+
}
|
|
170
|
+
if (options.packageId && !options.actions && packages.length === 0) {
|
|
171
|
+
const root = resolvePackageRoot(options.packageId, undefined, options.customHome);
|
|
172
|
+
if (!root || !existsSync(root)) {
|
|
173
|
+
throw new Error(`Package '${options.packageId}' not found in registry`);
|
|
174
|
+
}
|
|
175
|
+
packages.push({
|
|
176
|
+
packageRoot: root,
|
|
177
|
+
customHome: options.customHome,
|
|
178
|
+
configOverrides: options.configOverrides,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const target = await createActionDockTarget({
|
|
182
|
+
type: "local",
|
|
183
|
+
projectRoot,
|
|
184
|
+
packages: packages.length > 0 ? packages : undefined,
|
|
185
|
+
scanLinkedPackages: Boolean(options.all),
|
|
186
|
+
hostOptions: {
|
|
187
|
+
autoLoadCurrentProject: packages.length === 0,
|
|
188
|
+
},
|
|
189
|
+
customHome: options.customHome,
|
|
190
|
+
});
|
|
191
|
+
return { target, ownsTarget: true };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 创建并配置基于 ActionDockTarget 的 McpServer 适配层实例。
|
|
195
|
+
*/
|
|
196
|
+
export async function createActionDockMcpServer(options = {}) {
|
|
197
|
+
const { target } = await resolveTarget(options);
|
|
198
|
+
let serverName = "actiondock";
|
|
199
|
+
let serverVersion = ACTIONDOCK_VERSION;
|
|
200
|
+
try {
|
|
201
|
+
const info = await target.info();
|
|
202
|
+
if (info) {
|
|
203
|
+
serverName = info.name || info.id || "actiondock";
|
|
204
|
+
serverVersion = info.protocolVersion || ACTIONDOCK_VERSION;
|
|
205
|
+
if (info.packages && info.packages.length === 1) {
|
|
206
|
+
serverName = info.packages[0].name || info.packages[0].id || serverName;
|
|
207
|
+
serverVersion = info.packages[0].version || serverVersion;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// 忽略元数据读取失败,使用默认值
|
|
213
|
+
}
|
|
214
|
+
const server = new McpServer({
|
|
215
|
+
name: serverName,
|
|
216
|
+
version: serverVersion,
|
|
217
|
+
});
|
|
218
|
+
// 任务规范扩展注册集中隔离在独立模块,本层不再直接操作 SDK 内层实例
|
|
219
|
+
registerTasksExtension(server, target);
|
|
220
|
+
// 工具注册与模式映射:tools/list 纯粹委托 target.listActions()
|
|
221
|
+
const actions = await target.listActions();
|
|
222
|
+
// 统计 Action 基础 ID 出现频次,用于同名冲突命名空间隔离
|
|
223
|
+
const baseCounts = new Map();
|
|
224
|
+
for (const act of actions) {
|
|
225
|
+
let baseId = act.id;
|
|
226
|
+
if (act.id.includes("/")) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = ActionResolver.parseRef(act.id);
|
|
229
|
+
baseId = parsed.actionId;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
const idx = act.id.lastIndexOf("/");
|
|
233
|
+
baseId = act.id.slice(idx + 1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
baseCounts.set(baseId, (baseCounts.get(baseId) || 0) + 1);
|
|
237
|
+
}
|
|
238
|
+
const registeredToolNames = new Set();
|
|
239
|
+
for (const action of actions) {
|
|
240
|
+
let baseId = action.id;
|
|
241
|
+
let packageId = action.packageId;
|
|
242
|
+
if (action.id.includes("/")) {
|
|
243
|
+
try {
|
|
244
|
+
const parsed = ActionResolver.parseRef(action.id);
|
|
245
|
+
packageId = parsed.packageId || packageId;
|
|
246
|
+
baseId = parsed.actionId;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
const idx = action.id.lastIndexOf("/");
|
|
250
|
+
packageId = packageId || action.id.slice(0, idx);
|
|
251
|
+
baseId = action.id.slice(idx + 1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const count = baseCounts.get(baseId) || 1;
|
|
255
|
+
let toolName = baseId;
|
|
256
|
+
if (count > 1 && packageId) {
|
|
257
|
+
const cleanPkgId = packageId.replace(/^@/, "").replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
258
|
+
toolName = `${cleanPkgId}_${baseId}`;
|
|
259
|
+
}
|
|
260
|
+
else if (toolName.includes("/") && packageId) {
|
|
261
|
+
const cleanPkgId = packageId.replace(/^@/, "").replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
262
|
+
toolName = `${cleanPkgId}_${baseId}`;
|
|
263
|
+
}
|
|
264
|
+
if (toolName.length > 64) {
|
|
265
|
+
const hash = createHash("sha256").update(toolName).digest("hex").slice(0, 8);
|
|
266
|
+
toolName = `${toolName.slice(0, 55)}_${hash}`;
|
|
267
|
+
}
|
|
268
|
+
if (registeredToolNames.has(toolName)) {
|
|
269
|
+
throw toolNameCollisionError(toolName);
|
|
270
|
+
}
|
|
271
|
+
registeredToolNames.add(toolName);
|
|
272
|
+
const isMultiPackage = actions.some((a) => a.id.includes("/") || (a.packageId && a.packageId !== actions[0].packageId));
|
|
273
|
+
const description = isMultiPackage
|
|
274
|
+
? `[${action.id}] ${action.description || ""}`.trim()
|
|
275
|
+
: action.description;
|
|
276
|
+
// 工具执行:tools/call 委托 target.runAction() 或 target.startAction()
|
|
277
|
+
server.registerTool(toolName, {
|
|
278
|
+
description,
|
|
279
|
+
inputSchema: toMcpSchema(action.inputSchema),
|
|
280
|
+
outputSchema: action.outputSchema ? toMcpSchema(action.outputSchema) : undefined,
|
|
281
|
+
}, async (input, ctx) => {
|
|
282
|
+
// 异步执行模式只认显式约定字段 execution.mode,旧版 __async 仅作只读兼容探测
|
|
283
|
+
const isAsync = isAsyncExecutionRequested(input);
|
|
284
|
+
const signal = ctx.mcpReq?.signal;
|
|
285
|
+
// 分发前仅剥离适配层注入的包装字段,业务自有字段(含名为 async 的入参)原样透传
|
|
286
|
+
const cleanInput = stripExecutionWrapper(input);
|
|
287
|
+
if (isAsync) {
|
|
288
|
+
const ticket = await target.startAction(action.id, cleanInput, {
|
|
289
|
+
signal,
|
|
290
|
+
timeoutMs: options.timeoutMs,
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
content: [
|
|
294
|
+
{
|
|
295
|
+
type: "text",
|
|
296
|
+
text: JSON.stringify({
|
|
297
|
+
ok: true,
|
|
298
|
+
runId: ticket.runId,
|
|
299
|
+
taskId: ticket.runId,
|
|
300
|
+
status: "running",
|
|
301
|
+
}),
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const result = await target.runAction(action.id, cleanInput, {
|
|
307
|
+
signal,
|
|
308
|
+
timeoutMs: options.timeoutMs,
|
|
309
|
+
});
|
|
310
|
+
return toMcpResult(result);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
// 任务规范映射(tasks/get、tasks/cancel、tasks/list)已收敛至 registerTasksExtension 隔离模块
|
|
314
|
+
// 资源与规程映射:规程映射为只读 MCP Resource 与 Prompt
|
|
315
|
+
try {
|
|
316
|
+
const playbooks = await target.listPlaybooks();
|
|
317
|
+
for (const pb of playbooks) {
|
|
318
|
+
server.registerResource(pb.id, `playbook://${pb.id}`, {
|
|
319
|
+
title: pb.id,
|
|
320
|
+
description: pb.description,
|
|
321
|
+
mimeType: "text/markdown",
|
|
322
|
+
}, async (uri) => {
|
|
323
|
+
const spec = await target.describePlaybook(pb.id);
|
|
324
|
+
return {
|
|
325
|
+
contents: [
|
|
326
|
+
{
|
|
327
|
+
uri: uri.href,
|
|
328
|
+
text: spec.content,
|
|
329
|
+
mimeType: "text/markdown",
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
server.registerPrompt(pb.id, {
|
|
335
|
+
title: pb.id,
|
|
336
|
+
description: pb.description,
|
|
337
|
+
}, async () => {
|
|
338
|
+
const spec = await target.describePlaybook(pb.id);
|
|
339
|
+
return {
|
|
340
|
+
messages: [
|
|
341
|
+
{
|
|
342
|
+
role: "user",
|
|
343
|
+
content: {
|
|
344
|
+
type: "text",
|
|
345
|
+
text: spec.content,
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
};
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
// 忽略规程获取或注册异常
|
|
355
|
+
}
|
|
356
|
+
// 服务生命周期:默认 close 仅关闭 MCP 服务本身,不级联 target——
|
|
357
|
+
// SDK 传输层(HTTP 每请求 / stdio 探测回落)会销毁工厂产物,若 close 级联会误杀共享 target;
|
|
358
|
+
// 需要「一次 close 同时释放 target」的独立持有方显式传 cascadeTargetClose
|
|
359
|
+
const originalClose = server.close.bind(server);
|
|
360
|
+
let isClosed = false;
|
|
361
|
+
const closeFn = async () => {
|
|
362
|
+
if (isClosed)
|
|
363
|
+
return;
|
|
364
|
+
isClosed = true;
|
|
365
|
+
// 清理异常不吞没:先关服务再级联目标,server 关闭失败也继续释放 target 并聚合上抛
|
|
366
|
+
let closeError;
|
|
367
|
+
try {
|
|
368
|
+
await originalClose();
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
closeError = err;
|
|
372
|
+
}
|
|
373
|
+
if (options.cascadeTargetClose) {
|
|
374
|
+
try {
|
|
375
|
+
await target.close();
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
if (closeError !== undefined) {
|
|
379
|
+
throw new AggregateError([closeError, err], "Failed to close MCP server and target");
|
|
380
|
+
}
|
|
381
|
+
throw err;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (closeError !== undefined) {
|
|
385
|
+
throw closeError;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
const decorated = server;
|
|
389
|
+
decorated.close = closeFn;
|
|
390
|
+
decorated.target = target;
|
|
391
|
+
decorated.host = options.host;
|
|
392
|
+
decorated.app = options.app;
|
|
393
|
+
decorated.events = (runId, opts) => target.events(runId, opts);
|
|
394
|
+
return decorated;
|
|
395
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 工具调用的异步执行模式探测与包装字段剥离。
|
|
3
|
+
*
|
|
4
|
+
* 单一显式约定:异步执行模式只认 input.execution.mode === "async"。
|
|
5
|
+
* 旧版魔法字段(__async 布尔)保留只读兼容探测,但不再参与 schema 注入,
|
|
6
|
+
* 也不再吞没业务自有的 async 入参字段:
|
|
7
|
+
* - 探测阶段仅读取 execution.mode 与 __async,语义不变;
|
|
8
|
+
* - 剥离阶段仅删除适配层实际注入的包装字段 execution 与 __async,
|
|
9
|
+
* 名为 async 的业务字段原样透传给 Action。
|
|
10
|
+
*
|
|
11
|
+
* 保留命名空间权衡:`execution` 与 `__async` 是适配层声明的前缀保留字段;
|
|
12
|
+
* 业务 Action 入参若恰好撞名 `__async: true`,会被误判为异步意图并在转发前剥离。
|
|
13
|
+
* 这是旧版兼容通道的已知代价,新代码统一使用 execution.mode 声明异步,
|
|
14
|
+
* 业务字段命名应避开该前缀保留命名空间。
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 从工具调用入参中提取显式声明的异步执行模式。
|
|
18
|
+
*
|
|
19
|
+
* @param input 工具调用入参
|
|
20
|
+
* @returns 是否显式声明了异步执行(execution.mode 为 async,或旧版 __async 为 true)
|
|
21
|
+
*/
|
|
22
|
+
export declare function isAsyncExecutionRequested(input: unknown): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* 剥离适配层注入的执行控制包装字段,返回纯净的业务入参。
|
|
25
|
+
*
|
|
26
|
+
* 仅删除 WRAPPER_KEYS 中声明的包装字段;业务自有字段(含名为 async 的入参)原样保留。
|
|
27
|
+
*
|
|
28
|
+
* @param input 工具调用入参
|
|
29
|
+
*/
|
|
30
|
+
export declare function stripExecutionWrapper<T>(input: T): T;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 工具调用的异步执行模式探测与包装字段剥离。
|
|
3
|
+
*
|
|
4
|
+
* 单一显式约定:异步执行模式只认 input.execution.mode === "async"。
|
|
5
|
+
* 旧版魔法字段(__async 布尔)保留只读兼容探测,但不再参与 schema 注入,
|
|
6
|
+
* 也不再吞没业务自有的 async 入参字段:
|
|
7
|
+
* - 探测阶段仅读取 execution.mode 与 __async,语义不变;
|
|
8
|
+
* - 剥离阶段仅删除适配层实际注入的包装字段 execution 与 __async,
|
|
9
|
+
* 名为 async 的业务字段原样透传给 Action。
|
|
10
|
+
*
|
|
11
|
+
* 保留命名空间权衡:`execution` 与 `__async` 是适配层声明的前缀保留字段;
|
|
12
|
+
* 业务 Action 入参若恰好撞名 `__async: true`,会被误判为异步意图并在转发前剥离。
|
|
13
|
+
* 这是旧版兼容通道的已知代价,新代码统一使用 execution.mode 声明异步,
|
|
14
|
+
* 业务字段命名应避开该前缀保留命名空间。
|
|
15
|
+
*/
|
|
16
|
+
/** 适配层注入到工具 schema 的执行控制包装字段名集合。 */
|
|
17
|
+
const WRAPPER_KEYS = ["execution", "__async"];
|
|
18
|
+
/**
|
|
19
|
+
* 从工具调用入参中提取显式声明的异步执行模式。
|
|
20
|
+
*
|
|
21
|
+
* @param input 工具调用入参
|
|
22
|
+
* @returns 是否显式声明了异步执行(execution.mode 为 async,或旧版 __async 为 true)
|
|
23
|
+
*/
|
|
24
|
+
export function isAsyncExecutionRequested(input) {
|
|
25
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const record = input;
|
|
29
|
+
const execution = record.execution;
|
|
30
|
+
if (typeof execution === "object" &&
|
|
31
|
+
execution !== null &&
|
|
32
|
+
!Array.isArray(execution) &&
|
|
33
|
+
execution.mode === "async") {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
// 旧版魔法字段兼容探测:仅只读,不再注入 schema 也不再参与剥离以外的影响
|
|
37
|
+
if (record.__async === true)
|
|
38
|
+
return true;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 剥离适配层注入的执行控制包装字段,返回纯净的业务入参。
|
|
43
|
+
*
|
|
44
|
+
* 仅删除 WRAPPER_KEYS 中声明的包装字段;业务自有字段(含名为 async 的入参)原样保留。
|
|
45
|
+
*
|
|
46
|
+
* @param input 工具调用入参
|
|
47
|
+
*/
|
|
48
|
+
export function stripExecutionWrapper(input) {
|
|
49
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
50
|
+
return input;
|
|
51
|
+
}
|
|
52
|
+
const record = input;
|
|
53
|
+
const hasWrapper = WRAPPER_KEYS.some((key) => key in record);
|
|
54
|
+
if (!hasWrapper)
|
|
55
|
+
return input;
|
|
56
|
+
const rest = {};
|
|
57
|
+
for (const [key, value] of Object.entries(record)) {
|
|
58
|
+
if (WRAPPER_KEYS.includes(key))
|
|
59
|
+
continue;
|
|
60
|
+
rest[key] = value;
|
|
61
|
+
}
|
|
62
|
+
return rest;
|
|
63
|
+
}
|
package/dist/http.d.ts
CHANGED