@actiondock/mcp 2.0.11-beta.0 → 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
package/package.json
CHANGED
|
@@ -1,38 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actiondock/mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.12",
|
|
4
4
|
"description": "ActionDock MCP Adapter for exposing Actions as Model Context Protocol (MCP) Tools",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
7
|
-
"module": "./
|
|
8
|
-
"types": "./
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"
|
|
12
|
-
"
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
|
-
"
|
|
17
|
+
"dist",
|
|
17
18
|
"README.md"
|
|
18
19
|
],
|
|
19
20
|
"engines": {
|
|
20
|
-
"node": ">=22.
|
|
21
|
+
"node": ">=22.13.0"
|
|
21
22
|
},
|
|
22
23
|
"publishConfig": {
|
|
23
24
|
"access": "public",
|
|
24
25
|
"registry": "https://registry.npmjs.org/"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
28
|
+
"build": "bun run ../../scripts/build.ts",
|
|
27
29
|
"test": "bun test"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
30
|
-
"@actiondock/core": "2.0.
|
|
31
|
-
"@actiondock/sdk": "2.0.
|
|
32
|
+
"@actiondock/core": "^2.0.12",
|
|
33
|
+
"@actiondock/sdk": "^2.0.12",
|
|
32
34
|
"@modelcontextprotocol/server": "^2.0.0"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
|
-
"@types/bun": "latest",
|
|
36
37
|
"typescript": "^5.7.0"
|
|
37
38
|
},
|
|
38
39
|
"keywords": [
|
package/src/adapter.ts
DELETED
|
@@ -1,437 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
ActionRunner,
|
|
5
|
-
createStorage,
|
|
6
|
-
ExecutionManager,
|
|
7
|
-
findProjectRoot,
|
|
8
|
-
listLinkedPackages,
|
|
9
|
-
loadActions,
|
|
10
|
-
loadProjectConfig,
|
|
11
|
-
resolvePackageRoot,
|
|
12
|
-
ServerRuntimeRegistry,
|
|
13
|
-
} from "@actiondock/core";
|
|
14
|
-
import type { ProjectConfig, RuntimeStorage } from "@actiondock/core";
|
|
15
|
-
import type { ActionDefinition, ExecutionResult, RunRecord } from "@actiondock/sdk";
|
|
16
|
-
import { McpServer } from "@modelcontextprotocol/server";
|
|
17
|
-
import { toMcpSchema } from "./schemas";
|
|
18
|
-
import { toMcpTaskPayload, type ActionDockMcpOptions } from "./types";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 判断目标值是否为普通对象(Plain Object)。
|
|
22
|
-
*
|
|
23
|
-
* @param value 待检查的值
|
|
24
|
-
*/
|
|
25
|
-
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
26
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
const proto = Object.getPrototypeOf(value);
|
|
30
|
-
return proto === Object.prototype || proto === null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 将 ActionDock 标准的 ExecutionResult 信封结构转换为 MCP 协议规范的 Tool Call 返回结果。
|
|
35
|
-
*
|
|
36
|
-
* @param result ExecutionResult 结果对象
|
|
37
|
-
*/
|
|
38
|
-
export function toMcpResult(result: ExecutionResult) {
|
|
39
|
-
if (result.ok) {
|
|
40
|
-
const structuredContent = isPlainObject(result.data)
|
|
41
|
-
? (result.data as Record<string, unknown>)
|
|
42
|
-
: { value: result.data };
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
content: [
|
|
46
|
-
{
|
|
47
|
-
type: "text" as const,
|
|
48
|
-
text: JSON.stringify(result),
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
structuredContent,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
isError: true,
|
|
57
|
-
content: [
|
|
58
|
-
{
|
|
59
|
-
type: "text" as const,
|
|
60
|
-
text: JSON.stringify(result),
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface ResolvedTarget {
|
|
67
|
-
projectRoot: string;
|
|
68
|
-
config: ProjectConfig;
|
|
69
|
-
actions: Map<string, ActionDefinition>;
|
|
70
|
-
storage: RuntimeStorage;
|
|
71
|
-
runner: ActionRunner;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function createMcpToolCallback(
|
|
75
|
-
runner: ActionRunner,
|
|
76
|
-
actionId: string,
|
|
77
|
-
executionManager: ExecutionManager,
|
|
78
|
-
timeoutMs?: number
|
|
79
|
-
) {
|
|
80
|
-
return async (input: any, ctx: any) => {
|
|
81
|
-
const isAsync = Boolean(
|
|
82
|
-
input &&
|
|
83
|
-
typeof input === "object" &&
|
|
84
|
-
(input.execution?.mode === "async" ||
|
|
85
|
-
input.__async === true ||
|
|
86
|
-
input.async === true)
|
|
87
|
-
);
|
|
88
|
-
const signal = ctx.mcpReq?.signal;
|
|
89
|
-
|
|
90
|
-
if (isAsync) {
|
|
91
|
-
const handle = runner.start(actionId, input, {
|
|
92
|
-
signal,
|
|
93
|
-
timeoutMs,
|
|
94
|
-
});
|
|
95
|
-
executionManager.register(handle);
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
content: [
|
|
99
|
-
{
|
|
100
|
-
type: "text" as const,
|
|
101
|
-
text: JSON.stringify({
|
|
102
|
-
ok: true,
|
|
103
|
-
runId: handle.runId,
|
|
104
|
-
taskId: handle.runId,
|
|
105
|
-
status: "running",
|
|
106
|
-
}),
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const handle = runner.start(actionId, input, {
|
|
113
|
-
signal,
|
|
114
|
-
timeoutMs,
|
|
115
|
-
});
|
|
116
|
-
executionManager.register(handle);
|
|
117
|
-
const result = await handle.result;
|
|
118
|
-
return toMcpResult(result);
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export type ActionDockMcpServer = McpServer & {
|
|
123
|
-
close: () => Promise<void>;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Creates and configures an McpServer instance bound to one or more ActionDock packages with Tasks extension support.
|
|
128
|
-
*/
|
|
129
|
-
export async function createActionDockMcpServer(
|
|
130
|
-
options: ActionDockMcpOptions = {}
|
|
131
|
-
): Promise<ActionDockMcpServer> {
|
|
132
|
-
const targetRoots: string[] = [];
|
|
133
|
-
if (options.projectRoot) {
|
|
134
|
-
targetRoots.push(options.projectRoot);
|
|
135
|
-
}
|
|
136
|
-
if (options.projectRoots) {
|
|
137
|
-
targetRoots.push(...options.projectRoots);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const targetPackages: string[] = [];
|
|
141
|
-
if (options.packageId) {
|
|
142
|
-
targetPackages.push(options.packageId);
|
|
143
|
-
}
|
|
144
|
-
if (options.packageIds) {
|
|
145
|
-
targetPackages.push(...options.packageIds);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const resolvedRoots = new Set<string>();
|
|
149
|
-
|
|
150
|
-
// 1. Handle --all: discover all linked packages from registry
|
|
151
|
-
if (options.all) {
|
|
152
|
-
const linked = listLinkedPackages(options.customHome);
|
|
153
|
-
for (const pkg of linked) {
|
|
154
|
-
if (existsSync(pkg.path)) {
|
|
155
|
-
resolvedRoots.add(resolve(pkg.path));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const currentRoot = findProjectRoot(process.cwd());
|
|
159
|
-
if (currentRoot) {
|
|
160
|
-
resolvedRoots.add(resolve(currentRoot));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// 2. Handle specific package IDs
|
|
165
|
-
for (const pkgId of targetPackages) {
|
|
166
|
-
const root = resolvePackageRoot(pkgId, undefined, options.customHome);
|
|
167
|
-
if (!root || !existsSync(root)) {
|
|
168
|
-
throw new Error(`Package '${pkgId}' not found in registry`);
|
|
169
|
-
}
|
|
170
|
-
resolvedRoots.add(resolve(root));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 3. Handle specific directory paths
|
|
174
|
-
for (const dir of targetRoots) {
|
|
175
|
-
const absPath = resolve(dir);
|
|
176
|
-
const root = findProjectRoot(absPath);
|
|
177
|
-
if (!root) {
|
|
178
|
-
throw new Error(
|
|
179
|
-
`Project root '${dir}' is not a valid ActionDock package (actiondock.json not found)`
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
resolvedRoots.add(resolve(root));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// 4. Default fallback: current working directory
|
|
186
|
-
if (resolvedRoots.size === 0 && !options.all) {
|
|
187
|
-
const currentRoot = findProjectRoot(process.cwd());
|
|
188
|
-
if (currentRoot) {
|
|
189
|
-
resolvedRoots.add(resolve(currentRoot));
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const runtimeRegistry = options.runtimeRegistry ?? new ServerRuntimeRegistry();
|
|
194
|
-
const executionManager = options.executionManager ?? runtimeRegistry.executionManager;
|
|
195
|
-
const targets: ResolvedTarget[] = [];
|
|
196
|
-
|
|
197
|
-
// Handle case where options.actions is provided directly (e.g. unit tests or virtual packages)
|
|
198
|
-
if (resolvedRoots.size === 0 && options.actions) {
|
|
199
|
-
const dummyConfig: ProjectConfig = {
|
|
200
|
-
id: "virtual",
|
|
201
|
-
name: "Virtual Package",
|
|
202
|
-
version: "2.0.0",
|
|
203
|
-
description: "In-memory virtual package",
|
|
204
|
-
actionsDir: "actions",
|
|
205
|
-
playbooksDir: "playbooks",
|
|
206
|
-
};
|
|
207
|
-
const storage = options.storage ?? runtimeRegistry.getStorage("virtual");
|
|
208
|
-
const runner = new ActionRunner({
|
|
209
|
-
packageId: dummyConfig.id,
|
|
210
|
-
storage,
|
|
211
|
-
projectConfig: dummyConfig,
|
|
212
|
-
configOverrides: options.configOverrides,
|
|
213
|
-
actions: options.actions,
|
|
214
|
-
});
|
|
215
|
-
targets.push({
|
|
216
|
-
projectRoot: "virtual",
|
|
217
|
-
config: dummyConfig,
|
|
218
|
-
actions: options.actions,
|
|
219
|
-
storage,
|
|
220
|
-
runner,
|
|
221
|
-
});
|
|
222
|
-
} else {
|
|
223
|
-
if (resolvedRoots.size === 0) {
|
|
224
|
-
throw new Error(
|
|
225
|
-
"No ActionDock project root found. Run inside an ActionDock package or specify --dir / --package / --all."
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
for (const root of resolvedRoots) {
|
|
230
|
-
const projectConfig = loadProjectConfig(root);
|
|
231
|
-
const actions =
|
|
232
|
-
options.actions && resolvedRoots.size === 1
|
|
233
|
-
? options.actions
|
|
234
|
-
: await loadActions(root, projectConfig.actionsDir);
|
|
235
|
-
|
|
236
|
-
const storage =
|
|
237
|
-
options.storage && resolvedRoots.size === 1
|
|
238
|
-
? options.storage
|
|
239
|
-
: runtimeRegistry.getStorage(projectConfig.id, root);
|
|
240
|
-
|
|
241
|
-
const runner = new ActionRunner({
|
|
242
|
-
packageId: projectConfig.id,
|
|
243
|
-
storage,
|
|
244
|
-
projectConfig,
|
|
245
|
-
configOverrides: options.configOverrides,
|
|
246
|
-
actions,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
targets.push({
|
|
250
|
-
projectRoot: root,
|
|
251
|
-
config: projectConfig,
|
|
252
|
-
actions,
|
|
253
|
-
storage,
|
|
254
|
-
runner,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const isMultiPackage = targets.length > 1;
|
|
260
|
-
|
|
261
|
-
// Check action ID collision across packages
|
|
262
|
-
const actionIdCounts = new Map<string, number>();
|
|
263
|
-
for (const target of targets) {
|
|
264
|
-
for (const actionId of target.actions.keys()) {
|
|
265
|
-
actionIdCounts.set(actionId, (actionIdCounts.get(actionId) || 0) + 1);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const serverName =
|
|
270
|
-
targets.length === 1
|
|
271
|
-
? targets[0].config.id || targets[0].config.name || "actiondock"
|
|
272
|
-
: "actiondock";
|
|
273
|
-
const serverVersion =
|
|
274
|
-
targets.length === 1 ? targets[0].config.version || "2.0.0" : "2.0.0";
|
|
275
|
-
|
|
276
|
-
const server = new McpServer({
|
|
277
|
-
name: serverName,
|
|
278
|
-
version: serverVersion,
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
(server.server as any).registerCapabilities({
|
|
282
|
-
tasks: {
|
|
283
|
-
listChanged: true,
|
|
284
|
-
cancel: {},
|
|
285
|
-
},
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// 1. Register Action Tools across all targets
|
|
289
|
-
for (const target of targets) {
|
|
290
|
-
for (const action of target.actions.values()) {
|
|
291
|
-
const count = actionIdCounts.get(action.id) || 1;
|
|
292
|
-
const toolName = count > 1 ? `${target.config.id}_${action.id}` : action.id;
|
|
293
|
-
const description = isMultiPackage
|
|
294
|
-
? `[${target.config.id}] ${action.description || ""}`
|
|
295
|
-
: action.description;
|
|
296
|
-
|
|
297
|
-
server.registerTool(
|
|
298
|
-
toolName,
|
|
299
|
-
{
|
|
300
|
-
description,
|
|
301
|
-
inputSchema: toMcpSchema(action.inputSchema),
|
|
302
|
-
outputSchema: action.outputSchema ? toMcpSchema(action.outputSchema) : undefined,
|
|
303
|
-
},
|
|
304
|
-
createMcpToolCallback(
|
|
305
|
-
target.runner,
|
|
306
|
-
action.id,
|
|
307
|
-
executionManager,
|
|
308
|
-
options.timeoutMs
|
|
309
|
-
)
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const storages = targets.map((t) => t.storage);
|
|
315
|
-
|
|
316
|
-
// 2. Register Tasks extension endpoints: tasks/get
|
|
317
|
-
(server.server as any).setRequestHandler("tasks/get", async (req: any) => {
|
|
318
|
-
const taskId = req.params?.taskId;
|
|
319
|
-
if (!taskId) {
|
|
320
|
-
throw new Error("taskId parameter is required for tasks/get");
|
|
321
|
-
}
|
|
322
|
-
for (const storage of storages) {
|
|
323
|
-
const run = storage.getRun(taskId);
|
|
324
|
-
if (run) {
|
|
325
|
-
return {
|
|
326
|
-
task: toMcpTaskPayload(run),
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
throw new Error(`Task '${taskId}' not found`);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// 3. Register Tasks extension endpoints: tasks/cancel
|
|
334
|
-
(server.server as any).setRequestHandler("tasks/cancel", async (req: any) => {
|
|
335
|
-
const taskId = req.params?.taskId;
|
|
336
|
-
if (!taskId) {
|
|
337
|
-
throw new Error("taskId parameter is required for tasks/cancel");
|
|
338
|
-
}
|
|
339
|
-
const activeHandle = executionManager.get(taskId);
|
|
340
|
-
if (activeHandle) {
|
|
341
|
-
executionManager.cancel(taskId, req.params?.reason || "Cancelled via MCP tasks/cancel");
|
|
342
|
-
return {
|
|
343
|
-
taskId,
|
|
344
|
-
status: "cancelled",
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
for (const storage of storages) {
|
|
348
|
-
const run = storage.getRun(taskId);
|
|
349
|
-
if (run) {
|
|
350
|
-
if (run.status === "running") {
|
|
351
|
-
storage.updateRun(taskId, "cancelled", undefined, {
|
|
352
|
-
code: "ACTION_CANCELLED",
|
|
353
|
-
message: req.params?.reason || "Cancelled via MCP tasks/cancel",
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
return {
|
|
357
|
-
taskId,
|
|
358
|
-
status: "cancelled",
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
throw new Error(`Task '${taskId}' not found`);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// 4. Register Tasks extension endpoints: tasks/list
|
|
366
|
-
(server.server as any).setRequestHandler("tasks/list", async (req: any) => {
|
|
367
|
-
const limit = typeof req.params?.limit === "number" ? req.params.limit : 50;
|
|
368
|
-
const actionId = req.params?.actionId;
|
|
369
|
-
const allRuns: RunRecord[] = [];
|
|
370
|
-
for (const storage of storages) {
|
|
371
|
-
const runs = storage.listRuns({ limit, actionId });
|
|
372
|
-
allRuns.push(...runs);
|
|
373
|
-
}
|
|
374
|
-
allRuns.sort(
|
|
375
|
-
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
|
376
|
-
);
|
|
377
|
-
const trimmed = allRuns.slice(0, limit);
|
|
378
|
-
return {
|
|
379
|
-
tasks: trimmed.map(toMcpTaskPayload),
|
|
380
|
-
};
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
const originalClose = server.close.bind(server);
|
|
384
|
-
let isClosed = false;
|
|
385
|
-
|
|
386
|
-
const closeFn = async (): Promise<void> => {
|
|
387
|
-
if (isClosed) return;
|
|
388
|
-
isClosed = true;
|
|
389
|
-
|
|
390
|
-
process.removeListener("exit", onProcessExit);
|
|
391
|
-
|
|
392
|
-
for (const storage of storages) {
|
|
393
|
-
try {
|
|
394
|
-
storage.close();
|
|
395
|
-
} catch {
|
|
396
|
-
// ignore
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (!options.runtimeRegistry) {
|
|
401
|
-
try {
|
|
402
|
-
runtimeRegistry.close();
|
|
403
|
-
} catch {
|
|
404
|
-
// ignore
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
await originalClose();
|
|
410
|
-
} catch {
|
|
411
|
-
// ignore
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
const onProcessExit = () => {
|
|
416
|
-
for (const storage of storages) {
|
|
417
|
-
try {
|
|
418
|
-
storage.close();
|
|
419
|
-
} catch {
|
|
420
|
-
// ignore
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if (!options.runtimeRegistry) {
|
|
424
|
-
try {
|
|
425
|
-
runtimeRegistry.close();
|
|
426
|
-
} catch {
|
|
427
|
-
// ignore
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
process.once("exit", onProcessExit);
|
|
433
|
-
|
|
434
|
-
(server as any).close = closeFn;
|
|
435
|
-
|
|
436
|
-
return server as ActionDockMcpServer;
|
|
437
|
-
}
|
package/src/http.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isLoopbackHost,
|
|
3
|
-
launchHttpServer,
|
|
4
|
-
resolveCorsHeaders,
|
|
5
|
-
ServerRuntimeRegistry,
|
|
6
|
-
verifyBearerToken,
|
|
7
|
-
} from "@actiondock/core";
|
|
8
|
-
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
9
|
-
import { createActionDockMcpServer } from "./adapter";
|
|
10
|
-
import type { ActionDockMcpHttpOptions, ActionDockMcpHttpServerInstance } from "./types";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Starts an ActionDock MCP server over HTTP transport.
|
|
14
|
-
*/
|
|
15
|
-
export function startMcpHttpServer(
|
|
16
|
-
options: ActionDockMcpHttpOptions = {}
|
|
17
|
-
): ActionDockMcpHttpServerInstance {
|
|
18
|
-
const port = options.port ?? 5178;
|
|
19
|
-
const host = options.host ?? "127.0.0.1";
|
|
20
|
-
const token = options.token;
|
|
21
|
-
|
|
22
|
-
// Non-loopback address requires token authentication by default
|
|
23
|
-
if (!isLoopbackHost(host) && !token && !options.allowInsecureNoAuth) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
"Authentication token is required when binding to a non-loopback address. Use --allow-insecure-no-auth to override."
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const runtimeRegistry = new ServerRuntimeRegistry();
|
|
30
|
-
|
|
31
|
-
const handler = createMcpHandler(
|
|
32
|
-
() => {
|
|
33
|
-
return createActionDockMcpServer({
|
|
34
|
-
...options,
|
|
35
|
-
runtimeRegistry,
|
|
36
|
-
executionManager: runtimeRegistry.executionManager,
|
|
37
|
-
});
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
onerror: (err) => {
|
|
41
|
-
process.stderr.write(`[MCP HTTP Error] ${err?.message || String(err)}\n`);
|
|
42
|
-
},
|
|
43
|
-
}
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const server = launchHttpServer(port, host, async (req) => {
|
|
47
|
-
const origin = req.headers.get("origin");
|
|
48
|
-
const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
|
|
49
|
-
|
|
50
|
-
if (req.method === "OPTIONS") {
|
|
51
|
-
return new Response(null, {
|
|
52
|
-
status: 204,
|
|
53
|
-
headers: corsHeaders,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const url = new URL(req.url);
|
|
58
|
-
const pathname = url.pathname;
|
|
59
|
-
|
|
60
|
-
// 1. Health check
|
|
61
|
-
if (pathname === "/health" || pathname === "/api/v1/health") {
|
|
62
|
-
if (!verifyBearerToken(req, token)) {
|
|
63
|
-
return new Response(
|
|
64
|
-
JSON.stringify({
|
|
65
|
-
ok: false,
|
|
66
|
-
error: {
|
|
67
|
-
code: "UNAUTHORIZED",
|
|
68
|
-
message: "Invalid or missing Bearer token",
|
|
69
|
-
},
|
|
70
|
-
}),
|
|
71
|
-
{
|
|
72
|
-
status: 401,
|
|
73
|
-
headers: {
|
|
74
|
-
"Content-Type": "application/json",
|
|
75
|
-
...corsHeaders,
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
return new Response(
|
|
81
|
-
JSON.stringify({
|
|
82
|
-
status: "ok",
|
|
83
|
-
protocol: "mcp",
|
|
84
|
-
version: "2.0.0",
|
|
85
|
-
timestamp: new Date().toISOString(),
|
|
86
|
-
}),
|
|
87
|
-
{
|
|
88
|
-
status: 200,
|
|
89
|
-
headers: {
|
|
90
|
-
"Content-Type": "application/json",
|
|
91
|
-
...corsHeaders,
|
|
92
|
-
},
|
|
93
|
-
}
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 2. Authentication check
|
|
98
|
-
if (!verifyBearerToken(req, token)) {
|
|
99
|
-
return new Response(
|
|
100
|
-
JSON.stringify({
|
|
101
|
-
jsonrpc: "2.0",
|
|
102
|
-
error: {
|
|
103
|
-
code: -32000,
|
|
104
|
-
message: "Unauthorized: Invalid or missing Bearer token",
|
|
105
|
-
},
|
|
106
|
-
id: null,
|
|
107
|
-
}),
|
|
108
|
-
{
|
|
109
|
-
status: 401,
|
|
110
|
-
headers: {
|
|
111
|
-
"Content-Type": "application/json",
|
|
112
|
-
...corsHeaders,
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 3. Delegate MCP endpoint
|
|
119
|
-
if (pathname === "/mcp" || pathname === "/") {
|
|
120
|
-
const mcpResponse = await handler.fetch(req);
|
|
121
|
-
if (Object.keys(corsHeaders).length > 0) {
|
|
122
|
-
const newHeaders = new Headers(mcpResponse.headers);
|
|
123
|
-
for (const [k, v] of Object.entries(corsHeaders)) {
|
|
124
|
-
newHeaders.set(k, String(v));
|
|
125
|
-
}
|
|
126
|
-
return new Response(mcpResponse.body, {
|
|
127
|
-
status: mcpResponse.status,
|
|
128
|
-
statusText: mcpResponse.statusText,
|
|
129
|
-
headers: newHeaders,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
return mcpResponse;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return new Response(
|
|
136
|
-
JSON.stringify({
|
|
137
|
-
error: "Not Found",
|
|
138
|
-
}),
|
|
139
|
-
{
|
|
140
|
-
status: 404,
|
|
141
|
-
headers: {
|
|
142
|
-
"Content-Type": "application/json",
|
|
143
|
-
...corsHeaders,
|
|
144
|
-
},
|
|
145
|
-
}
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
const actualHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
151
|
-
const url = `http://${actualHost}:${server.port}`;
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
port: server.port ?? port,
|
|
155
|
-
host,
|
|
156
|
-
url,
|
|
157
|
-
stop: () => {
|
|
158
|
-
runtimeRegistry.close();
|
|
159
|
-
server.stop(true);
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
package/src/schemas.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { JsonSchema } from "@actiondock/sdk";
|
|
2
|
-
import { fromJsonSchema } from "@modelcontextprotocol/server";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Normalizes an ActionDock JSON Schema into a standard MCP Tool Schema.
|
|
6
|
-
*/
|
|
7
|
-
export function toMcpSchema(schema?: JsonSchema) {
|
|
8
|
-
if (schema === undefined || schema === true) {
|
|
9
|
-
return fromJsonSchema({});
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (schema === false) {
|
|
13
|
-
return fromJsonSchema({
|
|
14
|
-
not: {},
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return fromJsonSchema(schema as Record<string, unknown>);
|
|
19
|
-
}
|