infiniteboard-mcp 0.2.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 +1 -1
- package/server.mjs +1 -1
- package/tools.mjs +101 -10
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
package/server.mjs
CHANGED
|
@@ -99,7 +99,7 @@ function call(method, params = {}) {
|
|
|
99
99
|
|
|
100
100
|
// ---------- MCP server ----------
|
|
101
101
|
|
|
102
|
-
const server = new McpServer({ name: "infiniteboard", version: "0.
|
|
102
|
+
const server = new McpServer({ name: "infiniteboard", version: "0.3.0" }, { instructions: INSTRUCTIONS });
|
|
103
103
|
registerBoardTools(server, call);
|
|
104
104
|
await server.connect(new StdioServerTransport());
|
|
105
105
|
log("MCP server ready on stdio");
|
package/tools.mjs
CHANGED
|
@@ -3,14 +3,31 @@
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
export const INSTRUCTIONS = [
|
|
6
|
-
"Tools for reading and editing the user's open whiteboard (an infinite canvas).",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
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
12
|
].join(" ");
|
|
13
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
|
+
|
|
14
31
|
const asText = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 1) }] });
|
|
15
32
|
const asError = (e) => ({ isError: true, content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }] });
|
|
16
33
|
const relayVia = (call) => (method) => async (params) => {
|
|
@@ -39,7 +56,7 @@ export function registerBoardTools(server, call) {
|
|
|
39
56
|
color: color.optional().describe("Sticky/frame: background. Text/shape: text colour. Connector/stroke: line colour"),
|
|
40
57
|
fill: z.string().optional().describe('Shape fill colour, or "none"'),
|
|
41
58
|
stroke: color.optional().describe("Shape border colour"),
|
|
42
|
-
kind: z.enum(["rect", "roundrect", "ellipse", "diamond", "triangle"]).optional().describe("Shape kind (default rect)"),
|
|
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"),
|
|
43
60
|
fontSize: z.number().positive().optional(),
|
|
44
61
|
align: z.enum(["left", "center", "right"]).optional(),
|
|
45
62
|
bold: z.boolean().optional(),
|
|
@@ -57,8 +74,10 @@ export function registerBoardTools(server, call) {
|
|
|
57
74
|
|
|
58
75
|
server.registerTool("get_board", {
|
|
59
76
|
title: "Read the board",
|
|
60
|
-
description: "Lists the board's elements (id, type, position, size, text, colours, frame membership, connector endpoints), the visible viewport, content bounds and current selection.",
|
|
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.",
|
|
61
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"),
|
|
62
81
|
type: z.enum(["sticky", "shape", "text", "frame", "connector", "stroke", "image"]).optional().describe("Only list this element type"),
|
|
63
82
|
limit: z.number().int().positive().max(5000).optional().describe("Maximum elements to return (default 500)"),
|
|
64
83
|
},
|
|
@@ -82,6 +101,65 @@ export function registerBoardTools(server, call) {
|
|
|
82
101
|
},
|
|
83
102
|
}, relay("create_elements"));
|
|
84
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
|
+
|
|
85
163
|
server.registerTool("update_elements", {
|
|
86
164
|
title: "Update elements",
|
|
87
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.",
|
|
@@ -117,13 +195,26 @@ export function registerBoardTools(server, call) {
|
|
|
117
195
|
inputSchema: { ids: z.array(z.string()) },
|
|
118
196
|
}, relay("select"));
|
|
119
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
|
+
|
|
120
206
|
server.registerTool("undo", { title: "Undo", description: "Undoes the last change on the board (the user's or the assistant's).", inputSchema: {} }, relay("undo"));
|
|
121
207
|
server.registerTool("redo", { title: "Redo", description: "Redoes the last undone change.", inputSchema: {} }, relay("redo"));
|
|
122
208
|
|
|
123
209
|
server.registerTool("get_screenshot", {
|
|
124
210
|
title: "Screenshot the board",
|
|
125
|
-
description: "Returns
|
|
126
|
-
inputSchema: {
|
|
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
|
+
},
|
|
127
218
|
annotations: { readOnlyHint: true },
|
|
128
219
|
}, async (params) => {
|
|
129
220
|
try {
|