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 +78 -62
- package/dist/index.js +387 -48
- package/dist/index.js.map +1 -1
- package/overlay/dist/draw2agent-overlay.js +216 -216
- package/package.json +10 -3
- package/prompts/agent-ipad-instructions.txt +12 -0
- package/prompts/agent-scratch-instructions.txt +11 -0
package/README.md
CHANGED
|
@@ -1,62 +1,78 @@
|
|
|
1
|
-
# draw2agent ✏️
|
|
2
|
-
|
|
3
|
-
[![
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
1
|
+
# draw2agent ✏️
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/draw2agent)
|
|
4
|
+
[](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
|
+
[](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,
|
|
782
|
-
if (!
|
|
781
|
+
function getElementAtPath(obj, path4) {
|
|
782
|
+
if (!path4)
|
|
783
783
|
return obj;
|
|
784
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 = [...
|
|
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
|
|
1397
|
-
for (const seg of
|
|
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
|
|
13375
|
-
if (
|
|
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 (
|
|
13380
|
-
const key =
|
|
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
|
|
14003
|
-
import
|
|
14004
|
-
import { fileURLToPath as
|
|
14005
|
-
var
|
|
14006
|
-
var INSTRUCTIONS_PATH =
|
|
14007
|
-
var ERROR_INSTRUCTIONS_PATH =
|
|
14008
|
-
var CLOSE_INSTRUCTIONS_PATH =
|
|
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
|
-
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
|
|
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
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
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
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
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:
|
|
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;
|