@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 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-C_kjQL67.mjs";
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":"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"}
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":"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"}
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-C_kjQL67.mjs";
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 MCP_HASH_HEX_LENGTH = 8;
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
- const MessageToExtensionSchema = z.discriminatedUnion("type", [
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 inline themeable single-color SVGs and asset-backed fixed-color vectors as the tool default. `snapshot` preserves vector assets for fidelity. Final vector delivery may still be adapted to the Host app’s SVG policy.").optional()
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]{${MCP_HASH_HEX_LENGTH}})(?:\\.[a-z0-9]+)?$`, "i");
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]{${MCP_HASH_HEX_LENGTH}}$`, "i");
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, MCP_HASH_HEX_LENGTH) !== hash) {
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`, call `get_code` again for each listed `nodeId` before implementing.\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, but adapt final vector delivery to the Host app's existing SVG policy when integrating: existing icon/component primitives, import-time SVG transforms, inline SVG, or asset-backed `<img>`.\n- Themeable vectors: `themeable=true` means the SVG can safely adopt one contextual color channel, typically via `currentColor` driven by the wrapper/component `color` style or token. 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";
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 inline themeable single-color SVGs and asset-backed fixed-color vectors as the tool default; `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 `<img>`). 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, call get_code again for each listed nodeId. 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).",
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 formatBytes(bytes) {
739
- if (bytes < 1024) return `${bytes} B`;
740
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
741
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
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
- const summary = [];
746
- const codeSize = Buffer.byteLength(payload.code, "utf8");
747
- summary.push(`Generated \`${payload.lang}\` snippet (${formatBytes(codeSize)}).`);
748
- if (payload.warnings?.length) {
749
- const warningText = payload.warnings.map((warning) => warning.message).join(" ");
750
- summary.push(warningText);
751
- }
752
- summary.push(payload.assets?.length ? `Assets attached: ${payload.assets.length}. Download bytes from each asset.url.` : "No binary assets were attached to this response.");
753
- const tokenCount = payload.tokens ? Object.keys(payload.tokens).length : 0;
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
- }, instructions_default ? { instructions: instructions_default } : void 0);
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 definition = getToolDefinition(toolName);
987
- if (definition && hasFormatter(definition)) try {
988
- const formatter = definition.format;
989
- return formatter(payload);
990
- } catch (error) {
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
- error
994
- }, "Failed to format tool result; returning raw payload.");
995
- return coercePayloadToToolResponse(payload);
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 coercePayloadToToolResponse(payload);
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
- const payload = GetAssetsResultSchema.parse({
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