@ait-co/devtools 0.1.37 → 0.1.39

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/mcp/cli.js CHANGED
@@ -649,6 +649,173 @@ async function launchChromium(options = {}) {
649
649
  };
650
650
  }
651
651
  //#endregion
652
+ //#region src/mcp/qr-http-server.ts
653
+ /**
654
+ * 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.
655
+ * MCP debug server 생애주기에 묶어 사용 — `runDebugServer` shutdown 시 `close()`로 정리.
656
+ */
657
+ async function startQrHttpServer() {
658
+ const { default: QRCode } = await import("qrcode");
659
+ const server = createServer((req, res) => {
660
+ const [path, query = ""] = (req.url ?? "/").split("?", 2);
661
+ const params = new URLSearchParams(query ?? "");
662
+ if (path === "/attach") {
663
+ const encodedU = params.get("u") ?? "";
664
+ let attachUrl;
665
+ try {
666
+ attachUrl = decodeURIComponent(encodedU);
667
+ } catch {
668
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
669
+ res.end("잘못된 u 파라미터입니다.");
670
+ return;
671
+ }
672
+ let deploymentIdLabel = "attach";
673
+ try {
674
+ const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);
675
+ if (dpMatch?.[1]) deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);
676
+ } catch {}
677
+ QRCode.toDataURL(attachUrl, {
678
+ type: "image/png",
679
+ errorCorrectionLevel: "M"
680
+ }).then((dataUrl) => {
681
+ const html = buildAttachHtml(dataUrl, deploymentIdLabel.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`), attachUrl.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`));
682
+ res.writeHead(200, {
683
+ "Content-Type": "text/html; charset=utf-8",
684
+ "Cache-Control": "no-store"
685
+ });
686
+ res.end(html);
687
+ }).catch(() => {
688
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
689
+ res.end("QR 생성에 실패했습니다.");
690
+ });
691
+ return;
692
+ }
693
+ if (path === "/qr.png") {
694
+ const encodedU = params.get("u") ?? "";
695
+ let attachUrl;
696
+ try {
697
+ attachUrl = decodeURIComponent(encodedU);
698
+ } catch {
699
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
700
+ res.end("잘못된 u 파라미터입니다.");
701
+ return;
702
+ }
703
+ QRCode.toBuffer(attachUrl, {
704
+ type: "png",
705
+ errorCorrectionLevel: "M"
706
+ }).then((buf) => {
707
+ res.writeHead(200, {
708
+ "Content-Type": "image/png",
709
+ "Cache-Control": "no-store",
710
+ "Content-Length": String(buf.length)
711
+ });
712
+ res.end(buf);
713
+ }).catch(() => {
714
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
715
+ res.end("QR PNG 생성에 실패했습니다.");
716
+ });
717
+ return;
718
+ }
719
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
720
+ res.end("Not Found");
721
+ });
722
+ const listenPort = Number(process.env.AIT_DEBUG_HTTP_PORT ?? 0);
723
+ await new Promise((resolve, reject) => {
724
+ server.listen(listenPort, "127.0.0.1", () => resolve());
725
+ server.once("error", reject);
726
+ });
727
+ const address = server.address();
728
+ if (!address || typeof address === "string") throw new Error("qr-http-server: server.address()가 예상하지 못한 형태입니다.");
729
+ const port = address.port;
730
+ return {
731
+ port,
732
+ buildAttachPageUrl(attachUrl) {
733
+ return `http://127.0.0.1:${port}/attach?u=${encodeURIComponent(attachUrl)}`;
734
+ },
735
+ close() {
736
+ return new Promise((resolve, reject) => {
737
+ server.close((err) => err ? reject(err) : resolve());
738
+ });
739
+ }
740
+ };
741
+ }
742
+ /**
743
+ * QR 스캔 페이지 HTML 본문.
744
+ * dark theme, inline style, 외부 fetch 없음.
745
+ */
746
+ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl) {
747
+ return `<!DOCTYPE html>
748
+ <html lang="ko">
749
+ <head>
750
+ <meta charset="utf-8" />
751
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
752
+ <title>AIT 디버그 세션 — QR 스캔</title>
753
+ <style>
754
+ *, *::before, *::after { box-sizing: border-box; }
755
+ body {
756
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
757
+ background: #0d1117; color: #c9d1d9;
758
+ display: flex; flex-direction: column; align-items: center;
759
+ min-height: 100vh; margin: 0; padding: 2rem 1rem;
760
+ gap: 1.5rem;
761
+ }
762
+ h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
763
+ .label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
764
+ img.qr {
765
+ width: min(90vw, 360px); height: auto;
766
+ image-rendering: pixelated;
767
+ background: #fff; padding: 1rem; border-radius: 12px;
768
+ }
769
+ section { width: 100%; max-width: 480px; }
770
+ h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
771
+ ol, ul { margin: 0; padding-left: 1.25rem; }
772
+ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
773
+ .url-box {
774
+ font-family: monospace; font-size: 0.72rem;
775
+ word-break: break-all; opacity: 0.4;
776
+ background: #161b22; padding: 0.75rem 1rem;
777
+ border-radius: 6px; border: 1px solid #30363d;
778
+ }
779
+ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
780
+ </style>
781
+ </head>
782
+ <body>
783
+ <h1>AIT 디버그 세션 — QR 스캔</h1>
784
+ <p class="label">deployment: ${safeLabel}</p>
785
+ <img class="qr" src="${qrDataUrl}" alt="attach QR" />
786
+
787
+ <section>
788
+ <h2>스캔 절차</h2>
789
+ <ol>
790
+ <li>토스 앱을 실행하세요.</li>
791
+ <li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li>
792
+ <li>팝업이 뜨면 <strong>"토스로 열기"</strong>를 탭하세요.</li>
793
+ <li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li>
794
+ </ol>
795
+ </section>
796
+
797
+ <hr />
798
+
799
+ <section>
800
+ <h2>진단 체크리스트</h2>
801
+ <ul>
802
+ <li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li>
803
+ <li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li>
804
+ <li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li>
805
+ <li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li>
806
+ </ul>
807
+ </section>
808
+
809
+ <hr />
810
+
811
+ <section>
812
+ <h2>URL (fallback)</h2>
813
+ <p class="url-box">${safeAttachUrl}</p>
814
+ </section>
815
+ </body>
816
+ </html>`;
817
+ }
818
+ //#endregion
652
819
  //#region src/mcp/deeplink.ts
