@dadaki/mcp 1.0.0-beta.1 → 1.0.0-beta.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dadaki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -248,10 +248,25 @@ survive the feature being broken.
248
248
  ```bash
249
249
  pnpm test # agent API unit tests
250
250
  pnpm --filter @dadaki/mcp smoke # full MCP round-trip, every tool
251
+ pnpm --filter @dadaki/mcp smoke:bundle # the same, against the published bundle
251
252
  pnpm --filter @dadaki/mcp smoke:bridge # bridge mode, incl. its security rules
252
253
  pnpm --filter @dadaki/mcp smoke:modes <dir> # every mode; <dir> holds cert.pem/key.pem
254
+ pnpm --filter @dadaki/mcp smoke:offline # relay economics, against a stand-in backend
255
+ pnpm --filter @dadaki/mcp smoke:relay # relay against a real deployment
253
256
  ```
254
257
 
258
+ **`smoke:bundle` matters more than it looks.** What npm ships is not these
259
+ sources — it is one esbuild output with every dependency inlined and a
260
+ `createRequire` shim. A test that only runs `src/` cannot see a bundling
261
+ failure, and a bundling failure reaches users as a broken install. Every smoke
262
+ test takes `DADAKI_MCP_TEST_BUNDLE=1` and runs against `dist/index.js` instead.
263
+
264
+ **`smoke:offline` counts requests.** Against a real deployment you cannot tell
265
+ whether a tool call cost one HTTP round-trip or two, and that difference is the
266
+ whole latency of a drawing loop. A stand-in backend can also refuse a pairing
267
+ code or vanish entirely on demand, which is how the failure messages get tested
268
+ at all.
269
+
255
270
  The unit tests cover the agent API against the real WASM engine. The smoke tests
256
271
  cover what only exists assembled: the MCP handshake, each transport, CanvasKit
257
272
  booting, and rendering.
package/dist/index.js CHANGED
@@ -14501,7 +14501,7 @@ function cleanParams(params, data) {
14501
14501
  const p2 = typeof p === "string" ? { message: p } : p;
14502
14502
  return p2;
14503
14503
  }
