@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/dist/index.js
CHANGED
|
@@ -1,574 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
};
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./schemas.js";
|
|
3
|
+
export * from "./execution-mode.js";
|
|
4
|
+
export * from "./adapter.js";
|
|
5
|
+
export * from "./stdio.js";
|
|
6
|
+
export * from "./http.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ActionDockTarget } from "@actiondock/core";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
3
|
+
/**
|
|
4
|
+
* 在低层 Server 实例上注册 tasks 能力声明与三个请求处理器。
|
|
5
|
+
*
|
|
6
|
+
* @param server MCP 高层服务实例
|
|
7
|
+
* @param target ActionDockTarget 公共契约门面
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerTasksExtension(server: McpServer, target: ActionDockTarget): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { fromJsonSchema } from "@modelcontextprotocol/server";
|
|
2
|
+
import { toMcpTaskPayload, toMcpTaskStatus } from "./types.js";
|
|
3
|
+
const TASK_ID_PARAMS = fromJsonSchema({
|
|
4
|
+
type: "object",
|
|
5
|
+
properties: {
|
|
6
|
+
taskId: { type: "string" },
|
|
7
|
+
reason: { type: "string" },
|
|
8
|
+
},
|
|
9
|
+
required: ["taskId"],
|
|
10
|
+
});
|
|
11
|
+
const TASK_LIST_PARAMS = fromJsonSchema({
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
limit: { type: "number" },
|
|
15
|
+
actionId: { type: "string" },
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* 将内部 RunRecord 的 ISO 时间字符串转为毫秒时间戳,用于任务排序比较。
|
|
20
|
+
*
|
|
21
|
+
* @param value 待解析的时间字符串
|
|
22
|
+
*/
|
|
23
|
+
function toTimeMillis(value) {
|
|
24
|
+
const parsed = Date.parse(value);
|
|
25
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 在低层 Server 实例上注册 tasks 能力声明与三个请求处理器。
|
|
29
|
+
*
|
|
30
|
+
* @param server MCP 高层服务实例
|
|
31
|
+
* @param target ActionDockTarget 公共契约门面
|
|
32
|
+
*/
|
|
33
|
+
export function registerTasksExtension(server, target) {
|
|
34
|
+
server.server.registerCapabilities({
|
|
35
|
+
tasks: {
|
|
36
|
+
list: {},
|
|
37
|
+
cancel: {},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
server.server.setRequestHandler("tasks/get", { params: TASK_ID_PARAMS }, async (params) => {
|
|
41
|
+
const run = await target.getRun(params.taskId);
|
|
42
|
+
if (!run) {
|
|
43
|
+
throw new Error(`Task '${params.taskId}' not found`);
|
|
44
|
+
}
|
|
45
|
+
return { task: toMcpTaskPayload(run) };
|
|
46
|
+
});
|
|
47
|
+
server.server.setRequestHandler("tasks/cancel", { params: TASK_ID_PARAMS }, async (params) => {
|
|
48
|
+
const reason = params.reason || "Cancelled via MCP tasks/cancel";
|
|
49
|
+
const cancelRes = await target.cancelRun(params.taskId, reason);
|
|
50
|
+
if (cancelRes.outcome === "requested") {
|
|
51
|
+
return { taskId: params.taskId, status: "cancelled" };
|
|
52
|
+
}
|
|
53
|
+
if (cancelRes.outcome === "already_terminal") {
|
|
54
|
+
return { taskId: params.taskId, status: toMcpTaskStatus(cancelRes.status) };
|
|
55
|
+
}
|
|
56
|
+
const run = await target.getRun(params.taskId);
|
|
57
|
+
if (!run) {
|
|
58
|
+
throw new Error(`Task '${params.taskId}' not found`);
|
|
59
|
+
}
|
|
60
|
+
return { taskId: params.taskId, status: toMcpTaskStatus(run.status) };
|
|
61
|
+
});
|
|
62
|
+
server.server.setRequestHandler("tasks/list", { params: TASK_LIST_PARAMS }, async (params) => {
|
|
63
|
+
const limit = typeof params.limit === "number" ? params.limit : 50;
|
|
64
|
+
const actionId = params.actionId;
|
|
65
|
+
const runs = await target.listRuns({ limit, actionId });
|
|
66
|
+
const ordered = [...runs].sort((a, b) => toTimeMillis(b.startedAt) - toTimeMillis(a.startedAt));
|
|
67
|
+
return { tasks: ordered.slice(0, limit).map(toMcpTaskPayload) };
|
|
68
|
+
});
|
|
69
|
+
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JsonSchema } from "@actiondock/sdk";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 将 ActionDock 标准 JSON Schema 归一化为 MCP 标准 Tool Schema。
|
|
4
|
+
* 布尔分支已穷尽(true / false / 对象),无其余可达路径。
|
|
4
5
|
*/
|
|
5
6
|
export declare function toMcpSchema(schema?: JsonSchema): import("@modelcontextprotocol/server").StandardSchemaWithJSON<unknown, unknown>;
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { fromJsonSchema } from "@modelcontextprotocol/server";
|
|
2
|
+
/**
|
|
3
|
+
* 将 ActionDock 标准 JSON Schema 归一化为 MCP 标准 Tool Schema。
|
|
4
|
+
* 布尔分支已穷尽(true / false / 对象),无其余可达路径。
|
|
5
|
+
*/
|
|
6
|
+
export function toMcpSchema(schema) {
|
|
7
|
+
if (schema === undefined || schema === true) {
|
|
8
|
+
return fromJsonSchema({});
|
|
9
|
+
}
|
|
10
|
+
if (schema === false) {
|
|
11
|
+
return fromJsonSchema({
|
|
12
|
+
not: {},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
// JSON Schema 为纯数据结构,structuredClone 深拷贝避免污染调用方原始定义;布尔分支已穷尽,收窄为对象模式
|
|
16
|
+
const cloned = structuredClone(schema);
|
|
17
|
+
const properties = (cloned.properties ?? {});
|
|
18
|
+
if (cloned.type === "object" || cloned.properties) {
|
|
19
|
+
cloned.properties = properties;
|
|
20
|
+
properties.execution = {
|
|
21
|
+
type: "object",
|
|
22
|
+
description: "Execution options",
|
|
23
|
+
properties: {
|
|
24
|
+
mode: { type: "string", enum: ["sync", "async"] },
|
|
25
|
+
timeoutMs: { type: "number" },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return fromJsonSchema(cloned);
|
|
30
|
+
}
|