@actiondock/mcp 2.0.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/README.md +43 -0
- package/package.json +50 -0
- package/src/adapter.ts +363 -0
- package/src/http.ts +166 -0
- package/src/index.ts +5 -0
- package/src/schemas.ts +19 -0
- package/src/stdio.ts +16 -0
- package/src/types.ts +105 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @actiondock/mcp
|
|
2
|
+
|
|
3
|
+
Model Context Protocol (MCP) adapter for ActionDock 2.0.
|
|
4
|
+
|
|
5
|
+
[](https://bun.sh/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
8
|
+
|
|
9
|
+
> **Runtime requirement**: [Bun](https://bun.sh/) >= 1.2.0 is required.
|
|
10
|
+
|
|
11
|
+
`@actiondock/mcp` connects ActionDock Actions directly to the Model Context Protocol (MCP), exposing Actions as fully-typed MCP Tools over STDIO and HTTP transports.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add @actiondock/mcp
|
|
19
|
+
# or
|
|
20
|
+
npm install @actiondock/mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **STDIO & HTTP Transport**: Run locally via STDIO (for Claude Code, Cursor, Windsurf) or over HTTP with authentication and CORS.
|
|
28
|
+
- **Dynamic Tool Mapping**: Automatically converts Action `inputSchema` / `outputSchema` into standard MCP Tool contracts.
|
|
29
|
+
- **Cancellation Propagation**: Propagates MCP client cancellations directly to `ctx.signal` (`AbortSignal`).
|
|
30
|
+
- **Tasks Extension**: Supports asynchronous background tool calls via MCP Tasks extension (`tasks/get`, `tasks/cancel`, `tasks/list`).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📖 Documentation
|
|
35
|
+
|
|
36
|
+
- [MCP Integration Guide](https://github.com/team4u/actiondock/blob/main/docs/guides/mcp.md)
|
|
37
|
+
- [Action API Reference](https://github.com/team4u/actiondock/blob/main/docs/reference/action-api.md)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
[Apache-2.0](LICENSE) © team4u
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@actiondock/mcp",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "ActionDock MCP Adapter for exposing Actions as Model Context Protocol (MCP) Tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"module": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./src/index.ts",
|
|
12
|
+
"types": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"bun": ">=1.2.0"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"registry": "https://registry.npmjs.org/"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "bun test"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@actiondock/core": "^2.0.0",
|
|
31
|
+
"@actiondock/sdk": "^2.0.0",
|
|
32
|
+
"@modelcontextprotocol/server": "^2.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/bun": "latest",
|
|
36
|
+
"typescript": "^5.7.0"
|
|
37
|
+
},
|
|
38
|
+
"keywords": ["actiondock", "mcp", "model-context-protocol", "tools", "agent"],
|
|
39
|
+
"author": "team4u",
|
|
40
|
+
"license": "Apache-2.0",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/team4u/actiondock.git",
|
|
44
|
+
"directory": "packages/mcp"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/team4u/actiondock#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/team4u/actiondock/issues"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
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
|
+
* 将 ActionDock 标准的 ExecutionResult 信封结构转换为 MCP 协议规范的 Tool Call 返回结果。
|
|
22
|
+
*
|
|
23
|
+
* @param result ExecutionResult 结果对象
|
|
24
|
+
*/
|
|
25
|
+
export function toMcpResult(result: ExecutionResult) {
|
|
26
|
+
if (result.ok) {
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: "text" as const,
|
|
31
|
+
text: JSON.stringify(result),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
structuredContent: result.data as Record<string, unknown>,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
isError: true,
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text" as const,
|
|
43
|
+
text: JSON.stringify(result),
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ResolvedTarget {
|
|
50
|
+
projectRoot: string;
|
|
51
|
+
config: ProjectConfig;
|
|
52
|
+
actions: Map<string, ActionDefinition>;
|
|
53
|
+
storage: RuntimeStorage;
|
|
54
|
+
runner: ActionRunner;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createMcpToolCallback(
|
|
58
|
+
runner: ActionRunner,
|
|
59
|
+
actionId: string,
|
|
60
|
+
executionManager: ExecutionManager,
|
|
61
|
+
timeoutMs?: number
|
|
62
|
+
) {
|
|
63
|
+
return async (input: any, ctx: any) => {
|
|
64
|
+
const isAsync = Boolean(
|
|
65
|
+
input &&
|
|
66
|
+
typeof input === "object" &&
|
|
67
|
+
(input.execution?.mode === "async" ||
|
|
68
|
+
input.__async === true ||
|
|
69
|
+
input.async === true)
|
|
70
|
+
);
|
|
71
|
+
const signal = ctx.mcpReq?.signal;
|
|
72
|
+
|
|
73
|
+
if (isAsync) {
|
|
74
|
+
const handle = runner.start(actionId, input, {
|
|
75
|
+
signal,
|
|
76
|
+
timeoutMs,
|
|
77
|
+
});
|
|
78
|
+
executionManager.register(handle);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text" as const,
|
|
84
|
+
text: JSON.stringify({
|
|
85
|
+
ok: true,
|
|
86
|
+
runId: handle.runId,
|
|
87
|
+
taskId: handle.runId,
|
|
88
|
+
status: "running",
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const handle = runner.start(actionId, input, {
|
|
96
|
+
signal,
|
|
97
|
+
timeoutMs,
|
|
98
|
+
});
|
|
99
|
+
executionManager.register(handle);
|
|
100
|
+
const result = await handle.result;
|
|
101
|
+
return toMcpResult(result);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Creates and configures an McpServer instance bound to one or more ActionDock packages with Tasks extension support.
|
|
107
|
+
*/
|
|
108
|
+
export async function createActionDockMcpServer(
|
|
109
|
+
options: ActionDockMcpOptions = {}
|
|
110
|
+
): Promise<McpServer> {
|
|
111
|
+
const targetRoots: string[] = [];
|
|
112
|
+
if (options.projectRoot) {
|
|
113
|
+
targetRoots.push(options.projectRoot);
|
|
114
|
+
}
|
|
115
|
+
if (options.projectRoots) {
|
|
116
|
+
targetRoots.push(...options.projectRoots);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const targetPackages: string[] = [];
|
|
120
|
+
if (options.packageId) {
|
|
121
|
+
targetPackages.push(options.packageId);
|
|
122
|
+
}
|
|
123
|
+
if (options.packageIds) {
|
|
124
|
+
targetPackages.push(...options.packageIds);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const resolvedRoots = new Set<string>();
|
|
128
|
+
|
|
129
|
+
// 1. Handle --all: discover all linked packages from registry
|
|
130
|
+
if (options.all) {
|
|
131
|
+
const linked = listLinkedPackages(options.customHome);
|
|
132
|
+
for (const pkg of linked) {
|
|
133
|
+
if (existsSync(pkg.path)) {
|
|
134
|
+
resolvedRoots.add(resolve(pkg.path));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const currentRoot = findProjectRoot(process.cwd());
|
|
138
|
+
if (currentRoot) {
|
|
139
|
+
resolvedRoots.add(resolve(currentRoot));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 2. Handle specific package IDs
|
|
144
|
+
for (const pkgId of targetPackages) {
|
|
145
|
+
const root = resolvePackageRoot(pkgId, undefined, options.customHome);
|
|
146
|
+
if (!root || !existsSync(root)) {
|
|
147
|
+
throw new Error(`Package '${pkgId}' not found in registry`);
|
|
148
|
+
}
|
|
149
|
+
resolvedRoots.add(resolve(root));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Handle specific directory paths
|
|
153
|
+
for (const dir of targetRoots) {
|
|
154
|
+
const absPath = resolve(dir);
|
|
155
|
+
const root = findProjectRoot(absPath);
|
|
156
|
+
if (!root) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Project root '${dir}' is not a valid ActionDock package (actiondock.json not found)`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
resolvedRoots.add(resolve(root));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 4. Default fallback: current working directory
|
|
165
|
+
if (resolvedRoots.size === 0 && !options.all) {
|
|
166
|
+
const currentRoot = findProjectRoot(process.cwd());
|
|
167
|
+
if (currentRoot) {
|
|
168
|
+
resolvedRoots.add(resolve(currentRoot));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const runtimeRegistry = options.runtimeRegistry ?? new ServerRuntimeRegistry();
|
|
173
|
+
const executionManager = options.executionManager ?? runtimeRegistry.executionManager;
|
|
174
|
+
const targets: ResolvedTarget[] = [];
|
|
175
|
+
|
|
176
|
+
// Handle case where options.actions is provided directly (e.g. unit tests or virtual packages)
|
|
177
|
+
if (resolvedRoots.size === 0 && options.actions) {
|
|
178
|
+
const dummyConfig: ProjectConfig = {
|
|
179
|
+
id: "virtual",
|
|
180
|
+
name: "Virtual Package",
|
|
181
|
+
version: "2.0.0",
|
|
182
|
+
description: "In-memory virtual package",
|
|
183
|
+
actionsDir: "actions",
|
|
184
|
+
playbooksDir: "playbooks",
|
|
185
|
+
};
|
|
186
|
+
const storage = options.storage ?? runtimeRegistry.getStorage("virtual");
|
|
187
|
+
const runner = new ActionRunner({
|
|
188
|
+
packageId: dummyConfig.id,
|
|
189
|
+
storage,
|
|
190
|
+
projectConfig: dummyConfig,
|
|
191
|
+
configOverrides: options.configOverrides,
|
|
192
|
+
actions: options.actions,
|
|
193
|
+
});
|
|
194
|
+
targets.push({
|
|
195
|
+
projectRoot: "virtual",
|
|
196
|
+
config: dummyConfig,
|
|
197
|
+
actions: options.actions,
|
|
198
|
+
storage,
|
|
199
|
+
runner,
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
if (resolvedRoots.size === 0) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"No ActionDock project root found. Run inside an ActionDock package or specify --dir / --package / --all."
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const root of resolvedRoots) {
|
|
209
|
+
const projectConfig = loadProjectConfig(root);
|
|
210
|
+
const actions =
|
|
211
|
+
options.actions && resolvedRoots.size === 1
|
|
212
|
+
? options.actions
|
|
213
|
+
: await loadActions(root, projectConfig.actionsDir);
|
|
214
|
+
|
|
215
|
+
const storage =
|
|
216
|
+
options.storage && resolvedRoots.size === 1
|
|
217
|
+
? options.storage
|
|
218
|
+
: runtimeRegistry.getStorage(projectConfig.id, root);
|
|
219
|
+
|
|
220
|
+
const runner = new ActionRunner({
|
|
221
|
+
packageId: projectConfig.id,
|
|
222
|
+
storage,
|
|
223
|
+
projectConfig,
|
|
224
|
+
configOverrides: options.configOverrides,
|
|
225
|
+
actions,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
targets.push({
|
|
229
|
+
projectRoot: root,
|
|
230
|
+
config: projectConfig,
|
|
231
|
+
actions,
|
|
232
|
+
storage,
|
|
233
|
+
runner,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const isMultiPackage = targets.length > 1;
|
|
239
|
+
|
|
240
|
+
// Check action ID collision across packages
|
|
241
|
+
const actionIdCounts = new Map<string, number>();
|
|
242
|
+
for (const target of targets) {
|
|
243
|
+
for (const actionId of target.actions.keys()) {
|
|
244
|
+
actionIdCounts.set(actionId, (actionIdCounts.get(actionId) || 0) + 1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const serverName =
|
|
249
|
+
targets.length === 1
|
|
250
|
+
? targets[0].config.id || targets[0].config.name || "actiondock"
|
|
251
|
+
: "actiondock";
|
|
252
|
+
const serverVersion =
|
|
253
|
+
targets.length === 1 ? targets[0].config.version || "2.0.0" : "2.0.0";
|
|
254
|
+
|
|
255
|
+
const server = new McpServer({
|
|
256
|
+
name: serverName,
|
|
257
|
+
version: serverVersion,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
(server.server as any).registerCapabilities({
|
|
261
|
+
tasks: {
|
|
262
|
+
listChanged: true,
|
|
263
|
+
cancel: {},
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// 1. Register Action Tools across all targets
|
|
268
|
+
for (const target of targets) {
|
|
269
|
+
for (const action of target.actions.values()) {
|
|
270
|
+
const count = actionIdCounts.get(action.id) || 1;
|
|
271
|
+
const toolName = count > 1 ? `${target.config.id}_${action.id}` : action.id;
|
|
272
|
+
const description = isMultiPackage
|
|
273
|
+
? `[${target.config.id}] ${action.description || ""}`
|
|
274
|
+
: action.description;
|
|
275
|
+
|
|
276
|
+
server.registerTool(
|
|
277
|
+
toolName,
|
|
278
|
+
{
|
|
279
|
+
description,
|
|
280
|
+
inputSchema: toMcpSchema(action.inputSchema),
|
|
281
|
+
outputSchema: action.outputSchema ? toMcpSchema(action.outputSchema) : undefined,
|
|
282
|
+
},
|
|
283
|
+
createMcpToolCallback(
|
|
284
|
+
target.runner,
|
|
285
|
+
action.id,
|
|
286
|
+
executionManager,
|
|
287
|
+
options.timeoutMs
|
|
288
|
+
)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const storages = targets.map((t) => t.storage);
|
|
294
|
+
|
|
295
|
+
// 2. Register Tasks extension endpoints: tasks/get
|
|
296
|
+
(server.server as any).setRequestHandler("tasks/get", async (req: any) => {
|
|
297
|
+
const taskId = req.params?.taskId;
|
|
298
|
+
if (!taskId) {
|
|
299
|
+
throw new Error("taskId parameter is required for tasks/get");
|
|
300
|
+
}
|
|
301
|
+
for (const storage of storages) {
|
|
302
|
+
const run = storage.getRun(taskId);
|
|
303
|
+
if (run) {
|
|
304
|
+
return {
|
|
305
|
+
task: toMcpTaskPayload(run),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// 3. Register Tasks extension endpoints: tasks/cancel
|
|
313
|
+
(server.server as any).setRequestHandler("tasks/cancel", async (req: any) => {
|
|
314
|
+
const taskId = req.params?.taskId;
|
|
315
|
+
if (!taskId) {
|
|
316
|
+
throw new Error("taskId parameter is required for tasks/cancel");
|
|
317
|
+
}
|
|
318
|
+
const activeHandle = executionManager.get(taskId);
|
|
319
|
+
if (activeHandle) {
|
|
320
|
+
executionManager.cancel(taskId, req.params?.reason || "Cancelled via MCP tasks/cancel");
|
|
321
|
+
return {
|
|
322
|
+
taskId,
|
|
323
|
+
status: "cancelled",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
for (const storage of storages) {
|
|
327
|
+
const run = storage.getRun(taskId);
|
|
328
|
+
if (run) {
|
|
329
|
+
if (run.status === "running") {
|
|
330
|
+
storage.updateRun(taskId, "cancelled", undefined, {
|
|
331
|
+
code: "ACTION_CANCELLED",
|
|
332
|
+
message: req.params?.reason || "Cancelled via MCP tasks/cancel",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
taskId,
|
|
337
|
+
status: "cancelled",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throw new Error(`Task '${taskId}' not found`);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// 4. Register Tasks extension endpoints: tasks/list
|
|
345
|
+
(server.server as any).setRequestHandler("tasks/list", async (req: any) => {
|
|
346
|
+
const limit = typeof req.params?.limit === "number" ? req.params.limit : 50;
|
|
347
|
+
const actionId = req.params?.actionId;
|
|
348
|
+
const allRuns: RunRecord[] = [];
|
|
349
|
+
for (const storage of storages) {
|
|
350
|
+
const runs = storage.listRuns({ limit, actionId });
|
|
351
|
+
allRuns.push(...runs);
|
|
352
|
+
}
|
|
353
|
+
allRuns.sort(
|
|
354
|
+
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
|
355
|
+
);
|
|
356
|
+
const trimmed = allRuns.slice(0, limit);
|
|
357
|
+
return {
|
|
358
|
+
tasks: trimmed.map(toMcpTaskPayload),
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return server;
|
|
363
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isLoopbackHost,
|
|
3
|
+
resolveCorsHeaders,
|
|
4
|
+
ServerRuntimeRegistry,
|
|
5
|
+
verifyBearerToken,
|
|
6
|
+
} from "@actiondock/core";
|
|
7
|
+
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
8
|
+
import { createActionDockMcpServer } from "./adapter";
|
|
9
|
+
import type { ActionDockMcpHttpOptions, ActionDockMcpHttpServerInstance } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Starts an ActionDock MCP server over HTTP transport.
|
|
13
|
+
*/
|
|
14
|
+
export function startMcpHttpServer(
|
|
15
|
+
options: ActionDockMcpHttpOptions = {}
|
|
16
|
+
): ActionDockMcpHttpServerInstance {
|
|
17
|
+
const port = options.port ?? 5178;
|
|
18
|
+
const host = options.host ?? "127.0.0.1";
|
|
19
|
+
const token = options.token;
|
|
20
|
+
|
|
21
|
+
// Non-loopback address requires token authentication by default
|
|
22
|
+
if (!isLoopbackHost(host) && !token && !options.allowInsecureNoAuth) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Authentication token is required when binding to a non-loopback address. Use --allow-insecure-no-auth to override."
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const runtimeRegistry = new ServerRuntimeRegistry();
|
|
29
|
+
|
|
30
|
+
const handler = createMcpHandler(
|
|
31
|
+
() => {
|
|
32
|
+
return createActionDockMcpServer({
|
|
33
|
+
...options,
|
|
34
|
+
runtimeRegistry,
|
|
35
|
+
executionManager: runtimeRegistry.executionManager,
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
onerror: (err) => {
|
|
40
|
+
process.stderr.write(`[MCP HTTP Error] ${err?.message || String(err)}\n`);
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const server = Bun.serve({
|
|
47
|
+
port,
|
|
48
|
+
hostname: host,
|
|
49
|
+
async fetch(req) {
|
|
50
|
+
const origin = req.headers.get("origin");
|
|
51
|
+
const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
|
|
52
|
+
|
|
53
|
+
if (req.method === "OPTIONS") {
|
|
54
|
+
return new Response(null, {
|
|
55
|
+
status: 204,
|
|
56
|
+
headers: corsHeaders,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const url = new URL(req.url);
|
|
61
|
+
const pathname = url.pathname;
|
|
62
|
+
|
|
63
|
+
// 1. Health check
|
|
64
|
+
if (pathname === "/health" || pathname === "/api/v1/health") {
|
|
65
|
+
if (!verifyBearerToken(req, token)) {
|
|
66
|
+
return new Response(
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
ok: false,
|
|
69
|
+
error: {
|
|
70
|
+
code: "UNAUTHORIZED",
|
|
71
|
+
message: "Invalid or missing Bearer token",
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
{
|
|
75
|
+
status: 401,
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
...corsHeaders,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return new Response(
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
status: "ok",
|
|
86
|
+
protocol: "mcp",
|
|
87
|
+
version: "2.0.0",
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
}),
|
|
90
|
+
{
|
|
91
|
+
status: 200,
|
|
92
|
+
headers: {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
...corsHeaders,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2. Authentication check
|
|
101
|
+
if (!verifyBearerToken(req, token)) {
|
|
102
|
+
return new Response(
|
|
103
|
+
JSON.stringify({
|
|
104
|
+
jsonrpc: "2.0",
|
|
105
|
+
error: {
|
|
106
|
+
code: -32000,
|
|
107
|
+
message: "Unauthorized: Invalid or missing Bearer token",
|
|
108
|
+
},
|
|
109
|
+
id: null,
|
|
110
|
+
}),
|
|
111
|
+
{
|
|
112
|
+
status: 401,
|
|
113
|
+
headers: {
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
...corsHeaders,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. Delegate MCP endpoint
|
|
122
|
+
if (pathname === "/mcp" || pathname === "/") {
|
|
123
|
+
const mcpResponse = await handler.fetch(req);
|
|
124
|
+
if (Object.keys(corsHeaders).length > 0) {
|
|
125
|
+
const newHeaders = new Headers(mcpResponse.headers);
|
|
126
|
+
for (const [k, v] of Object.entries(corsHeaders)) {
|
|
127
|
+
newHeaders.set(k, String(v));
|
|
128
|
+
}
|
|
129
|
+
return new Response(mcpResponse.body, {
|
|
130
|
+
status: mcpResponse.status,
|
|
131
|
+
statusText: mcpResponse.statusText,
|
|
132
|
+
headers: newHeaders,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return mcpResponse;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return new Response(
|
|
139
|
+
JSON.stringify({
|
|
140
|
+
error: "Not Found",
|
|
141
|
+
}),
|
|
142
|
+
{
|
|
143
|
+
status: 404,
|
|
144
|
+
headers: {
|
|
145
|
+
"Content-Type": "application/json",
|
|
146
|
+
...corsHeaders,
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const actualHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
154
|
+
const url = `http://${actualHost}:${server.port}`;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
port: server.port ?? port,
|
|
158
|
+
host,
|
|
159
|
+
url,
|
|
160
|
+
stop: () => {
|
|
161
|
+
runtimeRegistry.close();
|
|
162
|
+
server.stop(true);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
package/src/index.ts
ADDED
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
}
|
package/src/stdio.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
2
|
+
import { createActionDockMcpServer } from "./adapter";
|
|
3
|
+
import type { ActionDockMcpOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Starts an ActionDock MCP server over STDIO transport.
|
|
7
|
+
*/
|
|
8
|
+
export async function startMcpStdio(
|
|
9
|
+
options: ActionDockMcpOptions = {}
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
serveStdio(() => createActionDockMcpServer(options), {
|
|
12
|
+
onerror: (err) => {
|
|
13
|
+
process.stderr.write(`[MCP Error] ${err?.message || String(err)}\n`);
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ExecutionManager, RuntimeStorage, ServerRuntimeRegistry } from "@actiondock/core";
|
|
2
|
+
import type { ActionDefinition, ExecutionResult, RunRecord, RunStatus } from "@actiondock/sdk";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MCP 任务状态枚举(兼容 Model Context Protocol Task 规范)。
|
|
6
|
+
*/
|
|
7
|
+
export type McpTaskStatus = "working" | "completed" | "failed" | "cancelled";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MCP 任务状态数据载荷结构体。
|
|
11
|
+
*/
|
|
12
|
+
export interface McpTaskPayload {
|
|
13
|
+
taskId: string;
|
|
14
|
+
status: McpTaskStatus;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
finishedAt?: string;
|
|
17
|
+
input?: unknown;
|
|
18
|
+
output?: unknown;
|
|
19
|
+
error?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 将 ActionDock 内部的 RunStatus 转换为 MCP 标准的 TaskStatus。
|
|
24
|
+
*/
|
|
25
|
+
export function toMcpTaskStatus(status: RunStatus): McpTaskStatus {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case "running":
|
|
28
|
+
return "working";
|
|
29
|
+
case "success":
|
|
30
|
+
return "completed";
|
|
31
|
+
case "failed":
|
|
32
|
+
return "failed";
|
|
33
|
+
case "cancelled":
|
|
34
|
+
return "cancelled";
|
|
35
|
+
default:
|
|
36
|
+
return "working";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 将内部 RunRecord 实体转换为向 MCP 客户端暴露的 McpTaskPayload。
|
|
42
|
+
*/
|
|
43
|
+
export function toMcpTaskPayload(run: RunRecord): McpTaskPayload {
|
|
44
|
+
return {
|
|
45
|
+
taskId: run.id,
|
|
46
|
+
status: toMcpTaskStatus(run.status),
|
|
47
|
+
createdAt: run.startedAt,
|
|
48
|
+
finishedAt: run.finishedAt,
|
|
49
|
+
input: run.input,
|
|
50
|
+
output: run.output,
|
|
51
|
+
error: run.error,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* ActionDock MCP 适配层初始化选项。
|
|
57
|
+
*/
|
|
58
|
+
export interface ActionDockMcpOptions {
|
|
59
|
+
/** 单个目标项目根目录 */
|
|
60
|
+
projectRoot?: string;
|
|
61
|
+
/** 多个项目根目录(用于多包聚合提供) */
|
|
62
|
+
projectRoots?: string[];
|
|
63
|
+
/** 目标 Package ID */
|
|
64
|
+
packageId?: string;
|
|
65
|
+
/** 多个 Package ID 列表 */
|
|
66
|
+
packageIds?: string[];
|
|
67
|
+
/** 是否聚合暴露全局 Registry 中的所有 Package */
|
|
68
|
+
all?: boolean;
|
|
69
|
+
/** 自定义家目录路径 */
|
|
70
|
+
customHome?: string;
|
|
71
|
+
/** 配置动态覆盖项 */
|
|
72
|
+
configOverrides?: Record<string, unknown>;
|
|
73
|
+
/** 单个 Tool 执行超时时间(毫秒) */
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
/** 预加载的 Action 映射表 */
|
|
76
|
+
actions?: Map<string, ActionDefinition>;
|
|
77
|
+
/** 底层存储实例 */
|
|
78
|
+
storage?: RuntimeStorage;
|
|
79
|
+
/** 服务端运行时注册表 */
|
|
80
|
+
runtimeRegistry?: ServerRuntimeRegistry;
|
|
81
|
+
/** 活跃执行任务管理器 */
|
|
82
|
+
executionManager?: ExecutionManager;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* HTTP 传输协议安全配置项。
|
|
87
|
+
*/
|
|
88
|
+
export interface HttpSecurityOptions {
|
|
89
|
+
host?: string;
|
|
90
|
+
port?: number;
|
|
91
|
+
token?: string;
|
|
92
|
+
allowInsecureNoAuth?: boolean;
|
|
93
|
+
corsOrigins?: string[];
|
|
94
|
+
maxBodyBytes?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ActionDockMcpHttpOptions extends ActionDockMcpOptions, HttpSecurityOptions {}
|
|
98
|
+
|
|
99
|
+
export interface ActionDockMcpHttpServerInstance {
|
|
100
|
+
port: number;
|
|
101
|
+
host: string;
|
|
102
|
+
url: string;
|
|
103
|
+
stop: () => void;
|
|
104
|
+
}
|
|
105
|
+
|