@ait-co/devtools 0.1.46 → 0.1.49

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
@@ -70,12 +70,12 @@ No HMR (Toss WebView cold-load only). Details: [`docs/scenarios/env-3.md`](./doc
70
70
  Attach a relay to a live OPENED app to observe runtime behavior.
71
71
 
72
72
  ```bash
73
- devtools-mcp # start MCP server
73
+ MCP_ENV=relay-live devtools-mcp # start MCP server (LIVE guard enabled)
74
74
  # call build_attach_url → scan QR → live app loads + relay attaches
75
- # call_sdk / evaluate: watch for side effects (real users may be affected)
75
+ # call_sdk / evaluate: confirm: true required (LIVE guard — real users affected)
76
76
  ```
77
77
 
78
- Details: [`docs/scenarios/env-4.md`](./docs/scenarios/env-4.md)
78
+ `MCP_ENV=relay-live` is required — without it the LIVE side-effect guard is inactive and SDK calls can affect real users. Details: [`docs/scenarios/env-4.md`](./docs/scenarios/env-4.md)
79
79
 
80
80
  ---
81
81
 
@@ -952,11 +952,12 @@ A local browser (env 1) and a phone Toss WebView (env 2/3) both speak CDP, so ev
952
952
 
953
953
  | Mode + target | Invocation | Env var | Target | Tools |
954
954
  |---|---|---|---|---|
955
- | `--mode=debug --target=relay` (default) | `MCP_ENV=relay devtools-mcp` | `MCP_ENV=relay` recommended | Dogfood bundle on a phone (CDP/Chii relay + cloudflared tunnel, env 2/3) | console/network/page + DOM/snapshot/screenshot + `AIT.*` |
955
+ | `--mode=debug --target=relay` (default) | `MCP_ENV=relay-dev devtools-mcp` | `MCP_ENV=relay-dev` recommended (env 3, dogfood) | Dogfood bundle on a phone (CDP/Chii relay + cloudflared tunnel, env 3) | console/network/page + DOM/snapshot/screenshot + `AIT.*` |
956
+ | `--mode=debug --target=relay` LIVE | `MCP_ENV=relay-live devtools-mcp` | `MCP_ENV=relay-live` **required** (env 4, LIVE guard enabled) | Live deployed app (env 4) — `call_sdk`/`evaluate` require `confirm: true` | same |
956
957
  | `--mode=debug --target=local` | `devtools-mcp --target=local` | `MCP_ENV=mock` (auto) | Local Chromium launched by the MCP server (CDP direct-attach, no relay needed, env 1) | same |
957
958
  | `--mode=dev` | `devtools-mcp --mode=dev` | `MCP_ENV=mock` (auto) | Mock state from a running Vite dev server (AIT.* only, no CDP) | `AIT.*` (+ `devtools_get_mock_state` alias) |
958
959
 
959
- `--target=local` opens `AIT_DEVTOOLS_URL` (default `http://localhost:5173`) and attaches directly to a local Chromium — no relay or tunnel required. `--mode=dev` reads the mock-state HTTP endpoint of the Vite dev server and does not provide CDP tools. For on-device sessions (env 3/4), setting `MCP_ENV=relay` explicitly ensures the relay tool surface is visible even before the tunnel URL is auto-detected.
960
+ `--target=local` opens `AIT_DEVTOOLS_URL` (default `http://localhost:5173`) and attaches directly to a local Chromium — no relay or tunnel required. `--mode=dev` reads the mock-state HTTP endpoint of the Vite dev server and does not provide CDP tools. For on-device sessions (env 3), setting `MCP_ENV=relay-dev` explicitly ensures the relay tool surface is visible before the tunnel URL is auto-detected. For env 4 (LIVE), `MCP_ENV=relay-live` is required — only this value activates the LIVE side-effect guard that protects real users.
960
961
 
961
962
  ### Debug mode (CDP via Chii)
962
963
 
@@ -966,6 +967,24 @@ Read-only tools only. Tools are registered in two tiers based on attach state
966
967
 
967
968
  Running `devtools-mcp` as a stdio server starts a local Chii relay on an OS-assigned port and opens a cloudflared quick tunnel, printing a public `wss://*.trycloudflare.com` URL and a QR code in the terminal (secrets/auth codes are never printed). When the phone enters the dogfood entry point, the in-app attach UI connects to the relay with that URL, and the agent reads console/network/page state via `chrome-devtools-mcp`-compatible tools — diagnosing regressions without anyone watching the phone.
968
969
 
970
+ Environment 3 (dogfood relay):
971
+
972
+ ```json
973
+ {
974
+ "mcpServers": {
975
+ "ait-debug": {
976
+ "command": "pnpm",
977
+ "args": ["exec", "devtools-mcp"],
978
+ "env": {
979
+ "MCP_ENV": "relay-dev"
980
+ }
981
+ }
982
+ }
983
+ }
984
+ ```
985
+
986
+ Environment 4 (LIVE relay, LIVE guard enabled):
987
+
969
988
  ```json
970
989
  {
971
990
  "mcpServers": {
@@ -973,14 +992,14 @@ Running `devtools-mcp` as a stdio server starts a local Chii relay on an OS-assi
973
992
  "command": "pnpm",
974
993
  "args": ["exec", "devtools-mcp"],
975
994
  "env": {
976
- "MCP_ENV": "relay"
995
+ "MCP_ENV": "relay-live"
977
996
  }
978
997
  }
979
998
  }
980
999
  }
981
1000
  ```
982
1001
 
983
- Setting `MCP_ENV=relay` explicitly ensures the relay tool surface is visible before the tunnel URL is auto-detected.
1002
+ Setting `MCP_ENV=relay-dev` explicitly ensures the relay tool surface is visible before the tunnel URL is auto-detected. `MCP_ENV=relay-live` activates the LIVE side-effect guard — any `call_sdk`/`evaluate` call without `confirm: true` is rejected to protect real users. `MCP_ENV=relay` is a backward-compat alias for `relay-dev`, so **always use `relay-live` explicitly for env 4**.
984
1003
 
985
1004
  | Tool | CDP / AIT backing | Description |
986
1005
  |---|---|---|
@@ -993,7 +1012,7 @@ Setting `MCP_ENV=relay` explicitly ensures the relay tool surface is visible bef
993
1012
  | `take_screenshot` | `Page.captureScreenshot` | Page PNG screenshot (returned as an MCP image content block) |
994
1013
  | `measure_safe_area` | `Runtime.evaluate` | Runs a safe-area probe on the attached page → returns normalized safe-area insets, viewport geometry, DPR, and User-Agent. Read-only. Use in a relay session to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires attach (`list_pages` first) |
995
1014
  | `evaluate` | `Runtime.evaluate` | Evaluates an arbitrary JS expression on the attached page (returnByValue) and returns the result. **Not read-only** — the expression can have side effects (DOM mutations, SDK calls, state changes). Requires attach |
996
- | `call_sdk` | `window.__sdkCall` bridge (via `Runtime.evaluate`) | Calls a dogfood SDK method via the `window.__sdkCall` bridge (exported by `@apps-in-toss/web-framework` in `__DEBUG_BUILD__` bundles only). **Not read-only** — SDK calls have side effects (navigation, payments, permissions, etc.). Hits the real SDK on env 2/3, mock SDK on env 1. Requires attach. Returns `{ok,value}` / `{ok,error}` |
1015
+ | `call_sdk` | `window.__sdkCall` bridge (via `Runtime.evaluate`) | Calls a dogfood SDK method via the `window.__sdkCall` bridge (exported by `@apps-in-toss/web-framework` in `__DEBUG_BUILD__` bundles only). **Not read-only** — SDK calls have side effects (navigation, payments, permissions, etc.). Hits the real SDK on env 3/4, mock SDK on env 1. Env 2 (PWA) does not inject the SDK — not available there. On env 4, `confirm: true` is required (LIVE guard). Requires attach. Returns `{ok,value}` / `{ok,error}` |
997
1016
  | `AIT.getSdkCallHistory` | AIT domain | SDK call trace (method, args, result/error, timestamp) |
998
1017
  | `AIT.getMockState` | AIT domain | Mock state snapshot (`window.__ait`) |
999
1018
  | `AIT.getOperationalEnvironment` | AIT domain | `getOperationalEnvironment()` + SDK version |
package/README.md CHANGED
@@ -70,12 +70,12 @@ HMR 없음(토스 WebView cold-load만). 상세: [`docs/scenarios/env-3.md`](./d
70
70
  검수를 통과하고 OPENED 상태인 실 출시 앱에 relay를 붙여 런타임을 관측합니다.
71
71
 
72
72
  ```bash
73
- devtools-mcp # MCP 서버 시작
73
+ MCP_ENV=relay-live devtools-mcp # MCP 서버 시작 (LIVE guard 활성화)
74
74
  # build_attach_url 호출 → QR 스캔 → LIVE 앱 로드 + relay attach
75
- # call_sdk / evaluate 는 side-effect 주의 (실유저 영향)
75
+ # call_sdk / evaluate 는 confirm: true 필수 (LIVE guard — 실유저 영향)
76
76
  ```
77
77
 
78
- 상세: [`docs/scenarios/env-4.md`](./docs/scenarios/env-4.md)
78
+ `MCP_ENV=relay-live` 필수 — 미설정 시 LIVE side-effect guard가 비활성화되어 실유저에게 영향을 줄 수 있습니다. 상세: [`docs/scenarios/env-4.md`](./docs/scenarios/env-4.md)
79
79
 
80
80
  ---
81
81
 
@@ -982,11 +982,12 @@ AI 코딩 에이전트(Claude Code, Cursor 등)가 [MCP(Model Context Protocol)]
982
982
 
983
983
  | 모드 + 타깃 | 호출 | 환경 변수 | 대상 | tool |
984
984
  |---|---|---|---|---|
985
- | `--mode=debug --target=relay` (기본값) | `MCP_ENV=relay devtools-mcp` | `MCP_ENV=relay` 권장 | 폰 안 dogfood 번들 (CDP/Chii relay + cloudflared 터널, 환경 3) | console/network/page + DOM/snapshot/screenshot + `AIT.*` |
985
+ | `--mode=debug --target=relay` (기본값) | `MCP_ENV=relay-dev devtools-mcp` | `MCP_ENV=relay-dev` 권장 (환경 3, dogfood) | 폰 안 dogfood 번들 (CDP/Chii relay + cloudflared 터널, 환경 3) | console/network/page + DOM/snapshot/screenshot + `AIT.*` |
986
+ | `--mode=debug --target=relay` LIVE | `MCP_ENV=relay-live devtools-mcp` | `MCP_ENV=relay-live` **필수** (환경 4, LIVE guard 활성화) | LIVE 배포 앱 (환경 4) — `call_sdk`/`evaluate`에 `confirm: true` 필요 | 동일 |
986
987
  | `--mode=debug --target=local` | `devtools-mcp --target=local` | `MCP_ENV=mock` (자동) | MCP가 직접 기동한 로컬 Chromium (CDP direct-attach, relay 불필요, 환경 1) | 동일 |
987
988
  | `--mode=dev` | `devtools-mcp --mode=dev` | `MCP_ENV=mock` (자동) | 실행 중인 Vite dev server의 mock state (AIT.* 전용, CDP 없음) | `AIT.*` (+ `devtools_get_mock_state` alias) |
988
989
 
989
- `--target=local`은 `AIT_DEVTOOLS_URL`(기본 `http://localhost:5173`)을 열고 로컬 Chromium에 CDP direct-attach합니다 — relay나 터널이 필요하지 않습니다. `--mode=dev`는 Vite dev server의 mock-state HTTP endpoint를 읽으며 CDP tool은 제공하지 않습니다. 실기기(환경 3·4) 진입 시 `MCP_ENV=relay`를 명시하면 터널 감지 bootstrap 단계에서도 relay tool이 올바르게 노출됩니다.
990
+ `--target=local`은 `AIT_DEVTOOLS_URL`(기본 `http://localhost:5173`)을 열고 로컬 Chromium에 CDP direct-attach합니다 — relay나 터널이 필요하지 않습니다. `--mode=dev`는 Vite dev server의 mock-state HTTP endpoint를 읽으며 CDP tool은 제공하지 않습니다. 실기기(환경 3) 진입 시 `MCP_ENV=relay-dev`를 명시하면 터널 감지 전에도 relay tool이 올바르게 노출됩니다. 환경 4(LIVE)는 `MCP_ENV=relay-live` 필수 — LIVE side-effect guard(실유저 보호)가 이 값일 때만 활성화됩니다.
990
991
 
991
992
  ### Debug 모드 (CDP via Chii)
992
993
 
@@ -1004,6 +1005,24 @@ tunnel로 공개 `wss://*.trycloudflare.com` URL을 발급한 뒤 QR을 터미
1004
1005
  에이전트가 `chrome-devtools-mcp` 호환 tool로 console/network/page 상태를 read합니다. 사람이 폰을
1005
1006
  지켜볼 필요 없이 회귀를 단독 진단하는 것이 목표입니다.
1006
1007
 
1008
+ 환경 3 (dogfood relay):
1009
+
1010
+ ```json
1011
+ {
1012
+ "mcpServers": {
1013
+ "ait-debug": {
1014
+ "command": "pnpm",
1015
+ "args": ["exec", "devtools-mcp"],
1016
+ "env": {
1017
+ "MCP_ENV": "relay-dev"
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+ ```
1023
+
1024
+ 환경 4 (LIVE relay, LIVE guard 활성화):
1025
+
1007
1026
  ```json
1008
1027
  {
1009
1028
  "mcpServers": {
@@ -1011,14 +1030,14 @@ tunnel로 공개 `wss://*.trycloudflare.com` URL을 발급한 뒤 QR을 터미
1011
1030
  "command": "pnpm",
1012
1031
  "args": ["exec", "devtools-mcp"],
1013
1032
  "env": {
1014
- "MCP_ENV": "relay"
1033
+ "MCP_ENV": "relay-live"
1015
1034
  }
1016
1035
  }
1017
1036
  }
