@venturewild/workspace 0.6.1 → 0.6.2
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/LICENSE +21 -21
- package/README.md +112 -112
- package/package.json +85 -85
- package/server/bin/wild-workspace.mjs +1096 -1096
- package/server/src/account.mjs +114 -114
- package/server/src/agent-login.mjs +146 -146
- package/server/src/agent-readiness.mjs +200 -200
- package/server/src/agent.mjs +468 -468
- package/server/src/bazaar/core.mjs +974 -974
- package/server/src/bazaar/index.mjs +88 -88
- package/server/src/bazaar/mcp-server.mjs +429 -429
- package/server/src/bazaar/mock-tickup.mjs +97 -97
- package/server/src/bazaar/preview-server.mjs +95 -95
- package/server/src/bazaar/seed-recipes/customer-feedback-form/know-how.md +23 -23
- package/server/src/bazaar/seed-recipes/customer-feedback-form/recipe.json +24 -24
- package/server/src/bazaar/seed-recipes/landing-page-launch/know-how.md +29 -29
- package/server/src/bazaar/seed-recipes/landing-page-launch/recipe.json +25 -25
- package/server/src/bazaar/seed-recipes/personal-portfolio/know-how.md +21 -21
- package/server/src/bazaar/seed-recipes/personal-portfolio/recipe.json +24 -24
- package/server/src/bazaar/seed-recipes/receipt-sorter/know-how.md +31 -31
- package/server/src/bazaar/seed-recipes/receipt-sorter/recipe.json +25 -25
- package/server/src/bazaar/seed-recipes/tickup-hr-matching/know-how.md +79 -79
- package/server/src/bazaar/seed-recipes/tickup-hr-matching/recipe.json +40 -40
- package/server/src/canvas/core.mjs +446 -446
- package/server/src/canvas/index.mjs +42 -42
- package/server/src/canvas/mcp-server.mjs +253 -253
- package/server/src/canvas-rails.mjs +108 -108
- package/server/src/config.mjs +404 -404
- package/server/src/daemon-bin.mjs +110 -110
- package/server/src/daemon-supervisor.mjs +285 -285
- package/server/src/doctor.mjs +375 -375
- package/server/src/inbox.mjs +86 -86
- package/server/src/index.mjs +3332 -3332
- package/server/src/listings-rails.mjs +156 -156
- package/server/src/logpaths.mjs +98 -98
- package/server/src/observability.mjs +45 -45
- package/server/src/operator.mjs +92 -92
- package/server/src/pairing.mjs +137 -137
- package/server/src/service.mjs +515 -515
- package/server/src/session-reporter.mjs +201 -201
- package/server/src/settings.mjs +145 -145
- package/server/src/share.mjs +182 -182
- package/server/src/skills.mjs +213 -213
- package/server/src/supervisor.mjs +647 -647
- package/server/src/support-consent.mjs +133 -133
- package/server/src/sync.mjs +248 -248
- package/server/src/transcript.mjs +121 -121
- package/server/src/turn-mcp.mjs +46 -46
- package/server/src/usage.mjs +405 -405
- package/server/src/workspace-registry.mjs +295 -295
- package/server/src/workspaces.mjs +145 -145
- package/web/dist/assets/index-BgFan7ls.js +131 -0
- package/web/dist/assets/index-DHts78rO.css +32 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CzUrGoMW.css +0 -32
- package/web/dist/assets/index-ZYLNuQRa.js +0 -131
|
@@ -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 };
|