infiniteboard-mcp 0.1.0 → 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.
Files changed (3) hide show
  1. package/package.json +2 -1
  2. package/server.mjs +4 -135
  3. package/tools.mjs +138 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infiniteboard-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.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 { z } from "zod";
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
- const asText = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 1) }] });
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.2.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,138 @@
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).",
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.",
12
+ ].join(" ");
13
+
14
+ const asText = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 1) }] });
15
+ const asError = (e) => ({ isError: true, content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }] });
16
+ const relayVia = (call) => (method) => async (params) => {
17
+ try {
18
+ return asText(await call(method, params));
19
+ } catch (e) {
20
+ return asError(e);
21
+ }
22
+ };
23
+
24
+ /**
25
+ * @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
26
+ * @param {(method: string, params?: object) => Promise<any>} call
27
+ */
28
+ export function registerBoardTools(server, call) {
29
+ const relay = relayVia(call);
30
+ const color = z.string().describe("Hex colour (#rrggbb) or a name: yellow, orange, pink, purple, blue, green, lime, gray, white, black, red, darkblue, darkgreen");
31
+ const point = z.tuple([z.number(), z.number()]);
32
+ const common = {
33
+ x: z.number().optional().describe("Left edge (world units). Omit x and y to auto-place in free space"),
34
+ y: z.number().optional().describe("Top edge (world units)"),
35
+ w: z.number().positive().optional(),
36
+ h: z.number().positive().optional(),
37
+ text: z.string().optional().describe("Text for sticky, shape and text elements (plain text, \\n for new lines)"),
38
+ title: z.string().optional().describe("Frame title"),
39
+ color: color.optional().describe("Sticky/frame: background. Text/shape: text colour. Connector/stroke: line colour"),
40
+ fill: z.string().optional().describe('Shape fill colour, or "none"'),
41
+ stroke: color.optional().describe("Shape border colour"),
42
+ kind: z.enum(["rect", "roundrect", "ellipse", "diamond", "triangle"]).optional().describe("Shape kind (default rect)"),
43
+ fontSize: z.number().positive().optional(),
44
+ align: z.enum(["left", "center", "right"]).optional(),
45
+ bold: z.boolean().optional(),
46
+ angle: z.number().optional().describe("Rotation in degrees, clockwise"),
47
+ from: z.string().optional().describe("Connector start: element id or ref"),
48
+ to: z.string().optional().describe("Connector end: element id or ref"),
49
+ fromPoint: point.optional().describe("Connector start as [x, y] when not attached"),
50
+ toPoint: point.optional().describe("Connector end as [x, y] when not attached"),
51
+ route: z.enum(["straight", "elbow", "curved"]).optional(),
52
+ label: z.string().optional().describe("Connector label"),
53
+ endHead: z.enum(["none", "open", "filled"]).optional(),
54
+ startHead: z.enum(["none", "open", "filled"]).optional(),
55
+ dash: z.enum(["solid", "dashed", "dotted"]).optional(),
56
+ };
57
+
58
+ server.registerTool("get_board", {
59
+ 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.",
61
+ inputSchema: {
62
+ type: z.enum(["sticky", "shape", "text", "frame", "connector", "stroke", "image"]).optional().describe("Only list this element type"),
63
+ limit: z.number().int().positive().max(5000).optional().describe("Maximum elements to return (default 500)"),
64
+ },
65
+ annotations: { readOnlyHint: true },
66
+ }, relay("get_board"));
67
+
68
+ server.registerTool("create_elements", {
69
+ title: "Create elements",
70
+ description:
71
+ "Creates sticky notes, shapes, text, frames, connectors and pen strokes in one undoable step. " +
72
+ "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'}]. " +
73
+ "Elements placed at least half inside a frame automatically belong to it. Returns the new ids.",
74
+ inputSchema: {
75
+ elements: z.array(z.object({
76
+ type: z.enum(["sticky", "shape", "text", "frame", "connector", "stroke"]),
77
+ ref: z.string().optional().describe("Your own key for referring to this element later in the same call"),
78
+ ...common,
79
+ points: z.array(point).optional().describe("Stroke points [[x, y], …] in world units"),
80
+ width: z.number().positive().optional().describe("Stroke width"),
81
+ })).min(1).max(500),
82
+ },
83
+ }, relay("create_elements"));
84
+
85
+ server.registerTool("update_elements", {
86
+ title: "Update elements",
87
+ 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.",
88
+ inputSchema: {
89
+ updates: z.array(z.object({ id: z.string(), ...common, locked: z.boolean().optional() })).min(1).max(1000),
90
+ },
91
+ }, relay("update_elements"));
92
+
93
+ server.registerTool("delete_elements", {
94
+ title: "Delete elements",
95
+ description: "Deletes elements by id (deleting a frame deletes its contents). Pass all: true to clear the board. One undoable step.",
96
+ inputSchema: {
97
+ ids: z.array(z.string()).optional(),
98
+ all: z.boolean().optional().describe("Delete everything on the board"),
99
+ },
100
+ annotations: { destructiveHint: true },
101
+ }, relay("delete_elements"));
102
+
103
+ server.registerTool("focus_view", {
104
+ title: "Move the view",
105
+ 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.",
106
+ inputSchema: {
107
+ ids: z.array(z.string()).optional(),
108
+ x: z.number().optional(),
109
+ y: z.number().optional(),
110
+ zoom: z.number().positive().max(8).optional().describe("1 = 100%"),
111
+ },
112
+ }, relay("focus_view"));
113
+
114
+ server.registerTool("select_elements", {
115
+ title: "Select elements",
116
+ description: "Highlights elements for the user by selecting them (empty list clears the selection).",
117
+ inputSchema: { ids: z.array(z.string()) },
118
+ }, relay("select"));
119
+
120
+ server.registerTool("undo", { title: "Undo", description: "Undoes the last change on the board (the user's or the assistant's).", inputSchema: {} }, relay("undo"));
121
+ server.registerTool("redo", { title: "Redo", description: "Redoes the last undone change.", inputSchema: {} }, relay("redo"));
122
+
123
+ server.registerTool("get_screenshot", {
124
+ 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)") },
127
+ annotations: { readOnlyHint: true },
128
+ }, async (params) => {
129
+ try {
130
+ const { image } = await call("screenshot", params);
131
+ const m = /^data:(image\/\w+);base64,(.*)$/s.exec(image ?? "");
132
+ if (!m) throw new Error("The board returned no image");
133
+ return { content: [{ type: "image", mimeType: m[1], data: m[2] }] };
134
+ } catch (e) {
135
+ return asError(e);
136
+ }
137
+ });
138
+ }