@actiondock/mcp 2.2.1 → 2.3.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.js +14 -4
- package/dist/execution-mode.d.ts +7 -0
- package/dist/execution-mode.js +21 -0
- package/dist/http.js +18 -10
- package/dist/register-tasks-extension.js +37 -3
- package/dist/schemas.d.ts +6 -1
- package/dist/schemas.js +7 -2
- package/dist/stdio.js +20 -0
- package/dist/types.d.ts +5 -0
- package/package.json +3 -3
package/dist/adapter.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ACTIONDOCK_VERSION, ActionResolver, createActionDockTarget, findProject
|
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
6
6
|
import { registerTasksExtension } from "./register-tasks-extension.js";
|
|
7
7
|
import { toMcpSchema } from "./schemas.js";
|
|
8
|
-
import { isAsyncExecutionRequested, stripExecutionWrapper, } from "./execution-mode.js";
|
|
8
|
+
import { extractExecutionTimeoutMs, isAsyncExecutionRequested, stripExecutionWrapper, } from "./execution-mode.js";
|
|
9
9
|
/**
|
|
10
10
|
* 判断目标值是否为普通对象(Plain Object)。
|
|
11
11
|
*
|
|
@@ -174,6 +174,7 @@ export async function resolveTarget(options) {
|
|
|
174
174
|
}
|
|
175
175
|
packages.push({
|
|
176
176
|
packageRoot: root,
|
|
177
|
+
storage: appStorage,
|
|
177
178
|
customHome: options.customHome,
|
|
178
179
|
configOverrides: options.configOverrides,
|
|
179
180
|
});
|
|
@@ -277,17 +278,26 @@ export async function createActionDockMcpServer(options = {}) {
|
|
|
277
278
|
server.registerTool(toolName, {
|
|
278
279
|
description,
|
|
279
280
|
inputSchema: toMcpSchema(action.inputSchema),
|
|
280
|
-
|
|
281
|
+
// 出参 schema 不注入 execution 包装字段:实际 structuredContent 不含该字段,注入会与真实返回结构不符
|
|
282
|
+
outputSchema: action.outputSchema
|
|
283
|
+
? toMcpSchema(action.outputSchema, false)
|
|
284
|
+
: undefined,
|
|
281
285
|
}, async (input, ctx) => {
|
|
282
286
|
// 异步执行模式只认显式约定字段 execution.mode,旧版 __async 仅作只读兼容探测
|
|
283
287
|
const isAsync = isAsyncExecutionRequested(input);
|
|
284
288
|
const signal = ctx.mcpReq?.signal;
|
|
285
289
|
// 分发前仅剥离适配层注入的包装字段,业务自有字段(含名为 async 的入参)原样透传
|
|
286
290
|
const cleanInput = stripExecutionWrapper(input);
|
|
291
|
+
// 超时组合策略:客户端声明与服务端配置同时存在时取较小值(更防御),
|
|
292
|
+
// 任一方单独存在则直接生效,双方均缺省时不设置超时
|
|
293
|
+
const clientTimeoutMs = extractExecutionTimeoutMs(input);
|
|
294
|
+
const effectiveTimeoutMs = typeof options.timeoutMs === "number" && typeof clientTimeoutMs === "number"
|
|
295
|
+
? Math.min(options.timeoutMs, clientTimeoutMs)
|
|
296
|
+
: clientTimeoutMs ?? options.timeoutMs;
|
|
287
297
|
if (isAsync) {
|
|
288
298
|
const ticket = await target.startAction(action.id, cleanInput, {
|
|
289
299
|
signal,
|
|
290
|
-
timeoutMs:
|
|
300
|
+
timeoutMs: effectiveTimeoutMs,
|
|
291
301
|
});
|
|
292
302
|
return {
|
|
293
303
|
content: [
|
|
@@ -305,7 +315,7 @@ export async function createActionDockMcpServer(options = {}) {
|
|
|
305
315
|
}
|
|
306
316
|
const result = await target.runAction(action.id, cleanInput, {
|
|
307
317
|
signal,
|
|
308
|
-
timeoutMs:
|
|
318
|
+
timeoutMs: effectiveTimeoutMs,
|
|
309
319
|
});
|
|
310
320
|
return toMcpResult(result);
|
|
311
321
|
});
|
package/dist/execution-mode.d.ts
CHANGED
|
@@ -28,3 +28,10 @@ export declare function isAsyncExecutionRequested(input: unknown): boolean;
|
|
|
28
28
|
* @param input 工具调用入参
|
|
29
29
|
*/
|
|
30
30
|
export declare function stripExecutionWrapper<T>(input: T): T;
|
|
31
|
+
/**
|
|
32
|
+
* 从工具调用入参的 execution 包装字段中提取客户端显式声明的超时时间。
|
|
33
|
+
*
|
|
34
|
+
* @param input 工具调用入参
|
|
35
|
+
* @returns 客户端声明的超时毫秒数,未声明或非法时返回 undefined
|
|
36
|
+
*/
|
|
37
|
+
export declare function extractExecutionTimeoutMs(input: unknown): number | undefined;
|
package/dist/execution-mode.js
CHANGED
|
@@ -61,3 +61,24 @@ export function stripExecutionWrapper(input) {
|
|
|
61
61
|
}
|
|
62
62
|
return rest;
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* 从工具调用入参的 execution 包装字段中提取客户端显式声明的超时时间。
|
|
66
|
+
*
|
|
67
|
+
* @param input 工具调用入参
|
|
68
|
+
* @returns 客户端声明的超时毫秒数,未声明或非法时返回 undefined
|
|
69
|
+
*/
|
|
70
|
+
export function extractExecutionTimeoutMs(input) {
|
|
71
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const execution = input.execution;
|
|
75
|
+
if (typeof execution !== "object" ||
|
|
76
|
+
execution === null ||
|
|
77
|
+
Array.isArray(execution)) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const timeoutMs = execution.timeoutMs;
|
|
81
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
82
|
+
? timeoutMs
|
|
83
|
+
: undefined;
|
|
84
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -12,6 +12,12 @@ export function startMcpHttpServer(options = {}) {
|
|
|
12
12
|
const port = options.port ?? 5178;
|
|
13
13
|
const host = hostString;
|
|
14
14
|
const token = options.token;
|
|
15
|
+
// 死选项透明化:allowInsecureHttp 是客户端侧豁免开关,服务端明文 HTTP 监听
|
|
16
|
+
// 由 allowInsecureNoAuth 与 token 策略约束,传入该字段不会产生任何效果,
|
|
17
|
+
// 检测到时输出警告避免调用方误以为已生效
|
|
18
|
+
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
|
+
}
|
|
15
21
|
// Non-loopback address requires token authentication by default
|
|
16
22
|
if (!isLoopbackHost(host) && !token && !options.allowInsecureNoAuth) {
|
|
17
23
|
throw new Error("Authentication token is required when binding to a non-loopback address. Use --allow-insecure-no-auth to override.");
|
|
@@ -196,24 +202,26 @@ export function startMcpHttpServer(options = {}) {
|
|
|
196
202
|
url,
|
|
197
203
|
target,
|
|
198
204
|
stop: async () => {
|
|
199
|
-
|
|
205
|
+
// 先停 HTTP 服务(等待在途请求收尾)再释放 target:
|
|
206
|
+
// 若先关 target,在途请求的后续 Action 调用会全部异常
|
|
207
|
+
let serverError;
|
|
200
208
|
try {
|
|
201
|
-
await
|
|
209
|
+
await server.stop(true);
|
|
202
210
|
}
|
|
203
211
|
catch (err) {
|
|
204
|
-
|
|
212
|
+
serverError = err;
|
|
205
213
|
}
|
|
206
214
|
try {
|
|
207
|
-
await
|
|
215
|
+
await target.close();
|
|
208
216
|
}
|
|
209
|
-
catch (
|
|
210
|
-
if (
|
|
211
|
-
throw new AggregateError([
|
|
217
|
+
catch (targetErr) {
|
|
218
|
+
if (serverError) {
|
|
219
|
+
throw new AggregateError([serverError, targetErr], "Failed to stop MCP HTTP server and target");
|
|
212
220
|
}
|
|
213
|
-
throw
|
|
221
|
+
throw targetErr;
|
|
214
222
|
}
|
|
215
|
-
if (
|
|
216
|
-
throw
|
|
223
|
+
if (serverError) {
|
|
224
|
+
throw serverError;
|
|
217
225
|
}
|
|
218
226
|
},
|
|
219
227
|
};
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { fromJsonSchema } from "@modelcontextprotocol/server";
|
|
2
2
|
import { toMcpTaskPayload, toMcpTaskStatus } from "./types.js";
|
|
3
|
+
/** JSON-RPC 保留码段:协议保留区为 -32000 至 -32099,服务端自定义错误码必须落在该区段内。 */
|
|
4
|
+
const TASK_NOT_FOUND_CODE = -32001;
|
|
5
|
+
/** 无效的 limit 参数错误码,同样落在服务端自定义码段。 */
|
|
6
|
+
const INVALID_LIMIT_CODE = -32002;
|
|
7
|
+
/** tasks/list 允许的最大单页数量上限。 */
|
|
8
|
+
const MAX_TASK_LIST_LIMIT = 500;
|
|
9
|
+
/**
|
|
10
|
+
* 构造携带 JSON-RPC 语义码的任务不存在异常。
|
|
11
|
+
*
|
|
12
|
+
* MCP SDK 对处理器抛出的异常会读取数字型 code 字段并原样透传到错误响应,
|
|
13
|
+
* 裸 Error 缺少 code 会被映射为内部错误(-32603),丢失调用方可分辨的语义。
|
|
14
|
+
*
|
|
15
|
+
* @param taskId 未找到的任务标识
|
|
16
|
+
*/
|
|
17
|
+
function taskNotFoundError(taskId) {
|
|
18
|
+
return Object.assign(new Error(`Task '${taskId}' not found`), { code: TASK_NOT_FOUND_CODE });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 构造携带 JSON-RPC 语义码的非法 limit 参数异常。
|
|
22
|
+
*
|
|
23
|
+
* @param limit 非法的 limit 入参
|
|
24
|
+
*/
|
|
25
|
+
function invalidLimitError(limit) {
|
|
26
|
+
return Object.assign(new Error(`Invalid tasks/list limit: expected an integer between 1 and ${MAX_TASK_LIST_LIMIT}, got ${JSON.stringify(limit)}`), { code: INVALID_LIMIT_CODE });
|
|
27
|
+
}
|
|
3
28
|
const TASK_ID_PARAMS = fromJsonSchema({
|
|
4
29
|
type: "object",
|
|
5
30
|
properties: {
|
|
@@ -40,7 +65,8 @@ export function registerTasksExtension(server, target) {
|
|
|
40
65
|
server.server.setRequestHandler("tasks/get", { params: TASK_ID_PARAMS }, async (params) => {
|
|
41
66
|
const run = await target.getRun(params.taskId);
|
|
42
67
|
if (!run) {
|
|
43
|
-
|
|
68
|
+
// 携带语义码透传,避免被 SDK 映射为内部错误
|
|
69
|
+
throw taskNotFoundError(params.taskId);
|
|
44
70
|
}
|
|
45
71
|
return { task: toMcpTaskPayload(run) };
|
|
46
72
|
});
|
|
@@ -55,12 +81,20 @@ export function registerTasksExtension(server, target) {
|
|
|
55
81
|
}
|
|
56
82
|
const run = await target.getRun(params.taskId);
|
|
57
83
|
if (!run) {
|
|
58
|
-
|
|
84
|
+
// 携带语义码透传,避免被 SDK 映射为内部错误
|
|
85
|
+
throw taskNotFoundError(params.taskId);
|
|
59
86
|
}
|
|
60
87
|
return { taskId: params.taskId, status: toMcpTaskStatus(run.status) };
|
|
61
88
|
});
|
|
62
89
|
server.server.setRequestHandler("tasks/list", { params: TASK_LIST_PARAMS }, async (params) => {
|
|
63
|
-
|
|
90
|
+
// 边界防御:limit 必须为整数,缺省取 50;超限钳制到 [1, 500],非法值抛带语义码的参数错误
|
|
91
|
+
let limit = 50;
|
|
92
|
+
if (params.limit !== undefined) {
|
|
93
|
+
if (typeof params.limit !== "number" || !Number.isInteger(params.limit)) {
|
|
94
|
+
throw invalidLimitError(params.limit);
|
|
95
|
+
}
|
|
96
|
+
limit = Math.min(Math.max(params.limit, 1), MAX_TASK_LIST_LIMIT);
|
|
97
|
+
}
|
|
64
98
|
const actionId = params.actionId;
|
|
65
99
|
const runs = await target.listRuns({ limit, actionId });
|
|
66
100
|
const ordered = [...runs].sort((a, b) => toTimeMillis(b.startedAt) - toTimeMillis(a.startedAt));
|
package/dist/schemas.d.ts
CHANGED
|
@@ -2,5 +2,10 @@ import type { JsonSchema } from "@actiondock/sdk";
|
|
|
2
2
|
/**
|
|
3
3
|
* 将 ActionDock 标准 JSON Schema 归一化为 MCP 标准 Tool Schema。
|
|
4
4
|
* 布尔分支已穷尽(true / false / 对象),无其余可达路径。
|
|
5
|
+
*
|
|
6
|
+
* @param schema 原始 JSON Schema 定义
|
|
7
|
+
* @param injectExecution 是否向 schema 注入适配层的 execution 执行控制包装字段:
|
|
8
|
+
* 入参 schema 需要注入(客户端借此声明执行模式与超时),
|
|
9
|
+
* 出参 schema 不注入(实际 structuredContent 不含该字段,注入会与真实返回结构不符)
|
|
5
10
|
*/
|
|
6
|
-
export declare function toMcpSchema(schema?: JsonSchema): import("@modelcontextprotocol/server").StandardSchemaWithJSON<unknown, unknown>;
|
|
11
|
+
export declare function toMcpSchema(schema?: JsonSchema, injectExecution?: boolean): import("@modelcontextprotocol/server").StandardSchemaWithJSON<unknown, unknown>;
|
package/dist/schemas.js
CHANGED
|
@@ -2,8 +2,13 @@ import { fromJsonSchema } from "@modelcontextprotocol/server";
|
|
|
2
2
|
/**
|
|
3
3
|
* 将 ActionDock 标准 JSON Schema 归一化为 MCP 标准 Tool Schema。
|
|
4
4
|
* 布尔分支已穷尽(true / false / 对象),无其余可达路径。
|
|
5
|
+
*
|
|
6
|
+
* @param schema 原始 JSON Schema 定义
|
|
7
|
+
* @param injectExecution 是否向 schema 注入适配层的 execution 执行控制包装字段:
|
|
8
|
+
* 入参 schema 需要注入(客户端借此声明执行模式与超时),
|
|
9
|
+
* 出参 schema 不注入(实际 structuredContent 不含该字段,注入会与真实返回结构不符)
|
|
5
10
|
*/
|
|
6
|
-
export function toMcpSchema(schema) {
|
|
11
|
+
export function toMcpSchema(schema, injectExecution = true) {
|
|
7
12
|
if (schema === undefined || schema === true) {
|
|
8
13
|
return fromJsonSchema({});
|
|
9
14
|
}
|
|
@@ -15,7 +20,7 @@ export function toMcpSchema(schema) {
|
|
|
15
20
|
// JSON Schema 为纯数据结构,structuredClone 深拷贝避免污染调用方原始定义;布尔分支已穷尽,收窄为对象模式
|
|
16
21
|
const cloned = structuredClone(schema);
|
|
17
22
|
const properties = (cloned.properties ?? {});
|
|
18
|
-
if (cloned.type === "object" || cloned.properties) {
|
|
23
|
+
if ((cloned.type === "object" || cloned.properties) && injectExecution) {
|
|
19
24
|
cloned.properties = properties;
|
|
20
25
|
properties.execution = {
|
|
21
26
|
type: "object",
|
package/dist/stdio.js
CHANGED
|
@@ -111,4 +111,24 @@ export async function startMcpStdio(options = {}) {
|
|
|
111
111
|
};
|
|
112
112
|
process.once("SIGINT", () => handleSignal("SIGINT"));
|
|
113
113
|
process.once("SIGTERM", () => handleSignal("SIGTERM"));
|
|
114
|
+
// MCP 客户端断开链路:serveStdio 在 stdin 关闭后仅关闭传输层,不退出进程,
|
|
115
|
+
// 监督进程与 IPC 宿主子进程会永久悬挂。这里监听 stdin 的 end 与 close 事件,
|
|
116
|
+
// 触发与信号一致的清理退出路径;exitCode 模式确保异步 cleanup 完成后再真正退出。
|
|
117
|
+
// 幂等防护:cleanup 已被信号或先到的 stdin 事件触发过时不重复执行
|
|
118
|
+
let cleanupTriggered = false;
|
|
119
|
+
const triggerCleanupOnce = () => {
|
|
120
|
+
if (cleanupTriggered)
|
|
121
|
+
return;
|
|
122
|
+
cleanupTriggered = true;
|
|
123
|
+
cleanup()
|
|
124
|
+
.then(() => {
|
|
125
|
+
process.exitCode = 0;
|
|
126
|
+
})
|
|
127
|
+
.catch((err) => {
|
|
128
|
+
process.stderr.write(`[MCP Stdio Cleanup Failed] ${err instanceof Error ? err.message : String(err)}\n`);
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
process.stdin.once("end", triggerCleanupOnce);
|
|
133
|
+
process.stdin.once("close", triggerCleanupOnce);
|
|
114
134
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -83,6 +83,11 @@ export interface HttpSecurityOptions {
|
|
|
83
83
|
token?: string;
|
|
84
84
|
allowInsecureNoAuth?: boolean;
|
|
85
85
|
allowQueryToken?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* 已废弃:该选项仅适用于远程 ActionDockTarget 客户端侧的明文 HTTP 豁免,
|
|
88
|
+
* 服务端监听安全性由 allowInsecureNoAuth 与 token 策略约束,传入不会产生任何效果;
|
|
89
|
+
* 服务端检测到该字段时仅输出警告日志
|
|
90
|
+
*/
|
|
86
91
|
allowInsecureHttp?: boolean;
|
|
87
92
|
corsOrigins?: string[];
|
|
88
93
|
maxBodyBytes?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actiondock/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "ActionDock MCP Adapter for exposing Actions as Model Context Protocol (MCP) Tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"test": "node ../../scripts/run-tests.ts"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@actiondock/core": "^2.
|
|
33
|
-
"@actiondock/sdk": "^2.
|
|
32
|
+
"@actiondock/core": "^2.3.0",
|
|
33
|
+
"@actiondock/sdk": "^2.3.0",
|
|
34
34
|
"@modelcontextprotocol/server": "^2.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|