14504
- function custom(check2, _params = {}, fatal) {
14504
+ function custom(check2, _params = {}, fatal2) {
14505
14505
  if (check2)
14506
14506
  return ZodAny.create().superRefine((data, ctx) => {
14507
14507
  const r = check2(data);
@@ -14509,14 +14509,14 @@ function custom(check2, _params = {}, fatal) {
14509
14509
  return r.then((r2) => {
14510
14510
  if (!r2) {
14511
14511
  const params = cleanParams(_params, data);
14512
- const _fatal = params.fatal ?? fatal ?? true;
14512
+ const _fatal = params.fatal ?? fatal2 ?? true;
14513
14513
  ctx.addIssue({ code: "custom", ...params, fatal: _fatal });
14514
14514
  }
14515
14515
  });
14516
14516
  }
14517
14517
  if (!r) {
14518
14518
  const params = cleanParams(_params, data);
14519
- const _fatal = params.fatal ?? fatal ?? true;
14519
+ const _fatal = params.fatal ?? fatal2 ?? true;
14520
14520
  ctx.addIssue({ code: "custom", ...params, fatal: _fatal });
14521
14521
  }
14522
14522
  return;
@@ -15146,10 +15146,10 @@ function prefixIssues(path, issues) {
15146
15146
  function unwrapMessage(message) {
15147
15147
  return typeof message === "string" ? message : message?.message;
15148
15148
  }
15149
- function finalizeIssue(iss, ctx, config3) {
15149
+ function finalizeIssue(iss, ctx, config2) {
15150
15150
  const full = { ...iss, path: iss.path ?? [] };
15151
15151
  if (!iss.message) {
15152
- const message = unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config3.customError?.(iss)) ?? unwrapMessage(config3.localeError?.(iss)) ?? "Invalid input";
15152
+ const message = unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config2.customError?.(iss)) ?? unwrapMessage(config2.localeError?.(iss)) ?? "Invalid input";
15153
15153
  full.message = message;
15154
15154
  }
15155
15155
  delete full.inst;
@@ -23894,13 +23894,13 @@ var ExperimentalMcpServerTasks = class {
23894
23894
  constructor(_mcpServer) {
23895
23895
  this._mcpServer = _mcpServer;
23896
23896
  }
23897
- registerToolTask(name, config3, handler) {
23898
- const execution = { taskSupport: "required", ...config3.execution };
23897
+ registerToolTask(name, config2, handler) {
23898
+ const execution = { taskSupport: "required", ...config2.execution };
23899
23899
  if (execution.taskSupport === "forbidden") {
23900
23900
  throw new Error(`Cannot register task-based tool '${name}' with taskSupport 'forbidden'. Use registerTool() instead.`);
23901
23901
  }
23902
23902
  const mcpServerInternal = this._mcpServer;
23903
- return mcpServerInternal._createRegisteredTool(name, config3.title, config3.description, config3.inputSchema, config3.outputSchema, config3.annotations, execution, config3._meta, handler);
23903
+ return mcpServerInternal._createRegisteredTool(name, config2.title, config2.description, config2.inputSchema, config2.outputSchema, config2.annotations, execution, config2._meta, handler);
23904
23904
  }
23905
23905
  };
23906
23906
 
@@ -24329,12 +24329,12 @@ var McpServer = class {
24329
24329
  return registeredResourceTemplate;
24330
24330
  }
24331
24331
  }
24332
- registerResource(name, uriOrTemplate, config3, readCallback) {
24332
+ registerResource(name, uriOrTemplate, config2, readCallback) {
24333
24333
  if (typeof uriOrTemplate === "string") {
24334
24334
  if (this._registeredResources[uriOrTemplate]) {
24335
24335
  throw new Error(`Resource ${uriOrTemplate} is already registered`);
24336
24336
  }
24337
- const registeredResource = this._createRegisteredResource(name, config3.title, uriOrTemplate, config3, readCallback);
24337
+ const registeredResource = this._createRegisteredResource(name, config2.title, uriOrTemplate, config2, readCallback);
24338
24338
  this.setResourceRequestHandlers();
24339
24339
  this.sendResourceListChanged();
24340
24340
  return registeredResource;
@@ -24342,7 +24342,7 @@ var McpServer = class {
24342
24342
  if (this._registeredResourceTemplates[name]) {
24343
24343
  throw new Error(`Resource template ${name} is already registered`);
24344
24344
  }
24345
- const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config3.title, uriOrTemplate, config3, readCallback);
24345
+ const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config2.title, uriOrTemplate, config2, readCallback);
24346
24346
  this.setResourceRequestHandlers();
24347
24347
  this.sendResourceListChanged();
24348
24348
  return registeredResourceTemplate;
@@ -24540,11 +24540,11 @@ var McpServer = class {
24540
24540
  /**
24541
24541
  * Registers a tool with a config object and callback.
24542
24542
  */
24543
- registerTool(name, config3, cb) {
24543
+ registerTool(name, config2, cb) {
24544
24544
  if (this._registeredTools[name]) {
24545
24545
  throw new Error(`Tool ${name} is already registered`);
24546
24546
  }
24547
- const { title, description, inputSchema, outputSchema, annotations, _meta } = config3;
24547
+ const { title, description, inputSchema, outputSchema, annotations, _meta } = config2;
24548
24548
  return this._createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, { taskSupport: "forbidden" }, _meta, cb);
24549
24549
  }
24550
24550
  prompt(name, ...rest) {
@@ -24568,11 +24568,11 @@ var McpServer = class {
24568
24568
  /**
24569
24569
  * Registers a prompt with a config object and callback.
24570
24570
  */
24571
- registerPrompt(name, config3, cb) {
24571
+ registerPrompt(name, config2, cb) {
24572
24572
  if (this._registeredPrompts[name]) {
24573
24573
  throw new Error(`Prompt ${name} is already registered`);
24574
24574
  }
24575
- const { title, description, argsSchema } = config3;
24575
+ const { title, description, argsSchema } = config2;
24576
24576
  const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema, cb);
24577
24577
  this.setPromptRequestHandlers();
24578
24578
  this.sendPromptListChanged();
@@ -24832,6 +24832,8 @@ var BridgeTransport = class {
24832
24832
  opts;
24833
24833
  /** Resolves when an editor attaches; replaced after each disconnect. */
24834
24834
  attached;
24835
+ /** Has an editor ever held this bridge? Decides how patient `call` is. */
24836
+ seenEditor = false;
24835
24837
  markAttached;
24836
24838
  constructor(opts) {
24837
24839
  this.opts = opts;
@@ -24890,6 +24892,7 @@ var BridgeTransport = class {
24890
24892
  return;
24891
24893
  }
24892
24894
  this.socket = ws;
24895
+ this.seenEditor = true;
24893
24896
  this.markAttached();
24894
24897
  console.error("[dadaki-mcp] editor attached");
24895
24898
  ws.on("message", (raw) => {
@@ -24924,7 +24927,7 @@ var BridgeTransport = class {
24924
24927
  async call(method, args = []) {
24925
24928
  await this.listen();
24926
24929
  if (!this.socket || this.socket.readyState !== this.socket.OPEN) {
24927
- const wait = this.opts.attachTimeoutMs ?? 12e4;
24930
+ const wait = this.opts.attachTimeoutMs ?? (this.seenEditor ? 45e3 : 4e3);
24928
24931
  const timedOut = /* @__PURE__ */ Symbol("timeout");
24929
24932
  let timer;
24930
24933
  const race = await Promise.race([
@@ -25049,11 +25052,10 @@ var RelayTransport = class {
25049
25052
  await new Promise((r) => setTimeout(r, 1e3));
25050
25053
  }
25051
25054
  }
25052
- async call(method, args = []) {
25053
- if (!await this.attached()) await this.waitForEditor();
25054
- let res;
25055
+ /** One call attempt. Network failures are the transport's own error. */
25056
+ async post(method, args) {
25055
25057
  try {
25056
- res = await fetch(`${this.base}/call`, {
25058
+ return await fetch(`${this.base}/call`, {
25057
25059
  method: "POST",
25058
25060
  headers: { "Content-Type": "application/json" },
25059
25061
  body: JSON.stringify({ token: this.opts.token, method, args }),
@@ -25064,12 +25066,20 @@ var RelayTransport = class {
25064
25066
  `could not reach the relay at ${this.base} (${err.message})`
25065
25067
  );
25066
25068
  }
25069
+ }
25070
+ async call(method, args = []) {
25071
+ let res = await this.post(method, args);
25072
+ if (res.status === 409) {
25073
+ await this.waitForEditor();
25074
+ res = await this.post(method, args);
25075
+ }
25067
25076
  if (res.status === 409) {
25068
25077
  throw new AgentCallError("no editor is attached to this session");
25069
25078
  }
25070
25079
  if (!res.ok) {
25071
25080
  throw new AgentCallError(`relay returned ${res.status} for ${method}`);
25072
25081
  }
25082
+ this.seenEditor = true;
25073
25083
  return unwrap(await res.json());
25074
25084
  }
25075
25085
  async close() {
@@ -25201,8 +25211,13 @@ async function createTransport(cfg) {
25201
25211
  }
25202
25212
 
25203
25213
  // src/index.ts
25204
- var config2 = readConfig();
25205
- var { transport: session, notice } = await createTransport(config2);
25214
+ function fatal(err) {
25215
+ console.error(err?.message ?? String(err));
25216
+ process.exit(1);
25217
+ }
25218
+ var { transport: session, notice } = await (async () => createTransport(readConfig()))().catch(
25219
+ fatal
25220
+ );
25206
25221
  console.error(notice);
25207
25222
  var INSTRUCTIONS = `Dadaki is a real vector editor. You are drawing actual
25208
25223
  vector geometry \u2014 paths, gradients, live text \u2014 not describing an image for
@@ -25660,4 +25675,7 @@ var shutdown = async () => {
25660
25675
  };
25661
25676
  process.on("SIGINT", shutdown);
25662
25677
  process.on("SIGTERM", shutdown);
25663
- await server.connect(new StdioServerTransport());
25678
+ var stdio = new StdioServerTransport();
25679
+ stdio.onclose = shutdown;
25680
+ process.stdin.on("end", shutdown);
25681
+ await server.connect(stdio);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dadaki/mcp",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "MCP server that lets an agent author vector artwork in Dadaki — intent-level drawing verbs plus render-back so the agent can see its own work.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
+ "LICENSE",
30
31
  "README.md"
31
32
  ],
32
33
  "engines": {
@@ -43,7 +44,9 @@
43
44
  "smoke": "node --experimental-strip-types smoke.ts",
44
45
  "smoke:bridge": "node --experimental-strip-types smoke_bridge.ts",
45
46
  "smoke:modes": "node --experimental-strip-types smoke_modes.ts",
46
- "smoke:relay": "node smoke_relay.mjs"
47
+ "smoke:relay": "node smoke_relay.mjs",
48
+ "smoke:offline": "node --experimental-strip-types smoke_relay_offline.ts",
49
+ "smoke:bundle": "node build.mjs && DADAKI_MCP_TEST_BUNDLE=1 node --experimental-strip-types smoke.ts"
47
50
  },
48
51
  "devDependencies": {
49
52
  "@modelcontextprotocol/sdk": "^1.22.0",