1018
1037
  }
1019
1038
  ```
1020
1039
 
1021
- `MCP_ENV=relay`를 명시하면 터널 URL 자동 감지 전에도 relay tool surface가 올바르게 노출됩니다.
1040
+ `MCP_ENV=relay-dev`를 명시하면 터널 URL 자동 감지 전에도 relay tool surface가 올바르게 노출됩니다. `MCP_ENV=relay-live`는 LIVE side-effect guard를 활성화합니다 — `call_sdk`/`evaluate`에 `confirm: true`가 없으면 거부하여 실유저 영향을 방지합니다. `MCP_ENV=relay`는 backward-compat alias로 `relay-dev`로 해석되므로 **환경 4에서는 `relay-live`를 명시해야 합니다**.
1022
1041
 
1023
1042
  | Tool | CDP / AIT 백킹 | 설명 |
1024
1043
  |---|---|---|
@@ -1031,7 +1050,7 @@ tunnel로 공개 `wss://*.trycloudflare.com` URL을 발급한 뒤 QR을 터미
1031
1050
  | `take_screenshot` | `Page.captureScreenshot` | 페이지 PNG 스크린샷 (MCP image content block 반환) |
1032
1051
  | `measure_safe_area` | `Runtime.evaluate` | attach된 페이지에서 safe-area 프로브 실행 → 정규화된 safe-area inset·뷰포트 geometry·DPR·User-Agent 반환. read-only. relay 세션(폰 attach)에서 viewport preset을 extrapolated/placeholder→measured로 승급할 ground truth 수집용. attach 필요 (`list_pages` 먼저) |
