@tempad-dev/mcp 0.5.0 → 0.6.0
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 +2 -2
- package/README.zh-Hans.md +2 -1
- package/dist/cli.mjs +3 -4
- package/dist/cli.mjs.map +1 -1
- package/dist/hub.mjs +289 -83
- package/dist/hub.mjs.map +1 -1
- package/dist/{shared-C_kjQL67.mjs → shared-C9V2G_pC.mjs} +12 -13
- package/dist/shared-C9V2G_pC.mjs.map +1 -0
- package/package.json +11 -8
- package/dist/shared-C_kjQL67.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Supported tools/resources:
|
|
|
28
28
|
|
|
29
29
|
Notes:
|
|
30
30
|
|
|
31
|
-
- When a selection is too large for the `get_code` budget, TemPad Dev may return a shell response instead of failing. The shell keeps the current node wrapper and lists omitted direct child ids in an inline code comment so agents can request them one by one.
|
|
31
|
+
- Tool responses use a shared `64 KiB` inline budget measured on the `CallToolResult` body. When a selection is too large for the `get_code` budget, TemPad Dev may return a shell response instead of failing. The shell keeps the current node wrapper and lists omitted direct child ids in an inline code comment so agents can request them one by one. The accompanying warning stays lightweight and only points agents to that comment.
|
|
32
32
|
- Assets are ephemeral and tool-linked; image/SVG bytes are downloaded via HTTP `asset.url` from tool results.
|
|
33
33
|
- Asset resources are not exposed via MCP `resources/list`/`resources/read`.
|
|
34
34
|
- The HTTP fallback URL uses `/assets/{hash}` and may include an image extension (for example `/assets/{hash}.png`). Both forms are accepted.
|
|
@@ -47,4 +47,4 @@ Optional environment variables:
|
|
|
47
47
|
|
|
48
48
|
## Requirements
|
|
49
49
|
|
|
50
|
-
- Node.js 18+
|
|
50
|
+
- Node.js 18.20.0+
|
package/README.zh-Hans.md
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
说明:
|
|
28
28
|
|
|
29
|
+
- 工具响应共用 `64 KiB` 的 inline budget,按 `CallToolResult` 整体响应体积计算。若选区过大而超出 `get_code` 的预算,TemPad Dev 可能返回 shell response 而不是直接失败。shell 会保留当前节点的包裹结构,并在内联代码注释中列出被省略的直接子节点 id,方便 agent 逐个继续拉取;配套 warning 只保留最小化的提示信息,用来指向这条注释。
|
|
29
30
|
- 资源是临时且与工具调用关联的;图片/SVG 请直接使用工具结果中的 HTTP `asset.url` 下载。
|
|
30
31
|
- MCP 不再暴露 `resources/list` / `resources/read` 用于 asset 内容读取。
|
|
31
32
|
- HTTP 回退 URL 使用 `/assets/{hash}`,也可能带图片扩展名(例如 `/assets/{hash}.png`),两种形式都支持。
|
|
@@ -44,4 +45,4 @@
|
|
|
44
45
|
|
|
45
46
|
## 要求
|
|
46
47
|
|
|
47
|
-
- Node.js 18+
|
|
48
|
+
- Node.js 18.20.0+
|
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as SOCK_PATH, c as log, i as RUNTIME_DIR, n as LOCK_PATH, o as ensureDir, r as PACKAGE_VERSION } from "./shared-
|
|
2
|
+
import { a as SOCK_PATH, c as log, i as RUNTIME_DIR, n as LOCK_PATH, o as ensureDir, r as PACKAGE_VERSION } from "./shared-C9V2G_pC.mjs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { connect } from "node:net";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import lockfile from "proper-lockfile";
|
|
8
|
-
|
|
9
8
|
//#region src/cli.ts
|
|
10
9
|
let activeSocket = null;
|
|
11
10
|
let shuttingDown = false;
|
|
@@ -141,7 +140,7 @@ async function main() {
|
|
|
141
140
|
}
|
|
142
141
|
}
|
|
143
142
|
main();
|
|
144
|
-
|
|
145
143
|
//#endregion
|
|
146
|
-
export {
|
|
144
|
+
export {};
|
|
145
|
+
|
|
147
146
|
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport type { ChildProcess } from 'node:child_process'\nimport type { Socket } from 'node:net'\n\nimport { spawn } from 'node:child_process'\nimport { connect } from 'node:net'\nimport { join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport lockfile from 'proper-lockfile'\n\nimport { PACKAGE_VERSION, log, LOCK_PATH, RUNTIME_DIR, SOCK_PATH, ensureDir } from './shared'\n\nlet activeSocket: Socket | null = null\nlet shuttingDown = false\n\nfunction closeActiveSocket() {\n if (!activeSocket) return\n try {\n activeSocket.end()\n } catch {\n // ignore\n }\n try {\n activeSocket.destroy()\n } catch {\n // ignore\n }\n activeSocket = null\n}\n\nfunction shutdownCli(reason: string) {\n if (shuttingDown) return\n shuttingDown = true\n log.info(`${reason} Shutting down CLI.`)\n closeActiveSocket()\n process.exit(0)\n}\n\nprocess.on('SIGINT', () => shutdownCli('SIGINT received.'))\nprocess.on('SIGTERM', () => shutdownCli('SIGTERM received.'))\n\nconst HUB_STARTUP_TIMEOUT = 5000\nconst CONNECT_RETRY_DELAY = 200\nconst FAILED_RESTART_DELAY = 5000\nconst HERE = fileURLToPath(new URL('.', import.meta.url))\nconst HUB_ENTRY = join(HERE, 'hub.mjs')\n\nensureDir(RUNTIME_DIR)\n\nfunction bridge(socket: Socket): Promise<void> {\n return new Promise((resolve) => {\n log.info('Bridge established with Hub. Forwarding I/O.')\n activeSocket = socket\n\n const onStdinEnd = () => {\n shutdownCli('Consumer stream ended.')\n }\n process.stdin.once('end', onStdinEnd)\n\n const onSocketClose = () => {\n log.warn('Connection to Hub lost. Attempting to reconnect...')\n activeSocket = null\n process.stdin.removeListener('end', onStdinEnd)\n process.stdin.unpipe(socket)\n socket.unpipe(process.stdout)\n socket.removeAllListeners()\n resolve()\n }\n socket.once('close', onSocketClose)\n socket.on('error', (err) => log.warn({ err }, 'Socket error occurred.'))\n\n // The `{ end: false }` option prevents stdin from closing the socket.\n process.stdin.pipe(socket, { end: false }).pipe(process.stdout)\n })\n}\n\nfunction connectHub(): Promise<Socket> {\n return new Promise((resolve, reject) => {\n const socket = connect(SOCK_PATH)\n socket.on('connect', () => {\n socket.removeAllListeners('error')\n resolve(socket)\n })\n socket.on('error', reject)\n })\n}\n\nasync function connectWithRetry(timeout: number): Promise<Socket> {\n const startTime = Date.now()\n let delay = CONNECT_RETRY_DELAY\n while (Date.now() - startTime < timeout) {\n try {\n return await connectHub()\n } catch (err: unknown) {\n if (\n err &&\n typeof err === 'object' &&\n 'code' in err &&\n (err.code === 'ENOENT' || err.code === 'ECONNREFUSED')\n ) {\n const remainingTime = timeout - (Date.now() - startTime)\n const waitTime = Math.min(delay, remainingTime)\n if (waitTime <= 0) break\n await new Promise((r) => setTimeout(r, waitTime))\n delay = Math.min(delay * 1.5, 1000)\n } else {\n throw err\n }\n }\n }\n throw new Error(`Failed to connect to Hub within ${timeout}ms.`)\n}\n\nfunction startHub(): ChildProcess {\n log.info('Spawning new Hub process...')\n return spawn(process.execPath, [HUB_ENTRY], {\n detached: true,\n stdio: 'ignore'\n })\n}\n\nasync function tryBecomeLeaderAndStartHub(): Promise<Socket> {\n let releaseLock: (() => Promise<void>) | undefined\n try {\n releaseLock = await lockfile.lock(LOCK_PATH, {\n retries: { retries: 5, factor: 1.2, minTimeout: 50 },\n stale: 15000\n })\n } catch {\n log.info('Another process is starting the Hub. Waiting...')\n return connectWithRetry(HUB_STARTUP_TIMEOUT)\n }\n\n log.info('Acquired lock. Starting Hub as the leader...')\n let child: ChildProcess | null = null\n try {\n try {\n return await connectHub()\n } catch {\n // If the Hub is not running, we proceed to start it.\n log.info('Hub not running. Proceeding to start it...')\n }\n child = startHub()\n child.on('error', (err) => log.error({ err }, 'Hub child process error.'))\n const socket = await connectWithRetry(HUB_STARTUP_TIMEOUT)\n child.unref()\n return socket\n } catch (err: unknown) {\n log.error({ err }, 'Failed to start or connect to the Hub.')\n if (child && !child.killed) {\n log.warn(`Killing stale Hub process (PID: ${child.pid})...`)\n child.kill('SIGTERM')\n }\n throw err\n } finally {\n if (releaseLock) await releaseLock()\n }\n}\n\nasync function main() {\n log.info({ version: PACKAGE_VERSION }, 'TemPad MCP Client starting...')\n\n while (true) {\n try {\n const socket = await connectHub().catch(() => {\n log.info('Hub not running. Initiating startup sequence...')\n return tryBecomeLeaderAndStartHub()\n })\n await bridge(socket)\n log.info('Bridge disconnected. Restarting connection process...')\n } catch (err: unknown) {\n log.error(\n { err },\n `Connection attempt failed. Retrying in ${FAILED_RESTART_DELAY / 1000}s...`\n )\n await new Promise((r) => setTimeout(r, FAILED_RESTART_DELAY))\n }\n }\n}\n\nmain()\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport type { ChildProcess } from 'node:child_process'\nimport type { Socket } from 'node:net'\n\nimport { spawn } from 'node:child_process'\nimport { connect } from 'node:net'\nimport { join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport lockfile from 'proper-lockfile'\n\nimport { PACKAGE_VERSION, log, LOCK_PATH, RUNTIME_DIR, SOCK_PATH, ensureDir } from './shared'\n\nlet activeSocket: Socket | null = null\nlet shuttingDown = false\n\nfunction closeActiveSocket() {\n if (!activeSocket) return\n try {\n activeSocket.end()\n } catch {\n // ignore\n }\n try {\n activeSocket.destroy()\n } catch {\n // ignore\n }\n activeSocket = null\n}\n\nfunction shutdownCli(reason: string) {\n if (shuttingDown) return\n shuttingDown = true\n log.info(`${reason} Shutting down CLI.`)\n closeActiveSocket()\n process.exit(0)\n}\n\nprocess.on('SIGINT', () => shutdownCli('SIGINT received.'))\nprocess.on('SIGTERM', () => shutdownCli('SIGTERM received.'))\n\nconst HUB_STARTUP_TIMEOUT = 5000\nconst CONNECT_RETRY_DELAY = 200\nconst FAILED_RESTART_DELAY = 5000\nconst HERE = fileURLToPath(new URL('.', import.meta.url))\nconst HUB_ENTRY = join(HERE, 'hub.mjs')\n\nensureDir(RUNTIME_DIR)\n\nfunction bridge(socket: Socket): Promise<void> {\n return new Promise((resolve) => {\n log.info('Bridge established with Hub. Forwarding I/O.')\n activeSocket = socket\n\n const onStdinEnd = () => {\n shutdownCli('Consumer stream ended.')\n }\n process.stdin.once('end', onStdinEnd)\n\n const onSocketClose = () => {\n log.warn('Connection to Hub lost. Attempting to reconnect...')\n activeSocket = null\n process.stdin.removeListener('end', onStdinEnd)\n process.stdin.unpipe(socket)\n socket.unpipe(process.stdout)\n socket.removeAllListeners()\n resolve()\n }\n socket.once('close', onSocketClose)\n socket.on('error', (err) => log.warn({ err }, 'Socket error occurred.'))\n\n // The `{ end: false }` option prevents stdin from closing the socket.\n process.stdin.pipe(socket, { end: false }).pipe(process.stdout)\n })\n}\n\nfunction connectHub(): Promise<Socket> {\n return new Promise((resolve, reject) => {\n const socket = connect(SOCK_PATH)\n socket.on('connect', () => {\n socket.removeAllListeners('error')\n resolve(socket)\n })\n socket.on('error', reject)\n })\n}\n\nasync function connectWithRetry(timeout: number): Promise<Socket> {\n const startTime = Date.now()\n let delay = CONNECT_RETRY_DELAY\n while (Date.now() - startTime < timeout) {\n try {\n return await connectHub()\n } catch (err: unknown) {\n if (\n err &&\n typeof err === 'object' &&\n 'code' in err &&\n (err.code === 'ENOENT' || err.code === 'ECONNREFUSED')\n ) {\n const remainingTime = timeout - (Date.now() - startTime)\n const waitTime = Math.min(delay, remainingTime)\n if (waitTime <= 0) break\n await new Promise((r) => setTimeout(r, waitTime))\n delay = Math.min(delay * 1.5, 1000)\n } else {\n throw err\n }\n }\n }\n throw new Error(`Failed to connect to Hub within ${timeout}ms.`)\n}\n\nfunction startHub(): ChildProcess {\n log.info('Spawning new Hub process...')\n return spawn(process.execPath, [HUB_ENTRY], {\n detached: true,\n stdio: 'ignore'\n })\n}\n\nasync function tryBecomeLeaderAndStartHub(): Promise<Socket> {\n let releaseLock: (() => Promise<void>) | undefined\n try {\n releaseLock = await lockfile.lock(LOCK_PATH, {\n retries: { retries: 5, factor: 1.2, minTimeout: 50 },\n stale: 15000\n })\n } catch {\n log.info('Another process is starting the Hub. Waiting...')\n return connectWithRetry(HUB_STARTUP_TIMEOUT)\n }\n\n log.info('Acquired lock. Starting Hub as the leader...')\n let child: ChildProcess | null = null\n try {\n try {\n return await connectHub()\n } catch {\n // If the Hub is not running, we proceed to start it.\n log.info('Hub not running. Proceeding to start it...')\n }\n child = startHub()\n child.on('error', (err) => log.error({ err }, 'Hub child process error.'))\n const socket = await connectWithRetry(HUB_STARTUP_TIMEOUT)\n child.unref()\n return socket\n } catch (err: unknown) {\n log.error({ err }, 'Failed to start or connect to the Hub.')\n if (child && !child.killed) {\n log.warn(`Killing stale Hub process (PID: ${child.pid})...`)\n child.kill('SIGTERM')\n }\n throw err\n } finally {\n if (releaseLock) await releaseLock()\n }\n}\n\nasync function main() {\n log.info({ version: PACKAGE_VERSION }, 'TemPad MCP Client starting...')\n\n while (true) {\n try {\n const socket = await connectHub().catch(() => {\n log.info('Hub not running. Initiating startup sequence...')\n return tryBecomeLeaderAndStartHub()\n })\n await bridge(socket)\n log.info('Bridge disconnected. Restarting connection process...')\n } catch (err: unknown) {\n log.error(\n { err },\n `Connection attempt failed. Retrying in ${FAILED_RESTART_DELAY / 1000}s...`\n )\n await new Promise((r) => setTimeout(r, FAILED_RESTART_DELAY))\n }\n }\n}\n\nmain()\n"],"mappings":";;;;;;;;AAaA,IAAI,eAA8B;AAClC,IAAI,eAAe;AAEnB,SAAS,oBAAoB;AAC3B,KAAI,CAAC,aAAc;AACnB,KAAI;AACF,eAAa,KAAK;SACZ;AAGR,KAAI;AACF,eAAa,SAAS;SAChB;AAGR,gBAAe;;AAGjB,SAAS,YAAY,QAAgB;AACnC,KAAI,aAAc;AAClB,gBAAe;AACf,KAAI,KAAK,GAAG,OAAO,qBAAqB;AACxC,oBAAmB;AACnB,SAAQ,KAAK,EAAE;;AAGjB,QAAQ,GAAG,gBAAgB,YAAY,mBAAmB,CAAC;AAC3D,QAAQ,GAAG,iBAAiB,YAAY,oBAAoB,CAAC;AAE7D,MAAM,sBAAsB;AAC5B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAE7B,MAAM,YAAY,KADL,cAAc,IAAI,IAAI,KAAK,OAAO,KAAK,IAAI,CAAC,EAC5B,UAAU;AAEvC,UAAU,YAAY;AAEtB,SAAS,OAAO,QAA+B;AAC7C,QAAO,IAAI,SAAS,YAAY;AAC9B,MAAI,KAAK,+CAA+C;AACxD,iBAAe;EAEf,MAAM,mBAAmB;AACvB,eAAY,yBAAyB;;AAEvC,UAAQ,MAAM,KAAK,OAAO,WAAW;EAErC,MAAM,sBAAsB;AAC1B,OAAI,KAAK,qDAAqD;AAC9D,kBAAe;AACf,WAAQ,MAAM,eAAe,OAAO,WAAW;AAC/C,WAAQ,MAAM,OAAO,OAAO;AAC5B,UAAO,OAAO,QAAQ,OAAO;AAC7B,UAAO,oBAAoB;AAC3B,YAAS;;AAEX,SAAO,KAAK,SAAS,cAAc;AACnC,SAAO,GAAG,UAAU,QAAQ,IAAI,KAAK,EAAE,KAAK,EAAE,yBAAyB,CAAC;AAGxE,UAAQ,MAAM,KAAK,QAAQ,EAAE,KAAK,OAAO,CAAC,CAAC,KAAK,QAAQ,OAAO;GAC/D;;AAGJ,SAAS,aAA8B;AACrC,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAS,QAAQ,UAAU;AACjC,SAAO,GAAG,iBAAiB;AACzB,UAAO,mBAAmB,QAAQ;AAClC,WAAQ,OAAO;IACf;AACF,SAAO,GAAG,SAAS,OAAO;GAC1B;;AAGJ,eAAe,iBAAiB,SAAkC;CAChE,MAAM,YAAY,KAAK,KAAK;CAC5B,IAAI,QAAQ;AACZ,QAAO,KAAK,KAAK,GAAG,YAAY,QAC9B,KAAI;AACF,SAAO,MAAM,YAAY;UAClB,KAAc;AACrB,MACE,OACA,OAAO,QAAQ,YACf,UAAU,QACT,IAAI,SAAS,YAAY,IAAI,SAAS,iBACvC;GACA,MAAM,gBAAgB,WAAW,KAAK,KAAK,GAAG;GAC9C,MAAM,WAAW,KAAK,IAAI,OAAO,cAAc;AAC/C,OAAI,YAAY,EAAG;AACnB,SAAM,IAAI,SAAS,MAAM,WAAW,GAAG,SAAS,CAAC;AACjD,WAAQ,KAAK,IAAI,QAAQ,KAAK,IAAK;QAEnC,OAAM;;AAIZ,OAAM,IAAI,MAAM,mCAAmC,QAAQ,KAAK;;AAGlE,SAAS,WAAyB;AAChC,KAAI,KAAK,8BAA8B;AACvC,QAAO,MAAM,QAAQ,UAAU,CAAC,UAAU,EAAE;EAC1C,UAAU;EACV,OAAO;EACR,CAAC;;AAGJ,eAAe,6BAA8C;CAC3D,IAAI;AACJ,KAAI;AACF,gBAAc,MAAM,SAAS,KAAK,WAAW;GAC3C,SAAS;IAAE,SAAS;IAAG,QAAQ;IAAK,YAAY;IAAI;GACpD,OAAO;GACR,CAAC;SACI;AACN,MAAI,KAAK,kDAAkD;AAC3D,SAAO,iBAAiB,oBAAoB;;AAG9C,KAAI,KAAK,+CAA+C;CACxD,IAAI,QAA6B;AACjC,KAAI;AACF,MAAI;AACF,UAAO,MAAM,YAAY;UACnB;AAEN,OAAI,KAAK,6CAA6C;;AAExD,UAAQ,UAAU;AAClB,QAAM,GAAG,UAAU,QAAQ,IAAI,MAAM,EAAE,KAAK,EAAE,2BAA2B,CAAC;EAC1E,MAAM,SAAS,MAAM,iBAAiB,oBAAoB;AAC1D,QAAM,OAAO;AACb,SAAO;UACA,KAAc;AACrB,MAAI,MAAM,EAAE,KAAK,EAAE,yCAAyC;AAC5D,MAAI,SAAS,CAAC,MAAM,QAAQ;AAC1B,OAAI,KAAK,mCAAmC,MAAM,IAAI,MAAM;AAC5D,SAAM,KAAK,UAAU;;AAEvB,QAAM;WACE;AACR,MAAI,YAAa,OAAM,aAAa;;;AAIxC,eAAe,OAAO;AACpB,KAAI,KAAK,EAAE,SAAS,iBAAiB,EAAE,gCAAgC;AAEvE,QAAO,KACL,KAAI;AAKF,QAAM,OAJS,MAAM,YAAY,CAAC,YAAY;AAC5C,OAAI,KAAK,kDAAkD;AAC3D,UAAO,4BAA4B;IACnC,CACkB;AACpB,MAAI,KAAK,wDAAwD;UAC1D,KAAc;AACrB,MAAI,MACF,EAAE,KAAK,EACP,0CAA0C,uBAAuB,IAAK,MACvE;AACD,QAAM,IAAI,SAAS,MAAM,WAAW,GAAG,qBAAqB,CAAC;;;AAKnE,MAAM"}
|
package/dist/hub.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as SOCK_PATH, c as log, i as RUNTIME_DIR, o as ensureDir, r as PACKAGE_VERSION, s as ensureFile, t as ASSET_DIR } from "./shared-
|
|
1
|
+
import { a as SOCK_PATH, c as log, i as RUNTIME_DIR, o as ensureDir, r as PACKAGE_VERSION, s as ensureFile, t as ASSET_DIR } from "./shared-C9V2G_pC.mjs";
|
|
2
2
|
import { createServer } from "node:net";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { URL } from "node:url";
|
|
@@ -11,7 +11,6 @@ import { WebSocketServer } from "ws";
|
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { createServer as createServer$1 } from "node:http";
|
|
13
13
|
import { Transform, pipeline } from "node:stream";
|
|
14
|
-
|
|
15
14
|
//#region ../shared/dist/index.js
|
|
16
15
|
const MCP_PORT_CANDIDATES = [
|
|
17
16
|
6220,
|
|
@@ -19,12 +18,12 @@ const MCP_PORT_CANDIDATES = [
|
|
|
19
18
|
8127
|
|
20
19
|
];
|
|
21
20
|
const MCP_MAX_PAYLOAD_BYTES = 4 * 1024 * 1024;
|
|
21
|
+
const MCP_TOOL_INLINE_BUDGET_BYTES = 64 * 1024;
|
|
22
22
|
const MCP_TOOL_TIMEOUT_MS = 15e3;
|
|
23
23
|
const MCP_AUTO_ACTIVATE_GRACE_MS = 1500;
|
|
24
24
|
const MCP_MAX_ASSET_BYTES = 8 * 1024 * 1024;
|
|
25
25
|
const MCP_ASSET_TTL_MS = 720 * 60 * 60 * 1e3;
|
|
26
|
-
const
|
|
27
|
-
const MCP_HASH_PATTERN = new RegExp(`^[a-f0-9]{${MCP_HASH_HEX_LENGTH}}$`, "i");
|
|
26
|
+
const MCP_HASH_PATTERN = new RegExp(`^[a-f0-9]{8}$`, "i");
|
|
28
27
|
const TEMPAD_MCP_ERROR_CODES = {
|
|
29
28
|
NO_ACTIVE_EXTENSION: "NO_ACTIVE_EXTENSION",
|
|
30
29
|
EXTENSION_TIMEOUT: "EXTENSION_TIMEOUT",
|
|
@@ -34,6 +33,107 @@ const TEMPAD_MCP_ERROR_CODES = {
|
|
|
34
33
|
ASSET_SERVER_NOT_CONFIGURED: "ASSET_SERVER_NOT_CONFIGURED",
|
|
35
34
|
TRANSPORT_NOT_CONNECTED: "TRANSPORT_NOT_CONNECTED"
|
|
36
35
|
};
|
|
36
|
+
const SERVER_NAME = "tempad-dev";
|
|
37
|
+
const SERVER_COMMAND = "npx";
|
|
38
|
+
const SERVER_ARGS = ["-y", "@tempad-dev/mcp@latest"];
|
|
39
|
+
const stdioConfig = {
|
|
40
|
+
type: "stdio",
|
|
41
|
+
command: SERVER_COMMAND,
|
|
42
|
+
args: [...SERVER_ARGS]
|
|
43
|
+
};
|
|
44
|
+
const commandConfig = {
|
|
45
|
+
command: SERVER_COMMAND,
|
|
46
|
+
args: [...SERVER_ARGS]
|
|
47
|
+
};
|
|
48
|
+
function toBase64(input) {
|
|
49
|
+
if (typeof globalThis.btoa === "function") return globalThis.btoa(input);
|
|
50
|
+
const bufferLike = globalThis.Buffer;
|
|
51
|
+
if (bufferLike) return bufferLike.from(input, "utf8").toString("base64");
|
|
52
|
+
throw new Error("Base64 encoding not supported in this environment.");
|
|
53
|
+
}
|
|
54
|
+
function buildVscodeDeepLink() {
|
|
55
|
+
return `vscode:mcp/install?${encodeURIComponent(JSON.stringify({
|
|
56
|
+
name: SERVER_NAME,
|
|
57
|
+
...stdioConfig
|
|
58
|
+
}))}`;
|
|
59
|
+
}
|
|
60
|
+
function buildCursorConfigBase64() {
|
|
61
|
+
return encodeURIComponent(toBase64(JSON.stringify(commandConfig)));
|
|
62
|
+
}
|
|
63
|
+
function buildCursorDeepLink() {
|
|
64
|
+
return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(SERVER_NAME)}&config=${buildCursorConfigBase64()}`;
|
|
65
|
+
}
|
|
66
|
+
function buildTraeDeepLink(protocol) {
|
|
67
|
+
return `${protocol}://trae.ai-ide/mcp-import?type=stdio&name=${encodeURIComponent(SERVER_NAME)}&config=${buildCursorConfigBase64()}`;
|
|
68
|
+
}
|
|
69
|
+
function buildWindsurfConfigSnippet() {
|
|
70
|
+
return JSON.stringify({ mcpServers: { [SERVER_NAME]: commandConfig } }, null, 2);
|
|
71
|
+
}
|
|
72
|
+
function buildCodexConfigSnippet() {
|
|
73
|
+
return [
|
|
74
|
+
`[mcp_servers.${SERVER_NAME}]`,
|
|
75
|
+
`command = ${JSON.stringify(SERVER_COMMAND)}`,
|
|
76
|
+
`args = [${SERVER_ARGS.map((arg) => JSON.stringify(arg)).join(", ")}]`
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
function buildCliCommand(prefix) {
|
|
80
|
+
const args = `${SERVER_COMMAND} ${SERVER_ARGS.join(" ")}`;
|
|
81
|
+
if (prefix === "claude") return `claude mcp add --transport stdio "${SERVER_NAME}" -- ${args}`;
|
|
82
|
+
return `codex mcp add "${SERVER_NAME}" -- ${args}`;
|
|
83
|
+
}
|
|
84
|
+
[...SERVER_ARGS];
|
|
85
|
+
JSON.stringify({ [SERVER_NAME]: commandConfig }, null, 2);
|
|
86
|
+
const MCP_CLIENTS_BY_ID = {
|
|
87
|
+
vscode: {
|
|
88
|
+
id: "vscode",
|
|
89
|
+
name: "VS Code",
|
|
90
|
+
brandColor: "#0098ff",
|
|
91
|
+
supportsDeepLink: true,
|
|
92
|
+
deepLink: buildVscodeDeepLink()
|
|
93
|
+
},
|
|
94
|
+
cursor: {
|
|
95
|
+
id: "cursor",
|
|
96
|
+
name: "Cursor",
|
|
97
|
+
brandColor: ["#000", "#fff"],
|
|
98
|
+
supportsDeepLink: true,
|
|
99
|
+
deepLink: buildCursorDeepLink()
|
|
100
|
+
},
|
|
101
|
+
windsurf: {
|
|
102
|
+
id: "windsurf",
|
|
103
|
+
name: "Windsurf",
|
|
104
|
+
brandColor: ["#0B100F", "#F0F3F2"],
|
|
105
|
+
supportsDeepLink: false,
|
|
106
|
+
copyText: buildWindsurfConfigSnippet(),
|
|
107
|
+
copyKind: "config"
|
|
108
|
+
},
|
|
109
|
+
claude: {
|
|
110
|
+
id: "claude",
|
|
111
|
+
name: "Claude Code",
|
|
112
|
+
brandColor: "#D97757",
|
|
113
|
+
supportsDeepLink: false,
|
|
114
|
+
copyText: buildCliCommand("claude"),
|
|
115
|
+
copyKind: "command"
|
|
116
|
+
},
|
|
117
|
+
codex: {
|
|
118
|
+
id: "codex",
|
|
119
|
+
name: "Codex CLI",
|
|
120
|
+
brandColor: ["#0d0d0d", "#fff"],
|
|
121
|
+
supportsDeepLink: false,
|
|
122
|
+
copyText: buildCliCommand("codex"),
|
|
123
|
+
copyKind: "command",
|
|
124
|
+
alternateCopyText: buildCodexConfigSnippet(),
|
|
125
|
+
alternateCopyKind: "config"
|
|
126
|
+
},
|
|
127
|
+
trae: {
|
|
128
|
+
id: "trae",
|
|
129
|
+
name: "TRAE",
|
|
130
|
+
brandColor: ["#0fdc78", "#32f08c"],
|
|
131
|
+
supportsDeepLink: true,
|
|
132
|
+
deepLink: buildTraeDeepLink("trae"),
|
|
133
|
+
fallbackDeepLink: buildTraeDeepLink("trae-cn")
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
MCP_CLIENTS_BY_ID.vscode, MCP_CLIENTS_BY_ID.cursor, MCP_CLIENTS_BY_ID.windsurf, MCP_CLIENTS_BY_ID.claude, MCP_CLIENTS_BY_ID.codex, MCP_CLIENTS_BY_ID.trae;
|
|
37
137
|
const RegisteredMessageSchema = z.object({
|
|
38
138
|
type: z.literal("registered"),
|
|
39
139
|
id: z.string()
|
|
@@ -54,7 +154,7 @@ const ToolCallMessageSchema = z.object({
|
|
|
54
154
|
id: z.string(),
|
|
55
155
|
payload: ToolCallPayloadSchema
|
|
56
156
|
});
|
|
57
|
-
|
|
157
|
+
z.discriminatedUnion("type", [
|
|
58
158
|
RegisteredMessageSchema,
|
|
59
159
|
StateMessageSchema,
|
|
60
160
|
ToolCallMessageSchema
|
|
@@ -67,6 +167,78 @@ const ToolResultMessageSchema = z.object({
|
|
|
67
167
|
error: z.unknown().optional()
|
|
68
168
|
});
|
|
69
169
|
const MessageFromExtensionSchema = z.discriminatedUnion("type", [ActivateMessageSchema, ToolResultMessageSchema]);
|
|
170
|
+
const ENCODER = new TextEncoder();
|
|
171
|
+
function utf8Bytes(value) {
|
|
172
|
+
return ENCODER.encode(serializeUtf8Value(value)).length;
|
|
173
|
+
}
|
|
174
|
+
function measureCallToolResultBytes(result) {
|
|
175
|
+
return utf8Bytes(result);
|
|
176
|
+
}
|
|
177
|
+
function buildGetCodeToolResult(payload) {
|
|
178
|
+
const summary = [];
|
|
179
|
+
const codeSize = utf8Bytes(payload.code);
|
|
180
|
+
summary.push(`Generated \`${payload.lang}\` snippet (${formatBytes(codeSize)}).`);
|
|
181
|
+
if (payload.warnings?.length) summary.push(...payload.warnings.map((warning) => warning.message));
|
|
182
|
+
summary.push(payload.assets?.length ? `Assets attached: ${payload.assets.length}. Download bytes from each asset.url.` : "No binary assets were attached to this response.");
|
|
183
|
+
const tokenCount = payload.tokens ? Object.keys(payload.tokens).length : 0;
|
|
184
|
+
if (tokenCount) summary.push(`Token references included: ${tokenCount}.`);
|
|
185
|
+
summary.push("Read structuredContent for the full code string and metadata.");
|
|
186
|
+
return buildTextToolResult(summary.join("\n"), payload);
|
|
187
|
+
}
|
|
188
|
+
function buildGetStructureToolResult(payload) {
|
|
189
|
+
const roots = payload.roots.length;
|
|
190
|
+
const nodeCount = countOutlineNodes(payload.roots);
|
|
191
|
+
return buildTextToolResult(`${roots === 0 ? "No structure nodes were returned." : `Returned structure outline with ${formatCount(roots, "root")} and ${formatCount(nodeCount, "node")}.`}\nRead structuredContent for the full outline payload.`, payload);
|
|
192
|
+
}
|
|
193
|
+
function buildGetTokenDefsToolResult(payload) {
|
|
194
|
+
const count = Object.keys(payload).length;
|
|
195
|
+
return buildTextToolResult(`${count === 0 ? "No token definitions were resolved." : `Resolved ${formatCount(count, "token definition")}.`}\nRead structuredContent for token values and aliases.`, payload);
|
|
196
|
+
}
|
|
197
|
+
function buildGetScreenshotToolResult(payload) {
|
|
198
|
+
return buildTextToolResult(`${describeScreenshot(payload)} - Download: ${payload.asset.url}`, payload);
|
|
199
|
+
}
|
|
200
|
+
function buildGetAssetsToolResult(payload) {
|
|
201
|
+
const summary = [];
|
|
202
|
+
summary.push(payload.assets.length ? `Resolved ${formatCount(payload.assets.length, "asset")}.` : "No assets were resolved for the requested hashes.");
|
|
203
|
+
if (payload.missing.length) summary.push(`Missing: ${payload.missing.join(", ")}`);
|
|
204
|
+
summary.push("Download bytes from each asset.url.");
|
|
205
|
+
return buildTextToolResult(summary.join("\n"), payload);
|
|
206
|
+
}
|
|
207
|
+
function buildTextToolResult(text, structuredContent) {
|
|
208
|
+
return {
|
|
209
|
+
content: [{
|
|
210
|
+
type: "text",
|
|
211
|
+
text
|
|
212
|
+
}],
|
|
213
|
+
structuredContent
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function serializeUtf8Value(value) {
|
|
217
|
+
if (typeof value === "string") return value;
|
|
218
|
+
return JSON.stringify(value, null, 0) ?? "undefined";
|
|
219
|
+
}
|
|
220
|
+
function countOutlineNodes(nodes) {
|
|
221
|
+
let count = 0;
|
|
222
|
+
const stack = [...nodes];
|
|
223
|
+
while (stack.length) {
|
|
224
|
+
const current = stack.pop();
|
|
225
|
+
if (!current) continue;
|
|
226
|
+
count += 1;
|
|
227
|
+
if (current.children?.length) stack.push(...current.children);
|
|
228
|
+
}
|
|
229
|
+
return count;
|
|
230
|
+
}
|
|
231
|
+
function formatBytes(bytes) {
|
|
232
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
233
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
234
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
235
|
+
}
|
|
236
|
+
function formatCount(count, singular) {
|
|
237
|
+
return `${count} ${count === 1 ? singular : `${singular}s`}`;
|
|
238
|
+
}
|
|
239
|
+
function describeScreenshot(result) {
|
|
240
|
+
return `Screenshot ${result.width}x${result.height} @${result.scale}x (${formatBytes(result.bytes)})`;
|
|
241
|
+
}
|
|
70
242
|
const AssetDescriptorSchema = z.object({
|
|
71
243
|
hash: z.string().min(1),
|
|
72
244
|
url: z.string().url(),
|
|
@@ -80,7 +252,7 @@ const GetCodeParametersSchema = z.object({
|
|
|
80
252
|
nodeId: z.string().describe("Optional target node id; omit to use the current single selection when pulling the baseline snapshot.").optional(),
|
|
81
253
|
preferredLang: z.enum(["jsx", "vue"]).describe("Preferred output language to bias the snapshot; otherwise uses the design’s hint/detected language, then falls back to JSX.").optional(),
|
|
82
254
|
resolveTokens: z.boolean().describe("Inline token values instead of references for quick renders; default false returns token metadata so you can map into your theming system. When true, values are resolved per-node (mode-aware).").optional(),
|
|
83
|
-
vectorMode: z.enum(["smart", "snapshot"]).describe("Vector output mode. `smart` (default) emits
|
|
255
|
+
vectorMode: z.enum(["smart", "snapshot"]).describe("Vector output mode. `smart` (default) emits `<svg data-src=\"...\">` placeholders in code and preserves themeable instance color on the emitted SVG root markup for downstream adaptation; if asset upload fails after export, the tool may inline the SVG as a fallback to preserve source of truth. `snapshot` preserves vector assets for fidelity. Final vector delivery may still be adapted to the Host app’s SVG policy.").optional()
|
|
84
256
|
});
|
|
85
257
|
const GetTokenDefsParametersSchema = z.object({
|
|
86
258
|
names: z.array(z.string().regex(/^--[a-zA-Z0-9-_]+$/)).min(1).describe("Canonical token names (CSS variable form) from Object.keys(get_code.tokens) or your own list to resolve, e.g., --color-primary."),
|
|
@@ -96,10 +268,9 @@ const GetAssetsResultSchema = z.object({
|
|
|
96
268
|
assets: z.array(AssetDescriptorSchema),
|
|
97
269
|
missing: z.array(z.string().min(1))
|
|
98
270
|
});
|
|
99
|
-
|
|
100
271
|
//#endregion
|
|
101
272
|
//#region src/asset-utils.ts
|
|
102
|
-
const HASH_FILENAME_PATTERN = new RegExp(`^([a-f0-9]{
|
|
273
|
+
const HASH_FILENAME_PATTERN = new RegExp(`^([a-f0-9]{8})(?:\\.[a-z0-9]+)?$`, "i");
|
|
103
274
|
const MIME_EXTENSION_OVERRIDES = new Map([["image/jpeg", "jpg"]]);
|
|
104
275
|
const SAFE_IMAGE_EXTENSION_PATTERN = /^[a-z0-9-]+$/;
|
|
105
276
|
function normalizeMimeType(mimeType) {
|
|
@@ -126,7 +297,6 @@ function getHashFromAssetFilename(filename) {
|
|
|
126
297
|
const match = HASH_FILENAME_PATTERN.exec(filename);
|
|
127
298
|
return match ? match[1] : null;
|
|
128
299
|
}
|
|
129
|
-
|
|
130
300
|
//#endregion
|
|
131
301
|
//#region src/config.ts
|
|
132
302
|
function parsePositiveInt(envValue, fallback) {
|
|
@@ -159,11 +329,10 @@ function getMcpServerConfig() {
|
|
|
159
329
|
assetTtlMs: resolveAssetTtlMs()
|
|
160
330
|
};
|
|
161
331
|
}
|
|
162
|
-
|
|
163
332
|
//#endregion
|
|
164
333
|
//#region src/asset-http-server.ts
|
|
165
334
|
const LOOPBACK_HOST = "127.0.0.1";
|
|
166
|
-
const HASH_HEX_PATTERN = new RegExp(`^[a-f0-9]{
|
|
335
|
+
const HASH_HEX_PATTERN = new RegExp(`^[a-f0-9]{8}$`, "i");
|
|
167
336
|
const { maxAssetSizeBytes } = getMcpServerConfig();
|
|
168
337
|
function createAssetHttpServer(store) {
|
|
169
338
|
const server = createServer$1(handleRequest);
|
|
@@ -367,7 +536,7 @@ function createAssetHttpServer(store) {
|
|
|
367
536
|
}
|
|
368
537
|
return;
|
|
369
538
|
}
|
|
370
|
-
if (hasher.digest("hex").slice(0,
|
|
539
|
+
if (hasher.digest("hex").slice(0, 8) !== hash) {
|
|
371
540
|
cleanup();
|
|
372
541
|
sendError(res, 400, "Hash Mismatch");
|
|
373
542
|
return;
|
|
@@ -420,7 +589,6 @@ function createAssetHttpServer(store) {
|
|
|
420
589
|
getBaseUrl
|
|
421
590
|
};
|
|
422
591
|
}
|
|
423
|
-
|
|
424
592
|
//#endregion
|
|
425
593
|
//#region src/asset-store.ts
|
|
426
594
|
const INDEX_FILENAME = "assets.json";
|
|
@@ -581,11 +749,9 @@ function createAssetStore(options = {}) {
|
|
|
581
749
|
flush
|
|
582
750
|
};
|
|
583
751
|
}
|
|
584
|
-
|
|
585
752
|
//#endregion
|
|
586
753
|
//#region src/instructions.md?raw
|
|
587
|
-
var instructions_default = "You are connected to a Figma design file via TemPad Dev MCP.\n\nTreat tool outputs as design facts. Refactor only to match the user’s repo conventions; do not invent key style values.\n\nRules:\n\n- Never output any `data-hint-*` attributes from tool outputs (hints only).\n- If `get_code` warns `depth-cap`,
|
|
588
|
-
|
|
754
|
+
var instructions_default = "You are connected to a Figma design file via TemPad Dev MCP.\n\nTreat tool outputs as design facts. Refactor only to match the user’s repo conventions; do not invent key style values.\n\nRules:\n\n- Never output any `data-hint-*` attributes from tool outputs (hints only).\n- If `get_code` warns `depth-cap`, keep the returned parent code as composition evidence and use returned `data-hint-id` values to choose narrower `get_code` follow-ups.\n- If `get_code` warns `shell`, read the inline code comment for omitted direct child ids, then call `get_code` for those ids in order and fill the results back into the returned shell.\n- Use `get_structure` only to resolve layout/overlap uncertainty; do not derive numeric values from images.\n- Tokens: `get_code.tokens` keys are canonical names (`--...`). Multi‑mode values use `${collectionName}:${modeName}`. Nodes may hint per-node overrides via `data-hint-variable-mode=\"Collection=Mode;...\"`.\n- Vectors: `vectorMode=smart` is the default. Treat the emitted markup as the source of truth for the current response; vector code is emitted as `<svg data-src=\"...\">` placeholders, but if asset upload fails after export the tool may inline the SVG as a fallback to preserve source of truth.\n- Themeable vectors: `themeable=true` means the SVG can safely adopt one contextual color channel. In `smart` mode, that color is typically already evidenced on the emitted `svg` root markup for the placeholder. It does not mean the SVG exposes multiple independent color parameters.\n- Assets: download bytes via `asset.url`. Asset resources are not exposed via MCP `resources/read`. Use `asset.themeable` only when an SVG still needs repo asset handling after you account for the Host app's vector policy.\n";
|
|
589
755
|
//#endregion
|
|
590
756
|
//#region src/request.ts
|
|
591
757
|
const pendingCalls = /* @__PURE__ */ new Map();
|
|
@@ -653,9 +819,23 @@ function cleanupAll() {
|
|
|
653
819
|
});
|
|
654
820
|
pendingCalls.clear();
|
|
655
821
|
}
|
|
656
|
-
|
|
657
822
|
//#endregion
|
|
658
823
|
//#region src/tools.ts
|
|
824
|
+
const CONNECTIVITY_ERROR_CODES = new Set([
|
|
825
|
+
TEMPAD_MCP_ERROR_CODES.NO_ACTIVE_EXTENSION,
|
|
826
|
+
TEMPAD_MCP_ERROR_CODES.EXTENSION_TIMEOUT,
|
|
827
|
+
TEMPAD_MCP_ERROR_CODES.EXTENSION_DISCONNECTED,
|
|
828
|
+
TEMPAD_MCP_ERROR_CODES.ASSET_SERVER_NOT_CONFIGURED,
|
|
829
|
+
TEMPAD_MCP_ERROR_CODES.TRANSPORT_NOT_CONNECTED
|
|
830
|
+
]);
|
|
831
|
+
const SELECTION_ERROR_CODES = new Set([TEMPAD_MCP_ERROR_CODES.INVALID_SELECTION, TEMPAD_MCP_ERROR_CODES.NODE_NOT_VISIBLE]);
|
|
832
|
+
const CONNECTIVITY_TROUBLESHOOTING_LINES = [
|
|
833
|
+
"Troubleshooting:",
|
|
834
|
+
"- In Figma, open TemPad Dev panel and enable MCP (Preferences → MCP server).",
|
|
835
|
+
"- If multiple Figma tabs are open, click the MCP badge to activate this tab.",
|
|
836
|
+
"- Keep the Figma tab active/foreground while running MCP tools."
|
|
837
|
+
];
|
|
838
|
+
const SELECTION_TROUBLESHOOTING_LINE = "Tip: Select exactly one visible node, or pass nodeId.";
|
|
659
839
|
function getRecordProperty$1(record, key) {
|
|
660
840
|
if (!record || typeof record !== "object") return;
|
|
661
841
|
return Reflect.get(record, key);
|
|
@@ -669,7 +849,7 @@ function hubTool(definition) {
|
|
|
669
849
|
const TOOL_DEFS = [
|
|
670
850
|
extTool({
|
|
671
851
|
name: "get_code",
|
|
672
|
-
description: "High-fidelity code snapshot for nodeId/current single selection (omit nodeId to use selection): JSX/Vue markup + Tailwind-like classes, plus assets/tokens metadata and codegen config. `vectorMode=smart` (default) emits
|
|
852
|
+
description: "High-fidelity code snapshot for nodeId/current single selection (omit nodeId to use selection): JSX/Vue markup + Tailwind-like classes, plus assets/tokens metadata and codegen config. `vectorMode=smart` (default) emits `<svg data-src=\"...\">` placeholders in code and preserves themeable instance color on the emitted SVG root markup for downstream adaptation; if asset upload fails after export, the tool may inline the SVG as a fallback to preserve source of truth. `vectorMode=snapshot` preserves vector assets for fidelity. Host apps should still refactor vector delivery to repo policy where needed (existing icon/component primitives, import-time SVG transforms, inline SVG, or asset-backed SVG usage). SVG asset metadata may include `themeable=true`, meaning the exported asset can safely adopt one contextual color channel. Start here, then refactor into repo conventions while preserving values/intent; strip any data-hint-* attributes (hints only). If warnings include depth-cap, use returned data-hint-id values to continue with narrower get_code calls. If warnings include shell, read the inline comment for omitted direct child ids and fetch them in order. If warnings include auto-layout (inferred), use get_structure to confirm hierarchy/overlap (do not derive numeric values from pixels). Tokens are keyed by canonical names like `--color-primary` (multi-mode keys use `${collection}:${mode}`; node overrides may appear as data-hint-variable-mode).",
|
|
673
853
|
parameters: GetCodeParametersSchema,
|
|
674
854
|
target: "extension",
|
|
675
855
|
format: createCodeToolResponse
|
|
@@ -679,6 +859,7 @@ const TOOL_DEFS = [
|
|
|
679
859
|
description: "Resolve canonical token names to literal values (optionally including all modes) for tokens referenced by get_code.",
|
|
680
860
|
parameters: GetTokenDefsParametersSchema,
|
|
681
861
|
target: "extension",
|
|
862
|
+
format: createTokenDefsToolResponse,
|
|
682
863
|
exposed: false
|
|
683
864
|
}),
|
|
684
865
|
extTool({
|
|
@@ -693,7 +874,8 @@ const TOOL_DEFS = [
|
|
|
693
874
|
name: "get_structure",
|
|
694
875
|
description: "Get a compact structural + geometry outline for nodeId/current single selection to understand hierarchy and layout intent.",
|
|
695
876
|
parameters: GetStructureParametersSchema,
|
|
696
|
-
target: "extension"
|
|
877
|
+
target: "extension",
|
|
878
|
+
format: createStructureToolResponse
|
|
697
879
|
}),
|
|
698
880
|
hubTool({
|
|
699
881
|
name: "get_assets",
|
|
@@ -726,53 +908,37 @@ function createToolErrorResponse(toolName, error) {
|
|
|
726
908
|
isError: true,
|
|
727
909
|
content: [{
|
|
728
910
|
type: "text",
|
|
729
|
-
text: `Tool "${toolName}" failed${code ? ` [${code}]` : ""}: ${message}${(
|
|
730
|
-
const help = [];
|
|
731
|
-
if (code === TEMPAD_MCP_ERROR_CODES.NO_ACTIVE_EXTENSION || code === TEMPAD_MCP_ERROR_CODES.EXTENSION_TIMEOUT || code === TEMPAD_MCP_ERROR_CODES.EXTENSION_DISCONNECTED || code === TEMPAD_MCP_ERROR_CODES.ASSET_SERVER_NOT_CONFIGURED || code === TEMPAD_MCP_ERROR_CODES.TRANSPORT_NOT_CONNECTED || /no active tempad dev extension/i.test(message) || /asset server url is not configured/i.test(message) || /mcp transport is not connected/i.test(message) || /websocket/i.test(message)) help.push("Troubleshooting:", "- In Figma, open TemPad Dev panel and enable MCP (Preferences → MCP server).", "- If multiple Figma tabs are open, click the MCP badge to activate this tab.", "- Keep the Figma tab active/foreground while running MCP tools.");
|
|
732
|
-
if (code === TEMPAD_MCP_ERROR_CODES.INVALID_SELECTION || code === TEMPAD_MCP_ERROR_CODES.NODE_NOT_VISIBLE || /select exactly one visible node/i.test(message) || /no visible node found/i.test(message)) help.push("Tip: Select exactly one visible node, or pass nodeId.");
|
|
733
|
-
return help.length ? `\n\n${help.join("\n")}` : "";
|
|
734
|
-
})()}`
|
|
911
|
+
text: `Tool "${toolName}" failed${code ? ` [${code}]` : ""}: ${message}${buildTroubleshootingText(code, message)}`
|
|
735
912
|
}]
|
|
736
913
|
};
|
|
737
914
|
}
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
if (
|
|
741
|
-
|
|
915
|
+
function buildTroubleshootingText(code, message) {
|
|
916
|
+
const help = [];
|
|
917
|
+
if (isConnectivityToolError(code, message)) help.push(...CONNECTIVITY_TROUBLESHOOTING_LINES);
|
|
918
|
+
if (isSelectionToolError(code, message)) help.push(SELECTION_TROUBLESHOOTING_LINE);
|
|
919
|
+
return help.length ? `\n\n${help.join("\n")}` : "";
|
|
920
|
+
}
|
|
921
|
+
function isConnectivityToolError(code, message) {
|
|
922
|
+
return (code ? CONNECTIVITY_ERROR_CODES.has(code) : false) || /no active tempad dev extension/i.test(message) || /asset server url is not configured/i.test(message) || /mcp transport is not connected/i.test(message) || /websocket/i.test(message);
|
|
923
|
+
}
|
|
924
|
+
function isSelectionToolError(code, message) {
|
|
925
|
+
return (code ? SELECTION_ERROR_CODES.has(code) : false) || /select exactly one visible node/i.test(message) || /no visible node found/i.test(message);
|
|
742
926
|
}
|
|
743
927
|
function createCodeToolResponse(payload) {
|
|
744
928
|
if (!isCodeResult(payload)) throw new Error("Invalid get_code payload received from extension.");
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if (payload.
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if (tokenCount) summary.push(`Token references included: ${tokenCount}.`);
|
|
755
|
-
summary.push("Read structuredContent for the full code string and asset metadata.");
|
|
756
|
-
return {
|
|
757
|
-
content: [{
|
|
758
|
-
type: "text",
|
|
759
|
-
text: summary.join("\n")
|
|
760
|
-
}],
|
|
761
|
-
structuredContent: payload
|
|
762
|
-
};
|
|
929
|
+
return toCallToolResult(buildGetCodeToolResult(payload));
|
|
930
|
+
}
|
|
931
|
+
function createStructureToolResponse(payload) {
|
|
932
|
+
if (!isStructureResult(payload)) throw new Error("Invalid get_structure payload received from extension.");
|
|
933
|
+
return toCallToolResult(buildGetStructureToolResult(payload));
|
|
934
|
+
}
|
|
935
|
+
function createTokenDefsToolResponse(payload) {
|
|
936
|
+
if (!isTokenDefsResult(payload)) throw new Error("Invalid get_token_defs payload received from extension.");
|
|
937
|
+
return toCallToolResult(buildGetTokenDefsToolResult(payload));
|
|
763
938
|
}
|
|
764
939
|
function createScreenshotToolResponse(payload) {
|
|
765
940
|
if (!isScreenshotResult(payload)) throw new Error("Invalid get_screenshot payload received from extension.");
|
|
766
|
-
return
|
|
767
|
-
content: [{
|
|
768
|
-
type: "text",
|
|
769
|
-
text: `${describeScreenshot(payload)} - Download: ${payload.asset.url}`
|
|
770
|
-
}],
|
|
771
|
-
structuredContent: payload
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
function describeScreenshot(result) {
|
|
775
|
-
return `Screenshot ${result.width}x${result.height} @${result.scale}x (${formatBytes(result.bytes)})`;
|
|
941
|
+
return toCallToolResult(buildGetScreenshotToolResult(payload));
|
|
776
942
|
}
|
|
777
943
|
function isScreenshotResult(payload) {
|
|
778
944
|
if (typeof payload !== "object" || !payload) return false;
|
|
@@ -784,6 +950,21 @@ function isCodeResult(payload) {
|
|
|
784
950
|
const candidate = payload;
|
|
785
951
|
return typeof candidate.code === "string" && typeof candidate.lang === "string" && (candidate.assets === void 0 || Array.isArray(candidate.assets));
|
|
786
952
|
}
|
|
953
|
+
function isStructureResult(payload) {
|
|
954
|
+
if (typeof payload !== "object" || !payload) return false;
|
|
955
|
+
const candidate = payload;
|
|
956
|
+
return Array.isArray(candidate.roots);
|
|
957
|
+
}
|
|
958
|
+
function isTokenDefsResult(payload) {
|
|
959
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
960
|
+
for (const value of Object.values(payload)) {
|
|
961
|
+
if (!value || typeof value !== "object") return false;
|
|
962
|
+
const token = value;
|
|
963
|
+
if (typeof token.kind !== "string") return false;
|
|
964
|
+
if (token.value === void 0) return false;
|
|
965
|
+
}
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
787
968
|
function coercePayloadToToolResponse(payload) {
|
|
788
969
|
if (payload && typeof payload === "object" && Array.isArray(payload.content)) return payload;
|
|
789
970
|
return { content: [{
|
|
@@ -791,7 +972,31 @@ function coercePayloadToToolResponse(payload) {
|
|
|
791
972
|
text: typeof payload === "string" ? payload : JSON.stringify(payload, null, 2)
|
|
792
973
|
}] };
|
|
793
974
|
}
|
|
794
|
-
|
|
975
|
+
function createAssetsToolResponse(payload) {
|
|
976
|
+
return toCallToolResult(buildGetAssetsToolResult(payload));
|
|
977
|
+
}
|
|
978
|
+
function createInlineBudgetExceededToolResponse(toolName, actualBytes) {
|
|
979
|
+
return {
|
|
980
|
+
isError: true,
|
|
981
|
+
content: [{
|
|
982
|
+
type: "text",
|
|
983
|
+
text: `Tool "${toolName}" exceeded the 64 KiB inline budget (${actualBytes} UTF-8 bytes > ${MCP_TOOL_INLINE_BUDGET_BYTES}). ${getBudgetRetryGuidance(toolName)}`
|
|
984
|
+
}]
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function toCallToolResult(result) {
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
function getBudgetRetryGuidance(toolName) {
|
|
991
|
+
switch (toolName) {
|
|
992
|
+
case "get_code": return "Reduce selection size or request a smaller nodeId subtree and retry.";
|
|
993
|
+
case "get_structure": return "Reduce selection size or pass a smaller depth and retry.";
|
|
994
|
+
case "get_token_defs": return "Reduce requested names or split them into smaller batches and retry.";
|
|
995
|
+
case "get_screenshot": return "Reduce selection size or scale and retry.";
|
|
996
|
+
case "get_assets": return "Request fewer hashes in a single call and retry.";
|
|
997
|
+
default: return "Retry with a narrower request.";
|
|
998
|
+
}
|
|
999
|
+
}
|
|
795
1000
|
//#endregion
|
|
796
1001
|
//#region src/hub.ts
|
|
797
1002
|
const SHUTDOWN_TIMEOUT = 2e3;
|
|
@@ -903,7 +1108,7 @@ function createMcpServer() {
|
|
|
903
1108
|
const mcp = new McpServer({
|
|
904
1109
|
name: "tempad-dev-mcp",
|
|
905
1110
|
version: PACKAGE_VERSION
|
|
906
|
-
},
|
|
1111
|
+
}, { instructions: instructions_default });
|
|
907
1112
|
const registered = [];
|
|
908
1113
|
for (const tool of TOOL_DEFINITIONS) {
|
|
909
1114
|
if ("exposed" in tool && tool.exposed === false) continue;
|
|
@@ -983,18 +1188,30 @@ function registerLocalTool(mcp, tool) {
|
|
|
983
1188
|
registerToolFn(tool.name, registrationOptions, registerHandler);
|
|
984
1189
|
}
|
|
985
1190
|
function createToolResponse(toolName, payload) {
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1191
|
+
const rawResult = (() => {
|
|
1192
|
+
const definition = getToolDefinition(toolName);
|
|
1193
|
+
if (definition && hasFormatter(definition)) try {
|
|
1194
|
+
const formatter = definition.format;
|
|
1195
|
+
return formatter(payload);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
log.warn({
|
|
1198
|
+
tool: toolName,
|
|
1199
|
+
error
|
|
1200
|
+
}, "Failed to format tool result; returning raw payload.");
|
|
1201
|
+
return coercePayloadToToolResponse(payload);
|
|
1202
|
+
}
|
|
1203
|
+
return coercePayloadToToolResponse(payload);
|
|
1204
|
+
})();
|
|
1205
|
+
const resultBytes = measureCallToolResultBytes(rawResult);
|
|
1206
|
+
if (resultBytes > 65536) {
|
|
991
1207
|
log.warn({
|
|
992
1208
|
tool: toolName,
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1209
|
+
resultBytes,
|
|
1210
|
+
inlineBudgetBytes: MCP_TOOL_INLINE_BUDGET_BYTES
|
|
1211
|
+
}, "Tool result exceeded inline budget; returning compact error response.");
|
|
1212
|
+
return createInlineBudgetExceededToolResponse(toolName, resultBytes);
|
|
996
1213
|
}
|
|
997
|
-
return
|
|
1214
|
+
return rawResult;
|
|
998
1215
|
}
|
|
999
1216
|
async function handleGetAssets({ hashes }) {
|
|
1000
1217
|
if (hashes.length > 100) throw new Error("Too many hashes requested. Limit is 100.");
|
|
@@ -1005,21 +1222,10 @@ async function handleGetAssets({ hashes }) {
|
|
|
1005
1222
|
return false;
|
|
1006
1223
|
});
|
|
1007
1224
|
const found = new Set(records.map((record) => record.hash));
|
|
1008
|
-
|
|
1225
|
+
return createAssetsToolResponse(GetAssetsResultSchema.parse({
|
|
1009
1226
|
assets: records.map((record) => buildAssetDescriptor(record)),
|
|
1010
1227
|
missing: unique.filter((hash) => !found.has(hash))
|
|
1011
|
-
});
|
|
1012
|
-
const summary = [];
|
|
1013
|
-
summary.push(payload.assets.length ? `Resolved ${payload.assets.length} asset${payload.assets.length === 1 ? "" : "s"}.` : "No assets were resolved for the requested hashes.");
|
|
1014
|
-
if (payload.missing.length) summary.push(`Missing: ${payload.missing.join(", ")}`);
|
|
1015
|
-
summary.push("Download bytes from each asset.url.");
|
|
1016
|
-
return {
|
|
1017
|
-
content: [{
|
|
1018
|
-
type: "text",
|
|
1019
|
-
text: summary.join("\n")
|
|
1020
|
-
}],
|
|
1021
|
-
structuredContent: payload
|
|
1022
|
-
};
|
|
1228
|
+
}));
|
|
1023
1229
|
}
|
|
1024
1230
|
function getActiveId() {
|
|
1025
1231
|
return extensions.find((e) => e.active)?.id ?? null;
|
|
@@ -1271,7 +1477,7 @@ wss.on("connection", (ws) => {
|
|
|
1271
1477
|
log.info({ port: selectedWsPort }, "WebSocket server ready.");
|
|
1272
1478
|
process.on("SIGINT", shutdown);
|
|
1273
1479
|
process.on("SIGTERM", shutdown);
|
|
1274
|
-
|
|
1275
1480
|
//#endregion
|
|
1276
|
-
export {
|
|
1481
|
+
export {};
|
|
1482
|
+
|
|
1277
1483
|
//# sourceMappingURL=hub.mjs.map
|