@gavana.ai/cli 0.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/CHANGELOG.md +54 -0
- package/LICENSE.md +7 -0
- package/README.md +237 -0
- package/bin/craftboard.mjs +5 -0
- package/bin/gavana.mjs +5 -0
- package/guides/connections.md +35 -0
- package/guides/examples-common-mistakes.md +29 -0
- package/guides/existing-canvases.md +19 -0
- package/guides/generated-assets.md +29 -0
- package/guides/getting-started.md +26 -0
- package/guides/notes-text-sections.md +44 -0
- package/guides/paid-action-safety.md +22 -0
- package/guides/prompt-lists.md +20 -0
- package/guides/sections-layout.md +43 -0
- package/guides/validation-recovery.md +33 -0
- package/package.json +44 -0
- package/src/canvas-agent-guide.mjs +133 -0
- package/src/canvas-agent-validation.mjs +554 -0
- package/src/canvas-layout.mjs +287 -0
- package/src/capabilities.mjs +61 -0
- package/src/client.mjs +1141 -0
- package/src/commands.mjs +259 -0
- package/src/config.mjs +197 -0
- package/src/guide-sources.mjs +86 -0
- package/src/runner.mjs +1968 -0
- package/src/tools/action_get.mjs +16 -0
- package/src/tools/action_list.mjs +21 -0
- package/src/tools/action_run.mjs +60 -0
- package/src/tools/agent_canvas_get.mjs +15 -0
- package/src/tools/asset_get.mjs +16 -0
- package/src/tools/asset_list.mjs +17 -0
- package/src/tools/asset_upload.mjs +24 -0
- package/src/tools/campaign_cancel.mjs +16 -0
- package/src/tools/campaign_get.mjs +16 -0
- package/src/tools/campaign_plan.mjs +31 -0
- package/src/tools/campaign_review.mjs +24 -0
- package/src/tools/campaign_start.mjs +19 -0
- package/src/tools/canvas_apply_batch.mjs +35 -0
- package/src/tools/canvas_create.mjs +15 -0
- package/src/tools/canvas_get.mjs +16 -0
- package/src/tools/canvas_list.mjs +17 -0
- package/src/tools/canvas_render.mjs +34 -0
- package/src/tools/canvas_validate.mjs +34 -0
- package/src/tools/connection_create.mjs +38 -0
- package/src/tools/connection_delete.mjs +31 -0
- package/src/tools/definitions.mjs +111 -0
- package/src/tools/guide_get.mjs +16 -0
- package/src/tools/guide_search.mjs +16 -0
- package/src/tools/helpers.mjs +66 -0
- package/src/tools/image_edit.mjs +8 -0
- package/src/tools/image_generate.mjs +8 -0
- package/src/tools/image_tool.mjs +56 -0
- package/src/tools/image_variations.mjs +8 -0
- package/src/tools/job_cancel.mjs +16 -0
- package/src/tools/job_get.mjs +17 -0
- package/src/tools/job_wait.mjs +18 -0
- package/src/tools/model_get.mjs +16 -0
- package/src/tools/model_list.mjs +23 -0
- package/src/tools/node_create.mjs +36 -0
- package/src/tools/node_delete.mjs +31 -0
- package/src/tools/node_get.mjs +16 -0
- package/src/tools/node_move.mjs +37 -0
- package/src/tools/node_resize.mjs +37 -0
- package/src/tools/node_update.mjs +36 -0
- package/src/tools/progress.mjs +101 -0
- package/src/tools/provider_list.mjs +17 -0
- package/src/tools/recipe_fork.mjs +32 -0
- package/src/tools/recipe_get.mjs +19 -0
- package/src/tools/recipe_run.mjs +61 -0
- package/src/tools/recipe_search.mjs +17 -0
- package/src/tools/registry.mjs +550 -0
- package/src/tools/run_cancel.mjs +16 -0
- package/src/tools/run_get.mjs +17 -0
- package/src/tools/run_wait.mjs +18 -0
- package/src/tools/schemas.mjs +165 -0
- package/src/tools/video_generate.mjs +37 -0
- package/src/version.mjs +12 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Tool definition: guide_search
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { GAVANA_CANVAS_GUIDE_VERSION, searchCanvasGuides } from "../canvas-agent-guide.mjs";
|
|
7
|
+
|
|
8
|
+
export function defineGuideSearch(client) {
|
|
9
|
+
return {
|
|
10
|
+
title: "Search the Canvas Agent Guide",
|
|
11
|
+
description: `Search the canonical Gavana Canvas Agent Guide v${GAVANA_CANVAS_GUIDE_VERSION}. Use this before the first canvas mutation in a session or whenever an operation is unfamiliar.`,
|
|
12
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
13
|
+
inputSchema: z.object({ query: z.string().max(240).default(""), limit: z.number().int().min(1).max(10).default(10) }),
|
|
14
|
+
handler: ({ query, limit }) => searchCanvasGuides(query, limit),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Helpers shared by the Gavana tool definitions.
|
|
2
|
+
//
|
|
3
|
+
// Moved out of scripts/gavana-agent-mcp.mjs with the tool schemas so tool
|
|
4
|
+
// definitions can live outside the server. listOptions normalises the pagination
|
|
5
|
+
// and filter options the list tools accept. readLocalReferenceImage reads a
|
|
6
|
+
// user-supplied local file for asset_upload; it is the only tool helper that
|
|
7
|
+
// touches the filesystem, so it and its validation helpers stay together and
|
|
8
|
+
// deliberately narrow.
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { MAX_LOCAL_REFERENCE_IMAGE_BYTES } from "./schemas.mjs";
|
|
12
|
+
|
|
13
|
+
function listOptions(limit, cursor) {
|
|
14
|
+
return {
|
|
15
|
+
...(limit !== undefined ? { limit } : {}),
|
|
16
|
+
...(cursor ? { cursor } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readLocalReferenceImage(value) {
|
|
21
|
+
const filePath = path.resolve(value);
|
|
22
|
+
let stat;
|
|
23
|
+
try {
|
|
24
|
+
stat = await fs.stat(filePath);
|
|
25
|
+
} catch {
|
|
26
|
+
throw localImageInputError(`Local image path was not found: ${value}`);
|
|
27
|
+
}
|
|
28
|
+
if (!stat.isFile()) throw localImageInputError(`Local image path is not a file: ${value}`);
|
|
29
|
+
if (stat.size > MAX_LOCAL_REFERENCE_IMAGE_BYTES) throw localImageInputError(`Local image is too large (maximum 50 MiB): ${value}`);
|
|
30
|
+
|
|
31
|
+
let bytes;
|
|
32
|
+
try {
|
|
33
|
+
bytes = await fs.readFile(filePath);
|
|
34
|
+
} catch {
|
|
35
|
+
throw localImageInputError(`Local image could not be read: ${value}`);
|
|
36
|
+
}
|
|
37
|
+
if (!bytes.byteLength) throw localImageInputError(`Local image is empty: ${value}`);
|
|
38
|
+
if (bytes.byteLength > MAX_LOCAL_REFERENCE_IMAGE_BYTES) throw localImageInputError(`Local image is too large (maximum 50 MiB): ${value}`);
|
|
39
|
+
if (!sniffRasterImageContentType(bytes)) throw localImageInputError(`Local image must be a PNG, JPEG, WebP, or GIF: ${value}`);
|
|
40
|
+
return { bytes, fileName: cleanUploadFileName(path.basename(filePath)) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cleanUploadFileName(value) {
|
|
44
|
+
const name = path.basename(String(value || "reference-image"));
|
|
45
|
+
return name.replace(/[\r\n]/g, " ").slice(0, 160) || "reference-image";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function localImageInputError(message) {
|
|
49
|
+
return Object.assign(new Error(`Input validation error: ${message}`), { code: "invalid_local_image" });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sniffRasterImageContentType(bytes) {
|
|
53
|
+
if (bytes.length >= 8 && bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) return "image/png";
|
|
54
|
+
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return "image/jpeg";
|
|
55
|
+
if (bytes.length >= 12 && bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP") return "image/webp";
|
|
56
|
+
if (bytes.length >= 6 && (bytes.toString("ascii", 0, 6) === "GIF87a" || bytes.toString("ascii", 0, 6) === "GIF89a")) return "image/gif";
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
cleanUploadFileName,
|
|
62
|
+
listOptions,
|
|
63
|
+
localImageInputError,
|
|
64
|
+
readLocalReferenceImage,
|
|
65
|
+
sniffRasterImageContentType,
|
|
66
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Shared definition for image_generate, image_edit and image_variations.
|
|
2
|
+
//
|
|
3
|
+
// The three differ only in the operation they queue and their description, so they
|
|
4
|
+
// share one factory rather than three near-identical modules. ./registry.mjs still
|
|
5
|
+
// lists them as three tools, which is what clients see.
|
|
6
|
+
import { imageSchema, validateImageInput } from "./schemas.mjs";
|
|
7
|
+
import { withProgress } from "./progress.mjs";
|
|
8
|
+
|
|
9
|
+
export function imageToolDefinition(operation, client) {
|
|
10
|
+
return {
|
|
11
|
+
title: `${operation === "generate" ? "Generate" : operation === "edit" ? "Edit" : "Vary"} canvas images`,
|
|
12
|
+
description:
|
|
13
|
+
operation === "generate"
|
|
14
|
+
? "Queue image generation into existing target image nodes. For an exact product, logo, face, garment, or other visual identity, do not rely on prompt-only generation: use asset_upload for a user-provided local image (or an existing node:/asset: handle), then call image_edit with references."
|
|
15
|
+
: operation === "edit"
|
|
16
|
+
? "Queue an image edit using stable node: or asset: references. Use this for exact product or visual identity work; call asset_upload first when the user supplied a local image."
|
|
17
|
+
: "Queue variations using an existing node: or asset: source.",
|
|
18
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
19
|
+
inputSchema: imageSchema(),
|
|
20
|
+
markdown: true,
|
|
21
|
+
handler: async (input, extra) => {
|
|
22
|
+
validateImageInput(operation, input);
|
|
23
|
+
const destination = input.destination || input.canvasId;
|
|
24
|
+
const prepared = await client.prepareImageDestination({
|
|
25
|
+
...input,
|
|
26
|
+
operation,
|
|
27
|
+
destination,
|
|
28
|
+
});
|
|
29
|
+
const {
|
|
30
|
+
destination: _destination,
|
|
31
|
+
canvasId: _canvasId,
|
|
32
|
+
canvasTitle: _canvasTitle,
|
|
33
|
+
targetNodeIds: _targetNodeIds,
|
|
34
|
+
targetTitle: _targetTitle,
|
|
35
|
+
targetX: _targetX,
|
|
36
|
+
targetY: _targetY,
|
|
37
|
+
targetWidth: _targetWidth,
|
|
38
|
+
targetHeight: _targetHeight,
|
|
39
|
+
wait: _wait,
|
|
40
|
+
timeoutSeconds: _timeoutSeconds,
|
|
41
|
+
...jobInput
|
|
42
|
+
} = input;
|
|
43
|
+
const queued = await client.startImage(operation, {
|
|
44
|
+
...jobInput,
|
|
45
|
+
canvasId: prepared.canvasId,
|
|
46
|
+
targetNodeIds: prepared.targetNodeIds,
|
|
47
|
+
baseRevision: prepared.baseRevision,
|
|
48
|
+
idempotencyKey: input.idempotencyKey,
|
|
49
|
+
});
|
|
50
|
+
const destinationResult = { requested: destination, canvasId: prepared.canvasId, targetNodeIds: prepared.targetNodeIds };
|
|
51
|
+
if (input.wait === false) return { ...queued, destination: destinationResult };
|
|
52
|
+
const result = await client.waitForRun(queued.run || queued.id, withProgress({ timeoutMs: (input.timeoutSeconds || 900) * 1000 }, extra));
|
|
53
|
+
return { ...result, destination: destinationResult };
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Tool definition: job_cancel
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { jobReference } from "./schemas.mjs";
|
|
7
|
+
|
|
8
|
+
export function defineJobCancel(client) {
|
|
9
|
+
return {
|
|
10
|
+
title: "Cancel an image, video, or Action job",
|
|
11
|
+
description: "Cancel queued or running image, video, or Action work. Use only after explicit user intent.",
|
|
12
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
13
|
+
inputSchema: z.object({ jobId: jobReference }),
|
|
14
|
+
handler: ({ jobId }) => client.cancelJob(jobId),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Tool definition: job_get
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { jobReference } from "./schemas.mjs";
|
|
7
|
+
|
|
8
|
+
export function defineJobGet(client) {
|
|
9
|
+
return {
|
|
10
|
+
title: "Read an image, video, or Action job",
|
|
11
|
+
description: "Read or resume asynchronous image, video, or Action work. Successful image and Action results include durable asset handles and scoped preview links.",
|
|
12
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
13
|
+
inputSchema: z.object({ jobId: jobReference }),
|
|
14
|
+
markdown: true,
|
|
15
|
+
handler: ({ jobId }) => client.getJob(jobId),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Tool definition: job_wait
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { jobReference } from "./schemas.mjs";
|
|
7
|
+
import { withProgress } from "./progress.mjs";
|
|
8
|
+
|
|
9
|
+
export function defineJobWait(client) {
|
|
10
|
+
return {
|
|
11
|
+
title: "Wait for an image, video, or Action job",
|
|
12
|
+
description: "Wait for existing image, video, or Action work to finish. Image and Action work returns durable chat-renderable assets; video returns its authenticated output path.",
|
|
13
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
14
|
+
inputSchema: z.object({ jobId: jobReference, timeoutSeconds: z.number().min(1).max(3_600).default(900) }),
|
|
15
|
+
markdown: true,
|
|
16
|
+
handler: ({ jobId, timeoutSeconds }, extra) => client.waitForJob(jobId, withProgress({ timeoutMs: timeoutSeconds * 1000 }, extra)),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Tool definition: model_get
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { modelReference } from "./schemas.mjs";
|
|
7
|
+
|
|
8
|
+
export function defineModelGet(client) {
|
|
9
|
+
return {
|
|
10
|
+
title: "Read an image model schema",
|
|
11
|
+
description: "Inspect one model's exact capabilities, accepted parameters, connection, and duration estimate.",
|
|
12
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
13
|
+
inputSchema: z.object({ modelId: modelReference }),
|
|
14
|
+
handler: ({ modelId }) => client.getModel(modelId),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Tool definition: model_list
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { listOptions } from "./helpers.mjs";
|
|
7
|
+
import { listCursor, listLimit } from "./schemas.mjs";
|
|
8
|
+
|
|
9
|
+
export function defineModelList(client) {
|
|
10
|
+
return {
|
|
11
|
+
title: "List runnable AI models",
|
|
12
|
+
description: "Discover image or video models from saved connections with capabilities, typed parameters, and estimated duration.",
|
|
13
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
query: z.string().max(240).optional(),
|
|
16
|
+
provider: z.enum(["craftboard", "openai", "google/nano-banana", "black-forest-labs", "recraft", "ideogram", "qwen", "openrouter"]).optional(),
|
|
17
|
+
capability: z.enum(["image.generate", "image.edit", "image.variations", "video.generate", "video.generate.fromImage", "video.generate.fromFrames", "video.generate.fromReferences"]).optional(),
|
|
18
|
+
limit: listLimit(100),
|
|
19
|
+
cursor: listCursor,
|
|
20
|
+
}),
|
|
21
|
+
handler: ({ query, provider, capability, limit, cursor }) => client.listModels({ query, provider, capability }, listOptions(limit, cursor)),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Tool definition: node_create
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
// Graph mutation: resolves baseRevision, then applies one operation atomically.
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { canvasReference, revisionFields } from "./schemas.mjs";
|
|
8
|
+
|
|
9
|
+
export function defineNodeCreate(client) {
|
|
10
|
+
const config = {
|
|
11
|
+
title: "Create a canvas node",
|
|
12
|
+
description: "Create a text, sticky, or empty image node. Image bytes must be attached through an image job.",
|
|
13
|
+
destructiveHint: false,
|
|
14
|
+
schema: z.object({
|
|
15
|
+
canvasId: canvasReference,
|
|
16
|
+
...revisionFields,
|
|
17
|
+
clientId: z.string().min(1).max(100).optional(),
|
|
18
|
+
node: z.record(z.unknown()),
|
|
19
|
+
}),
|
|
20
|
+
operation: ({ clientId, node }) => ({ type: "node.create", ...(clientId ? { clientId } : {}), node }),
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
title: config.title,
|
|
24
|
+
description: config.description,
|
|
25
|
+
annotations: { readOnlyHint: false, destructiveHint: config.destructiveHint, idempotentHint: true, openWorldHint: false },
|
|
26
|
+
inputSchema: config.schema,
|
|
27
|
+
handler: async (input) => {
|
|
28
|
+
const baseRevision = input.baseRevision || (await client.getCanvas(input.canvasId)).canvas.revision;
|
|
29
|
+
return client.applyOperations(input.canvasId, {
|
|
30
|
+
baseRevision,
|
|
31
|
+
idempotencyKey: input.idempotencyKey,
|
|
32
|
+
operations: [config.operation(input)],
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Tool definition: node_delete
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
// Graph mutation: resolves baseRevision, then applies one operation atomically.
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { canvasReference, nodeReference, revisionFields } from "./schemas.mjs";
|
|
8
|
+
|
|
9
|
+
export function defineNodeDelete(client) {
|
|
10
|
+
const config = {
|
|
11
|
+
title: "Delete a canvas node",
|
|
12
|
+
description: "Delete a node and its attached connections. This is destructive and should follow explicit user intent.",
|
|
13
|
+
destructiveHint: true,
|
|
14
|
+
schema: z.object({ canvasId: canvasReference, ...revisionFields, nodeId: nodeReference }),
|
|
15
|
+
operation: ({ nodeId }) => ({ type: "node.delete", nodeId }),
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
title: config.title,
|
|
19
|
+
description: config.description,
|
|
20
|
+
annotations: { readOnlyHint: false, destructiveHint: config.destructiveHint, idempotentHint: true, openWorldHint: false },
|
|
21
|
+
inputSchema: config.schema,
|
|
22
|
+
handler: async (input) => {
|
|
23
|
+
const baseRevision = input.baseRevision || (await client.getCanvas(input.canvasId)).canvas.revision;
|
|
24
|
+
return client.applyOperations(input.canvasId, {
|
|
25
|
+
baseRevision,
|
|
26
|
+
idempotencyKey: input.idempotencyKey,
|
|
27
|
+
operations: [config.operation(input)],
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Tool definition: node_get
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { canvasReference, nodeReference } from "./schemas.mjs";
|
|
7
|
+
|
|
8
|
+
export function defineNodeGet(client) {
|
|
9
|
+
return {
|
|
10
|
+
title: "Read a canvas node",
|
|
11
|
+
description: "Read one node with incoming and outgoing connections.",
|
|
12
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
13
|
+
inputSchema: z.object({ canvasId: canvasReference, nodeId: nodeReference }),
|
|
14
|
+
handler: ({ canvasId, nodeId }) => client.getNode(canvasId, nodeId),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Tool definition: node_move
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
// Graph mutation: resolves baseRevision, then applies one operation atomically.
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { canvasReference, nodeReference, revisionFields } from "./schemas.mjs";
|
|
8
|
+
|
|
9
|
+
export function defineNodeMove(client) {
|
|
10
|
+
const config = {
|
|
11
|
+
title: "Move a canvas node",
|
|
12
|
+
description: "Move one node to an exact canvas position.",
|
|
13
|
+
destructiveHint: false,
|
|
14
|
+
schema: z.object({
|
|
15
|
+
canvasId: canvasReference,
|
|
16
|
+
...revisionFields,
|
|
17
|
+
nodeId: nodeReference,
|
|
18
|
+
x: z.number().finite(),
|
|
19
|
+
y: z.number().finite(),
|
|
20
|
+
}),
|
|
21
|
+
operation: ({ nodeId, x, y }) => ({ type: "node.move", nodeId, position: { x, y } }),
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
title: config.title,
|
|
25
|
+
description: config.description,
|
|
26
|
+
annotations: { readOnlyHint: false, destructiveHint: config.destructiveHint, idempotentHint: true, openWorldHint: false },
|
|
27
|
+
inputSchema: config.schema,
|
|
28
|
+
handler: async (input) => {
|
|
29
|
+
const baseRevision = input.baseRevision || (await client.getCanvas(input.canvasId)).canvas.revision;
|
|
30
|
+
return client.applyOperations(input.canvasId, {
|
|
31
|
+
baseRevision,
|
|
32
|
+
idempotencyKey: input.idempotencyKey,
|
|
33
|
+
operations: [config.operation(input)],
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Tool definition: node_resize
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
// Graph mutation: resolves baseRevision, then applies one operation atomically.
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { canvasReference, nodeReference, revisionFields } from "./schemas.mjs";
|
|
8
|
+
|
|
9
|
+
export function defineNodeResize(client) {
|
|
10
|
+
const config = {
|
|
11
|
+
title: "Resize a canvas node",
|
|
12
|
+
description: "Resize one node to exact dimensions.",
|
|
13
|
+
destructiveHint: false,
|
|
14
|
+
schema: z.object({
|
|
15
|
+
canvasId: canvasReference,
|
|
16
|
+
...revisionFields,
|
|
17
|
+
nodeId: nodeReference,
|
|
18
|
+
width: z.number().min(40).max(10_000),
|
|
19
|
+
height: z.number().min(40).max(10_000),
|
|
20
|
+
}),
|
|
21
|
+
operation: ({ nodeId, width, height }) => ({ type: "node.resize", nodeId, width, height }),
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
title: config.title,
|
|
25
|
+
description: config.description,
|
|
26
|
+
annotations: { readOnlyHint: false, destructiveHint: config.destructiveHint, idempotentHint: true, openWorldHint: false },
|
|
27
|
+
inputSchema: config.schema,
|
|
28
|
+
handler: async (input) => {
|
|
29
|
+
const baseRevision = input.baseRevision || (await client.getCanvas(input.canvasId)).canvas.revision;
|
|
30
|
+
return client.applyOperations(input.canvasId, {
|
|
31
|
+
baseRevision,
|
|
32
|
+
idempotencyKey: input.idempotencyKey,
|
|
33
|
+
operations: [config.operation(input)],
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Tool definition: node_update
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
// Graph mutation: resolves baseRevision, then applies one operation atomically.
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { canvasReference, nodeReference, revisionFields } from "./schemas.mjs";
|
|
8
|
+
|
|
9
|
+
export function defineNodeUpdate(client) {
|
|
10
|
+
const config = {
|
|
11
|
+
title: "Update a canvas node",
|
|
12
|
+
description: "Update supported node title, position, size, or semantic metadata while preserving server-owned image fields.",
|
|
13
|
+
destructiveHint: false,
|
|
14
|
+
schema: z.object({
|
|
15
|
+
canvasId: canvasReference,
|
|
16
|
+
...revisionFields,
|
|
17
|
+
nodeId: nodeReference,
|
|
18
|
+
patch: z.record(z.unknown()),
|
|
19
|
+
}),
|
|
20
|
+
operation: ({ nodeId, patch }) => ({ type: "node.update", nodeId, patch }),
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
title: config.title,
|
|
24
|
+
description: config.description,
|
|
25
|
+
annotations: { readOnlyHint: false, destructiveHint: config.destructiveHint, idempotentHint: true, openWorldHint: false },
|
|
26
|
+
inputSchema: config.schema,
|
|
27
|
+
handler: async (input) => {
|
|
28
|
+
const baseRevision = input.baseRevision || (await client.getCanvas(input.canvasId)).canvas.revision;
|
|
29
|
+
return client.applyOperations(input.canvasId, {
|
|
30
|
+
baseRevision,
|
|
31
|
+
idempotencyKey: input.idempotencyKey,
|
|
32
|
+
operations: [config.operation(input)],
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Progress notifications for the tools that wait on a job or Run.
|
|
2
|
+
//
|
|
3
|
+
// image_generate, image_edit, image_variations, video_generate, recipe_run,
|
|
4
|
+
// action_run, job_wait, and run_wait can each block for minutes. Without progress
|
|
5
|
+
// the caller sees a single silent pause and cannot tell a queued job from a stuck
|
|
6
|
+
// one. The polling loops in client.mjs already fire onProgress on every status
|
|
7
|
+
// change; this turns that into an MCP notifications/progress message.
|
|
8
|
+
//
|
|
9
|
+
// Two rules shape everything below.
|
|
10
|
+
//
|
|
11
|
+
// A client that sent no progressToken must observe zero new messages. The MCP spec
|
|
12
|
+
// makes progress opt-in per request, and an unsolicited notification is a protocol
|
|
13
|
+
// violation the client is entitled to drop or error on.
|
|
14
|
+
//
|
|
15
|
+
// The payload carries a status word and nothing else. Run and job records hold
|
|
16
|
+
// signed URLs, storage keys, prompts, and output sizes; a notification is the
|
|
17
|
+
// easiest place to leak one by accident, because it looks like a log line rather
|
|
18
|
+
// than a response.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Status words safe to repeat back. Statuses come from the server, so an
|
|
22
|
+
* allowlist — rather than a filter — is what keeps an unexpected value (an error
|
|
23
|
+
* string, a provider message) out of the payload.
|
|
24
|
+
*/
|
|
25
|
+
export const REPORTABLE_STATUSES = new Set([
|
|
26
|
+
"canceled",
|
|
27
|
+
"canceling",
|
|
28
|
+
"expired",
|
|
29
|
+
"failed",
|
|
30
|
+
"finalizing",
|
|
31
|
+
"pending",
|
|
32
|
+
"preparing",
|
|
33
|
+
"queued",
|
|
34
|
+
"review_required",
|
|
35
|
+
"running",
|
|
36
|
+
"succeeded",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** The one line a progress notification is allowed to say. */
|
|
40
|
+
export function progressMessage(result) {
|
|
41
|
+
const status = String(result?.status || "").toLowerCase();
|
|
42
|
+
return REPORTABLE_STATUSES.has(status) ? `Status: ${status}` : "Working";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* How long to wait for one notification before giving up on it.
|
|
48
|
+
*
|
|
49
|
+
* Delivery is awaited so notifications stay ordered, but the await has to be
|
|
50
|
+
* bounded: stdout writes wait on drain, and a client that has stopped reading
|
|
51
|
+
* would otherwise stall the polling loop forever, holding back the terminal
|
|
52
|
+
* result the caller actually needs. Dropping a status line is the cheaper loss.
|
|
53
|
+
*/
|
|
54
|
+
const NOTIFICATION_SEND_TIMEOUT_MS = 5_000;
|
|
55
|
+
|
|
56
|
+
/** Await a send for at most NOTIFICATION_SEND_TIMEOUT_MS, and never throw. */
|
|
57
|
+
function deliver(sent) {
|
|
58
|
+
// A rejection after the race has moved on would otherwise be unhandled.
|
|
59
|
+
const settled = Promise.resolve(sent).catch(() => undefined);
|
|
60
|
+
let timer;
|
|
61
|
+
return Promise.race([
|
|
62
|
+
settled.finally(() => clearTimeout(timer)),
|
|
63
|
+
new Promise((resolve) => {
|
|
64
|
+
timer = setTimeout(resolve, NOTIFICATION_SEND_TIMEOUT_MS);
|
|
65
|
+
timer.unref?.();
|
|
66
|
+
}),
|
|
67
|
+
]).then(() => undefined);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* An onProgress callback for client.waitForJob / client.waitForRun, or undefined
|
|
72
|
+
* when the caller did not ask for progress.
|
|
73
|
+
*
|
|
74
|
+
* Returning undefined rather than a no-op function is deliberate: the wait options
|
|
75
|
+
* spread it, so undefined means the polling loop never calls anything, and a client
|
|
76
|
+
* that sent no progressToken cannot receive a message even by mistake.
|
|
77
|
+
*
|
|
78
|
+
* @param extra The second argument the MCP SDK hands a tool handler.
|
|
79
|
+
*/
|
|
80
|
+
export function createProgressReporter(extra) {
|
|
81
|
+
const progressToken = extra?._meta?.progressToken;
|
|
82
|
+
if (progressToken === undefined || progressToken === null) return undefined;
|
|
83
|
+
if (typeof extra?.sendNotification !== "function") return undefined;
|
|
84
|
+
|
|
85
|
+
let progress = 0;
|
|
86
|
+
return async (result) => {
|
|
87
|
+
// The spec requires progress to increase on every notification for a token.
|
|
88
|
+
// Duration is genuinely unknown here, so total is omitted rather than guessed.
|
|
89
|
+
progress += 1;
|
|
90
|
+
await deliver(extra.sendNotification({
|
|
91
|
+
method: "notifications/progress",
|
|
92
|
+
params: { progressToken, progress, message: progressMessage(result) },
|
|
93
|
+
}));
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Wait options for a polling call, carrying a reporter only when one was requested. */
|
|
98
|
+
export function withProgress(waitOptions, extra) {
|
|
99
|
+
const onProgress = createProgressReporter(extra);
|
|
100
|
+
return onProgress ? { ...waitOptions, onProgress } : waitOptions;
|
|
101
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Tool definition: provider_list
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { listOptions } from "./helpers.mjs";
|
|
7
|
+
import { listCursor, listLimit } from "./schemas.mjs";
|
|
8
|
+
|
|
9
|
+
export function defineProviderList(client) {
|
|
10
|
+
return {
|
|
11
|
+
title: "List AI providers",
|
|
12
|
+
description: "List the actor's saved AI connections and available image or video models without exposing provider secrets.",
|
|
13
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
14
|
+
inputSchema: z.object({ limit: listLimit(100), cursor: listCursor }),
|
|
15
|
+
handler: ({ limit, cursor }) => client.listConnections(listOptions(limit, cursor)),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Tool definition: recipe_fork
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { exactCanvasReference, recipeForkIdempotencyKey, recipeReference, revisionFields } from "./schemas.mjs";
|
|
7
|
+
|
|
8
|
+
export function defineRecipeFork(client) {
|
|
9
|
+
return {
|
|
10
|
+
title: "Add a compact Recipe workflow",
|
|
11
|
+
description: "Copy one approved Recipe into a canvas as one compact workflow node with a private internal graph. This never starts a job or generation.",
|
|
12
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
13
|
+
inputSchema: z.object({
|
|
14
|
+
recipeId: recipeReference,
|
|
15
|
+
canvasId: exactCanvasReference,
|
|
16
|
+
version: z.string().min(1).max(80).optional(),
|
|
17
|
+
baseRevision: revisionFields.baseRevision,
|
|
18
|
+
x: z.number().finite().min(-100_000).max(100_000).optional().describe("Optional fork anchor X coordinate."),
|
|
19
|
+
y: z.number().finite().min(-100_000).max(100_000).optional().describe("Optional fork anchor Y coordinate."),
|
|
20
|
+
idempotencyKey: recipeForkIdempotencyKey,
|
|
21
|
+
}),
|
|
22
|
+
handler: async ({ recipeId, canvasId, version, baseRevision, x, y, idempotencyKey }) =>
|
|
23
|
+
client.forkRecipe(recipeId, {
|
|
24
|
+
canvasId,
|
|
25
|
+
...(version ? { version } : {}),
|
|
26
|
+
...(baseRevision ? { baseRevision } : {}),
|
|
27
|
+
...(x !== undefined ? { x } : {}),
|
|
28
|
+
...(y !== undefined ? { y } : {}),
|
|
29
|
+
idempotencyKey,
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Tool definition: recipe_get
|
|
2
|
+
//
|
|
3
|
+
// Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
|
|
4
|
+
// this tool exists and on which surfaces; this file is what it does.
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { recipeReference } from "./schemas.mjs";
|
|
7
|
+
|
|
8
|
+
export function defineRecipeGet(client) {
|
|
9
|
+
return {
|
|
10
|
+
title: "Read a Recipe",
|
|
11
|
+
description: "Inspect an approved Recipe's graph, inputs, outputs, author, and version before deciding whether to fork it.",
|
|
12
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
13
|
+
inputSchema: z.object({
|
|
14
|
+
recipeId: recipeReference,
|
|
15
|
+
version: z.string().min(1).max(80).optional(),
|
|
16
|
+
}),
|
|
17
|
+
handler: ({ recipeId, version }) => client.getRecipe(recipeId, version),
|
|
18
|
+
};
|
|
19
|
+
}
|