1033
1052
  | `evaluate` | `Runtime.evaluate` | attach된 페이지에서 임의 JS 표현식 평가(returnByValue) → 결과 반환. **read-only 아님** — 표현식이 부작용(DOM 변경·SDK 호출·상태 변경)을 일으킬 수 있음. attach 필요 |
1034
- | `call_sdk` | `window.__sdkCall` 브리지 (`Runtime.evaluate` 경유) | dogfood SDK 메서드를 `window.__sdkCall` 브리지로 호출 (`@apps-in-toss/web-framework`가 `__DEBUG_BUILD__` 번들에서만 export). **read-only 아님** — SDK 호출은 부작용(내비게이션·결제·권한 등). 환경 2·3(실기기 relay)에선 실 SDK, 환경 1(로컬 mock)에선 mock SDK. attach 필요. `{ok,value}` / `{ok,error}` 반환 |
1053
+ | `call_sdk` | `window.__sdkCall` 브리지 (`Runtime.evaluate` 경유) | dogfood SDK 메서드를 `window.__sdkCall` 브리지로 호출 (`@apps-in-toss/web-framework`가 `__DEBUG_BUILD__` 번들에서만 export). **read-only 아님** — SDK 호출은 부작용(내비게이션·결제·권한 등). 환경 3·4(실기기 relay)에선 실 SDK, 환경 1(로컬 mock)에선 mock SDK. 환경 2(PWA)는 SDK 미주입으로 사용 불가. 환경 4에서는 `confirm: true` 필수(LIVE guard). attach 필요. `{ok,value}` / `{ok,error}` 반환 |
1035
1054
  | `AIT.getSdkCallHistory` | AIT 도메인 | SDK 호출 trace (method, args, result/error, timestamp) |
