@actiondock/mcp 2.0.11 → 2.0.12
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 +36 -0
- package/dist/http.d.ts +5 -0
- package/dist/index.js +574 -0
- package/dist/schemas.d.ts +5 -0
- package/dist/stdio.d.ts +5 -0
- package/dist/types.d.ts +74 -0
- package/package.json +12 -11
- package/src/adapter.ts +0 -437
- package/src/http.ts +0 -163
- package/src/schemas.ts +0 -19
- package/src/stdio.ts +0 -51
- package/src/types.ts +0 -105
- /package/{src/index.ts → dist/index.d.ts} +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ExecutionResult } from "@actiondock/sdk";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
3
|
+
import { type ActionDockMcpOptions } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* 判断目标值是否为普通对象(Plain Object)。
|
|
6
|
+
*
|
|
7
|
+
* @param value 待检查的值
|
|
8
|
+
*/
|
|
9
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* 将 ActionDock 标准的 ExecutionResult 信封结构转换为 MCP 协议规范的 Tool Call 返回结果。
|
|
12
|
+
*
|
|
13
|
+
* @param result ExecutionResult 结果对象
|
|
14
|
+
*/
|
|
15
|
+
export declare function toMcpResult(result: ExecutionResult): {
|
|
16
|
+
content: {
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
}[];
|
|
20
|
+
structuredContent: Record<string, unknown>;
|
|
21
|
+
isError?: undefined;
|
|
22
|
+
} | {
|
|
23
|
+
isError: boolean;
|
|
24
|
+
content: {
|
|
25
|
+
type: "text";
|
|
26
|
+
text: string;
|
|
27
|
+
}[];
|
|
28
|
+
structuredContent?: undefined;
|
|
29
|
+
};
|
|
30
|
+
export type ActionDockMcpServer = McpServer & {
|
|
31
|
+
close: () => Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Creates and configures an McpServer instance bound to one or more ActionDock packages with Tasks extension support.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createActionDockMcpServer(options?: ActionDockMcpOptions): Promise<ActionDockMcpServer>;
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ActionDockMcpHttpOptions, ActionDockMcpHttpServerInstance } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Starts an ActionDock MCP server over HTTP transport.
|
|
4
|
+
*/
|
|
5
|
+
export declare function startMcpHttpServer(options?: ActionDockMcpHttpOptions): Promise<ActionDockMcpHttpServerInstance>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
// packages/mcp/src/types.ts
|
|
2
|
+
function toMcpTaskStatus(status) {
|
|
3
|
+
switch (status) {
|
|
4
|
+
case "running":
|
|
5
|
+
return "working";
|
|
6
|
+
case "success":
|
|
7
|
+
return "completed";
|
|
8
|
+
case "failed":
|
|
9
|
+
case "timed_out":
|
|
10
|
+
case "interrupted":
|
|
11
|
+
return "failed";
|
|
12
|
+
case "cancelled":
|
|
13
|
+
return "cancelled";
|
|
14
|
+
default:
|
|
15
|
+
return "failed";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function toMcpTaskPayload(run) {
|
|
19
|
+
return {
|
|
20
|
+
taskId: run.id,
|
|
21
|
+
status: toMcpTaskStatus(run.status),
|
|
22
|
+
createdAt: run.startedAt,
|
|
23
|
+
finishedAt: run.finishedAt,
|
|
24
|
+
input: run.input,
|
|
25
|
+
output: run.output,
|
|
26
|
+
error: run.error
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// packages/mcp/src/schemas.ts
|
|
30
|
+
import { fromJsonSchema } from "@modelcontextprotocol/server";
|
|
31
|
+
function toMcpSchema(schema) {
|
|
32
|
+
if (schema === undefined || schema === true) {
|
|
33
|
+
return fromJsonSchema({});
|
|
34
|
+
}
|
|
35
|
+
if (schema === false) {
|
|
36
|
+
return fromJsonSchema({
|
|
37
|
+
not: {}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (typeof schema === "object" && schema !== null) {
|
|
41
|
+
const cloned = JSON.parse(JSON.stringify(schema));
|
|
42
|
+
if (cloned.type === "object" || cloned.properties) {
|
|
43
|
+
cloned.properties = cloned.properties || {};
|
|
44
|
+
cloned.properties.__async = { type: "boolean", description: "Execute action asynchronously" };
|
|
45
|
+
cloned.properties.execution = {
|
|
46
|
+
type: "object",
|
|
47
|
+
description: "Execution options",
|
|
48
|
+
properties: {
|
|
49
|
+
mode: { type: "string", enum: ["sync", "async"] },
|
|
50
|
+
timeoutMs: { type: "number" }
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return fromJsonSchema(cloned);
|
|
55
|
+
}
|
|
56
|
+
return fromJsonSchema(schema);
|
|
57
|
+
}
|
|
58
|
+
// packages/mcp/src/adapter.ts
|
|
59
|
+
import { createHash } from "node:crypto";
|
|
60
|
+
import { existsSync } from "node:fs";
|
|
61
|
+
import { resolve } from "node:path";
|
|
62
|
+
import {
|
|
63
|
+
ACTIONDOCK_VERSION,
|
|
64
|
+
ensureDependencyClosure,
|
|
65
|
+
findProjectRoot,
|
|
66
|
+
listLinkedPackages,
|
|
67
|
+
loadActions,
|
|
68
|
+
loadProjectConfig,
|
|
69
|
+
resolvePackageRoot,
|
|
70
|
+
ServerRuntimeRegistry
|
|
71
|
+
} from "@actiondock/core";
|
|
72
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
73
|
+
function isPlainObject(value) {
|
|
74
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const proto = Object.getPrototypeOf(value);
|
|
78
|
+
return proto === Object.prototype || proto === null;
|
|
79
|
+
}
|
|
80
|
+
function toMcpResult(result) {
|
|
81
|
+
if (result.ok) {
|
|
82
|
+
const structuredContent = isPlainObject(result.data) ? result.data : { value: result.data };
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: JSON.stringify(result)
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
structuredContent
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
isError: true,
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: "text",
|
|
98
|
+
text: JSON.stringify(result)
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function createMcpToolCallback(executionService, actionId, timeoutMs) {
|
|
104
|
+
return async (input, ctx) => {
|
|
105
|
+
const isAsync = Boolean(input && typeof input === "object" && (input.execution?.mode === "async" || input.__async === true || input.async === true));
|
|
106
|
+
const signal = ctx.mcpReq?.signal;
|
|
107
|
+
let cleanInput = input;
|
|
108
|
+
if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
109
|
+
const { execution, __async, async: _async, ...rest } = input;
|
|
110
|
+
cleanInput = rest;
|
|
111
|
+
}
|
|
112
|
+
if (isAsync) {
|
|
113
|
+
const ticket = await executionService.start(actionId, cleanInput, {
|
|
114
|
+
signal,
|
|
115
|
+
timeoutMs
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: JSON.stringify({
|
|
122
|
+
ok: true,
|
|
123
|
+
runId: ticket.runId,
|
|
124
|
+
taskId: ticket.runId,
|
|
125
|
+
status: "running"
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const result = await executionService.execute(actionId, cleanInput, {
|
|
132
|
+
signal,
|
|
133
|
+
timeoutMs
|
|
134
|
+
});
|
|
135
|
+
return toMcpResult(result);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function createActionDockMcpServer(options = {}) {
|
|
139
|
+
const targetRoots = [];
|
|
140
|
+
if (options.projectRoot) {
|
|
141
|
+
targetRoots.push(options.projectRoot);
|
|
142
|
+
}
|
|
143
|
+
if (options.projectRoots) {
|
|
144
|
+
targetRoots.push(...options.projectRoots);
|
|
145
|
+
}
|
|
146
|
+
const targetPackages = [];
|
|
147
|
+
if (options.packageId) {
|
|
148
|
+
targetPackages.push(options.packageId);
|
|
149
|
+
}
|
|
150
|
+
if (options.packageIds) {
|
|
151
|
+
targetPackages.push(...options.packageIds);
|
|
152
|
+
}
|
|
153
|
+
const resolvedRoots = new Set;
|
|
154
|
+
if (options.all) {
|
|
155
|
+
const linked = listLinkedPackages(options.customHome);
|
|
156
|
+
for (const pkg of linked) {
|
|
157
|
+
if (existsSync(pkg.path)) {
|
|
158
|
+
resolvedRoots.add(resolve(pkg.path));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const currentRoot = findProjectRoot(process.cwd());
|
|
162
|
+
if (currentRoot) {
|
|
163
|
+
resolvedRoots.add(resolve(currentRoot));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const pkgId of targetPackages) {
|
|
167
|
+
const root = resolvePackageRoot(pkgId, undefined, options.customHome);
|
|
168
|
+
if (!root || !existsSync(root)) {
|
|
169
|
+
throw new Error(`Package '${pkgId}' not found in registry`);
|
|
170
|
+
}
|
|
171
|
+
resolvedRoots.add(resolve(root));
|
|
172
|
+
}
|
|
173
|
+
for (const dir of targetRoots) {
|
|
174
|
+
const absPath = resolve(dir);
|
|
175
|
+
const root = findProjectRoot(absPath);
|
|
176
|
+
if (!root) {
|
|
177
|
+
throw new Error(`Project root '${dir}' is not a valid ActionDock package (actiondock.json not found)`);
|
|
178
|
+
}
|
|
179
|
+
resolvedRoots.add(resolve(root));
|
|
180
|
+
}
|
|
181
|
+
if (resolvedRoots.size === 0 && !options.all) {
|
|
182
|
+
const currentRoot = findProjectRoot(process.cwd());
|
|
183
|
+
if (currentRoot) {
|
|
184
|
+
resolvedRoots.add(resolve(currentRoot));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const isInternalRegistry = !options.runtimeRegistry;
|
|
188
|
+
const runtimeRegistry = options.runtimeRegistry ?? new ServerRuntimeRegistry(options.customHome);
|
|
189
|
+
const executionManager = options.executionManager ?? runtimeRegistry.executionManager;
|
|
190
|
+
const targets = [];
|
|
191
|
+
if (resolvedRoots.size === 0 && options.actions) {
|
|
192
|
+
const dummyConfig = {
|
|
193
|
+
id: "virtual",
|
|
194
|
+
name: "Virtual Package",
|
|
195
|
+
version: ACTIONDOCK_VERSION,
|
|
196
|
+
description: "In-memory virtual package",
|
|
197
|
+
actionsDir: "actions",
|
|
198
|
+
playbooksDir: "playbooks"
|
|
199
|
+
};
|
|
200
|
+
const storage = options.storage ?? runtimeRegistry.getStorage("virtual");
|
|
201
|
+
const executionService = runtimeRegistry.getExecutionService("virtual", undefined, dummyConfig, {
|
|
202
|
+
storage,
|
|
203
|
+
actions: options.actions,
|
|
204
|
+
configOverrides: options.configOverrides
|
|
205
|
+
});
|
|
206
|
+
targets.push({
|
|
207
|
+
projectRoot: "virtual",
|
|
208
|
+
config: dummyConfig,
|
|
209
|
+
actions: options.actions,
|
|
210
|
+
storage,
|
|
211
|
+
executionService
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
if (resolvedRoots.size === 0) {
|
|
215
|
+
throw new Error("No ActionDock project root found. Run inside an ActionDock package or specify --dir / --package / --all.");
|
|
216
|
+
}
|
|
217
|
+
await ensureDependencyClosure(Array.from(resolvedRoots), {
|
|
218
|
+
customHome: options.customHome
|
|
219
|
+
});
|
|
220
|
+
for (const root of resolvedRoots) {
|
|
221
|
+
const projectConfig = loadProjectConfig(root);
|
|
222
|
+
const actions = options.actions && resolvedRoots.size === 1 ? options.actions : await loadActions(root, projectConfig.actionsDir, { autoInstall: false });
|
|
223
|
+
const storage = options.storage && resolvedRoots.size === 1 ? options.storage : runtimeRegistry.getStorage(projectConfig.id, root);
|
|
224
|
+
const executionService = runtimeRegistry.getExecutionService(projectConfig.id, root, projectConfig, {
|
|
225
|
+
storage,
|
|
226
|
+
actions,
|
|
227
|
+
configOverrides: options.configOverrides
|
|
228
|
+
});
|
|
229
|
+
targets.push({
|
|
230
|
+
projectRoot: root,
|
|
231
|
+
config: projectConfig,
|
|
232
|
+
actions,
|
|
233
|
+
storage,
|
|
234
|
+
executionService
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const isMultiPackage = targets.length > 1;
|
|
239
|
+
const actionIdCounts = new Map;
|
|
240
|
+
for (const target of targets) {
|
|
241
|
+
for (const actionId of target.actions.keys()) {
|
|
242
|
+
actionIdCounts.set(actionId, (actionIdCounts.get(actionId) || 0) + 1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const serverName = targets.length === 1 ? targets[0].config.id || targets[0].config.name || "actiondock" : "actiondock";
|
|
246
|
+
const serverVersion = targets.length === 1 ? targets[0].config.version || ACTIONDOCK_VERSION : ACTIONDOCK_VERSION;
|
|
247
|
+
const server = new McpServer({
|
|
248
|
+
name: serverName,
|
|
249
|
+
version: serverVersion
|
|
250
|
+
});
|
|
251
|
+
server.server.registerCapabilities({
|
|
252
|
+
tasks: {
|
|
253
|
+
listChanged: true,
|
|
254
|
+
cancel: {}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
for (const target of targets) {
|
|
258
|
+
for (const action of target.actions.values()) {
|
|
259
|
+
const count = actionIdCounts.get(action.id) || 1;
|
|
260
|
+
const cleanPkgId = target.config.id.replace(/^@/, "").replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
261
|
+
let toolName = count > 1 ? `${cleanPkgId}_${action.id}` : action.id;
|
|
262
|
+
if (toolName.length > 64) {
|
|
263
|
+
const hash = createHash("sha256").update(toolName).digest("hex").slice(0, 8);
|
|
264
|
+
toolName = `${toolName.slice(0, 55)}_${hash}`;
|
|
265
|
+
}
|
|
266
|
+
const description = isMultiPackage ? `[${target.config.id}] ${action.description || ""}` : action.description;
|
|
267
|
+
server.registerTool(toolName, {
|
|
268
|
+
description,
|
|
269
|
+
inputSchema: toMcpSchema(action.inputSchema),
|
|
270
|
+
outputSchema: action.outputSchema ? toMcpSchema(action.outputSchema) : undefined
|
|
271
|
+
}, createMcpToolCallback(target.executionService, action.id, options.timeoutMs));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const storages = targets.map((t) => t.storage);
|
|
275
|
+
server.server.setRequestHandler("tasks/get", async (req) => {
|
|
276
|
+
const taskId = req.params?.taskId;
|
|
277
|
+
if (!taskId) {
|
|
278
|
+
throw new Error("taskId parameter is required for tasks/get");
|
|
279
|
+
}
|
|
280
|
+
for (const storage of storages) {
|
|
281
|
+
const run = storage.getRun(taskId);
|
|
282
|
+
if (run) {
|
|
283
|
+
return {
|
|
284
|
+
task: toMcpTaskPayload(run)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
289
|
+
});
|
|
290
|
+
server.server.setRequestHandler("tasks/cancel", async (req) => {
|
|
291
|
+
const taskId = req.params?.taskId;
|
|
292
|
+
if (!taskId) {
|
|
293
|
+
throw new Error("taskId parameter is required for tasks/cancel");
|
|
294
|
+
}
|
|
295
|
+
for (const target of targets) {
|
|
296
|
+
const cancelRes = await target.executionService.cancel(taskId, req.params?.reason || "Cancelled via MCP tasks/cancel");
|
|
297
|
+
if (cancelRes.outcome === "requested") {
|
|
298
|
+
return {
|
|
299
|
+
taskId,
|
|
300
|
+
status: "cancelled"
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (cancelRes.outcome === "already_terminal") {
|
|
304
|
+
return {
|
|
305
|
+
taskId,
|
|
306
|
+
status: toMcpTaskStatus(cancelRes.status)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const storage of storages) {
|
|
311
|
+
const run = storage.getRun(taskId);
|
|
312
|
+
if (run) {
|
|
313
|
+
if (run.status === "running") {
|
|
314
|
+
storage.updateRun(taskId, "cancelled", undefined, {
|
|
315
|
+
code: "ACTION_CANCELLED",
|
|
316
|
+
message: req.params?.reason || "Cancelled via MCP tasks/cancel"
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
taskId,
|
|
320
|
+
status: "cancelled"
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
taskId,
|
|
325
|
+
status: toMcpTaskStatus(run.status)
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
330
|
+
});
|
|
331
|
+
server.server.setRequestHandler("tasks/list", async (req) => {
|
|
332
|
+
const limit = typeof req.params?.limit === "number" ? req.params.limit : 50;
|
|
333
|
+
const actionId = req.params?.actionId;
|
|
334
|
+
const allRuns = [];
|
|
335
|
+
for (const storage of storages) {
|
|
336
|
+
const runs = storage.listRuns({ limit, actionId });
|
|
337
|
+
allRuns.push(...runs);
|
|
338
|
+
}
|
|
339
|
+
allRuns.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
340
|
+
const trimmed = allRuns.slice(0, limit);
|
|
341
|
+
return {
|
|
342
|
+
tasks: trimmed.map(toMcpTaskPayload)
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
const originalClose = server.close.bind(server);
|
|
346
|
+
let isClosed = false;
|
|
347
|
+
const closeFn = async () => {
|
|
348
|
+
if (isClosed)
|
|
349
|
+
return;
|
|
350
|
+
isClosed = true;
|
|
351
|
+
process.removeListener("exit", onProcessExit);
|
|
352
|
+
if (isInternalRegistry) {
|
|
353
|
+
for (const target of targets) {
|
|
354
|
+
if (target.storage !== options.storage) {
|
|
355
|
+
try {
|
|
356
|
+
target.storage.close();
|
|
357
|
+
} catch {}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
await runtimeRegistry.close();
|
|
362
|
+
} catch {}
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
await originalClose();
|
|
366
|
+
} catch {}
|
|
367
|
+
};
|
|
368
|
+
const onProcessExit = () => {
|
|
369
|
+
if (isInternalRegistry) {
|
|
370
|
+
for (const target of targets) {
|
|
371
|
+
if (target.storage !== options.storage) {
|
|
372
|
+
try {
|
|
373
|
+
target.storage.close();
|
|
374
|
+
} catch {}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
runtimeRegistry.close();
|
|
379
|
+
} catch {}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
process.once("exit", onProcessExit);
|
|
383
|
+
server.close = closeFn;
|
|
384
|
+
return server;
|
|
385
|
+
}
|
|
386
|
+
// packages/mcp/src/stdio.ts
|
|
387
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
388
|
+
async function startMcpStdio(options = {}) {
|
|
389
|
+
let activeServer;
|
|
390
|
+
const stdioHandler = serveStdio(async () => {
|
|
391
|
+
const server = await createActionDockMcpServer(options);
|
|
392
|
+
activeServer = server;
|
|
393
|
+
return server;
|
|
394
|
+
}, {
|
|
395
|
+
onerror: (err) => {
|
|
396
|
+
process.stderr.write(`[MCP Error] ${err?.message || String(err)}
|
|
397
|
+
`);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
const cleanup = async () => {
|
|
401
|
+
if (activeServer) {
|
|
402
|
+
try {
|
|
403
|
+
await activeServer.close();
|
|
404
|
+
} catch {}
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
process.once("SIGINT", async () => {
|
|
408
|
+
await cleanup();
|
|
409
|
+
try {
|
|
410
|
+
await stdioHandler.close();
|
|
411
|
+
} catch {}
|
|
412
|
+
process.exit(0);
|
|
413
|
+
});
|
|
414
|
+
process.once("SIGTERM", async () => {
|
|
415
|
+
await cleanup();
|
|
416
|
+
try {
|
|
417
|
+
await stdioHandler.close();
|
|
418
|
+
} catch {}
|
|
419
|
+
process.exit(0);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
// packages/mcp/src/http.ts
|
|
423
|
+
import {
|
|
424
|
+
ACTIONDOCK_VERSION as ACTIONDOCK_VERSION2,
|
|
425
|
+
isLoopbackHost,
|
|
426
|
+
launchHttpServer,
|
|
427
|
+
resolveCorsHeaders,
|
|
428
|
+
ServerRuntimeRegistry as ServerRuntimeRegistry2,
|
|
429
|
+
verifyBearerToken
|
|
430
|
+
} from "@actiondock/core";
|
|
431
|
+
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
432
|
+
function startMcpHttpServer(options = {}) {
|
|
433
|
+
const port = options.port ?? 5178;
|
|
434
|
+
const host = options.host ?? "127.0.0.1";
|
|
435
|
+
const token = options.token;
|
|
436
|
+
if (!isLoopbackHost(host) && !token && !options.allowInsecureNoAuth) {
|
|
437
|
+
throw new Error("Authentication token is required when binding to a non-loopback address. Use --allow-insecure-no-auth to override.");
|
|
438
|
+
}
|
|
439
|
+
return (async () => {
|
|
440
|
+
const isInternalRegistry = !options.runtimeRegistry;
|
|
441
|
+
const runtimeRegistry = options.runtimeRegistry ?? new ServerRuntimeRegistry2(options.customHome);
|
|
442
|
+
const handler = createMcpHandler(() => {
|
|
443
|
+
return createActionDockMcpServer({
|
|
444
|
+
...options,
|
|
445
|
+
runtimeRegistry,
|
|
446
|
+
executionManager: runtimeRegistry.executionManager
|
|
447
|
+
});
|
|
448
|
+
}, {
|
|
449
|
+
onerror: (err) => {
|
|
450
|
+
process.stderr.write(`[MCP HTTP Error] ${err?.message || String(err)}
|
|
451
|
+
`);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
const server = await launchHttpServer(port, host, async (req) => {
|
|
455
|
+
const origin = req.headers.get("origin");
|
|
456
|
+
const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
|
|
457
|
+
if (req.method === "OPTIONS") {
|
|
458
|
+
return new Response(null, {
|
|
459
|
+
status: 204,
|
|
460
|
+
headers: corsHeaders
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const url = new URL(req.url);
|
|
464
|
+
const pathname = url.pathname;
|
|
465
|
+
if (pathname === "/health" || pathname === "/api/v1/health") {
|
|
466
|
+
if (!verifyBearerToken(req, token)) {
|
|
467
|
+
return new Response(JSON.stringify({
|
|
468
|
+
ok: false,
|
|
469
|
+
error: {
|
|
470
|
+
code: "UNAUTHORIZED",
|
|
471
|
+
message: "Invalid or missing Bearer token"
|
|
472
|
+
}
|
|
473
|
+
}), {
|
|
474
|
+
status: 401,
|
|
475
|
+
headers: {
|
|
476
|
+
"Content-Type": "application/json",
|
|
477
|
+
...corsHeaders
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return new Response(JSON.stringify({
|
|
482
|
+
status: "ok",
|
|
483
|
+
protocol: "mcp",
|
|
484
|
+
version: ACTIONDOCK_VERSION2,
|
|
485
|
+
timestamp: new Date().toISOString()
|
|
486
|
+
}), {
|
|
487
|
+
status: 200,
|
|
488
|
+
headers: {
|
|
489
|
+
"Content-Type": "application/json",
|
|
490
|
+
...corsHeaders
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
if (!verifyBearerToken(req, token)) {
|
|
495
|
+
return new Response(JSON.stringify({
|
|
496
|
+
jsonrpc: "2.0",
|
|
497
|
+
error: {
|
|
498
|
+
code: -32000,
|
|
499
|
+
message: "Unauthorized: Invalid or missing Bearer token"
|
|
500
|
+
},
|
|
501
|
+
id: null
|
|
502
|
+
}), {
|
|
503
|
+
status: 401,
|
|
504
|
+
headers: {
|
|
505
|
+
"Content-Type": "application/json",
|
|
506
|
+
...corsHeaders
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (pathname === "/mcp" || pathname === "/") {
|
|
511
|
+
const mcpResponse = await handler.fetch(req);
|
|
512
|
+
if (Object.keys(corsHeaders).length > 0) {
|
|
513
|
+
const newHeaders = new Headers(mcpResponse.headers);
|
|
514
|
+
for (const [k, v] of Object.entries(corsHeaders)) {
|
|
515
|
+
newHeaders.set(k, String(v));
|
|
516
|
+
}
|
|
517
|
+
return new Response(mcpResponse.body, {
|
|
518
|
+
status: mcpResponse.status,
|
|
519
|
+
statusText: mcpResponse.statusText,
|
|
520
|
+
headers: newHeaders
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
return mcpResponse;
|
|
524
|
+
}
|
|
525
|
+
return new Response(JSON.stringify({
|
|
526
|
+
error: "Not Found"
|
|
527
|
+
}), {
|
|
528
|
+
status: 404,
|
|
529
|
+
headers: {
|
|
530
|
+
"Content-Type": "application/json",
|
|
531
|
+
...corsHeaders
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
const actualHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
536
|
+
const url = `http://${actualHost}:${server.port}`;
|
|
537
|
+
return {
|
|
538
|
+
port: server.port ?? port,
|
|
539
|
+
host,
|
|
540
|
+
url,
|
|
541
|
+
stop: async () => {
|
|
542
|
+
let registryError;
|
|
543
|
+
if (isInternalRegistry) {
|
|
544
|
+
try {
|
|
545
|
+
await runtimeRegistry.close();
|
|
546
|
+
} catch (err) {
|
|
547
|
+
registryError = err;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
await server.stop(true);
|
|
552
|
+
} catch (serverErr) {
|
|
553
|
+
if (registryError) {
|
|
554
|
+
throw new AggregateError([registryError, serverErr], "Failed to stop MCP HTTP server and runtime registry");
|
|
555
|
+
}
|
|
556
|
+
throw serverErr;
|
|
557
|
+
}
|
|
558
|
+
if (registryError) {
|
|
559
|
+
throw registryError;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
})();
|
|
564
|
+
}
|
|
565
|
+
export {
|
|
566
|
+
createActionDockMcpServer,
|
|
567
|
+
isPlainObject,
|
|
568
|
+
startMcpHttpServer,
|
|
569
|
+
startMcpStdio,
|
|
570
|
+
toMcpResult,
|
|
571
|
+
toMcpSchema,
|
|
572
|
+
toMcpTaskPayload,
|
|
573
|
+
toMcpTaskStatus
|
|
574
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { JsonSchema } from "@actiondock/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes an ActionDock JSON Schema into a standard MCP Tool Schema.
|
|
4
|
+
*/
|
|
5
|
+
export declare function toMcpSchema(schema?: JsonSchema): import("@modelcontextprotocol/server").StandardSchemaWithJSON<unknown, unknown>;
|
package/dist/stdio.d.ts
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ExecutionManager, RuntimeStorage, ServerRuntimeRegistry } 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;
|
|
27
|
+
/**
|
|
28
|
+
* ActionDock MCP 适配层初始化选项。
|
|
29
|
+
*/
|
|
30
|
+
export interface ActionDockMcpOptions {
|
|
31
|
+
/** 单个目标项目根目录 */
|
|
32
|
+
projectRoot?: string;
|
|
33
|
+
/** 多个项目根目录(用于多包聚合提供) */
|
|
34
|
+
projectRoots?: string[];
|
|
35
|
+
/** 目标 Package ID */
|
|
36
|
+
packageId?: string;
|
|
37
|
+
/** 多个 Package ID 列表 */
|
|
38
|
+
packageIds?: string[];
|
|
39
|
+
/** 是否聚合暴露全局 Registry 中的所有 Package */
|
|
40
|
+
all?: boolean;
|
|
41
|
+
/** 自定义家目录路径 */
|
|
42
|
+
customHome?: string;
|
|
43
|
+
/** 配置动态覆盖项 */
|
|
44
|
+
configOverrides?: Record<string, unknown>;
|
|
45
|
+
/** 单个 Tool 执行超时时间(毫秒) */
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
/** 预加载的 Action 映射表 */
|
|
48
|
+
actions?: Map<string, ActionDefinition>;
|
|
49
|
+
/** 底层存储实例 */
|
|
50
|
+
storage?: RuntimeStorage;
|
|
51
|
+
/** 服务端运行时注册表 */
|
|
52
|
+
runtimeRegistry?: ServerRuntimeRegistry;
|
|
53
|
+
/** 活跃执行任务管理器 */
|
|
54
|
+
executionManager?: ExecutionManager;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* HTTP 传输协议安全配置项。
|
|
58
|
+
*/
|
|
59
|
+
export interface HttpSecurityOptions {
|
|
60
|
+
host?: string;
|
|
61
|
+
port?: number;
|
|
62
|
+
token?: string;
|
|
63
|
+
allowInsecureNoAuth?: boolean;
|
|
64
|
+
corsOrigins?: string[];
|
|
65
|
+
maxBodyBytes?: number;
|
|
66
|
+
}
|
|
67
|
+
export interface ActionDockMcpHttpOptions extends ActionDockMcpOptions, HttpSecurityOptions {
|
|
68
|
+
}
|
|
69
|
+
export interface ActionDockMcpHttpServerInstance {
|
|
70
|
+
port: number;
|
|
71
|
+
host: string;
|
|
72
|
+
url: string;
|
|
73
|
+
stop: () => Promise<void>;
|
|
74
|
+
}
|