653
820
  /**
654
821
  * Build a self-attaching dogfood deep link.
@@ -1009,102 +1176,136 @@ function canOpenBrowser() {
1009
1176
  if (platform === "linux") return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);
1010
1177
  return false;
1011
1178
  }
1179
+ /** platform별 browser open 명령 후보 목록 — 앞에서부터 순차 시도. */
1180
+ function getBrowserCandidates(httpUrl) {
1181
+ const platform = process.platform;
1182
+ if (platform === "darwin") return [
1183
+ {
1184
+ cmd: "open",
1185
+ args: [httpUrl]
1186
+ },
1187
+ {
1188
+ cmd: "open",
1189
+ args: [
1190
+ "-a",
1191
+ "Safari",
1192
+ httpUrl
1193
+ ]
1194
+ },
1195
+ {
1196
+ cmd: "open",
1197
+ args: [
1198
+ "-a",
1199
+ "Google Chrome",
1200
+ httpUrl
1201
+ ]
1202
+ },
1203
+ {
1204
+ cmd: "open",
1205
+ args: [
1206
+ "-a",
1207
+ "Firefox",
1208
+ httpUrl
1209
+ ]
1210
+ }
1211
+ ];
1212
+ if (platform === "win32") return [{
1213
+ cmd: "cmd",
1214
+ args: [
1215
+ "/c",
1216
+ "start",
1217
+ "",
1218
+ httpUrl
1219
+ ]
1220
+ }, {
1221
+ cmd: "rundll32",
1222
+ args: ["url.dll,FileProtocolHandler", httpUrl]
1223
+ }];
1224
+ return [
1225
+ {
1226
+ cmd: "xdg-open",
1227
+ args: [httpUrl]
1228
+ },
1229
+ {
1230
+ cmd: "sensible-browser",
1231
+ args: [httpUrl]
1232
+ },
1233
+ {
1234
+ cmd: "x-www-browser",
1235
+ args: [httpUrl]
1236
+ },
1237
+ {
1238
+ cmd: "firefox",
1239
+ args: [httpUrl]
1240
+ },
1241
+ {
1242
+ cmd: "google-chrome",
1243
+ args: [httpUrl]
1244
+ },
1245
+ {
1246
+ cmd: "chromium",
1247
+ args: [httpUrl]
1248
+ }
1249
+ ];
1250
+ }
1251
+ /** stderr에서 at= TOTP 코드 값을 redact한다. */
1252
+ function redactSecrets(text) {
1253
+ return text.replace(/\bat=([^&\s"']+)/g, "at=<redacted>");
1254
+ }
1255
+ /** spawnSync exit 0이어도 stderr에 launch 실패 시그널이 있으면 실패로 판단한다. */
1256
+ const LAUNCH_FAILURE_PATTERNS = [
1257
+ /LSOpenURLsWithRole\(\) failed/,
1258
+ /kLSApplicationNotFoundErr/,
1259
+ /No application/,
1260
+ /Unable to find application/,
1261
+ /xdg-open: not found/,
1262
+ /command not found/
1263
+ ];
1264
+ function isLaunchFailureStderr(stderr) {
1265
+ return LAUNCH_FAILURE_PATTERNS.some((p) => p.test(stderr));
1266
+ }
1012
1267
  /**
1013
- * Writes the attach URL as a QR PNG + a wrapper HTML page to the OS temp
1014
- * directory, then opens the HTML in the OS default browser.
1268
+ * 로컬 HTTP 서버 URL(`http://127.0.0.1:<port>/attach?u=...`)을 OS 기본 브라우저로 연다.
1269
+ *
1270
+ * platform별 fallback chain으로 시도하며, 모두 실패해도 `opened: false` + `httpUrl`을
1271
+ * 반환해 사용자가 직접 브라우저에 붙여넣을 수 있게 한다.
1015
1272
  *
1016
1273
  * SECRET-HANDLING:
1017
- * - File names are derived from a short timestamp, NOT from the attach URL or
1018
- * any token/code value. The `at=` code is NOT in the file name.
1019
- * - The attach URL (which may carry `at=`) is embedded inside the HTML page
1020
- * body that is the intended delivery channel for the QR.
1021
- * - This function must NOT write the attach URL, deploymentId, or any
1022
- * TOTP code to stdout, stderr, or any log.
1023
- *
1024
- * @param attachUrl - The deep link to encode as a QR. May contain `at=<code>`.
1025
- * @param deploymentId - Optional human-readable label for the HTML page (e.g. UUID substring).
1026
- * Must NOT be derived from the `at=` code value.
1027
- * @returns `OpenQrInBrowserResult` — never throws (errors are returned in `.error`).
1274
+ * - tmp 파일을 만들지 않는다 (HTML/PNG는 HTTP 서버가 메모리에서 응답).
1275
+ * - httpUrl/pngUrl은 127.0.0.1 로컬 전용.
1276
+ * - stderr 캡처 결과에서 at= 코드 값을 redact한 stderrSummary에 포함.
1277
+ * - attachUrl, deploymentId, TOTP 코드를 stdout/stderr/로그에 직접 출력 금지.
1278
+ *
1279
+ * @param httpUrl - `http://127.0.0.1:<port>/attach?u=<encoded>` HTTP URL.
1280
+ * @param pngUrl - `http://127.0.0.1:<port>/qr.png?u=<encoded>` PNG fallback URL.
1028
1281
  */
1029
- async function openQrInBrowser(attachUrl, deploymentId) {
1030
- const { tmpdir } = await import("node:os");
1031
- const { writeFileSync } = await import("node:fs");
1032
- const { join } = await import("node:path");
1282
+ async function openQrInBrowser(httpUrl, pngUrl) {
1033
1283
  const { spawnSync } = await import("node:child_process");
1034
- const { default: QRCode } = await import("qrcode");
1035
- const stamp = Date.now();
1036
- const pngPath = join(tmpdir(), `ait-qr-${stamp}.png`);
1037
- const htmlPath = join(tmpdir(), `ait-qr-${stamp}.html`);
1038
- try {
1039
- await QRCode.toFile(pngPath, attachUrl, {
1040
- type: "png",
1041
- errorCorrectionLevel: "M"
1284
+ const candidates = getBrowserCandidates(httpUrl);
1285
+ const stderrLines = [];
1286
+ for (const { cmd, args } of candidates) {
1287
+ const result = spawnSync(cmd, args, {
1288
+ encoding: "utf8",
1289
+ timeout: 5e3
1042
1290
  });
1043
- } catch (err) {
1044
- return {
1045
- opened: false,
1046
- htmlPath,
1047
- pngPath,
1048
- error: `QR PNG write failed: ${err instanceof Error ? err.message : String(err)}`
1049
- };
1050
- }
1051
- const htmlContent = `<!DOCTYPE html>
1052
- <html lang="ko">
1053
- <head>
1054
- <meta charset="utf-8" />
1055
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1056
- <title>AIT Debug — QR</title>
1057
- <style>
1058
- body { font-family: monospace; background: #111; color: #eee; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; gap: 1.5rem; padding: 2rem; box-sizing: border-box; }
1059
- img { width: min(90vw, 400px); height: auto; image-rendering: pixelated; background: #fff; padding: 1rem; border-radius: 8px; }
1060
- .label { font-size: 0.85rem; opacity: 0.6; }
1061
- .url { font-size: 0.75rem; word-break: break-all; max-width: 60ch; opacity: 0.5; }
1062
- </style>
1063
- </head>
1064
- <body>
1065
- <img src="${pngPath}" alt="QR code" />
1066
- <p class="label">deployment: ${deploymentId ? deploymentId.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`) : "attach"}</p>
1067
- </body>
1068
- </html>`;
1069
- try {
1070
- writeFileSync(htmlPath, htmlContent, "utf8");
1071
- } catch (err) {
1072
- return {
1073
- opened: false,
1074
- htmlPath,
1075
- pngPath,
1076
- error: `HTML write failed: ${err instanceof Error ? err.message : String(err)}`
1291
+ if (result.error) {
1292
+ stderrLines.push(`${cmd}: ${result.error.message}`);
1293
+ continue;
1294
+ }
1295
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
1296
+ if (stderr) stderrLines.push(`${cmd}: ${redactSecrets(stderr.trim())}`);
1297
+ if (result.status === 0 && !isLaunchFailureStderr(stderr)) return {
1298
+ opened: true,
1299
+ httpUrl,
1300
+ pngUrl
1077
1301
  };
1078
1302
  }
1079
- const platform = process.platform;
1080
- let openCmd;
1081
- let openArgs;
1082
- if (platform === "darwin") {
1083
- openCmd = "open";
1084
- openArgs = [htmlPath];
1085
- } else if (platform === "win32") {
1086
- openCmd = "cmd";
1087
- openArgs = [
1088
- "/c",
1089
- "start",
1090
- "",
1091
- htmlPath
1092
- ];
1093
- } else {
1094
- openCmd = "xdg-open";
1095
- openArgs = [htmlPath];
1096
- }
1097
- const spawnResult = spawnSync(openCmd, openArgs, { timeout: 5e3 });
1098
- if (spawnResult.error) return {
1099
- opened: false,
1100
- htmlPath,
1101
- pngPath,
1102
- error: `Browser open failed (${openCmd}): ${spawnResult.error.message}`
1103
- };
1104
1303
  return {
1105
- opened: true,
1106
- htmlPath,
1107
- pngPath
1304
+ opened: false,
1305
+ httpUrl,
1306
+ pngUrl,
1307
+ error: "모든 브라우저 실행 후보가 실패했습니다.",
1308
+ stderrSummary: stderrLines.length > 0 ? stderrLines.join("\n") : void 0
1108
1309
  };
1109
1310
  }
1110
1311
  /** Returns the DOM tree of the attached page (`DOM.getDocument`). */
@@ -1623,10 +1824,10 @@ async function printAttachBanner(input) {
1623
1824
  * naturally via `enableDomains`). The tier only controls visibility.
1624
1825
  */
1625
1826
  function createDebugServer(deps) {
1626
- const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4 } = deps;
1827
+ const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer } = deps;
1627
1828
  const server = new Server({
1628
1829
  name: "ait-debug",
1629
- version: "0.1.37"
1830
+ version: "0.1.39"
1630
1831
  }, { capabilities: { tools: { listChanged: true } } });
1631
1832
  server.setRequestHandler(ListToolsRequestSchema, () => {
1632
1833
  return { tools: connection.listTargets().length > 0 ? DEBUG_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) : DEBUG_TOOL_DEFINITIONS.filter((tool) => BOOTSTRAP_TOOL_NAMES.has(tool.name)).map((tool) => ({ ...tool })) };
@@ -1666,15 +1867,10 @@ function createDebugServer(deps) {
1666
1867
  const { attachUrl, relayUrl, authorityWarning } = buildAttachUrl(schemeUrl, getTunnelStatus());
1667
1868
  const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
1668
1869
  const header = "This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).";
1669
- if (openInBrowser && canOpenBrowser()) {
1670
- let deploymentIdLabel;
1671
- try {
1672
- const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);
1673
- if (dpMatch?.[1]) deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);
1674
- } catch {}
1675
- const browserResult = await openQrInBrowser(attachUrl, deploymentIdLabel);
1870
+ if (openInBrowser && canOpenBrowser() && qrHttpServer) {
1871
+ const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
1676
1872
  if (browserResult.opened) {
1677
- const shortText = `${warningPrefix}${header}\n${JSON.stringify({ relayUrl }, null, 2)}\n\nブラウザにQRを表示しました。\nQR画像: ${browserResult.pngPath}\nHTMLページ: ${browserResult.htmlPath}\n\n브라우저에 QR을 띄웠습니다. 스마트폰 카메라로 스캔하세요.\nPNG: ${browserResult.pngPath}`;
1873
+ const shortText = `${warningPrefix}${header}\n${JSON.stringify({ relayUrl }, null, 2)}\n\n브라우저에서 QR을 열었습니다. 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`;
1678
1874
  if (!waitForAttach) return { content: [{
1679
1875
  type: "text",
1680
1876
  text: shortText
@@ -1701,7 +1897,8 @@ function createDebugServer(deps) {
1701
1897
  text: `${shortText}\n\n${JSON.stringify(pagesResult, null, 2)}`
1702
1898
  }] };
1703
1899
  }
1704
- const fallbackNote = `(브라우저 열기 실패: ${browserResult.error ?? "unknown"} — 텍스트 QR로 대체)\n`;
1900
+ const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
1901
+ const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
1705
1902
  const qr = await renderQr(attachUrl);
1706
1903
  const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
1707
1904
  attachUrl,
@@ -1925,26 +2122,37 @@ async function runDebugServer(options = {}) {
1925
2122
  wssUrl: null
1926
2123
  };
1927
2124
  generateAttachToken();
1928
- try {
1929
- tunnel = await startQuickTunnel(relay.port);
2125
+ startQuickTunnel(relay.port).then((t) => {
2126
+ tunnel = t;
1930
2127
  tunnelStatus = {
1931
2128
  up: true,
1932
- wssUrl: tunnel.wssUrl
2129
+ wssUrl: t.wssUrl
1933
2130
  };
1934
- await printAttachBanner({
1935
- wssUrl: tunnel.wssUrl,
2131
+ return printAttachBanner({
2132
+ wssUrl: t.wssUrl,
1936
2133
  totpEnabled
1937
2134
  });
1938
- } catch (err) {
2135
+ }, (err) => {
1939
2136
  const message = err instanceof Error ? err.message : String(err);
1940
2137
  process.stderr.write(`[ait-debug] Failed to open cloudflared quick tunnel: ${message}\n[ait-debug] The relay is up locally; attach over the public URL is unavailable until the tunnel starts.
1941
2138
  `);
1942
- }
2139
+ });
1943
2140
  const connection = new ChiiCdpConnection({ relayBaseUrl: relay.baseUrl });
2141
+ const aitSource = new ChiiAitSource(connection);
2142
+ let qrServer;
2143
+ startQrHttpServer().then((s) => {
2144
+ qrServer = s;
2145
+ }, (err) => {
2146
+ const message = err instanceof Error ? err.message : String(err);
2147
+ process.stderr.write(`[ait-debug] QR HTTP 서버 시작 실패 (text QR fallback 사용): ${message}\n`);
2148
+ });
1944
2149
  const server = createDebugServer({
1945
2150
  connection,
1946
- aitSource: new ChiiAitSource(connection),
1947
- getTunnelStatus: () => tunnelStatus
2151
+ aitSource,
2152
+ getTunnelStatus: () => tunnelStatus,
2153
+ get qrHttpServer() {
2154
+ return qrServer;
2155
+ }
1948
2156
  });
1949
2157
  const transport = new StdioServerTransport();
1950
2158
  let closed = false;
@@ -1957,6 +2165,7 @@ async function runDebugServer(options = {}) {
1957
2165
  tunnel?.stop();
1958
2166
  relay.close();
1959
2167
  server.close();
2168
+ qrServer?.close();
1960
2169
  };
1961
2170
  process.once("SIGINT", shutdown);
1962
2171
  process.once("SIGTERM", shutdown);
@@ -2169,7 +2378,7 @@ function createDevServer(deps = {}) {
2169
2378
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
2170
2379
  const server = new Server({
2171
2380
  name: "ait-devtools",
2172
- version: "0.1.37"
2381
+ version: "0.1.39"
2173
2382
  }, { capabilities: { tools: {} } });
2174
2383
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
2175
2384
  server.setRequestHandler(CallToolRequestSchema, async (request) => {