@dadaki/mcp 1.0.0-beta.1

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/README.md +273 -0
  2. package/dist/index.js +25663 -0
  3. package/package.json +56 -0
package/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # @dadaki/mcp
2
+
3
+ An MCP server that lets an agent author real vector artwork in Dadaki — as
4
+ vector geometry, not a generated raster image.
5
+
6
+ ## Why it works this way
7
+
8
+ The obvious reading of "let an agent use a vector editor like a human" is to
9
+ give it a mouse: screenshot, click the rectangle tool, drag. That is the wrong
10
+ implementation. It's slow, it breaks on every UI change, and agents are poor at
11
+ pixel-precise dragging.
12
+
13
+ What actually carries over from a human at a drawing tool is the **feedback
14
+ loop** — place something, look at it, correct it. So the tools here are verbs at
15
+ the level of *intent* (`create_rect`, `align`, `boolean`), and they're paired
16
+ with `describe_scene` and `render_png_image` so the agent can see what it made.
17
+ Every mutating tool also returns the affected node, so placement can be checked
18
+ without a round-trip.
19
+
20
+ ## Architecture
21
+
22
+ The editor is a browser application: CanvasKit, the WASM engine and the whole
23
+ tool layer assume a DOM and a GPU canvas. Rather than maintain a second,
24
+ inevitably-divergent Node implementation, the server drives the **real** editor.
25
+ Agent edits therefore go through the same engine, history and export paths as a
26
+ human's.
27
+
28
+ That browser is always **yours** — a tab you have open. The server launches no
29
+ browser of its own, which is why installing it downloads no browser either: it
30
+ is a few hundred kilobytes of JavaScript and nothing else.
31
+
32
+ The agent API (`EditorHandle.agent`) is identical in every editor instance. What
33
+ differs between modes is only how a call gets into the page, which is isolated
34
+ behind one `EditorTransport` interface — so **every tool works in every mode**,
35
+ and nothing in the tool layer knows which is in use.
36
+
37
+ ```
38
+ ┌── ws ────▶ your editor tab, on localhost
39
+ agent ──stdio──▶ MCP server┤
40
+ └── HTTPS ─▶ app backend ──SSE──▶ your tab, hosted
41
+ ```
42
+
43
+ Rendering is not special-cased per mode. `render_png` is an ordinary call to
44
+ `agent.toPNG()`, which CanvasKit services inside the page through the editor's
45
+ own export path — so a render produces the same pixels in every mode, with no
46
+ editor chrome, at whatever scale is asked for.
47
+
48
+ ## Modes
49
+
50
+ | Mode | What it does | Use it for |
51
+ | --- | --- | --- |
52
+ | `relay` | Drives your tab in the **hosted app** | dadaki.com — the default |
53
+ | `bridge` | Drives your tab on **localhost** | Local dev against `pnpm dev` |
54
+ | `--url <addr>` | Either of the above, pointed elsewhere | Dev server, staging, another deployment |
55
+
56
+ **Why two "your tab" modes.** `bridge` has the page dial `ws://127.0.0.1`
57
+ directly, which only works when the page itself is served from localhost. From
58
+ a public origin Chrome refuses the connection outright — Local Network Access
59
+ checks, `ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS` — so the hosted app needs
60
+ `relay`, where both sides connect outward and the backend pairs them.
61
+
62
+ **There used to be two more modes**, `headless` and `headful`, in which the
63
+ server launched a browser through puppeteer and served the local build to it.
64
+ They are gone. They cost every install a Chrome download, they only ever worked
65
+ from a repo checkout, and the document they produced lived in a window nobody
66
+ could see — lost on the next server restart, which MCP clients do routinely.
67
+ Both flags now fail with a message pointing at `relay` or `bridge`.
68
+
69
+ ### Setup
70
+
71
+ Nothing to clone, nothing to build:
72
+
73
+ ```bash
74
+ claude mcp add dadaki -- npx -y @dadaki/mcp@latest --mode relay --url https://dadaki.com/
75
+ ```
76
+
77
+ Any other client, same command in its own config:
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "dadaki": {
83
+ "command": "npx",
84
+ "args": ["-y", "@dadaki/mcp@latest", "--mode", "relay", "--url", "https://dadaki.com/"]
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ `relay` against `https://dadaki.com` is also what you get with no arguments at
91
+ all. Add `"--mode", "bridge"` — or `"env": {"DADAKI_MCP_MODE": "bridge"}` — to
92
+ drive an editor running on your own machine instead.
93
+
94
+ Then open a document, click **Connect agent**, and give your agent the
95
+ 8-character code. It calls the `connect` tool with it and is attached.
96
+
97
+ #### From a checkout
98
+
99
+ Working on the server itself, run it from source:
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "dadaki": {
105
+ "command": "node",
106
+ "args": ["--experimental-strip-types", "/path/to/dadaki/packages/mcp/src/index.ts"]
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ `pnpm --filter @dadaki/mcp build` produces `dist/index.js`, the single file the
113
+ npm package ships; `npm publish` from `packages/mcp` runs it first.
114
+
115
+ ### Relay mode (the hosted app)
116
+
117
+ ```bash
118
+ npx -y @dadaki/mcp --mode relay --url https://your-app/
119
+ ```
120
+
121
+ Then, in the app: open a document, click **Connect agent**, and give the agent
122
+ the 8-character code. The agent calls the `connect` tool with it and is attached
123
+ — no URL, no token, nothing to paste into a config. Codes are single-use and
124
+ expire in ten minutes.
125
+
126
+ `?agentBridge=cloud&token=…` still works for scripted setups, but nothing needs
127
+ it any more. From either route it behaves exactly like bridge mode — same tools,
128
+ same badge, same one-call-one-undo — except the calls travel through the app's
129
+ backend (`/api/functions/agent-bridge`) instead of a loopback socket.
130
+
131
+ If a tool reports that no editor is attached, that is the message to act on: it
132
+ names the button and the code, and it comes back in a few seconds rather than
133
+ blocking until the client's own timeout fires.
134
+
135
+ Sessions live in the backend process's memory, so a horizontally scaled
136
+ deployment would need sticky routing or a shared store.
137
+
138
+ ### Bridge mode
139
+
140
+ It listens on loopback and prints a URL:
141
+
142
+ ```
143
+ [dadaki-mcp] bridge mode — listening on 127.0.0.1:54666
144
+ [dadaki-mcp] open your editor with this URL to attach it:
145
+
146
+ http://localhost:5199/?agentBridge=54666&token=…
147
+ ```
148
+
149
+ Open that once and the tab attaches. The credentials are stripped from the
150
+ address bar (so the token doesn't linger in history or get pasted into a shared
151
+ link) and remembered, so reloads stay attached — call
152
+ `clearBridgeCredentials()` to stop.
153
+
154
+ **The URL is stable, so "once" really means once.** The port is fixed (7331) and
155
+ the token is persisted to `~/.dadaki/agent-bridge.json` (mode 0600, outside any
156
+ repo so it is never committed). A token minted per run would reject the attached
157
+ tab on every server restart, which would mean re-pasting a URL constantly and
158
+ make the mode unusable. If port 7331 is taken the server falls back to a free
159
+ one and says so — that run's URL is the one to use.
160
+
161
+ You keep working in the same window while the agent does. Its edits are ordinary
162
+ edits: same undo history, same granularity, so you can undo its work, correct
163
+ it, or take over mid-drawing.
164
+
165
+ The channel is a remote control into your document, so it is deliberately
166
+ narrow: loopback only, one editor at a time (a second is refused, so a call is
167
+ never ambiguous about which document it hit), token-gated with a timing-safe
168
+ comparison, and able to invoke only functions that exist on the agent API — it
169
+ cannot evaluate arbitrary code in your page.
170
+
171
+ ### A correction worth keeping
172
+
173
+ An earlier version of this file claimed bridge mode worked against the deployed
174
+ app, "verified against a real HTTPS origin." It did not, and the verification
175
+ was invalid: the test served its HTTPS page from `https://127.0.0.1`, so it was
176
+ a *loopback* origin talking to loopback — which is permitted. A public origin
177
+ talking to loopback is not, and fails with
178
+ `ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS`.
179
+
180
+ That is why `relay` exists. `smoke_relay.mjs` runs against the real deployment
181
+ (`RELAY_FRONTEND` / `RELAY_BACKEND`), and asserts the absence of those errors
182
+ specifically — a test that would have caught the original mistake.
183
+
184
+ ## Tools
185
+
186
+ | Group | Tools |
187
+ | --- | --- |
188
+ | Seeing | `describe_scene`, `render_png`, `render_png_image`, `export_svg` |
189
+ | Creating | `create_rect`, `create_ellipse`, `create_polygon`, `create_star`, `create_path`, `create_path_data`, `create_text`, `import_svg` |
190
+ | Styling | `set_fill`, `set_gradient`, `set_stroke`, `set_opacity`, `set_corner_radius`, `set_text` |
191
+ | Arranging | `move`, `set_position`, `resize`, `rotate`, `align`, `distribute`, `bring_to_front`, `send_to_back` |
192
+ | Canvas | `set_canvas`, `fit_canvas_to_artwork` |
193
+ | Structuring | `group`, `ungroup`, `duplicate`, `delete`, `clear`, `rename`, `boolean` |
194
+ | Session | `undo`, `redo` |
195
+
196
+ Coordinates are world units with y growing downward. Colours are CSS hex.
197
+
198
+ Three are worth calling out:
199
+
200
+ - **`import_svg`** is usually the fastest route to complex artwork — compose the
201
+ drawing as SVG markup, import it, then refine with the other verbs. Gradients,
202
+ groups and transforms survive.
203
+ - **`create_path_data`** takes an SVG `d` attribute. Agents are far more fluent
204
+ in path data than in point arrays, and arcs can't be expressed any other way.
205
+ - **`align` / `distribute`** are exact. Computing even spacing by hand is
206
+ precisely what agents get subtly wrong.
207
+
208
+ ## Design notes
209
+
210
+ Several behaviours differ from the editor's internal defaults. The rule behind
211
+ all of them: **an agent cannot notice what a human would.** A human sees a stray
212
+ outline and deletes it; an agent ships it.
213
+
214
+ - **Strokes are opt-in at creation.** The engine's default node style carries a
215
+ black 2px stroke, so "a yellow circle" would arrive with an unintended black
216
+ outline.
217
+ - **Text defaults to black, not white.** The engine defaults text to a white
218
+ fill, which is invisible on the default white artboard — the node describes
219
+ perfectly and draws nothing, which is undiagnosable from the agent's side.
220
+ - **Renders frame the artboard, not the editor.** Rulers, grid and the artboard
221
+ label cost resolution and are indistinguishable from artwork in a screenshot.
222
+ The view is fitted first so the drawing fills the image.
223
+ - **Renders clear the selection first.** Selection handles read as a stray
224
+ outlined rectangle.
225
+ - **Gradients are given as an angle**, resolved against each node's own local
226
+ box. That box is not uniform — a Rect spans `0..w` from a top-left origin, an
227
+ Ellipse spans `-r..r` about its centre — and getting it wrong yields a shape
228
+ that reports as a gradient fill but renders as flat colour.
229
+
230
+ One agent call is exactly one undo step (everything routes through
231
+ `WasmScene.transaction()`), so a human can step back through an agent's work at
232
+ the same granularity as their own. That invariant is pinned in
233
+ `packages/editor/src/agent.test.ts`.
234
+
235
+ ## A note on testing this
236
+
237
+ Every one of the defaults above exists because of a bug found by **rendering the
238
+ output and looking at it**, not by a failing assertion. The gradient bug is the
239
+ sharpest example: the test asserted `fillType === 'gradient'` and passed, while
240
+ the shape rendered as flat blue.
241
+
242
+ If you extend this, assert on the thing that makes the artwork correct —
243
+ gradient endpoints, path bounds, resolved colours — not on metadata that would
244
+ survive the feature being broken.
245
+
246
+ ## Testing
247
+
248
+ ```bash
249
+ pnpm test # agent API unit tests
250
+ pnpm --filter @dadaki/mcp smoke # full MCP round-trip, every tool
251
+ pnpm --filter @dadaki/mcp smoke:bridge # bridge mode, incl. its security rules
252
+ pnpm --filter @dadaki/mcp smoke:modes <dir> # every mode; <dir> holds cert.pem/key.pem
253
+ ```
254
+
255
+ The unit tests cover the agent API against the real WASM engine. The smoke tests
256
+ cover what only exists assembled: the MCP handshake, each transport, CanvasKit
257
+ booting, and rendering.
258
+
259
+ Since the server drives a tab a *person* has open, the tests have to supply the
260
+ person: `harness.ts` serves the built app, opens it with puppeteer, and attaches
261
+ it over the bridge. That harness is the only thing here that touches puppeteer,
262
+ it lives outside `src/`, and it is never published — which is what keeps a
263
+ browser download out of `npx @dadaki/mcp`.
264
+
265
+ `smoke:bridge` is the one worth reading. Bridge mode's correctness claim is that
266
+ a call lands in *somebody else's* page, so the test opens an editor itself,
267
+ attaches it, drives it over MCP, and then reads that page back **directly** —
268
+ not through MCP — to prove the edit really landed there. It also checks the
269
+ security rules hold: a second editor is refused, and a bad token gets nothing.
270
+
271
+ `smoke:modes` needs a self-signed cert to test the HTTPS case; generate one with
272
+ `openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 2 -nodes
273
+ -subj /CN=localhost`. Without it that one check is skipped, not failed.