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 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: *"Sketch a user-signup flowchart on my board"*.
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.2.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": {
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.2.0" }, { instructions: INSTRUCTIONS });
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
- "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.",
8
- "Typical sizes: sticky 180×180, shape 160×110, frame 800×500, text 20px font. Leave ~40–80 units between items.",
9
- "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.",
10
- "Connectors attach to elements via from/to (ids, or refs defined earlier in the same create_elements call) and follow them when they move.",
11
- "Each create/update/delete call is one undo step for the user. Use get_screenshot to check the visual result.",
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 a PNG of what the user currently sees. Use focus_view first to frame the area you want to check.",
126
- inputSchema: { maxWidth: z.number().int().min(200).max(2400).optional().describe("Maximum image width in pixels (default 1280)") },
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 {