@venturewild/workspace 0.3.6 → 0.3.7

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 (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +112 -112
  3. package/package.json +83 -83
  4. package/server/bin/wild-workspace.mjs +995 -995
  5. package/server/src/account.mjs +114 -114
  6. package/server/src/agent-login.mjs +146 -146
  7. package/server/src/agent-readiness.mjs +200 -200
  8. package/server/src/agent.mjs +468 -453
  9. package/server/src/bazaar/core.mjs +579 -579
  10. package/server/src/bazaar/index.mjs +75 -75
  11. package/server/src/bazaar/mcp-server.mjs +328 -328
  12. package/server/src/bazaar/mock-tickup.mjs +97 -97
  13. package/server/src/bazaar/preview-server.mjs +95 -95
  14. package/server/src/bazaar/seed-recipes/customer-feedback-form/know-how.md +23 -23
  15. package/server/src/bazaar/seed-recipes/customer-feedback-form/recipe.json +24 -24
  16. package/server/src/bazaar/seed-recipes/landing-page-launch/know-how.md +29 -29
  17. package/server/src/bazaar/seed-recipes/landing-page-launch/recipe.json +25 -25
  18. package/server/src/bazaar/seed-recipes/personal-portfolio/know-how.md +21 -21
  19. package/server/src/bazaar/seed-recipes/personal-portfolio/recipe.json +24 -24
  20. package/server/src/bazaar/seed-recipes/receipt-sorter/know-how.md +31 -31
  21. package/server/src/bazaar/seed-recipes/receipt-sorter/recipe.json +25 -25
  22. package/server/src/bazaar/seed-recipes/tickup-hr-matching/know-how.md +79 -79
  23. package/server/src/bazaar/seed-recipes/tickup-hr-matching/recipe.json +32 -32
  24. package/server/src/canvas/core.mjs +421 -421
  25. package/server/src/canvas/index.mjs +42 -42
  26. package/server/src/canvas/mcp-server.mjs +253 -253
  27. package/server/src/config.mjs +404 -404
  28. package/server/src/daemon-bin.mjs +110 -110
  29. package/server/src/daemon-supervisor.mjs +285 -285
  30. package/server/src/doctor.mjs +375 -375
  31. package/server/src/inbox.mjs +86 -86
  32. package/server/src/index.mjs +2475 -2365
  33. package/server/src/logpaths.mjs +98 -98
  34. package/server/src/observability.mjs +45 -45
  35. package/server/src/operator.mjs +92 -92
  36. package/server/src/pairing.mjs +137 -137
  37. package/server/src/service.mjs +515 -515
  38. package/server/src/session-reporter.mjs +201 -201
  39. package/server/src/settings.mjs +145 -0
  40. package/server/src/share.mjs +182 -182
  41. package/server/src/skills.mjs +213 -0
  42. package/server/src/supervisor.mjs +647 -647
  43. package/server/src/support-consent.mjs +133 -133
  44. package/server/src/sync.mjs +248 -248
  45. package/server/src/transcript.mjs +121 -121
  46. package/server/src/turn-mcp.mjs +46 -46
  47. package/server/src/usage.mjs +405 -0
  48. package/web/dist/assets/index-BxRx8EsD.js +91 -0
  49. package/web/dist/assets/index-DoOPBr3s.css +1 -0
  50. package/web/dist/index.html +2 -2
  51. package/web/dist/assets/index-B7cOsWLt.js +0 -91
  52. package/web/dist/assets/index-Dl0VT5e6.css +0 -1
@@ -1,42 +1,42 @@
1
- // Canvas runtime glue for the main server: the agent-facing system prompt and the
2
- // path to the canvas MCP server (wired into the turn's --mcp-config by turn-mcp.mjs).
3
-
4
- import path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
-
9
- export const CANVAS_MCP_SERVER_PATH = path.join(__dirname, 'mcp-server.mjs');
10
-
11
- // Appended to the agent's system prompt on user chat turns. Sets the disposition:
12
- // the canvas is the user's space and the agent can build them a widget on request,
13
- // but it builds DATA (a declarative block), never code, and it touches the canvas
14
- // lightly (offer/confirm, don't spam). Plain language, no jargon.
15
- export const CANVAS_SYSTEM_PROMPT = [
16
- "The user's workspace is a *block canvas* — a home screen of widget-cards (like iPhone widgets or",
17
- "Notion blocks) they arrange themselves. You can BUILD them a custom block with the canvas tools",
18
- "(mcp__canvas__make_block, update_block, list_blocks).",
19
- "",
20
- "When the user asks to \"make/add a block\", wants a small dashboard or widget, or asks to \"show me",
21
- "<something>\" that's worth keeping in view (a count, a leaderboard, a status, a short note), build it:",
22
- "first work out the ACTUAL data yourself (read files or run commands as needed), then call make_block",
23
- "with the simplest kind that fits — metric (one headline number), list ({label,value} rows), table",
24
- "(columns + rows), or markdown (a short note). The block appears on their canvas right away; then say",
25
- "in one short line what you added.",
26
- "",
27
- "You are shipping DATA, not code — you pass values, and the canvas renders them. Don't try to write a",
28
- "component or HTML; pick a kind and fill its fields.",
29
- "",
30
- "To keep a block current, call update_block with its id and the changed fields (that's how a block",
31
- "stays \"live\" — you re-push when the numbers change). Use list_blocks to find an id when the user",
32
- "refers to a block you made earlier. Only make a block when it genuinely helps — don't clutter their",
33
- "canvas with blocks they didn't ask for.",
34
- "",
35
- "You can also RESTYLE the whole workspace with set_theme when the user asks to change the look",
36
- "(\"make it dark\", \"match my brand\", \"give me a calm sunset theme\", etc.). Choose a base mode",
37
- "(light|dark) and the token colours (bg, surface, text, border, and the three wallpaper stops",
38
- "canvas1/2/3) that complete the look — pass hex values like \"#1a0f14\". You're shipping COLOURS, not",
39
- "CSS; the change applies to their screen at once. You do NOT set the accent — that's the user's own",
40
- "signature colour, preserved across restyles — so describe the mode and background you applied, never",
41
- "an accent change. Call get_theme first if you're tweaking the current look. Keep good contrast.",
42
- ].join('\n');
1
+ // Canvas runtime glue for the main server: the agent-facing system prompt and the
2
+ // path to the canvas MCP server (wired into the turn's --mcp-config by turn-mcp.mjs).
3
+
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ export const CANVAS_MCP_SERVER_PATH = path.join(__dirname, 'mcp-server.mjs');
10
+
11
+ // Appended to the agent's system prompt on user chat turns. Sets the disposition:
12
+ // the canvas is the user's space and the agent can build them a widget on request,
13
+ // but it builds DATA (a declarative block), never code, and it touches the canvas
14
+ // lightly (offer/confirm, don't spam). Plain language, no jargon.
15
+ export const CANVAS_SYSTEM_PROMPT = [
16
+ "The user's workspace is a *block canvas* — a home screen of widget-cards (like iPhone widgets or",
17
+ "Notion blocks) they arrange themselves. You can BUILD them a custom block with the canvas tools",
18
+ "(mcp__canvas__make_block, update_block, list_blocks).",
19
+ "",
20
+ "When the user asks to \"make/add a block\", wants a small dashboard or widget, or asks to \"show me",
21
+ "<something>\" that's worth keeping in view (a count, a leaderboard, a status, a short note), build it:",
22
+ "first work out the ACTUAL data yourself (read files or run commands as needed), then call make_block",
23
+ "with the simplest kind that fits — metric (one headline number), list ({label,value} rows), table",
24
+ "(columns + rows), or markdown (a short note). The block appears on their canvas right away; then say",
25
+ "in one short line what you added.",
26
+ "",
27
+ "You are shipping DATA, not code — you pass values, and the canvas renders them. Don't try to write a",
28
+ "component or HTML; pick a kind and fill its fields.",
29
+ "",
30
+ "To keep a block current, call update_block with its id and the changed fields (that's how a block",
31
+ "stays \"live\" — you re-push when the numbers change). Use list_blocks to find an id when the user",
32
+ "refers to a block you made earlier. Only make a block when it genuinely helps — don't clutter their",
33
+ "canvas with blocks they didn't ask for.",
34
+ "",
35
+ "You can also RESTYLE the whole workspace with set_theme when the user asks to change the look",
36
+ "(\"make it dark\", \"match my brand\", \"give me a calm sunset theme\", etc.). Choose a base mode",
37
+ "(light|dark) and the token colours (bg, surface, text, border, and the three wallpaper stops",
38
+ "canvas1/2/3) that complete the look — pass hex values like \"#1a0f14\". You're shipping COLOURS, not",
39
+ "CSS; the change applies to their screen at once. You do NOT set the accent — that's the user's own",
40
+ "signature colour, preserved across restyles — so describe the mode and background you applied, never",
41
+ "an accent change. Call get_theme first if you're tweaking the current look. Keep good contrast.",
42
+ ].join('\n');
@@ -1,253 +1,253 @@
1
- // Canvas MCP server — the tools the agent uses to BUILD the user a block (§3.3).
2
- //
3
- // Same hand-rolled stdio JSON-RPC 2.0 shape as the bazaar MCP server (no new dep,
4
- // wrap-don't-embed). `claude` spawns this per turn via --mcp-config and exposes the
5
- // tools as mcp__canvas__<name>. It imports the SAME core.mjs the main server uses
6
- // (state under ~/.wild-workspace/canvas/), so there is one source of truth and no
7
- // HTTP/port handshake. Tool results are JSON envelopes the UI parses to mount the
8
- // block on the canvas.
9
- //
10
- // stdout carries ONLY JSON-RPC. Any diagnostics go to stderr.
11
-
12
- import readline from 'node:readline';
13
- import { createCanvas, KINDS } from './core.mjs';
14
-
15
- // The theme-token params set_theme accepts (each an optional hex color). Kept here
16
- // as schema; core.mjs is the validator (hex-only, allowlisted). "Data, not CSS."
17
- const THEME_TOKEN_PROPS = {
18
- bg: { type: 'string', description: 'page background, hex e.g. "#0a0c10".' },
19
- surface: { type: 'string', description: 'card / panel face, hex.' },
20
- text: { type: 'string', description: 'primary text, hex.' },
21
- textMuted: { type: 'string', description: 'secondary/muted text, hex.' },
22
- border: { type: 'string', description: 'hairline borders, hex.' },
23
- canvas1: { type: 'string', description: 'wallpaper gradient stop 1, hex.' },
24
- canvas2: { type: 'string', description: 'wallpaper gradient stop 2, hex.' },
25
- canvas3: { type: 'string', description: 'wallpaper gradient stop 3, hex.' },
26
- };
27
-
28
- const canvas = createCanvas(); // baseDir from WILD_WORKSPACE_GLOBAL_DIR (set by parent)
29
- const PROTOCOL_VERSION = '2025-06-18';
30
-
31
- function send(msg) {
32
- process.stdout.write(`${JSON.stringify(msg)}\n`);
33
- }
34
- function result(id, res) {
35
- send({ jsonrpc: '2.0', id, result: res });
36
- }
37
- function errorReply(id, code, message) {
38
- send({ jsonrpc: '2.0', id, error: { code, message } });
39
- }
40
- function textContent(obj) {
41
- return { content: [{ type: 'text', text: JSON.stringify(obj) }] };
42
- }
43
-
44
- // --- tool definitions -----------------------------------------------------
45
-
46
- // Shared description of the spec vocabulary, kept in one place so make/update agree.
47
- const SHAPE_HELP =
48
- "Pick the simplest `kind` that fits and fill only that kind's fields:\n" +
49
- "- metric: a single headline number. value (required, e.g. \"42\" or \"$1,240\"), " +
50
- "label (what it counts), delta (\"+12 today\"), trend (\"up\"|\"down\"|\"flat\"), " +
51
- "spark (array of recent numbers for a tiny chart).\n" +
52
- "- list: items — an array of { label, value } (value optional). Good for leaderboards/breakdowns.\n" +
53
- "- table: columns (array of headers) + rows (array of arrays of cells).\n" +
54
- "- markdown: markdown — a short rich-text note.\n" +
55
- "Compute the actual values yourself first (read files / run commands as needed), then pass them in. " +
56
- "You are shipping DATA, not code.";
57
-
58
- const TOOLS = [
59
- {
60
- name: 'make_block',
61
- description:
62
- "Build the user a custom block and pin it to their canvas. Use this when the user asks to " +
63
- "\"make/add a block\", wants a dashboard/widget, or asks to \"show me <something>\" that's worth " +
64
- "keeping in view. The block appears on their canvas immediately. " +
65
- SHAPE_HELP,
66
- inputSchema: {
67
- type: 'object',
68
- properties: {
69
- title: { type: 'string', description: 'Short title shown on the block header.' },
70
- kind: { type: 'string', enum: KINDS, description: 'The presentation primitive.' },
71
- icon: { type: 'string', description: 'A single emoji for the block (optional).' },
72
- note: { type: 'string', description: 'A one-line subtitle: what this shows (optional).' },
73
- value: { type: 'string', description: 'metric: the headline value.' },
74
- label: { type: 'string', description: 'metric: what the value counts.' },
75
- delta: { type: 'string', description: 'metric: a change indicator, e.g. "+12 today".' },
76
- trend: { type: 'string', enum: ['up', 'down', 'flat'], description: 'metric: colours the delta.' },
77
- spark: { type: 'array', items: { type: 'number' }, description: 'metric: recent numbers for a sparkline.' },
78
- items: {
79
- type: 'array',
80
- description: 'list: { label, value } entries.',
81
- items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } } },
82
- },
83
- columns: { type: 'array', items: { type: 'string' }, description: 'table: column headers.' },
84
- rows: { type: 'array', items: { type: 'array', items: { type: 'string' } }, description: 'table: rows of cells.' },
85
- markdown: { type: 'string', description: 'markdown: the rich-text body.' },
86
- },
87
- required: ['title', 'kind'],
88
- },
89
- },
90
- {
91
- name: 'update_block',
92
- description:
93
- "Refresh an existing custom block with new data (this is how a block stays \"live\" — you re-push " +
94
- "when the underlying numbers change). Pass the block id and only the fields that change. " +
95
- SHAPE_HELP,
96
- inputSchema: {
97
- type: 'object',
98
- properties: {
99
- id: { type: 'string', description: 'The block id returned by make_block.' },
100
- title: { type: 'string' },
101
- kind: { type: 'string', enum: KINDS },
102
- icon: { type: 'string' },
103
- note: { type: 'string' },
104
- value: { type: 'string' },
105
- label: { type: 'string' },
106
- delta: { type: 'string' },
107
- trend: { type: 'string', enum: ['up', 'down', 'flat'] },
108
- spark: { type: 'array', items: { type: 'number' } },
109
- items: { type: 'array', items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } } } },
110
- columns: { type: 'array', items: { type: 'string' } },
111
- rows: { type: 'array', items: { type: 'array', items: { type: 'string' } } },
112
- markdown: { type: 'string' },
113
- },
114
- required: ['id'],
115
- },
116
- },
117
- {
118
- name: 'list_blocks',
119
- description:
120
- "List the custom blocks currently on the user's canvas (id, title, kind) — use this to find the " +
121
- "id of a block the user refers to before calling update_block.",
122
- inputSchema: { type: 'object', properties: {} },
123
- },
124
- {
125
- name: 'set_theme',
126
- description:
127
- "Restyle the user's whole workspace (the \"make it mine\" surface). Use this when the user asks to " +
128
- "change the look — \"make it dark\", \"match my brand\", \"give me a sunset / forest / high-contrast " +
129
- "theme\", etc. Pick a `mode` (light|dark) as the base, then the token colours that complete the look — " +
130
- "the background, surfaces, text, and the three wallpaper stops. The change applies immediately. You " +
131
- "ship COLOURS (hex values), not CSS. NOTE: you do NOT set the accent colour — that is the user's own " +
132
- "signature colour (chosen in onboarding or the theme picker) and is preserved across every restyle. " +
133
- "Describe the mode and background/wallpaper you applied; do NOT claim to have changed their accent.",
134
- inputSchema: {
135
- type: 'object',
136
- properties: {
137
- mode: { type: 'string', enum: ['light', 'dark'], description: 'Base palette: light or dark.' },
138
- name: { type: 'string', description: 'A short name for the theme (optional, e.g. "Sunset").' },
139
- ...THEME_TOKEN_PROPS,
140
- },
141
- },
142
- },
143
- {
144
- name: 'get_theme',
145
- description: "Read the user's current theme (mode, accent, tokens) — use before tweaking it.",
146
- inputSchema: { type: 'object', properties: {} },
147
- },
148
- ];
149
-
150
- // --- tool dispatch --------------------------------------------------------
151
-
152
- function callTool(name, args = {}) {
153
- switch (name) {
154
- case 'make_block': {
155
- try {
156
- const block = canvas.addBlock(args);
157
- return textContent({
158
- kind: 'block',
159
- op: 'make',
160
- block,
161
- note: 'Block is now on the user\'s canvas. Tell them in one short line what you added.',
162
- });
163
- } catch (e) {
164
- return { ...textContent({ kind: 'error', error: e?.message || String(e) }), isError: true };
165
- }
166
- }
167
- case 'update_block': {
168
- const block = canvas.updateBlock(args.id, args);
169
- if (!block) return { ...textContent({ kind: 'error', error: `no block "${args.id}"` }), isError: true };
170
- return textContent({ kind: 'block', op: 'update', block, note: 'Block updated in place on the canvas.' });
171
- }
172
- case 'list_blocks': {
173
- const blocks = canvas.listBlocks().map((b) => ({ id: b.id, title: b.title, kind: b.kind }));
174
- return textContent({ kind: 'blocks', count: blocks.length, blocks });
175
- }
176
- case 'set_theme': {
177
- const theme = canvas.setTheme(args);
178
- return textContent({
179
- kind: 'theme',
180
- op: 'set',
181
- theme,
182
- note:
183
- "The user's workspace is now restyled (mode + background/wallpaper). Their accent colour is " +
184
- "preserved — it's their own. Tell them in one short line what look you applied; describe the " +
185
- "mode and background, NOT an accent colour.",
186
- });
187
- }
188
- case 'get_theme': {
189
- return textContent({ kind: 'theme', op: 'get', theme: canvas.getTheme() });
190
- }
191
- default:
192
- return { ...textContent({ kind: 'error', error: `unknown tool ${name}` }), isError: true };
193
- }
194
- }
195
-
196
- // --- JSON-RPC loop --------------------------------------------------------
197
-
198
- export function handleMessage(msg) {
199
- if (!msg || msg.jsonrpc !== '2.0') return;
200
- const { id, method, params } = msg;
201
- const isNotification = id === undefined || id === null;
202
-
203
- switch (method) {
204
- case 'initialize':
205
- return result(id, {
206
- protocolVersion: params?.protocolVersion || PROTOCOL_VERSION,
207
- capabilities: { tools: {} },
208
- serverInfo: { name: 'canvas', version: '1.0.0' },
209
- });
210
- case 'notifications/initialized':
211
- case 'initialized':
212
- return; // notification, no response
213
- case 'ping':
214
- return result(id, {});
215
- case 'tools/list':
216
- return result(id, { tools: TOOLS });
217
- case 'tools/call': {
218
- try {
219
- const out = callTool(params?.name, params?.arguments || {});
220
- return result(id, out);
221
- } catch (e) {
222
- return errorReply(id, -32603, `tool error: ${e?.message || e}`);
223
- }
224
- }
225
- default:
226
- if (!isNotification) errorReply(id, -32601, `method not found: ${method}`);
227
- return;
228
- }
229
- }
230
-
231
- // Only run the stdio loop when executed as the MCP process (not when imported by a test).
232
- const isDirectRun = process.argv[1] && process.argv[1].endsWith('mcp-server.mjs');
233
- if (isDirectRun) {
234
- const rl = readline.createInterface({ input: process.stdin });
235
- rl.on('line', (line) => {
236
- const trimmed = line.trim();
237
- if (!trimmed) return;
238
- let msg;
239
- try {
240
- msg = JSON.parse(trimmed);
241
- } catch {
242
- return; // ignore non-JSON noise
243
- }
244
- try {
245
- handleMessage(msg);
246
- } catch (e) {
247
- process.stderr.write(`canvas mcp error: ${e?.message || e}\n`);
248
- }
249
- });
250
- process.stderr.write('canvas mcp server ready\n');
251
- }
252
-
253
- export { TOOLS, callTool };
1
+ // Canvas MCP server — the tools the agent uses to BUILD the user a block (§3.3).
2
+ //
3
+ // Same hand-rolled stdio JSON-RPC 2.0 shape as the bazaar MCP server (no new dep,
4
+ // wrap-don't-embed). `claude` spawns this per turn via --mcp-config and exposes the
5
+ // tools as mcp__canvas__<name>. It imports the SAME core.mjs the main server uses
6
+ // (state under ~/.wild-workspace/canvas/), so there is one source of truth and no
7
+ // HTTP/port handshake. Tool results are JSON envelopes the UI parses to mount the
8
+ // block on the canvas.
9
+ //
10
+ // stdout carries ONLY JSON-RPC. Any diagnostics go to stderr.
11
+
12
+ import readline from 'node:readline';
13
+ import { createCanvas, KINDS } from './core.mjs';
14
+
15
+ // The theme-token params set_theme accepts (each an optional hex color). Kept here
16
+ // as schema; core.mjs is the validator (hex-only, allowlisted). "Data, not CSS."
17
+ const THEME_TOKEN_PROPS = {
18
+ bg: { type: 'string', description: 'page background, hex e.g. "#0a0c10".' },
19
+ surface: { type: 'string', description: 'card / panel face, hex.' },
20
+ text: { type: 'string', description: 'primary text, hex.' },
21
+ textMuted: { type: 'string', description: 'secondary/muted text, hex.' },
22
+ border: { type: 'string', description: 'hairline borders, hex.' },
23
+ canvas1: { type: 'string', description: 'wallpaper gradient stop 1, hex.' },
24
+ canvas2: { type: 'string', description: 'wallpaper gradient stop 2, hex.' },
25
+ canvas3: { type: 'string', description: 'wallpaper gradient stop 3, hex.' },
26
+ };
27
+
28
+ const canvas = createCanvas(); // baseDir from WILD_WORKSPACE_GLOBAL_DIR (set by parent)
29
+ const PROTOCOL_VERSION = '2025-06-18';
30
+
31
+ function send(msg) {
32
+ process.stdout.write(`${JSON.stringify(msg)}\n`);
33
+ }
34
+ function result(id, res) {
35
+ send({ jsonrpc: '2.0', id, result: res });
36
+ }
37
+ function errorReply(id, code, message) {
38
+ send({ jsonrpc: '2.0', id, error: { code, message } });
39
+ }
40
+ function textContent(obj) {
41
+ return { content: [{ type: 'text', text: JSON.stringify(obj) }] };
42
+ }
43
+
44
+ // --- tool definitions -----------------------------------------------------
45
+
46
+ // Shared description of the spec vocabulary, kept in one place so make/update agree.
47
+ const SHAPE_HELP =
48
+ "Pick the simplest `kind` that fits and fill only that kind's fields:\n" +
49
+ "- metric: a single headline number. value (required, e.g. \"42\" or \"$1,240\"), " +
50
+ "label (what it counts), delta (\"+12 today\"), trend (\"up\"|\"down\"|\"flat\"), " +
51
+ "spark (array of recent numbers for a tiny chart).\n" +
52
+ "- list: items — an array of { label, value } (value optional). Good for leaderboards/breakdowns.\n" +
53
+ "- table: columns (array of headers) + rows (array of arrays of cells).\n" +
54
+ "- markdown: markdown — a short rich-text note.\n" +
55
+ "Compute the actual values yourself first (read files / run commands as needed), then pass them in. " +
56
+ "You are shipping DATA, not code.";
57
+
58
+ const TOOLS = [
59
+ {
60
+ name: 'make_block',
61
+ description:
62
+ "Build the user a custom block and pin it to their canvas. Use this when the user asks to " +
63
+ "\"make/add a block\", wants a dashboard/widget, or asks to \"show me <something>\" that's worth " +
64
+ "keeping in view. The block appears on their canvas immediately. " +
65
+ SHAPE_HELP,
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ title: { type: 'string', description: 'Short title shown on the block header.' },
70
+ kind: { type: 'string', enum: KINDS, description: 'The presentation primitive.' },
71
+ icon: { type: 'string', description: 'A single emoji for the block (optional).' },
72
+ note: { type: 'string', description: 'A one-line subtitle: what this shows (optional).' },
73
+ value: { type: 'string', description: 'metric: the headline value.' },
74
+ label: { type: 'string', description: 'metric: what the value counts.' },
75
+ delta: { type: 'string', description: 'metric: a change indicator, e.g. "+12 today".' },
76
+ trend: { type: 'string', enum: ['up', 'down', 'flat'], description: 'metric: colours the delta.' },
77
+ spark: { type: 'array', items: { type: 'number' }, description: 'metric: recent numbers for a sparkline.' },
78
+ items: {
79
+ type: 'array',
80
+ description: 'list: { label, value } entries.',
81
+ items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } } },
82
+ },
83
+ columns: { type: 'array', items: { type: 'string' }, description: 'table: column headers.' },
84
+ rows: { type: 'array', items: { type: 'array', items: { type: 'string' } }, description: 'table: rows of cells.' },
85
+ markdown: { type: 'string', description: 'markdown: the rich-text body.' },
86
+ },
87
+ required: ['title', 'kind'],
88
+ },
89
+ },
90
+ {
91
+ name: 'update_block',
92
+ description:
93
+ "Refresh an existing custom block with new data (this is how a block stays \"live\" — you re-push " +
94
+ "when the underlying numbers change). Pass the block id and only the fields that change. " +
95
+ SHAPE_HELP,
96
+ inputSchema: {
97
+ type: 'object',
98
+ properties: {
99
+ id: { type: 'string', description: 'The block id returned by make_block.' },
100
+ title: { type: 'string' },
101
+ kind: { type: 'string', enum: KINDS },
102
+ icon: { type: 'string' },
103
+ note: { type: 'string' },
104
+ value: { type: 'string' },
105
+ label: { type: 'string' },
106
+ delta: { type: 'string' },
107
+ trend: { type: 'string', enum: ['up', 'down', 'flat'] },
108
+ spark: { type: 'array', items: { type: 'number' } },
109
+ items: { type: 'array', items: { type: 'object', properties: { label: { type: 'string' }, value: { type: 'string' } } } },
110
+ columns: { type: 'array', items: { type: 'string' } },
111
+ rows: { type: 'array', items: { type: 'array', items: { type: 'string' } } },
112
+ markdown: { type: 'string' },
113
+ },
114
+ required: ['id'],
115
+ },
116
+ },
117
+ {
118
+ name: 'list_blocks',
119
+ description:
120
+ "List the custom blocks currently on the user's canvas (id, title, kind) — use this to find the " +
121
+ "id of a block the user refers to before calling update_block.",
122
+ inputSchema: { type: 'object', properties: {} },
123
+ },
124
+ {
125
+ name: 'set_theme',
126
+ description:
127
+ "Restyle the user's whole workspace (the \"make it mine\" surface). Use this when the user asks to " +
128
+ "change the look — \"make it dark\", \"match my brand\", \"give me a sunset / forest / high-contrast " +
129
+ "theme\", etc. Pick a `mode` (light|dark) as the base, then the token colours that complete the look — " +
130
+ "the background, surfaces, text, and the three wallpaper stops. The change applies immediately. You " +
131
+ "ship COLOURS (hex values), not CSS. NOTE: you do NOT set the accent colour — that is the user's own " +
132
+ "signature colour (chosen in onboarding or the theme picker) and is preserved across every restyle. " +
133
+ "Describe the mode and background/wallpaper you applied; do NOT claim to have changed their accent.",
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ mode: { type: 'string', enum: ['light', 'dark'], description: 'Base palette: light or dark.' },
138
+ name: { type: 'string', description: 'A short name for the theme (optional, e.g. "Sunset").' },
139
+ ...THEME_TOKEN_PROPS,
140
+ },
141
+ },
142
+ },
143
+ {
144
+ name: 'get_theme',
145
+ description: "Read the user's current theme (mode, accent, tokens) — use before tweaking it.",
146
+ inputSchema: { type: 'object', properties: {} },
147
+ },
148
+ ];
149
+
150
+ // --- tool dispatch --------------------------------------------------------
151
+
152
+ function callTool(name, args = {}) {
153
+ switch (name) {
154
+ case 'make_block': {
155
+ try {
156
+ const block = canvas.addBlock(args);
157
+ return textContent({
158
+ kind: 'block',
159
+ op: 'make',
160
+ block,
161
+ note: 'Block is now on the user\'s canvas. Tell them in one short line what you added.',
162
+ });
163
+ } catch (e) {
164
+ return { ...textContent({ kind: 'error', error: e?.message || String(e) }), isError: true };
165
+ }
166
+ }
167
+ case 'update_block': {
168
+ const block = canvas.updateBlock(args.id, args);
169
+ if (!block) return { ...textContent({ kind: 'error', error: `no block "${args.id}"` }), isError: true };
170
+ return textContent({ kind: 'block', op: 'update', block, note: 'Block updated in place on the canvas.' });
171
+ }
172
+ case 'list_blocks': {
173
+ const blocks = canvas.listBlocks().map((b) => ({ id: b.id, title: b.title, kind: b.kind }));
174
+ return textContent({ kind: 'blocks', count: blocks.length, blocks });
175
+ }
176
+ case 'set_theme': {
177
+ const theme = canvas.setTheme(args);
178
+ return textContent({
179
+ kind: 'theme',
180
+ op: 'set',
181
+ theme,
182
+ note:
183
+ "The user's workspace is now restyled (mode + background/wallpaper). Their accent colour is " +
184
+ "preserved — it's their own. Tell them in one short line what look you applied; describe the " +
185
+ "mode and background, NOT an accent colour.",
186
+ });
187
+ }
188
+ case 'get_theme': {
189
+ return textContent({ kind: 'theme', op: 'get', theme: canvas.getTheme() });
190
+ }
191
+ default:
192
+ return { ...textContent({ kind: 'error', error: `unknown tool ${name}` }), isError: true };
193
+ }
194
+ }
195
+
196
+ // --- JSON-RPC loop --------------------------------------------------------
197
+
198
+ export function handleMessage(msg) {
199
+ if (!msg || msg.jsonrpc !== '2.0') return;
200
+ const { id, method, params } = msg;
201
+ const isNotification = id === undefined || id === null;
202
+
203
+ switch (method) {
204
+ case 'initialize':
205
+ return result(id, {
206
+ protocolVersion: params?.protocolVersion || PROTOCOL_VERSION,
207
+ capabilities: { tools: {} },
208
+ serverInfo: { name: 'canvas', version: '1.0.0' },
209
+ });
210
+ case 'notifications/initialized':
211
+ case 'initialized':
212
+ return; // notification, no response
213
+ case 'ping':
214
+ return result(id, {});
215
+ case 'tools/list':
216
+ return result(id, { tools: TOOLS });
217
+ case 'tools/call': {
218
+ try {
219
+ const out = callTool(params?.name, params?.arguments || {});
220
+ return result(id, out);
221
+ } catch (e) {
222
+ return errorReply(id, -32603, `tool error: ${e?.message || e}`);
223
+ }
224
+ }
225
+ default:
226
+ if (!isNotification) errorReply(id, -32601, `method not found: ${method}`);
227
+ return;
228
+ }
229
+ }
230
+
231
+ // Only run the stdio loop when executed as the MCP process (not when imported by a test).
232
+ const isDirectRun = process.argv[1] && process.argv[1].endsWith('mcp-server.mjs');
233
+ if (isDirectRun) {
234
+ const rl = readline.createInterface({ input: process.stdin });
235
+ rl.on('line', (line) => {
236
+ const trimmed = line.trim();
237
+ if (!trimmed) return;
238
+ let msg;
239
+ try {
240
+ msg = JSON.parse(trimmed);
241
+ } catch {
242
+ return; // ignore non-JSON noise
243
+ }
244
+ try {
245
+ handleMessage(msg);
246
+ } catch (e) {
247
+ process.stderr.write(`canvas mcp error: ${e?.message || e}\n`);
248
+ }
249
+ });
250
+ process.stderr.write('canvas mcp server ready\n');
251
+ }
252
+
253
+ export { TOOLS, callTool };