@ait-co/devtools 0.1.44 → 0.1.45

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
@@ -850,6 +850,28 @@ var AutoDevtoolsOpener = class {
850
850
  //#endregion
851
851
  //#region src/mcp/environment.ts
852
852
  /**
853
+ * Returns `true` when the environment is any relay variant (`relay-dev` or
854
+ * `relay-live`). Use this instead of `env === 'relay'` for tier checks.
855
+ */
856
+ function isRelayEnv(env) {
857
+ return env === "relay-dev" || env === "relay-live";
858
+ }
859
+ /**
860
+ * Returns `true` when the environment is the LIVE relay (`relay-live`).
861
+ * This is the guard condition for side-effect tool protection.
862
+ */
863
+ function isLiveRelayEnv(env) {
864
+ return env === "relay-live";
865
+ }
866
+ /**
867
+ * Maps the new `McpEnvironment` union to the legacy two-value union
868
+ * (`'mock' | 'relay'`) for backward-compatible fields in diagnostics output.
869
+ */
870
+ function toLegacyEnv(env) {
871
+ if (env === "mock") return "mock";
872
+ return "relay";
873
+ }
874
+ /**
853
875
  * URL patterns that mark a CDP target as a real-device WebView relay.
854
876
  *
855
877
  * - `intoss-private://` is the Toss in-app private scheme — only ever observed
@@ -874,28 +896,43 @@ function isRelayUrl(url) {
874
896
  * regardless of env vars or connection state. Cleared with `null`.
875
897
  */
876
898
  let envOverride = null;
877
- /** Parses the `MCP_ENV` env var into a `McpEnvironment` if valid. */
899
+ /**
900
+ * Parses the `MCP_ENV` env var into a `McpEnvironment` if valid.
901
+ *
902
+ * Accepted values:
903
+ * - `mock` → `mock`
904
+ * - `relay-dev` → `relay-dev`
905
+ * - `relay-live` → `relay-live`
906
+ * - `relay` → `relay-dev` (backward-compat alias — resolves to relay-dev)
907
+ *
908
+ * Any other value is ignored and falls through to the next precedence step.
909
+ */
878
910
  function readEnvVar() {
879
911
  const raw = process.env.MCP_ENV;
880
- if (raw === "mock" || raw === "relay") return raw;
912
+ if (raw === "mock") return "mock";
913
+ if (raw === "relay-dev") return "relay-dev";
914
+ if (raw === "relay-live") return "relay-live";
915
+ if (raw === "relay") return "relay-dev";
881
916
  }
882
917
  /**
883
918
  * Returns the current MCP environment, applying the precedence rules:
884
919
  * 1. test override (if set)
885
920
  * 2. `MCP_ENV` env var
886
- * 3. CDP target URL pattern match
887
- * 4. default `mock`
921
+ * 3. CDP target URL pattern match → `relay-dev` (conservative — LIVE
922
+ * requires explicit MCP_ENV=relay-live opt-in)
923
+ * 4. caller-stated `defaultEnv` (intent hint from the CLI mode)
924
+ * 5. baked-in default `mock`
888
925
  */
889
926
  function getEnvironment(input = {}) {
890
927
  if (envOverride !== null) return envOverride;
891
928
  const fromEnv = readEnvVar();
892
929
  if (fromEnv !== void 0) return fromEnv;
893
- const { connection } = input;
930
+ const { connection, defaultEnv } = input;
894
931
  if (connection !== void 0) {
895
932
  const targets = connection.listTargets();
896
- for (const t of targets) if (isRelayUrl(t.url)) return "relay";
933
+ for (const t of targets) if (isRelayUrl(t.url)) return "relay-dev";
897
934
  }
898
- return "mock";
935
+ return defaultEnv ?? "mock";
899
936
  }
900
937
  /**
901
938
  * Returns the `EnvironmentReason` that drove the current `getEnvironment()`
@@ -904,15 +941,23 @@ function getEnvironment(input = {}) {
904
941
  * secret value is ever returned.
905
942
  */
906
943
  function getEnvironmentReason(input = {}) {
907
- if (envOverride !== null) return envOverride === "mock" ? "env-var-mock" : "env-var-relay";
944
+ if (envOverride !== null) {
945
+ if (envOverride === "mock") return "env-var-mock";
946
+ if (envOverride === "relay-live") return "env-var-relay-live";
947
+ return "env-var-relay-dev";
948
+ }
949
+ const rawVar = process.env.MCP_ENV;
908
950
  const fromEnv = readEnvVar();
909
951
  if (fromEnv === "mock") return "env-var-mock";
910
- if (fromEnv === "relay") return "env-var-relay";
911
- const { connection } = input;
952
+ if (fromEnv === "relay-live") return "env-var-relay-live";
953
+ if (fromEnv === "relay-dev") return rawVar === "relay" ? "env-var-relay-compat" : "env-var-relay-dev";
954
+ const { connection, defaultEnv } = input;
912
955
  if (connection !== void 0) {
913
956
  const targets = connection.listTargets();
914
957
  for (const t of targets) if (isRelayUrl(t.url)) return "cdp-target-url-relay-pattern";
915
958
  }
959
+ if (defaultEnv === "relay-live") return "default-relay-live";
960
+ if (defaultEnv === "relay-dev") return "default-relay-dev";
916
961
  return "default-mock";
917
962
  }
918
963
  //#endregion
@@ -940,7 +985,7 @@ function mcpError(message) {
940
985
  * @param reason - 환경이 결정된 근거 (EnvironmentReason 문자열).
941
986
  */
942
987
  function tierRejectionError(toolName, requiredEnv, currentEnv, reason) {
943
- return mcpError(`${`${toolName}은 ${requiredEnv === "relay" ? "relay (실기기 연결)" : "mock (로컬 브라우저)"} 환경에서만 사용할 수 있습니다. 현재 환경: ${currentEnv === "relay" ? "relay" : "mock"} (${reason}). ${requiredEnv === "relay" ? "build_attach_url → QR 스캔으로 실기기를 attach하세요." : "MCP_ENV=mock 또는 relay 환경변수를 확인하세요."}`}\n\n${`tool ${toolName} is available only in ${requiredEnv}. Current environment is ${currentEnv} (${reason}).`}`);
988
+ return mcpError(`${`${toolName}은 ${requiredEnv === "relay" ? "relay (실기기 연결)" : "mock (로컬 브라우저)"} 환경에서만 사용할 수 있습니다. 현재 환경: ${currentEnv === "relay" ? "relay" : "mock"} (${reason}). ${requiredEnv === "relay" ? "relay로 전환하려면 MCP_ENV=relay 설정 후 서버를 재시작하고 build_attach_url → QR 스캔으로 실기기를 attach하세요." : "mock으로 전환하려면 MCP_ENV=mock 설정 서버를 재시작하세요."}`}\n\n${`tool ${toolName} is available only in ${requiredEnv}. Current environment is ${currentEnv} (${reason}).`}`);
944
989
  }
945
990
  /**
946
991
  * 상태 1: tunnel 미가동 — cloudflared 터널이 아직 뜨지 않았다.
@@ -956,7 +1001,7 @@ function tunnelDownError() {
956
1001
  * enableDomains()가 "No mini-app page attached" 에러를 던질 때.
957
1002
  */
958
1003
  function pageMissingError(toolName) {
959
- return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 attach 안 됨. build_attach_url로 deep link를 생성하고 QR을 스캔해 미니앱을 attach하세요.`);
1004
+ return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 attach 안 됨. dogfood 번들 배포 build_attach_url을 호출해 QR을 생성하세요: \`ait deploy --scheme-only\` → \`build_attach_url(scheme_url)\` → QR 스캔.`);
960
1005
  }
961
1006
  /**
962
1007
  * 상태 3: page crash — 연결됐던 페이지가 crash/destroy됐다.
@@ -973,7 +1018,24 @@ function pageCrashError(toolName) {
973
1018
  * call_sdk 호출 시 브리지가 없을 때.
974
1019
  */
975
1020
  function sdkAbsentError(toolName) {
976
- return mcpError(`${toolName ? `${toolName}: ` : ""}window.__sdkCall이 주입되지 않았습니다 (dogfood 빌드가 아닙니다). dogfood 채널(intoss-private)로 번들을 재배포한 재시도하세요.`);
1021
+ return mcpError(`${toolName ? `${toolName}: ` : ""}window.__sdkCall이 주입되지 않았습니다 (dogfood 빌드가 아닙니다). dogfood 채널(intoss-private)로 재배포 QR을 다시 스캔하세요: \`ait build && aitcc app deploy\`.`);
1022
+ }
1023
+ /**
1024
+ * relay-live 환경에서 side-effect 도구(`call_sdk`, `evaluate`)를 `confirm: true`
1025
+ * 없이 호출했을 때 반환하는 거부 메시지.
1026
+ *
1027
+ * 다음 행동을 두 가지로 제시한다:
1028
+ * 1. 같은 호출에 `confirm: true` 인자를 추가해 재시도.
1029
+ * 2. 읽기 전용 환경(relay-dev, mock)으로 전환.
1030
+ */
1031
+ function liveGuardError(toolName) {
1032
+ return mcpError(`[LIVE relay guard] ${toolName}은 현재 relay-live(실 출시 런타임) 세션에서 side-effect 호출입니다. 실유저에게 영향을 줄 수 있어 명시적 동의가 필요합니다.
1033
+
1034
+ 다음 중 하나를 선택하세요:
1035
+ 1. \`confirm: true\` 인자를 추가해 재호출: ${toolName}(…, confirm: true)\n 2. 읽기 전용 도구(list_pages, list_console_messages, take_screenshot 등)를 사용하세요.
1036
+ 3. dogfood 빌드(relay-dev 환경)에서 먼저 검증 후 live에 적용하세요.
1037
+
1038
+ live-guard: MCP_ENV=relay-live + confirm: true missing`);
977
1039
  }
978
1040
  /**
979
1041
  * relay WebSocket 연결이 끊겼을 때 — 크래시가 아닌 네트워크/프로세스 종료.
@@ -1541,7 +1603,9 @@ var ServerLockConflictError = class extends Error {
1541
1603
  existingPid;
1542
1604
  /** wssUrl from the existing lock — may be `null` if the tunnel is still starting. */
1543
1605
  existingWssUrl;
1544
- constructor(existingPid, existingWssUrl) {
1606
+ /** ISO timestamp from the existing lock — when that session started. */
1607
+ existingStartedAt;
1608
+ constructor(existingPid, existingWssUrl, existingStartedAt) {
1545
1609
  const urlNote = existingWssUrl != null ? ` relay URL: ${existingWssUrl}\n` : " relay URL: (tunnel still starting — retry in a moment)\n";
1546
1610
  super(`A debug server is already running (PID ${existingPid}).\n` + urlNote + `Stop the existing session before starting a new one.
1547
1611
  If it is already stopped but this error persists, remove the lock file:
@@ -1549,6 +1613,7 @@ If it is already stopped but this error persists, remove the lock file:
1549
1613
  this.name = "ServerLockConflictError";
1550
1614
  this.existingPid = existingPid;
1551
1615
  this.existingWssUrl = existingWssUrl;
1616
+ this.existingStartedAt = existingStartedAt;
1552
1617
  }
1553
1618
  };
1554
1619
  /** Returns `~/.ait-devtools/server.lock` (or `AIT_DEVTOOLS_LOCK_DIR` override for tests). */
@@ -1601,6 +1666,29 @@ function removeLock(lockPath) {
1601
1666
  } catch {}
1602
1667
  }
1603
1668
  /**
1669
+ * Sends SIGTERM to `pid` and waits up to `graceMs` (default 2 000 ms) for it
1670
+ * to exit; then falls back to SIGKILL. Synchronous — uses a busy-wait loop so
1671
+ * it is usable in the top-level startup path without async plumbing.
1672
+ *
1673
+ * Ignores errors from `process.kill` so that a race where the target exits
1674
+ * between the alive check and the kill call does not crash the caller.
1675
+ */
1676
+ function killAndWait(pid, graceMs = 2e3) {
1677
+ try {
1678
+ process.kill(pid, "SIGTERM");
1679
+ } catch {
1680
+ return;
1681
+ }
1682
+ const deadline = Date.now() + graceMs;
1683
+ while (isPidAlive(pid) && Date.now() < deadline) {
1684
+ const end = Date.now() + 100;
1685
+ while (Date.now() < end);
1686
+ }
1687
+ if (isPidAlive(pid)) try {
1688
+ process.kill(pid, "SIGKILL");
1689
+ } catch {}
1690
+ }
1691
+ /**
1604
1692
  * Reads the current lock file without acquiring it. Returns the parsed
1605
1693
  * `LockData` when the file exists and is valid, otherwise `null`. Used by
1606
1694
  * `get_diagnostics` to surface the `serverLockHolder` field without
@@ -1614,18 +1702,28 @@ function readServerLock() {
1614
1702
  *
1615
1703
  * - If no lock exists (or the lock is stale): writes a new lock and returns a
1616
1704
  * `LockHandle` with `updateWssUrl` + `release`.
1617
- * - If a live process holds the lock: throws `ServerLockConflictError`.
1705
+ * - If a live process holds the lock and `force` is `false` (default): writes
1706
+ * a clear recovery message to stderr and throws `ServerLockConflictError`.
1707
+ * - If a live process holds the lock and `force` is `true`: sends SIGTERM to
1708
+ * that process (waiting up to 2 s then SIGKILL) and takes over the lock.
1618
1709
  *
1619
1710
  * The initial `wssUrl` in the lock file is `null` — call
1620
1711
  * `handle.updateWssUrl(url)` once the cloudflared tunnel is ready.
1621
1712
  */
1622
- function acquireLock() {
1713
+ function acquireLock(options = {}) {
1714
+ const { force = false } = options;
1623
1715
  const lockPath = lockFilePath();
1624
1716
  const existing = readLock(lockPath);
1625
- if (existing !== null) {
1626
- if (isPidAlive(existing.pid)) throw new ServerLockConflictError(existing.pid, existing.wssUrl);
1627
- process.stderr.write(`[ait-debug] stale lock from PID ${existing.pid} recovered — starting fresh.\n`);
1717
+ if (existing !== null) if (isPidAlive(existing.pid)) if (force) {
1718
+ process.stderr.write(`[ait-debug] --force: terminating existing session PID=${existing.pid} …\n`);
1719
+ killAndWait(existing.pid);
1720
+ process.stderr.write(`[ait-debug] --force: PID=${existing.pid} stopped, taking over.\n`);
1721
+ } else {
1722
+ const urlPart = existing.wssUrl != null ? `wssUrl=${existing.wssUrl}` : "wssUrl=(tunnel starting)";
1723
+ process.stderr.write(`[ait-debug] 기존 debug-mode 세션이 이미 실행 중 — PID=${existing.pid}, started ${existing.startedAt}, ${urlPart}\n[ait-debug] 회복: \`kill ${existing.pid}\` 또는 \`npx @ait-co/devtools devtools-mcp --force\`\n`);
1724
+ throw new ServerLockConflictError(existing.pid, existing.wssUrl, existing.startedAt);
1628
1725
  }
1726
+ else process.stderr.write(`[ait-debug] stale lock from PID ${existing.pid} recovered — starting fresh.\n`);
1629
1727
  const data = {
1630
1728
  pid: process.pid,
1631
1729
  wssUrl: null,
@@ -1940,7 +2038,7 @@ const DEBUG_TOOL_DEFINITIONS = [
1940
2038
  },
1941
2039
  {
1942
2040
  name: "list_pages",
1943
- description: "Returns the single active page (at most one) the relay sees attached. When a second page attaches, the previous one is evicted (last-attach wins — single-attach model). The result includes `singleAttachModel: true` so the agent knows the array is always 0 or 1 entries. Also returns whether the cloudflared tunnel is up and the public wss relay URL. The `tunnel` field includes `droppedAt` (ISO timestamp or null/undefined): when non-null the tunnel has permanently dropped after 3 failed reissue attempts — restart the debug server with `npx @ait-co/devtools devtools-mcp`. Each page entry includes a `lastSeenAt` ISO timestamp (last inbound CDP message from that target — useful to detect stale entries when the phone app backgrounded). The result also includes `crashDetectedAt` (ISO timestamp or null): when non-null, a page crash was detected via Inspector.targetCrashed / Target.targetDestroyed since the last attach, the pages list will be empty, and `crashWarning` shows a Korean hint to re-attach. Call this first to confirm a page is attached before reading console/network.",
2041
+ description: "Returns the single active page (at most one) the relay sees attached. When a second page attaches, the previous one is evicted (last-attach wins — single-attach model). The result includes `singleAttachModel: true` so the agent knows the array is always 0 or 1 entries. Also returns whether the cloudflared tunnel is up and the public wss relay URL. The `tunnel` field includes `droppedAt` (ISO timestamp or null/undefined): when non-null the tunnel has permanently dropped after 3 failed reissue attempts — restart the debug server with `npx @ait-co/devtools devtools-mcp`. Each page entry includes a `lastSeenAt` ISO timestamp (last inbound CDP message from that target — useful to detect stale entries when the phone app backgrounded). The result also includes `crashDetectedAt` (ISO timestamp or null): when non-null, a page crash was detected via Inspector.targetCrashed / Target.targetDestroyed since the last attach, the pages list will be empty, and `crashWarning` shows a Korean hint to re-attach. Call this first to confirm a page is attached before reading console/network. When a page attaches or detaches the server emits notifications/tools/list_changed — call tools/list again to get the full updated tool surface.",
1944
2042
  inputSchema: {
1945
2043
  type: "object",
1946
2044
  properties: {},
@@ -1950,7 +2048,7 @@ const DEBUG_TOOL_DEFINITIONS = [
1950
2048
  },
1951
2049
  {
1952
2050
  name: "build_attach_url",
1953
- 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. Turns an `ait deploy --scheme-only` URL (intoss-private://…?_deploymentId=<uuid>) into a self-attaching deep link by splicing in debug=1 and the live relay URL for this session. Returns the deep link JSON and a unicode QR of that deep link. 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). Requires the tunnel to be up — call list_pages first. Set wait_for_attach=true to block until the phone scans and a page attaches (polls listTargets up to 90 s), then returns the attached page info too. 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).",
2051
+ 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. Turns an `ait deploy --scheme-only` URL (intoss-private://…?_deploymentId=<uuid>) into a self-attaching deep link by splicing in debug=1 and the live relay URL for this session. Returns the deep link JSON and a unicode QR of that deep link. 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). Requires the tunnel to be up — call list_pages first. If the tunnel is not up, restart the MCP server: `npx @ait-co/devtools devtools-mcp`. Set wait_for_attach=true to block until the phone scans and a page attaches (polls listTargets up to 30 s by default), then returns the attached page info too. 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). Requires MCP_ENV=relay (set automatically when a relay tunnel is detected).",
1954
2052
  inputSchema: {
1955
2053
  type: "object",
1956
2054
  properties: {
@@ -1960,7 +2058,7 @@ const DEBUG_TOOL_DEFINITIONS = [
1960
2058
  },
1961
2059
  wait_for_attach: {
1962
2060
  type: "boolean",
1963
- description: "If true, block after returning the QR until a page attaches to the relay (polls listTargets ~1 s interval, timeout 90 s). On attach, the response includes the attached page list. On timeout, returns an error with a list_pages retry hint."
2061
+ 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."
1964
2062
  },
1965
2063
  open_in_browser: {
1966
2064
  type: "boolean",
@@ -1993,7 +2091,7 @@ const DEBUG_TOOL_DEFINITIONS = [
1993
2091
  },
1994
2092
  {
1995
2093
  name: "take_screenshot",
1996
- description: "Captures a PNG screenshot of the attached mini-app page over CDP (Page.captureScreenshot) so the agent can see the phone screen directly. Read-only. Returns an image content block.",
2094
+ description: "Captures a PNG screenshot of the attached mini-app page over CDP (Page.captureScreenshot) so the agent can see the phone screen directly. Read-only. Returns an image content block — this is the only debug tool that returns an image; all other debug tools return text (JSON).",
1997
2095
  inputSchema: {
1998
2096
  type: "object",
1999
2097
  properties: {},
@@ -2013,13 +2111,19 @@ const DEBUG_TOOL_DEFINITIONS = [
2013
2111
  },
2014
2112
  {
2015
2113
  name: "evaluate",
2016
- description: "Evaluates an arbitrary JavaScript expression on the attached mini-app page via CDP Runtime.evaluate (returnByValue: true) and returns the result. NOT read-only — the expression can have side effects (DOM mutations, SDK calls, state changes). Requires the relay to be attached — call list_pages first. Throws if the evaluation throws an exception on the page.",
2114
+ description: "Evaluates an arbitrary JavaScript expression on the attached mini-app page via CDP Runtime.evaluate (returnByValue: true) and returns the result. NOT read-only — the expression can have side effects (DOM mutations, SDK calls, state changes). Requires the relay to be attached — call list_pages first. Throws if the evaluation throws an exception on the page.\n\nSECURITY: expression and result are not redacted — never include secrets or auth tokens in the expression.\n\nLIVE guard: when running against a live/production relay (relay-live env, MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that the expression may affect real users. Without it the call is rejected with a structured error. mock and relay-dev sessions are unaffected.",
2017
2115
  inputSchema: {
2018
2116
  type: "object",
2019
- properties: { expression: {
2020
- type: "string",
2021
- description: "JavaScript expression to evaluate in the page context."
2022
- } },
2117
+ properties: {
2118
+ expression: {
2119
+ type: "string",
2120
+ description: "JavaScript expression to evaluate in the page context."
2121
+ },
2122
+ confirm: {
2123
+ type: "boolean",
2124
+ description: "Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge that this expression may have side effects on real/live users. Omitting this in a relay-live session results in a structured rejection error. Has no effect in mock or relay-dev sessions."
2125
+ }
2126
+ },
2023
2127
  required: ["expression"]
2024
2128
  },
2025
2129
  availableIn: "both"
@@ -2039,7 +2143,7 @@ const DEBUG_TOOL_DEFINITIONS = [
2039
2143
  },
2040
2144
  {
2041
2145
  name: "call_sdk",
2042
- description: "Calls a dogfood SDK method via the window.__sdkCall bridge (exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). On env 2/3 (real device relay) this hits the real SDK; on env 1 (local mock) it hits the mock SDK. Requires the relay to be attached — call list_pages first. Returns {ok: true, value} on success or {ok: false, error} on failure. If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], the result also includes `recentException` for crash triage. Returns a clear error if window.__sdkCall is not available (non-dogfood bundle).\n\nIMPORTANT — 인자 시그니처 (잘못된 인자로 호출하면 토스 앱 crash 위험):\n setDeviceOrientation: call_sdk(\"setDeviceOrientation\", [{ type: \"landscape\" }]) // NOT \"landscape\"\n setIosSwipeGestureEnabled: call_sdk(\"setIosSwipeGestureEnabled\", [{ isEnabled: false }])\n setSecureScreen: call_sdk(\"setSecureScreen\", [{ enabled: true }])\n setScreenAwakeMode: call_sdk(\"setScreenAwakeMode\", [{ enabled: true }])\n getOperationalEnvironment: call_sdk(\"getOperationalEnvironment\", [])\n getPlatformOS: call_sdk(\"getPlatformOS\", [])\n getDeviceId: call_sdk(\"getDeviceId\", [])\n getLocale: call_sdk(\"getLocale\", [])\n getNetworkStatus: call_sdk(\"getNetworkStatus\", [])\n getSchemeUri: call_sdk(\"getSchemeUri\", [])\n requestReview: call_sdk(\"requestReview\", [])\n closeView: call_sdk(\"closeView\", [])",
2146
+ description: "Calls a dogfood SDK method via the window.__sdkCall bridge (exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). On env 2/3 (real device relay) this hits the real SDK; on env 1 (local mock) it hits the mock SDK. Requires the relay to be attached — call list_pages first. Returns {ok: true, value} on success or {ok: false, error} on failure. If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], the result also includes `recentException` for crash triage. Returns a clear error if window.__sdkCall is not available (non-dogfood bundle) — redeploy via dogfood channel: `ait build && aitcc app deploy`.\n\nSECURITY: method name, args, and result value are not redacted — never include secrets.\n\nLIVE guard: when running against a live/production relay (relay-live env, MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that the SDK call may affect real users. Without it the call is rejected with a structured error. mock and relay-dev sessions are unaffected.\n\nIMPORTANT — 인자 시그니처 (잘못된 인자로 호출하면 토스 앱 crash 위험):\n setDeviceOrientation: call_sdk(\"setDeviceOrientation\", [{ type: \"landscape\" }]) // NOT \"landscape\"\n setIosSwipeGestureEnabled: call_sdk(\"setIosSwipeGestureEnabled\", [{ isEnabled: false }])\n setSecureScreen: call_sdk(\"setSecureScreen\", [{ enabled: true }])\n setScreenAwakeMode: call_sdk(\"setScreenAwakeMode\", [{ enabled: true }])\n getOperationalEnvironment: call_sdk(\"getOperationalEnvironment\", [])\n getPlatformOS: call_sdk(\"getPlatformOS\", [])\n getDeviceId: call_sdk(\"getDeviceId\", [])\n getLocale: call_sdk(\"getLocale\", [])\n getNetworkStatus: call_sdk(\"getNetworkStatus\", [])\n getSchemeUri: call_sdk(\"getSchemeUri\", [])\n requestReview: call_sdk(\"requestReview\", [])\n closeView: call_sdk(\"closeView\", [])",
2043
2147
  inputSchema: {
2044
2148
  type: "object",
2045
2149
  properties: {
@@ -2051,6 +2155,10 @@ const DEBUG_TOOL_DEFINITIONS = [
2051
2155
  type: "array",
2052
2156
  description: "Arguments to pass to the SDK method (optional, default []).",
2053
2157
  items: {}
2158
+ },
2159
+ confirm: {
2160
+ type: "boolean",
2161
+ description: "Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge that this SDK call may have side effects on real/live users. Omitting this in a relay-live session results in a structured rejection error. Has no effect in mock or relay-dev sessions."
2054
2162
  }
2055
2163
  },
2056
2164
  required: ["name"]
@@ -2089,7 +2197,7 @@ const DEBUG_TOOL_DEFINITIONS = [
2089
2197
  },
2090
2198
  {
2091
2199
  name: "get_diagnostics",
2092
- description: "Returns a single-call server status snapshot so the agent can diagnose \"why is this not working?\" without calling multiple tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), environment (getEnvironment() result + reason), serverLockHolder (pid + startedAt from the lock file, or null). All fields are nullable — missing data is null, not an error. Tier C (both mock and relay). Call this first when debugging session state.",
2200
+ description: "Returns a single-call server status snapshot so the agent can diagnose \"why is this not working?\" without calling multiple tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), environment (kind: mock|relay-dev|relay-live, env: mock|relay backward-compat, reason, liveGuardActive: true when relay-live LIVE guard is active), serverLockHolder (pid + startedAt from the lock file, or null), nextRecommendedAction ({tool, reason} or null — the single next tool to call). All fields are nullable — missing data is null, not an error. debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. Tier C (both mock and relay). Call this first when debugging session state.",
2093
2201
  inputSchema: {
2094
2202
  type: "object",
2095
2203
  properties: { recent_errors_limit: {
@@ -2118,20 +2226,26 @@ function getToolAvailability(name) {
2118
2226
  * Returns true when the named tool is available in the given environment.
2119
2227
  * Unknown tools return `false` — callers should reject them as unknown rather
2120
2228
  * than as env-mismatched.
2229
+ *
2230
+ * Relay variants (`relay-dev`, `relay-live`) both satisfy the `'relay'`
2231
+ * availability tier — `isRelayEnv()` is used for the check.
2121
2232
  */
2122
2233
  function isToolAvailableIn(name, env) {
2123
2234
  const availability = getToolAvailability(name);
2124
2235
  if (availability === void 0) return false;
2125
2236
  if (availability === "both") return true;
2237
+ if (availability === "relay") return isRelayEnv(env);
2126
2238
  return availability === env;
2127
2239
  }
2128
2240
  /**
2129
2241
  * Filters a `DEBUG_TOOL_DEFINITIONS`-shaped list to those whose `availableIn`
2130
2242
  * matches the given env. Pure — preserves order; both Tier C ("both") and the
2131
2243
  * matching single-env tier pass through.
2244
+ *
2245
+ * Relay variants (`relay-dev`, `relay-live`) both satisfy the `'relay'` tier.
2132
2246
  */
2133
2247
  function filterToolsByEnvironment(tools, env) {
2134
- return tools.filter((t) => t.availableIn === "both" || t.availableIn === env);
2248
+ return tools.filter((t) => t.availableIn === "both" || t.availableIn === "relay" && isRelayEnv(env) || t.availableIn === env);
2135
2249
  }
2136
2250
  /**
2137
2251
  * Tool names that are available before any page attaches (bootstrap tier).
@@ -2879,6 +2993,32 @@ function readDevtoolsVersion() {
2879
2993
  }
2880
2994
  }
2881
2995
  /**
2996
+ * Derives the next recommended action from a completed diagnostics snapshot.
2997
+ *
2998
+ * Branch rules (evaluated in priority order):
2999
+ * 1. tunnel.up === false → restart
3000
+ * 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)
3001
+ * 3. pages has entry + crashDetectedAt non-null → build_attach_url (re-attach after crash)
3002
+ * 4. otherwise → null (session looks healthy)
3003
+ *
3004
+ * Pure — does not throw; receives the final assembled snapshot fields.
3005
+ */
3006
+ function computeNextRecommendedAction(tunnel, pages, env) {
3007
+ if (!tunnel.up) return {
3008
+ tool: "restart",
3009
+ reason: "tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart"
3010
+ };
3011
+ if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
3012
+ tool: "build_attach_url",
3013
+ reason: "tunnel ready, no pages attached — call build_attach_url to generate the attach QR"
3014
+ };
3015
+ if (pages !== null && pages.crashDetectedAt !== null) return {
3016
+ tool: "build_attach_url",
3017
+ reason: `page crashed at ${pages.crashDetectedAt} — call build_attach_url to re-attach`
3018
+ };
3019
+ return null;
3020
+ }
3021
+ /**
2882
3022
  * Builds the `get_diagnostics` response. Pure — does not throw; missing data
2883
3023
  * fields are `null`. Async because `readMcpSdkVersion` needs `import()`.
2884
3024
  *
@@ -2909,6 +3049,7 @@ async function getDiagnostics(input) {
2909
3049
  } catch {}
2910
3050
  const limit = Math.min(Math.max(1, recentErrorsLimit), 50);
2911
3051
  const recentErrors = collector.getRecentErrors(limit);
3052
+ const nextRecommendedAction = computeNextRecommendedAction(tunnelInfo, pages, env);
2912
3053
  return {
2913
3054
  mcpVersion,
2914
3055
  devtoolsVersion,
@@ -2918,10 +3059,13 @@ async function getDiagnostics(input) {
2918
3059
  lastDetachAt: collector.getLastDetachAt(),
2919
3060
  recentErrors,
2920
3061
  environment: {
2921
- env,
2922
- reason: envReason
3062
+ kind: env,
3063
+ env: toLegacyEnv(env),
3064
+ reason: envReason,
3065
+ liveGuardActive: isLiveRelayEnv(env)
2923
3066
  },
2924
- serverLockHolder
3067
+ serverLockHolder,
3068
+ nextRecommendedAction
2925
3069
  };
2926
3070
  }
2927
3071
  //#endregion
@@ -3374,13 +3518,19 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
3374
3518
  * naturally via `enableDomains`). The tier only controls visibility.
3375
3519
  */
3376
3520
  function createDebugServer(deps) {
3377
- const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep } = deps;
3378
- const resolveEnvironment = getEnvDep ?? (() => getEnvironment({ connection }));
3379
- const resolveEnvironmentReason = getEnvReasonDep ?? (() => getEnvironmentReason({ connection }));
3521
+ const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, defaultEnv } = deps;
3522
+ const resolveEnvironment = getEnvDep ?? (() => getEnvironment({
3523
+ connection,
3524
+ defaultEnv
3525
+ }));
3526
+ const resolveEnvironmentReason = getEnvReasonDep ?? (() => getEnvironmentReason({
3527
+ connection,
3528
+ defaultEnv
3529
+ }));
3380
3530
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
3381
3531
  const server = new Server({
3382
3532
  name: "ait-debug",
3383
- version: "0.1.44"
3533
+ version: "0.1.45"
3384
3534
  }, { capabilities: { tools: { listChanged: true } } });
3385
3535
  server.setRequestHandler(ListToolsRequestSchema, () => {
3386
3536
  const env = resolveEnvironment();
@@ -3637,13 +3787,16 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
3637
3787
  case "evaluate": {
3638
3788
  const expression = request.params.arguments?.expression;
3639
3789
  if (typeof expression !== "string" || expression === "") return mcpError("evaluate: expression 인자가 비어 있습니다. 평가할 JavaScript 표현식을 전달하세요.");
3790
+ if (isLiveRelayEnv(env) && request.params.arguments?.confirm !== true) return liveGuardError("evaluate");
3640
3791
  return jsonResult$1(await evaluate(connection, expression));
3641
3792
  }
3642
3793
  case "call_sdk": {
3643
3794
  const sdkName = request.params.arguments?.name;
3644
3795
  if (typeof sdkName !== "string" || sdkName === "") return mcpError("call_sdk: name 인자가 비어 있습니다. 호출할 SDK 메서드 이름을 전달하세요.");
3645
3796
  const rawArgs = request.params.arguments?.args;
3646
- const sdkResult = await callSdk(connection, sdkName, Array.isArray(rawArgs) ? rawArgs : []);
3797
+ const sdkArgs = Array.isArray(rawArgs) ? rawArgs : [];
3798
+ if (isLiveRelayEnv(env) && request.params.arguments?.confirm !== true) return liveGuardError("call_sdk");
3799
+ const sdkResult = await callSdk(connection, sdkName, sdkArgs);
3647
3800
  if (!sdkResult.ok && typeof sdkResult.error === "string" && sdkResult.error.startsWith("sdk-absent:")) return sdkAbsentError("call_sdk");
3648
3801
  return jsonResult$1(sdkResult);
3649
3802
  }
@@ -3756,7 +3909,7 @@ function buildRelayVerifyAuth() {
3756
3909
  * 4. expose the debug tools backed by a `ChiiCdpConnection` + `ChiiAitSource`.
3757
3910
  */
3758
3911
  async function runDebugServer(options = {}) {
3759
- const lockHandle = acquireLock();
3912
+ const lockHandle = acquireLock({ force: options.force ?? false });
3760
3913
  const relayPort = options.relayPort ?? 0;
3761
3914
  const verifyAuth = buildRelayVerifyAuth();
3762
3915
  const totpEnabled = verifyAuth !== void 0;
@@ -3821,7 +3974,8 @@ async function runDebugServer(options = {}) {
3821
3974
  get qrHttpServer() {
3822
3975
  return qrServer;
3823
3976
  },
3824
- diagnosticsCollector
3977
+ diagnosticsCollector,
3978
+ defaultEnv: "relay-dev"
3825
3979
  });
3826
3980
  const transport = new StdioServerTransport();
3827
3981
  let closed = false;
@@ -3869,7 +4023,10 @@ async function runDebugServer(options = {}) {
3869
4023
  await server.connect(transport);
3870
4024
  attachWatcher = startAttachWatcher(connection, server, 1e3, () => {
3871
4025
  diagnosticsCollector.recordAttach();
3872
- devtoolsOpener.open(tunnelStatus.wssUrl, getEnvironment({ connection }));
4026
+ devtoolsOpener.open(tunnelStatus.wssUrl, getEnvironment({
4027
+ connection,
4028
+ defaultEnv: "relay-dev"
4029
+ }));
3873
4030
  });
3874
4031
  }
3875
4032
  /**
@@ -3891,7 +4048,7 @@ async function runDebugServer(options = {}) {
3891
4048
  * expected and noted in the PR as an explicit out-of-scope follow-up.
3892
4049
  */
3893
4050
  async function runLocalDebugServer(options = {}) {
3894
- const lockHandle = acquireLock();
4051
+ const lockHandle = acquireLock({ force: options.force ?? false });
3895
4052
  const chromium = await launchChromium({
3896
4053
  port: options.cdpPort ?? 0,
3897
4054
  devUrl: options.devUrl ?? process.env.AIT_DEVTOOLS_URL ?? "http://localhost:5173"
@@ -3906,7 +4063,8 @@ async function runLocalDebugServer(options = {}) {
3906
4063
  const server = createDebugServer({
3907
4064
  connection,
3908
4065
  aitSource,
3909
- getTunnelStatus: () => tunnelStatus
4066
+ getTunnelStatus: () => tunnelStatus,
4067
+ defaultEnv: "mock"
3910
4068
  });
3911
4069
  const transport = new StdioServerTransport();
3912
4070
  let closed = false;
@@ -4011,6 +4169,27 @@ var HttpAitSource = class {
4011
4169
  * (dev). `devtools_get_mock_state` (the original devtools#130 name) is kept as a
4012
4170
  * backward-compatible alias of `AIT.getMockState`.
4013
4171
  *
4172
+ * Issue #305 (M2-1) — dev/debug tool-surface unification:
4173
+ * dev-mode now also exposes `list_pages`, `get_diagnostics`, `measure_safe_area`,
4174
+ * and `call_sdk` so the docs/qa/scenarios.md acceptance sequence
4175
+ * `list_pages → measure_safe_area → call_sdk` works in dev mode without
4176
+ * "Unknown tool" failures.
4177
+ *
4178
+ * - `list_pages` — shim: returns the Vite dev URL as a single-entry array.
4179
+ * - `get_diagnostics` — dumps dev-mode server state (endpoint URL, last fetch
4180
+ * error, reachability, mode/environment metadata).
4181
+ * - `measure_safe_area`— reads safeAreaInsets from the mock state snapshot
4182
+ * (source: 'mock-vite').
4183
+ * - `call_sdk` — reads mock state and builds a mock-equivalent result
4184
+ * using window.__ait.state for supported methods; returns
4185
+ * an explicit tier-filter error for methods that require
4186
+ * a live CDP bridge.
4187
+ * - CDP-only tools (`evaluate`, `take_screenshot`, `get_dom_document`,
4188
+ * `take_snapshot`, `list_console_messages`,
4189
+ * `list_network_requests`, `list_exceptions`) — return an
4190
+ * explicit tier-filter error explaining that CDP is unavailable
4191
+ * in dev-mode and pointing to `--mode=local` or `--mode=debug`.
4192
+ *
4014
4193
  * This module is reached via the `devtools-mcp --mode=dev` CLI entry (see
4015
4194
  * `cli.ts`); the default (no flag) bin mode is the debug-mode CDP/Chii server.
4016
4195
  *
@@ -4025,12 +4204,18 @@ var HttpAitSource = class {
4025
4204
  * }
4026
4205
  * }
4027
4206
  */
4207
+ /** Error message prefix for CDP-dependent tools called in dev-mode. */
4208
+ const CDP_UNAVAILABLE_IN_DEV_MODE = "dev-mode에서는 CDP 연결이 없어 이 도구를 사용할 수 없습니다. 실기기 또는 로컬 Chromium에 붙이려면 `devtools-mcp --mode=local` 또는 `devtools-mcp` (debug 모드 기본)로 전환하세요.";
4028
4209
  /**
4029
4210
  * Tool descriptors served by the dev-mode server.
4030
4211
  *
4031
4212
  * All dev-mode tools are Tier C (both envs) per RFC #277 — the dev-mode server
4032
4213
  * itself is the mock-side embodiment of those Tier C tools. `availableIn` is
4033
4214
  * declared so the surface stays consistent with the debug-mode registry.
4215
+ *
4216
+ * Issue #305: CDP-only tools are also listed with explicit descriptions so
4217
+ * agents do not get "Unknown tool" failures — they get a clear tier-filter
4218
+ * error message instead.
4034
4219
  */
4035
4220
  const DEV_TOOL_DEFINITIONS = [
4036
4221
  {
@@ -4072,30 +4257,290 @@ const DEV_TOOL_DEFINITIONS = [
4072
4257
  required: []
4073
4258
  },
4074
4259
  availableIn: "both"
4260
+ },
4261
+ {
4262
+ name: "list_pages",
4263
+ description: "dev-mode: returns the Vite dev server URL as a single-entry page list. No CDP relay is involved — `tunnel.up` is always false and `devMode: true` marks this as a shim result. Call this first to confirm the dev server is reachable. In debug mode (`devtools-mcp` / `--mode=local`) this returns real attached pages.",
4264
+ inputSchema: {
4265
+ type: "object",
4266
+ properties: {},
4267
+ required: []
4268
+ },
4269
+ availableIn: "both"
4270
+ },
4271
+ {
4272
+ name: "get_diagnostics",
4273
+ description: "dev-mode: returns server diagnostics — Vite endpoint URL, last fetch timestamp/error, mock state endpoint reachability, mode (\"dev\"), and environment metadata. Call this when the dev server connection is suspect. In debug mode this returns tunnel/relay/attach status instead.",
4274
+ inputSchema: {
4275
+ type: "object",
4276
+ properties: { recent_errors_limit: {
4277
+ type: "number",
4278
+ description: "Ignored in dev-mode (no error ring buffer). Present for schema parity."
4279
+ } },
4280
+ required: []
4281
+ },
4282
+ availableIn: "both"
4283
+ },
4284
+ {
4285
+ name: "measure_safe_area",
4286
+ description: "dev-mode: reads safe-area insets from the mock state snapshot via the Vite endpoint. Returns `{ source: \"mock-vite\", sdkInsets, sdkInsetsSource: \"window.__ait\", ... }`. Values reflect what the DevTools panel reports at the time of the last state push. In debug mode this runs a Runtime.evaluate CDP probe on the attached page.",
4287
+ inputSchema: {
4288
+ type: "object",
4289
+ properties: {},
4290
+ required: []
4291
+ },
4292
+ availableIn: "both"
4293
+ },
4294
+ {
4295
+ name: "call_sdk",
4296
+ description: "dev-mode: calls a mock SDK method via the Vite mock state endpoint. Supported methods read from window.__ait mock state (e.g. getOperationalEnvironment). Returns the same `{ok, value}` / `{ok, error}` envelope as debug mode. In debug mode this calls the real SDK via window.__sdkCall over CDP.",
4297
+ inputSchema: {
4298
+ type: "object",
4299
+ properties: {
4300
+ name: {
4301
+ type: "string",
4302
+ description: "Mock SDK method name to call (e.g. \"getOperationalEnvironment\")."
4303
+ },
4304
+ args: {
4305
+ type: "array",
4306
+ description: "Arguments (ignored in dev-mode mock path; present for schema parity).",
4307
+ items: {}
4308
+ }
4309
+ },
4310
+ required: ["name"]
4311
+ },
4312
+ availableIn: "both"
4313
+ },
4314
+ {
4315
+ name: "evaluate",
4316
+ description: "Evaluates an arbitrary JavaScript expression via CDP Runtime.evaluate. NOT available in dev-mode (no CDP connection). Switch to `--mode=local` or `--mode=debug` for CDP access.",
4317
+ inputSchema: {
4318
+ type: "object",
4319
+ properties: { expression: {
4320
+ type: "string",
4321
+ description: "JavaScript expression to evaluate."
4322
+ } },
4323
+ required: ["expression"]
4324
+ },
4325
+ availableIn: "both"
4326
+ },
4327
+ {
4328
+ name: "take_screenshot",
4329
+ description: "Captures a PNG screenshot via CDP Page.captureScreenshot. NOT available in dev-mode (no CDP connection). Switch to `--mode=local` or `--mode=debug`.",
4330
+ inputSchema: {
4331
+ type: "object",
4332
+ properties: {},
4333
+ required: []
4334
+ },
4335
+ availableIn: "both"
4336
+ },
4337
+ {
4338
+ name: "get_dom_document",
4339
+ description: "Returns the DOM tree via CDP DOM.getDocument. NOT available in dev-mode (no CDP connection). Switch to `--mode=local` or `--mode=debug`.",
4340
+ inputSchema: {
4341
+ type: "object",
4342
+ properties: {},
4343
+ required: []
4344
+ },
4345
+ availableIn: "both"
4346
+ },
4347
+ {
4348
+ name: "take_snapshot",
4349
+ description: "Captures a serialized page snapshot via CDP DOMSnapshot.captureSnapshot. NOT available in dev-mode (no CDP connection). Switch to `--mode=local` or `--mode=debug`.",
4350
+ inputSchema: {
4351
+ type: "object",
4352
+ properties: {},
4353
+ required: []
4354
+ },
4355
+ availableIn: "both"
4356
+ },
4357
+ {
4358
+ name: "list_console_messages",
4359
+ description: "Lists console messages captured via CDP Runtime.consoleAPICalled. NOT available in dev-mode (no CDP connection). Switch to `--mode=local` or `--mode=debug`.",
4360
+ inputSchema: {
4361
+ type: "object",
4362
+ properties: {},
4363
+ required: []
4364
+ },
4365
+ availableIn: "both"
4366
+ },
4367
+ {
4368
+ name: "list_network_requests",
4369
+ description: "Lists network requests captured via CDP Network events. NOT available in dev-mode (no CDP connection). Switch to `--mode=local` or `--mode=debug`.",
4370
+ inputSchema: {
4371
+ type: "object",
4372
+ properties: {},
4373
+ required: []
4374
+ },
4375
+ availableIn: "both"
4376
+ },
4377
+ {
4378
+ name: "list_exceptions",
4379
+ description: "Lists JS exceptions captured via CDP Runtime.exceptionThrown. NOT available in dev-mode (no CDP connection). Switch to `--mode=local` or `--mode=debug`.",
4380
+ inputSchema: {
4381
+ type: "object",
4382
+ properties: { limit: {
4383
+ type: "number",
4384
+ description: "Maximum exceptions to return."
4385
+ } },
4386
+ required: []
4387
+ },
4388
+ availableIn: "both"
4075
4389
  }
4076
4390
  ];
4391
+ /** All tool names served in dev-mode (including tier-filter stubs). */
4077
4392
  const DEV_TOOL_NAMES = new Set(DEV_TOOL_DEFINITIONS.map((t) => t.name));
4393
+ /** CDP-only tools — return a tier-filter error in dev-mode. */
4394
+ const CDP_ONLY_TOOL_NAMES = new Set([
4395
+ "evaluate",
4396
+ "take_screenshot",
4397
+ "get_dom_document",
4398
+ "take_snapshot",
4399
+ "list_console_messages",
4400
+ "list_network_requests",
4401
+ "list_exceptions"
4402
+ ]);
4403
+ /**
4404
+ * Builds the `list_pages` dev-mode shim response.
4405
+ * Returns the Vite dev URL as a single-entry page list with `devMode: true`.
4406
+ */
4407
+ function buildDevListPagesResult(devtoolsUrl) {
4408
+ return {
4409
+ pages: [{
4410
+ url: devtoolsUrl,
4411
+ title: "dev fixture",
4412
+ attached: true
4413
+ }],
4414
+ tunnel: { up: false },
4415
+ devMode: true,
4416
+ singleAttachModel: true
4417
+ };
4418
+ }
4419
+ /**
4420
+ * Builds the `get_diagnostics` dev-mode response.
4421
+ * Probes the mock state endpoint reachability and returns server metadata.
4422
+ */
4423
+ async function buildDevDiagnostics(devtoolsUrl, stateEndpoint, fetchImpl) {
4424
+ let reachable = false;
4425
+ let lastFetchError = null;
4426
+ let lastFetchAt = null;
4427
+ try {
4428
+ const res = await fetchImpl(stateEndpoint);
4429
+ reachable = res.ok;
4430
+ lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
4431
+ if (!res.ok) lastFetchError = `HTTP ${res.status} ${res.statusText}`;
4432
+ } catch (err) {
4433
+ lastFetchError = err instanceof Error ? err.message : String(err);
4434
+ lastFetchAt = (/* @__PURE__ */ new Date()).toISOString();
4435
+ }
4436
+ return {
4437
+ mode: "dev",
4438
+ devtoolsUrl,
4439
+ mcpStateEndpoint: stateEndpoint,
4440
+ mockStateEndpointReachable: reachable,
4441
+ lastFetchAt,
4442
+ lastFetchError,
4443
+ environment: {
4444
+ kind: "mock",
4445
+ reason: "dev-mode — Vite HTTP endpoint, no CDP connection"
4446
+ },
4447
+ nextRecommendedAction: reachable ? null : "mock state endpoint가 응답하지 않습니다. Vite dev 서버가 `mcp: true` 옵션으로 실행 중인지 확인하고, 필요하면 dev 서버를 재시작하세요."
4448
+ };
4449
+ }
4450
+ /**
4451
+ * Builds the `measure_safe_area` dev-mode response from mock state.
4452
+ * Reads `safeAreaInsets` from the AIT mock state and returns a parity-schema
4453
+ * result with `source: 'mock-vite'`.
4454
+ */
4455
+ async function buildDevMeasureSafeArea(aitSource) {
4456
+ const rawInsets = (await aitSource.get("AIT.getMockState")).safeAreaInsets;
4457
+ let sdkInsets = null;
4458
+ if (rawInsets !== null && typeof rawInsets === "object" && !Array.isArray(rawInsets)) {
4459
+ const r = rawInsets;
4460
+ sdkInsets = {
4461
+ top: typeof r.top === "number" ? r.top : 0,
4462
+ right: typeof r.right === "number" ? r.right : 0,
4463
+ bottom: typeof r.bottom === "number" ? r.bottom : 0,
4464
+ left: typeof r.left === "number" ? r.left : 0
4465
+ };
4466
+ }
4467
+ return {
4468
+ source: "mock-vite",
4469
+ cssEnv: {
4470
+ top: 0,
4471
+ right: 0,
4472
+ bottom: 0,
4473
+ left: 0
4474
+ },
4475
+ sdkInsets,
4476
+ sdkInsetsSource: sdkInsets !== null ? "window.__ait" : null,
4477
+ ...sdkInsets === null ? { sdkInsetsError: "window.__ait.state.safeAreaInsets not found in mock state snapshot" } : {},
4478
+ innerWidth: null,
4479
+ innerHeight: null,
4480
+ devicePixelRatio: null,
4481
+ userAgent: null,
4482
+ navBarHeight: null,
4483
+ navBarHeightSource: "not-available-in-dev-mode"
4484
+ };
4485
+ }
4486
+ /**
4487
+ * Builds the `call_sdk` dev-mode response.
4488
+ *
4489
+ * Supported methods are served from the mock state snapshot. Unsupported
4490
+ * methods return `{ ok: false, error: 'dev-mode-unsupported: ...' }` so the
4491
+ * agent gets an informative message rather than a generic failure.
4492
+ */
4493
+ async function buildDevCallSdk(methodName, aitSource) {
4494
+ switch (methodName) {
4495
+ case "getOperationalEnvironment": {
4496
+ const env = await aitSource.get("AIT.getOperationalEnvironment");
4497
+ return {
4498
+ ok: true,
4499
+ value: {
4500
+ environment: env.environment,
4501
+ sdkVersion: env.sdkVersion
4502
+ }
4503
+ };
4504
+ }
4505
+ default: return {
4506
+ ok: false,
4507
+ error: `dev-mode-unsupported: "${methodName}"은 dev-mode에서 직접 호출할 수 없습니다. CDP bridge(window.__sdkCall)가 없으므로 실제 SDK 호출은 \`--mode=local\` 또는 debug 모드에서만 가능합니다. 지원 메서드: getOperationalEnvironment (mock state에서 읽음).`
4508
+ };
4509
+ }
4510
+ }
4078
4511
  /** Builds the dev-mode MCP server (does not connect a transport). */
4079
4512
  function createDevServer(deps = {}) {
4080
- const stateEndpoint = `${process.env.AIT_DEVTOOLS_URL ?? "http://localhost:5173"}/api/ait-devtools/state`;
4513
+ const devtoolsUrl = process.env.AIT_DEVTOOLS_URL ?? "http://localhost:5173";
4514
+ const stateEndpoint = `${devtoolsUrl}/api/ait-devtools/state`;
4081
4515
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
4082
4516
  const server = new Server({
4083
4517
  name: "ait-devtools",
4084
- version: "0.1.44"
4518
+ version: "0.1.45"
4085
4519
  }, { capabilities: { tools: {} } });
4086
4520
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
4087
4521
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
4088
4522
  const name = request.params.name;
4089
4523
  if (!DEV_TOOL_NAMES.has(name)) return mcpError(`알 수 없는 tool: ${name}`);
4524
+ if (CDP_ONLY_TOOL_NAMES.has(name)) return mcpError(`${name}: ${CDP_UNAVAILABLE_IN_DEV_MODE}`);
4090
4525
  try {
4091
4526
  const effective = name === "devtools_get_mock_state" ? "AIT.getMockState" : name;
4092
- if (!isAitToolName(effective)) return mcpError(`알 수 없는 tool: ${name}`);
4093
- switch (effective) {
4527
+ if (isAitToolName(effective)) switch (effective) {
4094
4528
  case "AIT.getMockState": return jsonResult(await getMockState(aitSource));
4095
4529
  case "AIT.getOperationalEnvironment": return jsonResult(await getOperationalEnvironment(aitSource));
4096
4530
  case "AIT.getSdkCallHistory": return jsonResult(await getSdkCallHistory(aitSource));
4097
4531
  default: return mcpError(`알 수 없는 tool: ${name}`);
4098
4532
  }
4533
+ switch (name) {
4534
+ case "list_pages": return jsonResult(buildDevListPagesResult(devtoolsUrl));
4535
+ case "get_diagnostics": return jsonResult(await buildDevDiagnostics(devtoolsUrl, stateEndpoint, (url) => fetch(url)));
4536
+ case "measure_safe_area": return jsonResult(await buildDevMeasureSafeArea(aitSource));
4537
+ case "call_sdk": {
4538
+ const sdkName = request.params.arguments?.name;
4539
+ if (typeof sdkName !== "string" || sdkName === "") return mcpError("call_sdk: name 인자가 비어 있습니다. 호출할 메서드 이름을 전달하세요.");
4540
+ return jsonResult(await buildDevCallSdk(sdkName, aitSource));
4541
+ }
4542
+ default: return mcpError(`알 수 없는 tool: ${name}`);
4543
+ }
4099
4544
  } catch (err) {
4100
4545
  return mcpError(`${name} 실패: ${err instanceof Error ? err.message : String(err)}\nVite dev 서버가 @ait-co/devtools unplugin \`mcp: true\` 옵션으로 실행 중인지 확인하세요. AIT_DEVTOOLS_URL 환경변수가 올바르게 설정됐는지도 확인하세요.`);
4101
4546
  }
@@ -4135,6 +4580,15 @@ async function runDevServer() {
4135
4580
  *
4136
4581
  * Node-only stdio process.
4137
4582
  */
4583
+ /**
4584
+ * Returns `true` when `--force` or `--takeover` is present in argv.
4585
+ *
4586
+ * Both flags are accepted as aliases — `--force` is the short form listed in
4587
+ * the `--help` output; `--takeover` is a longer synonym.
4588
+ */
4589
+ function parseForce(argv) {
4590
+ return argv.includes("--force") || argv.includes("--takeover");
4591
+ }
4138
4592
  /** Parses `--mode=<value>` / `--mode <value>` from argv; default `debug`. */
4139
4593
  function parseMode(argv) {
4140
4594
  for (let i = 0; i < argv.length; i++) {
@@ -4182,8 +4636,12 @@ function normalizeTarget(value) {
4182
4636
  async function main() {
4183
4637
  const args = process.argv.slice(2);
4184
4638
  if (parseMode(args) === "dev") await runDevServer();
4185
- else if (parseTarget(args) === "local") await runLocalDebugServer();
4186
- else await runDebugServer();
4639
+ else {
4640
+ const target = parseTarget(args);
4641
+ const force = parseForce(args);
4642
+ if (target === "local") await runLocalDebugServer({ force });
4643
+ else await runDebugServer({ force });
4644
+ }
4187
4645
  }
4188
4646
  /**
4189
4647
  * True when this file is the process entry (the bin), not an import.
@@ -4210,6 +4668,6 @@ if (isEntrypoint()) main().catch((err) => {
4210
4668
  process.exitCode = 1;
4211
4669
  });
4212
4670
  //#endregion
4213
- export { parseMode, parseTarget };
4671
+ export { parseForce, parseMode, parseTarget };
4214
4672
 
4215
4673
  //# sourceMappingURL=cli.js.map