@tempad-dev/mcp 0.3.3 → 0.3.5

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/dist/hub.js DELETED
@@ -1,1326 +0,0 @@
1
- // src/hub.ts
2
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { nanoid as nanoid3 } from "nanoid";
5
- import { existsSync as existsSync3, rmSync as rmSync2, chmodSync, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
6
- import { createServer as createServer2 } from "node:net";
7
- import { WebSocketServer } from "ws";
8
-
9
- // ../mcp/shared/constants.ts
10
- var MCP_PORT_CANDIDATES = [6220, 7431, 8127];
11
- var MCP_MAX_PAYLOAD_BYTES = 4 * 1024 * 1024;
12
- var MCP_TOOL_TIMEOUT_MS = 15e3;
13
- var MCP_AUTO_ACTIVATE_GRACE_MS = 1500;
14
- var MCP_MAX_ASSET_BYTES = 8 * 1024 * 1024;
15
- var MCP_ASSET_RESOURCE_NAME = "tempad-assets";
16
- var MCP_ASSET_URI_PREFIX = "asset://tempad/";
17
- var MCP_ASSET_URI_TEMPLATE = `${MCP_ASSET_URI_PREFIX}{hash}`;
18
- var MCP_HASH_PATTERN = /^[a-f0-9]{64}$/i;
19
-
20
- // src/asset-http-server.ts
21
- import { nanoid } from "nanoid";
22
- import { createHash } from "node:crypto";
23
- import {
24
- createReadStream,
25
- createWriteStream,
26
- existsSync,
27
- renameSync,
28
- statSync,
29
- unlinkSync
30
- } from "node:fs";
31
- import { createServer } from "node:http";
32
- import { join as join2 } from "node:path";
33
- import { pipeline, Transform } from "node:stream";
34
- import { URL } from "node:url";
35
-
36
- // src/config.ts
37
- function parsePositiveInt(envValue, fallback) {
38
- const parsed = envValue ? Number.parseInt(envValue, 10) : Number.NaN;
39
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
40
- }
41
- function resolveToolTimeoutMs() {
42
- return parsePositiveInt(process.env.TEMPAD_MCP_TOOL_TIMEOUT, MCP_TOOL_TIMEOUT_MS);
43
- }
44
- function resolveAutoActivateGraceMs() {
45
- return parsePositiveInt(process.env.TEMPAD_MCP_AUTO_ACTIVATE_GRACE, MCP_AUTO_ACTIVATE_GRACE_MS);
46
- }
47
- function resolveMaxAssetSizeBytes() {
48
- return parsePositiveInt(process.env.TEMPAD_MCP_MAX_ASSET_BYTES, MCP_MAX_ASSET_BYTES);
49
- }
50
- function getMcpServerConfig() {
51
- return {
52
- wsPortCandidates: [...MCP_PORT_CANDIDATES],
53
- toolTimeoutMs: resolveToolTimeoutMs(),
54
- maxPayloadBytes: MCP_MAX_PAYLOAD_BYTES,
55
- autoActivateGraceMs: resolveAutoActivateGraceMs(),
56
- maxAssetSizeBytes: resolveMaxAssetSizeBytes()
57
- };
58
- }
59
-
60
- // src/shared.ts
61
- import { closeSync, mkdirSync, openSync } from "node:fs";
62
- import { tmpdir } from "node:os";
63
- import { join } from "node:path";
64
- import pino from "pino";
65
-
66
- // package.json
67
- var package_default = {
68
- name: "@tempad-dev/mcp",
69
- description: "MCP server for TemPad Dev.",
70
- version: "0.3.3",
71
- type: "module",
72
- main: "dist/cli.js",
73
- bin: "dist/cli.js",
74
- files: [
75
- "dist/**/*",
76
- "README.md"
77
- ],
78
- scripts: {
79
- build: "tsc -p ./tsconfig.json --noEmit && node ./scripts/build.mjs",
80
- prepublishOnly: "npm run build"
81
- },
82
- dependencies: {
83
- "@modelcontextprotocol/sdk": "^1.22.0",
84
- nanoid: "^5.1.6",
85
- pino: "^9.14.0",
86
- "pino-pretty": "^11.2.2",
87
- "proper-lockfile": "^4.1.2",
88
- ws: "^8.18.3",
89
- zod: "^4.1.12"
90
- }
91
- };
92
-
93
- // src/shared.ts
94
- function ensureDir(dirPath) {
95
- mkdirSync(dirPath, { recursive: true, mode: 448 });
96
- }
97
- var pkg = package_default;
98
- var PACKAGE_VERSION = typeof pkg.version === "string" ? pkg.version : "0.0.0";
99
- function resolveRuntimeDir() {
100
- if (process.env.TEMPAD_MCP_RUNTIME_DIR) return process.env.TEMPAD_MCP_RUNTIME_DIR;
101
- return join(tmpdir(), "tempad-dev", "run");
102
- }
103
- function resolveLogDir() {
104
- if (process.env.TEMPAD_MCP_LOG_DIR) return process.env.TEMPAD_MCP_LOG_DIR;
105
- return join(tmpdir(), "tempad-dev", "log");
106
- }
107
- function resolveAssetDir() {
108
- if (process.env.TEMPAD_MCP_ASSET_DIR) return process.env.TEMPAD_MCP_ASSET_DIR;
109
- return join(tmpdir(), "tempad-dev", "assets");
110
- }
111
- var RUNTIME_DIR = resolveRuntimeDir();
112
- var LOG_DIR = resolveLogDir();
113
- var ASSET_DIR = resolveAssetDir();
114
- ensureDir(RUNTIME_DIR);
115
- ensureDir(LOG_DIR);
116
- ensureDir(ASSET_DIR);
117
- function ensureFile(filePath) {
118
- const fd = openSync(filePath, "a");
119
- closeSync(fd);
120
- }
121
- var LOCK_PATH = join(RUNTIME_DIR, "mcp.lock");
122
- ensureFile(LOCK_PATH);
123
- var timestamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-").replaceAll(".", "-");
124
- var pid = process.pid;
125
- var LOG_FILE = join(LOG_DIR, `mcp-${timestamp}-${pid}.log`);
126
- var prettyTransport = pino.transport({
127
- target: "pino-pretty",
128
- options: {
129
- translateTime: "SYS:HH:MM:ss",
130
- destination: LOG_FILE
131
- }
132
- });
133
- var log = pino(
134
- {
135
- level: process.env.DEBUG ? "debug" : "info",
136
- msgPrefix: "[tempad-dev/mcp] "
137
- },
138
- prettyTransport
139
- );
140
- var SOCK_PATH = process.platform === "win32" ? "\\\\.\\pipe\\tempad-mcp" : join(RUNTIME_DIR, "mcp.sock");
141
-
142
- // src/asset-http-server.ts
143
- var LOOPBACK_HOST = "127.0.0.1";
144
- var { maxAssetSizeBytes } = getMcpServerConfig();
145
- function createAssetHttpServer(store) {
146
- const server = createServer(handleRequest);
147
- let port2 = null;
148
- async function start() {
149
- if (port2 !== null) return;
150
- await new Promise((resolve2, reject2) => {
151
- const onError = (error) => {
152
- server.off("listening", onListening);
153
- reject2(error);
154
- };
155
- const onListening = () => {
156
- server.off("error", onError);
157
- const address = server.address();
158
- if (address && typeof address === "object") {
159
- port2 = address.port;
160
- resolve2();
161
- } else {
162
- reject2(new Error("Failed to determine HTTP server port."));
163
- }
164
- };
165
- server.once("error", onError);
166
- server.once("listening", onListening);
167
- server.listen(0, LOOPBACK_HOST);
168
- });
169
- log.info({ port: port2 }, "Asset HTTP server ready.");
170
- }
171
- function stop() {
172
- if (port2 === null) return;
173
- server.close();
174
- port2 = null;
175
- }
176
- function getBaseUrl() {
177
- if (port2 === null) throw new Error("Asset HTTP server is not running.");
178
- return `http://${LOOPBACK_HOST}:${port2}`;
179
- }
180
- function handleRequest(req, res) {
181
- res.setHeader("Access-Control-Allow-Origin", "*");
182
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
183
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Asset-Width, X-Asset-Height");
184
- if (req.method === "OPTIONS") {
185
- res.writeHead(204);
186
- res.end();
187
- return;
188
- }
189
- if (!req.url) {
190
- res.writeHead(400);
191
- res.end("Missing URL");
192
- return;
193
- }
194
- const url = new URL(req.url, getBaseUrl());
195
- const segments = url.pathname.split("/").filter(Boolean);
196
- if (segments.length !== 2 || segments[0] !== "assets") {
197
- res.writeHead(404);
198
- res.end("Not Found");
199
- return;
200
- }
201
- const hash = segments[1];
202
- if (req.method === "POST") {
203
- handleUpload(req, res, hash);
204
- return;
205
- }
206
- if (req.method === "GET") {
207
- handleDownload(req, res, hash);
208
- return;
209
- }
210
- res.writeHead(405);
211
- res.end("Method Not Allowed");
212
- }
213
- function handleDownload(req, res, hash) {
214
- const record = store.get(hash);
215
- if (!record) {
216
- res.writeHead(404);
217
- res.end("Not Found");
218
- return;
219
- }
220
- let stat;
221
- try {
222
- stat = statSync(record.filePath);
223
- } catch (error) {
224
- const err = error;
225
- if (err.code === "ENOENT") {
226
- store.remove(hash, { removeFile: false });
227
- res.writeHead(404);
228
- res.end("Not Found");
229
- } else {
230
- log.error({ error, hash }, "Failed to stat asset file.");
231
- res.writeHead(500);
232
- res.end("Internal Server Error");
233
- }
234
- return;
235
- }
236
- res.writeHead(200, {
237
- "Content-Type": record.mimeType,
238
- "Content-Length": stat.size.toString(),
239
- "Cache-Control": "public, max-age=31536000, immutable"
240
- });
241
- const stream = createReadStream(record.filePath);
242
- stream.on("error", (error) => {
243
- log.warn({ error, hash }, "Failed to stream asset file.");
244
- if (!res.headersSent) {
245
- res.writeHead(500);
246
- }
247
- res.end("Internal Server Error");
248
- });
249
- stream.on("open", () => {
250
- store.touch(hash);
251
- });
252
- stream.pipe(res);
253
- }
254
- function handleUpload(req, res, hash) {
255
- if (!MCP_HASH_PATTERN.test(hash)) {
256
- res.writeHead(400);
257
- res.end("Invalid Hash Format");
258
- return;
259
- }
260
- const mimeType = req.headers["content-type"] || "application/octet-stream";
261
- const filePath = join2(ASSET_DIR, hash);
262
- const width = parseInt(req.headers["x-asset-width"], 10);
263
- const height = parseInt(req.headers["x-asset-height"], 10);
264
- const metadata = !isNaN(width) && !isNaN(height) && width > 0 && height > 0 ? { width, height } : void 0;
265
- if (store.has(hash) && existsSync(filePath)) {
266
- req.resume();
267
- const existing = store.get(hash);
268
- let changed = false;
269
- if (metadata) {
270
- existing.metadata = metadata;
271
- changed = true;
272
- }
273
- if (existing.mimeType !== mimeType) {
274
- existing.mimeType = mimeType;
275
- changed = true;
276
- }
277
- if (changed) {
278
- store.upsert(existing);
279
- }
280
- store.touch(hash);
281
- res.writeHead(200);
282
- res.end("OK");
283
- return;
284
- }
285
- const tmpPath = `${filePath}.tmp.${nanoid()}`;
286
- const writeStream = createWriteStream(tmpPath);
287
- const hasher = createHash("sha256");
288
- let size = 0;
289
- const cleanup = () => {
290
- if (existsSync(tmpPath)) {
291
- try {
292
- unlinkSync(tmpPath);
293
- } catch (e) {
294
- log.warn({ error: e, tmpPath }, "Failed to cleanup temp file.");
295
- }
296
- }
297
- };
298
- const monitor = new Transform({
299
- transform(chunk, encoding, callback) {
300
- size += chunk.length;
301
- if (size > maxAssetSizeBytes) {
302
- callback(new Error("PayloadTooLarge"));
303
- return;
304
- }
305
- hasher.update(chunk);
306
- callback(null, chunk);
307
- }
308
- });
309
- pipeline(req, monitor, writeStream, (err) => {
310
- if (err) {
311
- cleanup();
312
- if (err.message === "PayloadTooLarge") {
313
- res.writeHead(413);
314
- res.end("Payload Too Large");
315
- } else if (err.code === "ERR_STREAM_PREMATURE_CLOSE") {
316
- log.warn({ hash }, "Upload request closed prematurely.");
317
- } else {
318
- log.error({ error: err, hash }, "Upload pipeline failed.");
319
- if (!res.headersSent) {
320
- res.writeHead(500);
321
- res.end("Internal Server Error");
322
- }
323
- }
324
- return;
325
- }
326
- const computedHash = hasher.digest("hex");
327
- if (computedHash !== hash) {
328
- cleanup();
329
- res.writeHead(400);
330
- res.end("Hash Mismatch");
331
- return;
332
- }
333
- try {
334
- renameSync(tmpPath, filePath);
335
- } catch (error) {
336
- log.error({ error, hash }, "Failed to rename temp file to asset.");
337
- cleanup();
338
- res.writeHead(500);
339
- res.end("Internal Server Error");
340
- return;
341
- }
342
- store.upsert({
343
- hash,
344
- filePath,
345
- mimeType,
346
- size,
347
- metadata
348
- });
349
- log.info({ hash, size }, "Stored uploaded asset via HTTP.");
350
- res.writeHead(201);
351
- res.end("Created");
352
- });
353
- }
354
- return {
355
- start,
356
- stop,
357
- getBaseUrl
358
- };
359
- }
360
-
361
- // src/asset-store.ts
362
- import { existsSync as existsSync2, readFileSync, rmSync, writeFileSync, readdirSync, statSync as statSync2 } from "node:fs";
363
- import { join as join3 } from "node:path";
364
- var INDEX_FILENAME = "assets.json";
365
- var DEFAULT_INDEX_PATH = join3(ASSET_DIR, INDEX_FILENAME);
366
- function readIndex(indexPath) {
367
- if (!existsSync2(indexPath)) return [];
368
- try {
369
- const raw = readFileSync(indexPath, "utf8").trim();
370
- if (!raw) return [];
371
- const parsed = JSON.parse(raw);
372
- return Array.isArray(parsed) ? parsed : [];
373
- } catch (error) {
374
- log.warn({ error, indexPath }, "Failed to read asset catalog; starting fresh.");
375
- return [];
376
- }
377
- }
378
- function writeIndex(indexPath, values) {
379
- const payload = JSON.stringify(values, null, 2);
380
- writeFileSync(indexPath, payload, "utf8");
381
- }
382
- function createAssetStore(options = {}) {
383
- ensureDir(ASSET_DIR);
384
- const indexPath = options.indexPath ?? DEFAULT_INDEX_PATH;
385
- ensureFile(indexPath);
386
- const records = /* @__PURE__ */ new Map();
387
- let persistTimer = null;
388
- function loadExisting() {
389
- const list2 = readIndex(indexPath);
390
- for (const record of list2) {
391
- if (record?.hash && record?.filePath) {
392
- records.set(record.hash, record);
393
- }
394
- }
395
- }
396
- function persist() {
397
- if (persistTimer) return;
398
- persistTimer = setTimeout(() => {
399
- persistTimer = null;
400
- writeIndex(indexPath, [...records.values()]);
401
- }, 5e3);
402
- if (typeof persistTimer.unref === "function") {
403
- persistTimer.unref();
404
- }
405
- }
406
- function flush() {
407
- if (persistTimer) {
408
- clearTimeout(persistTimer);
409
- persistTimer = null;
410
- }
411
- writeIndex(indexPath, [...records.values()]);
412
- }
413
- function list() {
414
- return [...records.values()];
415
- }
416
- function has(hash) {
417
- return records.has(hash);
418
- }
419
- function get(hash) {
420
- return records.get(hash);
421
- }
422
- function getMany(hashes) {
423
- return hashes.map((hash) => records.get(hash)).filter((record) => !!record);
424
- }
425
- function upsert(input) {
426
- const now = Date.now();
427
- const record = {
428
- ...input,
429
- uploadedAt: input.uploadedAt ?? now,
430
- lastAccess: input.lastAccess ?? now
431
- };
432
- records.set(record.hash, record);
433
- persist();
434
- return record;
435
- }
436
- function touch(hash) {
437
- const existing = records.get(hash);
438
- if (!existing) return void 0;
439
- existing.lastAccess = Date.now();
440
- persist();
441
- return existing;
442
- }
443
- function remove(hash, { removeFile = true } = {}) {
444
- const record = records.get(hash);
445
- if (!record) return;
446
- records.delete(hash);
447
- persist();
448
- if (removeFile) {
449
- try {
450
- rmSync(record.filePath, { force: true });
451
- } catch (error) {
452
- log.warn({ hash, error }, "Failed to remove asset file on delete.");
453
- }
454
- }
455
- }
456
- function reconcile() {
457
- let changed = false;
458
- for (const [hash, record] of records) {
459
- if (!existsSync2(record.filePath)) {
460
- records.delete(hash);
461
- changed = true;
462
- }
463
- }
464
- try {
465
- const files = readdirSync(ASSET_DIR);
466
- const now = Date.now();
467
- for (const file of files) {
468
- if (file === INDEX_FILENAME) continue;
469
- if (file.includes(".tmp.")) {
470
- try {
471
- const filePath = join3(ASSET_DIR, file);
472
- const stat = statSync2(filePath);
473
- if (now - stat.mtimeMs > 3600 * 1e3) {
474
- rmSync(filePath, { force: true });
475
- log.info({ file }, "Cleaned up stale temp file.");
476
- }
477
- } catch (e) {
478
- log.debug({ error: e, file }, "Failed to cleanup stale temp file.");
479
- }
480
- continue;
481
- }
482
- if (!/^[a-f0-9]{64}$/i.test(file)) continue;
483
- if (!records.has(file)) {
484
- const filePath = join3(ASSET_DIR, file);
485
- try {
486
- const stat = statSync2(filePath);
487
- records.set(file, {
488
- hash: file,
489
- filePath,
490
- mimeType: "application/octet-stream",
491
- size: stat.size,
492
- uploadedAt: stat.birthtimeMs,
493
- lastAccess: stat.atimeMs
494
- });
495
- changed = true;
496
- log.info({ hash: file }, "Recovered orphan asset file.");
497
- } catch (e) {
498
- log.warn({ error: e, file }, "Failed to stat orphan file.");
499
- }
500
- }
501
- }
502
- } catch (error) {
503
- log.warn({ error }, "Failed to scan asset directory for orphans.");
504
- }
505
- if (changed) flush();
506
- }
507
- loadExisting();
508
- reconcile();
509
- return {
510
- list,
511
- has,
512
- get,
513
- getMany,
514
- upsert,
515
- touch,
516
- remove,
517
- reconcile,
518
- flush
519
- };
520
- }
521
-
522
- // src/protocol.ts
523
- import { z } from "zod";
524
- var RegisteredMessageSchema = z.object({
525
- type: z.literal("registered"),
526
- id: z.string()
527
- });
528
- var StateMessageSchema = z.object({
529
- type: z.literal("state"),
530
- activeId: z.string().nullable(),
531
- count: z.number().nonnegative(),
532
- port: z.number().positive(),
533
- assetServerUrl: z.string().url()
534
- });
535
- var ToolCallPayloadSchema = z.object({
536
- name: z.string(),
537
- args: z.unknown()
538
- });
539
- var ToolCallMessageSchema = z.object({
540
- type: z.literal("toolCall"),
541
- id: z.string(),
542
- payload: ToolCallPayloadSchema
543
- });
544
- var MessageToExtensionSchema = z.discriminatedUnion("type", [
545
- RegisteredMessageSchema,
546
- StateMessageSchema,
547
- ToolCallMessageSchema
548
- ]);
549
- var ActivateMessageSchema = z.object({
550
- type: z.literal("activate")
551
- });
552
- var ToolResultMessageSchema = z.object({
553
- type: z.literal("toolResult"),
554
- id: z.string(),
555
- payload: z.unknown().optional(),
556
- error: z.unknown().optional()
557
- });
558
- var MessageFromExtensionSchema = z.discriminatedUnion("type", [
559
- ActivateMessageSchema,
560
- ToolResultMessageSchema
561
- ]);
562
-
563
- // src/request.ts
564
- import { nanoid as nanoid2 } from "nanoid";
565
- var pendingCalls = /* @__PURE__ */ new Map();
566
- function register(extensionId, timeout) {
567
- const requestId = nanoid2();
568
- const promise = new Promise((resolve2, reject2) => {
569
- const timer = setTimeout(() => {
570
- pendingCalls.delete(requestId);
571
- reject2(new Error(`Extension did not respond within ${timeout / 1e3}s.`));
572
- }, timeout);
573
- pendingCalls.set(requestId, {
574
- resolve: resolve2,
575
- reject: reject2,
576
- timer,
577
- extensionId
578
- });
579
- });
580
- return { promise, requestId };
581
- }
582
- function resolve(requestId, payload) {
583
- const call = pendingCalls.get(requestId);
584
- if (call) {
585
- const { timer, resolve: finish } = call;
586
- clearTimeout(timer);
587
- finish(payload);
588
- pendingCalls.delete(requestId);
589
- } else {
590
- log.warn({ reqId: requestId }, "Received result for unknown/timed-out call.");
591
- }
592
- }
593
- function reject(requestId, error) {
594
- const call = pendingCalls.get(requestId);
595
- if (call) {
596
- const { timer, reject: fail } = call;
597
- clearTimeout(timer);
598
- fail(error);
599
- pendingCalls.delete(requestId);
600
- } else {
601
- log.warn({ reqId: requestId }, "Received error for unknown/timed-out call.");
602
- }
603
- }
604
- function cleanupForExtension(extensionId) {
605
- for (const [reqId, call] of pendingCalls.entries()) {
606
- const { timer, reject: fail, extensionId: extId } = call;
607
- if (extId === extensionId) {
608
- clearTimeout(timer);
609
- fail(new Error("Extension disconnected before providing a result."));
610
- pendingCalls.delete(reqId);
611
- log.warn({ reqId, extId: extensionId }, "Rejected pending call from disconnected extension.");
612
- }
613
- }
614
- }
615
- function cleanupAll() {
616
- pendingCalls.forEach((call, reqId) => {
617
- const { timer, reject: fail } = call;
618
- clearTimeout(timer);
619
- fail(new Error("Hub is shutting down."));
620
- log.debug({ reqId }, "Rejected pending tool call due to shutdown.");
621
- });
622
- pendingCalls.clear();
623
- }
624
-
625
- // src/tools.ts
626
- import { z as z2 } from "zod";
627
-
628
- // src/instructions.md
629
- var instructions_default = "You are connected to a Figma design file via the MCP server. Convert design elements into production code, preserving design intent while fitting the user\u2019s codebase conventions.\n\n- Start from `get_code` as the baseline, then refactor to match project conventions (components, styling system, file structure, naming).\n- Layout confidence:\n - If `get_code` contains no `data-hint-auto-layout`, it likely indicates the layout is explicit. You can be more confident implementing directly from `get_code`.\n - If any `data-hint-auto-layout` is `none` or `inferred`, the corresponding layout may be uncertain. If you feel ambiguity or uncertainty, consult `get_structure` (hierarchy + geometry) and `get_screenshot` (visual intent such as layering/overlap/masks/shadows/translucency).\n- If `data-hint-component` plus repetition supports it, extract reusable components/variants aligned with project patterns. Do not preserve hint strings in output.\n- Tokens: follow the project\u2019s token/theming framework; if needed, use `get_code.usedToken` metadata (collection, mode) to extend a responsive token system within that framework.\n- Assets: follow the project\u2019s existing conventions/practices (icon system, asset pipeline, import/path rules, optimization) to decide how to represent and reference assets. If `get_code` uses resource URIs, you may replace them with the project\u2019s canonical references when appropriate without changing rendering.\n- Do not output any `data-*` attributes returned by `get_code`.\n- For SVG/vector assets: use the exact provided asset, preserving `path` data, `viewBox`, and full SVG structure. Never redraw or approximate vectors.\n";
630
-
631
- // src/tools.ts
632
- var GetCodeParametersSchema = z2.object({
633
- nodeId: z2.string().describe(
634
- "Optional target node id; omit to use the current single selection when pulling the baseline snapshot."
635
- ).optional(),
636
- preferredLang: z2.enum(["jsx", "vue"]).describe(
637
- "Preferred output language to bias the snapshot; otherwise uses the design\u2019s hint/detected language, then falls back to JSX."
638
- ).optional(),
639
- resolveTokens: z2.boolean().describe(
640
- "Inline token values instead of references for quick renders; default false returns token metadata so you can map into your theming system."
641
- ).optional()
642
- });
643
- var GetTokenDefsParametersSchema = z2.object({
644
- names: z2.array(z2.string().regex(/^--[a-zA-Z0-9-_]+$/)).min(1).describe(
645
- "Canonical token names (CSS variable form) from get_code.usedTokens or your own list to resolve, e.g., --color-primary."
646
- ),
647
- includeAllModes: z2.boolean().describe(
648
- "Include all token modes (light/dark/etc.) instead of just the active one to mirror responsive tokens; default false."
649
- ).optional()
650
- });
651
- var AssetDescriptorSchema = z2.object({
652
- hash: z2.string().min(1),
653
- url: z2.string().url(),
654
- mimeType: z2.string().min(1),
655
- size: z2.number().int().nonnegative(),
656
- resourceUri: z2.string().regex(/^asset:\/\/tempad\/[a-f0-9]{64}$/i),
657
- width: z2.number().int().positive().optional(),
658
- height: z2.number().int().positive().optional()
659
- });
660
- var GetScreenshotParametersSchema = z2.object({
661
- nodeId: z2.string().describe(
662
- "Optional node id to screenshot; defaults to the current single selection. Useful when layout/overlap is uncertain (auto-layout none/inferred)."
663
- ).optional()
664
- });
665
- var GetStructureParametersSchema = z2.object({
666
- nodeId: z2.string().describe(
667
- "Optional node id to outline; defaults to the current single selection. Useful when auto-layout hints are none/inferred or you need explicit geometry for refactors."
668
- ).optional(),
669
- options: z2.object({
670
- depth: z2.number().int().positive().describe("Limit traversal depth; defaults to full tree (subject to safety caps).").optional()
671
- }).optional()
672
- });
673
- var GetAssetsParametersSchema = z2.object({
674
- hashes: z2.array(z2.string().regex(MCP_HASH_PATTERN)).min(1).describe(
675
- "Asset hashes returned from get_code (or other tools) to download/resolve exact bytes for rasterized images or SVGs before routing through your asset pipeline."
676
- )
677
- });
678
- var GetAssetsResultSchema = z2.object({
679
- assets: z2.array(AssetDescriptorSchema),
680
- missing: z2.array(z2.string().min(1))
681
- });
682
- function extTool(definition) {
683
- return definition;
684
- }
685
- function hubTool(definition) {
686
- return definition;
687
- }
688
- var TOOL_DEFS = [
689
- extTool({
690
- name: "get_code",
691
- description: "Get a high-fidelity code snapshot for a nodeId/current selection, including assets/usedTokens and codegen preset/config. Start here, then refactor into your component/styling/file/naming conventions; strip any data-* hints. If no data-hint-auto-layout is present, layout is explicit; if any hint is none/inferred, pair with get_structure/get_screenshot to confirm hierarchy/overlap. Use data-hint-component plus repetition to decide on reusable components. Replace resource URIs with your canonical asset system as needed.",
692
- parameters: GetCodeParametersSchema,
693
- target: "extension",
694
- format: createCodeToolResponse
695
- }),
696
- extTool({
697
- name: "get_token_defs",
698
- description: "Resolve canonical token names to values (including modes) for tokens referenced by get_code. Use this to map into your design token/theming system, including responsive tokens.",
699
- parameters: GetTokenDefsParametersSchema,
700
- target: "extension",
701
- exposed: false
702
- }),
703
- extTool({
704
- name: "get_screenshot",
705
- description: "Capture a rendered screenshot for a nodeId/current selection for visual verification. Useful for confirming layering/overlap/masks/shadows/translucency when auto-layout hints are none/inferred.",
706
- parameters: GetScreenshotParametersSchema,
707
- target: "extension",
708
- format: createScreenshotToolResponse
709
- }),
710
- extTool({
711
- name: "get_structure",
712
- description: "Get a structural + geometry outline for a nodeId/current selection to understand hierarchy and layout intent. Use when auto-layout hints are none/inferred or you need explicit bounds for refactors/component extraction.",
713
- parameters: GetStructureParametersSchema,
714
- target: "extension"
715
- }),
716
- hubTool({
717
- name: "get_assets",
718
- description: "Resolve asset hashes to downloadable URLs/URIs for assets referenced by get_code, preserving vectors exactly. Pull bytes before routing through your asset/icon pipeline.",
719
- parameters: GetAssetsParametersSchema,
720
- target: "hub",
721
- outputSchema: GetAssetsResultSchema,
722
- exposed: false
723
- })
724
- ];
725
- function createToolErrorResponse(toolName, error) {
726
- const message = error instanceof Error ? error.message || "Unknown error occurred." : typeof error === "string" ? error : "Unknown error occurred.";
727
- return {
728
- content: [
729
- {
730
- type: "text",
731
- text: `Tool "${toolName}" failed: ${message}`
732
- }
733
- ]
734
- };
735
- }
736
- function formatBytes(bytes) {
737
- if (bytes < 1024) return `${bytes} B`;
738
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
739
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
740
- }
741
- function createCodeToolResponse(payload) {
742
- if (!isCodeResult(payload)) {
743
- throw new Error("Invalid get_code payload received from extension.");
744
- }
745
- const summary = [];
746
- const codeSize = Buffer.byteLength(payload.code, "utf8");
747
- summary.push(`Generated \`${payload.lang}\` snippet (${formatBytes(codeSize)}).`);
748
- if (payload.message) {
749
- summary.push(payload.message);
750
- }
751
- summary.push(
752
- payload.assets.length ? `Assets attached: ${payload.assets.length}. Fetch bytes via resources/read using resourceUri.` : "No binary assets were attached to this response."
753
- );
754
- if (payload.usedTokens?.length) {
755
- summary.push(`Token references included: ${payload.usedTokens.length}.`);
756
- }
757
- summary.push("Read structuredContent for the full code string and asset metadata.");
758
- const assetLinks = payload.assets.length > 0 ? payload.assets.map((asset) => createAssetResourceLinkBlock(asset)) : [];
759
- return {
760
- content: [
761
- {
762
- type: "text",
763
- text: summary.join("\n")
764
- },
765
- ...assetLinks
766
- ],
767
- structuredContent: payload
768
- };
769
- }
770
- function createScreenshotToolResponse(payload) {
771
- if (!isScreenshotResult(payload)) {
772
- throw new Error("Invalid get_screenshot payload received from extension.");
773
- }
774
- const descriptionBlock = {
775
- type: "text",
776
- text: describeScreenshot(payload)
777
- };
778
- return {
779
- content: [
780
- descriptionBlock,
781
- {
782
- type: "text",
783
- text: `![Screenshot](${payload.asset.url})`
784
- },
785
- createResourceLinkBlock(payload.asset, payload)
786
- ],
787
- structuredContent: payload
788
- };
789
- }
790
- function createResourceLinkBlock(asset, result) {
791
- return {
792
- type: "resource_link",
793
- name: "Screenshot",
794
- uri: asset.resourceUri,
795
- mimeType: asset.mimeType,
796
- description: `Screenshot ${result.width}x${result.height} @${result.scale}x - Download: ${asset.url}`
797
- };
798
- }
799
- function describeScreenshot(result) {
800
- return `Screenshot ${result.width}x${result.height} @${result.scale}x (${formatBytes(result.bytes)})`;
801
- }
802
- function isScreenshotResult(payload) {
803
- if (typeof payload !== "object" || !payload) return false;
804
- const candidate = payload;
805
- return typeof candidate.asset === "object" && candidate.asset !== null && typeof candidate.width === "number" && typeof candidate.height === "number" && typeof candidate.scale === "number" && typeof candidate.bytes === "number" && typeof candidate.format === "string";
806
- }
807
- function isCodeResult(payload) {
808
- if (typeof payload !== "object" || !payload) return false;
809
- const candidate = payload;
810
- return typeof candidate.code === "string" && typeof candidate.lang === "string" && Array.isArray(candidate.assets);
811
- }
812
- function createAssetResourceLinkBlock(asset) {
813
- return {
814
- type: "resource_link",
815
- name: formatAssetResourceName(asset.hash),
816
- uri: asset.resourceUri,
817
- mimeType: asset.mimeType,
818
- description: `${describeAsset(asset)} - Download: ${asset.url}`
819
- };
820
- }
821
- function describeAsset(asset) {
822
- return `${asset.mimeType} (${formatBytes(asset.size)})`;
823
- }
824
- function formatAssetResourceName(hash) {
825
- return `asset:${hash.slice(0, 8)}`;
826
- }
827
- function coercePayloadToToolResponse(payload) {
828
- if (payload && typeof payload === "object" && Array.isArray(payload.content)) {
829
- return payload;
830
- }
831
- return {
832
- content: [
833
- {
834
- type: "text",
835
- text: typeof payload === "string" ? payload : JSON.stringify(payload, null, 2)
836
- }
837
- ]
838
- };
839
- }
840
-
841
- // src/hub.ts
842
- var SHUTDOWN_TIMEOUT = 2e3;
843
- var { wsPortCandidates, toolTimeoutMs, maxPayloadBytes, autoActivateGraceMs } = getMcpServerConfig();
844
- log.info({ version: PACKAGE_VERSION }, "TemPad MCP Hub starting...");
845
- var extensions = [];
846
- var consumerCount = 0;
847
- var autoActivateTimer = null;
848
- var selectedWsPort = 0;
849
- var mcp = new McpServer(
850
- { name: "tempad-dev-mcp", version: PACKAGE_VERSION },
851
- instructions_default ? { instructions: instructions_default } : void 0
852
- );
853
- function enrichToolDefinition(tool) {
854
- if (tool.target === "extension") {
855
- return tool;
856
- }
857
- switch (tool.name) {
858
- case "get_assets":
859
- return {
860
- ...tool,
861
- handler: handleGetAssets
862
- };
863
- default:
864
- throw new Error("No handler configured for hub tool.");
865
- }
866
- }
867
- var TOOL_DEFINITIONS = TOOL_DEFS.map(
868
- (tool) => enrichToolDefinition(tool)
869
- );
870
- function hasFormatter(tool) {
871
- return tool.target === "extension" && "format" in tool;
872
- }
873
- var TOOL_BY_NAME = Object.fromEntries(
874
- TOOL_DEFINITIONS.map((tool) => [tool.name, tool])
875
- );
876
- function getToolDefinition(name) {
877
- return TOOL_BY_NAME[name];
878
- }
879
- var assetStore = createAssetStore();
880
- var assetHttpServer = createAssetHttpServer(assetStore);
881
- await assetHttpServer.start();
882
- registerAssetResources();
883
- function registerAssetResources() {
884
- const template = new ResourceTemplate(MCP_ASSET_URI_TEMPLATE, {
885
- list: async () => ({
886
- resources: assetStore.list().filter((record) => existsSync3(record.filePath)).map((record) => ({
887
- uri: buildAssetResourceUri(record.hash),
888
- name: formatAssetResourceName2(record.hash),
889
- description: `${record.mimeType} (${formatBytes2(record.size)})`,
890
- mimeType: record.mimeType
891
- }))
892
- })
893
- });
894
- mcp.registerResource(
895
- MCP_ASSET_RESOURCE_NAME,
896
- template,
897
- {
898
- description: "Binary assets captured by the TemPad Dev hub."
899
- },
900
- async (_uri, variables) => {
901
- const hash = typeof variables.hash === "string" ? variables.hash : "";
902
- return readAssetResource(hash);
903
- }
904
- );
905
- }
906
- async function readAssetResource(hash) {
907
- if (!hash) {
908
- throw new Error("Missing asset hash in resource URI.");
909
- }
910
- const record = assetStore.get(hash);
911
- if (!record) {
912
- throw new Error(`Asset ${hash} not found.`);
913
- }
914
- if (!existsSync3(record.filePath)) {
915
- assetStore.remove(hash, { removeFile: false });
916
- throw new Error(`Asset ${hash} file is missing.`);
917
- }
918
- const stat = statSync3(record.filePath);
919
- const estimatedSize = Math.ceil(stat.size / 3) * 4;
920
- if (estimatedSize > maxPayloadBytes) {
921
- throw new Error(
922
- `Asset ${hash} is too large (${formatBytes2(stat.size)}, encoded: ${formatBytes2(estimatedSize)}) to read via MCP protocol. Use HTTP download.`
923
- );
924
- }
925
- assetStore.touch(hash);
926
- const buffer = readFileSync2(record.filePath);
927
- const resourceUri = buildAssetResourceUri(hash);
928
- if (isTextualMime(record.mimeType)) {
929
- return {
930
- contents: [
931
- {
932
- uri: resourceUri,
933
- mimeType: record.mimeType,
934
- text: buffer.toString("utf8")
935
- }
936
- ]
937
- };
938
- }
939
- return {
940
- contents: [
941
- {
942
- uri: resourceUri,
943
- mimeType: record.mimeType,
944
- blob: buffer.toString("base64")
945
- }
946
- ]
947
- };
948
- }
949
- function isTextualMime(mimeType) {
950
- return mimeType === "image/svg+xml" || mimeType.startsWith("text/");
951
- }
952
- function buildAssetResourceUri(hash) {
953
- return `${MCP_ASSET_URI_PREFIX}${hash}`;
954
- }
955
- function formatAssetResourceName2(hash) {
956
- return `asset:${hash.slice(0, 8)}`;
957
- }
958
- function buildAssetDescriptor(record) {
959
- return {
960
- hash: record.hash,
961
- url: `${assetHttpServer.getBaseUrl()}/assets/${record.hash}`,
962
- mimeType: record.mimeType,
963
- size: record.size,
964
- resourceUri: buildAssetResourceUri(record.hash),
965
- width: record.metadata?.width,
966
- height: record.metadata?.height
967
- };
968
- }
969
- function createAssetResourceLinkBlock2(asset) {
970
- return {
971
- type: "resource_link",
972
- name: formatAssetResourceName2(asset.hash),
973
- uri: asset.resourceUri,
974
- mimeType: asset.mimeType,
975
- description: `${describeAsset2(asset)} - Download: ${asset.url}`
976
- };
977
- }
978
- function describeAsset2(asset) {
979
- return `${asset.mimeType} (${formatBytes2(asset.size)})`;
980
- }
981
- function registerTools() {
982
- const registered = [];
983
- for (const tool of TOOL_DEFINITIONS) {
984
- if ("exposed" in tool && tool.exposed === false) continue;
985
- registerTool(tool);
986
- registered.push(tool.name);
987
- }
988
- log.info({ tools: registered }, "Registered tools.");
989
- }
990
- registerTools();
991
- function registerTool(tool) {
992
- if (tool.target === "extension") {
993
- registerProxiedTool(tool);
994
- } else {
995
- registerLocalTool(tool);
996
- }
997
- }
998
- function registerProxiedTool(tool) {
999
- const schema = tool.parameters;
1000
- mcp.registerTool(
1001
- tool.name,
1002
- {
1003
- description: tool.description,
1004
- inputSchema: schema
1005
- },
1006
- async (args) => {
1007
- try {
1008
- const parsedArgs = schema.parse(args);
1009
- const activeExt = extensions.find((e) => e.active);
1010
- if (!activeExt) throw new Error("No active TemPad Dev extension available.");
1011
- const { promise, requestId } = register(activeExt.id, toolTimeoutMs);
1012
- const message = {
1013
- type: "toolCall",
1014
- id: requestId,
1015
- payload: {
1016
- name: tool.name,
1017
- args: parsedArgs
1018
- }
1019
- };
1020
- activeExt.ws.send(JSON.stringify(message));
1021
- log.info({ tool: tool.name, req: requestId, extId: activeExt.id }, "Forwarded tool call.");
1022
- const payload = await promise;
1023
- return createToolResponse(tool.name, payload);
1024
- } catch (error) {
1025
- log.error({ tool: tool.name, error }, "Tool invocation failed before reaching extension.");
1026
- return createToolErrorResponse(tool.name, error);
1027
- }
1028
- }
1029
- );
1030
- }
1031
- function registerLocalTool(tool) {
1032
- const schema = tool.parameters;
1033
- const handler = tool.handler;
1034
- const registrationOptions = {
1035
- description: tool.description,
1036
- inputSchema: schema
1037
- };
1038
- if (tool.outputSchema) {
1039
- registrationOptions.outputSchema = tool.outputSchema;
1040
- }
1041
- mcp.registerTool(tool.name, registrationOptions, async (args) => {
1042
- try {
1043
- const parsed = schema.parse(args);
1044
- return await handler(parsed);
1045
- } catch (error) {
1046
- log.error({ tool: tool.name, error }, "Local tool invocation failed.");
1047
- return createToolErrorResponse(tool.name, error);
1048
- }
1049
- });
1050
- }
1051
- function createToolResponse(toolName, payload) {
1052
- const definition = getToolDefinition(toolName);
1053
- if (definition && hasFormatter(definition)) {
1054
- try {
1055
- const formatter = definition.format;
1056
- return formatter(payload);
1057
- } catch (error) {
1058
- log.warn({ tool: toolName, error }, "Failed to format tool result; returning raw payload.");
1059
- return coercePayloadToToolResponse(payload);
1060
- }
1061
- }
1062
- return coercePayloadToToolResponse(payload);
1063
- }
1064
- async function handleGetAssets({ hashes }) {
1065
- if (hashes.length > 100) {
1066
- throw new Error("Too many hashes requested. Limit is 100.");
1067
- }
1068
- const unique = Array.from(new Set(hashes));
1069
- const records = assetStore.getMany(unique).filter((record) => {
1070
- if (existsSync3(record.filePath)) return true;
1071
- assetStore.remove(record.hash, { removeFile: false });
1072
- return false;
1073
- });
1074
- const found = new Set(records.map((record) => record.hash));
1075
- const payload = GetAssetsResultSchema.parse({
1076
- assets: records.map((record) => buildAssetDescriptor(record)),
1077
- missing: unique.filter((hash) => !found.has(hash))
1078
- });
1079
- const summary = [];
1080
- summary.push(
1081
- payload.assets.length ? `Resolved ${payload.assets.length} asset${payload.assets.length === 1 ? "" : "s"}.` : "No assets were resolved for the requested hashes."
1082
- );
1083
- if (payload.missing.length) {
1084
- summary.push(`Missing: ${payload.missing.join(", ")}`);
1085
- }
1086
- summary.push(
1087
- "Use resources/read with each resourceUri or fetch the fallback URL to download bytes."
1088
- );
1089
- const content = [
1090
- {
1091
- type: "text",
1092
- text: summary.join("\n")
1093
- },
1094
- ...payload.assets.map((asset) => createAssetResourceLinkBlock2(asset))
1095
- ];
1096
- return {
1097
- content,
1098
- structuredContent: payload
1099
- };
1100
- }
1101
- function getActiveId() {
1102
- return extensions.find((e) => e.active)?.id ?? null;
1103
- }
1104
- function setActive(targetId) {
1105
- extensions.forEach((e) => {
1106
- e.active = targetId !== null && e.id === targetId;
1107
- });
1108
- }
1109
- function clearAutoActivateTimer() {
1110
- if (autoActivateTimer) {
1111
- clearTimeout(autoActivateTimer);
1112
- autoActivateTimer = null;
1113
- }
1114
- }
1115
- function scheduleAutoActivate() {
1116
- clearAutoActivateTimer();
1117
- if (extensions.length !== 1 || getActiveId()) {
1118
- return;
1119
- }
1120
- const target = extensions[0];
1121
- autoActivateTimer = setTimeout(() => {
1122
- autoActivateTimer = null;
1123
- if (extensions.length === 1 && !getActiveId()) {
1124
- setActive(target.id);
1125
- log.info({ id: target.id }, "Auto-activated sole extension after grace period.");
1126
- broadcastState();
1127
- }
1128
- }, autoActivateGraceMs);
1129
- }
1130
- function unrefTimer(timer) {
1131
- if (typeof timer === "object" && timer !== null) {
1132
- const handle = timer;
1133
- if (typeof handle.unref === "function") {
1134
- handle.unref();
1135
- }
1136
- }
1137
- }
1138
- function broadcastState() {
1139
- const activeId = getActiveId();
1140
- const message = {
1141
- type: "state",
1142
- activeId,
1143
- count: extensions.length,
1144
- port: selectedWsPort,
1145
- assetServerUrl: assetHttpServer.getBaseUrl()
1146
- };
1147
- extensions.forEach((ext) => ext.ws.send(JSON.stringify(message)));
1148
- log.debug({ activeId, count: extensions.length }, "Broadcasted state.");
1149
- }
1150
- function rawDataToBuffer(raw) {
1151
- if (typeof raw === "string") return Buffer.from(raw);
1152
- if (Buffer.isBuffer(raw)) return raw;
1153
- if (raw instanceof ArrayBuffer) return Buffer.from(raw);
1154
- return Buffer.concat(raw);
1155
- }
1156
- function formatBytes2(bytes) {
1157
- if (bytes < 1024) return `${bytes} B`;
1158
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1159
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1160
- }
1161
- function shutdown() {
1162
- log.info("Hub is shutting down...");
1163
- assetStore.flush();
1164
- assetHttpServer.stop();
1165
- netServer.close(() => log.info("Net server closed."));
1166
- wss?.close(() => log.info("WebSocket server closed."));
1167
- cleanupAll();
1168
- const timer = setTimeout(() => {
1169
- log.warn("Shutdown timed out. Forcing exit.");
1170
- process.exit(1);
1171
- }, SHUTDOWN_TIMEOUT);
1172
- unrefTimer(timer);
1173
- }
1174
- try {
1175
- ensureDir(RUNTIME_DIR);
1176
- if (process.platform !== "win32" && existsSync3(SOCK_PATH)) {
1177
- log.warn({ sock: SOCK_PATH }, "Removing stale socket file.");
1178
- rmSync2(SOCK_PATH);
1179
- }
1180
- } catch (error) {
1181
- log.error({ err: error }, "Failed to initialize runtime environment.");
1182
- process.exit(1);
1183
- }
1184
- var netServer = createServer2((sock) => {
1185
- consumerCount++;
1186
- log.info(`Consumer connected. Total: ${consumerCount}`);
1187
- const transport = new StdioServerTransport(sock, sock);
1188
- mcp.connect(transport).catch((err) => {
1189
- log.error({ err }, "Failed to attach MCP transport.");
1190
- transport.close().catch((closeErr) => log.warn({ err: closeErr }, "Transport close failed."));
1191
- sock.destroy();
1192
- });
1193
- sock.on("error", (err) => {
1194
- log.warn({ err }, "Consumer socket error.");
1195
- transport.close().catch((closeErr) => log.warn({ err: closeErr }, "Transport close failed."));
1196
- });
1197
- sock.on("close", async () => {
1198
- await transport.close();
1199
- consumerCount--;
1200
- log.info(`Consumer disconnected. Remaining: ${consumerCount}`);
1201
- if (consumerCount === 0) {
1202
- log.info("Last consumer disconnected. Shutting down.");
1203
- shutdown();
1204
- }
1205
- });
1206
- });
1207
- netServer.on("error", (err) => {
1208
- log.error({ err }, "Net server error.");
1209
- process.exit(1);
1210
- });
1211
- netServer.listen(SOCK_PATH, () => {
1212
- try {
1213
- if (process.platform !== "win32") chmodSync(SOCK_PATH, 384);
1214
- } catch (err) {
1215
- log.error({ err }, "Failed to set socket permissions. Shutting down.");
1216
- process.exit(1);
1217
- }
1218
- log.info({ sock: SOCK_PATH }, "Hub socket ready.");
1219
- });
1220
- async function startWebSocketServer() {
1221
- for (const candidate of wsPortCandidates) {
1222
- const server = new WebSocketServer({
1223
- host: "127.0.0.1",
1224
- port: candidate,
1225
- maxPayload: maxPayloadBytes
1226
- });
1227
- try {
1228
- await new Promise((resolve2, reject2) => {
1229
- const onError = (err) => {
1230
- server.off("listening", onListening);
1231
- reject2(err);
1232
- };
1233
- const onListening = () => {
1234
- server.off("error", onError);
1235
- resolve2();
1236
- };
1237
- server.once("error", onError);
1238
- server.once("listening", onListening);
1239
- });
1240
- return { wss: server, port: candidate };
1241
- } catch (err) {
1242
- server.close();
1243
- const errno = err;
1244
- if (errno.code === "EADDRINUSE") {
1245
- log.warn({ port: candidate }, "WebSocket port in use, trying next candidate.");
1246
- continue;
1247
- }
1248
- log.error({ err: errno, port: candidate }, "Failed to start WebSocket server.");
1249
- process.exit(1);
1250
- }
1251
- }
1252
- log.error(
1253
- { candidates: wsPortCandidates },
1254
- "Unable to start WebSocket server on any candidate port."
1255
- );
1256
- process.exit(1);
1257
- }
1258
- var { wss, port } = await startWebSocketServer();
1259
- selectedWsPort = port;
1260
- wss.on("error", (err) => {
1261
- log.error({ err }, "WebSocket server critical error. Exiting.");
1262
- process.exit(1);
1263
- });
1264
- wss.on("connection", (ws) => {
1265
- const ext = { id: nanoid3(), ws, active: false };
1266
- extensions.push(ext);
1267
- log.info({ id: ext.id }, `Extension connected. Total: ${extensions.length}`);
1268
- const message = { type: "registered", id: ext.id };
1269
- ws.send(JSON.stringify(message));
1270
- broadcastState();
1271
- scheduleAutoActivate();
1272
- ws.on("message", (raw, isBinary) => {
1273
- if (isBinary) {
1274
- log.warn({ extId: ext.id }, "Unexpected binary message received.");
1275
- return;
1276
- }
1277
- const messageBuffer = rawDataToBuffer(raw);
1278
- let parsedJson;
1279
- try {
1280
- parsedJson = JSON.parse(messageBuffer.toString("utf-8"));
1281
- } catch (e) {
1282
- log.warn({ err: e, extId: ext.id }, "Failed to parse message.");
1283
- return;
1284
- }
1285
- const parseResult = MessageFromExtensionSchema.safeParse(parsedJson);
1286
- if (!parseResult.success) {
1287
- log.warn({ error: parseResult.error.flatten(), extId: ext.id }, "Invalid message shape.");
1288
- return;
1289
- }
1290
- const msg = parseResult.data;
1291
- switch (msg.type) {
1292
- case "activate": {
1293
- setActive(ext.id);
1294
- log.info({ id: ext.id }, "Extension activated.");
1295
- broadcastState();
1296
- scheduleAutoActivate();
1297
- break;
1298
- }
1299
- case "toolResult": {
1300
- const { id, payload, error } = msg;
1301
- if (error) {
1302
- reject(id, error instanceof Error ? error : new Error(String(error)));
1303
- } else {
1304
- resolve(id, payload);
1305
- }
1306
- break;
1307
- }
1308
- }
1309
- });
1310
- ws.on("close", () => {
1311
- const index = extensions.findIndex((e) => e.id === ext.id);
1312
- if (index > -1) extensions.splice(index, 1);
1313
- log.info({ id: ext.id }, `Extension disconnected. Remaining: ${extensions.length}`);
1314
- cleanupForExtension(ext.id);
1315
- if (ext.active) {
1316
- log.warn({ id: ext.id }, "Active extension disconnected.");
1317
- setActive(null);
1318
- }
1319
- broadcastState();
1320
- scheduleAutoActivate();
1321
- });
1322
- });
1323
- log.info({ port: selectedWsPort }, "WebSocket server ready.");
1324
- process.on("SIGINT", shutdown);
1325
- process.on("SIGTERM", shutdown);
1326
- //# sourceMappingURL=hub.js.map