1036
1055
  | `AIT.getMockState` | AIT 도메인 | mock state 스냅샷 (`window.__ait`) |
1037
1056
  | `AIT.getOperationalEnvironment` | AIT 도메인 | `getOperationalEnvironment()` + SDK 버전 |
package/dist/mcp/cli.js CHANGED
@@ -2223,7 +2223,7 @@ const DEBUG_TOOL_DEFINITIONS = [
2223
2223
  },
2224
2224
  {
2225
2225
  name: "measure_safe_area",
2226
- description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay\"` field so consumers can identify provenance without inspecting payload values. Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires a page to be attached — call list_pages first.",
2226
+ description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-live\"` field so consumers can identify provenance without inspecting payload values. Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires a page to be attached — call list_pages first.",
2227
2227
  inputSchema: {
2228
2228
  type: "object",
2229
2229
  properties: {},
@@ -2265,7 +2265,7 @@ const DEBUG_TOOL_DEFINITIONS = [
2265
2265
  },
2266
2266
  {
2267
2267
  name: "call_sdk",
2268
- 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\", [])",
2268
+ 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 3/4 (real device relay) this hits the real SDK; on env 1 (local mock) it hits the mock SDK. (env 2 PWA does not inject the SDK — call_sdk is not available there.) 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\", [])",
2269
2269
  inputSchema: {
2270
2270
  type: "object",
2271
2271
  properties: {
@@ -2319,7 +2319,7 @@ const DEBUG_TOOL_DEFINITIONS = [
2319
2319
  },
2320
2320
  {
2321
2321
  name: "get_diagnostics",
2322
- 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.",
2322
+ 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; in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). 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.",
2323
2323
  inputSchema: {
2324
2324
  type: "object",
2325
2325
  properties: { recent_errors_limit: {
@@ -3148,7 +3148,8 @@ function readDevtoolsVersion() {
3148
3148
  * Derives the next recommended action from a completed diagnostics snapshot.
3149
3149
  *
3150
3150
  * Branch rules (evaluated in priority order):
3151
- * 1. tunnel.up === false → restart
3151
+ * 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)
3152
+ * 1b. tunnel.up === false AND env is mock → wait_for_page (local target: tunnel-less is normal)
3152
3153
  * 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)
3153
3154
  * 3. pages has entry + crashDetectedAt non-null → build_attach_url (re-attach after crash)
3154
3155
  * 4. otherwise → null (session looks healthy)
@@ -3156,7 +3157,12 @@ function readDevtoolsVersion() {
3156
3157
  * Pure — does not throw; receives the final assembled snapshot fields.
3157
3158
  */
3158
3159
  function computeNextRecommendedAction(tunnel, pages, env) {
3159
- if (!tunnel.up) return {
3160
+ if (!tunnel.up) if (!isRelayEnv(env)) {
3161
+ if (pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
3162
+ tool: "wait_for_page",
3163
+ reason: "local Chromium spawn 직후 — 페이지 로드를 기다리거나 list_pages를 재호출하세요 (local 모드는 tunnel이 없는 게 정상입니다)"
3164
+ };
3165
+ } else return {
3160
3166
  tool: "restart",
3161
3167
  reason: "tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart"
3162
3168
  };
@@ -3605,7 +3611,7 @@ function createDebugServer(deps) {
3605
3611
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
3606
3612
  const server = new Server({
3607
3613
  name: "ait-debug",
3608
- version: "0.1.46"
3614
+ version: "0.1.49"
3609
3615
  }, { capabilities: { tools: { listChanged: true } } });
3610
3616
  server.setRequestHandler(ListToolsRequestSchema, () => {
3611
3617
  const env = resolveEnvironment();
@@ -3659,7 +3665,7 @@ function createDebugServer(deps) {
3659
3665
  recentErrorsLimit
3660
3666
  });
3661
3667
  const attached = connection.listTargets().length > 0;
3662
- return envelopeResult(result, name, resolveEnvironment(), attached);
3668
+ return envelopeResult$1(result, name, resolveEnvironment(), attached);
3663
3669
  } catch (err) {
3664
3670
  return errorResult(err, name);
3665
3671
  }
@@ -3839,7 +3845,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
3839
3845
  } catch {}
3840
3846
  const pagesData = listPages(connection, getTunnelStatus());
3841
3847
  const attached = connection.listTargets().length > 0;
3842
- return envelopeResult(pagesData, name, resolveEnvironment(), attached);
3848
+ return envelopeResult$1(pagesData, name, resolveEnvironment(), attached);
3843
3849
  }
3844
3850
  return classifyEnableDomainError(err, name);
3845
3851
  }
@@ -3857,7 +3863,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
3857
3863
  } catch {}
3858
3864
  const listPagesData = listPages(connection, getTunnelStatus());
3859
3865
  const listPagesAttached = connection.listTargets().length > 0;
3860
- return envelopeResult(listPagesData, name, resolveEnvironment(), listPagesAttached);
3866
+ return envelopeResult$1(listPagesData, name, resolveEnvironment(), listPagesAttached);
3861
3867
  }
3862
3868
  case "get_dom_document": return jsonResult$1(await getDomDocument(connection));
3863
3869
  case "take_snapshot": return jsonResult$1(await takeSnapshot(connection));
@@ -3872,7 +3878,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
3872
3878
  case "measure_safe_area": {
3873
3879
  const safeAreaData = await measureSafeArea(connection, resolveEnvironment());
3874
3880
  const safeAreaAttached = connection.listTargets().length > 0;
3875
- return envelopeResult(safeAreaData, name, resolveEnvironment(), safeAreaAttached);
3881
+ return envelopeResult$1(safeAreaData, name, resolveEnvironment(), safeAreaAttached);
3876
3882
  }
3877
3883
  case "evaluate": {
3878
3884
  const expression = request.params.arguments?.expression;
@@ -3889,7 +3895,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
3889
3895
  const sdkResult = await callSdk(connection, sdkName, sdkArgs);
3890
3896
  if (!sdkResult.ok && typeof sdkResult.error === "string" && sdkResult.error.startsWith("sdk-absent:")) return sdkAbsentError("call_sdk");
3891
3897
  const callSdkAttached = connection.listTargets().length > 0;
3892
- return envelopeResult(sdkResult, name, resolveEnvironment(), callSdkAttached);
3898
+ return envelopeResult$1(sdkResult, name, resolveEnvironment(), callSdkAttached);
3893
3899
  }
3894
3900
  default: return unknownTool(name);
3895
3901
  }
@@ -3910,7 +3916,7 @@ function jsonResult$1(value) {
3910
3916
  * as a text content block. When `AIT_MCP_COMPAT=chrome-devtools` is set the
3911
3917
  * envelope is skipped and the raw value is returned — identical to `jsonResult`.
3912
3918
  */
3913
- function envelopeResult(value, tool, env, attached) {
3919
+ function envelopeResult$1(value, tool, env, attached) {
3914
3920
  const wrapped = wrapEnvelope(value, {
3915
3921
  tool,
3916
3922
  env,
@@ -4419,6 +4425,29 @@ const DEV_TOOL_DEFINITIONS = [
4419
4425
  },
4420
4426
  availableIn: "both"
4421
4427
  },
4428
+ {
4429
+ name: "build_attach_url",
4430
+ description: "Turns an `ait deploy --scheme-only` URL into a self-attaching deep link for a real device. NOT available in dev-mode — requires a live cloudflared relay (Tier B, relay-only). To use this tool: restart the MCP server with `--mode=debug` (or omit --mode) and set MCP_ENV=relay, then call build_attach_url to generate the QR for phone scanning. See: https://docs.aitc.dev/guides/debug-relay",
4431
+ inputSchema: {
4432
+ type: "object",
4433
+ properties: {
4434
+ scheme_url: {
4435
+ type: "string",
4436
+ description: "The intoss-private:// URL from `ait deploy --scheme-only`."
4437
+ },
4438
+ wait_for_attach: {
4439
+ type: "boolean",
4440
+ description: "If true, block until a page attaches."
4441
+ },
4442
+ open_in_browser: {
4443
+ type: "boolean",
4444
+ description: "If true (default), open the QR PNG in the OS browser."
4445
+ }
4446
+ },
4447
+ required: ["scheme_url"]
4448
+ },
4449
+ availableIn: "relay"
4450
+ },
4422
4451
  {
4423
4452
  name: "evaluate",
4424
4453
  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.",
@@ -4509,6 +4538,12 @@ const CDP_ONLY_TOOL_NAMES = new Set([
4509
4538
  "list_exceptions"
4510
4539
  ]);
4511
4540
  /**
4541
+ * Tier B tools — relay-only per RFC #277.
4542
+ * Listed in dev-mode tool surface (issue #323) so agents get a hand-off hint
4543
+ * toward `--mode=debug` instead of "Unknown tool".
4544
+ */
4545
+ const TIER_B_TOOL_NAMES = new Set(["build_attach_url"]);
4546
+ /**
4512
4547
  * Builds the `list_pages` dev-mode shim response.
4513
4548
  * Returns the Vite dev URL as a single-entry page list with `devMode: true`.
4514
4549
  */
@@ -4623,13 +4658,14 @@ function createDevServer(deps = {}) {
4623
4658
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
4624
4659
  const server = new Server({
4625
4660
  name: "ait-devtools",
4626
- version: "0.1.46"
4661
+ version: "0.1.49"
4627
4662
  }, { capabilities: { tools: {} } });
4628
4663
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
4629
4664
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
4630
4665
  const name = request.params.name;
4631
4666
  if (!DEV_TOOL_NAMES.has(name)) return mcpError(`알 수 없는 tool: ${name}`);
4632
4667
  if (CDP_ONLY_TOOL_NAMES.has(name)) return mcpError(`${name}: ${CDP_UNAVAILABLE_IN_DEV_MODE}`);
4668
+ if (TIER_B_TOOL_NAMES.has(name)) return tierRejectionError(name, "relay", "mock", "dev-mode — Vite HTTP endpoint, no CDP/relay connection. `--mode=debug` (または `devtools-mcp` without --mode) + MCP_ENV=relay로 재시작하세요.");
4633
4669
  try {
4634
4670
  const effective = name === "devtools_get_mock_state" ? "AIT.getMockState" : name;
4635
4671
  if (isAitToolName(effective)) switch (effective) {
@@ -4639,13 +4675,13 @@ function createDevServer(deps = {}) {
4639
4675
  default: return mcpError(`알 수 없는 tool: ${name}`);
4640
4676
  }
4641
4677
  switch (name) {
4642
- case "list_pages": return jsonResult(buildDevListPagesResult(devtoolsUrl));
4643
- case "get_diagnostics": return jsonResult(await buildDevDiagnostics(devtoolsUrl, stateEndpoint, (url) => fetch(url)));
4644
- case "measure_safe_area": return jsonResult(await buildDevMeasureSafeArea(aitSource));
4678
+ case "list_pages": return envelopeResult("list_pages", buildDevListPagesResult(devtoolsUrl));
4679
+ case "get_diagnostics": return envelopeResult("get_diagnostics", await buildDevDiagnostics(devtoolsUrl, stateEndpoint, (url) => fetch(url)));
4680
+ case "measure_safe_area": return envelopeResult("measure_safe_area", await buildDevMeasureSafeArea(aitSource));
4645
4681
  case "call_sdk": {
4646
4682
  const sdkName = request.params.arguments?.name;
4647
4683
  if (typeof sdkName !== "string" || sdkName === "") return mcpError("call_sdk: name 인자가 비어 있습니다. 호출할 메서드 이름을 전달하세요.");
4648
- return jsonResult(await buildDevCallSdk(sdkName, aitSource));
4684
+ return envelopeResult("call_sdk", await buildDevCallSdk(sdkName, aitSource));
4649
4685
  }
4650
4686
  default: return mcpError(`알 수 없는 tool: ${name}`);
4651
4687
  }
@@ -4661,6 +4697,26 @@ function jsonResult(value) {
4661
4697
  text: JSON.stringify(value, null, 2)
4662
4698
  }] };
4663
4699
  }
4700
+ /**
4701
+ * Wraps `value` in a `ToolEnvelope` (when compat mode is off) and returns it
4702
+ * as a text content block. In dev-mode `env` is always `'mock'` and
4703
+ * `attached` is always `true` (the Vite dev server is the single implicit
4704
+ * "attached" page).
4705
+ *
4706
+ * When `AIT_MCP_COMPAT=chrome-devtools` the envelope is skipped and the raw
4707
+ * value is returned — identical to `jsonResult` (0.1.x back-compat).
4708
+ */
4709
+ function envelopeResult(tool, value) {
4710
+ const wrapped = wrapEnvelope(value, {
4711
+ tool,
4712
+ env: "mock",
4713
+ attached: true
4714
+ });
4715
+ return { content: [{
4716
+ type: "text",
4717
+ text: JSON.stringify(wrapped, null, 2)
4718
+ }] };
4719
+ }
4664
4720
  /** Builds the dev-mode server and connects it over stdio. */
4665
4721
  async function runDevServer() {
4666
4722
  const server = createDevServer();