infiniteboard-mcp 0.1.0 → 0.3.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 +3 -1
- package/package.json +2 -1
- package/server.mjs +4 -135
- package/tools.mjs +229 -0
package/README.md
CHANGED
|
@@ -9,7 +9,9 @@ Lets AI assistants (Claude Code, Claude Desktop, Cursor, VS Code, or any MCP cli
|
|
|
9
9
|
npx -y infiniteboard-mcp
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
3. Ask away: *"
|
|
12
|
+
3. Ask away: *"Plan a habit-tracking app on my board: users, features, architecture and a roadmap"*.
|
|
13
|
+
|
|
14
|
+
Assistants can build laid-out flowcharts, system architectures, org charts, mind maps, kanban columns, grids and timelines in one call (`create_diagram`), tidy existing content (`arrange_elements`), and check their work with screenshots that don't move your view. In Claude Code, `/mcp__infiniteboard__plan_board` starts a guided plan of any idea.
|
|
13
15
|
|
|
14
16
|
The server runs on your machine. The board tab connects to it over `ws://127.0.0.1:3917` only while its AI switch is on.
|
|
15
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infiniteboard-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Let AI assistants (Claude, Cursor, VS Code, any MCP client) draw on your open Infiniteboard whiteboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"server.mjs",
|
|
19
|
+
"tools.mjs",
|
|
19
20
|
"README.md",
|
|
20
21
|
"LICENSE"
|
|
21
22
|
],
|
package/server.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
11
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
12
|
import { WebSocketServer } from "ws";
|
|
13
|
-
import {
|
|
13
|
+
import { INSTRUCTIONS, registerBoardTools } from "./tools.mjs";
|
|
14
14
|
|
|
15
15
|
const argv = process.argv.slice(2);
|
|
16
16
|
const flag = (name) => argv.flatMap((a, i) => (a === name && argv[i + 1] ? [argv[i + 1]] : a.startsWith(`${name}=`) ? [a.slice(name.length + 1)] : []));
|
|
@@ -97,141 +97,10 @@ function call(method, params = {}) {
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
const asError = (e) => ({ isError: true, content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }] });
|
|
102
|
-
const relay = (method) => async (params) => {
|
|
103
|
-
try {
|
|
104
|
-
return asText(await call(method, params));
|
|
105
|
-
} catch (e) {
|
|
106
|
-
return asError(e);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// ---------- tools ----------
|
|
111
|
-
|
|
112
|
-
const server = new McpServer(
|
|
113
|
-
{ name: "infiniteboard", version: "0.1.0" },
|
|
114
|
-
{
|
|
115
|
-
instructions: [
|
|
116
|
-
"Tools for reading and editing the user's open whiteboard (an infinite canvas).",
|
|
117
|
-
"Coordinates are world units: 1 unit = 1 px at 100% zoom, x grows right, y grows down. Elements have x/y (top-left) and w/h.",
|
|
118
|
-
"Typical sizes: sticky 180×180, shape 160×110, frame 800×500, text 20px font. Leave ~40–80 units between items.",
|
|
119
|
-
"Start with get_board to see what exists; omit x/y when creating to place items in free space to the right of existing content.",
|
|
120
|
-
"Connectors attach to elements via from/to (ids, or refs defined earlier in the same create_elements call) and follow them when they move.",
|
|
121
|
-
"Each create/update/delete call is one undo step for the user. Use get_screenshot to check the visual result.",
|
|
122
|
-
].join(" "),
|
|
123
|
-
},
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const color = z.string().describe("Hex colour (#rrggbb) or a name: yellow, orange, pink, purple, blue, green, lime, gray, white, black, red, darkblue, darkgreen");
|
|
127
|
-
const point = z.tuple([z.number(), z.number()]);
|
|
128
|
-
const common = {
|
|
129
|
-
x: z.number().optional().describe("Left edge (world units). Omit x and y to auto-place in free space"),
|
|
130
|
-
y: z.number().optional().describe("Top edge (world units)"),
|
|
131
|
-
w: z.number().positive().optional(),
|
|
132
|
-
h: z.number().positive().optional(),
|
|
133
|
-
text: z.string().optional().describe("Text for sticky, shape and text elements (plain text, \\n for new lines)"),
|
|
134
|
-
title: z.string().optional().describe("Frame title"),
|
|
135
|
-
color: color.optional().describe("Sticky/frame: background. Text/shape: text colour. Connector/stroke: line colour"),
|
|
136
|
-
fill: z.string().optional().describe('Shape fill colour, or "none"'),
|
|
137
|
-
stroke: color.optional().describe("Shape border colour"),
|
|
138
|
-
kind: z.enum(["rect", "roundrect", "ellipse", "diamond", "triangle"]).optional().describe("Shape kind (default rect)"),
|
|
139
|
-
fontSize: z.number().positive().optional(),
|
|
140
|
-
align: z.enum(["left", "center", "right"]).optional(),
|
|
141
|
-
bold: z.boolean().optional(),
|
|
142
|
-
angle: z.number().optional().describe("Rotation in degrees, clockwise"),
|
|
143
|
-
from: z.string().optional().describe("Connector start: element id or ref"),
|
|
144
|
-
to: z.string().optional().describe("Connector end: element id or ref"),
|
|
145
|
-
fromPoint: point.optional().describe("Connector start as [x, y] when not attached"),
|
|
146
|
-
toPoint: point.optional().describe("Connector end as [x, y] when not attached"),
|
|
147
|
-
route: z.enum(["straight", "elbow", "curved"]).optional(),
|
|
148
|
-
label: z.string().optional().describe("Connector label"),
|
|
149
|
-
endHead: z.enum(["none", "open", "filled"]).optional(),
|
|
150
|
-
startHead: z.enum(["none", "open", "filled"]).optional(),
|
|
151
|
-
dash: z.enum(["solid", "dashed", "dotted"]).optional(),
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
server.registerTool("get_board", {
|
|
155
|
-
title: "Read the board",
|
|
156
|
-
description: "Lists the board's elements (id, type, position, size, text, colours, frame membership, connector endpoints), the visible viewport, content bounds and current selection.",
|
|
157
|
-
inputSchema: {
|
|
158
|
-
type: z.enum(["sticky", "shape", "text", "frame", "connector", "stroke", "image"]).optional().describe("Only list this element type"),
|
|
159
|
-
limit: z.number().int().positive().max(5000).optional().describe("Maximum elements to return (default 500)"),
|
|
160
|
-
},
|
|
161
|
-
annotations: { readOnlyHint: true },
|
|
162
|
-
}, relay("get_board"));
|
|
163
|
-
|
|
164
|
-
server.registerTool("create_elements", {
|
|
165
|
-
title: "Create elements",
|
|
166
|
-
description:
|
|
167
|
-
"Creates sticky notes, shapes, text, frames, connectors and pen strokes in one undoable step. " +
|
|
168
|
-
"Give elements a `ref` to connect them in the same call, e.g. [{type:'sticky',ref:'a',text:'Idea'},{type:'sticky',ref:'b',text:'Plan'},{type:'connector',from:'a',to:'b'}]. " +
|
|
169
|
-
"Elements placed at least half inside a frame automatically belong to it. Returns the new ids.",
|
|
170
|
-
inputSchema: {
|
|
171
|
-
elements: z.array(z.object({
|
|
172
|
-
type: z.enum(["sticky", "shape", "text", "frame", "connector", "stroke"]),
|
|
173
|
-
ref: z.string().optional().describe("Your own key for referring to this element later in the same call"),
|
|
174
|
-
...common,
|
|
175
|
-
points: z.array(point).optional().describe("Stroke points [[x, y], …] in world units"),
|
|
176
|
-
width: z.number().positive().optional().describe("Stroke width"),
|
|
177
|
-
})).min(1).max(500),
|
|
178
|
-
},
|
|
179
|
-
}, relay("create_elements"));
|
|
180
|
-
|
|
181
|
-
server.registerTool("update_elements", {
|
|
182
|
-
title: "Update elements",
|
|
183
|
-
description: "Moves, resizes, recolours, retexts or rewires existing elements (one undoable step). Only the fields you pass change. Set locked to stop the user moving an element.",
|
|
184
|
-
inputSchema: {
|
|
185
|
-
updates: z.array(z.object({ id: z.string(), ...common, locked: z.boolean().optional() })).min(1).max(1000),
|
|
186
|
-
},
|
|
187
|
-
}, relay("update_elements"));
|
|
188
|
-
|
|
189
|
-
server.registerTool("delete_elements", {
|
|
190
|
-
title: "Delete elements",
|
|
191
|
-
description: "Deletes elements by id (deleting a frame deletes its contents). Pass all: true to clear the board. One undoable step.",
|
|
192
|
-
inputSchema: {
|
|
193
|
-
ids: z.array(z.string()).optional(),
|
|
194
|
-
all: z.boolean().optional().describe("Delete everything on the board"),
|
|
195
|
-
},
|
|
196
|
-
annotations: { destructiveHint: true },
|
|
197
|
-
}, relay("delete_elements"));
|
|
198
|
-
|
|
199
|
-
server.registerTool("focus_view", {
|
|
200
|
-
title: "Move the view",
|
|
201
|
-
description: "Pans/zooms the user's view: fit the given element ids, centre on x/y (optionally at a zoom), set a zoom level, or with no arguments fit the whole board.",
|
|
202
|
-
inputSchema: {
|
|
203
|
-
ids: z.array(z.string()).optional(),
|
|
204
|
-
x: z.number().optional(),
|
|
205
|
-
y: z.number().optional(),
|
|
206
|
-
zoom: z.number().positive().max(8).optional().describe("1 = 100%"),
|
|
207
|
-
},
|
|
208
|
-
}, relay("focus_view"));
|
|
209
|
-
|
|
210
|
-
server.registerTool("select_elements", {
|
|
211
|
-
title: "Select elements",
|
|
212
|
-
description: "Highlights elements for the user by selecting them (empty list clears the selection).",
|
|
213
|
-
inputSchema: { ids: z.array(z.string()) },
|
|
214
|
-
}, relay("select"));
|
|
215
|
-
|
|
216
|
-
server.registerTool("undo", { title: "Undo", description: "Undoes the last change on the board (the user's or the assistant's).", inputSchema: {} }, relay("undo"));
|
|
217
|
-
server.registerTool("redo", { title: "Redo", description: "Redoes the last undone change.", inputSchema: {} }, relay("redo"));
|
|
218
|
-
|
|
219
|
-
server.registerTool("get_screenshot", {
|
|
220
|
-
title: "Screenshot the board",
|
|
221
|
-
description: "Returns a PNG of what the user currently sees. Use focus_view first to frame the area you want to check.",
|
|
222
|
-
inputSchema: { maxWidth: z.number().int().min(200).max(2400).optional().describe("Maximum image width in pixels (default 1280)") },
|
|
223
|
-
annotations: { readOnlyHint: true },
|
|
224
|
-
}, async (params) => {
|
|
225
|
-
try {
|
|
226
|
-
const { image } = await call("screenshot", params);
|
|
227
|
-
const m = /^data:(image\/\w+);base64,(.*)$/s.exec(image ?? "");
|
|
228
|
-
if (!m) throw new Error("The board returned no image");
|
|
229
|
-
return { content: [{ type: "image", mimeType: m[1], data: m[2] }] };
|
|
230
|
-
} catch (e) {
|
|
231
|
-
return asError(e);
|
|
232
|
-
}
|
|
233
|
-
});
|
|
100
|
+
// ---------- MCP server ----------
|
|
234
101
|
|
|
102
|
+
const server = new McpServer({ name: "infiniteboard", version: "0.3.0" }, { instructions: INSTRUCTIONS });
|
|
103
|
+
registerBoardTools(server, call);
|
|
235
104
|
await server.connect(new StdioServerTransport());
|
|
236
105
|
log("MCP server ready on stdio");
|
|
237
106
|
|
package/tools.mjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Board tools shared by the local stdio server (server.mjs) and the hosted endpoint (api/mcp.js).
|
|
2
|
+
// `call(method, params)` delivers one command to the open board tab and resolves with its result.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
export const INSTRUCTIONS = [
|
|
6
|
+
"Tools for reading and editing the user's open whiteboard (an infinite canvas). The user watches changes live and can undo each call.",
|
|
7
|
+
"For anything with structure (flowcharts, processes, system architecture, org charts, mind maps, feature breakdowns, kanban, roadmaps, timelines, brainstorm clusters) use create_diagram: describe nodes, edges and groups and the board lays them out, sizes nodes to their text and routes the connectors. Don't compute coordinates by hand for those.",
|
|
8
|
+
"For planning a whole product, project or idea, follow the plan_board prompt: a set of titled sections placed side by side, each built with create_diagram, then checked with get_screenshot.",
|
|
9
|
+
"Use create_elements for small additions (a note, a label, one connector) and update_elements / arrange_elements to edit or tidy. Coordinates are world units, x right, y down; elements have x/y (top-left) and w/h.",
|
|
10
|
+
"On a board with existing content, start with get_board {outline:true}, then read sections with get_board {frame:id}. Place new work with place:{near:id, side} so it doesn't overlap.",
|
|
11
|
+
"get_screenshot {ids:[...]} shows any region without moving the user's view; check your work with it and fix overlaps or unclear labels.",
|
|
12
|
+
].join(" ");
|
|
13
|
+
|
|
14
|
+
export const PLAN_BOARD = `Plan this on my InfiniteBoard as a clear visual workspace: {idea}
|
|
15
|
+
|
|
16
|
+
Work like a strong product lead and systems designer:
|
|
17
|
+
1. Think first. Work out the goal, the users, the core features, how it works, and how to get there. Make sensible assumptions and state them on the board instead of asking.
|
|
18
|
+
2. Choose the sections that fit this idea (not all are needed; add others if they help). For a product, typically:
|
|
19
|
+
- Overview: problem, goal, target users, success metrics (grid of notes or cards, grouped)
|
|
20
|
+
- Users and their journey (flow, direction right)
|
|
21
|
+
- Features by priority (columns: Must / Should / Later)
|
|
22
|
+
- System architecture (flow, direction right, groups such as Client / Backend / Data / External, with database and queue kinds and labelled edges)
|
|
23
|
+
- Key flow or data model (flow with decisions)
|
|
24
|
+
- Roadmap (timeline with dates in detail, or columns Now / Next / Later)
|
|
25
|
+
- Risks and open questions (grid)
|
|
26
|
+
3. Build each section with one create_diagram call and a title. Place the first section where it lands by default, the next ones with place:{near:<previous section frame or heading id>, side:"right"}, and start a new row with side:"below" from the first section of the previous row. Use 2-3 sections per row.
|
|
27
|
+
4. Keep labels short and specific to this idea; no filler.
|
|
28
|
+
5. Check each section with get_screenshot {ids:[sectionId]} and fix anything unclear, crowded or overlapping (update_elements, arrange_elements, or delete and recreate).
|
|
29
|
+
6. Finish with focus_view (no arguments) to show the whole plan, and summarise what you put on the board in a few lines.`;
|
|
30
|
+
|
|
31
|
+
const asText = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 1) }] });
|
|
32
|
+
const asError = (e) => ({ isError: true, content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }] });
|
|
33
|
+
const relayVia = (call) => (method) => async (params) => {
|
|
34
|
+
try {
|
|
35
|
+
return asText(await call(method, params));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return asError(e);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
|
|
43
|
+
* @param {(method: string, params?: object) => Promise<any>} call
|
|
44
|
+
*/
|
|
45
|
+
export function registerBoardTools(server, call) {
|
|
46
|
+
const relay = relayVia(call);
|
|
47
|
+
const color = z.string().describe("Hex colour (#rrggbb) or a name: yellow, orange, pink, purple, blue, green, lime, gray, white, black, red, darkblue, darkgreen");
|
|
48
|
+
const point = z.tuple([z.number(), z.number()]);
|
|
49
|
+
const common = {
|
|
50
|
+
x: z.number().optional().describe("Left edge (world units). Omit x and y to auto-place in free space"),
|
|
51
|
+
y: z.number().optional().describe("Top edge (world units)"),
|
|
52
|
+
w: z.number().positive().optional(),
|
|
53
|
+
h: z.number().positive().optional(),
|
|
54
|
+
text: z.string().optional().describe("Text for sticky, shape and text elements (plain text, \\n for new lines)"),
|
|
55
|
+
title: z.string().optional().describe("Frame title"),
|
|
56
|
+
color: color.optional().describe("Sticky/frame: background. Text/shape: text colour. Connector/stroke: line colour"),
|
|
57
|
+
fill: z.string().optional().describe('Shape fill colour, or "none"'),
|
|
58
|
+
stroke: color.optional().describe("Shape border colour"),
|
|
59
|
+
kind: z.enum(["rect", "roundrect", "ellipse", "diamond", "triangle", "hexagon", "parallelogram", "cylinder"]).optional().describe("Shape kind (default rect). cylinder = database/storage, parallelogram = input/output, hexagon = queue/event"),
|
|
60
|
+
fontSize: z.number().positive().optional(),
|
|
61
|
+
align: z.enum(["left", "center", "right"]).optional(),
|
|
62
|
+
bold: z.boolean().optional(),
|
|
63
|
+
angle: z.number().optional().describe("Rotation in degrees, clockwise"),
|
|
64
|
+
from: z.string().optional().describe("Connector start: element id or ref"),
|
|
65
|
+
to: z.string().optional().describe("Connector end: element id or ref"),
|
|
66
|
+
fromPoint: point.optional().describe("Connector start as [x, y] when not attached"),
|
|
67
|
+
toPoint: point.optional().describe("Connector end as [x, y] when not attached"),
|
|
68
|
+
route: z.enum(["straight", "elbow", "curved"]).optional(),
|
|
69
|
+
label: z.string().optional().describe("Connector label"),
|
|
70
|
+
endHead: z.enum(["none", "open", "filled"]).optional(),
|
|
71
|
+
startHead: z.enum(["none", "open", "filled"]).optional(),
|
|
72
|
+
dash: z.enum(["solid", "dashed", "dotted"]).optional(),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
server.registerTool("get_board", {
|
|
76
|
+
title: "Read the board",
|
|
77
|
+
description: "Lists the board's elements (id, type, position, size, text, colours, frame membership, connector endpoints), the visible viewport, content bounds and current selection. On big boards use outline:true first (sections only), then frame:<id> to read one section.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
outline: z.boolean().optional().describe("Return only an outline: frames (sections) with titles, bounds, what they contain and sample text, plus headings"),
|
|
80
|
+
frame: z.string().optional().describe("Only list this frame and what's inside it"),
|
|
81
|
+
type: z.enum(["sticky", "shape", "text", "frame", "connector", "stroke", "image"]).optional().describe("Only list this element type"),
|
|
82
|
+
limit: z.number().int().positive().max(5000).optional().describe("Maximum elements to return (default 500)"),
|
|
83
|
+
},
|
|
84
|
+
annotations: { readOnlyHint: true },
|
|
85
|
+
}, relay("get_board"));
|
|
86
|
+
|
|
87
|
+
server.registerTool("create_elements", {
|
|
88
|
+
title: "Create elements",
|
|
89
|
+
description:
|
|
90
|
+
"Creates sticky notes, shapes, text, frames, connectors and pen strokes in one undoable step. " +
|
|
91
|
+
"Give elements a `ref` to connect them in the same call, e.g. [{type:'sticky',ref:'a',text:'Idea'},{type:'sticky',ref:'b',text:'Plan'},{type:'connector',from:'a',to:'b'}]. " +
|
|
92
|
+
"Elements placed at least half inside a frame automatically belong to it. Returns the new ids.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
elements: z.array(z.object({
|
|
95
|
+
type: z.enum(["sticky", "shape", "text", "frame", "connector", "stroke"]),
|
|
96
|
+
ref: z.string().optional().describe("Your own key for referring to this element later in the same call"),
|
|
97
|
+
...common,
|
|
98
|
+
points: z.array(point).optional().describe("Stroke points [[x, y], …] in world units"),
|
|
99
|
+
width: z.number().positive().optional().describe("Stroke width"),
|
|
100
|
+
})).min(1).max(500),
|
|
101
|
+
},
|
|
102
|
+
}, relay("create_elements"));
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
const nodeKind = z.enum(["process", "decision", "terminal", "io", "database", "service", "user", "external", "queue", "card", "note", "topic", "text"]);
|
|
106
|
+
server.registerTool("create_diagram", {
|
|
107
|
+
title: "Create a laid-out diagram",
|
|
108
|
+
description: [
|
|
109
|
+
"Builds a complete, automatically laid-out diagram in one undoable step. You describe the structure; the board sizes every node to its text, positions everything without overlaps, draws group frames and routes connectors.",
|
|
110
|
+
"Layouts: flow = layered graph from edges (flowcharts, processes, system architecture, data flow, dependencies; direction down or right); tree = hierarchy from each node's parent (org chart, feature/work breakdown, sitemap); mindmap = one root (no parent) with branches on both sides, coloured per branch; columns = one column per group, nodes stacked (kanban, Now/Next/Later roadmap, user-story map, pros/cons, categories); grid = cards in a grid, one cluster per group (brainstorm, affinity map, personas, risks); timeline = nodes in the given order along an axis, detail shown as the date, groups become phases.",
|
|
111
|
+
"Node kinds: process (rounded box), decision (diamond), terminal (start/end pill), io (parallelogram), database (cylinder), service (box), user (actor), external (grey box), queue (hexagon), card (white card), note (sticky note), topic (mind-map topic), text (plain label). Defaults: flow/tree process, mindmap topic, columns/timeline card, grid note.",
|
|
112
|
+
"Groups become frames around their nodes (e.g. Frontend / Backend / Data in an architecture; lanes; phases). Give the diagram a title to make it a titled section.",
|
|
113
|
+
"Keep node text short (2–8 words); put extra explanation in detail. Returns the element id for each node id, group frame ids, a section id (when titled) and bounds. Place the next diagram with place:{near:<section id>, side}; it goes beside the whole section.",
|
|
114
|
+
].join(" "),
|
|
115
|
+
inputSchema: {
|
|
116
|
+
layout: z.enum(["flow", "tree", "mindmap", "columns", "grid", "timeline"]),
|
|
117
|
+
title: z.string().optional().describe("Section title: shown as the frame title (or as a heading above group frames)"),
|
|
118
|
+
direction: z.enum(["down", "right"]).optional().describe("flow/tree: down (default) or right (good for architecture and pipelines)"),
|
|
119
|
+
nodes: z.array(z.object({
|
|
120
|
+
id: z.string().describe("Your id for this node, used by edges, parent and in the result"),
|
|
121
|
+
text: z.string().describe("Label, short"),
|
|
122
|
+
detail: z.string().optional().describe("Second line of smaller detail; for timeline nodes, the date or period"),
|
|
123
|
+
kind: nodeKind.optional(),
|
|
124
|
+
color: color.optional().describe("Fill tone (pastel): yellow, orange, red, pink, purple, blue, teal, green, lime, gray, white, or #rrggbb"),
|
|
125
|
+
group: z.string().optional().describe("Group id: frame (flow/grid), column (columns) or phase (timeline)"),
|
|
126
|
+
parent: z.string().optional().describe("tree/mindmap: parent node id (omit on the root)"),
|
|
127
|
+
})).min(1).max(400),
|
|
128
|
+
edges: z.array(z.object({
|
|
129
|
+
from: z.string(),
|
|
130
|
+
to: z.string(),
|
|
131
|
+
label: z.string().optional().describe("Short label, e.g. Yes / No / HTTPS / publishes"),
|
|
132
|
+
style: z.enum(["solid", "dashed", "dotted"]).optional().describe("dashed for async/optional/indirect"),
|
|
133
|
+
route: z.enum(["straight", "elbow", "curved"]).optional(),
|
|
134
|
+
bidirectional: z.boolean().optional(),
|
|
135
|
+
})).max(800).optional().describe("Connections. tree/mindmap edges from parent come automatically; add extra cross-links here"),
|
|
136
|
+
groups: z.array(z.object({
|
|
137
|
+
id: z.string(),
|
|
138
|
+
title: z.string().optional(),
|
|
139
|
+
color: color.optional(),
|
|
140
|
+
})).optional().describe("Titles, colours and order for groups (optional; groups used by nodes are created anyway)"),
|
|
141
|
+
place: z.union([
|
|
142
|
+
z.object({ x: z.number(), y: z.number() }).describe("Top-left corner"),
|
|
143
|
+
z.object({ near: z.string().describe("Existing element or frame id"), side: z.enum(["right", "below", "left", "above"]).optional(), gap: z.number().optional() }),
|
|
144
|
+
]).optional().describe("Where to put it. Default: to the right of existing content (or centred in view on an empty board)"),
|
|
145
|
+
spacing: z.enum(["compact", "normal", "roomy"]).optional(),
|
|
146
|
+
focus: z.boolean().optional().describe("Move the user's view to the new diagram (default true)"),
|
|
147
|
+
},
|
|
148
|
+
}, relay("create_diagram"));
|
|
149
|
+
|
|
150
|
+
server.registerTool("arrange_elements", {
|
|
151
|
+
title: "Arrange elements",
|
|
152
|
+
description: "Tidies existing elements in one undoable step: flow/tree re-layout using the connectors between them (connectors re-attach cleanly), or grid/row/column. Pass ids, or a frame to arrange its contents (the frame grows to fit).",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
ids: z.array(z.string()).optional(),
|
|
155
|
+
frame: z.string().optional(),
|
|
156
|
+
layout: z.enum(["flow", "tree", "grid", "row", "column"]).optional().describe("Default flow"),
|
|
157
|
+
direction: z.enum(["down", "right"]).optional(),
|
|
158
|
+
columns: z.number().int().positive().optional().describe("grid: number of columns"),
|
|
159
|
+
spacing: z.enum(["compact", "normal", "roomy"]).optional(),
|
|
160
|
+
},
|
|
161
|
+
}, relay("arrange_elements"));
|
|
162
|
+
|
|
163
|
+
server.registerTool("update_elements", {
|
|
164
|
+
title: "Update elements",
|
|
165
|
+
description: "Moves, resizes, recolours, retexts or rewires existing elements (one undoable step). Only the fields you pass change. Set locked to stop the user moving an element.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
updates: z.array(z.object({ id: z.string(), ...common, locked: z.boolean().optional() })).min(1).max(1000),
|
|
168
|
+
},
|
|
169
|
+
}, relay("update_elements"));
|
|
170
|
+
|
|
171
|
+
server.registerTool("delete_elements", {
|
|
172
|
+
title: "Delete elements",
|
|
173
|
+
description: "Deletes elements by id (deleting a frame deletes its contents). Pass all: true to clear the board. One undoable step.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
ids: z.array(z.string()).optional(),
|
|
176
|
+
all: z.boolean().optional().describe("Delete everything on the board"),
|
|
177
|
+
},
|
|
178
|
+
annotations: { destructiveHint: true },
|
|
179
|
+
}, relay("delete_elements"));
|
|
180
|
+
|
|
181
|
+
server.registerTool("focus_view", {
|
|
182
|
+
title: "Move the view",
|
|
183
|
+
description: "Pans/zooms the user's view: fit the given element ids, centre on x/y (optionally at a zoom), set a zoom level, or with no arguments fit the whole board.",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
ids: z.array(z.string()).optional(),
|
|
186
|
+
x: z.number().optional(),
|
|
187
|
+
y: z.number().optional(),
|
|
188
|
+
zoom: z.number().positive().max(8).optional().describe("1 = 100%"),
|
|
189
|
+
},
|
|
190
|
+
}, relay("focus_view"));
|
|
191
|
+
|
|
192
|
+
server.registerTool("select_elements", {
|
|
193
|
+
title: "Select elements",
|
|
194
|
+
description: "Highlights elements for the user by selecting them (empty list clears the selection).",
|
|
195
|
+
inputSchema: { ids: z.array(z.string()) },
|
|
196
|
+
}, relay("select"));
|
|
197
|
+
|
|
198
|
+
server.registerPrompt("plan_board", {
|
|
199
|
+
title: "Plan an idea on the board",
|
|
200
|
+
description: "Turns a product, project or idea into a structured visual plan on the board.",
|
|
201
|
+
argsSchema: { idea: z.string().describe("What you want to plan, build or explore") },
|
|
202
|
+
}, ({ idea }) => ({
|
|
203
|
+
messages: [{ role: "user", content: { type: "text", text: PLAN_BOARD.replace("{idea}", idea) } }],
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
server.registerTool("undo", { title: "Undo", description: "Undoes the last change on the board (the user's or the assistant's).", inputSchema: {} }, relay("undo"));
|
|
207
|
+
server.registerTool("redo", { title: "Redo", description: "Redoes the last undone change.", inputSchema: {} }, relay("redo"));
|
|
208
|
+
|
|
209
|
+
server.registerTool("get_screenshot", {
|
|
210
|
+
title: "Screenshot the board",
|
|
211
|
+
description: "Returns an image of the board. With ids (or bounds, or all:true) it renders that region without moving the user's view, which is the best way to check a diagram you just made; with no arguments it shows what the user currently sees.",
|
|
212
|
+
inputSchema: {
|
|
213
|
+
ids: z.array(z.string()).optional().describe("Render the area around these elements (e.g. a section frame id)"),
|
|
214
|
+
bounds: z.object({ x: z.number(), y: z.number(), w: z.number().positive(), h: z.number().positive() }).optional(),
|
|
215
|
+
all: z.boolean().optional().describe("Render the whole board"),
|
|
216
|
+
maxWidth: z.number().int().min(200).max(2400).optional().describe("Maximum image width in pixels (default 1280)"),
|
|
217
|
+
},
|
|
218
|
+
annotations: { readOnlyHint: true },
|
|
219
|
+
}, async (params) => {
|
|
220
|
+
try {
|
|
221
|
+
const { image } = await call("screenshot", params);
|
|
222
|
+
const m = /^data:(image\/\w+);base64,(.*)$/s.exec(image ?? "");
|
|
223
|
+
if (!m) throw new Error("The board returned no image");
|
|
224
|
+
return { content: [{ type: "image", mimeType: m[1], data: m[2] }] };
|
|
225
|
+
} catch (e) {
|
|
226
|
+
return asError(e);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|