draw2agent 1.3.4 → 2.0.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 CHANGED
@@ -1,62 +1,78 @@
1
- # draw2agent ✏️
2
-
3
- [![mcp-registry](https://img.shields.io/badge/mcp--registry-draw2agent%401.3.3-blue)](https://mcpregistry.com/packages/draw2agent)
4
-
5
- Draw on your website. Your AI agent sees it.
6
-
7
- **draw2agent** is an MCP server that lets you draw annotations directly on top of your local dev page. When you submit, your IDE agent receives a screenshot, structured DOM data, and annotation context to make precise code edits.
8
-
9
- 👉 **Try it out at:** [draw2agent.vercel.app](https://draw2agent.vercel.app)
10
-
11
- ## Demo
12
-
13
- [![draw2agent demo video](https://img.youtube.com/vi/siv1ioOnOXk/maxresdefault.jpg)](https://youtu.be/siv1ioOnOXk)
14
- *(Note: Video hosted on YouTube. Demo files have been ignored from the repository to save space.)*
15
-
16
- ## Quick Start
17
-
18
- ### 1. Add to your IDE (one-time)
19
-
20
- **Cursor** (`~/.cursor/mcp.json`):
21
- ```json
22
- {
23
- "mcpServers": {
24
- "draw2agent": {
25
- "command": "npx",
26
- "args": ["-y", "draw2agent"]
27
- }
28
- }
29
- }
30
- ```
31
-
32
- ### 2. Use it
33
-
34
- Tell your agent:
35
- > "Use draw2agent to fix the navbar"
36
-
37
- 1. 🌐 Agent opens your browser with drawing tools on your page
38
- 2. ✏️ Draw circles, arrows, text directly on your website
39
- 3. 📸 Click **Submit**
40
- 4. 🤖 Agent reads the visual context and applies code changes
41
-
42
- ## How It Works
43
-
44
- ```
45
- Your Dev Page (proxied)
46
- ├── Your original page content
47
- └── Excalidraw overlay (transparent, on top)
48
- ├── Draw mode: annotate directly on the page
49
- ├── Select mode: interact with the page normally (Esc)
50
- └── Submit: screenshot + DOM + annotations → agent
51
- ```
52
-
53
- The MCP server exposes two tools:
54
-
55
- | Tool | Description |
56
- |---|---|
57
- | `launch_canvas` | Opens your dev page with the drawing overlay |
58
- | `get_drawing_state` | Returns screenshot, DOM nodes, and annotations |
59
-
60
- ## License
61
-
62
- MIT
1
+ # draw2agent ✏️
2
+
3
+ [![npm version](https://img.shields.io/npm/v/draw2agent)](https://www.npmjs.com/package/draw2agent)
4
+ [![mcp-registry](https://img.shields.io/badge/mcp--registry-io.github.zero--abd%2Fdraw2agent%402.0.0-blue)](https://registry.modelcontextprotocol.io/?q=draw2agent)
5
+
6
+ Draw on your website. Your AI agent sees it.
7
+
8
+ **draw2agent** is an MCP server that lets you draw annotations directly on top of your local dev page. When you submit, your IDE agent receives a screenshot, structured DOM data, and annotation context to make precise code edits.
9
+
10
+ 👉 **Try it out at:** [draw2agent.vercel.app](https://draw2agent.vercel.app)
11
+
12
+ ## Demo
13
+
14
+ [![draw2agent demo video](https://img.youtube.com/vi/siv1ioOnOXk/maxresdefault.jpg)](https://youtu.be/siv1ioOnOXk)
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Add to your IDE (one-time)
19
+
20
+ **Cursor** (`~/.cursor/mcp.json`):
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "draw2agent": {
25
+ "command": "npx",
26
+ "args": ["-y", "draw2agent@latest"]
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### 2. Use it
33
+
34
+ Tell your agent:
35
+ > "Use draw2agent to fix the navbar"
36
+
37
+ 1. 🌐 Agent opens your browser with drawing tools on your page
38
+ 2. ✏️ Draw circles, arrows, text directly on your website
39
+ 3. 📸 Click **Submit**
40
+ 4. 🤖 Agent reads the visual context and applies code changes
41
+
42
+ ## How It Works
43
+
44
+ ```
45
+ Your Dev Page (proxied)
46
+ ├── Your original page content
47
+ └── Excalidraw overlay (transparent, on top)
48
+ ├── Draw mode: annotate directly on the page
49
+ ├── Select mode: interact with the page normally (Esc)
50
+ └── Submit: screenshot + DOM + annotations → agent
51
+ ```
52
+
53
+ ## Tools
54
+
55
+ The MCP server exposes the following tools:
56
+
57
+ | Tool | Description |
58
+ |---|---|
59
+ | `launch_canvas` | Opens your dev page with the drawing overlay |
60
+ | `launch_ipad_canvas` | Creates a tunnel and returns a QR code for remote drawing from iPad/mobile |
61
+ | `launch_scratch` | Opens a standalone Excalidraw whiteboard for freehand sketching |
62
+ | `get_drawing_state` | Returns screenshot, DOM nodes, and annotations for the current state |
63
+
64
+ ### `launch_canvas`
65
+ The core tool — proxies your localhost dev server and injects an Excalidraw overlay. Draw annotations directly on your running app, then submit to send visual context to your agent. The tool blocks until you submit.
66
+
67
+ ### `launch_ipad_canvas`
68
+ Same as `launch_canvas`, but exposes the proxy over the internet via a secure tunnel. Returns a QR code that you can scan from your iPad or phone to draw annotations with touch. Perfect for whiteboard-style feedback sessions.
69
+
70
+ ### `launch_scratch`
71
+ Opens a blank Excalidraw whiteboard — no target URL needed. Sketch UI mockups, wireframes, or diagrams from scratch. Your agent receives the drawing and implements the design.
72
+
73
+ ### `get_drawing_state`
74
+ Returns the last captured drawing state (screenshot, DOM nodes, annotations) without launching a new session. Useful for re-fetching context.
75
+
76
+ ## License
77
+
78
+ MIT
package/dist/index.js CHANGED
@@ -778,10 +778,10 @@ function mergeDefs(...defs) {
778
778
  function cloneDef(schema) {
779
779
  return mergeDefs(schema._zod.def);
780
780
  }
781
- function getElementAtPath(obj, path3) {
782
- if (!path3)
781
+ function getElementAtPath(obj, path4) {
782
+ if (!path4)
783
783
  return obj;
784
- return path3.reduce((acc, key) => acc?.[key], obj);
784
+ return path4.reduce((acc, key) => acc?.[key], obj);
785
785
  }
786
786
  function promiseAllObject(promisesObj) {
787
787
  const keys = Object.keys(promisesObj);
@@ -1164,11 +1164,11 @@ function aborted(x, startIndex = 0) {
1164
1164
  }
1165
1165
  return false;
1166
1166
  }
1167
- function prefixIssues(path3, issues) {
1167
+ function prefixIssues(path4, issues) {
1168
1168
  return issues.map((iss) => {
1169
1169
  var _a2;
1170
1170
  (_a2 = iss).path ?? (_a2.path = []);
1171
- iss.path.unshift(path3);
1171
+ iss.path.unshift(path4);
1172
1172
  return iss;
1173
1173
  });
1174
1174
  }
@@ -1351,7 +1351,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
1351
1351
  }
1352
1352
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
1353
1353
  const result = { errors: [] };
1354
- const processError = (error49, path3 = []) => {
1354
+ const processError = (error49, path4 = []) => {
1355
1355
  var _a2, _b;
1356
1356
  for (const issue2 of error49.issues) {
1357
1357
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1361,7 +1361,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1361
1361
  } else if (issue2.code === "invalid_element") {
1362
1362
  processError({ issues: issue2.issues }, issue2.path);
1363
1363
  } else {
1364
- const fullpath = [...path3, ...issue2.path];
1364
+ const fullpath = [...path4, ...issue2.path];
1365
1365
  if (fullpath.length === 0) {
1366
1366
  result.errors.push(mapper(issue2));
1367
1367
  continue;
@@ -1393,8 +1393,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
1393
1393
  }
1394
1394
  function toDotPath(_path) {
1395
1395
  const segs = [];
1396
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1397
- for (const seg of path3) {
1396
+ const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1397
+ for (const seg of path4) {
1398
1398
  if (typeof seg === "number")
1399
1399
  segs.push(`[${seg}]`);
1400
1400
  else if (typeof seg === "symbol")
@@ -13371,13 +13371,13 @@ function resolveRef(ref, ctx) {
13371
13371
  if (!ref.startsWith("#")) {
13372
13372
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
13373
13373
  }
13374
- const path3 = ref.slice(1).split("/").filter(Boolean);
13375
- if (path3.length === 0) {
13374
+ const path4 = ref.slice(1).split("/").filter(Boolean);
13375
+ if (path4.length === 0) {
13376
13376
  return ctx.rootSchema;
13377
13377
  }
13378
13378
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
13379
- if (path3[0] === defsKey) {
13380
- const key = path3[1];
13379
+ if (path4[0] === defsKey) {
13380
+ const key = path4[1];
13381
13381
  if (!key || !ctx.defs[key]) {
13382
13382
  throw new Error(`Reference not found: ${ref}`);
13383
13383
  }
@@ -13827,6 +13827,9 @@ function rejectState(errorMsg) {
13827
13827
  rejectPendingState = null;
13828
13828
  }
13829
13829
  }
13830
+ function getState() {
13831
+ return currentState;
13832
+ }
13830
13833
  function clearState() {
13831
13834
  currentState = null;
13832
13835
  isSessionClosed = false;
@@ -13992,6 +13995,203 @@ function stopHttpServer() {
13992
13995
  }
13993
13996
  }
13994
13997
 
13998
+ // src/scratch-server.ts
13999
+ import http2 from "http";
14000
+ import fs2 from "fs";
14001
+ import path2 from "path";
14002
+ import { fileURLToPath as fileURLToPath2 } from "url";
14003
+ var __dirname2 = path2.dirname(fileURLToPath2(import.meta.url));
14004
+ var OVERLAY_DIR2 = path2.resolve(__dirname2, "..", "overlay", "dist");
14005
+ var D2A_PREFIX2 = "/__d2a__";
14006
+ var MIME_TYPES2 = {
14007
+ ".js": "application/javascript",
14008
+ ".css": "text/css",
14009
+ ".html": "text/html",
14010
+ ".json": "application/json",
14011
+ ".png": "image/png",
14012
+ ".svg": "image/svg+xml",
14013
+ ".woff2": "font/woff2",
14014
+ ".woff": "font/woff",
14015
+ ".ttf": "font/ttf"
14016
+ };
14017
+ var scratchServer = null;
14018
+ function getScratchHTML() {
14019
+ return `<!DOCTYPE html>
14020
+ <html lang="en">
14021
+ <head>
14022
+ <meta charset="UTF-8">
14023
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
14024
+ <title>draw2agent \u2014 Scratch Whiteboard</title>
14025
+ <link rel="stylesheet" href="${D2A_PREFIX2}/draw2agent-overlay.css">
14026
+ <style>
14027
+ * { margin: 0; padding: 0; box-sizing: border-box; }
14028
+ html, body { width: 100%; height: 100%; overflow: hidden; background: #1e1e2e; }
14029
+ /* In scratch mode, make the Excalidraw canvas fill the entire page with a visible background */
14030
+ #draw2agent-root {
14031
+ position: fixed !important;
14032
+ inset: 0 !important;
14033
+ z-index: 1 !important;
14034
+ }
14035
+ #draw2agent-root .d2a-canvas-container {
14036
+ pointer-events: all !important;
14037
+ position: fixed !important;
14038
+ inset: 0 !important;
14039
+ z-index: 1 !important;
14040
+ }
14041
+ /* Show a background for the Excalidraw canvas in scratch mode */
14042
+ #draw2agent-root .excalidraw {
14043
+ --ui-font: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
14044
+ }
14045
+ /* Override transparent background for scratch mode */
14046
+ #draw2agent-root .excalidraw .excalidraw__canvas {
14047
+ background: #1e1e2e !important;
14048
+ }
14049
+ </style>
14050
+ </head>
14051
+ <body data-d2a-mode="scratch">
14052
+ <script src="${D2A_PREFIX2}/draw2agent-overlay.js"></script>
14053
+ </body>
14054
+ </html>`;
14055
+ }
14056
+ function startScratchServer(port) {
14057
+ return new Promise((resolve, reject) => {
14058
+ if (scratchServer) {
14059
+ resolve(`http://localhost:${port}`);
14060
+ return;
14061
+ }
14062
+ scratchServer = http2.createServer((req, res) => {
14063
+ const url2 = req.url || "/";
14064
+ if (req.method === "OPTIONS") {
14065
+ res.writeHead(204, {
14066
+ "Access-Control-Allow-Origin": "*",
14067
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
14068
+ "Access-Control-Allow-Headers": "Content-Type"
14069
+ });
14070
+ res.end();
14071
+ return;
14072
+ }
14073
+ if (url2 === `${D2A_PREFIX2}/capture` && req.method === "POST") {
14074
+ let body = "";
14075
+ req.on("data", (chunk) => body += chunk);
14076
+ req.on("end", () => {
14077
+ try {
14078
+ const payload = JSON.parse(body);
14079
+ payload.timestamp = (/* @__PURE__ */ new Date()).toISOString();
14080
+ payload.targetUrl = "scratch://whiteboard";
14081
+ setState(payload);
14082
+ res.writeHead(200, {
14083
+ "Content-Type": "application/json",
14084
+ "Access-Control-Allow-Origin": "*"
14085
+ });
14086
+ res.end(JSON.stringify({ success: true }));
14087
+ console.error("[draw2agent] \u2705 Scratch state captured successfully");
14088
+ } catch (err) {
14089
+ const msg = err instanceof Error ? err.message : String(err);
14090
+ console.error("[draw2agent] \u274C Scratch capture error:", msg);
14091
+ rejectState(`Failed to parse capture payload: ${msg}`);
14092
+ res.writeHead(400, { "Content-Type": "application/json" });
14093
+ res.end(JSON.stringify({ error: "Invalid JSON payload" }));
14094
+ }
14095
+ });
14096
+ return;
14097
+ }
14098
+ if (url2 === `${D2A_PREFIX2}/close` && req.method === "POST") {
14099
+ rejectState("User closed the draw2agent session.");
14100
+ res.writeHead(200, {
14101
+ "Content-Type": "application/json",
14102
+ "Access-Control-Allow-Origin": "*"
14103
+ });
14104
+ res.end(JSON.stringify({ success: true }));
14105
+ console.error("[draw2agent] \u{1F6D1} Scratch session closed by user");
14106
+ return;
14107
+ }
14108
+ if (url2.startsWith(D2A_PREFIX2 + "/")) {
14109
+ const filePath = path2.join(OVERLAY_DIR2, url2.slice(D2A_PREFIX2.length));
14110
+ const ext = path2.extname(filePath);
14111
+ const mime = MIME_TYPES2[ext] || "application/octet-stream";
14112
+ fs2.readFile(filePath, (err, data) => {
14113
+ if (err) {
14114
+ res.writeHead(404, { "Content-Type": "text/plain" });
14115
+ res.end("Not found");
14116
+ return;
14117
+ }
14118
+ res.writeHead(200, {
14119
+ "Content-Type": mime,
14120
+ "Access-Control-Allow-Origin": "*",
14121
+ "Cache-Control": "no-cache"
14122
+ });
14123
+ res.end(data);
14124
+ });
14125
+ return;
14126
+ }
14127
+ if (url2 === "/" || url2 === "/index.html") {
14128
+ const html = getScratchHTML();
14129
+ res.writeHead(200, {
14130
+ "Content-Type": "text/html",
14131
+ "Cache-Control": "no-cache"
14132
+ });
14133
+ res.end(html);
14134
+ return;
14135
+ }
14136
+ res.writeHead(404, { "Content-Type": "text/plain" });
14137
+ res.end("Not found");
14138
+ });
14139
+ scratchServer.listen(port, () => {
14140
+ const url2 = `http://localhost:${port}`;
14141
+ console.error(`[draw2agent] \u{1F3A8} Scratch whiteboard at ${url2}`);
14142
+ resolve(url2);
14143
+ });
14144
+ scratchServer.on("error", (err) => {
14145
+ reject(err);
14146
+ });
14147
+ });
14148
+ }
14149
+ function stopScratchServer() {
14150
+ if (scratchServer) {
14151
+ scratchServer.close();
14152
+ scratchServer = null;
14153
+ }
14154
+ }
14155
+
14156
+ // src/tunnel.ts
14157
+ import localtunnel from "localtunnel";
14158
+ var activeTunnel = null;
14159
+ async function startTunnel(localPort) {
14160
+ await stopTunnel();
14161
+ const tunnel = await localtunnel({ port: localPort });
14162
+ tunnel.on("close", () => {
14163
+ console.error("[draw2agent] \u{1F50C} Tunnel closed");
14164
+ activeTunnel = null;
14165
+ });
14166
+ tunnel.on("error", (err) => {
14167
+ console.error("[draw2agent] Tunnel error:", err.message);
14168
+ });
14169
+ activeTunnel = tunnel;
14170
+ console.error(`[draw2agent] \u{1F310} Tunnel opened: ${tunnel.url}`);
14171
+ return tunnel.url;
14172
+ }
14173
+ async function stopTunnel() {
14174
+ if (activeTunnel) {
14175
+ activeTunnel.close();
14176
+ activeTunnel = null;
14177
+ }
14178
+ }
14179
+
14180
+ // src/utils/qrcode.ts
14181
+ import QRCode from "qrcode";
14182
+ async function generateQR(url2) {
14183
+ const [dataUrl, ascii] = await Promise.all([
14184
+ QRCode.toDataURL(url2, {
14185
+ type: "image/png",
14186
+ width: 400,
14187
+ margin: 2,
14188
+ color: { dark: "#000000", light: "#ffffff" }
14189
+ }),
14190
+ QRCode.toString(url2, { type: "terminal", small: true })
14191
+ ]);
14192
+ return { dataUrl, ascii };
14193
+ }
14194
+
13995
14195
  // src/utils/browser.ts
13996
14196
  import open from "open";
13997
14197
  async function openBrowser(url2) {
@@ -13999,14 +14199,50 @@ async function openBrowser(url2) {
13999
14199
  }
14000
14200
 
14001
14201
  // src/mcp-server.ts
14002
- import fs2 from "fs";
14003
- import path2 from "path";
14004
- import { fileURLToPath as fileURLToPath2 } from "url";
14005
- var __dirname2 = path2.dirname(fileURLToPath2(import.meta.url));
14006
- var INSTRUCTIONS_PATH = path2.resolve(__dirname2, "..", "prompts", "agent-instructions.txt");
14007
- var ERROR_INSTRUCTIONS_PATH = path2.resolve(__dirname2, "..", "prompts", "agent-error-instructions.txt");
14008
- var CLOSE_INSTRUCTIONS_PATH = path2.resolve(__dirname2, "..", "prompts", "agent-close-instructions.txt");
14202
+ import fs3 from "fs";
14203
+ import path3 from "path";
14204
+ import { fileURLToPath as fileURLToPath3 } from "url";
14205
+ var __dirname3 = path3.dirname(fileURLToPath3(import.meta.url));
14206
+ var INSTRUCTIONS_PATH = path3.resolve(__dirname3, "..", "prompts", "agent-instructions.txt");
14207
+ var ERROR_INSTRUCTIONS_PATH = path3.resolve(__dirname3, "..", "prompts", "agent-error-instructions.txt");
14208
+ var CLOSE_INSTRUCTIONS_PATH = path3.resolve(__dirname3, "..", "prompts", "agent-close-instructions.txt");
14209
+ var IPAD_INSTRUCTIONS_PATH = path3.resolve(__dirname3, "..", "prompts", "agent-ipad-instructions.txt");
14210
+ var SCRATCH_INSTRUCTIONS_PATH = path3.resolve(__dirname3, "..", "prompts", "agent-scratch-instructions.txt");
14009
14211
  var DEFAULT_PORT = 9742;
14212
+ var DEFAULT_SCRATCH_PORT = 9743;
14213
+ function readPromptFile(filePath, fallback) {
14214
+ try {
14215
+ if (fs3.existsSync(filePath)) {
14216
+ return fs3.readFileSync(filePath, "utf-8");
14217
+ }
14218
+ } catch (e) {
14219
+ console.error(`[draw2agent] Failed to read ${path3.basename(filePath)}, using default.`, e);
14220
+ }
14221
+ return fallback;
14222
+ }
14223
+ function handleToolError(err, toolName) {
14224
+ const message = err instanceof Error ? err.message : String(err);
14225
+ let customInstructions = `\u274C Failed to capture canvas: ${message}`;
14226
+ let isErrorResult = true;
14227
+ if (message.includes("User closed the draw2agent session")) {
14228
+ customInstructions = readPromptFile(
14229
+ CLOSE_INSTRUCTIONS_PATH,
14230
+ "The user closed the draw2agent session. Please summarize the changes you made."
14231
+ );
14232
+ customInstructions = customInstructions.replace(/launch_canvas/g, toolName);
14233
+ isErrorResult = false;
14234
+ stopHttpServer();
14235
+ stopScratchServer();
14236
+ stopTunnel();
14237
+ clearProxyInfo();
14238
+ } else {
14239
+ customInstructions = readPromptFile(ERROR_INSTRUCTIONS_PATH, customInstructions).replace("{{ERROR_MESSAGE}}", message);
14240
+ }
14241
+ return {
14242
+ content: [{ type: "text", text: customInstructions }],
14243
+ isError: isErrorResult
14244
+ };
14245
+ }
14010
14246
  function createMcpServer() {
14011
14247
  const server2 = new McpServer({
14012
14248
  name: "draw2agent",
@@ -14046,15 +14282,10 @@ function createMcpServer() {
14046
14282
  }
14047
14283
  clearState();
14048
14284
  const state = await waitForState();
14049
- let instructionsUrl = "";
14050
- let customInstructions = "Analyze the attached screenshot with user annotations and implement the requested UI changes in the codebase.";
14051
- try {
14052
- if (fs2.existsSync(INSTRUCTIONS_PATH)) {
14053
- customInstructions = fs2.readFileSync(INSTRUCTIONS_PATH, "utf-8");
14054
- }
14055
- } catch (e) {
14056
- console.error("[draw2agent] Failed to read agent-instructions.txt, using default.", e);
14057
- }
14285
+ const customInstructions = readPromptFile(
14286
+ INSTRUCTIONS_PATH,
14287
+ "Analyze the attached screenshot with user annotations and implement the requested UI changes in the codebase."
14288
+ );
14058
14289
  return {
14059
14290
  content: [
14060
14291
  {
@@ -14069,37 +14300,145 @@ function createMcpServer() {
14069
14300
  ]
14070
14301
  };
14071
14302
  } catch (err) {
14072
- const message = err instanceof Error ? err.message : String(err);
14073
- let customInstructions = `\u274C Failed to capture canvas: ${message}`;
14074
- let isErrorResult = true;
14303
+ return handleToolError(err, "launch_canvas");
14304
+ }
14305
+ }
14306
+ );
14307
+ server2.tool(
14308
+ "launch_ipad_canvas",
14309
+ "Launch a remote drawing canvas accessible from an iPad or mobile device. Creates a tunnel to expose the local dev page over the internet and returns a QR code that the user can scan from their iPad. The user draws annotations on their device, and this tool blocks until they submit, returning the visual context. Ideal for touch-based annotation workflows.",
14310
+ {
14311
+ targetUrl: external_exports.string().describe("The URL of the local dev server to overlay (e.g. http://localhost:3000)"),
14312
+ port: external_exports.number().optional().describe("Port for the draw2agent proxy server (default: 9742)")
14313
+ },
14314
+ async ({ targetUrl, port }) => {
14315
+ const proxyPort = port ?? DEFAULT_PORT;
14316
+ try {
14075
14317
  try {
14076
- if (message.includes("User closed the draw2agent session")) {
14077
- if (fs2.existsSync(CLOSE_INSTRUCTIONS_PATH)) {
14078
- customInstructions = fs2.readFileSync(CLOSE_INSTRUCTIONS_PATH, "utf-8");
14079
- } else {
14080
- customInstructions = "The user closed the draw2agent session. Please summarize the changes you made.";
14081
- }
14082
- isErrorResult = false;
14083
- stopHttpServer();
14084
- clearProxyInfo();
14085
- } else {
14086
- if (fs2.existsSync(ERROR_INSTRUCTIONS_PATH)) {
14087
- customInstructions = fs2.readFileSync(ERROR_INSTRUCTIONS_PATH, "utf-8").replace("{{ERROR_MESSAGE}}", message);
14088
- }
14318
+ const checkUrl = targetUrl.replace("://localhost", "://127.0.0.1");
14319
+ await fetch(checkUrl);
14320
+ } catch (err) {
14321
+ if (err.cause?.code === "ECONNREFUSED") {
14322
+ return {
14323
+ content: [
14324
+ {
14325
+ type: "text",
14326
+ text: `\u274C Connection refused to ${targetUrl}. There is no dev server running on that port. Please ask the user to confirm the correct local server URL.`
14327
+ }
14328
+ ],
14329
+ isError: true
14330
+ };
14089
14331
  }
14090
- } catch (e) {
14091
- console.error("[draw2agent] Failed to read fallback instructions.", e);
14092
14332
  }
14333
+ const proxyInfo2 = getProxyInfo();
14334
+ if (!proxyInfo2.running) {
14335
+ const proxyUrl = await startHttpServer(targetUrl, proxyPort);
14336
+ setProxyInfo(proxyUrl);
14337
+ }
14338
+ const tunnelUrl = await startTunnel(proxyPort);
14339
+ const qr = await generateQR(tunnelUrl);
14340
+ console.error(`
14341
+ [draw2agent] \u{1F4F1} iPad Canvas Ready!`);
14342
+ console.error(`[draw2agent] \u{1F517} Scan this QR code or open: ${tunnelUrl}`);
14343
+ console.error(qr.ascii);
14344
+ clearState();
14345
+ const state = await waitForState();
14346
+ await stopTunnel();
14347
+ const customInstructions = readPromptFile(
14348
+ IPAD_INSTRUCTIONS_PATH,
14349
+ "Analyze the attached screenshot with user annotations and implement the requested UI changes in the codebase."
14350
+ );
14093
14351
  return {
14094
14352
  content: [
14095
14353
  {
14096
14354
  type: "text",
14097
14355
  text: customInstructions
14356
+ },
14357
+ {
14358
+ type: "image",
14359
+ data: state.annotatedScreenshot.replace(/^data:image\/\w+;base64,/, ""),
14360
+ mimeType: "image/png"
14361
+ }
14362
+ ]
14363
+ };
14364
+ } catch (err) {
14365
+ await stopTunnel();
14366
+ return handleToolError(err, "launch_ipad_canvas");
14367
+ }
14368
+ }
14369
+ );
14370
+ server2.tool(
14371
+ "launch_scratch",
14372
+ "Open a standalone whiteboard for freehand drawing and sketching. No target URL needed \u2014 the user gets a blank Excalidraw canvas to sketch UI mockups, wireframes, or diagrams. The agent receives the drawing as visual context. This tool blocks until the user submits their sketch.",
14373
+ {
14374
+ port: external_exports.number().optional().describe("Port for the scratch whiteboard server (default: 9743)")
14375
+ },
14376
+ async ({ port }) => {
14377
+ const scratchPort = port ?? DEFAULT_SCRATCH_PORT;
14378
+ try {
14379
+ const scratchUrl = await startScratchServer(scratchPort);
14380
+ await openBrowser(scratchUrl);
14381
+ clearState();
14382
+ const state = await waitForState();
14383
+ const customInstructions = readPromptFile(
14384
+ SCRATCH_INSTRUCTIONS_PATH,
14385
+ "The user has drawn a freehand sketch. Analyze it and implement the design."
14386
+ );
14387
+ return {
14388
+ content: [
14389
+ {
14390
+ type: "text",
14391
+ text: customInstructions
14392
+ },
14393
+ {
14394
+ type: "image",
14395
+ data: state.annotatedScreenshot.replace(/^data:image\/\w+;base64,/, ""),
14396
+ mimeType: "image/png"
14397
+ }
14398
+ ]
14399
+ };
14400
+ } catch (err) {
14401
+ return handleToolError(err, "launch_scratch");
14402
+ }
14403
+ }
14404
+ );
14405
+ server2.tool(
14406
+ "get_drawing_state",
14407
+ "Returns the current drawing state including screenshot, DOM nodes, and annotations. Use this to retrieve the latest captured state without launching a new canvas session. Returns an error if no state has been captured yet.",
14408
+ {},
14409
+ async () => {
14410
+ const state = getState();
14411
+ if (!state) {
14412
+ return {
14413
+ content: [
14414
+ {
14415
+ type: "text",
14416
+ text: "\u274C No drawing state available. The user has not submitted any drawings yet. Use `launch_canvas`, `launch_ipad_canvas`, or `launch_scratch` first."
14098
14417
  }
14099
14418
  ],
14100
- isError: isErrorResult
14419
+ isError: true
14101
14420
  };
14102
14421
  }
14422
+ return {
14423
+ content: [
14424
+ {
14425
+ type: "text",
14426
+ text: JSON.stringify({
14427
+ timestamp: state.timestamp,
14428
+ targetUrl: state.targetUrl,
14429
+ viewportSize: state.viewportSize,
14430
+ drawingBounds: state.drawingBounds,
14431
+ domNodes: state.domNodes,
14432
+ annotationCount: state.annotations.length
14433
+ }, null, 2)
14434
+ },
14435
+ {
14436
+ type: "image",
14437
+ data: state.annotatedScreenshot.replace(/^data:image\/\w+;base64,/, ""),
14438
+ mimeType: "image/png"
14439
+ }
14440
+ ]
14441
+ };
14103
14442
  }
14104
14443
  );
14105
14444
  return server2;