@ait-co/devtools 0.1.84 → 0.1.86

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.en.md CHANGED
@@ -106,7 +106,7 @@ For environments 3 and 4 (intoss-private relay), the relay QR deep-link carries
106
106
 
107
107
  **"QR window doesn't open"**
108
108
 
109
- Either `build_attach_url` wasn't called first, or `open_in_browser` failed silently in a headless environment. The terminal output includes a path to a saved PNG open that file directly, or scan the text QR printed in the terminal. (Related: [#288](https://github.com/apps-in-toss-community/devtools/issues/288))
109
+ Either `build_attach_url` wasn't called first, or the MCP server is running in a headless environment where no browser can be opened. The tool result always includes a text QR scan it directly with your phone camera. On a local GUI machine, the dashboard opens automatically in the browser.
110
110
 
111
111
  **"Page not attached" — list_pages returns an empty array**
112
112
 
package/README.md CHANGED
@@ -106,7 +106,7 @@ import '@ait-co/devtools/in-app/auto';
106
106
 
107
107
  **"QR 창이 안 열림"**
108
108
 
109
- `build_attach_url`을 먼저 호출하지 않았거나, GUI 없는 headless 환경에서 `open_in_browser`가 실패한 경우입니다. 터미널에 PNG 저장 경로가 출력됩니다 파일을 직접 열거나, 텍스트 QR을 터미널에서 스캔하세요. (관련: [#288](https://github.com/apps-in-toss-community/devtools/issues/288))
109
+ `build_attach_url`을 먼저 호출하지 않았거나, GUI 없는 headless 환경이라 대시보드를 수 없는 경우입니다. 도구 결과에 텍스트 QR이 출력되므로 카메라로 직접 스캔하세요. 로컬 GUI 환경에서는 대시보드가 자동으로 브라우저에 열립니다.
110
110
 
111
111
  **"page 미attach" — list_pages가 빈 배열 반환**
112
112
 
package/dist/mcp/cli.js CHANGED
@@ -3761,7 +3761,7 @@ const DEBUG_TOOL_DEFINITIONS = [
3761
3761
  },
3762
3762
  {
3763
3763
  name: "build_attach_url",
3764
- description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep-link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep-link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). When projectRoot is given, the app name from <projectRoot>/package.json is automatically added as name= so the launcher partner bar shows it. Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). \n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes (the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.\n\nselfdebug (env 2 / relay-sandbox only): pass selfdebug=true to add &selfdebug=1 to the launcher deep-link. The launcher PWA then registers its own document as the CDP target instead of the framed mini-app. SINGLE-ATTACH MODEL: attaching the launcher self-target evicts any currently-attached mini-app target — use this mode exclusively for diagnosing the launcher document itself (DOM, safe-area, console). Not applicable in env 3/4 (relay-staging/relay-live) — passing selfdebug=true there returns an error.",
3764
+ description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep-link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep-link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). When projectRoot is given, the app name from <projectRoot>/package.json is automatically added as name= so the launcher partner bar shows it. Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (default 60 s, adjustable via wait_timeout_seconds). On timeout, call build_attach_url again to resume polling. The server automatically opens the QR dashboard in the OS default browser when running on a local GUI machine headless/remote environments fall back to the text QR in the tool output.\n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes (the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.\n\nselfdebug (env 2 / relay-sandbox only): pass selfdebug=true to add &selfdebug=1 to the launcher deep-link. The launcher PWA then registers its own document as the CDP target instead of the framed mini-app. SINGLE-ATTACH MODEL: attaching the launcher self-target evicts any currently-attached mini-app target — use this mode exclusively for diagnosing the launcher document itself (DOM, safe-area, console). Not applicable in env 3/4 (relay-staging/relay-live) — passing selfdebug=true there returns an error.",
3765
3765
  inputSchema: {
3766
3766
  type: "object",
3767
3767
  properties: {
@@ -3771,11 +3771,11 @@ const DEBUG_TOOL_DEFINITIONS = [
3771
3771
  },
3772
3772
  wait_for_attach: {
3773
3773
  type: "boolean",
3774
- description: "If true, block after returning the QR until a page attaches to the relay (polls listTargets ~1 s interval, timeout 30 s). On attach, the response includes the attached page list. On timeout, call build_attach_url again to resume polling."
3774
+ description: "If true, block after returning the QR until a page attaches to the relay (polls listTargets ~1 s interval, default 60 s). On attach, the response includes the attached page list. On timeout, call build_attach_url again to resume polling."
3775
3775
  },
3776
- open_in_browser: {
3777
- type: "boolean",
3778
- description: "If true (default), render the QR as a PNG and open it in the OS default browser. Only works when the MCP server is running on a local GUI machine — headless or remote container environments should set this to false to use the text QR fallback."
3776
+ wait_timeout_seconds: {
3777
+ type: "number",
3778
+ description: "Maximum seconds to wait when wait_for_attach=true (default 60, range 1–600). Values outside the range or invalid inputs (0, negative, NaN) fall back to the default silently. Only meaningful when wait_for_attach=true."
3779
3779
  },
3780
3780
  projectRoot: {
3781
3781
  type: "string",
@@ -4779,7 +4779,7 @@ async function readMcpSdkVersion() {
4779
4779
  * some test environments that skip the build step).
4780
4780
  */
4781
4781
  function readDevtoolsVersion() {
4782
- return "0.1.84";
4782
+ return "0.1.86";
4783
4783
  }
4784
4784
  /**
4785
4785
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -4855,9 +4855,14 @@ async function getDiagnostics(input) {
4855
4855
  reissueAttempts: tunnel.reissueAttempts ?? 0
4856
4856
  };
4857
4857
  let pages = null;
4858
- if (connection !== void 0) try {
4859
- pages = listPages(connection, tunnel);
4860
- } catch {}
4858
+ if (connection !== void 0) {
4859
+ try {
4860
+ await connection.refreshTargets?.();
4861
+ } catch {}
4862
+ try {
4863
+ pages = listPages(connection, tunnel);
4864
+ } catch {}
4865
+ }
4861
4866
  const limit = Math.min(Math.max(1, recentErrorsLimit), 50);
4862
4867
  const recentErrors = collector.getRecentErrors(limit);
4863
4868
  const authRejects = collector.getAuthRejects();
@@ -5275,7 +5280,7 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
5275
5280
  * naturally via `enableDomains`). The tier only controls visibility.
5276
5281
  */
5277
5282
  function createDebugServer(deps) {
5278
- const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret, onAttachUrlBuilt } = deps;
5283
+ const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 6e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret, onAttachUrlBuilt } = deps;
5279
5284
  const getTotpSecret = deps.getTotpSecret ?? (() => totpSecret);
5280
5285
  const router = routerDep ?? makeSingleConnectionRouter(connection);
5281
5286
  const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin));
@@ -5283,7 +5288,7 @@ function createDebugServer(deps) {
5283
5288
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
5284
5289
  const server = new Server({
5285
5290
  name: "ait-debug",
5286
- version: "0.1.84"
5291
+ version: "0.1.86"
5287
5292
  }, { capabilities: { tools: { listChanged: true } } });
5288
5293
  server.setRequestHandler(ListToolsRequestSchema, () => {
5289
5294
  const conn = router.active;
@@ -5356,8 +5361,14 @@ function createDebugServer(deps) {
5356
5361
  }
5357
5362
  if (name === "build_attach_url") {
5358
5363
  const waitForAttach = request.params.arguments?.wait_for_attach === true;
5359
- const openInBrowser = request.params.arguments?.open_in_browser !== false;
5360
5364
  const selfdebug = request.params.arguments?.selfdebug === true;
5365
+ const rawWaitTimeout = request.params.arguments?.wait_timeout_seconds;
5366
+ const callTimeoutMs = (() => {
5367
+ if (typeof rawWaitTimeout !== "number" || !Number.isFinite(rawWaitTimeout)) return waitForAttachTimeoutMs;
5368
+ const clamped = Math.max(1, Math.min(600, rawWaitTimeout));
5369
+ if (rawWaitTimeout <= 0) return waitForAttachTimeoutMs;
5370
+ return Math.round(clamped) * 1e3;
5371
+ })();
5361
5372
  if (selfdebug && env !== "relay-mobile") return mcpError("build_attach_url: selfdebug=true는 env 2 / relay-sandbox 전용 기능입니다. 현재 환경(env 3/4)에서는 launcher가 없어 self-target 모드를 지원하지 않습니다. launcher self-target이 필요하다면 relay-sandbox 모드로 재시작하세요.");
5362
5373
  if (env === "relay-mobile") {
5363
5374
  const rawBuildProjectRoot = request.params.arguments?.projectRoot;
@@ -5414,8 +5425,8 @@ function createDebugServer(deps) {
5414
5425
  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).";
5415
5426
  const warningPrefix = "";
5416
5427
  const guiAvailable = canOpenBrowser();
5417
- if (openInBrowser && !guiAvailable) {
5418
- const headlessNote = "[open_in_browser] GUI 환경이 감지되지 않았습니다 (headless/remote 환경). open_in_browser=false로 자동 폴백합니다. 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
5428
+ if (!guiAvailable) {
5429
+ const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
5419
5430
  const qrHeadless = await renderQr(attachUrl);
5420
5431
  const headlessText = `${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
5421
5432
  attachUrl,
@@ -5428,13 +5439,13 @@ function createDebugServer(deps) {
5428
5439
  }] };
5429
5440
  let attachedPagesHl = [];
5430
5441
  try {
5431
- attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
5442
+ attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
5432
5443
  } catch {
5433
5444
  attachedPagesHl = conn.listTargets();
5434
5445
  return {
5435
5446
  content: [{
5436
5447
  type: "text",
5437
- text: buildTimeoutError(headlessText, waitForAttachTimeoutMs / 1e3, attachedPagesHl)
5448
+ text: buildTimeoutError(headlessText, callTimeoutMs / 1e3, attachedPagesHl)
5438
5449
  }],
5439
5450
  isError: true
5440
5451
  };
@@ -5445,7 +5456,7 @@ function createDebugServer(deps) {
5445
5456
  text: `${headlessText}\n\n${JSON.stringify(pagesResultHl, null, 2)}`
5446
5457
  }] };
5447
5458
  }
5448
- if (openInBrowser && guiAvailable && qrHttpServer) {
5459
+ if (guiAvailable && qrHttpServer) {
5449
5460
  const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
5450
5461
  if (browserResult.opened) {
5451
5462
  const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
@@ -5465,13 +5476,13 @@ function createDebugServer(deps) {
5465
5476
  }] };
5466
5477
  let attachedPages = [];
5467
5478
  try {
5468
- attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
5479
+ attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
5469
5480
  } catch {
5470
5481
  attachedPages = conn.listTargets();
5471
5482
  return {
5472
5483
  content: [{
5473
5484
  type: "text",
5474
- text: buildTimeoutError(shortText, waitForAttachTimeoutMs / 1e3, attachedPages)
5485
+ text: buildTimeoutError(shortText, callTimeoutMs / 1e3, attachedPages)
5475
5486
  }],
5476
5487
  isError: true
5477
5488
  };
@@ -5490,7 +5501,7 @@ function createDebugServer(deps) {
5490
5501
  ...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
5491
5502
  };
5492
5503
  const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
5493
- const fallbackNote = `[open_in_browser] 브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
5504
+ const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
5494
5505
  const qr = await renderQr(attachUrl);
5495
5506
  const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
5496
5507
  attachUrl,
@@ -5504,13 +5515,13 @@ function createDebugServer(deps) {
5504
5515
  }] };
5505
5516
  let attachedPagesFb = [];
5506
5517
  try {
5507
- attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
5518
+ attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
5508
5519
  } catch {
5509
5520
  attachedPagesFb = conn.listTargets();
5510
5521
  return {
5511
5522
  content: [{
5512
5523
  type: "text",
5513
- text: buildTimeoutError(baseText, waitForAttachTimeoutMs / 1e3, attachedPagesFb)
5524
+ text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPagesFb)
5514
5525
  }],
5515
5526
  isError: true
5516
5527
  };
@@ -5533,13 +5544,13 @@ function createDebugServer(deps) {
5533
5544
  }] };
5534
5545
  let attachedPages = [];
5535
5546
  try {
5536
- attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
5547
+ attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
5537
5548
  } catch {
5538
5549
  attachedPages = conn.listTargets();
5539
5550
  return {
5540
5551
  content: [{
5541
5552
  type: "text",
5542
- text: buildTimeoutError(baseText, waitForAttachTimeoutMs / 1e3, attachedPages)
5553
+ text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPages)
5543
5554
  }],
5544
5555
  isError: true
5545
5556
  };
@@ -5585,8 +5596,8 @@ function createDebugServer(deps) {
5585
5596
  const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
5586
5597
  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).";
5587
5598
  const guiAvailable = canOpenBrowser();
5588
- if (openInBrowser && !guiAvailable) {
5589
- const headlessNote = "[open_in_browser] GUI 환경이 감지되지 않았습니다 (headless/remote 환경). open_in_browser=false로 자동 폴백합니다. 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
5599
+ if (!guiAvailable) {
5600
+ const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
5590
5601
  const qrHeadless = await renderQr(attachUrl);
5591
5602
  const headlessText = `${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
5592
5603
  attachUrl,
@@ -5599,13 +5610,13 @@ function createDebugServer(deps) {
5599
5610
  }] };
5600
5611
  let attachedPagesHl = [];
5601
5612
  try {
5602
- attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
5613
+ attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
5603
5614
  } catch {
5604
5615
  attachedPagesHl = conn.listTargets();
5605
5616
  return {
5606
5617
  content: [{
5607
5618
  type: "text",
5608
- text: buildTimeoutError(headlessText, waitForAttachTimeoutMs / 1e3, attachedPagesHl)
5619
+ text: buildTimeoutError(headlessText, callTimeoutMs / 1e3, attachedPagesHl)
5609
5620
  }],
5610
5621
  isError: true
5611
5622
  };
@@ -5616,7 +5627,7 @@ function createDebugServer(deps) {
5616
5627
  text: `${headlessText}\n\n${JSON.stringify(pagesResultHl, null, 2)}`
5617
5628
  }] };
5618
5629
  }
5619
- if (openInBrowser && guiAvailable && qrHttpServer) {
5630
+ if (guiAvailable && qrHttpServer) {
5620
5631
  const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
5621
5632
  if (browserResult.opened) {
5622
5633
  const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
@@ -5636,13 +5647,13 @@ function createDebugServer(deps) {
5636
5647
  }] };
5637
5648
  let attachedPages = [];
5638
5649
  try {
5639
- attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
5650
+ attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
5640
5651
  } catch {
5641
5652
  attachedPages = conn.listTargets();
5642
5653
  return {
5643
5654
  content: [{
5644
5655
  type: "text",
5645
- text: buildTimeoutError(shortText, waitForAttachTimeoutMs / 1e3, attachedPages)
5656
+ text: buildTimeoutError(shortText, callTimeoutMs / 1e3, attachedPages)
5646
5657
  }],
5647
5658
  isError: true
5648
5659
  };
@@ -5661,7 +5672,7 @@ function createDebugServer(deps) {
5661
5672
  ...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
5662
5673
  };
5663
5674
  const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
5664
- const fallbackNote = `[open_in_browser] 브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:
5675
+ const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:
5665
5676
  ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
5666
5677
  const qr = await renderQr(attachUrl);
5667
5678
  const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
@@ -5676,13 +5687,13 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
5676
5687
  }] };
5677
5688
  let attachedPagesFb = [];
5678
5689
  try {
5679
- attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
5690
+ attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
5680
5691
  } catch {
5681
5692
  attachedPagesFb = conn.listTargets();
5682
5693
  return {
5683
5694
  content: [{
5684
5695
  type: "text",
5685
- text: buildTimeoutError(baseText, waitForAttachTimeoutMs / 1e3, attachedPagesFb)
5696
+ text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPagesFb)
5686
5697
  }],
5687
5698
  isError: true
5688
5699
  };
@@ -5705,13 +5716,13 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
5705
5716
  }] };
5706
5717
  let attachedPages = [];
5707
5718
  try {
5708
- attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
5719
+ attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
5709
5720
  } catch {
5710
5721
  attachedPages = conn.listTargets();
5711
5722
  return {
5712
5723
  content: [{
5713
5724
  type: "text",
5714
- text: buildTimeoutError(baseText, waitForAttachTimeoutMs / 1e3, attachedPages)
5725
+ text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPages)
5715
5726
  }],
5716
5727
  isError: true
5717
5728
  };
@@ -7116,11 +7127,11 @@ const DEV_TOOL_DEFINITIONS = [
7116
7127
  },
7117
7128
  wait_for_attach: {
7118
7129
  type: "boolean",
7119
- description: "If true, block until a page attaches."
7130
+ description: "If true, block until a page attaches (default 60 s)."
7120
7131
  },
7121
- open_in_browser: {
7122
- type: "boolean",
7123
- description: "If true (default), open the QR PNG in the OS browser."
7132
+ wait_timeout_seconds: {
7133
+ type: "number",
7134
+ description: "Maximum seconds to wait when wait_for_attach=true (default 60, range 1–600). Invalid inputs fall back to default."
7124
7135
  }
7125
7136
  },
7126
7137
  required: ["scheme_url"]
@@ -7331,7 +7342,7 @@ function createDevServer(deps = {}) {
7331
7342
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
7332
7343
  const server = new Server({
7333
7344
  name: "ait-devtools",
7334
- version: "0.1.84"
7345
+ version: "0.1.86"
7335
7346
  }, { capabilities: { tools: {} } });
7336
7347
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
7337
7348
  server.setRequestHandler(CallToolRequestSchema, async (request) => {