@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.
- package/README.md +273 -0
- package/dist/index.js +25663 -0
- 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.
|