@actiondock/mcp 2.0.12 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,574 +1,6 @@
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
- };
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
- * Normalizes an ActionDock JSON Schema into a standard MCP Tool Schema.
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>;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};