@ait-co/devtools 0.1.45 → 0.1.48
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 +31 -8
- package/README.md +31 -8
- package/dist/mcp/cli.js +270 -106
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +115 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +2 -2
- package/package.json +1 -1
package/dist/mcp/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":["isObject"],"sources":["../../src/mcp/ait-http-source.ts","../../src/mcp/errors.ts","../../src/mcp/sdk-signatures.ts","../../src/mcp/tools.ts","../../src/mcp/server.ts"],"sourcesContent":["/**\n * Dev-mode `AitSource` — backed by the Vite dev server's mock-state endpoint.\n *\n * The dev server already exposes the live browser mock state at\n * `GET /api/ait-devtools/state` (registered by the unplugin with `mcp: true`).\n * Phase 3 aligns dev mode and debug mode on the same `AIT.*` tool surface, so\n * dev mode serves those tools off this one HTTP source instead of a CDP channel:\n *\n * - `AIT.getMockState` → the full state snapshot (verbatim).\n * - `AIT.getOperationalEnvironment` → derived from the snapshot's\n * `environment` + `appVersion` fields.\n * - `AIT.getSdkCallHistory` → empty (the dev endpoint does not record\n * an SDK call trace — honest, not faked).\n *\n * An AI agent thus sees the same `AIT.getMockState` tool whether attached to a\n * phone (debug) or a dev browser (dev). Tests inject a fake `fetch`.\n */\n\nimport type {\n AitMethodMap,\n AitMethodName,\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\n\n/** Minimal `fetch` shape this source needs (injectable in tests). */\nexport type FetchLike = (url: string) => Promise<{\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n}>;\n\nexport interface HttpAitSourceOptions {\n /** Full URL of the mock-state endpoint, e.g. `http://localhost:5173/api/ait-devtools/state`. */\n stateEndpoint: string;\n /** Injected for tests; defaults to global `fetch`. */\n fetchImpl?: FetchLike;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport class HttpAitSource implements AitSource {\n private readonly stateEndpoint: string;\n private readonly fetchImpl: FetchLike;\n\n constructor(options: HttpAitSourceOptions) {\n this.stateEndpoint = options.stateEndpoint;\n this.fetchImpl = options.fetchImpl ?? ((url) => fetch(url));\n }\n\n private async fetchState(): Promise<AitMockState> {\n const res = await this.fetchImpl(this.stateEndpoint);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch mock state from ${this.stateEndpoint}: HTTP ${res.status} ${res.statusText}. ` +\n 'Ensure the Vite dev server is running with the @ait-co/devtools unplugin option `mcp: true`.',\n );\n }\n const body = await res.json();\n return isObject(body) ? body : {};\n }\n\n async get<M extends AitMethodName>(method: M): Promise<AitMethodMap[M]> {\n switch (method) {\n case 'AIT.getMockState': {\n const state = await this.fetchState();\n return state as AitMethodMap[M];\n }\n case 'AIT.getOperationalEnvironment': {\n const state = await this.fetchState();\n const environment = typeof state.environment === 'string' ? state.environment : 'unknown';\n const sdkVersion = typeof state.appVersion === 'string' ? state.appVersion : null;\n const result: AitOperationalEnvironment = { environment, sdkVersion };\n return result as AitMethodMap[M];\n }\n case 'AIT.getSdkCallHistory': {\n // sdkCallLog slice is now part of the mock state pushed by the browser panel.\n // Read it from the state snapshot rather than returning an empty stub.\n const state = await this.fetchState();\n const raw = state.sdkCallLog;\n const calls = Array.isArray(raw) ? (raw as AitSdkCallHistory['calls']) : [];\n const result: AitSdkCallHistory = { calls };\n return result as AitMethodMap[M];\n }\n default:\n throw new Error(`Unknown AIT method: ${String(method)}`);\n }\n }\n}\n","/**\n * MCP tool 거부/에러 응답 메시지 헬퍼 — 4상태 차별화 + Tier 거부 통일.\n *\n * 모든 tool 거부/에러 응답을 \"원인 + 다음 행동\" 한국어 한 줄 포맷으로 일원화한다.\n * debug-server.ts · tools.ts의 거부 응답 호출부가 이 헬퍼를 통해 생성된다.\n *\n * 4가지 상태 (진단 메시지 차별화):\n * - tunnel-down : cloudflared 터널 미가동 — 서버 재시작 필요\n * - page-missing : 페이지가 attach 안 됨 — build_attach_url → QR 스캔\n * - page-crash : 페이지 crash 감지 — 앱 재실행 후 재attach\n * - sdk-absent : window.__sdkCall 미주입 — dogfood 채널로 재배포\n */\n\n/** MCP tool-result 에러 응답 형식. */\nexport interface McpErrorResult {\n content: Array<{ type: 'text'; text: string }>;\n isError: true;\n}\n\n/**\n * 한국어 한 줄 \"원인 + 다음 행동\" 포맷으로 에러 결과를 빌드한다.\n *\n * @param message - 사용자에게 보여줄 에러 본문 (원인 + 다음 행동 포함).\n */\nexport function mcpError(message: string): McpErrorResult {\n return {\n content: [{ type: 'text', text: message }],\n isError: true,\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Tier 거부 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Tier A/B 환경 불일치 거부 메시지.\n *\n * @param toolName - 거부된 tool 이름.\n * @param requiredEnv - 해당 tool이 요구하는 환경 ('mock' | 'relay').\n * @param currentEnv - 현재 세션 환경.\n * @param reason - 환경이 결정된 근거 (EnvironmentReason 문자열).\n */\nexport function tierRejectionError(\n toolName: string,\n requiredEnv: string,\n currentEnv: string,\n reason: string,\n): McpErrorResult {\n const envLabel = requiredEnv === 'relay' ? 'relay (실기기 연결)' : 'mock (로컬 브라우저)';\n const currentLabel = currentEnv === 'relay' ? 'relay' : 'mock';\n const hint =\n requiredEnv === 'relay'\n ? 'relay로 전환하려면 MCP_ENV=relay 설정 후 서버를 재시작하고 build_attach_url → QR 스캔으로 실기기를 attach하세요.'\n : 'mock으로 전환하려면 MCP_ENV=mock 설정 후 서버를 재시작하세요.';\n const text =\n `${toolName}은 ${envLabel} 환경에서만 사용할 수 있습니다. ` +\n `현재 환경: ${currentLabel} (${reason}). ${hint}`;\n // 하위 호환 — 기존 테스트가 기대하는 영문 패턴도 유지\n const compat = `tool ${toolName} is available only in ${requiredEnv}. Current environment is ${currentEnv} (${reason}).`;\n return mcpError(`${text}\\n\\n${compat}`);\n}\n\n/* -------------------------------------------------------------------------- */\n/* 4상태 차별화 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * 상태 1: tunnel 미가동 — cloudflared 터널이 아직 뜨지 않았다.\n *\n * `build_attach_url` 호출 시 tunnel.up === false 인 경우.\n */\nexport function tunnelDownError(): McpErrorResult {\n return mcpError(\n 'cloudflared 터널이 안 떠 있습니다. ' +\n 'MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.',\n );\n}\n\n/**\n * 상태 2: page 미attach — 터널은 살아 있으나 아직 페이지가 연결되지 않았다.\n *\n * enableDomains()가 \"No mini-app page attached\" 에러를 던질 때.\n */\nexport function pageMissingError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}페이지가 attach 안 됨. ` +\n 'dogfood 번들 배포 후 build_attach_url을 호출해 QR을 생성하세요: ' +\n '`ait deploy --scheme-only` → `build_attach_url(scheme_url)` → QR 스캔.',\n );\n}\n\n/**\n * 상태 3: page crash — 연결됐던 페이지가 crash/destroy됐다.\n *\n * chii-connection 이 'replaced-by-new-attach' / 'targetCrashed' / 'targetDestroyed' 를\n * 던질 때 이 메시지를 사용한다.\n */\nexport function pageCrashError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}페이지가 crash됐습니다. ` +\n '토스 앱을 재실행한 뒤 build_attach_url → QR 스캔으로 재attach하세요.',\n );\n}\n\n/**\n * 상태 4: SDK 부재 — window.__sdkCall이 주입되지 않았다 (dogfood 빌드가 아님).\n *\n * call_sdk 호출 시 브리지가 없을 때.\n */\nexport function sdkAbsentError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}window.__sdkCall이 주입되지 않았습니다 (dogfood 빌드가 아닙니다). ` +\n 'dogfood 채널(intoss-private)로 재배포 후 QR을 다시 스캔하세요: ' +\n '`ait build && aitcc app deploy`.',\n );\n}\n\n/* -------------------------------------------------------------------------- */\n/* LIVE side-effect guard 메시지 (relay-live env) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * relay-live 환경에서 side-effect 도구(`call_sdk`, `evaluate`)를 `confirm: true`\n * 없이 호출했을 때 반환하는 거부 메시지.\n *\n * 다음 행동을 두 가지로 제시한다:\n * 1. 같은 호출에 `confirm: true` 인자를 추가해 재시도.\n * 2. 읽기 전용 환경(relay-dev, mock)으로 전환.\n */\nexport function liveGuardError(toolName: string): McpErrorResult {\n const text =\n `[LIVE relay guard] ${toolName}은 현재 relay-live(실 출시 런타임) 세션에서 ` +\n 'side-effect 호출입니다. 실유저에게 영향을 줄 수 있어 명시적 동의가 필요합니다.\\n\\n' +\n '다음 중 하나를 선택하세요:\\n' +\n ` 1. \\`confirm: true\\` 인자를 추가해 재호출: ${toolName}(…, confirm: true)\\n` +\n ' 2. 읽기 전용 도구(list_pages, list_console_messages, take_screenshot 등)를 사용하세요.\\n' +\n ' 3. dogfood 빌드(relay-dev 환경)에서 먼저 검증 후 live에 적용하세요.\\n\\n' +\n 'live-guard: MCP_ENV=relay-live + confirm: true missing';\n return mcpError(text);\n}\n\n/* -------------------------------------------------------------------------- */\n/* relay 연결 끊김 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * relay WebSocket 연결이 끊겼을 때 — 크래시가 아닌 네트워크/프로세스 종료.\n */\nexport function relayDisconnectError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}relay 연결이 끊겼습니다. ` +\n 'list_pages로 상태를 확인하고, 필요하면 앱을 재실행 후 재attach하세요.',\n );\n}\n\n/* -------------------------------------------------------------------------- */\n/* 일반 tool 에러 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * CDP/AIT 명령 중 발생한 예외를 4상태로 분류해 적절한 에러 결과를 반환한다.\n *\n * - SDK 부재 패턴 (`window.__sdkCall is not available`) → sdkAbsentError\n * - crash 패턴 (`replaced-by-new-attach`, `targetCrashed`, `targetDestroyed`) → pageCrashError\n * - 연결 끊김 패턴 (`relay에 연결되어 있지 않습니다`, `relay WebSocket`) → relayDisconnectError\n * - 그 외 (일반 에러) → 원본 메시지를 포함한 mcpError\n */\nexport function classifyToolError(err: unknown, toolName: string): McpErrorResult {\n const message = err instanceof Error ? err.message : String(err);\n\n // 상태 1: tunnel 미가동 (buildAttachUrl이 던지는 패턴)\n if (message.startsWith('tunnel-down:') || message.includes('터널이 안 떠 있습니다')) {\n return tunnelDownError();\n }\n\n // 상태 4: SDK 부재\n if (\n message.startsWith('sdk-absent:') ||\n message.includes('__sdkCall이 주입되지 않았습니다') ||\n message.includes('window.__sdkCall is not available') ||\n (message.includes('__sdkCall') && message.includes('not available'))\n ) {\n return sdkAbsentError(toolName);\n }\n\n // 상태 3: page crash / target destroyed / replaced-by-new-attach\n if (\n message.includes('replaced-by-new-attach') ||\n message.includes('targetCrashed') ||\n message.includes('targetDestroyed') ||\n message.includes('detachedFromTarget')\n ) {\n return pageCrashError(toolName);\n }\n\n // relay 연결 끊김 (단순 disconnect — crash 아님)\n if (message.includes('relay에 연결되어 있지 않습니다') || message.includes('relay WebSocket')) {\n return relayDisconnectError(toolName);\n }\n\n // 그 외: 원본 메시지를 포함하되 list_pages 다음 행동 안내 추가\n return mcpError(\n `${toolName} 실패: ${message}\\nlist_pages로 미니앱이 relay에 attach됐는지 확인하세요.`,\n );\n}\n","/**\n * call_sdk 인자 시그니처 레지스트리\n *\n * 잘 알려진 SDK 메서드의 인자 schema를 수동으로 등록한다.\n * 목적: 잘못된 인자가 native bridge에 도달하기 전에 MCP 레이어에서 reject하여\n * 토스 앱 crash(Swift/Kotlin 측에서 `.type` 등을 undefined로 읽는 경우)를 예방.\n *\n * 등록되지 않은 메서드는 passthrough — 알 수 없는 메서드에 대해 stderr 경고 1회.\n *\n * 시그니처 출처:\n * - `src/__typecheck.ts` — Original SDK 타입 호환성 검증\n * - `src/mock/navigation/index.ts` — mock 구현의 함수 시그니처\n * - `src/mock/device/` — device mock 시그니처\n *\n * 새 메서드 추가 방법:\n * 1. `src/__typecheck.ts` 또는 mock 구현에서 시그니처 확인\n * 2. 아래 SIGNATURES 배열에 `SdkSignature` 항목 추가\n * 3. `src/__tests__/call-sdk-validation.test.ts`에 ok + bad 케이스 추가\n */\n\n/** 단일 메서드에 대한 인자 검증 결과 */\nexport type ValidationResult = { ok: true } | { ok: false; expected: string; received: string };\n\n/** 등록된 SDK 메서드 시그니처 */\nexport interface SdkSignature {\n /** SDK 메서드 이름 (예: \"setDeviceOrientation\") */\n name: string;\n /**\n * 인자 배열을 검증하는 함수.\n * `args[0]` 등 필요한 인자를 `unknown` 타입으로 받아 type guard로 검증.\n */\n validateArgs(args: unknown[]): ValidationResult;\n /**\n * 에러 메시지에 포함할 올바른 호출 예시.\n * 예: `call_sdk('setDeviceOrientation', [{ type: 'landscape' }])`\n */\n example: string;\n}\n\n/* -------------------------------------------------------------------------- */\n/* 헬퍼 — 공통 type guard */\n/* -------------------------------------------------------------------------- */\n\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction describeArgs(args: unknown[]): string {\n try {\n return JSON.stringify(args);\n } catch {\n return String(args);\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* 시그니처 레지스트리 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * 등록된 메서드 목록.\n *\n * 시그니처 출처 확인:\n * - 함수가 인자를 받지 않으면 args[0] 없음 → `args.length === 0`을 체크하지 않고\n * 그냥 통과시킨다(args 무시하는 stub가 많아서 noArgs 체크가 noise).\n * - 실 SDK 시그니처는 `src/__typecheck.ts`의 `Assert<Mock, Original>` 줄로 보장.\n */\nconst SIGNATURES: SdkSignature[] = [\n // --- setDeviceOrientation ---\n // 실 시그니처: setDeviceOrientation(options: { type: 'portrait' | 'landscape' }): Promise<void>\n // 출처: src/mock/navigation/index.ts:40 / src/__typecheck.ts:55\n {\n name: 'setDeviceOrientation',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg)) {\n return {\n ok: false,\n expected: \"{ type: 'portrait' | 'landscape' }\",\n received: describeArgs(args),\n };\n }\n const type = arg.type;\n if (type !== 'portrait' && type !== 'landscape') {\n return {\n ok: false,\n expected: \"{ type: 'portrait' | 'landscape' }\",\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setDeviceOrientation', [{ type: 'landscape' }])\",\n },\n\n // --- setIosSwipeGestureEnabled ---\n // 실 시그니처: setIosSwipeGestureEnabled(options: { isEnabled: boolean }): Promise<void>\n // 출처: src/mock/navigation/index.ts:32 / src/__typecheck.ts:51\n {\n name: 'setIosSwipeGestureEnabled',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.isEnabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ isEnabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setIosSwipeGestureEnabled', [{ isEnabled: false }])\",\n },\n\n // --- setSecureScreen ---\n // 실 시그니처: setSecureScreen(options: { enabled: boolean }): Promise<{ enabled: boolean }>\n // 출처: src/mock/navigation/index.ts:66 / src/__typecheck.ts:46\n {\n name: 'setSecureScreen',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.enabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ enabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setSecureScreen', [{ enabled: true }])\",\n },\n\n // --- setScreenAwakeMode ---\n // 실 시그니처: setScreenAwakeMode(options: { enabled: boolean }): Promise<{ enabled: boolean }>\n // 출처: src/mock/navigation/index.ts:57 / src/__typecheck.ts:47\n {\n name: 'setScreenAwakeMode',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.enabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ enabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setScreenAwakeMode', [{ enabled: true }])\",\n },\n\n // --- getOperationalEnvironment ---\n // 실 시그니처: getOperationalEnvironment(): 'toss' | 'sandbox'\n // 인자 없음 — args는 무시 (SDK 자체가 인자를 무시함)\n // 출처: src/mock/navigation/index.ts:88 / src/__typecheck.ts:62\n {\n name: 'getOperationalEnvironment',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getOperationalEnvironment', [])\",\n },\n\n // --- getPlatformOS ---\n // 실 시그니처: getPlatformOS(): 'ios' | 'android'\n // 출처: src/mock/navigation/index.ts:84 / src/__typecheck.ts:61\n {\n name: 'getPlatformOS',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getPlatformOS', [])\",\n },\n\n // --- getDeviceId ---\n // 실 시그니처: getDeviceId(): string\n // 출처: src/mock/navigation/index.ts:119 / src/__typecheck.ts:74\n {\n name: 'getDeviceId',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getDeviceId', [])\",\n },\n\n // --- getLocale ---\n // 실 시그니처: getLocale(): string\n // 출처: src/mock/navigation/index.ts:115 / src/__typecheck.ts:72\n {\n name: 'getLocale',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getLocale', [])\",\n },\n\n // --- getNetworkStatus ---\n // 실 시그니처: getNetworkStatus(): Promise<NetworkStatus>\n // 출처: src/mock/navigation/index.ts:127 / src/__typecheck.ts:73\n {\n name: 'getNetworkStatus',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getNetworkStatus', [])\",\n },\n\n // --- getSchemeUri ---\n // 실 시그니처: getSchemeUri(): string\n // 출처: src/mock/navigation/index.ts:111 / src/__typecheck.ts:71\n {\n name: 'getSchemeUri',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getSchemeUri', [])\",\n },\n\n // --- requestReview ---\n // 실 시그니처: requestReview(): Promise<void>\n // 출처: src/mock/navigation/index.ts:75 / src/__typecheck.ts:76\n {\n name: 'requestReview',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('requestReview', [])\",\n },\n\n // --- closeView ---\n // 실 시그니처: closeView(): Promise<void>\n // 출처: src/mock/navigation/index.ts:10 / src/__typecheck.ts:42\n {\n name: 'closeView',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('closeView', [])\",\n },\n];\n\n/* -------------------------------------------------------------------------- */\n/* 레지스트리 공개 API */\n/* -------------------------------------------------------------------------- */\n\nconst SIGNATURE_MAP = new Map<string, SdkSignature>(SIGNATURES.map((s) => [s.name, s]));\n\n/** 세션 내 passthrough 경고를 한 번만 emit하기 위한 Set */\nconst _warnedPassthrough = new Set<string>();\n\n/**\n * 메서드 이름으로 시그니처를 조회한다.\n * 등록된 메서드이면 `SdkSignature`를 반환하고, 미등록이면 `undefined`.\n */\nexport function lookupSignature(name: string): SdkSignature | undefined {\n return SIGNATURE_MAP.get(name);\n}\n\n/**\n * 미등록 메서드에 대해 stderr에 passthrough 경고를 1회 출력한다.\n * 세션 내 동일 메서드 이름은 최초 1회만 출력.\n */\nexport function warnPassthrough(name: string): void {\n if (_warnedPassthrough.has(name)) return;\n _warnedPassthrough.add(name);\n process.stderr.write(`[ait-debug] call_sdk: \"${name}\" 시그니처가 등록되지 않음 — passthrough\\n`);\n}\n\n/**\n * 테스트에서 passthrough 경고 Set을 초기화하기 위한 헬퍼.\n * 프로덕션 코드에서는 호출하지 않는다.\n */\nexport function _resetWarnedPassthroughForTest(): void {\n _warnedPassthrough.clear();\n}\n\n/**\n * 등록된 메서드 이름 목록 — tool description 생성 등에서 사용.\n */\nexport const REGISTERED_METHOD_NAMES: ReadonlyArray<string> = SIGNATURES.map((s) => s.name);\n","/**\n * Debug-mode MCP tools (Phase 1–3 + safe-area probe).\n *\n * Read-only tools that normalize CDP / AIT data into `chrome-devtools-mcp`-\n * compatible shapes. The tools never touch a websocket or HTTP endpoint\n * directly — they read from an injected `CdpConnection` (CDP events/commands)\n * or `AitSource` (AIT.* domain), which is what makes them unit-testable with a\n * fake. No phone and no running dev server are needed in tests.\n *\n * Phase 1 (CDP events):\n * - `list_console_messages` ← Runtime.consoleAPICalled\n * - `list_network_requests` ← Network.requestWillBeSent + responseReceived\n * - `list_pages` ← Chii relay target list + tunnel status\n * Phase 2 (CDP commands):\n * - `get_dom_document` ← DOM.getDocument\n * - `take_snapshot` ← DOMSnapshot.captureSnapshot\n * - `take_screenshot` ← Page.captureScreenshot\n * - `measure_safe_area` ← Runtime.evaluate (safe-area probe)\n * Phase 3 (AIT.* domain — CDP can't cover these):\n * - `AIT.getSdkCallHistory`\n * - `AIT.getMockState`\n * - `AIT.getOperationalEnvironment`\n */\n\nimport type {\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\nimport type {\n CdpCallFrame,\n CdpConnection,\n CdpRemoteObject,\n ConsoleApiCalledEvent,\n DomGetDocumentResult,\n DomSnapshotResult,\n NetworkRequestWillBeSentEvent,\n NetworkResponseReceivedEvent,\n RuntimeExceptionThrownEvent,\n} from './cdp-connection.js';\nimport { buildDeepLinkAttachUrl, validateSchemeAuthority } from './deeplink.js';\nimport type { McpEnvironment } from './environment.js';\nimport { isLiveRelayEnv, isRelayEnv, toLegacyEnv } from './environment.js';\nimport { lookupSignature, warnPassthrough } from './sdk-signatures.js';\n\n/** Tunnel state surfaced by `list_pages`. */\nexport interface TunnelStatus {\n /** Whether the cloudflared quick tunnel is up. */\n up: boolean;\n /** Public `wss://*.trycloudflare.com` relay URL the phone attaches to. */\n wssUrl: string | null;\n /**\n * ISO timestamp when a tunnel drop was first detected by the health probe.\n * `null` means the tunnel has not dropped (or has recovered since the last\n * drop). When non-null and `up` is false, the tunnel is down and the probe\n * has exhausted all reissue attempts — the server must be restarted.\n */\n droppedAt?: string | null;\n /**\n * Number of automatic reissue attempts made after a drop was detected.\n * Resets to 0 after a successful reissue. Reaches `MAX_REISSUE_ATTEMPTS`\n * (3) before the probe gives up and enters the permanent-error state.\n */\n reissueAttempts?: number;\n}\n\n/**\n * Tier classification per RFC #277 (\"MCP tool surface fidelity\"):\n *\n * - **Tier A** (`mock` only) — mock-internal state dials with no real-device\n * equivalent. Hidden when env is `relay`.\n * - **Tier B** (`relay` only) — relay infrastructure tools that have no mock\n * equivalent (e.g. `build_attach_url` needs a cloudflared tunnel URL). Hidden\n * when env is `mock`.\n * - **Tier C** (`both`) — fidelity-parallel tools that produce semantically\n * equivalent results across mock and relay. The agent sees the same tool with\n * the same shape; only the `source` provenance field (where applicable)\n * differs.\n */\nexport type ToolAvailability = 'mock' | 'relay' | 'both';\n\n/** Static MCP tool descriptors (name + JSONSchema) for the full debug tool surface. */\nexport const DEBUG_TOOL_DEFINITIONS = [\n {\n name: 'list_console_messages',\n description:\n 'Lists recent console messages (console.log/warn/error/info) captured from the attached ' +\n 'mini-app page over CDP (Runtime.consoleAPICalled). Read-only. Returns level, text, ' +\n 'timestamp, and stringified args, oldest-first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_network_requests',\n description:\n 'Lists recent network requests (XHR/fetch) captured from the attached mini-app page over ' +\n 'CDP (Network.requestWillBeSent + Network.responseReceived). Read-only. Returns url, ' +\n 'method, status, and timing, oldest-first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_pages',\n description:\n 'Returns the single active page (at most one) the relay sees attached. ' +\n 'When a second page attaches, the previous one is evicted (last-attach wins — ' +\n 'single-attach model). The result includes `singleAttachModel: true` so the agent ' +\n 'knows the array is always 0 or 1 entries. ' +\n 'Also returns whether the cloudflared tunnel is up and the public wss relay URL. ' +\n 'The `tunnel` field includes `droppedAt` (ISO timestamp or null/undefined): when non-null ' +\n 'the tunnel has permanently dropped after 3 failed reissue attempts — restart the debug ' +\n 'server with `npx @ait-co/devtools devtools-mcp`. ' +\n 'Each page entry includes a `lastSeenAt` ISO timestamp (last inbound CDP message from ' +\n 'that target — useful to detect stale entries when the phone app backgrounded). ' +\n 'The result also includes `crashDetectedAt` (ISO timestamp or null): when non-null, ' +\n 'a page crash was detected via Inspector.targetCrashed / Target.targetDestroyed since ' +\n 'the last attach, the pages list will be empty, and `crashWarning` shows a Korean hint ' +\n 'to re-attach. ' +\n 'Call this first to confirm a page is attached before reading console/network. ' +\n 'When a page attaches or detaches the server emits notifications/tools/list_changed — ' +\n 'call tools/list again to get the full updated tool surface.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'build_attach_url',\n description:\n \"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. \" +\n 'Turns an `ait deploy --scheme-only` URL (intoss-private://…?_deploymentId=<uuid>) into a ' +\n 'self-attaching deep link by splicing in debug=1 and the live relay URL for this session. ' +\n 'Returns the deep link JSON and a unicode QR of that deep link. Scan the QR with the phone ' +\n 'camera to open the mini-app and attach it to this debug session (QR is the single entry ' +\n 'path — no USB cable or platform CLI needed). Requires the tunnel to be up — call ' +\n 'list_pages first. If the tunnel is not up, restart the MCP server: ' +\n '`npx @ait-co/devtools devtools-mcp`. ' +\n 'Set wait_for_attach=true to block until the phone scans and a page attaches ' +\n '(polls listTargets up to 30 s by default), then returns the attached page info too. ' +\n 'On timeout, call build_attach_url again to resume polling. ' +\n 'When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default ' +\n 'browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). ' +\n 'Requires MCP_ENV=relay (set automatically when a relay tunnel is detected).',\n inputSchema: {\n type: 'object',\n properties: {\n scheme_url: {\n type: 'string',\n description:\n 'The intoss-private:// scheme URL from `ait deploy --scheme-only` (must carry _deploymentId). ' +\n 'The authority (host) must be the app name (e.g. intoss-private://aitc-sdk-example?_deploymentId=…). ' +\n 'Generic values like \"web\" or an empty host indicate a malformed URL.',\n },\n wait_for_attach: {\n type: 'boolean',\n description:\n 'If true, block after returning the QR until a page attaches to the relay (polls ' +\n 'listTargets ~1 s interval, timeout 30 s). On attach, the response includes the ' +\n 'attached page list. On timeout, call build_attach_url again to resume polling.',\n },\n open_in_browser: {\n type: 'boolean',\n description:\n 'If true (default), render the QR as a PNG and open it in the OS default browser. ' +\n 'Only works when the MCP server is running on a local GUI machine — headless or ' +\n 'remote container environments should set this to false to use the text QR fallback.',\n },\n },\n required: ['scheme_url'],\n },\n // Tier B per RFC #277 — the URL synthesis requires a live cloudflared\n // tunnel + relay, which only exists in the `relay` environment.\n availableIn: 'relay' as ToolAvailability,\n },\n {\n name: 'get_dom_document',\n description:\n 'Returns the DOM tree of the attached mini-app page over CDP (DOM.getDocument). Read-only. ' +\n 'Use for structural/layout regression diagnosis (e.g. confirming an element exists, ' +\n 'inspecting attributes). Returns the document root node with children.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_snapshot',\n description:\n 'Captures a serialized snapshot of the attached page over CDP (DOMSnapshot.captureSnapshot). ' +\n 'Read-only. Returns the documents + interned strings table for visual-regression diagnosis ' +\n '(e.g. checking computed CSS custom properties like --sat against the live layout).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_screenshot',\n description:\n 'Captures a PNG screenshot of the attached mini-app page over CDP (Page.captureScreenshot) ' +\n 'so the agent can see the phone screen directly. Read-only. ' +\n 'Returns an image content block — this is the only debug tool that returns an image; ' +\n 'all other debug tools return text (JSON).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'measure_safe_area',\n description:\n 'Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns ' +\n 'normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. ' +\n 'Read-only — does not modify page state. ' +\n 'Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel ' +\n 'page with window.__ait state) and `relay` (real-device WebView with window.__sdk). ' +\n 'The result includes a `source: \"mock\" | \"relay\"` field so consumers can identify ' +\n 'provenance without inspecting payload values. ' +\n 'Use in a relay session (phone attached) to get ground-truth values for upgrading a ' +\n 'viewport preset from extrapolated/placeholder to measured. ' +\n 'Requires a page to be attached — call list_pages first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'evaluate',\n description:\n 'Evaluates an arbitrary JavaScript expression on the attached mini-app page via ' +\n 'CDP Runtime.evaluate (returnByValue: true) and returns the result. ' +\n 'NOT read-only — the expression can have side effects (DOM mutations, SDK calls, ' +\n 'state changes). Requires the relay to be attached — call list_pages first. ' +\n 'Throws if the evaluation throws an exception on the page.\\n\\n' +\n 'SECURITY: expression and result are not redacted — never include secrets or auth ' +\n 'tokens in the expression.\\n\\n' +\n 'LIVE guard: when running against a live/production relay (relay-live env, ' +\n 'MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that ' +\n 'the expression may affect real users. Without it the call is rejected with a ' +\n 'structured error. mock and relay-dev sessions are unaffected.',\n inputSchema: {\n type: 'object',\n properties: {\n expression: {\n type: 'string',\n description: 'JavaScript expression to evaluate in the page context.',\n },\n confirm: {\n type: 'boolean',\n description:\n 'Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge ' +\n 'that this expression may have side effects on real/live users. ' +\n 'Omitting this in a relay-live session results in a structured rejection error. ' +\n 'Has no effect in mock or relay-dev sessions.',\n },\n },\n required: ['expression'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_exceptions',\n description:\n 'Lists JS-level exceptions captured via `Runtime.exceptionThrown` from the relay attached ' +\n 'page. Includes timestamp, exception text, source URL/line, and stack trace. ' +\n 'Use to root-cause SDK throws that may precede a Toss app crash (#265 / #267). ' +\n 'The buffer holds up to 50 most recent exceptions and survives target ' +\n 'replaced/crashed/destroyed events so an exception just before a crash is preserved. ' +\n 'Returns up to 50 most recent by default.',\n inputSchema: {\n type: 'object',\n properties: {\n limit: {\n type: 'number',\n description: 'Maximum number of exceptions to return (default 50, max 50).',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'call_sdk',\n description:\n 'Calls a dogfood SDK method via the window.__sdkCall bridge ' +\n '(exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). ' +\n 'NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). ' +\n 'On env 2/3 (real device relay) this hits the real SDK; on env 1 (local mock) it hits ' +\n 'the mock SDK. Requires the relay to be attached — call list_pages first. ' +\n 'Returns {ok: true, value} on success or {ok: false, error} on failure. ' +\n 'If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], ' +\n 'the result also includes `recentException` for crash triage. ' +\n 'Returns a clear error if window.__sdkCall is not available (non-dogfood bundle) — ' +\n 'redeploy via dogfood channel: `ait build && aitcc app deploy`.\\n\\n' +\n 'SECURITY: method name, args, and result value are not redacted — never include secrets.\\n\\n' +\n 'LIVE guard: when running against a live/production relay (relay-live env, ' +\n 'MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that ' +\n 'the SDK call may affect real users. Without it the call is rejected with a ' +\n 'structured error. mock and relay-dev sessions are unaffected.\\n\\n' +\n 'IMPORTANT — 인자 시그니처 (잘못된 인자로 호출하면 토스 앱 crash 위험):\\n' +\n ' setDeviceOrientation: call_sdk(\"setDeviceOrientation\", [{ type: \"landscape\" }]) // NOT \"landscape\"\\n' +\n ' setIosSwipeGestureEnabled: call_sdk(\"setIosSwipeGestureEnabled\", [{ isEnabled: false }])\\n' +\n ' setSecureScreen: call_sdk(\"setSecureScreen\", [{ enabled: true }])\\n' +\n ' setScreenAwakeMode: call_sdk(\"setScreenAwakeMode\", [{ enabled: true }])\\n' +\n ' getOperationalEnvironment: call_sdk(\"getOperationalEnvironment\", [])\\n' +\n ' getPlatformOS: call_sdk(\"getPlatformOS\", [])\\n' +\n ' getDeviceId: call_sdk(\"getDeviceId\", [])\\n' +\n ' getLocale: call_sdk(\"getLocale\", [])\\n' +\n ' getNetworkStatus: call_sdk(\"getNetworkStatus\", [])\\n' +\n ' getSchemeUri: call_sdk(\"getSchemeUri\", [])\\n' +\n ' requestReview: call_sdk(\"requestReview\", [])\\n' +\n ' closeView: call_sdk(\"closeView\", [])',\n inputSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'SDK method name to call (e.g. \"getOperationalEnvironment\").',\n },\n args: {\n type: 'array',\n description: 'Arguments to pass to the SDK method (optional, default []).',\n items: {},\n },\n confirm: {\n type: 'boolean',\n description:\n 'Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge ' +\n 'that this SDK call may have side effects on real/live users. ' +\n 'Omitting this in a relay-live session results in a structured rejection error. ' +\n 'Has no effect in mock or relay-dev sessions.',\n },\n },\n required: ['name'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getSdkCallHistory',\n description:\n 'Returns the recent Apps In Toss SDK call trace (method, args, result/error, timestamp) that ' +\n 'raw CDP cannot observe. Read-only. Use to confirm an SDK call fired and how it resolved ' +\n '(e.g. a saveBase64Data permission regression).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getMockState',\n description:\n 'Returns the devtools mock state snapshot (window.__ait) — environment, permissions, location, ' +\n 'auth, network, IAP, and more. Read-only. In dev mode this is the live browser mock state; in ' +\n 'debug mode the in-app side reports it over the AIT domain.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getOperationalEnvironment',\n description:\n 'Returns getOperationalEnvironment() plus the resolved SDK version — metadata raw CDP cannot ' +\n 'observe. Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_diagnostics',\n description:\n 'Returns a single-call server status snapshot so the agent can diagnose \"why is this not ' +\n 'working?\" without calling multiple tools. Fields: mcpVersion (MCP SDK version), ' +\n 'devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), ' +\n 'pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, ' +\n 'recentErrors (last N server-side errors, PII/secret redacted), ' +\n 'environment (kind: mock|relay-dev|relay-live, env: mock|relay backward-compat, reason, ' +\n 'liveGuardActive: true when relay-live LIVE guard is active), ' +\n 'serverLockHolder (pid + startedAt from the lock file, or null), ' +\n 'nextRecommendedAction ({tool, reason} or null — the single next tool to call). ' +\n 'All fields are nullable — missing data is null, not an error. ' +\n 'debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. ' +\n 'Tier C (both mock and relay). Call this first when debugging session state.',\n inputSchema: {\n type: 'object',\n properties: {\n recent_errors_limit: {\n type: 'number',\n description:\n 'Maximum number of recent server-side errors to include (default 10, max 50).',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n] as const;\n\nexport type DebugToolName = (typeof DEBUG_TOOL_DEFINITIONS)[number]['name'];\n\nconst DEBUG_TOOL_NAMES = new Set<string>(DEBUG_TOOL_DEFINITIONS.map((t) => t.name));\n\nexport function isDebugToolName(name: string): name is DebugToolName {\n return DEBUG_TOOL_NAMES.has(name);\n}\n\n/**\n * Returns the `ToolAvailability` declared on a registered debug tool, or\n * `undefined` when the name is not a known debug tool. Used by the tool\n * registry to filter `tools/list` by current env and by the call handler to\n * reject env-mismatch invocations.\n */\nexport function getToolAvailability(name: string): ToolAvailability | undefined {\n for (const t of DEBUG_TOOL_DEFINITIONS) {\n if (t.name === name) return t.availableIn;\n }\n return undefined;\n}\n\n/**\n * Returns true when the named tool is available in the given environment.\n * Unknown tools return `false` — callers should reject them as unknown rather\n * than as env-mismatched.\n *\n * Relay variants (`relay-dev`, `relay-live`) both satisfy the `'relay'`\n * availability tier — `isRelayEnv()` is used for the check.\n */\nexport function isToolAvailableIn(name: string, env: McpEnvironment): boolean {\n const availability = getToolAvailability(name);\n if (availability === undefined) return false;\n if (availability === 'both') return true;\n if (availability === 'relay') return isRelayEnv(env);\n return availability === env;\n}\n\n/**\n * Filters a `DEBUG_TOOL_DEFINITIONS`-shaped list to those whose `availableIn`\n * matches the given env. Pure — preserves order; both Tier C (\"both\") and the\n * matching single-env tier pass through.\n *\n * Relay variants (`relay-dev`, `relay-live`) both satisfy the `'relay'` tier.\n */\nexport function filterToolsByEnvironment<T extends { name: string; availableIn: ToolAvailability }>(\n tools: ReadonlyArray<T>,\n env: McpEnvironment,\n): T[] {\n return tools.filter(\n (t) =>\n t.availableIn === 'both' ||\n (t.availableIn === 'relay' && isRelayEnv(env)) ||\n t.availableIn === env,\n );\n}\n\n/**\n * Tool names that are available before any page attaches (bootstrap tier).\n *\n * `build_attach_url` — pure URL synthesis, no attach needed.\n * `list_pages` — reports tunnel status + empty pages even pre-attach.\n *\n * All other tools require an attached page (`enableDomains` must succeed) and\n * are only advertised in `tools/list` once a target appears.\n */\nexport const BOOTSTRAP_TOOL_NAMES: ReadonlySet<string> = new Set<string>([\n 'build_attach_url',\n 'get_diagnostics',\n 'list_pages',\n]);\n\n/** Normalized console message returned by `list_console_messages`. */\nexport interface ConsoleMessage {\n level: string;\n text: string;\n timestamp: number;\n args: string[];\n}\n\n/** Normalized network request returned by `list_network_requests`. */\nexport interface NetworkRequest {\n requestId: string;\n url: string;\n method: string;\n /** HTTP status once a response was seen, else null (still in-flight). */\n status: number | null;\n statusText: string | null;\n /** Request start (CDP timestamp). */\n startTime: number;\n /** Response received (CDP timestamp), else null. */\n endTime: number | null;\n}\n\n/** Renders a CDP `RemoteObject` console arg to a stable display string. */\nfunction renderRemoteObject(arg: CdpRemoteObject): string {\n if (arg.value !== undefined) {\n if (typeof arg.value === 'string') return arg.value;\n try {\n return JSON.stringify(arg.value);\n } catch {\n return String(arg.value);\n }\n }\n if (arg.description !== undefined) return arg.description;\n if (arg.className !== undefined) return arg.className;\n return arg.subtype ?? arg.type;\n}\n\nexport function normalizeConsoleMessage(event: ConsoleApiCalledEvent): ConsoleMessage {\n const args = event.args.map(renderRemoteObject);\n return {\n level: event.type,\n text: args.join(' '),\n timestamp: event.timestamp,\n args,\n };\n}\n\nexport function listConsoleMessages(connection: CdpConnection): ConsoleMessage[] {\n return connection\n .getBufferedEvents('Runtime.consoleAPICalled')\n .map((event) => normalizeConsoleMessage(event));\n}\n\nexport function listNetworkRequests(connection: CdpConnection): NetworkRequest[] {\n const requests = connection.getBufferedEvents('Network.requestWillBeSent');\n const responses = connection.getBufferedEvents('Network.responseReceived');\n\n const responseByRequestId = new Map<string, NetworkResponseReceivedEvent>();\n for (const response of responses) {\n responseByRequestId.set(response.requestId, response);\n }\n\n return requests.map((request: NetworkRequestWillBeSentEvent) => {\n const response = responseByRequestId.get(request.requestId);\n return {\n requestId: request.requestId,\n url: request.request.url,\n method: request.request.method,\n status: response ? response.response.status : null,\n statusText: response ? response.response.statusText : null,\n startTime: request.timestamp,\n endTime: response ? response.timestamp : null,\n };\n });\n}\n\n/* -------------------------------------------------------------------------- */\n/* list_exceptions — Runtime.exceptionThrown ring buffer */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Normalized exception returned by `list_exceptions`.\n *\n * Flattens the CDP `Runtime.ExceptionDetails` shape into the most useful\n * fields. The `raw` field carries the original event for callers that need\n * the full payload.\n */\nexport interface BufferedException {\n /** Wall-clock ms since epoch (CDP `Runtime.Timestamp`). */\n timestamp: number;\n /** Short summary text from `exceptionDetails.text`. */\n text: string;\n /** Source URL where the exception was thrown, if known. */\n url?: string;\n /** 0-based line number in the source file, if known. */\n lineNumber?: number;\n /** 0-based column number in the source file, if known. */\n columnNumber?: number;\n /** `description` of the thrown `RemoteObject` (e.g. \"TypeError: …\"). */\n exceptionText?: string;\n /**\n * Formatted stack trace: `at fn (url:line:col)` lines joined by `\\n`.\n * Omitted when no `stackTrace.callFrames` are available.\n */\n stack?: string;\n /** Full original `Runtime.exceptionThrown` event payload. */\n raw: RuntimeExceptionThrownEvent;\n}\n\n/** Formats a single CDP call frame into `at fn (url:line:col)`. */\nfunction formatCallFrame(frame: CdpCallFrame): string {\n const fn = frame.functionName || '(anonymous)';\n return `at ${fn} (${frame.url}:${frame.lineNumber}:${frame.columnNumber})`;\n}\n\n/** Normalizes a raw `Runtime.exceptionThrown` event into a `BufferedException`. */\nexport function normalizeException(event: RuntimeExceptionThrownEvent): BufferedException {\n const { timestamp, exceptionDetails } = event;\n const frames = exceptionDetails.stackTrace?.callFrames;\n const stack = frames && frames.length > 0 ? frames.map(formatCallFrame).join('\\n') : undefined;\n const exceptionText = exceptionDetails.exception?.description ?? undefined;\n\n const result: BufferedException = {\n timestamp,\n text: exceptionDetails.text,\n raw: event,\n };\n if (exceptionDetails.url !== undefined) result.url = exceptionDetails.url;\n if (exceptionDetails.lineNumber !== undefined) result.lineNumber = exceptionDetails.lineNumber;\n if (exceptionDetails.columnNumber !== undefined)\n result.columnNumber = exceptionDetails.columnNumber;\n if (exceptionText !== undefined) result.exceptionText = exceptionText;\n if (stack !== undefined) result.stack = stack;\n return result;\n}\n\n/**\n * Returns the most recent buffered `Runtime.exceptionThrown` events, normalized.\n * Oldest-first; limited to `limit` entries (default 50, max 50).\n */\nexport function listExceptions(connection: CdpConnection, limit = 50): BufferedException[] {\n const cap = Math.min(Math.max(1, limit), 50);\n const events = connection.getBufferedEvents('Runtime.exceptionThrown');\n // Slice from the tail to respect the cap while preserving oldest-first order.\n const sliced = events.length > cap ? events.slice(events.length - cap) : events;\n return sliced.map((e) => normalizeException(e));\n}\n\n/** A page entry in the `list_pages` result, extended with freshness info. */\nexport interface ListPagesEntry {\n id: string;\n title: string;\n url: string;\n /** ISO timestamp of the last inbound CDP message from this target, or null. */\n lastSeenAt: string | null;\n}\n\n/** Result of `list_pages`: attach status + tunnel state + crash info. */\nexport interface ListPagesResult {\n /**\n * The single active page, or an empty array when nothing is attached.\n * Under the single-attach model this is always 0 or 1 entries.\n */\n pages: ListPagesEntry[];\n tunnel: TunnelStatus;\n /**\n * ISO timestamp of the most recent crash / targetDestroyed / detachedFromTarget\n * event detected since the last `enableDomains()`, or `null` if none.\n * When non-null, all attached pages have been removed from the relay map and\n * a new `enableDomains()` call is required to resume debugging.\n */\n crashDetectedAt: string | null;\n /** Korean warning line shown in tool output when a crash was detected. */\n crashWarning: string | null;\n /**\n * Always `true` — signals to the agent that at most one page is ever present.\n * When a second page attaches, the previous one is evicted (last-attach wins).\n */\n singleAttachModel: true;\n}\n\n/**\n * Duck-type interface for the crash-detection extras exposed by `ChiiCdpConnection`.\n * The base `CdpConnection` interface is kept minimal (fake-friendly); the extras\n * are opt-in so tests without them continue to compile.\n */\ninterface CrashAwareCdpConnection extends CdpConnection {\n getLastCrashDetectedAt(): number | null;\n getTargetLastSeenAt(targetId: string): number | null;\n}\n\nfunction isCrashAware(conn: CdpConnection): conn is CrashAwareCdpConnection {\n return (\n typeof (conn as CrashAwareCdpConnection).getLastCrashDetectedAt === 'function' &&\n typeof (conn as CrashAwareCdpConnection).getTargetLastSeenAt === 'function'\n );\n}\n\nexport function listPages(connection: CdpConnection, tunnel: TunnelStatus): ListPagesResult {\n const rawTargets = connection.listTargets();\n const pages: ListPagesEntry[] = rawTargets.map((t) => {\n const lastSeenMs = isCrashAware(connection) ? connection.getTargetLastSeenAt(t.id) : null;\n return {\n id: t.id,\n title: t.title,\n url: t.url,\n lastSeenAt: lastSeenMs !== null ? new Date(lastSeenMs).toISOString() : null,\n };\n });\n\n const crashMs = isCrashAware(connection) ? connection.getLastCrashDetectedAt() : null;\n const crashDetectedAt = crashMs !== null ? new Date(crashMs).toISOString() : null;\n const crashWarning = crashDetectedAt\n ? `[ait-debug] page crash 감지됨 — 새 attach 필요 (관측 시각: ${crashDetectedAt})`\n : null;\n\n return { pages, tunnel, crashDetectedAt, crashWarning, singleAttachModel: true };\n}\n\n/** A `build_attach_url` result: the spliced deep link the phone should open. */\nexport interface BuildAttachUrlResult {\n /** The scheme URL with `debug=1&relay=<wss>` spliced in. */\n attachUrl: string;\n /** The relay URL that was spliced in (this session's quick tunnel). */\n relayUrl: string;\n /**\n * Non-fatal warning about the scheme URL's authority being missing or\n * suspicious (e.g. \"web\", \"localhost\"). Callers should surface this to\n * help the user catch a malformed URL early.\n */\n authorityWarning?: string;\n}\n\n/**\n * Builds a self-attaching dogfood deep link from an `ait deploy --scheme-only`\n * URL plus this session's live relay. Throws if the tunnel is not up yet (no\n * relay URL to splice in) — the caller surfaces that as a tool error.\n *\n * Also validates the scheme URL's authority. A suspicious authority (empty,\n * \"web\", \"localhost\", etc.) is surfaced as a non-fatal `authorityWarning` on\n * the result so the caller can show a helpful hint without blocking the link\n * generation (the warning is consistent with how other validation in\n * `buildDeepLinkAttachUrl` works — hard errors for relay, soft warning for\n * the scheme authority which is in the caller's input, not ours to own).\n */\nexport function buildAttachUrl(schemeUrl: string, tunnel: TunnelStatus): BuildAttachUrlResult {\n if (!tunnel.up || tunnel.wssUrl === null) {\n throw new Error(\n 'tunnel-down: cloudflared 터널이 안 떠 있습니다. ' +\n 'MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.',\n );\n }\n const authorityWarning = validateSchemeAuthority(schemeUrl) ?? undefined;\n return {\n attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl),\n relayUrl: tunnel.wssUrl,\n ...(authorityWarning !== undefined ? { authorityWarning } : {}),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* QR PNG rendering + browser open */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Heuristic: can this process open a GUI browser?\n *\n * Returns `true` when we think a GUI is available:\n * - On macOS (`darwin`) we assume yes (MCP normally runs on the user's Mac).\n * - On Linux we check for `DISPLAY` or `WAYLAND_DISPLAY`.\n * - On Windows we assume yes.\n * - In a CI environment (`CI=true`) we assume no.\n */\nexport function canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\n}\n\n/**\n * Result of `openQrInBrowser`.\n *\n * HTTP URL 기반으로 재구현 — tmp 파일 없음. `httpUrl`이 브라우저에 전달되는 URL이다.\n * SECRET-HANDLING: `httpUrl`은 127.0.0.1 로컬 전용이며 at= 코드 값을 직접 담지 않는다\n * (attachUrl은 /attach?u= query로 전달되어 서버 메모리에서만 처리).\n */\nexport interface OpenQrInBrowserResult {\n /** `true` if the browser was successfully opened. */\n opened: boolean;\n /** `http://127.0.0.1:<port>/attach?u=...` — 브라우저에 전달된 URL. */\n httpUrl: string;\n /** `http://127.0.0.1:<port>/qr.png?u=...` — PNG fallback URL. */\n pngUrl: string;\n /** Error message if `opened` is false (browser spawn failed). */\n error?: string;\n /** Captured stderr from failed spawn attempts (at= 값은 redact됨). */\n stderrSummary?: string;\n /**\n * `true` when the first attempt failed but a retry succeeded.\n * Helps distinguish \"worked on first try\" from \"needed retry\" in diagnostics.\n */\n retried?: boolean;\n}\n\n/** platform별 browser open 명령 후보 목록 — 앞에서부터 순차 시도. */\nfunction getBrowserCandidates(httpUrl: string): Array<{ cmd: string; args: string[] }> {\n const platform = process.platform;\n if (platform === 'darwin') {\n return [\n { cmd: 'open', args: [httpUrl] },\n { cmd: 'open', args: ['-a', 'Safari', httpUrl] },\n { cmd: 'open', args: ['-a', 'Google Chrome', httpUrl] },\n { cmd: 'open', args: ['-a', 'Firefox', httpUrl] },\n ];\n }\n if (platform === 'win32') {\n return [\n { cmd: 'cmd', args: ['/c', 'start', '', httpUrl] },\n { cmd: 'rundll32', args: ['url.dll,FileProtocolHandler', httpUrl] },\n ];\n }\n // linux + fallback\n return [\n { cmd: 'xdg-open', args: [httpUrl] },\n { cmd: 'sensible-browser', args: [httpUrl] },\n { cmd: 'x-www-browser', args: [httpUrl] },\n { cmd: 'firefox', args: [httpUrl] },\n { cmd: 'google-chrome', args: [httpUrl] },\n { cmd: 'chromium', args: [httpUrl] },\n ];\n}\n\n/** stderr에서 at= TOTP 코드 값을 redact한다. */\nfunction redactSecrets(text: string): string {\n // at=<value> 패턴에서 값 부분을 redact — TOTP 코드가 노출되지 않도록.\n return text.replace(/\\bat=([^&\\s\"']+)/g, 'at=<redacted>');\n}\n\n/** spawnSync exit 0이어도 stderr에 launch 실패 시그널이 있으면 실패로 판단한다. */\nconst LAUNCH_FAILURE_PATTERNS = [\n /LSOpenURLsWithRole\\(\\) failed/,\n /kLSApplicationNotFoundErr/,\n /No application/,\n /Unable to find application/,\n /xdg-open: not found/,\n /command not found/,\n];\n\nfunction isLaunchFailureStderr(stderr: string): boolean {\n return LAUNCH_FAILURE_PATTERNS.some((p) => p.test(stderr));\n}\n\n/**\n * 로컬 HTTP 서버 URL(`http://127.0.0.1:<port>/attach?u=...`)을 OS 기본 브라우저로 연다.\n *\n * platform별 fallback chain으로 시도하며, 모두 실패하면 1회 retry를 수행한다\n * (ephemeral process launch 타이밍 문제 대응). retry까지 실패해도 `opened: false` +\n * `httpUrl`을 반환해 사용자가 직접 브라우저에 붙여넣을 수 있게 한다.\n *\n * SECRET-HANDLING:\n * - tmp 파일을 만들지 않는다 (HTML/PNG는 HTTP 서버가 메모리에서 응답).\n * - httpUrl/pngUrl은 127.0.0.1 로컬 전용.\n * - stderr 캡처 결과에서 at= 코드 값을 redact한 후 stderrSummary에 포함.\n * - attachUrl, deploymentId, TOTP 코드를 stdout/stderr/로그에 직접 출력 금지.\n *\n * @param httpUrl - `http://127.0.0.1:<port>/attach?u=<encoded>` HTTP URL.\n * @param pngUrl - `http://127.0.0.1:<port>/qr.png?u=<encoded>` PNG fallback URL.\n */\nexport async function openQrInBrowser(\n httpUrl: string,\n pngUrl: string,\n): Promise<OpenQrInBrowserResult> {\n const { spawnSync } = await import('node:child_process');\n\n /**\n * 한 번의 fallback chain 시도. 성공하면 열린 후보 cmd를 반환, 실패하면 null.\n * stderrLines에 각 후보의 stderr를 누적한다.\n */\n function tryOnce(stderrLines: string[]): boolean {\n const candidates = getBrowserCandidates(httpUrl);\n for (const { cmd, args } of candidates) {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5000 });\n\n if (result.error) {\n stderrLines.push(`${cmd}: ${result.error.message}`);\n continue;\n }\n\n const stderr = typeof result.stderr === 'string' ? result.stderr : '';\n if (stderr) {\n stderrLines.push(`${cmd}: ${redactSecrets(stderr.trim())}`);\n }\n\n if (result.status === 0 && !isLaunchFailureStderr(stderr)) {\n return true;\n }\n }\n return false;\n }\n\n const stderrLines: string[] = [];\n\n // 1차 시도\n if (tryOnce(stderrLines)) {\n return { opened: true, httpUrl, pngUrl };\n }\n\n // 1회 retry (ephemeral process launch 타이밍 문제 대응)\n if (tryOnce(stderrLines)) {\n return { opened: true, httpUrl, pngUrl, retried: true };\n }\n\n const stderrSummary = stderrLines.length > 0 ? stderrLines.join('\\n') : undefined;\n return {\n opened: false,\n httpUrl,\n pngUrl,\n error: '모든 브라우저 실행 후보가 실패했습니다.',\n stderrSummary,\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Phase 2 — DOM / snapshot / screenshot (CDP commands) */\n/* -------------------------------------------------------------------------- */\n\n/** Returns the DOM tree of the attached page (`DOM.getDocument`). */\nexport function getDomDocument(connection: CdpConnection): Promise<DomGetDocumentResult> {\n // `pierce: true` flattens shadow roots; depth -1 returns the whole subtree so\n // a single call yields the full tree for structural diagnosis.\n return connection.send('DOM.getDocument', { depth: -1, pierce: true });\n}\n\n/** Returns a serialized page snapshot (`DOMSnapshot.captureSnapshot`). */\nexport function takeSnapshot(connection: CdpConnection): Promise<DomSnapshotResult> {\n return connection.send('DOMSnapshot.captureSnapshot', {});\n}\n\n/** A `take_screenshot` result: the raw base64 PNG plus a ready-to-use data URI. */\nexport interface ScreenshotResult {\n /** Base64-encoded PNG bytes (no data-URI prefix). */\n data: string;\n /** `data:image/png;base64,…` form for clients that render a URI. */\n dataUri: string;\n mimeType: 'image/png';\n}\n\n/** Captures a PNG screenshot of the attached page (`Page.captureScreenshot`). */\nexport async function takeScreenshot(connection: CdpConnection): Promise<ScreenshotResult> {\n const { data } = await connection.send('Page.captureScreenshot', { format: 'png' });\n return { data, dataUri: `data:image/png;base64,${data}`, mimeType: 'image/png' };\n}\n\n/* -------------------------------------------------------------------------- */\n/* measure_safe_area — Runtime.evaluate probe */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The JS probe injected via `Runtime.evaluate`. It reads:\n * 1. `env(safe-area-inset-*)` via a temporary element with padding set to\n * those CSS env vars, then `getComputedStyle`.\n * 2. SDK insets via a priority chain so the SAME probe works on both relay\n * (real device) and mock (devtools panel page):\n * a. `window.__sdk.SafeAreaInsets.get()` — dogfood bundle on real device.\n * b. `window.__sdk.getSafeAreaInsets()` — dogfood bundle (deprecated).\n * c. `window.__ait.state.safeAreaInsets` — devtools mock state (mock env).\n * The probe records `sdkInsetsSource` = `'window.__sdk'` | `'window.__ait'`\n * | `null`. If all paths fail the result carries `sdkInsetsError`.\n * 3. nav bar geometry: the SDK does not expose navBar height as a standalone\n * API — `.ait-navbar` DOM height is read as a cross-check, and\n * `navBarHeightSource` records where it came from.\n * 4. `innerWidth`, `innerHeight`, `devicePixelRatio`, `navigator.userAgent`.\n *\n * Returns a plain JSON-serialisable object so `returnByValue: true` works.\n *\n * NOTE: This expression is evaluated in the page context — on the real device\n * (relay) or on the mock panel page. It does not mutate any page state — the\n * temporary element is removed after reading. No secret or auth token is read\n * or returned.\n *\n * RFC #277 Tier C parity: the SAME probe string runs in both envs. Mock fidelity\n * comes from the panel's `applyViewport` / `computeSafeAreaInsets` correctly\n * setting `window.__ait.state.safeAreaInsets` (#275). When that is correct,\n * the cssEnv + sdkInsets pair returned here matches the relay's shape.\n */\nexport const SAFE_AREA_PROBE_EXPRESSION = `\n(function() {\n var el = document.createElement('div');\n el.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;visibility:hidden;' +\n 'padding-top:env(safe-area-inset-top,0px);' +\n 'padding-right:env(safe-area-inset-right,0px);' +\n 'padding-bottom:env(safe-area-inset-bottom,0px);' +\n 'padding-left:env(safe-area-inset-left,0px)';\n document.documentElement.appendChild(el);\n var cs = window.getComputedStyle(el);\n var cssEnv = {\n top: parseFloat(cs.paddingTop) || 0,\n right: parseFloat(cs.paddingRight) || 0,\n bottom: parseFloat(cs.paddingBottom) || 0,\n left: parseFloat(cs.paddingLeft) || 0\n };\n document.documentElement.removeChild(el);\n var sdkInsets = null;\n var sdkInsetsSource = null;\n var sdkInsetsError = undefined;\n try {\n var sdk = window.__sdk;\n var ait = window.__ait;\n if (sdk && sdk.SafeAreaInsets && typeof sdk.SafeAreaInsets.get === 'function') {\n sdkInsets = sdk.SafeAreaInsets.get();\n sdkInsetsSource = 'window.__sdk';\n } else if (sdk && typeof sdk.getSafeAreaInsets === 'function') {\n sdkInsets = sdk.getSafeAreaInsets();\n sdkInsetsSource = 'window.__sdk';\n } else if (ait && ait.state && ait.state.safeAreaInsets &&\n typeof ait.state.safeAreaInsets.top === 'number') {\n var s = ait.state.safeAreaInsets;\n sdkInsets = { top: s.top, bottom: s.bottom, left: s.left, right: s.right };\n sdkInsetsSource = 'window.__ait';\n } else if (!sdk && !ait) {\n sdkInsetsError = 'neither window.__sdk (relay) nor window.__ait (mock) available';\n } else if (sdk) {\n sdkInsetsError = 'neither SafeAreaInsets.get nor getSafeAreaInsets found on window.__sdk';\n } else {\n sdkInsetsError = 'window.__ait.state.safeAreaInsets is missing or malformed';\n }\n } catch(e) {\n sdkInsetsError = String(e && e.message || e);\n }\n var navBarHeight = null;\n var navBarHeightSource = 'not-exposed-by-sdk';\n try {\n var nb = document.querySelector('.ait-navbar');\n if (nb) {\n navBarHeight = nb.getBoundingClientRect().height;\n navBarHeightSource = 'dom-.ait-navbar';\n }\n } catch(_) {}\n var result = {\n cssEnv: cssEnv,\n sdkInsets: sdkInsets,\n sdkInsetsSource: sdkInsetsSource,\n navBarHeight: navBarHeight,\n navBarHeightSource: navBarHeightSource,\n innerWidth: window.innerWidth,\n innerHeight: window.innerHeight,\n devicePixelRatio: window.devicePixelRatio,\n userAgent: navigator.userAgent\n };\n if (sdkInsetsError !== undefined) result.sdkInsetsError = sdkInsetsError;\n return JSON.stringify(result);\n})()\n`.trim();\n\n/**\n * Where the SDK insets came from. `null` when the lookup failed (in which case\n * `sdkInsetsError` is populated).\n *\n * - `'window.__sdk'` — real-device dogfood bundle (relay env).\n * - `'window.__ait'` — devtools mock state (mock env).\n * - `null` — both paths absent or threw.\n */\nexport type SdkInsetsSource = 'window.__sdk' | 'window.__ait' | null;\n\n/**\n * Normalized result returned by `measure_safe_area`.\n *\n * All inset values are in CSS pixels as reported by the page context.\n * `userAgent` is included for device identification; it never contains\n * authentication secrets or session tokens.\n */\nexport interface SafeAreaMeasurement {\n /**\n * MCP environment this measurement was taken in:\n * - `'mock'` — dev browser panel\n * - `'relay-dev'` — real-device WebView, dogfood build\n * - `'relay-live'` — real-device WebView, live/production build\n *\n * Set by the caller (`measureSafeArea`) from the env detection SSoT\n * (`getEnvironment`).\n */\n source: McpEnvironment;\n /**\n * `env(safe-area-inset-*)` values read via `getComputedStyle` on the page.\n * On iOS inside the Toss host WebView this is typically all-zero because the\n * WebView viewport is placed below the physical notch by the host app.\n */\n cssEnv: { top: number; right: number; bottom: number; left: number };\n /**\n * SDK insets from one of three paths (in priority order):\n * - `window.__sdk.SafeAreaInsets.get()` (relay, dogfood bundle)\n * - `window.__sdk.getSafeAreaInsets()` (relay, deprecated)\n * - `window.__ait.state.safeAreaInsets` (mock, devtools panel state)\n *\n * `null` when all paths fail — see `sdkInsetsError` for the reason.\n * In the Toss host WebView `top` is the nav bar height and `bottom` is the\n * home-indicator height.\n */\n sdkInsets: { top: number; right: number; bottom: number; left: number } | null;\n /**\n * Which path resolved `sdkInsets` — useful for diagnosis of fidelity gaps\n * between mock and relay. `null` when `sdkInsets` is `null`.\n */\n sdkInsetsSource: SdkInsetsSource;\n /**\n * Populated when the SDK inset lookup failed (all paths absent or threw).\n * `undefined` when `sdkInsets` is non-null (i.e. the lookup succeeded).\n *\n * Example values:\n * - `\"neither window.__sdk (relay) nor window.__ait (mock) available\"`\n * - `\"neither SafeAreaInsets.get nor getSafeAreaInsets found on window.__sdk\"`\n * - `\"window.__ait.state.safeAreaInsets is missing or malformed\"`\n * - `\"TypeError: ...\"`\n */\n sdkInsetsError?: string;\n /**\n * Height of the `.ait-navbar` element (px) if present, else `null`.\n * The SDK does not expose navBar height as a standalone API; this DOM\n * measurement is used to cross-validate `sdkInsets.top`.\n */\n navBarHeight: number | null;\n /**\n * Describes where `navBarHeight` came from:\n * - `\"dom-.ait-navbar\"` — read from the `.ait-navbar` element's bounding rect.\n * - `\"not-exposed-by-sdk\"` — the SDK has no standalone navBar height API and\n * no `.ait-navbar` element was found in the DOM.\n */\n navBarHeightSource: string;\n /** CSS viewport width (`window.innerWidth`). */\n innerWidth: number;\n /** CSS viewport height (`window.innerHeight`). */\n innerHeight: number;\n /**\n * Device pixel ratio (`window.devicePixelRatio`).\n * Note: `window.devicePixelRatio` is read-only in the browser, so devtools\n * cannot emulate DPR locally — this is the ground-truth value from the device.\n */\n devicePixelRatio: number;\n /**\n * `navigator.userAgent` string for device identification.\n * Does not contain authentication secrets.\n */\n userAgent: string;\n}\n\n/**\n * Parses a raw `Runtime.evaluate` result value into a `SafeAreaMeasurement`.\n * The probe returns a JSON string (because `returnByValue:true` with a plain\n * object works unreliably across Chii relay versions — stringifying is safer).\n *\n * `source` is supplied by the caller (`measureSafeArea`) from the env SSoT.\n *\n * Throws if the result is missing, contains an exception, or cannot be parsed.\n */\nexport function normalizeSafeAreaResult(\n rawValue: unknown,\n source: McpEnvironment,\n): SafeAreaMeasurement {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `measure_safe_area: probe returned unexpected type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n throw new Error(`measure_safe_area: probe returned non-JSON string: ${rawValue}`);\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('measure_safe_area: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n\n function requireInsets(\n key: string,\n ): { top: number; right: number; bottom: number; left: number } | null {\n const v = obj[key];\n if (v === null || v === undefined) return null;\n if (typeof v !== 'object') return null;\n const r = v as Record<string, unknown>;\n return {\n top: typeof r.top === 'number' ? r.top : 0,\n right: typeof r.right === 'number' ? r.right : 0,\n bottom: typeof r.bottom === 'number' ? r.bottom : 0,\n left: typeof r.left === 'number' ? r.left : 0,\n };\n }\n\n const cssEnv = requireInsets('cssEnv') ?? { top: 0, right: 0, bottom: 0, left: 0 };\n const sdkInsets = requireInsets('sdkInsets');\n const sdkInsetsSource: SdkInsetsSource =\n obj.sdkInsetsSource === 'window.__sdk' || obj.sdkInsetsSource === 'window.__ait'\n ? obj.sdkInsetsSource\n : null;\n const sdkInsetsError = typeof obj.sdkInsetsError === 'string' ? obj.sdkInsetsError : undefined;\n const navBarHeight = typeof obj.navBarHeight === 'number' ? obj.navBarHeight : null;\n const navBarHeightSource =\n typeof obj.navBarHeightSource === 'string' ? obj.navBarHeightSource : 'not-exposed-by-sdk';\n const innerWidth = typeof obj.innerWidth === 'number' ? obj.innerWidth : 0;\n const innerHeight = typeof obj.innerHeight === 'number' ? obj.innerHeight : 0;\n const devicePixelRatio = typeof obj.devicePixelRatio === 'number' ? obj.devicePixelRatio : 1;\n const userAgent = typeof obj.userAgent === 'string' ? obj.userAgent : '';\n\n return {\n source,\n cssEnv,\n sdkInsets,\n sdkInsetsSource,\n ...(sdkInsetsError !== undefined ? { sdkInsetsError } : {}),\n navBarHeight,\n navBarHeightSource,\n innerWidth,\n innerHeight,\n devicePixelRatio,\n userAgent,\n };\n}\n\n/**\n * Runs the safe-area probe on the attached page and returns a normalized\n * `SafeAreaMeasurement`. Read-only — does not mutate page state.\n *\n * `source` is supplied by the caller from the env detection SSoT (see\n * `src/mcp/environment.ts`). The same `Runtime.evaluate` call runs in both\n * envs — the probe expression tries `window.__sdk` first (relay) then\n * `window.__ait` (mock), so mock fidelity is enforced by the panel's\n * `applyViewport`/`computeSafeAreaInsets` keeping `__ait.state.safeAreaInsets`\n * correct (RFC #277 Tier C parity, #275 model).\n *\n * Throws on CDP error, probe exception, or result parse failure.\n */\nexport async function measureSafeArea(\n connection: CdpConnection,\n source: McpEnvironment,\n): Promise<SafeAreaMeasurement> {\n const result = await connection.send('Runtime.evaluate', {\n expression: SAFE_AREA_PROBE_EXPRESSION,\n returnByValue: true,\n awaitPromise: false,\n });\n if (result.exceptionDetails) {\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`measure_safe_area: probe threw — ${msg}`);\n }\n return normalizeSafeAreaResult(result.result.value, source);\n}\n\n/* -------------------------------------------------------------------------- */\n/* evaluate — arbitrary JS via Runtime.evaluate */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result returned by the `evaluate` tool.\n *\n * `value` holds the `returnByValue` result from CDP — it may be any\n * JSON-serialisable type. Treat it as opaque for logging purposes (it could\n * carry sensitive data from the page context).\n *\n * SECRET-HANDLING: do NOT write `value` to any log or stderr — return it to\n * the agent via the tool result only.\n */\nexport interface EvaluateResult {\n /** The evaluated result value (`returnByValue: true`). */\n value: unknown;\n /** CDP type string of the result (e.g. \"string\", \"number\", \"object\"). */\n type: string;\n}\n\n/**\n * Evaluates an arbitrary JS expression on the attached page via\n * `Runtime.evaluate`. NOT read-only — the expression may have side effects.\n *\n * Throws if the evaluation produced a CDP exception.\n *\n * SECRET-HANDLING: expression and result value are NOT written to any log.\n */\nexport async function evaluate(\n connection: CdpConnection,\n expression: string,\n): Promise<EvaluateResult> {\n const result = await connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: false,\n });\n if (result.exceptionDetails) {\n // Surface only the engine error string — never the expression or result value.\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`evaluate failed: ${msg}`);\n }\n return { value: result.result.value, type: result.result.type };\n}\n\n/* -------------------------------------------------------------------------- */\n/* call_sdk — window.__sdkCall bridge via Runtime.evaluate */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result returned by the `call_sdk` tool.\n * The bridge call wraps success/failure in a JSON envelope so cross-Chii\n * stringification is reliable (same approach as `measure_safe_area`).\n *\n * `recentException` is populated when a `Runtime.exceptionThrown` event was\n * observed within the heuristic triage window [callStart-50ms, callEnd+200ms].\n * This helps correlate an SDK throw with the bridge result, especially when\n * the SDK throws synchronously before the promise resolves.\n */\nexport type CallSdkResult =\n | { ok: true; value: unknown; recentException?: BufferedException }\n | { ok: false; error: string; recentException?: BufferedException };\n\n/**\n * Builds the Runtime.evaluate expression that calls `window.__sdkCall` with\n * the given method name and args, awaits the promise, and returns a JSON\n * envelope `{ok, value/error}` as a string.\n *\n * Name and args are embedded via `JSON.stringify` so they are safely escaped.\n * The expression checks for `window.__sdkCall` and returns a clear error if\n * it is absent (non-dogfood bundle).\n *\n * SECRET-HANDLING: the expression is built here and MUST NOT be written to\n * any log or stderr by the caller.\n */\nexport function buildCallSdkExpression(name: string, args: unknown[]): string {\n const safeName = JSON.stringify(name);\n const safeArgs = JSON.stringify(args);\n return (\n `(async () => {` +\n ` if (typeof window.__sdkCall !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'sdk-absent: window.__sdkCall이 주입되지 않았습니다 (dogfood 빌드가 아닙니다). dogfood 채널로 재배포하세요.'});` +\n ` }` +\n ` try {` +\n ` const r = await window.__sdkCall(${safeName}, ...${safeArgs});` +\n ` return JSON.stringify({ok:true,value:r});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Parses the JSON envelope string returned by the `call_sdk` expression.\n * Returns a typed `CallSdkResult`.\n *\n * Throws only on parse failure (not on ok:false — that is a normal result).\n */\nexport function normalizeCallSdkResult(rawValue: unknown): CallSdkResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `call_sdk: bridge returned unexpected type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue in the error message — it could contain secrets.\n throw new Error('call_sdk: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('call_sdk: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, value: obj.value };\n }\n if (obj.ok === false) {\n return { ok: false, error: typeof obj.error === 'string' ? obj.error : String(obj.error) };\n }\n throw new Error('call_sdk: bridge result missing \"ok\" field');\n}\n\n/**\n * Looks up the most recent exception from the buffer that falls within the\n * triage window [windowStart, windowEnd]. Returns `undefined` if none found.\n *\n * The heuristic window is:\n * - windowStart = callStart - 50ms (catch sync throws before bridge fires)\n * - windowEnd = callEnd + 200ms (catch async throws resolved soon after)\n *\n * Only the most recent exception within the window is returned (the one most\n * likely to be causally related to the SDK call).\n */\nfunction findRecentException(\n connection: CdpConnection,\n windowStart: number,\n windowEnd: number,\n): BufferedException | undefined {\n const events = connection.getBufferedEvents('Runtime.exceptionThrown');\n // Scan from the tail (most recent) to find the closest-in-time exception.\n for (let i = events.length - 1; i >= 0; i--) {\n const e = events[i];\n if (e.timestamp >= windowStart && e.timestamp <= windowEnd) {\n return normalizeException(e);\n }\n }\n return undefined;\n}\n\n/**\n * Calls a dogfood SDK method via `window.__sdkCall` on the attached page.\n * NOT read-only — SDK calls may have side effects.\n *\n * On env 2/3 (real device relay) this hits the real SDK; on env 1 (local\n * mock) it hits the mock SDK.\n *\n * 인자 시그니처 검증: 등록된 메서드는 bridge 호출 전에 인자를 검증하고, mismatch면\n * `{ok:false, error}` MCP 오류 결과를 반환한다(bridge에 도달하지 않음).\n * 미등록 메서드는 passthrough + stderr 경고 1회.\n *\n * Throws on CDP error or result parse failure. Returns `{ok:false, error}`\n * for bridge-level errors (method not found, SDK threw, bridge absent) or\n * argument schema violations.\n *\n * If a `Runtime.exceptionThrown` event was observed within the triage window\n * [callStart-50ms, callEnd+200ms], the result includes `recentException` for\n * crash triage. This window is a heuristic — it catches the common case of an\n * SDK throw immediately before/after the bridge resolves.\n *\n * SECRET-HANDLING: name, args, and the result value are NOT written to any log.\n */\nexport async function callSdk(\n connection: CdpConnection,\n name: string,\n args: unknown[],\n): Promise<CallSdkResult> {\n // 인자 시그니처 검증 — bridge 호출 전에 reject하여 native crash를 예방한다.\n const signature = lookupSignature(name);\n if (signature !== undefined) {\n const validation = signature.validateArgs(args);\n if (!validation.ok) {\n // isError: true 형태로 반환 — bridge에 도달하지 않음.\n const errorText =\n `call_sdk(\"${name}\") 인자 시그니처 오류.\\n` +\n `받음: ${validation.received}\\n` +\n `기대: ${validation.expected}\\n` +\n `올바른 예시: ${signature.example}`;\n return { ok: false, error: errorText };\n }\n } else {\n // 미등록 메서드 — passthrough하지만 stderr에 경고 1회.\n warnPassthrough(name);\n }\n\n const callStart = Date.now();\n const expression = buildCallSdkExpression(name, args);\n const result = await connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\n });\n const callEnd = Date.now();\n\n if (result.exceptionDetails) {\n // Surface only the engine error string — never name, args, or result value.\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`call_sdk threw: ${msg}`);\n }\n\n const sdkResult = normalizeCallSdkResult(result.result.value);\n\n // Triage window: [callStart - 50ms, callEnd + 200ms].\n // -50ms: catches sync throws that fire just before the bridge call is sent.\n // +200ms: catches async throws resolved shortly after the bridge returns.\n const recentException = findRecentException(connection, callStart - 50, callEnd + 200);\n\n if (recentException !== undefined) {\n return { ...sdkResult, recentException };\n }\n return sdkResult;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Phase 3 — AIT.* domain (CDP can't cover these) */\n/* -------------------------------------------------------------------------- */\n\n/** Set of tool names served by the AIT source rather than the CDP connection. */\nconst AIT_TOOL_NAMES = new Set<string>([\n 'AIT.getSdkCallHistory',\n 'AIT.getMockState',\n 'AIT.getOperationalEnvironment',\n]);\n\n/** True for the Phase 3 AIT.* tools (served by an `AitSource`, not CDP). */\nexport function isAitToolName(name: string): boolean {\n return AIT_TOOL_NAMES.has(name);\n}\n\n/** Returns the recent SDK call trace (`AIT.getSdkCallHistory`). */\nexport function getSdkCallHistory(source: AitSource): Promise<AitSdkCallHistory> {\n return source.get('AIT.getSdkCallHistory');\n}\n\n/** Returns the devtools mock-state snapshot (`AIT.getMockState`). */\nexport function getMockState(source: AitSource): Promise<AitMockState> {\n return source.get('AIT.getMockState');\n}\n\n/** Returns the operational environment + SDK version (`AIT.getOperationalEnvironment`). */\nexport function getOperationalEnvironment(source: AitSource): Promise<AitOperationalEnvironment> {\n return source.get('AIT.getOperationalEnvironment');\n}\n\n/* -------------------------------------------------------------------------- */\n/* get_diagnostics — single-call server status snapshot (#286) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Represents a single redacted server-side error entry in the diagnostics\n * snapshot. PII / secrets are scrubbed before this is returned.\n */\nexport interface DiagnosticsError {\n /** ISO timestamp when the error was recorded. */\n timestamp: string;\n /** Error message with PII/secrets redacted (e.g. `at=<redacted>`). */\n message: string;\n /** Optional error category for quick triage. */\n category?: string;\n}\n\n/**\n * Tunnel state in the diagnostics snapshot. Same shape as `TunnelStatus` but\n * extended with the lock-file data (pid, startedAt) when available.\n */\nexport interface DiagnosticsTunnelInfo {\n /** Whether the cloudflared quick tunnel is currently up. */\n up: boolean;\n /** Public `wss://*.trycloudflare.com` relay URL, or `null`. */\n wssUrl: string | null;\n /**\n * PID of the MCP server process that owns the tunnel (from the lock file),\n * or `null` when no lock is present.\n */\n pid: number | null;\n /**\n * ISO timestamp when the owning server process started (from the lock file),\n * or `null`.\n */\n startedAt: string | null;\n}\n\n/**\n * Server-lock holder info from `~/.ait-devtools/server.lock`. `null` when\n * no lock file exists (server was cleanly shut down or never started).\n */\nexport interface DiagnosticsLockHolder {\n pid: number;\n startedAt: string;\n /** wssUrl recorded in the lock file — may be `null` when tunnel is still starting. */\n wssUrl: string | null;\n}\n\n/**\n * The next recommended tool for the agent to call, based on the current server\n * state snapshot. `null` means the session looks healthy — no specific action needed.\n */\nexport interface NextRecommendedAction {\n /** MCP tool name to call next (e.g. `'build_attach_url'`, `'restart'`). */\n tool: string;\n /** Human-readable reason explaining why this action is recommended. */\n reason: string;\n}\n\n/**\n * Full server status snapshot returned by `get_diagnostics`.\n *\n * All fields are nullable — a missing value means \"not yet known\" (e.g. tunnel\n * not up yet) rather than an error. The schema is intentionally stable across\n * versions: new optional fields may be added but existing fields are not\n * removed or renamed.\n *\n * SECRET-HANDLING: No TOTP secret, cookie, deploy key, or `at=` code value\n * appears in this snapshot. `recentErrors` entries are redacted before inclusion.\n */\nexport interface DiagnosticsResult {\n /** `@modelcontextprotocol/sdk` package version string. */\n mcpVersion: string | null;\n /** `@ait-co/devtools` package version string. */\n devtoolsVersion: string | null;\n /** Tunnel state including lock-file pid/startedAt. */\n tunnel: DiagnosticsTunnelInfo;\n /** Current list_pages result (pages + crash info + singleAttachModel). */\n pages: ListPagesResult | null;\n /** ISO timestamp of the most recent page attach, or `null`. */\n lastAttachAt: string | null;\n /** ISO timestamp of the most recent page detach, or `null`. */\n lastDetachAt: string | null;\n /**\n * Recent server-side errors (up to `recent_errors_limit`, default 10).\n * Redacted: `at=<redacted>`, cookie headers stripped, AITCC_API_KEY masked.\n */\n recentErrors: DiagnosticsError[];\n /**\n * Resolved environment and the reason string.\n *\n * `kind` — the precise three-value environment (`mock` | `relay-dev` |\n * `relay-live`). Use this for new code.\n * `env` — backward-compat two-value alias (`mock` | `relay`). Kept so\n * existing callers that only distinguish mock vs relay continue to work.\n */\n environment: {\n kind: McpEnvironment;\n /** @deprecated Use `kind` instead. Kept for backward compatibility. */\n env: 'mock' | 'relay';\n reason: string;\n /** `true` when the LIVE side-effect guard is active (`kind === 'relay-live'`). */\n liveGuardActive: boolean;\n };\n /**\n * Contents of `~/.ait-devtools/server.lock`, or `null` when absent.\n * Useful for diagnosing stale-lock conflicts without running the full server.\n */\n serverLockHolder: DiagnosticsLockHolder | null;\n /**\n * Single next recommended action for the agent, or `null` when the session\n * looks healthy. Derived deterministically from the other snapshot fields —\n * the agent should call this tool next rather than inferring from raw fields.\n *\n * Branch rules (evaluated in priority order):\n * 1. tunnel.up === false → restart\n * 2. tunnel.up, pages empty, env === relay → build_attach_url\n * 3. pages[0] exists + crashDetectedAt non-null → build_attach_url (re-attach)\n * 4. otherwise → null\n */\n nextRecommendedAction: NextRecommendedAction | null;\n}\n\n/**\n * Registry of server-side errors collected by `DiagnosticsCollector`.\n * Injected into `createDebugServer` so it is testable without a real process.\n */\nexport interface DiagnosticsCollector {\n /** Records a server-side error for later surfacing in `get_diagnostics`. */\n recordError(message: string, category?: string): void;\n /** Returns the most recent `limit` errors, oldest-first. */\n getRecentErrors(limit: number): DiagnosticsError[];\n /** Records an attach event (ISO timestamp stored). */\n recordAttach(): void;\n /** Records a detach event (ISO timestamp stored). */\n recordDetach(): void;\n /** Returns the ISO timestamp of the last attach, or `null`. */\n getLastAttachAt(): string | null;\n /** Returns the ISO timestamp of the last detach, or `null`. */\n getLastDetachAt(): string | null;\n}\n\n/** Secret-redaction patterns applied before error messages enter the buffer. */\nconst SECRET_REDACT_PATTERNS: ReadonlyArray<[RegExp, string]> = [\n // TOTP at= code value.\n [/\\bat=([^&\\s\"']+)/g, 'at=<redacted>'],\n // Cookie / Set-Cookie header values — replace everything after the colon.\n [/((?:set-)?cookie)\\s*:\\s*.+/gi, '$1: <redacted>'],\n // AITCC_API_KEY env-var-style references.\n [/AITCC_API_KEY\\s*=\\s*\\S+/gi, 'AITCC_API_KEY=<redacted>'],\n // Authorization header (covers \"Authorization: Bearer …\" and bare \"Bearer <token>\").\n [/Authorization\\s*:\\s*.+/gi, 'Authorization: <redacted>'],\n [/\\bBearer\\s+\\S+/g, 'Bearer <redacted>'],\n];\n\n/**\n * Applies all secret-redaction patterns to an error message string.\n * Used before storing errors in the `DiagnosticsCollector` ring buffer.\n *\n * SECRET-HANDLING: this is the single bottleneck for redaction — all error\n * strings must pass through here before reaching the buffer.\n */\nexport function redactErrorMessage(message: string): string {\n let result = message;\n for (const [pattern, replacement] of SECRET_REDACT_PATTERNS) {\n result = result.replace(pattern, replacement);\n }\n return result;\n}\n\n/** Default max buffer size for the error ring buffer. */\nconst DEFAULT_ERROR_BUFFER_SIZE = 50;\n\n/**\n * In-memory implementation of `DiagnosticsCollector`. Thread-safe in the\n * single-threaded Node.js sense (synchronous mutations only).\n */\nexport class InMemoryDiagnosticsCollector implements DiagnosticsCollector {\n private readonly buffer: DiagnosticsError[] = [];\n private readonly maxSize: number;\n private lastAttachAt: string | null = null;\n private lastDetachAt: string | null = null;\n\n constructor(maxSize = DEFAULT_ERROR_BUFFER_SIZE) {\n this.maxSize = maxSize;\n }\n\n recordError(message: string, category?: string): void {\n const entry: DiagnosticsError = {\n timestamp: new Date().toISOString(),\n message: redactErrorMessage(message),\n ...(category !== undefined ? { category } : {}),\n };\n this.buffer.push(entry);\n // Keep only the most recent `maxSize` entries.\n if (this.buffer.length > this.maxSize) {\n this.buffer.shift();\n }\n }\n\n getRecentErrors(limit: number): DiagnosticsError[] {\n const cap = Math.min(Math.max(1, limit), DEFAULT_ERROR_BUFFER_SIZE);\n const sliced =\n this.buffer.length > cap ? this.buffer.slice(this.buffer.length - cap) : [...this.buffer];\n return sliced;\n }\n\n recordAttach(): void {\n this.lastAttachAt = new Date().toISOString();\n }\n\n recordDetach(): void {\n this.lastDetachAt = new Date().toISOString();\n }\n\n getLastAttachAt(): string | null {\n return this.lastAttachAt;\n }\n\n getLastDetachAt(): string | null {\n return this.lastDetachAt;\n }\n}\n\n/**\n * Reads the `@modelcontextprotocol/sdk` package version from the installed\n * package's `package.json`. Returns `null` on any error (missing file, JSON\n * parse failure, etc.) — diagnostics must never throw.\n *\n * Node-only — uses dynamic `import()` so it does not pollute the browser\n * module graph.\n */\nexport async function readMcpSdkVersion(): Promise<string | null> {\n try {\n // Resolve the package.json adjacent to the installed SDK entry point.\n const { createRequire } = await import('node:module');\n const req = createRequire(import.meta.url);\n const pkgPath = req.resolve('@modelcontextprotocol/sdk/package.json');\n const { readFileSync } = await import('node:fs');\n const raw = readFileSync(pkgPath, 'utf8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n return typeof parsed.version === 'string' ? parsed.version : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Returns the `@ait-co/devtools` package version injected at build time via\n * the `__VERSION__` define. Returns `null` when the global is absent (e.g. in\n * some test environments that skip the build step).\n */\nexport function readDevtoolsVersion(): string | null {\n try {\n // `__VERSION__` is injected by tsdown / vite via `define`.\n // biome-ignore lint/suspicious/noExplicitAny: intentional global check\n const v = (globalThis as any).__VERSION__;\n return typeof v === 'string' && v.length > 0 ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Derives the next recommended action from a completed diagnostics snapshot.\n *\n * Branch rules (evaluated in priority order):\n * 1. tunnel.up === false → restart\n * 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)\n * 3. pages has entry + crashDetectedAt non-null → build_attach_url (re-attach after crash)\n * 4. otherwise → null (session looks healthy)\n *\n * Pure — does not throw; receives the final assembled snapshot fields.\n */\nexport function computeNextRecommendedAction(\n tunnel: DiagnosticsTunnelInfo,\n pages: ListPagesResult | null,\n env: McpEnvironment,\n): NextRecommendedAction | null {\n // Rule 1: tunnel is down — must restart the MCP server.\n if (!tunnel.up) {\n return {\n tool: 'restart',\n reason: 'tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart',\n };\n }\n\n // Rule 2: tunnel up but no pages attached in relay env → start attach.\n if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) {\n return {\n tool: 'build_attach_url',\n reason: 'tunnel ready, no pages attached — call build_attach_url to generate the attach QR',\n };\n }\n\n // Rule 3: crash detected — need to re-attach.\n if (pages !== null && pages.crashDetectedAt !== null) {\n return {\n tool: 'build_attach_url',\n reason: `page crashed at ${pages.crashDetectedAt} — call build_attach_url to re-attach`,\n };\n }\n\n // Rule 4: session looks healthy.\n return null;\n}\n\n/** Input for `getDiagnostics`. */\nexport interface GetDiagnosticsInput {\n /** Current tunnel status (from the server's live `getTunnelStatus()`). */\n tunnel: TunnelStatus;\n /**\n * CDP connection used to call `list_pages` — may be absent in edge cases\n * (e.g. called from the dev-mode server which has no CDP connection).\n */\n connection?: CdpConnection;\n /**\n * Resolved MCP environment (`mock` | `relay-dev` | `relay-live`). Caller\n * obtains via `resolveEnvironment()`.\n */\n env: McpEnvironment;\n /** Human-readable reason for the env decision. */\n envReason: string;\n /** Diagnostics collector for errors / attach events. */\n collector: DiagnosticsCollector;\n /** Lock-file reader — injected so tests can override without touching the FS. */\n readLock: () => import('./server-lock.js').LockData | null;\n /** Maximum number of recent errors to include (default 10). */\n recentErrorsLimit?: number;\n /** Optional async resolver for the MCP SDK version. */\n getMcpVersion?: () => Promise<string | null>;\n}\n\n/**\n * Builds the `get_diagnostics` response. Pure — does not throw; missing data\n * fields are `null`. Async because `readMcpSdkVersion` needs `import()`.\n *\n * SECRET-HANDLING:\n * - `recentErrors` messages are already redacted by `recordError` (via\n * `redactErrorMessage`). No additional redaction needed here.\n * - `tunnel.wssUrl` is a public cloudflared hostname — not a secret.\n * - Lock file data contains only pid + startedAt + wssUrl — no secrets.\n */\nexport async function getDiagnostics(input: GetDiagnosticsInput): Promise<DiagnosticsResult> {\n const {\n tunnel,\n connection,\n env,\n envReason,\n collector,\n readLock: readLockFn,\n recentErrorsLimit = 10,\n getMcpVersion = readMcpSdkVersion,\n } = input;\n\n const [mcpVersion, devtoolsVersion] = await Promise.all([\n getMcpVersion(),\n Promise.resolve(readDevtoolsVersion()),\n ]);\n\n // Read lock file for serverLockHolder + tunnel pid/startedAt.\n const lockData = readLockFn();\n const serverLockHolder: DiagnosticsLockHolder | null = lockData\n ? { pid: lockData.pid, startedAt: lockData.startedAt, wssUrl: lockData.wssUrl }\n : null;\n\n const tunnelInfo: DiagnosticsTunnelInfo = {\n up: tunnel.up,\n wssUrl: tunnel.wssUrl,\n pid: lockData?.pid ?? null,\n startedAt: lockData?.startedAt ?? null,\n };\n\n // list_pages — non-fatal; null on any error.\n let pages: ListPagesResult | null = null;\n if (connection !== undefined) {\n try {\n pages = listPages(connection, tunnel);\n } catch {\n // Ignore — pages stays null.\n }\n }\n\n const limit = Math.min(Math.max(1, recentErrorsLimit), 50);\n const recentErrors = collector.getRecentErrors(limit);\n\n const nextRecommendedAction = computeNextRecommendedAction(tunnelInfo, pages, env);\n\n return {\n mcpVersion,\n devtoolsVersion,\n tunnel: tunnelInfo,\n pages,\n lastAttachAt: collector.getLastAttachAt(),\n lastDetachAt: collector.getLastDetachAt(),\n recentErrors,\n environment: {\n kind: env,\n env: toLegacyEnv(env),\n reason: envReason,\n liveGuardActive: isLiveRelayEnv(env),\n },\n serverLockHolder,\n nextRecommendedAction,\n };\n}\n","/**\n * @ait-co/devtools dev-mode MCP server (stdio).\n *\n * Exposes the live browser mock state from a running Vite dev server to AI\n * coding agents via the Model Context Protocol (MCP).\n *\n * Architecture:\n * Browser (aitState) → Vite dev server endpoint (/api/ait-devtools/state)\n * ← HTTP GET ← this stdio MCP server ← AI agent\n *\n * The Vite endpoint is registered by the unplugin when `mcp: true` is set in\n * the plugin options (see `src/unplugin/index.ts`).\n *\n * Phase 3 tool-surface alignment: dev mode and debug mode now expose the same\n * `AIT.*` tools (`AIT.getMockState`, `AIT.getOperationalEnvironment`,\n * `AIT.getSdkCallHistory`). In dev mode they are backed by the HTTP mock-state\n * endpoint (see `HttpAitSource`); in debug mode by the Chii channel. So an AI\n * sees a coherent tool whether attached to a phone (debug) or a dev browser\n * (dev). `devtools_get_mock_state` (the original devtools#130 name) is kept as a\n * backward-compatible alias of `AIT.getMockState`.\n *\n * Issue #305 (M2-1) — dev/debug tool-surface unification:\n * dev-mode now also exposes `list_pages`, `get_diagnostics`, `measure_safe_area`,\n * and `call_sdk` so the docs/qa/scenarios.md acceptance sequence\n * `list_pages → measure_safe_area → call_sdk` works in dev mode without\n * \"Unknown tool\" failures.\n *\n * - `list_pages` — shim: returns the Vite dev URL as a single-entry array.\n * - `get_diagnostics` — dumps dev-mode server state (endpoint URL, last fetch\n * error, reachability, mode/environment metadata).\n * - `measure_safe_area`— reads safeAreaInsets from the mock state snapshot\n * (source: 'mock-vite').\n * - `call_sdk` — reads mock state and builds a mock-equivalent result\n * using window.__ait.state for supported methods; returns\n * an explicit tier-filter error for methods that require\n * a live CDP bridge.\n * - CDP-only tools (`evaluate`, `take_screenshot`, `get_dom_document`,\n * `take_snapshot`, `list_console_messages`,\n * `list_network_requests`, `list_exceptions`) — return an\n * explicit tier-filter error explaining that CDP is unavailable\n * in dev-mode and pointing to `--mode=local` or `--mode=debug`.\n *\n * This module is reached via the `devtools-mcp --mode=dev` CLI entry (see\n * `cli.ts`); the default (no flag) bin mode is the debug-mode CDP/Chii server.\n *\n * Usage (in your MCP client config, e.g. Claude Desktop):\n * {\n * \"mcpServers\": {\n * \"ait-devtools\": {\n * \"command\": \"pnpm\",\n * \"args\": [\"exec\", \"devtools-mcp\", \"--mode=dev\"],\n * \"env\": { \"AIT_DEVTOOLS_URL\": \"http://localhost:5173\" }\n * }\n * }\n * }\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { HttpAitSource } from './ait-http-source.js';\nimport type { AitSource } from './ait-source.js';\nimport { mcpError } from './errors.js';\nimport {\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n isAitToolName,\n type ToolAvailability,\n} from './tools.js';\n\n/** Error message prefix for CDP-dependent tools called in dev-mode. */\nconst CDP_UNAVAILABLE_IN_DEV_MODE =\n 'dev-mode에서는 CDP 연결이 없어 이 도구를 사용할 수 없습니다. ' +\n '실기기 또는 로컬 Chromium에 붙이려면 `devtools-mcp --mode=local` 또는 ' +\n '`devtools-mcp` (debug 모드 기본)로 전환하세요.';\n\n/**\n * Tool descriptors served by the dev-mode server.\n *\n * All dev-mode tools are Tier C (both envs) per RFC #277 — the dev-mode server\n * itself is the mock-side embodiment of those Tier C tools. `availableIn` is\n * declared so the surface stays consistent with the debug-mode registry.\n *\n * Issue #305: CDP-only tools are also listed with explicit descriptions so\n * agents do not get \"Unknown tool\" failures — they get a clear tier-filter\n * error message instead.\n */\nconst DEV_TOOL_DEFINITIONS = [\n /* ------------------------------------------------------------------ */\n /* AIT.* tools — HTTP mock-state backed */\n /* ------------------------------------------------------------------ */\n {\n name: 'AIT.getMockState',\n description:\n 'Returns the devtools mock state snapshot (window.__ait) from the running browser session — ' +\n 'environment, permissions, location, auth, network, IAP, and more. Read-only. ' +\n 'Requires the Vite dev server running with the @ait-co/devtools unplugin option `mcp: true`. ' +\n 'Same tool as in debug mode, where the in-app side reports it over the AIT domain.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getOperationalEnvironment',\n description:\n 'Returns the operational environment + SDK/app version derived from the dev mock state. ' +\n 'Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getSdkCallHistory',\n description:\n 'Returns the SDK call trace. In dev mode the HTTP mock-state endpoint records no trace, so ' +\n 'this returns an empty list; in debug mode it is populated over the AIT domain. Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'devtools_get_mock_state',\n description:\n 'Backward-compatible alias of AIT.getMockState (the original devtools#130 name). Returns the ' +\n 'current AIT DevTools mock state snapshot. Read-only. Prefer AIT.getMockState in new configs.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* Unified surface — dev-mode shims (issue #305) */\n /* ------------------------------------------------------------------ */\n {\n name: 'list_pages',\n description:\n 'dev-mode: returns the Vite dev server URL as a single-entry page list. ' +\n 'No CDP relay is involved — `tunnel.up` is always false and `devMode: true` marks ' +\n 'this as a shim result. Call this first to confirm the dev server is reachable. ' +\n 'In debug mode (`devtools-mcp` / `--mode=local`) this returns real attached pages.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_diagnostics',\n description:\n 'dev-mode: returns server diagnostics — Vite endpoint URL, last fetch timestamp/error, ' +\n 'mock state endpoint reachability, mode (\"dev\"), and environment metadata. ' +\n 'Call this when the dev server connection is suspect. ' +\n 'In debug mode this returns tunnel/relay/attach status instead.',\n inputSchema: {\n type: 'object',\n properties: {\n recent_errors_limit: {\n type: 'number',\n description: 'Ignored in dev-mode (no error ring buffer). Present for schema parity.',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'measure_safe_area',\n description:\n 'dev-mode: reads safe-area insets from the mock state snapshot via the Vite endpoint. ' +\n 'Returns `{ source: \"mock-vite\", sdkInsets, sdkInsetsSource: \"window.__ait\", ... }`. ' +\n 'Values reflect what the DevTools panel reports at the time of the last state push. ' +\n 'In debug mode this runs a Runtime.evaluate CDP probe on the attached page.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'call_sdk',\n description:\n 'dev-mode: calls a mock SDK method via the Vite mock state endpoint. ' +\n 'Supported methods read from window.__ait mock state (e.g. getOperationalEnvironment). ' +\n 'Returns the same `{ok, value}` / `{ok, error}` envelope as debug mode. ' +\n 'In debug mode this calls the real SDK via window.__sdkCall over CDP.',\n inputSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'Mock SDK method name to call (e.g. \"getOperationalEnvironment\").',\n },\n args: {\n type: 'array',\n description: 'Arguments (ignored in dev-mode mock path; present for schema parity).',\n items: {},\n },\n },\n required: ['name'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* CDP-only tools — tier-filter stubs so agents see a clear error */\n /* instead of \"Unknown tool\" (issue #305) */\n /* ------------------------------------------------------------------ */\n {\n name: 'evaluate',\n description:\n 'Evaluates an arbitrary JavaScript expression via CDP Runtime.evaluate. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug` for CDP access.',\n inputSchema: {\n type: 'object',\n properties: {\n expression: { type: 'string', description: 'JavaScript expression to evaluate.' },\n },\n required: ['expression'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_screenshot',\n description:\n 'Captures a PNG screenshot via CDP Page.captureScreenshot. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_dom_document',\n description:\n 'Returns the DOM tree via CDP DOM.getDocument. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_snapshot',\n description:\n 'Captures a serialized page snapshot via CDP DOMSnapshot.captureSnapshot. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_console_messages',\n description:\n 'Lists console messages captured via CDP Runtime.consoleAPICalled. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_network_requests',\n description:\n 'Lists network requests captured via CDP Network events. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_exceptions',\n description:\n 'Lists JS exceptions captured via CDP Runtime.exceptionThrown. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: {\n type: 'object',\n properties: {\n limit: { type: 'number', description: 'Maximum exceptions to return.' },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n] as const;\n\n/** All tool names served in dev-mode (including tier-filter stubs). */\nconst DEV_TOOL_NAMES = new Set<string>(DEV_TOOL_DEFINITIONS.map((t) => t.name));\n\n/** CDP-only tools — return a tier-filter error in dev-mode. */\nconst CDP_ONLY_TOOL_NAMES = new Set<string>([\n 'evaluate',\n 'take_screenshot',\n 'get_dom_document',\n 'take_snapshot',\n 'list_console_messages',\n 'list_network_requests',\n 'list_exceptions',\n]);\n\nexport interface CreateDevServerDeps {\n /** AIT source for the dev tools. Defaults to an HTTP source over the dev server. */\n aitSource?: AitSource;\n}\n\n/**\n * Builds the `list_pages` dev-mode shim response.\n * Returns the Vite dev URL as a single-entry page list with `devMode: true`.\n */\nfunction buildDevListPagesResult(devtoolsUrl: string) {\n return {\n pages: [\n {\n url: devtoolsUrl,\n title: 'dev fixture',\n attached: true,\n },\n ],\n tunnel: { up: false },\n devMode: true,\n singleAttachModel: true,\n };\n}\n\n/**\n * Builds the `get_diagnostics` dev-mode response.\n * Probes the mock state endpoint reachability and returns server metadata.\n */\nasync function buildDevDiagnostics(\n devtoolsUrl: string,\n stateEndpoint: string,\n fetchImpl: (url: string) => Promise<Response>,\n): Promise<Record<string, unknown>> {\n let reachable = false;\n let lastFetchError: string | null = null;\n let lastFetchAt: string | null = null;\n\n try {\n const res = await fetchImpl(stateEndpoint);\n reachable = res.ok;\n lastFetchAt = new Date().toISOString();\n if (!res.ok) {\n lastFetchError = `HTTP ${res.status} ${res.statusText}`;\n }\n } catch (err) {\n lastFetchError = err instanceof Error ? err.message : String(err);\n lastFetchAt = new Date().toISOString();\n }\n\n return {\n mode: 'dev',\n devtoolsUrl,\n mcpStateEndpoint: stateEndpoint,\n mockStateEndpointReachable: reachable,\n lastFetchAt,\n lastFetchError,\n environment: {\n kind: 'mock',\n reason: 'dev-mode — Vite HTTP endpoint, no CDP connection',\n },\n nextRecommendedAction: reachable\n ? null\n : 'mock state endpoint가 응답하지 않습니다. Vite dev 서버가 `mcp: true` 옵션으로 실행 중인지 확인하고, 필요하면 dev 서버를 재시작하세요.',\n };\n}\n\n/**\n * Builds the `measure_safe_area` dev-mode response from mock state.\n * Reads `safeAreaInsets` from the AIT mock state and returns a parity-schema\n * result with `source: 'mock-vite'`.\n */\nasync function buildDevMeasureSafeArea(aitSource: AitSource): Promise<Record<string, unknown>> {\n const state = await aitSource.get('AIT.getMockState');\n const raw = state as Record<string, unknown>;\n\n // Extract safeAreaInsets from the mock state.\n const rawInsets = raw.safeAreaInsets;\n let sdkInsets: { top: number; right: number; bottom: number; left: number } | null = null;\n if (rawInsets !== null && typeof rawInsets === 'object' && !Array.isArray(rawInsets)) {\n const r = rawInsets as Record<string, unknown>;\n sdkInsets = {\n top: typeof r.top === 'number' ? r.top : 0,\n right: typeof r.right === 'number' ? r.right : 0,\n bottom: typeof r.bottom === 'number' ? r.bottom : 0,\n left: typeof r.left === 'number' ? r.left : 0,\n };\n }\n\n return {\n source: 'mock-vite',\n // CSS env() vars are not available from the server side — report zeros.\n cssEnv: { top: 0, right: 0, bottom: 0, left: 0 },\n sdkInsets,\n sdkInsetsSource: sdkInsets !== null ? 'window.__ait' : null,\n ...(sdkInsets === null\n ? { sdkInsetsError: 'window.__ait.state.safeAreaInsets not found in mock state snapshot' }\n : {}),\n // Viewport geometry is not available from server side.\n innerWidth: null,\n innerHeight: null,\n devicePixelRatio: null,\n userAgent: null,\n navBarHeight: null,\n navBarHeightSource: 'not-available-in-dev-mode',\n };\n}\n\n/**\n * Builds the `call_sdk` dev-mode response.\n *\n * Supported methods are served from the mock state snapshot. Unsupported\n * methods return `{ ok: false, error: 'dev-mode-unsupported: ...' }` so the\n * agent gets an informative message rather than a generic failure.\n */\nasync function buildDevCallSdk(\n methodName: string,\n aitSource: AitSource,\n): Promise<Record<string, unknown>> {\n switch (methodName) {\n case 'getOperationalEnvironment': {\n const env = await aitSource.get('AIT.getOperationalEnvironment');\n return {\n ok: true,\n value: {\n environment: env.environment,\n sdkVersion: env.sdkVersion,\n },\n };\n }\n default: {\n // For methods not readable from mock state, return a structured error.\n return {\n ok: false,\n error:\n `dev-mode-unsupported: \"${methodName}\"은 dev-mode에서 직접 호출할 수 없습니다. ` +\n 'CDP bridge(window.__sdkCall)가 없으므로 실제 SDK 호출은 `--mode=local` 또는 ' +\n 'debug 모드에서만 가능합니다. ' +\n '지원 메서드: getOperationalEnvironment (mock state에서 읽음).',\n };\n }\n }\n}\n\n/** Builds the dev-mode MCP server (does not connect a transport). */\nexport function createDevServer(deps: CreateDevServerDeps = {}): Server {\n const devtoolsUrl = process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n const stateEndpoint = `${devtoolsUrl}/api/ait-devtools/state`;\n const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });\n\n const server = new Server(\n { name: 'ait-devtools', version: __VERSION__ },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, () => ({\n tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params.name;\n if (!DEV_TOOL_NAMES.has(name)) {\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n\n // CDP-only tools — tier-filter error with mode-switch hint.\n if (CDP_ONLY_TOOL_NAMES.has(name)) {\n return mcpError(`${name}: ${CDP_UNAVAILABLE_IN_DEV_MODE}`);\n }\n\n try {\n // `devtools_get_mock_state` is an alias of `AIT.getMockState`.\n const effective = name === 'devtools_get_mock_state' ? 'AIT.getMockState' : name;\n\n // AIT.* tools backed by HTTP mock-state endpoint.\n if (isAitToolName(effective)) {\n switch (effective) {\n case 'AIT.getMockState':\n return jsonResult(await getMockState(aitSource));\n case 'AIT.getOperationalEnvironment':\n return jsonResult(await getOperationalEnvironment(aitSource));\n case 'AIT.getSdkCallHistory':\n return jsonResult(await getSdkCallHistory(aitSource));\n default:\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n }\n\n // Unified-surface tools (issue #305 shims).\n switch (name) {\n case 'list_pages':\n return jsonResult(buildDevListPagesResult(devtoolsUrl));\n\n case 'get_diagnostics':\n return jsonResult(\n await buildDevDiagnostics(devtoolsUrl, stateEndpoint, (url) => fetch(url)),\n );\n\n case 'measure_safe_area':\n return jsonResult(await buildDevMeasureSafeArea(aitSource));\n\n case 'call_sdk': {\n const sdkName = request.params.arguments?.name;\n if (typeof sdkName !== 'string' || sdkName === '') {\n return mcpError(\n 'call_sdk: name 인자가 비어 있습니다. 호출할 메서드 이름을 전달하세요.',\n );\n }\n return jsonResult(await buildDevCallSdk(sdkName, aitSource));\n }\n\n default:\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return mcpError(\n `${name} 실패: ${message}\\n` +\n 'Vite dev 서버가 @ait-co/devtools unplugin `mcp: true` 옵션으로 실행 중인지 확인하세요. ' +\n 'AIT_DEVTOOLS_URL 환경변수가 올바르게 설정됐는지도 확인하세요.',\n );\n }\n });\n\n return server;\n}\n\nfunction jsonResult(value: unknown) {\n return { content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }] };\n}\n\n/** Builds the dev-mode server and connects it over stdio. */\nexport async function runDevServer(): Promise<void> {\n const server = createDevServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","names":["isObject"],"sources":["../../src/mcp/ait-http-source.ts","../../src/mcp/envelope.ts","../../src/mcp/errors.ts","../../src/mcp/sdk-signatures.ts","../../src/mcp/tools.ts","../../src/mcp/server.ts"],"sourcesContent":["/**\n * Dev-mode `AitSource` — backed by the Vite dev server's mock-state endpoint.\n *\n * The dev server already exposes the live browser mock state at\n * `GET /api/ait-devtools/state` (registered by the unplugin with `mcp: true`).\n * Phase 3 aligns dev mode and debug mode on the same `AIT.*` tool surface, so\n * dev mode serves those tools off this one HTTP source instead of a CDP channel:\n *\n * - `AIT.getMockState` → the full state snapshot (verbatim).\n * - `AIT.getOperationalEnvironment` → derived from the snapshot's\n * `environment` + `appVersion` fields.\n * - `AIT.getSdkCallHistory` → empty (the dev endpoint does not record\n * an SDK call trace — honest, not faked).\n *\n * An AI agent thus sees the same `AIT.getMockState` tool whether attached to a\n * phone (debug) or a dev browser (dev). Tests inject a fake `fetch`.\n */\n\nimport type {\n AitMethodMap,\n AitMethodName,\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\n\n/** Minimal `fetch` shape this source needs (injectable in tests). */\nexport type FetchLike = (url: string) => Promise<{\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n}>;\n\nexport interface HttpAitSourceOptions {\n /** Full URL of the mock-state endpoint, e.g. `http://localhost:5173/api/ait-devtools/state`. */\n stateEndpoint: string;\n /** Injected for tests; defaults to global `fetch`. */\n fetchImpl?: FetchLike;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nexport class HttpAitSource implements AitSource {\n private readonly stateEndpoint: string;\n private readonly fetchImpl: FetchLike;\n\n constructor(options: HttpAitSourceOptions) {\n this.stateEndpoint = options.stateEndpoint;\n this.fetchImpl = options.fetchImpl ?? ((url) => fetch(url));\n }\n\n private async fetchState(): Promise<AitMockState> {\n const res = await this.fetchImpl(this.stateEndpoint);\n if (!res.ok) {\n throw new Error(\n `Failed to fetch mock state from ${this.stateEndpoint}: HTTP ${res.status} ${res.statusText}. ` +\n 'Ensure the Vite dev server is running with the @ait-co/devtools unplugin option `mcp: true`.',\n );\n }\n const body = await res.json();\n return isObject(body) ? body : {};\n }\n\n async get<M extends AitMethodName>(method: M): Promise<AitMethodMap[M]> {\n switch (method) {\n case 'AIT.getMockState': {\n const state = await this.fetchState();\n return state as AitMethodMap[M];\n }\n case 'AIT.getOperationalEnvironment': {\n const state = await this.fetchState();\n const environment = typeof state.environment === 'string' ? state.environment : 'unknown';\n const sdkVersion = typeof state.appVersion === 'string' ? state.appVersion : null;\n const result: AitOperationalEnvironment = { environment, sdkVersion };\n return result as AitMethodMap[M];\n }\n case 'AIT.getSdkCallHistory': {\n // sdkCallLog slice is now part of the mock state pushed by the browser panel.\n // Read it from the state snapshot rather than returning an empty stub.\n const state = await this.fetchState();\n const raw = state.sdkCallLog;\n const calls = Array.isArray(raw) ? (raw as AitSdkCallHistory['calls']) : [];\n const result: AitSdkCallHistory = { calls };\n return result as AitMethodMap[M];\n }\n default:\n throw new Error(`Unknown AIT method: ${String(method)}`);\n }\n }\n}\n","/**\n * Unified response envelope for all MCP debug tools.\n *\n * Every tool result is wrapped in a `ToolEnvelope<T>` so agents can use a\n * single parser regardless of which tool they called. Before this, tool shapes\n * diverged: raw array returns, `{exceptions}`, `{value,type}`, `{ok,value|error}` …\n *\n * ## Schema\n *\n * ```ts\n * {\n * ok: boolean,\n * data?: T, // tool payload (absent when ok:false)\n * error?: { code, message, nextRecommendedAction? },\n * meta: {\n * tool: string,\n * env: 'mock' | 'relay-dev' | 'relay-live',\n * attached: boolean,\n * contentType: 'json' | 'image',\n * }\n * }\n * ```\n *\n * ## Compat mode\n *\n * Set `AIT_MCP_COMPAT=chrome-devtools` to bypass envelope wrapping and return\n * the raw payload. This restores 0.1.x behaviour for consumers that already\n * parse the old shapes (e.g. chrome-devtools-mcp integrations).\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n/** Allowed values for `meta.env`. */\nexport type EnvelopeEnv = 'mock' | 'relay-dev' | 'relay-live';\n\n/** The unified envelope returned by every debug MCP tool (when compat mode is off). */\nexport interface ToolEnvelope<T = unknown> {\n ok: boolean;\n data?: T;\n error?: {\n code: string;\n message: string;\n nextRecommendedAction?: {\n tool: string;\n reason: string;\n };\n };\n meta: {\n tool: string;\n env: EnvelopeEnv;\n attached: boolean;\n contentType: 'json' | 'image';\n };\n}\n\n/**\n * Returns `true` when `AIT_MCP_COMPAT=chrome-devtools` is set, which bypasses\n * envelope wrapping and returns raw payloads (0.1.x back-compat).\n */\nexport function isCompatMode(): boolean {\n return process.env.AIT_MCP_COMPAT === 'chrome-devtools';\n}\n\n/**\n * Maps `McpEnvironment` to `EnvelopeEnv`. After #307 these are the same\n * union (`mock | relay-dev | relay-live`), so this is identity — kept as a\n * named export for surface stability if envelope env diverges in the future.\n */\nexport function toEnvelopeEnv(env: McpEnvironment): EnvelopeEnv {\n return env;\n}\n\n/**\n * Context passed to `wrapEnvelope` that carries the per-request metadata.\n */\nexport interface EnvelopeContext {\n tool: string;\n env: McpEnvironment;\n attached: boolean;\n contentType?: 'json' | 'image';\n}\n\n/**\n * Wraps `data` in a `ToolEnvelope<T>` **unless** compat mode is active, in\n * which case `data` is returned as-is.\n *\n * Use this at every tool call-site in `debug-server.ts` and `server.ts`.\n *\n * @example\n * ```ts\n * return jsonResult(wrapEnvelope(listPages(connection, tunnel), {\n * tool: 'list_pages',\n * env: resolveEnvironment(),\n * attached: connection.listTargets().length > 0,\n * }));\n * ```\n */\nexport function wrapEnvelope<T>(data: T, ctx: EnvelopeContext): ToolEnvelope<T> | T {\n if (isCompatMode()) return data;\n return {\n ok: true,\n data,\n meta: {\n tool: ctx.tool,\n env: toEnvelopeEnv(ctx.env),\n attached: ctx.attached,\n contentType: ctx.contentType ?? 'json',\n },\n };\n}\n","/**\n * MCP tool 거부/에러 응답 메시지 헬퍼 — 4상태 차별화 + Tier 거부 통일.\n *\n * 모든 tool 거부/에러 응답을 \"원인 + 다음 행동\" 한국어 한 줄 포맷으로 일원화한다.\n * debug-server.ts · tools.ts의 거부 응답 호출부가 이 헬퍼를 통해 생성된다.\n *\n * 4가지 상태 (진단 메시지 차별화):\n * - tunnel-down : cloudflared 터널 미가동 — 서버 재시작 필요\n * - page-missing : 페이지가 attach 안 됨 — build_attach_url → QR 스캔\n * - page-crash : 페이지 crash 감지 — 앱 재실행 후 재attach\n * - sdk-absent : window.__sdkCall 미주입 — dogfood 채널로 재배포\n */\n\n/** MCP tool-result 에러 응답 형식. */\nexport interface McpErrorResult {\n content: Array<{ type: 'text'; text: string }>;\n isError: true;\n}\n\n/**\n * 한국어 한 줄 \"원인 + 다음 행동\" 포맷으로 에러 결과를 빌드한다.\n *\n * @param message - 사용자에게 보여줄 에러 본문 (원인 + 다음 행동 포함).\n */\nexport function mcpError(message: string): McpErrorResult {\n return {\n content: [{ type: 'text', text: message }],\n isError: true,\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Tier 거부 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Tier A/B 환경 불일치 거부 메시지.\n *\n * @param toolName - 거부된 tool 이름.\n * @param requiredEnv - 해당 tool이 요구하는 환경 ('mock' | 'relay').\n * @param currentEnv - 현재 세션 환경.\n * @param reason - 환경이 결정된 근거 (EnvironmentReason 문자열).\n */\nexport function tierRejectionError(\n toolName: string,\n requiredEnv: string,\n currentEnv: string,\n reason: string,\n): McpErrorResult {\n const envLabel = requiredEnv === 'relay' ? 'relay (실기기 연결)' : 'mock (로컬 브라우저)';\n const currentLabel = currentEnv === 'relay' ? 'relay' : 'mock';\n const hint =\n requiredEnv === 'relay'\n ? 'relay로 전환하려면 MCP_ENV=relay 설정 후 서버를 재시작하고 build_attach_url → QR 스캔으로 실기기를 attach하세요.'\n : 'mock으로 전환하려면 MCP_ENV=mock 설정 후 서버를 재시작하세요.';\n const text =\n `${toolName}은 ${envLabel} 환경에서만 사용할 수 있습니다. ` +\n `현재 환경: ${currentLabel} (${reason}). ${hint}`;\n // 하위 호환 — 기존 테스트가 기대하는 영문 패턴도 유지\n const compat = `tool ${toolName} is available only in ${requiredEnv}. Current environment is ${currentEnv} (${reason}).`;\n return mcpError(`${text}\\n\\n${compat}`);\n}\n\n/* -------------------------------------------------------------------------- */\n/* 4상태 차별화 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * 상태 1: tunnel 미가동 — cloudflared 터널이 아직 뜨지 않았다.\n *\n * `build_attach_url` 호출 시 tunnel.up === false 인 경우.\n */\nexport function tunnelDownError(): McpErrorResult {\n return mcpError(\n 'cloudflared 터널이 안 떠 있습니다. ' +\n 'MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.',\n );\n}\n\n/**\n * 상태 2: page 미attach — 터널은 살아 있으나 아직 페이지가 연결되지 않았다.\n *\n * enableDomains()가 \"No mini-app page attached\" 에러를 던질 때.\n */\nexport function pageMissingError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}페이지가 attach 안 됨. ` +\n 'dogfood 번들 배포 후 build_attach_url을 호출해 QR을 생성하세요: ' +\n '`ait deploy --scheme-only` → `build_attach_url(scheme_url)` → QR 스캔.',\n );\n}\n\n/**\n * 상태 3: page crash — 연결됐던 페이지가 crash/destroy됐다.\n *\n * chii-connection 이 'replaced-by-new-attach' / 'targetCrashed' / 'targetDestroyed' 를\n * 던질 때 이 메시지를 사용한다.\n */\nexport function pageCrashError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}페이지가 crash됐습니다. ` +\n '토스 앱을 재실행한 뒤 build_attach_url → QR 스캔으로 재attach하세요.',\n );\n}\n\n/**\n * 상태 4: SDK 부재 — window.__sdkCall이 주입되지 않았다 (dogfood 빌드가 아님).\n *\n * call_sdk 호출 시 브리지가 없을 때.\n */\nexport function sdkAbsentError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}window.__sdkCall이 주입되지 않았습니다 (dogfood 빌드가 아닙니다). ` +\n 'dogfood 채널(intoss-private)로 재배포 후 QR을 다시 스캔하세요: ' +\n '`ait build && aitcc app deploy`.',\n );\n}\n\n/* -------------------------------------------------------------------------- */\n/* LIVE side-effect guard 메시지 (relay-live env) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * relay-live 환경에서 side-effect 도구(`call_sdk`, `evaluate`)를 `confirm: true`\n * 없이 호출했을 때 반환하는 거부 메시지.\n *\n * 다음 행동을 두 가지로 제시한다:\n * 1. 같은 호출에 `confirm: true` 인자를 추가해 재시도.\n * 2. 읽기 전용 환경(relay-dev, mock)으로 전환.\n */\nexport function liveGuardError(toolName: string): McpErrorResult {\n const text =\n `[LIVE relay guard] ${toolName}은 현재 relay-live(실 출시 런타임) 세션에서 ` +\n 'side-effect 호출입니다. 실유저에게 영향을 줄 수 있어 명시적 동의가 필요합니다.\\n\\n' +\n '다음 중 하나를 선택하세요:\\n' +\n ` 1. \\`confirm: true\\` 인자를 추가해 재호출: ${toolName}(…, confirm: true)\\n` +\n ' 2. 읽기 전용 도구(list_pages, list_console_messages, take_screenshot 등)를 사용하세요.\\n' +\n ' 3. dogfood 빌드(relay-dev 환경)에서 먼저 검증 후 live에 적용하세요.\\n\\n' +\n 'live-guard: MCP_ENV=relay-live + confirm: true missing';\n return mcpError(text);\n}\n\n/* -------------------------------------------------------------------------- */\n/* relay 연결 끊김 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * relay WebSocket 연결이 끊겼을 때 — 크래시가 아닌 네트워크/프로세스 종료.\n */\nexport function relayDisconnectError(toolName?: string): McpErrorResult {\n const prefix = toolName ? `${toolName}: ` : '';\n return mcpError(\n `${prefix}relay 연결이 끊겼습니다. ` +\n 'list_pages로 상태를 확인하고, 필요하면 앱을 재실행 후 재attach하세요.',\n );\n}\n\n/* -------------------------------------------------------------------------- */\n/* 일반 tool 에러 메시지 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * CDP/AIT 명령 중 발생한 예외를 4상태로 분류해 적절한 에러 결과를 반환한다.\n *\n * - SDK 부재 패턴 (`window.__sdkCall is not available`) → sdkAbsentError\n * - crash 패턴 (`replaced-by-new-attach`, `targetCrashed`, `targetDestroyed`) → pageCrashError\n * - 연결 끊김 패턴 (`relay에 연결되어 있지 않습니다`, `relay WebSocket`) → relayDisconnectError\n * - 그 외 (일반 에러) → 원본 메시지를 포함한 mcpError\n */\nexport function classifyToolError(err: unknown, toolName: string): McpErrorResult {\n const message = err instanceof Error ? err.message : String(err);\n\n // 상태 1: tunnel 미가동 (buildAttachUrl이 던지는 패턴)\n if (message.startsWith('tunnel-down:') || message.includes('터널이 안 떠 있습니다')) {\n return tunnelDownError();\n }\n\n // 상태 4: SDK 부재\n if (\n message.startsWith('sdk-absent:') ||\n message.includes('__sdkCall이 주입되지 않았습니다') ||\n message.includes('window.__sdkCall is not available') ||\n (message.includes('__sdkCall') && message.includes('not available'))\n ) {\n return sdkAbsentError(toolName);\n }\n\n // 상태 3: page crash / target destroyed / replaced-by-new-attach\n if (\n message.includes('replaced-by-new-attach') ||\n message.includes('targetCrashed') ||\n message.includes('targetDestroyed') ||\n message.includes('detachedFromTarget')\n ) {\n return pageCrashError(toolName);\n }\n\n // relay 연결 끊김 (단순 disconnect — crash 아님)\n if (message.includes('relay에 연결되어 있지 않습니다') || message.includes('relay WebSocket')) {\n return relayDisconnectError(toolName);\n }\n\n // 그 외: 원본 메시지를 포함하되 list_pages 다음 행동 안내 추가\n return mcpError(\n `${toolName} 실패: ${message}\\nlist_pages로 미니앱이 relay에 attach됐는지 확인하세요.`,\n );\n}\n","/**\n * call_sdk 인자 시그니처 레지스트리\n *\n * 잘 알려진 SDK 메서드의 인자 schema를 수동으로 등록한다.\n * 목적: 잘못된 인자가 native bridge에 도달하기 전에 MCP 레이어에서 reject하여\n * 토스 앱 crash(Swift/Kotlin 측에서 `.type` 등을 undefined로 읽는 경우)를 예방.\n *\n * 등록되지 않은 메서드는 passthrough — 알 수 없는 메서드에 대해 stderr 경고 1회.\n *\n * 시그니처 출처:\n * - `src/__typecheck.ts` — Original SDK 타입 호환성 검증\n * - `src/mock/navigation/index.ts` — mock 구현의 함수 시그니처\n * - `src/mock/device/` — device mock 시그니처\n *\n * 새 메서드 추가 방법:\n * 1. `src/__typecheck.ts` 또는 mock 구현에서 시그니처 확인\n * 2. 아래 SIGNATURES 배열에 `SdkSignature` 항목 추가\n * 3. `src/__tests__/call-sdk-validation.test.ts`에 ok + bad 케이스 추가\n */\n\n/** 단일 메서드에 대한 인자 검증 결과 */\nexport type ValidationResult = { ok: true } | { ok: false; expected: string; received: string };\n\n/** 등록된 SDK 메서드 시그니처 */\nexport interface SdkSignature {\n /** SDK 메서드 이름 (예: \"setDeviceOrientation\") */\n name: string;\n /**\n * 인자 배열을 검증하는 함수.\n * `args[0]` 등 필요한 인자를 `unknown` 타입으로 받아 type guard로 검증.\n */\n validateArgs(args: unknown[]): ValidationResult;\n /**\n * 에러 메시지에 포함할 올바른 호출 예시.\n * 예: `call_sdk('setDeviceOrientation', [{ type: 'landscape' }])`\n */\n example: string;\n}\n\n/* -------------------------------------------------------------------------- */\n/* 헬퍼 — 공통 type guard */\n/* -------------------------------------------------------------------------- */\n\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction describeArgs(args: unknown[]): string {\n try {\n return JSON.stringify(args);\n } catch {\n return String(args);\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* 시그니처 레지스트리 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * 등록된 메서드 목록.\n *\n * 시그니처 출처 확인:\n * - 함수가 인자를 받지 않으면 args[0] 없음 → `args.length === 0`을 체크하지 않고\n * 그냥 통과시킨다(args 무시하는 stub가 많아서 noArgs 체크가 noise).\n * - 실 SDK 시그니처는 `src/__typecheck.ts`의 `Assert<Mock, Original>` 줄로 보장.\n */\nconst SIGNATURES: SdkSignature[] = [\n // --- setDeviceOrientation ---\n // 실 시그니처: setDeviceOrientation(options: { type: 'portrait' | 'landscape' }): Promise<void>\n // 출처: src/mock/navigation/index.ts:40 / src/__typecheck.ts:55\n {\n name: 'setDeviceOrientation',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg)) {\n return {\n ok: false,\n expected: \"{ type: 'portrait' | 'landscape' }\",\n received: describeArgs(args),\n };\n }\n const type = arg.type;\n if (type !== 'portrait' && type !== 'landscape') {\n return {\n ok: false,\n expected: \"{ type: 'portrait' | 'landscape' }\",\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setDeviceOrientation', [{ type: 'landscape' }])\",\n },\n\n // --- setIosSwipeGestureEnabled ---\n // 실 시그니처: setIosSwipeGestureEnabled(options: { isEnabled: boolean }): Promise<void>\n // 출처: src/mock/navigation/index.ts:32 / src/__typecheck.ts:51\n {\n name: 'setIosSwipeGestureEnabled',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.isEnabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ isEnabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setIosSwipeGestureEnabled', [{ isEnabled: false }])\",\n },\n\n // --- setSecureScreen ---\n // 실 시그니처: setSecureScreen(options: { enabled: boolean }): Promise<{ enabled: boolean }>\n // 출처: src/mock/navigation/index.ts:66 / src/__typecheck.ts:46\n {\n name: 'setSecureScreen',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.enabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ enabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setSecureScreen', [{ enabled: true }])\",\n },\n\n // --- setScreenAwakeMode ---\n // 실 시그니처: setScreenAwakeMode(options: { enabled: boolean }): Promise<{ enabled: boolean }>\n // 출처: src/mock/navigation/index.ts:57 / src/__typecheck.ts:47\n {\n name: 'setScreenAwakeMode',\n validateArgs(args) {\n const arg = args[0];\n if (!isObject(arg) || typeof arg.enabled !== 'boolean') {\n return {\n ok: false,\n expected: '{ enabled: boolean }',\n received: describeArgs(args),\n };\n }\n return { ok: true };\n },\n example: \"call_sdk('setScreenAwakeMode', [{ enabled: true }])\",\n },\n\n // --- getOperationalEnvironment ---\n // 실 시그니처: getOperationalEnvironment(): 'toss' | 'sandbox'\n // 인자 없음 — args는 무시 (SDK 자체가 인자를 무시함)\n // 출처: src/mock/navigation/index.ts:88 / src/__typecheck.ts:62\n {\n name: 'getOperationalEnvironment',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getOperationalEnvironment', [])\",\n },\n\n // --- getPlatformOS ---\n // 실 시그니처: getPlatformOS(): 'ios' | 'android'\n // 출처: src/mock/navigation/index.ts:84 / src/__typecheck.ts:61\n {\n name: 'getPlatformOS',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getPlatformOS', [])\",\n },\n\n // --- getDeviceId ---\n // 실 시그니처: getDeviceId(): string\n // 출처: src/mock/navigation/index.ts:119 / src/__typecheck.ts:74\n {\n name: 'getDeviceId',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getDeviceId', [])\",\n },\n\n // --- getLocale ---\n // 실 시그니처: getLocale(): string\n // 출처: src/mock/navigation/index.ts:115 / src/__typecheck.ts:72\n {\n name: 'getLocale',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getLocale', [])\",\n },\n\n // --- getNetworkStatus ---\n // 실 시그니처: getNetworkStatus(): Promise<NetworkStatus>\n // 출처: src/mock/navigation/index.ts:127 / src/__typecheck.ts:73\n {\n name: 'getNetworkStatus',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getNetworkStatus', [])\",\n },\n\n // --- getSchemeUri ---\n // 실 시그니처: getSchemeUri(): string\n // 출처: src/mock/navigation/index.ts:111 / src/__typecheck.ts:71\n {\n name: 'getSchemeUri',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('getSchemeUri', [])\",\n },\n\n // --- requestReview ---\n // 실 시그니처: requestReview(): Promise<void>\n // 출처: src/mock/navigation/index.ts:75 / src/__typecheck.ts:76\n {\n name: 'requestReview',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('requestReview', [])\",\n },\n\n // --- closeView ---\n // 실 시그니처: closeView(): Promise<void>\n // 출처: src/mock/navigation/index.ts:10 / src/__typecheck.ts:42\n {\n name: 'closeView',\n validateArgs(_args) {\n return { ok: true };\n },\n example: \"call_sdk('closeView', [])\",\n },\n];\n\n/* -------------------------------------------------------------------------- */\n/* 레지스트리 공개 API */\n/* -------------------------------------------------------------------------- */\n\nconst SIGNATURE_MAP = new Map<string, SdkSignature>(SIGNATURES.map((s) => [s.name, s]));\n\n/** 세션 내 passthrough 경고를 한 번만 emit하기 위한 Set */\nconst _warnedPassthrough = new Set<string>();\n\n/**\n * 메서드 이름으로 시그니처를 조회한다.\n * 등록된 메서드이면 `SdkSignature`를 반환하고, 미등록이면 `undefined`.\n */\nexport function lookupSignature(name: string): SdkSignature | undefined {\n return SIGNATURE_MAP.get(name);\n}\n\n/**\n * 미등록 메서드에 대해 stderr에 passthrough 경고를 1회 출력한다.\n * 세션 내 동일 메서드 이름은 최초 1회만 출력.\n */\nexport function warnPassthrough(name: string): void {\n if (_warnedPassthrough.has(name)) return;\n _warnedPassthrough.add(name);\n process.stderr.write(`[ait-debug] call_sdk: \"${name}\" 시그니처가 등록되지 않음 — passthrough\\n`);\n}\n\n/**\n * 테스트에서 passthrough 경고 Set을 초기화하기 위한 헬퍼.\n * 프로덕션 코드에서는 호출하지 않는다.\n */\nexport function _resetWarnedPassthroughForTest(): void {\n _warnedPassthrough.clear();\n}\n\n/**\n * 등록된 메서드 이름 목록 — tool description 생성 등에서 사용.\n */\nexport const REGISTERED_METHOD_NAMES: ReadonlyArray<string> = SIGNATURES.map((s) => s.name);\n","/**\n * Debug-mode MCP tools (Phase 1–3 + safe-area probe).\n *\n * Read-only tools that normalize CDP / AIT data into `chrome-devtools-mcp`-\n * compatible shapes. The tools never touch a websocket or HTTP endpoint\n * directly — they read from an injected `CdpConnection` (CDP events/commands)\n * or `AitSource` (AIT.* domain), which is what makes them unit-testable with a\n * fake. No phone and no running dev server are needed in tests.\n *\n * Phase 1 (CDP events):\n * - `list_console_messages` ← Runtime.consoleAPICalled\n * - `list_network_requests` ← Network.requestWillBeSent + responseReceived\n * - `list_pages` ← Chii relay target list + tunnel status\n * Phase 2 (CDP commands):\n * - `get_dom_document` ← DOM.getDocument\n * - `take_snapshot` ← DOMSnapshot.captureSnapshot\n * - `take_screenshot` ← Page.captureScreenshot\n * - `measure_safe_area` ← Runtime.evaluate (safe-area probe)\n * Phase 3 (AIT.* domain — CDP can't cover these):\n * - `AIT.getSdkCallHistory`\n * - `AIT.getMockState`\n * - `AIT.getOperationalEnvironment`\n */\n\nimport type {\n AitMockState,\n AitOperationalEnvironment,\n AitSdkCallHistory,\n AitSource,\n} from './ait-source.js';\nimport type {\n CdpCallFrame,\n CdpConnection,\n CdpRemoteObject,\n ConsoleApiCalledEvent,\n DomGetDocumentResult,\n DomSnapshotResult,\n NetworkRequestWillBeSentEvent,\n NetworkResponseReceivedEvent,\n RuntimeExceptionThrownEvent,\n} from './cdp-connection.js';\nimport { buildDeepLinkAttachUrl, validateSchemeAuthority } from './deeplink.js';\nimport type { McpEnvironment } from './environment.js';\nimport { isLiveRelayEnv, isRelayEnv, toLegacyEnv } from './environment.js';\nimport { lookupSignature, warnPassthrough } from './sdk-signatures.js';\nimport { generateTotp } from './totp.js';\n\n/** Tunnel state surfaced by `list_pages`. */\nexport interface TunnelStatus {\n /** Whether the cloudflared quick tunnel is up. */\n up: boolean;\n /** Public `wss://*.trycloudflare.com` relay URL the phone attaches to. */\n wssUrl: string | null;\n /**\n * ISO timestamp when a tunnel drop was first detected by the health probe.\n * `null` means the tunnel has not dropped (or has recovered since the last\n * drop). When non-null and `up` is false, the tunnel is down and the probe\n * has exhausted all reissue attempts — the server must be restarted.\n */\n droppedAt?: string | null;\n /**\n * Number of automatic reissue attempts made after a drop was detected.\n * Resets to 0 after a successful reissue. Reaches `MAX_REISSUE_ATTEMPTS`\n * (3) before the probe gives up and enters the permanent-error state.\n */\n reissueAttempts?: number;\n}\n\n/**\n * Tier classification per RFC #277 (\"MCP tool surface fidelity\"):\n *\n * - **Tier A** (`mock` only) — mock-internal state dials with no real-device\n * equivalent. Hidden when env is `relay`.\n * - **Tier B** (`relay` only) — relay infrastructure tools that have no mock\n * equivalent (e.g. `build_attach_url` needs a cloudflared tunnel URL). Hidden\n * when env is `mock`.\n * - **Tier C** (`both`) — fidelity-parallel tools that produce semantically\n * equivalent results across mock and relay. The agent sees the same tool with\n * the same shape; only the `source` provenance field (where applicable)\n * differs.\n */\nexport type ToolAvailability = 'mock' | 'relay' | 'both';\n\n/** Static MCP tool descriptors (name + JSONSchema) for the full debug tool surface. */\nexport const DEBUG_TOOL_DEFINITIONS = [\n {\n name: 'list_console_messages',\n description:\n 'Lists recent console messages (console.log/warn/error/info) captured from the attached ' +\n 'mini-app page over CDP (Runtime.consoleAPICalled). Read-only. Returns level, text, ' +\n 'timestamp, and stringified args, oldest-first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_network_requests',\n description:\n 'Lists recent network requests (XHR/fetch) captured from the attached mini-app page over ' +\n 'CDP (Network.requestWillBeSent + Network.responseReceived). Read-only. Returns url, ' +\n 'method, status, and timing, oldest-first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_pages',\n description:\n 'Returns the single active page (at most one) the relay sees attached. ' +\n 'When a second page attaches, the previous one is evicted (last-attach wins — ' +\n 'single-attach model). The result includes `singleAttachModel: true` so the agent ' +\n 'knows the array is always 0 or 1 entries. ' +\n 'Also returns whether the cloudflared tunnel is up and the public wss relay URL. ' +\n 'The `tunnel` field includes `droppedAt` (ISO timestamp or null/undefined): when non-null ' +\n 'the tunnel has permanently dropped after 3 failed reissue attempts — restart the debug ' +\n 'server with `npx @ait-co/devtools devtools-mcp`. ' +\n 'Each page entry includes a `lastSeenAt` ISO timestamp (last inbound CDP message from ' +\n 'that target — useful to detect stale entries when the phone app backgrounded). ' +\n 'The result also includes `crashDetectedAt` (ISO timestamp or null): when non-null, ' +\n 'a page crash was detected via Inspector.targetCrashed / Target.targetDestroyed since ' +\n 'the last attach, the pages list will be empty, and `crashWarning` shows a Korean hint ' +\n 'to re-attach. ' +\n 'Call this first to confirm a page is attached before reading console/network. ' +\n 'When a page attaches or detaches the server emits notifications/tools/list_changed — ' +\n 'call tools/list again to get the full updated tool surface.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'build_attach_url',\n description:\n \"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. \" +\n 'Turns an `ait deploy --scheme-only` URL (intoss-private://…?_deploymentId=<uuid>) into a ' +\n 'self-attaching deep link by splicing in debug=1 and the live relay URL for this session. ' +\n 'Returns the deep link JSON and a unicode QR of that deep link. Scan the QR with the phone ' +\n 'camera to open the mini-app and attach it to this debug session (QR is the single entry ' +\n 'path — no USB cable or platform CLI needed). Requires the tunnel to be up — call ' +\n 'list_pages first. If the tunnel is not up, restart the MCP server: ' +\n '`npx @ait-co/devtools devtools-mcp`. ' +\n 'Set wait_for_attach=true to block until the phone scans and a page attaches ' +\n '(polls listTargets up to 30 s by default), then returns the attached page info too. ' +\n 'On timeout, call build_attach_url again to resume polling. ' +\n 'When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default ' +\n 'browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). ' +\n 'Requires MCP_ENV=relay-dev or relay-live (set automatically in debug-mode default).\\n\\n' +\n 'TOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl ' +\n 'automatically includes the current one-time code (at=<code>) — the URL is single-use for ' +\n 'that 30-second step. The response includes a `totp` field with `expiresAt` (ISO timestamp). ' +\n 'If the phone scan happens after expiresAt, the relay will reject the code — just call ' +\n 'build_attach_url again to get a fresh one-time URL. ' +\n 'Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.',\n inputSchema: {\n type: 'object',\n properties: {\n scheme_url: {\n type: 'string',\n description:\n 'The intoss-private:// scheme URL from `ait deploy --scheme-only` (must carry _deploymentId). ' +\n 'The authority (host) must be the app name (e.g. intoss-private://aitc-sdk-example?_deploymentId=…). ' +\n 'Generic values like \"web\" or an empty host indicate a malformed URL.',\n },\n wait_for_attach: {\n type: 'boolean',\n description:\n 'If true, block after returning the QR until a page attaches to the relay (polls ' +\n 'listTargets ~1 s interval, timeout 30 s). On attach, the response includes the ' +\n 'attached page list. On timeout, call build_attach_url again to resume polling.',\n },\n open_in_browser: {\n type: 'boolean',\n description:\n 'If true (default), render the QR as a PNG and open it in the OS default browser. ' +\n 'Only works when the MCP server is running on a local GUI machine — headless or ' +\n 'remote container environments should set this to false to use the text QR fallback.',\n },\n },\n required: ['scheme_url'],\n },\n // Tier B per RFC #277 — the URL synthesis requires a live cloudflared\n // tunnel + relay, which only exists in the `relay` environment.\n availableIn: 'relay' as ToolAvailability,\n },\n {\n name: 'get_dom_document',\n description:\n 'Returns the DOM tree of the attached mini-app page over CDP (DOM.getDocument). Read-only. ' +\n 'Use for structural/layout regression diagnosis (e.g. confirming an element exists, ' +\n 'inspecting attributes). Returns the document root node with children.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_snapshot',\n description:\n 'Captures a serialized snapshot of the attached page over CDP (DOMSnapshot.captureSnapshot). ' +\n 'Read-only. Returns the documents + interned strings table for visual-regression diagnosis ' +\n '(e.g. checking computed CSS custom properties like --sat against the live layout).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_screenshot',\n description:\n 'Captures a PNG screenshot of the attached mini-app page over CDP (Page.captureScreenshot) ' +\n 'so the agent can see the phone screen directly. Read-only. ' +\n 'Returns an image content block — this is the only debug tool that returns an image; ' +\n 'all other debug tools return text (JSON).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'measure_safe_area',\n description:\n 'Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns ' +\n 'normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. ' +\n 'Read-only — does not modify page state. ' +\n 'Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel ' +\n 'page with window.__ait state) and `relay` (real-device WebView with window.__sdk). ' +\n 'The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-live\"` field so consumers can identify ' +\n 'provenance without inspecting payload values. ' +\n 'Use in a relay session (phone attached) to get ground-truth values for upgrading a ' +\n 'viewport preset from extrapolated/placeholder to measured. ' +\n 'Requires a page to be attached — call list_pages first.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'evaluate',\n description:\n 'Evaluates an arbitrary JavaScript expression on the attached mini-app page via ' +\n 'CDP Runtime.evaluate (returnByValue: true) and returns the result. ' +\n 'NOT read-only — the expression can have side effects (DOM mutations, SDK calls, ' +\n 'state changes). Requires the relay to be attached — call list_pages first. ' +\n 'Throws if the evaluation throws an exception on the page.\\n\\n' +\n 'SECURITY: expression and result are not redacted — never include secrets or auth ' +\n 'tokens in the expression.\\n\\n' +\n 'LIVE guard: when running against a live/production relay (relay-live env, ' +\n 'MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that ' +\n 'the expression may affect real users. Without it the call is rejected with a ' +\n 'structured error. mock and relay-dev sessions are unaffected.',\n inputSchema: {\n type: 'object',\n properties: {\n expression: {\n type: 'string',\n description: 'JavaScript expression to evaluate in the page context.',\n },\n confirm: {\n type: 'boolean',\n description:\n 'Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge ' +\n 'that this expression may have side effects on real/live users. ' +\n 'Omitting this in a relay-live session results in a structured rejection error. ' +\n 'Has no effect in mock or relay-dev sessions.',\n },\n },\n required: ['expression'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_exceptions',\n description:\n 'Lists JS-level exceptions captured via `Runtime.exceptionThrown` from the relay attached ' +\n 'page. Includes timestamp, exception text, source URL/line, and stack trace. ' +\n 'Use to root-cause SDK throws that may precede a Toss app crash (#265 / #267). ' +\n 'The buffer holds up to 50 most recent exceptions and survives target ' +\n 'replaced/crashed/destroyed events so an exception just before a crash is preserved. ' +\n 'Returns up to 50 most recent by default.',\n inputSchema: {\n type: 'object',\n properties: {\n limit: {\n type: 'number',\n description: 'Maximum number of exceptions to return (default 50, max 50).',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'call_sdk',\n description:\n 'Calls a dogfood SDK method via the window.__sdkCall bridge ' +\n '(exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). ' +\n 'NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). ' +\n 'On env 3/4 (real device relay) this hits the real SDK; on env 1 (local mock) it hits ' +\n 'the mock SDK. (env 2 PWA does not inject the SDK — call_sdk is not available there.) ' +\n 'Requires the relay to be attached — call list_pages first. ' +\n 'Returns {ok: true, value} on success or {ok: false, error} on failure. ' +\n 'If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], ' +\n 'the result also includes `recentException` for crash triage. ' +\n 'Returns a clear error if window.__sdkCall is not available (non-dogfood bundle) — ' +\n 'redeploy via dogfood channel: `ait build && aitcc app deploy`.\\n\\n' +\n 'SECURITY: method name, args, and result value are not redacted — never include secrets.\\n\\n' +\n 'LIVE guard: when running against a live/production relay (relay-live env, ' +\n 'MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that ' +\n 'the SDK call may affect real users. Without it the call is rejected with a ' +\n 'structured error. mock and relay-dev sessions are unaffected.\\n\\n' +\n 'IMPORTANT — 인자 시그니처 (잘못된 인자로 호출하면 토스 앱 crash 위험):\\n' +\n ' setDeviceOrientation: call_sdk(\"setDeviceOrientation\", [{ type: \"landscape\" }]) // NOT \"landscape\"\\n' +\n ' setIosSwipeGestureEnabled: call_sdk(\"setIosSwipeGestureEnabled\", [{ isEnabled: false }])\\n' +\n ' setSecureScreen: call_sdk(\"setSecureScreen\", [{ enabled: true }])\\n' +\n ' setScreenAwakeMode: call_sdk(\"setScreenAwakeMode\", [{ enabled: true }])\\n' +\n ' getOperationalEnvironment: call_sdk(\"getOperationalEnvironment\", [])\\n' +\n ' getPlatformOS: call_sdk(\"getPlatformOS\", [])\\n' +\n ' getDeviceId: call_sdk(\"getDeviceId\", [])\\n' +\n ' getLocale: call_sdk(\"getLocale\", [])\\n' +\n ' getNetworkStatus: call_sdk(\"getNetworkStatus\", [])\\n' +\n ' getSchemeUri: call_sdk(\"getSchemeUri\", [])\\n' +\n ' requestReview: call_sdk(\"requestReview\", [])\\n' +\n ' closeView: call_sdk(\"closeView\", [])',\n inputSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'SDK method name to call (e.g. \"getOperationalEnvironment\").',\n },\n args: {\n type: 'array',\n description: 'Arguments to pass to the SDK method (optional, default []).',\n items: {},\n },\n confirm: {\n type: 'boolean',\n description:\n 'Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge ' +\n 'that this SDK call may have side effects on real/live users. ' +\n 'Omitting this in a relay-live session results in a structured rejection error. ' +\n 'Has no effect in mock or relay-dev sessions.',\n },\n },\n required: ['name'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getSdkCallHistory',\n description:\n 'Returns the recent Apps In Toss SDK call trace (method, args, result/error, timestamp) that ' +\n 'raw CDP cannot observe. Read-only. Use to confirm an SDK call fired and how it resolved ' +\n '(e.g. a saveBase64Data permission regression).',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getMockState',\n description:\n 'Returns the devtools mock state snapshot (window.__ait) — environment, permissions, location, ' +\n 'auth, network, IAP, and more. Read-only. In dev mode this is the live browser mock state; in ' +\n 'debug mode the in-app side reports it over the AIT domain.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getOperationalEnvironment',\n description:\n 'Returns getOperationalEnvironment() plus the resolved SDK version — metadata raw CDP cannot ' +\n 'observe. Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_diagnostics',\n description:\n 'Returns a single-call server status snapshot so the agent can diagnose \"why is this not ' +\n 'working?\" without calling multiple tools. Fields: mcpVersion (MCP SDK version), ' +\n 'devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), ' +\n 'pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, ' +\n 'recentErrors (last N server-side errors, PII/secret redacted), ' +\n 'environment (kind: mock|relay-dev|relay-live, env: mock|relay backward-compat, reason, ' +\n 'liveGuardActive: true when relay-live LIVE guard is active), ' +\n 'serverLockHolder (pid + startedAt from the lock file, or null), ' +\n 'nextRecommendedAction ({tool, reason} or null — the single next tool to call; ' +\n 'in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). ' +\n 'All fields are nullable — missing data is null, not an error. ' +\n 'debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. ' +\n 'Tier C (both mock and relay). Call this first when debugging session state.',\n inputSchema: {\n type: 'object',\n properties: {\n recent_errors_limit: {\n type: 'number',\n description:\n 'Maximum number of recent server-side errors to include (default 10, max 50).',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n] as const;\n\nexport type DebugToolName = (typeof DEBUG_TOOL_DEFINITIONS)[number]['name'];\n\nconst DEBUG_TOOL_NAMES = new Set<string>(DEBUG_TOOL_DEFINITIONS.map((t) => t.name));\n\nexport function isDebugToolName(name: string): name is DebugToolName {\n return DEBUG_TOOL_NAMES.has(name);\n}\n\n/**\n * Returns the `ToolAvailability` declared on a registered debug tool, or\n * `undefined` when the name is not a known debug tool. Used by the tool\n * registry to filter `tools/list` by current env and by the call handler to\n * reject env-mismatch invocations.\n */\nexport function getToolAvailability(name: string): ToolAvailability | undefined {\n for (const t of DEBUG_TOOL_DEFINITIONS) {\n if (t.name === name) return t.availableIn;\n }\n return undefined;\n}\n\n/**\n * Returns true when the named tool is available in the given environment.\n * Unknown tools return `false` — callers should reject them as unknown rather\n * than as env-mismatched.\n *\n * Relay variants (`relay-dev`, `relay-live`) both satisfy the `'relay'`\n * availability tier — `isRelayEnv()` is used for the check.\n */\nexport function isToolAvailableIn(name: string, env: McpEnvironment): boolean {\n const availability = getToolAvailability(name);\n if (availability === undefined) return false;\n if (availability === 'both') return true;\n if (availability === 'relay') return isRelayEnv(env);\n return availability === env;\n}\n\n/**\n * Filters a `DEBUG_TOOL_DEFINITIONS`-shaped list to those whose `availableIn`\n * matches the given env. Pure — preserves order; both Tier C (\"both\") and the\n * matching single-env tier pass through.\n *\n * Relay variants (`relay-dev`, `relay-live`) both satisfy the `'relay'` tier.\n */\nexport function filterToolsByEnvironment<T extends { name: string; availableIn: ToolAvailability }>(\n tools: ReadonlyArray<T>,\n env: McpEnvironment,\n): T[] {\n return tools.filter(\n (t) =>\n t.availableIn === 'both' ||\n (t.availableIn === 'relay' && isRelayEnv(env)) ||\n t.availableIn === env,\n );\n}\n\n/**\n * Tool names that are available before any page attaches (bootstrap tier).\n *\n * `build_attach_url` — pure URL synthesis, no attach needed.\n * `list_pages` — reports tunnel status + empty pages even pre-attach.\n *\n * All other tools require an attached page (`enableDomains` must succeed) and\n * are only advertised in `tools/list` once a target appears.\n */\nexport const BOOTSTRAP_TOOL_NAMES: ReadonlySet<string> = new Set<string>([\n 'build_attach_url',\n 'get_diagnostics',\n 'list_pages',\n]);\n\n/** Normalized console message returned by `list_console_messages`. */\nexport interface ConsoleMessage {\n level: string;\n text: string;\n timestamp: number;\n args: string[];\n}\n\n/** Normalized network request returned by `list_network_requests`. */\nexport interface NetworkRequest {\n requestId: string;\n url: string;\n method: string;\n /** HTTP status once a response was seen, else null (still in-flight). */\n status: number | null;\n statusText: string | null;\n /** Request start (CDP timestamp). */\n startTime: number;\n /** Response received (CDP timestamp), else null. */\n endTime: number | null;\n}\n\n/** Renders a CDP `RemoteObject` console arg to a stable display string. */\nfunction renderRemoteObject(arg: CdpRemoteObject): string {\n if (arg.value !== undefined) {\n if (typeof arg.value === 'string') return arg.value;\n try {\n return JSON.stringify(arg.value);\n } catch {\n return String(arg.value);\n }\n }\n if (arg.description !== undefined) return arg.description;\n if (arg.className !== undefined) return arg.className;\n return arg.subtype ?? arg.type;\n}\n\nexport function normalizeConsoleMessage(event: ConsoleApiCalledEvent): ConsoleMessage {\n const args = event.args.map(renderRemoteObject);\n return {\n level: event.type,\n text: args.join(' '),\n timestamp: event.timestamp,\n args,\n };\n}\n\nexport function listConsoleMessages(connection: CdpConnection): ConsoleMessage[] {\n return connection\n .getBufferedEvents('Runtime.consoleAPICalled')\n .map((event) => normalizeConsoleMessage(event));\n}\n\nexport function listNetworkRequests(connection: CdpConnection): NetworkRequest[] {\n const requests = connection.getBufferedEvents('Network.requestWillBeSent');\n const responses = connection.getBufferedEvents('Network.responseReceived');\n\n const responseByRequestId = new Map<string, NetworkResponseReceivedEvent>();\n for (const response of responses) {\n responseByRequestId.set(response.requestId, response);\n }\n\n return requests.map((request: NetworkRequestWillBeSentEvent) => {\n const response = responseByRequestId.get(request.requestId);\n return {\n requestId: request.requestId,\n url: request.request.url,\n method: request.request.method,\n status: response ? response.response.status : null,\n statusText: response ? response.response.statusText : null,\n startTime: request.timestamp,\n endTime: response ? response.timestamp : null,\n };\n });\n}\n\n/* -------------------------------------------------------------------------- */\n/* list_exceptions — Runtime.exceptionThrown ring buffer */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Normalized exception returned by `list_exceptions`.\n *\n * Flattens the CDP `Runtime.ExceptionDetails` shape into the most useful\n * fields. The `raw` field carries the original event for callers that need\n * the full payload.\n */\nexport interface BufferedException {\n /** Wall-clock ms since epoch (CDP `Runtime.Timestamp`). */\n timestamp: number;\n /** Short summary text from `exceptionDetails.text`. */\n text: string;\n /** Source URL where the exception was thrown, if known. */\n url?: string;\n /** 0-based line number in the source file, if known. */\n lineNumber?: number;\n /** 0-based column number in the source file, if known. */\n columnNumber?: number;\n /** `description` of the thrown `RemoteObject` (e.g. \"TypeError: …\"). */\n exceptionText?: string;\n /**\n * Formatted stack trace: `at fn (url:line:col)` lines joined by `\\n`.\n * Omitted when no `stackTrace.callFrames` are available.\n */\n stack?: string;\n /** Full original `Runtime.exceptionThrown` event payload. */\n raw: RuntimeExceptionThrownEvent;\n}\n\n/** Formats a single CDP call frame into `at fn (url:line:col)`. */\nfunction formatCallFrame(frame: CdpCallFrame): string {\n const fn = frame.functionName || '(anonymous)';\n return `at ${fn} (${frame.url}:${frame.lineNumber}:${frame.columnNumber})`;\n}\n\n/** Normalizes a raw `Runtime.exceptionThrown` event into a `BufferedException`. */\nexport function normalizeException(event: RuntimeExceptionThrownEvent): BufferedException {\n const { timestamp, exceptionDetails } = event;\n const frames = exceptionDetails.stackTrace?.callFrames;\n const stack = frames && frames.length > 0 ? frames.map(formatCallFrame).join('\\n') : undefined;\n const exceptionText = exceptionDetails.exception?.description ?? undefined;\n\n const result: BufferedException = {\n timestamp,\n text: exceptionDetails.text,\n raw: event,\n };\n if (exceptionDetails.url !== undefined) result.url = exceptionDetails.url;\n if (exceptionDetails.lineNumber !== undefined) result.lineNumber = exceptionDetails.lineNumber;\n if (exceptionDetails.columnNumber !== undefined)\n result.columnNumber = exceptionDetails.columnNumber;\n if (exceptionText !== undefined) result.exceptionText = exceptionText;\n if (stack !== undefined) result.stack = stack;\n return result;\n}\n\n/**\n * Returns the most recent buffered `Runtime.exceptionThrown` events, normalized.\n * Oldest-first; limited to `limit` entries (default 50, max 50).\n */\nexport function listExceptions(connection: CdpConnection, limit = 50): BufferedException[] {\n const cap = Math.min(Math.max(1, limit), 50);\n const events = connection.getBufferedEvents('Runtime.exceptionThrown');\n // Slice from the tail to respect the cap while preserving oldest-first order.\n const sliced = events.length > cap ? events.slice(events.length - cap) : events;\n return sliced.map((e) => normalizeException(e));\n}\n\n/** A page entry in the `list_pages` result, extended with freshness info. */\nexport interface ListPagesEntry {\n id: string;\n title: string;\n url: string;\n /** ISO timestamp of the last inbound CDP message from this target, or null. */\n lastSeenAt: string | null;\n}\n\n/** Result of `list_pages`: attach status + tunnel state + crash info. */\nexport interface ListPagesResult {\n /**\n * The single active page, or an empty array when nothing is attached.\n * Under the single-attach model this is always 0 or 1 entries.\n */\n pages: ListPagesEntry[];\n tunnel: TunnelStatus;\n /**\n * ISO timestamp of the most recent crash / targetDestroyed / detachedFromTarget\n * event detected since the last `enableDomains()`, or `null` if none.\n * When non-null, all attached pages have been removed from the relay map and\n * a new `enableDomains()` call is required to resume debugging.\n */\n crashDetectedAt: string | null;\n /** Korean warning line shown in tool output when a crash was detected. */\n crashWarning: string | null;\n /**\n * Always `true` — signals to the agent that at most one page is ever present.\n * When a second page attaches, the previous one is evicted (last-attach wins).\n */\n singleAttachModel: true;\n}\n\n/**\n * Duck-type interface for the crash-detection extras exposed by `ChiiCdpConnection`.\n * The base `CdpConnection` interface is kept minimal (fake-friendly); the extras\n * are opt-in so tests without them continue to compile.\n */\ninterface CrashAwareCdpConnection extends CdpConnection {\n getLastCrashDetectedAt(): number | null;\n getTargetLastSeenAt(targetId: string): number | null;\n}\n\nfunction isCrashAware(conn: CdpConnection): conn is CrashAwareCdpConnection {\n return (\n typeof (conn as CrashAwareCdpConnection).getLastCrashDetectedAt === 'function' &&\n typeof (conn as CrashAwareCdpConnection).getTargetLastSeenAt === 'function'\n );\n}\n\nexport function listPages(connection: CdpConnection, tunnel: TunnelStatus): ListPagesResult {\n const rawTargets = connection.listTargets();\n const pages: ListPagesEntry[] = rawTargets.map((t) => {\n const lastSeenMs = isCrashAware(connection) ? connection.getTargetLastSeenAt(t.id) : null;\n return {\n id: t.id,\n title: t.title,\n url: t.url,\n lastSeenAt: lastSeenMs !== null ? new Date(lastSeenMs).toISOString() : null,\n };\n });\n\n const crashMs = isCrashAware(connection) ? connection.getLastCrashDetectedAt() : null;\n const crashDetectedAt = crashMs !== null ? new Date(crashMs).toISOString() : null;\n const crashWarning = crashDetectedAt\n ? `[ait-debug] page crash 감지됨 — 새 attach 필요 (관측 시각: ${crashDetectedAt})`\n : null;\n\n return { pages, tunnel, crashDetectedAt, crashWarning, singleAttachModel: true };\n}\n\n/** A `build_attach_url` result: the spliced deep link the phone should open. */\nexport interface BuildAttachUrlResult {\n /** The scheme URL with `debug=1&relay=<wss>[&at=<totp-code>]` spliced in. */\n attachUrl: string;\n /** The relay URL that was spliced in (this session's quick tunnel). */\n relayUrl: string;\n /**\n * Non-fatal warning about the scheme URL's authority being missing or\n * suspicious (e.g. \"web\", \"localhost\"). Callers should surface this to\n * help the user catch a malformed URL early.\n */\n authorityWarning?: string;\n /**\n * TOTP metadata — present when `AIT_DEBUG_TOTP_SECRET` is set.\n *\n * SECRET-HANDLING: the `at=` code value is spliced into `attachUrl` only.\n * It is never surfaced separately here to avoid inadvertent logging of the\n * one-time code outside of the URL.\n */\n totp?: {\n /** `true` when a TOTP code was spliced into `attachUrl`. */\n enabled: true;\n /** RFC 6238 step duration in seconds. */\n ttlSeconds: number;\n /** ISO timestamp when the current step expires. Rescan or call build_attach_url again after this. */\n expiresAt: string;\n };\n}\n\n/**\n * Builds a self-attaching dogfood deep link from an `ait deploy --scheme-only`\n * URL plus this session's live relay. Throws if the tunnel is not up yet (no\n * relay URL to splice in) — the caller surfaces that as a tool error.\n *\n * When `AIT_DEBUG_TOTP_SECRET` is set, generates the current TOTP code and\n * splices it as `at=<code>` into the attach URL. The code is valid for one\n * 30-second time step (±1 skew accepted by the relay, so the effective window\n * is up to 90 s). If the scan happens after `totp.expiresAt`, call\n * `build_attach_url` again to get a fresh code.\n *\n * Also validates the scheme URL's authority. A suspicious authority (empty,\n * \"web\", \"localhost\", etc.) is surfaced as a non-fatal `authorityWarning` on\n * the result so the caller can show a helpful hint without blocking the link\n * generation (the warning is consistent with how other validation in\n * `buildDeepLinkAttachUrl` works — hard errors for relay, soft warning for\n * the scheme authority which is in the caller's input, not ours to own).\n *\n * SECRET-HANDLING: `totpSecret` (if provided) is used only to compute a code\n * and must never appear in any log, error message, or output outside of the\n * spliced `at=` param in `attachUrl`.\n *\n * @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL.\n * @param tunnel - Current tunnel status from the running debug server.\n * @param totpSecret - Optional hex-encoded TOTP secret (from\n * `AIT_DEBUG_TOTP_SECRET`). When provided, the current code is spliced into\n * the attach URL as `at=<code>`.\n */\nexport function buildAttachUrl(\n schemeUrl: string,\n tunnel: TunnelStatus,\n totpSecret?: string,\n): BuildAttachUrlResult {\n if (!tunnel.up || tunnel.wssUrl === null) {\n throw new Error(\n 'tunnel-down: cloudflared 터널이 안 떠 있습니다. ' +\n 'MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.',\n );\n }\n const authorityWarning = validateSchemeAuthority(schemeUrl) ?? undefined;\n\n // Generate a live TOTP code when a secret is provided.\n // SECRET-HANDLING: the code value is placed into attachUrl only — not logged.\n let totpCode: string | undefined;\n let totpMeta: BuildAttachUrlResult['totp'];\n if (totpSecret !== undefined && totpSecret !== '') {\n const now = Date.now();\n totpCode = generateTotp(totpSecret, now);\n const STEP_SECONDS = 30;\n // Current step number (floor). The step expires at the start of the NEXT step.\n const currentStep = Math.floor(now / 1000 / STEP_SECONDS);\n const expiresAtMs = (currentStep + 1) * STEP_SECONDS * 1000;\n totpMeta = {\n enabled: true,\n ttlSeconds: STEP_SECONDS,\n expiresAt: new Date(expiresAtMs).toISOString(),\n };\n }\n\n return {\n attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl, totpCode),\n relayUrl: tunnel.wssUrl,\n ...(authorityWarning !== undefined ? { authorityWarning } : {}),\n ...(totpMeta !== undefined ? { totp: totpMeta } : {}),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* QR PNG rendering + browser open */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Heuristic: can this process open a GUI browser?\n *\n * Returns `true` when we think a GUI is available:\n * - On macOS (`darwin`) we assume yes (MCP normally runs on the user's Mac).\n * - On Linux we check for `DISPLAY` or `WAYLAND_DISPLAY`.\n * - On Windows we assume yes.\n * - In a CI environment (`CI=true`) we assume no.\n */\nexport function canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\n}\n\n/**\n * Result of `openQrInBrowser`.\n *\n * HTTP URL 기반으로 재구현 — tmp 파일 없음. `httpUrl`이 브라우저에 전달되는 URL이다.\n * SECRET-HANDLING: `httpUrl`은 127.0.0.1 로컬 전용이며 at= 코드 값을 직접 담지 않는다\n * (attachUrl은 /attach?u= query로 전달되어 서버 메모리에서만 처리).\n */\nexport interface OpenQrInBrowserResult {\n /** `true` if the browser was successfully opened. */\n opened: boolean;\n /** `http://127.0.0.1:<port>/attach?u=...` — 브라우저에 전달된 URL. */\n httpUrl: string;\n /** `http://127.0.0.1:<port>/qr.png?u=...` — PNG fallback URL. */\n pngUrl: string;\n /** Error message if `opened` is false (browser spawn failed). */\n error?: string;\n /** Captured stderr from failed spawn attempts (at= 값은 redact됨). */\n stderrSummary?: string;\n /**\n * `true` when the first attempt failed but a retry succeeded.\n * Helps distinguish \"worked on first try\" from \"needed retry\" in diagnostics.\n */\n retried?: boolean;\n}\n\n/** platform별 browser open 명령 후보 목록 — 앞에서부터 순차 시도. */\nfunction getBrowserCandidates(httpUrl: string): Array<{ cmd: string; args: string[] }> {\n const platform = process.platform;\n if (platform === 'darwin') {\n return [\n { cmd: 'open', args: [httpUrl] },\n { cmd: 'open', args: ['-a', 'Safari', httpUrl] },\n { cmd: 'open', args: ['-a', 'Google Chrome', httpUrl] },\n { cmd: 'open', args: ['-a', 'Firefox', httpUrl] },\n ];\n }\n if (platform === 'win32') {\n return [\n { cmd: 'cmd', args: ['/c', 'start', '', httpUrl] },\n { cmd: 'rundll32', args: ['url.dll,FileProtocolHandler', httpUrl] },\n ];\n }\n // linux + fallback\n return [\n { cmd: 'xdg-open', args: [httpUrl] },\n { cmd: 'sensible-browser', args: [httpUrl] },\n { cmd: 'x-www-browser', args: [httpUrl] },\n { cmd: 'firefox', args: [httpUrl] },\n { cmd: 'google-chrome', args: [httpUrl] },\n { cmd: 'chromium', args: [httpUrl] },\n ];\n}\n\n/** stderr에서 at= TOTP 코드 값을 redact한다. */\nfunction redactSecrets(text: string): string {\n // at=<value> 패턴에서 값 부분을 redact — TOTP 코드가 노출되지 않도록.\n return text.replace(/\\bat=([^&\\s\"']+)/g, 'at=<redacted>');\n}\n\n/** spawnSync exit 0이어도 stderr에 launch 실패 시그널이 있으면 실패로 판단한다. */\nconst LAUNCH_FAILURE_PATTERNS = [\n /LSOpenURLsWithRole\\(\\) failed/,\n /kLSApplicationNotFoundErr/,\n /No application/,\n /Unable to find application/,\n /xdg-open: not found/,\n /command not found/,\n];\n\nfunction isLaunchFailureStderr(stderr: string): boolean {\n return LAUNCH_FAILURE_PATTERNS.some((p) => p.test(stderr));\n}\n\n/**\n * 로컬 HTTP 서버 URL(`http://127.0.0.1:<port>/attach?u=...`)을 OS 기본 브라우저로 연다.\n *\n * platform별 fallback chain으로 시도하며, 모두 실패하면 1회 retry를 수행한다\n * (ephemeral process launch 타이밍 문제 대응). retry까지 실패해도 `opened: false` +\n * `httpUrl`을 반환해 사용자가 직접 브라우저에 붙여넣을 수 있게 한다.\n *\n * SECRET-HANDLING:\n * - tmp 파일을 만들지 않는다 (HTML/PNG는 HTTP 서버가 메모리에서 응답).\n * - httpUrl/pngUrl은 127.0.0.1 로컬 전용.\n * - stderr 캡처 결과에서 at= 코드 값을 redact한 후 stderrSummary에 포함.\n * - attachUrl, deploymentId, TOTP 코드를 stdout/stderr/로그에 직접 출력 금지.\n *\n * @param httpUrl - `http://127.0.0.1:<port>/attach?u=<encoded>` HTTP URL.\n * @param pngUrl - `http://127.0.0.1:<port>/qr.png?u=<encoded>` PNG fallback URL.\n */\nexport async function openQrInBrowser(\n httpUrl: string,\n pngUrl: string,\n): Promise<OpenQrInBrowserResult> {\n const { spawnSync } = await import('node:child_process');\n\n /**\n * 한 번의 fallback chain 시도. 성공하면 열린 후보 cmd를 반환, 실패하면 null.\n * stderrLines에 각 후보의 stderr를 누적한다.\n */\n function tryOnce(stderrLines: string[]): boolean {\n const candidates = getBrowserCandidates(httpUrl);\n for (const { cmd, args } of candidates) {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5000 });\n\n if (result.error) {\n stderrLines.push(`${cmd}: ${result.error.message}`);\n continue;\n }\n\n const stderr = typeof result.stderr === 'string' ? result.stderr : '';\n if (stderr) {\n stderrLines.push(`${cmd}: ${redactSecrets(stderr.trim())}`);\n }\n\n if (result.status === 0 && !isLaunchFailureStderr(stderr)) {\n return true;\n }\n }\n return false;\n }\n\n const stderrLines: string[] = [];\n\n // 1차 시도\n if (tryOnce(stderrLines)) {\n return { opened: true, httpUrl, pngUrl };\n }\n\n // 1회 retry (ephemeral process launch 타이밍 문제 대응)\n if (tryOnce(stderrLines)) {\n return { opened: true, httpUrl, pngUrl, retried: true };\n }\n\n const stderrSummary = stderrLines.length > 0 ? stderrLines.join('\\n') : undefined;\n return {\n opened: false,\n httpUrl,\n pngUrl,\n error: '모든 브라우저 실행 후보가 실패했습니다.',\n stderrSummary,\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Phase 2 — DOM / snapshot / screenshot (CDP commands) */\n/* -------------------------------------------------------------------------- */\n\n/** Returns the DOM tree of the attached page (`DOM.getDocument`). */\nexport function getDomDocument(connection: CdpConnection): Promise<DomGetDocumentResult> {\n // `pierce: true` flattens shadow roots; depth -1 returns the whole subtree so\n // a single call yields the full tree for structural diagnosis.\n return connection.send('DOM.getDocument', { depth: -1, pierce: true });\n}\n\n/** Returns a serialized page snapshot (`DOMSnapshot.captureSnapshot`). */\nexport function takeSnapshot(connection: CdpConnection): Promise<DomSnapshotResult> {\n return connection.send('DOMSnapshot.captureSnapshot', {});\n}\n\n/** A `take_screenshot` result: the raw base64 PNG plus a ready-to-use data URI. */\nexport interface ScreenshotResult {\n /** Base64-encoded PNG bytes (no data-URI prefix). */\n data: string;\n /** `data:image/png;base64,…` form for clients that render a URI. */\n dataUri: string;\n mimeType: 'image/png';\n}\n\n/** Captures a PNG screenshot of the attached page (`Page.captureScreenshot`). */\nexport async function takeScreenshot(connection: CdpConnection): Promise<ScreenshotResult> {\n const { data } = await connection.send('Page.captureScreenshot', { format: 'png' });\n return { data, dataUri: `data:image/png;base64,${data}`, mimeType: 'image/png' };\n}\n\n/* -------------------------------------------------------------------------- */\n/* measure_safe_area — Runtime.evaluate probe */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The JS probe injected via `Runtime.evaluate`. It reads:\n * 1. `env(safe-area-inset-*)` via a temporary element with padding set to\n * those CSS env vars, then `getComputedStyle`.\n * 2. SDK insets via a priority chain so the SAME probe works on both relay\n * (real device) and mock (devtools panel page):\n * a. `window.__sdk.SafeAreaInsets.get()` — dogfood bundle on real device.\n * b. `window.__sdk.getSafeAreaInsets()` — dogfood bundle (deprecated).\n * c. `window.__ait.state.safeAreaInsets` — devtools mock state (mock env).\n * The probe records `sdkInsetsSource` = `'window.__sdk'` | `'window.__ait'`\n * | `null`. If all paths fail the result carries `sdkInsetsError`.\n * 3. nav bar geometry: the SDK does not expose navBar height as a standalone\n * API — `.ait-navbar` DOM height is read as a cross-check, and\n * `navBarHeightSource` records where it came from.\n * 4. `innerWidth`, `innerHeight`, `devicePixelRatio`, `navigator.userAgent`.\n *\n * Returns a plain JSON-serialisable object so `returnByValue: true` works.\n *\n * NOTE: This expression is evaluated in the page context — on the real device\n * (relay) or on the mock panel page. It does not mutate any page state — the\n * temporary element is removed after reading. No secret or auth token is read\n * or returned.\n *\n * RFC #277 Tier C parity: the SAME probe string runs in both envs. Mock fidelity\n * comes from the panel's `applyViewport` / `computeSafeAreaInsets` correctly\n * setting `window.__ait.state.safeAreaInsets` (#275). When that is correct,\n * the cssEnv + sdkInsets pair returned here matches the relay's shape.\n */\nexport const SAFE_AREA_PROBE_EXPRESSION = `\n(function() {\n var el = document.createElement('div');\n el.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;visibility:hidden;' +\n 'padding-top:env(safe-area-inset-top,0px);' +\n 'padding-right:env(safe-area-inset-right,0px);' +\n 'padding-bottom:env(safe-area-inset-bottom,0px);' +\n 'padding-left:env(safe-area-inset-left,0px)';\n document.documentElement.appendChild(el);\n var cs = window.getComputedStyle(el);\n var cssEnv = {\n top: parseFloat(cs.paddingTop) || 0,\n right: parseFloat(cs.paddingRight) || 0,\n bottom: parseFloat(cs.paddingBottom) || 0,\n left: parseFloat(cs.paddingLeft) || 0\n };\n document.documentElement.removeChild(el);\n var sdkInsets = null;\n var sdkInsetsSource = null;\n var sdkInsetsError = undefined;\n try {\n var sdk = window.__sdk;\n var ait = window.__ait;\n if (sdk && sdk.SafeAreaInsets && typeof sdk.SafeAreaInsets.get === 'function') {\n sdkInsets = sdk.SafeAreaInsets.get();\n sdkInsetsSource = 'window.__sdk';\n } else if (sdk && typeof sdk.getSafeAreaInsets === 'function') {\n sdkInsets = sdk.getSafeAreaInsets();\n sdkInsetsSource = 'window.__sdk';\n } else if (ait && ait.state && ait.state.safeAreaInsets &&\n typeof ait.state.safeAreaInsets.top === 'number') {\n var s = ait.state.safeAreaInsets;\n sdkInsets = { top: s.top, bottom: s.bottom, left: s.left, right: s.right };\n sdkInsetsSource = 'window.__ait';\n } else if (!sdk && !ait) {\n sdkInsetsError = 'neither window.__sdk (relay) nor window.__ait (mock) available';\n } else if (sdk) {\n sdkInsetsError = 'neither SafeAreaInsets.get nor getSafeAreaInsets found on window.__sdk';\n } else {\n sdkInsetsError = 'window.__ait.state.safeAreaInsets is missing or malformed';\n }\n } catch(e) {\n sdkInsetsError = String(e && e.message || e);\n }\n var navBarHeight = null;\n var navBarHeightSource = 'not-exposed-by-sdk';\n try {\n var nb = document.querySelector('.ait-navbar');\n if (nb) {\n navBarHeight = nb.getBoundingClientRect().height;\n navBarHeightSource = 'dom-.ait-navbar';\n }\n } catch(_) {}\n var result = {\n cssEnv: cssEnv,\n sdkInsets: sdkInsets,\n sdkInsetsSource: sdkInsetsSource,\n navBarHeight: navBarHeight,\n navBarHeightSource: navBarHeightSource,\n innerWidth: window.innerWidth,\n innerHeight: window.innerHeight,\n devicePixelRatio: window.devicePixelRatio,\n userAgent: navigator.userAgent\n };\n if (sdkInsetsError !== undefined) result.sdkInsetsError = sdkInsetsError;\n return JSON.stringify(result);\n})()\n`.trim();\n\n/**\n * Where the SDK insets came from. `null` when the lookup failed (in which case\n * `sdkInsetsError` is populated).\n *\n * - `'window.__sdk'` — real-device dogfood bundle (relay env).\n * - `'window.__ait'` — devtools mock state (mock env).\n * - `null` — both paths absent or threw.\n */\nexport type SdkInsetsSource = 'window.__sdk' | 'window.__ait' | null;\n\n/**\n * Normalized result returned by `measure_safe_area`.\n *\n * All inset values are in CSS pixels as reported by the page context.\n * `userAgent` is included for device identification; it never contains\n * authentication secrets or session tokens.\n */\nexport interface SafeAreaMeasurement {\n /**\n * MCP environment this measurement was taken in:\n * - `'mock'` — dev browser panel\n * - `'relay-dev'` — real-device WebView, dogfood build\n * - `'relay-live'` — real-device WebView, live/production build\n *\n * Set by the caller (`measureSafeArea`) from the env detection SSoT\n * (`getEnvironment`).\n */\n source: McpEnvironment;\n /**\n * `env(safe-area-inset-*)` values read via `getComputedStyle` on the page.\n * On iOS inside the Toss host WebView this is typically all-zero because the\n * WebView viewport is placed below the physical notch by the host app.\n */\n cssEnv: { top: number; right: number; bottom: number; left: number };\n /**\n * SDK insets from one of three paths (in priority order):\n * - `window.__sdk.SafeAreaInsets.get()` (relay, dogfood bundle)\n * - `window.__sdk.getSafeAreaInsets()` (relay, deprecated)\n * - `window.__ait.state.safeAreaInsets` (mock, devtools panel state)\n *\n * `null` when all paths fail — see `sdkInsetsError` for the reason.\n * In the Toss host WebView `top` is the nav bar height and `bottom` is the\n * home-indicator height.\n */\n sdkInsets: { top: number; right: number; bottom: number; left: number } | null;\n /**\n * Which path resolved `sdkInsets` — useful for diagnosis of fidelity gaps\n * between mock and relay. `null` when `sdkInsets` is `null`.\n */\n sdkInsetsSource: SdkInsetsSource;\n /**\n * Populated when the SDK inset lookup failed (all paths absent or threw).\n * `undefined` when `sdkInsets` is non-null (i.e. the lookup succeeded).\n *\n * Example values:\n * - `\"neither window.__sdk (relay) nor window.__ait (mock) available\"`\n * - `\"neither SafeAreaInsets.get nor getSafeAreaInsets found on window.__sdk\"`\n * - `\"window.__ait.state.safeAreaInsets is missing or malformed\"`\n * - `\"TypeError: ...\"`\n */\n sdkInsetsError?: string;\n /**\n * Height of the `.ait-navbar` element (px) if present, else `null`.\n * The SDK does not expose navBar height as a standalone API; this DOM\n * measurement is used to cross-validate `sdkInsets.top`.\n */\n navBarHeight: number | null;\n /**\n * Describes where `navBarHeight` came from:\n * - `\"dom-.ait-navbar\"` — read from the `.ait-navbar` element's bounding rect.\n * - `\"not-exposed-by-sdk\"` — the SDK has no standalone navBar height API and\n * no `.ait-navbar` element was found in the DOM.\n */\n navBarHeightSource: string;\n /** CSS viewport width (`window.innerWidth`). */\n innerWidth: number;\n /** CSS viewport height (`window.innerHeight`). */\n innerHeight: number;\n /**\n * Device pixel ratio (`window.devicePixelRatio`).\n * Note: `window.devicePixelRatio` is read-only in the browser, so devtools\n * cannot emulate DPR locally — this is the ground-truth value from the device.\n */\n devicePixelRatio: number;\n /**\n * `navigator.userAgent` string for device identification.\n * Does not contain authentication secrets.\n */\n userAgent: string;\n}\n\n/**\n * Parses a raw `Runtime.evaluate` result value into a `SafeAreaMeasurement`.\n * The probe returns a JSON string (because `returnByValue:true` with a plain\n * object works unreliably across Chii relay versions — stringifying is safer).\n *\n * `source` is supplied by the caller (`measureSafeArea`) from the env SSoT.\n *\n * Throws if the result is missing, contains an exception, or cannot be parsed.\n */\nexport function normalizeSafeAreaResult(\n rawValue: unknown,\n source: McpEnvironment,\n): SafeAreaMeasurement {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `measure_safe_area: probe returned unexpected type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n throw new Error(`measure_safe_area: probe returned non-JSON string: ${rawValue}`);\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('measure_safe_area: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n\n function requireInsets(\n key: string,\n ): { top: number; right: number; bottom: number; left: number } | null {\n const v = obj[key];\n if (v === null || v === undefined) return null;\n if (typeof v !== 'object') return null;\n const r = v as Record<string, unknown>;\n return {\n top: typeof r.top === 'number' ? r.top : 0,\n right: typeof r.right === 'number' ? r.right : 0,\n bottom: typeof r.bottom === 'number' ? r.bottom : 0,\n left: typeof r.left === 'number' ? r.left : 0,\n };\n }\n\n const cssEnv = requireInsets('cssEnv') ?? { top: 0, right: 0, bottom: 0, left: 0 };\n const sdkInsets = requireInsets('sdkInsets');\n const sdkInsetsSource: SdkInsetsSource =\n obj.sdkInsetsSource === 'window.__sdk' || obj.sdkInsetsSource === 'window.__ait'\n ? obj.sdkInsetsSource\n : null;\n const sdkInsetsError = typeof obj.sdkInsetsError === 'string' ? obj.sdkInsetsError : undefined;\n const navBarHeight = typeof obj.navBarHeight === 'number' ? obj.navBarHeight : null;\n const navBarHeightSource =\n typeof obj.navBarHeightSource === 'string' ? obj.navBarHeightSource : 'not-exposed-by-sdk';\n const innerWidth = typeof obj.innerWidth === 'number' ? obj.innerWidth : 0;\n const innerHeight = typeof obj.innerHeight === 'number' ? obj.innerHeight : 0;\n const devicePixelRatio = typeof obj.devicePixelRatio === 'number' ? obj.devicePixelRatio : 1;\n const userAgent = typeof obj.userAgent === 'string' ? obj.userAgent : '';\n\n return {\n source,\n cssEnv,\n sdkInsets,\n sdkInsetsSource,\n ...(sdkInsetsError !== undefined ? { sdkInsetsError } : {}),\n navBarHeight,\n navBarHeightSource,\n innerWidth,\n innerHeight,\n devicePixelRatio,\n userAgent,\n };\n}\n\n/**\n * Runs the safe-area probe on the attached page and returns a normalized\n * `SafeAreaMeasurement`. Read-only — does not mutate page state.\n *\n * `source` is supplied by the caller from the env detection SSoT (see\n * `src/mcp/environment.ts`). The same `Runtime.evaluate` call runs in both\n * envs — the probe expression tries `window.__sdk` first (relay) then\n * `window.__ait` (mock), so mock fidelity is enforced by the panel's\n * `applyViewport`/`computeSafeAreaInsets` keeping `__ait.state.safeAreaInsets`\n * correct (RFC #277 Tier C parity, #275 model).\n *\n * Throws on CDP error, probe exception, or result parse failure.\n */\nexport async function measureSafeArea(\n connection: CdpConnection,\n source: McpEnvironment,\n): Promise<SafeAreaMeasurement> {\n const result = await connection.send('Runtime.evaluate', {\n expression: SAFE_AREA_PROBE_EXPRESSION,\n returnByValue: true,\n awaitPromise: false,\n });\n if (result.exceptionDetails) {\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`measure_safe_area: probe threw — ${msg}`);\n }\n return normalizeSafeAreaResult(result.result.value, source);\n}\n\n/* -------------------------------------------------------------------------- */\n/* evaluate — arbitrary JS via Runtime.evaluate */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result returned by the `evaluate` tool.\n *\n * `value` holds the `returnByValue` result from CDP — it may be any\n * JSON-serialisable type. Treat it as opaque for logging purposes (it could\n * carry sensitive data from the page context).\n *\n * SECRET-HANDLING: do NOT write `value` to any log or stderr — return it to\n * the agent via the tool result only.\n */\nexport interface EvaluateResult {\n /** The evaluated result value (`returnByValue: true`). */\n value: unknown;\n /** CDP type string of the result (e.g. \"string\", \"number\", \"object\"). */\n type: string;\n}\n\n/**\n * Evaluates an arbitrary JS expression on the attached page via\n * `Runtime.evaluate`. NOT read-only — the expression may have side effects.\n *\n * Throws if the evaluation produced a CDP exception.\n *\n * SECRET-HANDLING: expression and result value are NOT written to any log.\n */\nexport async function evaluate(\n connection: CdpConnection,\n expression: string,\n): Promise<EvaluateResult> {\n const result = await connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: false,\n });\n if (result.exceptionDetails) {\n // Surface only the engine error string — never the expression or result value.\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`evaluate failed: ${msg}`);\n }\n return { value: result.result.value, type: result.result.type };\n}\n\n/* -------------------------------------------------------------------------- */\n/* call_sdk — window.__sdkCall bridge via Runtime.evaluate */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result returned by the `call_sdk` tool.\n * The bridge call wraps success/failure in a JSON envelope so cross-Chii\n * stringification is reliable (same approach as `measure_safe_area`).\n *\n * `recentException` is populated when a `Runtime.exceptionThrown` event was\n * observed within the heuristic triage window [callStart-50ms, callEnd+200ms].\n * This helps correlate an SDK throw with the bridge result, especially when\n * the SDK throws synchronously before the promise resolves.\n */\nexport type CallSdkResult =\n | { ok: true; value: unknown; recentException?: BufferedException }\n | { ok: false; error: string; recentException?: BufferedException };\n\n/**\n * Builds the Runtime.evaluate expression that calls `window.__sdkCall` with\n * the given method name and args, awaits the promise, and returns a JSON\n * envelope `{ok, value/error}` as a string.\n *\n * Name and args are embedded via `JSON.stringify` so they are safely escaped.\n * The expression checks for `window.__sdkCall` and returns a clear error if\n * it is absent (non-dogfood bundle).\n *\n * SECRET-HANDLING: the expression is built here and MUST NOT be written to\n * any log or stderr by the caller.\n */\nexport function buildCallSdkExpression(name: string, args: unknown[]): string {\n const safeName = JSON.stringify(name);\n const safeArgs = JSON.stringify(args);\n return (\n `(async () => {` +\n ` if (typeof window.__sdkCall !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'sdk-absent: window.__sdkCall이 주입되지 않았습니다 (dogfood 빌드가 아닙니다). dogfood 채널로 재배포하세요.'});` +\n ` }` +\n ` try {` +\n ` const r = await window.__sdkCall(${safeName}, ...${safeArgs});` +\n ` return JSON.stringify({ok:true,value:r});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Parses the JSON envelope string returned by the `call_sdk` expression.\n * Returns a typed `CallSdkResult`.\n *\n * Throws only on parse failure (not on ok:false — that is a normal result).\n */\nexport function normalizeCallSdkResult(rawValue: unknown): CallSdkResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `call_sdk: bridge returned unexpected type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue in the error message — it could contain secrets.\n throw new Error('call_sdk: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('call_sdk: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, value: obj.value };\n }\n if (obj.ok === false) {\n return { ok: false, error: typeof obj.error === 'string' ? obj.error : String(obj.error) };\n }\n throw new Error('call_sdk: bridge result missing \"ok\" field');\n}\n\n/**\n * Looks up the most recent exception from the buffer that falls within the\n * triage window [windowStart, windowEnd]. Returns `undefined` if none found.\n *\n * The heuristic window is:\n * - windowStart = callStart - 50ms (catch sync throws before bridge fires)\n * - windowEnd = callEnd + 200ms (catch async throws resolved soon after)\n *\n * Only the most recent exception within the window is returned (the one most\n * likely to be causally related to the SDK call).\n */\nfunction findRecentException(\n connection: CdpConnection,\n windowStart: number,\n windowEnd: number,\n): BufferedException | undefined {\n const events = connection.getBufferedEvents('Runtime.exceptionThrown');\n // Scan from the tail (most recent) to find the closest-in-time exception.\n for (let i = events.length - 1; i >= 0; i--) {\n const e = events[i];\n if (e.timestamp >= windowStart && e.timestamp <= windowEnd) {\n return normalizeException(e);\n }\n }\n return undefined;\n}\n\n/**\n * Calls a dogfood SDK method via `window.__sdkCall` on the attached page.\n * NOT read-only — SDK calls may have side effects.\n *\n * On env 2/3 (real device relay) this hits the real SDK; on env 1 (local\n * mock) it hits the mock SDK.\n *\n * 인자 시그니처 검증: 등록된 메서드는 bridge 호출 전에 인자를 검증하고, mismatch면\n * `{ok:false, error}` MCP 오류 결과를 반환한다(bridge에 도달하지 않음).\n * 미등록 메서드는 passthrough + stderr 경고 1회.\n *\n * Throws on CDP error or result parse failure. Returns `{ok:false, error}`\n * for bridge-level errors (method not found, SDK threw, bridge absent) or\n * argument schema violations.\n *\n * If a `Runtime.exceptionThrown` event was observed within the triage window\n * [callStart-50ms, callEnd+200ms], the result includes `recentException` for\n * crash triage. This window is a heuristic — it catches the common case of an\n * SDK throw immediately before/after the bridge resolves.\n *\n * SECRET-HANDLING: name, args, and the result value are NOT written to any log.\n */\nexport async function callSdk(\n connection: CdpConnection,\n name: string,\n args: unknown[],\n): Promise<CallSdkResult> {\n // 인자 시그니처 검증 — bridge 호출 전에 reject하여 native crash를 예방한다.\n const signature = lookupSignature(name);\n if (signature !== undefined) {\n const validation = signature.validateArgs(args);\n if (!validation.ok) {\n // isError: true 형태로 반환 — bridge에 도달하지 않음.\n const errorText =\n `call_sdk(\"${name}\") 인자 시그니처 오류.\\n` +\n `받음: ${validation.received}\\n` +\n `기대: ${validation.expected}\\n` +\n `올바른 예시: ${signature.example}`;\n return { ok: false, error: errorText };\n }\n } else {\n // 미등록 메서드 — passthrough하지만 stderr에 경고 1회.\n warnPassthrough(name);\n }\n\n const callStart = Date.now();\n const expression = buildCallSdkExpression(name, args);\n const result = await connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\n });\n const callEnd = Date.now();\n\n if (result.exceptionDetails) {\n // Surface only the engine error string — never name, args, or result value.\n const msg =\n result.exceptionDetails.exception?.description ??\n result.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`call_sdk threw: ${msg}`);\n }\n\n const sdkResult = normalizeCallSdkResult(result.result.value);\n\n // Triage window: [callStart - 50ms, callEnd + 200ms].\n // -50ms: catches sync throws that fire just before the bridge call is sent.\n // +200ms: catches async throws resolved shortly after the bridge returns.\n const recentException = findRecentException(connection, callStart - 50, callEnd + 200);\n\n if (recentException !== undefined) {\n return { ...sdkResult, recentException };\n }\n return sdkResult;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Phase 3 — AIT.* domain (CDP can't cover these) */\n/* -------------------------------------------------------------------------- */\n\n/** Set of tool names served by the AIT source rather than the CDP connection. */\nconst AIT_TOOL_NAMES = new Set<string>([\n 'AIT.getSdkCallHistory',\n 'AIT.getMockState',\n 'AIT.getOperationalEnvironment',\n]);\n\n/** True for the Phase 3 AIT.* tools (served by an `AitSource`, not CDP). */\nexport function isAitToolName(name: string): boolean {\n return AIT_TOOL_NAMES.has(name);\n}\n\n/** Returns the recent SDK call trace (`AIT.getSdkCallHistory`). */\nexport function getSdkCallHistory(source: AitSource): Promise<AitSdkCallHistory> {\n return source.get('AIT.getSdkCallHistory');\n}\n\n/** Returns the devtools mock-state snapshot (`AIT.getMockState`). */\nexport function getMockState(source: AitSource): Promise<AitMockState> {\n return source.get('AIT.getMockState');\n}\n\n/** Returns the operational environment + SDK version (`AIT.getOperationalEnvironment`). */\nexport function getOperationalEnvironment(source: AitSource): Promise<AitOperationalEnvironment> {\n return source.get('AIT.getOperationalEnvironment');\n}\n\n/* -------------------------------------------------------------------------- */\n/* get_diagnostics — single-call server status snapshot (#286) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Represents a single redacted server-side error entry in the diagnostics\n * snapshot. PII / secrets are scrubbed before this is returned.\n */\nexport interface DiagnosticsError {\n /** ISO timestamp when the error was recorded. */\n timestamp: string;\n /** Error message with PII/secrets redacted (e.g. `at=<redacted>`). */\n message: string;\n /** Optional error category for quick triage. */\n category?: string;\n}\n\n/**\n * Tunnel state in the diagnostics snapshot. Same shape as `TunnelStatus` but\n * extended with the lock-file data (pid, startedAt) when available.\n */\nexport interface DiagnosticsTunnelInfo {\n /** Whether the cloudflared quick tunnel is currently up. */\n up: boolean;\n /** Public `wss://*.trycloudflare.com` relay URL, or `null`. */\n wssUrl: string | null;\n /**\n * PID of the MCP server process that owns the tunnel (from the lock file),\n * or `null` when no lock is present.\n */\n pid: number | null;\n /**\n * ISO timestamp when the owning server process started (from the lock file),\n * or `null`.\n */\n startedAt: string | null;\n}\n\n/**\n * Server-lock holder info from `~/.ait-devtools/server.lock`. `null` when\n * no lock file exists (server was cleanly shut down or never started).\n */\nexport interface DiagnosticsLockHolder {\n pid: number;\n startedAt: string;\n /** wssUrl recorded in the lock file — may be `null` when tunnel is still starting. */\n wssUrl: string | null;\n}\n\n/**\n * The next recommended tool for the agent to call, based on the current server\n * state snapshot. `null` means the session looks healthy — no specific action needed.\n */\nexport interface NextRecommendedAction {\n /** MCP tool name to call next (e.g. `'build_attach_url'`, `'restart'`). */\n tool: string;\n /** Human-readable reason explaining why this action is recommended. */\n reason: string;\n}\n\n/**\n * Full server status snapshot returned by `get_diagnostics`.\n *\n * All fields are nullable — a missing value means \"not yet known\" (e.g. tunnel\n * not up yet) rather than an error. The schema is intentionally stable across\n * versions: new optional fields may be added but existing fields are not\n * removed or renamed.\n *\n * SECRET-HANDLING: No TOTP secret, cookie, deploy key, or `at=` code value\n * appears in this snapshot. `recentErrors` entries are redacted before inclusion.\n */\nexport interface DiagnosticsResult {\n /** `@modelcontextprotocol/sdk` package version string. */\n mcpVersion: string | null;\n /** `@ait-co/devtools` package version string. */\n devtoolsVersion: string | null;\n /** Tunnel state including lock-file pid/startedAt. */\n tunnel: DiagnosticsTunnelInfo;\n /** Current list_pages result (pages + crash info + singleAttachModel). */\n pages: ListPagesResult | null;\n /** ISO timestamp of the most recent page attach, or `null`. */\n lastAttachAt: string | null;\n /** ISO timestamp of the most recent page detach, or `null`. */\n lastDetachAt: string | null;\n /**\n * Recent server-side errors (up to `recent_errors_limit`, default 10).\n * Redacted: `at=<redacted>`, cookie headers stripped, AITCC_API_KEY masked.\n */\n recentErrors: DiagnosticsError[];\n /**\n * Resolved environment and the reason string.\n *\n * `kind` — the precise three-value environment (`mock` | `relay-dev` |\n * `relay-live`). Use this for new code.\n * `env` — backward-compat two-value alias (`mock` | `relay`). Kept so\n * existing callers that only distinguish mock vs relay continue to work.\n */\n environment: {\n kind: McpEnvironment;\n /** @deprecated Use `kind` instead. Kept for backward compatibility. */\n env: 'mock' | 'relay';\n reason: string;\n /** `true` when the LIVE side-effect guard is active (`kind === 'relay-live'`). */\n liveGuardActive: boolean;\n };\n /**\n * Contents of `~/.ait-devtools/server.lock`, or `null` when absent.\n * Useful for diagnosing stale-lock conflicts without running the full server.\n */\n serverLockHolder: DiagnosticsLockHolder | null;\n /**\n * Single next recommended action for the agent, or `null` when the session\n * looks healthy. Derived deterministically from the other snapshot fields —\n * the agent should call this tool next rather than inferring from raw fields.\n *\n * Branch rules (evaluated in priority order):\n * 1. tunnel.up === false AND relay env → restart\n * 1b. tunnel.up === false AND mock env, no pages → wait_for_page (local target is tunnel-less)\n * 2. tunnel.up, pages empty, env === relay → build_attach_url\n * 3. pages[0] exists + crashDetectedAt non-null → build_attach_url (re-attach)\n * 4. otherwise → null\n */\n nextRecommendedAction: NextRecommendedAction | null;\n}\n\n/**\n * Registry of server-side errors collected by `DiagnosticsCollector`.\n * Injected into `createDebugServer` so it is testable without a real process.\n */\nexport interface DiagnosticsCollector {\n /** Records a server-side error for later surfacing in `get_diagnostics`. */\n recordError(message: string, category?: string): void;\n /** Returns the most recent `limit` errors, oldest-first. */\n getRecentErrors(limit: number): DiagnosticsError[];\n /** Records an attach event (ISO timestamp stored). */\n recordAttach(): void;\n /** Records a detach event (ISO timestamp stored). */\n recordDetach(): void;\n /** Returns the ISO timestamp of the last attach, or `null`. */\n getLastAttachAt(): string | null;\n /** Returns the ISO timestamp of the last detach, or `null`. */\n getLastDetachAt(): string | null;\n}\n\n/** Secret-redaction patterns applied before error messages enter the buffer. */\nconst SECRET_REDACT_PATTERNS: ReadonlyArray<[RegExp, string]> = [\n // TOTP at= code value.\n [/\\bat=([^&\\s\"']+)/g, 'at=<redacted>'],\n // Cookie / Set-Cookie header values — replace everything after the colon.\n [/((?:set-)?cookie)\\s*:\\s*.+/gi, '$1: <redacted>'],\n // AITCC_API_KEY env-var-style references.\n [/AITCC_API_KEY\\s*=\\s*\\S+/gi, 'AITCC_API_KEY=<redacted>'],\n // Authorization header (covers \"Authorization: Bearer …\" and bare \"Bearer <token>\").\n [/Authorization\\s*:\\s*.+/gi, 'Authorization: <redacted>'],\n [/\\bBearer\\s+\\S+/g, 'Bearer <redacted>'],\n];\n\n/**\n * Applies all secret-redaction patterns to an error message string.\n * Used before storing errors in the `DiagnosticsCollector` ring buffer.\n *\n * SECRET-HANDLING: this is the single bottleneck for redaction — all error\n * strings must pass through here before reaching the buffer.\n */\nexport function redactErrorMessage(message: string): string {\n let result = message;\n for (const [pattern, replacement] of SECRET_REDACT_PATTERNS) {\n result = result.replace(pattern, replacement);\n }\n return result;\n}\n\n/** Default max buffer size for the error ring buffer. */\nconst DEFAULT_ERROR_BUFFER_SIZE = 50;\n\n/**\n * In-memory implementation of `DiagnosticsCollector`. Thread-safe in the\n * single-threaded Node.js sense (synchronous mutations only).\n */\nexport class InMemoryDiagnosticsCollector implements DiagnosticsCollector {\n private readonly buffer: DiagnosticsError[] = [];\n private readonly maxSize: number;\n private lastAttachAt: string | null = null;\n private lastDetachAt: string | null = null;\n\n constructor(maxSize = DEFAULT_ERROR_BUFFER_SIZE) {\n this.maxSize = maxSize;\n }\n\n recordError(message: string, category?: string): void {\n const entry: DiagnosticsError = {\n timestamp: new Date().toISOString(),\n message: redactErrorMessage(message),\n ...(category !== undefined ? { category } : {}),\n };\n this.buffer.push(entry);\n // Keep only the most recent `maxSize` entries.\n if (this.buffer.length > this.maxSize) {\n this.buffer.shift();\n }\n }\n\n getRecentErrors(limit: number): DiagnosticsError[] {\n const cap = Math.min(Math.max(1, limit), DEFAULT_ERROR_BUFFER_SIZE);\n const sliced =\n this.buffer.length > cap ? this.buffer.slice(this.buffer.length - cap) : [...this.buffer];\n return sliced;\n }\n\n recordAttach(): void {\n this.lastAttachAt = new Date().toISOString();\n }\n\n recordDetach(): void {\n this.lastDetachAt = new Date().toISOString();\n }\n\n getLastAttachAt(): string | null {\n return this.lastAttachAt;\n }\n\n getLastDetachAt(): string | null {\n return this.lastDetachAt;\n }\n}\n\n/**\n * Reads the `@modelcontextprotocol/sdk` package version from the installed\n * package's `package.json`. Returns `null` on any error (missing file, JSON\n * parse failure, etc.) — diagnostics must never throw.\n *\n * Node-only — uses dynamic `import()` so it does not pollute the browser\n * module graph.\n */\nexport async function readMcpSdkVersion(): Promise<string | null> {\n try {\n // Resolve the package.json adjacent to the installed SDK entry point.\n const { createRequire } = await import('node:module');\n const req = createRequire(import.meta.url);\n const pkgPath = req.resolve('@modelcontextprotocol/sdk/package.json');\n const { readFileSync } = await import('node:fs');\n const raw = readFileSync(pkgPath, 'utf8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n return typeof parsed.version === 'string' ? parsed.version : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Returns the `@ait-co/devtools` package version injected at build time via\n * the `__VERSION__` define. Returns `null` when the global is absent (e.g. in\n * some test environments that skip the build step).\n */\nexport function readDevtoolsVersion(): string | null {\n try {\n // `__VERSION__` is injected by tsdown / vite via `define`.\n // biome-ignore lint/suspicious/noExplicitAny: intentional global check\n const v = (globalThis as any).__VERSION__;\n return typeof v === 'string' && v.length > 0 ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Derives the next recommended action from a completed diagnostics snapshot.\n *\n * Branch rules (evaluated in priority order):\n * 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)\n * 1b. tunnel.up === false AND env is mock → wait_for_page (local target: tunnel-less is normal)\n * 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)\n * 3. pages has entry + crashDetectedAt non-null → build_attach_url (re-attach after crash)\n * 4. otherwise → null (session looks healthy)\n *\n * Pure — does not throw; receives the final assembled snapshot fields.\n */\nexport function computeNextRecommendedAction(\n tunnel: DiagnosticsTunnelInfo,\n pages: ListPagesResult | null,\n env: McpEnvironment,\n): NextRecommendedAction | null {\n // Rule 1: tunnel is down.\n if (!tunnel.up) {\n // Rule 1b: local-target (mock env) runs without a relay tunnel by design —\n // tunnel.up === false is the expected steady state. Instead of recommending\n // a server restart, guide the agent to wait for the page to load.\n if (!isRelayEnv(env)) {\n // Only surface wait_for_page when no page is attached yet; once a page\n // attaches the session is healthy and null is the correct return value.\n if (pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) {\n return {\n tool: 'wait_for_page',\n reason:\n 'local Chromium spawn 직후 — 페이지 로드를 기다리거나 list_pages를 재호출하세요 ' +\n '(local 모드는 tunnel이 없는 게 정상입니다)',\n };\n }\n // Page already attached or crash detected — fall through to other rules.\n } else {\n // Rule 1 (relay env): tunnel must be up for relay to work — restart.\n return {\n tool: 'restart',\n reason: 'tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart',\n };\n }\n }\n\n // Rule 2: tunnel up but no pages attached in relay env → start attach.\n if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) {\n return {\n tool: 'build_attach_url',\n reason: 'tunnel ready, no pages attached — call build_attach_url to generate the attach QR',\n };\n }\n\n // Rule 3: crash detected — need to re-attach.\n if (pages !== null && pages.crashDetectedAt !== null) {\n return {\n tool: 'build_attach_url',\n reason: `page crashed at ${pages.crashDetectedAt} — call build_attach_url to re-attach`,\n };\n }\n\n // Rule 4: session looks healthy.\n return null;\n}\n\n/** Input for `getDiagnostics`. */\nexport interface GetDiagnosticsInput {\n /** Current tunnel status (from the server's live `getTunnelStatus()`). */\n tunnel: TunnelStatus;\n /**\n * CDP connection used to call `list_pages` — may be absent in edge cases\n * (e.g. called from the dev-mode server which has no CDP connection).\n */\n connection?: CdpConnection;\n /**\n * Resolved MCP environment (`mock` | `relay-dev` | `relay-live`). Caller\n * obtains via `resolveEnvironment()`.\n */\n env: McpEnvironment;\n /** Human-readable reason for the env decision. */\n envReason: string;\n /** Diagnostics collector for errors / attach events. */\n collector: DiagnosticsCollector;\n /** Lock-file reader — injected so tests can override without touching the FS. */\n readLock: () => import('./server-lock.js').LockData | null;\n /** Maximum number of recent errors to include (default 10). */\n recentErrorsLimit?: number;\n /** Optional async resolver for the MCP SDK version. */\n getMcpVersion?: () => Promise<string | null>;\n}\n\n/**\n * Builds the `get_diagnostics` response. Pure — does not throw; missing data\n * fields are `null`. Async because `readMcpSdkVersion` needs `import()`.\n *\n * SECRET-HANDLING:\n * - `recentErrors` messages are already redacted by `recordError` (via\n * `redactErrorMessage`). No additional redaction needed here.\n * - `tunnel.wssUrl` is a public cloudflared hostname — not a secret.\n * - Lock file data contains only pid + startedAt + wssUrl — no secrets.\n */\nexport async function getDiagnostics(input: GetDiagnosticsInput): Promise<DiagnosticsResult> {\n const {\n tunnel,\n connection,\n env,\n envReason,\n collector,\n readLock: readLockFn,\n recentErrorsLimit = 10,\n getMcpVersion = readMcpSdkVersion,\n } = input;\n\n const [mcpVersion, devtoolsVersion] = await Promise.all([\n getMcpVersion(),\n Promise.resolve(readDevtoolsVersion()),\n ]);\n\n // Read lock file for serverLockHolder + tunnel pid/startedAt.\n const lockData = readLockFn();\n const serverLockHolder: DiagnosticsLockHolder | null = lockData\n ? { pid: lockData.pid, startedAt: lockData.startedAt, wssUrl: lockData.wssUrl }\n : null;\n\n const tunnelInfo: DiagnosticsTunnelInfo = {\n up: tunnel.up,\n wssUrl: tunnel.wssUrl,\n pid: lockData?.pid ?? null,\n startedAt: lockData?.startedAt ?? null,\n };\n\n // list_pages — non-fatal; null on any error.\n let pages: ListPagesResult | null = null;\n if (connection !== undefined) {\n try {\n pages = listPages(connection, tunnel);\n } catch {\n // Ignore — pages stays null.\n }\n }\n\n const limit = Math.min(Math.max(1, recentErrorsLimit), 50);\n const recentErrors = collector.getRecentErrors(limit);\n\n const nextRecommendedAction = computeNextRecommendedAction(tunnelInfo, pages, env);\n\n return {\n mcpVersion,\n devtoolsVersion,\n tunnel: tunnelInfo,\n pages,\n lastAttachAt: collector.getLastAttachAt(),\n lastDetachAt: collector.getLastDetachAt(),\n recentErrors,\n environment: {\n kind: env,\n env: toLegacyEnv(env),\n reason: envReason,\n liveGuardActive: isLiveRelayEnv(env),\n },\n serverLockHolder,\n nextRecommendedAction,\n };\n}\n","/**\n * @ait-co/devtools dev-mode MCP server (stdio).\n *\n * Exposes the live browser mock state from a running Vite dev server to AI\n * coding agents via the Model Context Protocol (MCP).\n *\n * Architecture:\n * Browser (aitState) → Vite dev server endpoint (/api/ait-devtools/state)\n * ← HTTP GET ← this stdio MCP server ← AI agent\n *\n * The Vite endpoint is registered by the unplugin when `mcp: true` is set in\n * the plugin options (see `src/unplugin/index.ts`).\n *\n * Phase 3 tool-surface alignment: dev mode and debug mode now expose the same\n * `AIT.*` tools (`AIT.getMockState`, `AIT.getOperationalEnvironment`,\n * `AIT.getSdkCallHistory`). In dev mode they are backed by the HTTP mock-state\n * endpoint (see `HttpAitSource`); in debug mode by the Chii channel. So an AI\n * sees a coherent tool whether attached to a phone (debug) or a dev browser\n * (dev). `devtools_get_mock_state` (the original devtools#130 name) is kept as a\n * backward-compatible alias of `AIT.getMockState`.\n *\n * Issue #305 (M2-1) — dev/debug tool-surface unification:\n * dev-mode now also exposes `list_pages`, `get_diagnostics`, `measure_safe_area`,\n * and `call_sdk` so the docs/qa/scenarios.md acceptance sequence\n * `list_pages → measure_safe_area → call_sdk` works in dev mode without\n * \"Unknown tool\" failures.\n *\n * - `list_pages` — shim: returns the Vite dev URL as a single-entry array.\n * - `get_diagnostics` — dumps dev-mode server state (endpoint URL, last fetch\n * error, reachability, mode/environment metadata).\n * - `measure_safe_area`— reads safeAreaInsets from the mock state snapshot\n * (source: 'mock-vite').\n * - `call_sdk` — reads mock state and builds a mock-equivalent result\n * using window.__ait.state for supported methods; returns\n * an explicit tier-filter error for methods that require\n * a live CDP bridge.\n * - CDP-only tools (`evaluate`, `take_screenshot`, `get_dom_document`,\n * `take_snapshot`, `list_console_messages`,\n * `list_network_requests`, `list_exceptions`) — return an\n * explicit tier-filter error explaining that CDP is unavailable\n * in dev-mode and pointing to `--mode=local` or `--mode=debug`.\n *\n * This module is reached via the `devtools-mcp --mode=dev` CLI entry (see\n * `cli.ts`); the default (no flag) bin mode is the debug-mode CDP/Chii server.\n *\n * Usage (in your MCP client config, e.g. Claude Desktop):\n * {\n * \"mcpServers\": {\n * \"ait-devtools\": {\n * \"command\": \"pnpm\",\n * \"args\": [\"exec\", \"devtools-mcp\", \"--mode=dev\"],\n * \"env\": { \"AIT_DEVTOOLS_URL\": \"http://localhost:5173\" }\n * }\n * }\n * }\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { HttpAitSource } from './ait-http-source.js';\nimport type { AitSource } from './ait-source.js';\nimport { wrapEnvelope } from './envelope.js';\nimport { mcpError, tierRejectionError } from './errors.js';\nimport {\n getMockState,\n getOperationalEnvironment,\n getSdkCallHistory,\n isAitToolName,\n type ToolAvailability,\n} from './tools.js';\n\n/** Error message prefix for CDP-dependent tools called in dev-mode. */\nconst CDP_UNAVAILABLE_IN_DEV_MODE =\n 'dev-mode에서는 CDP 연결이 없어 이 도구를 사용할 수 없습니다. ' +\n '실기기 또는 로컬 Chromium에 붙이려면 `devtools-mcp --mode=local` 또는 ' +\n '`devtools-mcp` (debug 모드 기본)로 전환하세요.';\n\n/**\n * Tool descriptors served by the dev-mode server.\n *\n * All dev-mode tools are Tier C (both envs) per RFC #277 — the dev-mode server\n * itself is the mock-side embodiment of those Tier C tools. `availableIn` is\n * declared so the surface stays consistent with the debug-mode registry.\n *\n * Issue #305: CDP-only tools are also listed with explicit descriptions so\n * agents do not get \"Unknown tool\" failures — they get a clear tier-filter\n * error message instead.\n */\nconst DEV_TOOL_DEFINITIONS = [\n /* ------------------------------------------------------------------ */\n /* AIT.* tools — HTTP mock-state backed */\n /* ------------------------------------------------------------------ */\n {\n name: 'AIT.getMockState',\n description:\n 'Returns the devtools mock state snapshot (window.__ait) from the running browser session — ' +\n 'environment, permissions, location, auth, network, IAP, and more. Read-only. ' +\n 'Requires the Vite dev server running with the @ait-co/devtools unplugin option `mcp: true`. ' +\n 'Same tool as in debug mode, where the in-app side reports it over the AIT domain.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getOperationalEnvironment',\n description:\n 'Returns the operational environment + SDK/app version derived from the dev mock state. ' +\n 'Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'AIT.getSdkCallHistory',\n description:\n 'Returns the SDK call trace. In dev mode the HTTP mock-state endpoint records no trace, so ' +\n 'this returns an empty list; in debug mode it is populated over the AIT domain. Read-only.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'devtools_get_mock_state',\n description:\n 'Backward-compatible alias of AIT.getMockState (the original devtools#130 name). Returns the ' +\n 'current AIT DevTools mock state snapshot. Read-only. Prefer AIT.getMockState in new configs.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* Unified surface — dev-mode shims (issue #305) */\n /* ------------------------------------------------------------------ */\n {\n name: 'list_pages',\n description:\n 'dev-mode: returns the Vite dev server URL as a single-entry page list. ' +\n 'No CDP relay is involved — `tunnel.up` is always false and `devMode: true` marks ' +\n 'this as a shim result. Call this first to confirm the dev server is reachable. ' +\n 'In debug mode (`devtools-mcp` / `--mode=local`) this returns real attached pages.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_diagnostics',\n description:\n 'dev-mode: returns server diagnostics — Vite endpoint URL, last fetch timestamp/error, ' +\n 'mock state endpoint reachability, mode (\"dev\"), and environment metadata. ' +\n 'Call this when the dev server connection is suspect. ' +\n 'In debug mode this returns tunnel/relay/attach status instead.',\n inputSchema: {\n type: 'object',\n properties: {\n recent_errors_limit: {\n type: 'number',\n description: 'Ignored in dev-mode (no error ring buffer). Present for schema parity.',\n },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'measure_safe_area',\n description:\n 'dev-mode: reads safe-area insets from the mock state snapshot via the Vite endpoint. ' +\n 'Returns `{ source: \"mock-vite\", sdkInsets, sdkInsetsSource: \"window.__ait\", ... }`. ' +\n 'Values reflect what the DevTools panel reports at the time of the last state push. ' +\n 'In debug mode this runs a Runtime.evaluate CDP probe on the attached page.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'call_sdk',\n description:\n 'dev-mode: calls a mock SDK method via the Vite mock state endpoint. ' +\n 'Supported methods read from window.__ait mock state (e.g. getOperationalEnvironment). ' +\n 'Returns the same `{ok, value}` / `{ok, error}` envelope as debug mode. ' +\n 'In debug mode this calls the real SDK via window.__sdkCall over CDP.',\n inputSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description: 'Mock SDK method name to call (e.g. \"getOperationalEnvironment\").',\n },\n args: {\n type: 'array',\n description: 'Arguments (ignored in dev-mode mock path; present for schema parity).',\n items: {},\n },\n },\n required: ['name'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* Tier B tool — tier-filter stub (issue #323) */\n /* */\n /* build_attach_url is relay-only (Tier B per RFC #277). Listing it */\n /* here in dev-mode ensures agents don't hit \"Unknown tool\" and get a */\n /* clear hand-off hint toward --mode=debug (station 2 → 3 seam). */\n /* ------------------------------------------------------------------ */\n {\n name: 'build_attach_url',\n description:\n 'Turns an `ait deploy --scheme-only` URL into a self-attaching deep link for a real device. ' +\n 'NOT available in dev-mode — requires a live cloudflared relay (Tier B, relay-only). ' +\n 'To use this tool: restart the MCP server with `--mode=debug` (or omit --mode) and set ' +\n 'MCP_ENV=relay, then call build_attach_url to generate the QR for phone scanning. ' +\n 'See: https://docs.aitc.dev/guides/debug-relay',\n inputSchema: {\n type: 'object',\n properties: {\n scheme_url: {\n type: 'string',\n description: 'The intoss-private:// URL from `ait deploy --scheme-only`.',\n },\n wait_for_attach: {\n type: 'boolean',\n description: 'If true, block until a page attaches.',\n },\n open_in_browser: {\n type: 'boolean',\n description: 'If true (default), open the QR PNG in the OS browser.',\n },\n },\n required: ['scheme_url'],\n },\n availableIn: 'relay' as ToolAvailability,\n },\n /* ------------------------------------------------------------------ */\n /* CDP-only tools — tier-filter stubs so agents see a clear error */\n /* instead of \"Unknown tool\" (issue #305) */\n /* ------------------------------------------------------------------ */\n {\n name: 'evaluate',\n description:\n 'Evaluates an arbitrary JavaScript expression via CDP Runtime.evaluate. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug` for CDP access.',\n inputSchema: {\n type: 'object',\n properties: {\n expression: { type: 'string', description: 'JavaScript expression to evaluate.' },\n },\n required: ['expression'],\n },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_screenshot',\n description:\n 'Captures a PNG screenshot via CDP Page.captureScreenshot. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'get_dom_document',\n description:\n 'Returns the DOM tree via CDP DOM.getDocument. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'take_snapshot',\n description:\n 'Captures a serialized page snapshot via CDP DOMSnapshot.captureSnapshot. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_console_messages',\n description:\n 'Lists console messages captured via CDP Runtime.consoleAPICalled. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_network_requests',\n description:\n 'Lists network requests captured via CDP Network events. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: { type: 'object', properties: {}, required: [] },\n availableIn: 'both' as ToolAvailability,\n },\n {\n name: 'list_exceptions',\n description:\n 'Lists JS exceptions captured via CDP Runtime.exceptionThrown. ' +\n 'NOT available in dev-mode (no CDP connection). ' +\n 'Switch to `--mode=local` or `--mode=debug`.',\n inputSchema: {\n type: 'object',\n properties: {\n limit: { type: 'number', description: 'Maximum exceptions to return.' },\n },\n required: [],\n },\n availableIn: 'both' as ToolAvailability,\n },\n] as const;\n\n/** All tool names served in dev-mode (including tier-filter stubs). */\nconst DEV_TOOL_NAMES = new Set<string>(DEV_TOOL_DEFINITIONS.map((t) => t.name));\n\n/** CDP-only tools — return a tier-filter error in dev-mode. */\nconst CDP_ONLY_TOOL_NAMES = new Set<string>([\n 'evaluate',\n 'take_screenshot',\n 'get_dom_document',\n 'take_snapshot',\n 'list_console_messages',\n 'list_network_requests',\n 'list_exceptions',\n]);\n\n/**\n * Tier B tools — relay-only per RFC #277.\n * Listed in dev-mode tool surface (issue #323) so agents get a hand-off hint\n * toward `--mode=debug` instead of \"Unknown tool\".\n */\nconst TIER_B_TOOL_NAMES = new Set<string>(['build_attach_url']);\n\nexport interface CreateDevServerDeps {\n /** AIT source for the dev tools. Defaults to an HTTP source over the dev server. */\n aitSource?: AitSource;\n}\n\n/**\n * Builds the `list_pages` dev-mode shim response.\n * Returns the Vite dev URL as a single-entry page list with `devMode: true`.\n */\nfunction buildDevListPagesResult(devtoolsUrl: string) {\n return {\n pages: [\n {\n url: devtoolsUrl,\n title: 'dev fixture',\n attached: true,\n },\n ],\n tunnel: { up: false },\n devMode: true,\n singleAttachModel: true,\n };\n}\n\n/**\n * Builds the `get_diagnostics` dev-mode response.\n * Probes the mock state endpoint reachability and returns server metadata.\n */\nasync function buildDevDiagnostics(\n devtoolsUrl: string,\n stateEndpoint: string,\n fetchImpl: (url: string) => Promise<Response>,\n): Promise<Record<string, unknown>> {\n let reachable = false;\n let lastFetchError: string | null = null;\n let lastFetchAt: string | null = null;\n\n try {\n const res = await fetchImpl(stateEndpoint);\n reachable = res.ok;\n lastFetchAt = new Date().toISOString();\n if (!res.ok) {\n lastFetchError = `HTTP ${res.status} ${res.statusText}`;\n }\n } catch (err) {\n lastFetchError = err instanceof Error ? err.message : String(err);\n lastFetchAt = new Date().toISOString();\n }\n\n return {\n mode: 'dev',\n devtoolsUrl,\n mcpStateEndpoint: stateEndpoint,\n mockStateEndpointReachable: reachable,\n lastFetchAt,\n lastFetchError,\n environment: {\n kind: 'mock',\n reason: 'dev-mode — Vite HTTP endpoint, no CDP connection',\n },\n nextRecommendedAction: reachable\n ? null\n : 'mock state endpoint가 응답하지 않습니다. Vite dev 서버가 `mcp: true` 옵션으로 실행 중인지 확인하고, 필요하면 dev 서버를 재시작하세요.',\n };\n}\n\n/**\n * Builds the `measure_safe_area` dev-mode response from mock state.\n * Reads `safeAreaInsets` from the AIT mock state and returns a parity-schema\n * result with `source: 'mock-vite'`.\n */\nasync function buildDevMeasureSafeArea(aitSource: AitSource): Promise<Record<string, unknown>> {\n const state = await aitSource.get('AIT.getMockState');\n const raw = state as Record<string, unknown>;\n\n // Extract safeAreaInsets from the mock state.\n const rawInsets = raw.safeAreaInsets;\n let sdkInsets: { top: number; right: number; bottom: number; left: number } | null = null;\n if (rawInsets !== null && typeof rawInsets === 'object' && !Array.isArray(rawInsets)) {\n const r = rawInsets as Record<string, unknown>;\n sdkInsets = {\n top: typeof r.top === 'number' ? r.top : 0,\n right: typeof r.right === 'number' ? r.right : 0,\n bottom: typeof r.bottom === 'number' ? r.bottom : 0,\n left: typeof r.left === 'number' ? r.left : 0,\n };\n }\n\n return {\n source: 'mock-vite',\n // CSS env() vars are not available from the server side — report zeros.\n cssEnv: { top: 0, right: 0, bottom: 0, left: 0 },\n sdkInsets,\n sdkInsetsSource: sdkInsets !== null ? 'window.__ait' : null,\n ...(sdkInsets === null\n ? { sdkInsetsError: 'window.__ait.state.safeAreaInsets not found in mock state snapshot' }\n : {}),\n // Viewport geometry is not available from server side.\n innerWidth: null,\n innerHeight: null,\n devicePixelRatio: null,\n userAgent: null,\n navBarHeight: null,\n navBarHeightSource: 'not-available-in-dev-mode',\n };\n}\n\n/**\n * Builds the `call_sdk` dev-mode response.\n *\n * Supported methods are served from the mock state snapshot. Unsupported\n * methods return `{ ok: false, error: 'dev-mode-unsupported: ...' }` so the\n * agent gets an informative message rather than a generic failure.\n */\nasync function buildDevCallSdk(\n methodName: string,\n aitSource: AitSource,\n): Promise<Record<string, unknown>> {\n switch (methodName) {\n case 'getOperationalEnvironment': {\n const env = await aitSource.get('AIT.getOperationalEnvironment');\n return {\n ok: true,\n value: {\n environment: env.environment,\n sdkVersion: env.sdkVersion,\n },\n };\n }\n default: {\n // For methods not readable from mock state, return a structured error.\n return {\n ok: false,\n error:\n `dev-mode-unsupported: \"${methodName}\"은 dev-mode에서 직접 호출할 수 없습니다. ` +\n 'CDP bridge(window.__sdkCall)가 없으므로 실제 SDK 호출은 `--mode=local` 또는 ' +\n 'debug 모드에서만 가능합니다. ' +\n '지원 메서드: getOperationalEnvironment (mock state에서 읽음).',\n };\n }\n }\n}\n\n/** Builds the dev-mode MCP server (does not connect a transport). */\nexport function createDevServer(deps: CreateDevServerDeps = {}): Server {\n const devtoolsUrl = process.env.AIT_DEVTOOLS_URL ?? 'http://localhost:5173';\n const stateEndpoint = `${devtoolsUrl}/api/ait-devtools/state`;\n const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });\n\n const server = new Server(\n { name: 'ait-devtools', version: __VERSION__ },\n { capabilities: { tools: {} } },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, () => ({\n tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })),\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const name = request.params.name;\n if (!DEV_TOOL_NAMES.has(name)) {\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n\n // CDP-only tools — tier-filter error with mode-switch hint.\n if (CDP_ONLY_TOOL_NAMES.has(name)) {\n return mcpError(`${name}: ${CDP_UNAVAILABLE_IN_DEV_MODE}`);\n }\n\n // Tier B tools (relay-only) — return a tier-filter error with a hand-off\n // hint toward --mode=debug so the station 2 → 3 seam is explicit.\n // (issue #323, Option B: list Tier B in dev tools/list + reject on call)\n if (TIER_B_TOOL_NAMES.has(name)) {\n return tierRejectionError(\n name,\n 'relay',\n 'mock',\n 'dev-mode — Vite HTTP endpoint, no CDP/relay connection. ' +\n '`--mode=debug` (または `devtools-mcp` without --mode) + MCP_ENV=relay로 재시작하세요.',\n );\n }\n\n try {\n // `devtools_get_mock_state` is an alias of `AIT.getMockState`.\n const effective = name === 'devtools_get_mock_state' ? 'AIT.getMockState' : name;\n\n // AIT.* tools backed by HTTP mock-state endpoint.\n if (isAitToolName(effective)) {\n switch (effective) {\n case 'AIT.getMockState':\n return jsonResult(await getMockState(aitSource));\n case 'AIT.getOperationalEnvironment':\n return jsonResult(await getOperationalEnvironment(aitSource));\n case 'AIT.getSdkCallHistory':\n return jsonResult(await getSdkCallHistory(aitSource));\n default:\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n }\n\n // Unified-surface tools (issue #305 shims).\n // Responses are wrapped in ToolEnvelope (issue #322) so agents use the\n // same {ok, data, meta} parser regardless of dev vs debug mode.\n switch (name) {\n case 'list_pages':\n return envelopeResult('list_pages', buildDevListPagesResult(devtoolsUrl));\n\n case 'get_diagnostics':\n return envelopeResult(\n 'get_diagnostics',\n await buildDevDiagnostics(devtoolsUrl, stateEndpoint, (url) => fetch(url)),\n );\n\n case 'measure_safe_area':\n return envelopeResult('measure_safe_area', await buildDevMeasureSafeArea(aitSource));\n\n case 'call_sdk': {\n const sdkName = request.params.arguments?.name;\n if (typeof sdkName !== 'string' || sdkName === '') {\n return mcpError(\n 'call_sdk: name 인자가 비어 있습니다. 호출할 메서드 이름을 전달하세요.',\n );\n }\n return envelopeResult('call_sdk', await buildDevCallSdk(sdkName, aitSource));\n }\n\n default:\n return mcpError(`알 수 없는 tool: ${name}`);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return mcpError(\n `${name} 실패: ${message}\\n` +\n 'Vite dev 서버가 @ait-co/devtools unplugin `mcp: true` 옵션으로 실행 중인지 확인하세요. ' +\n 'AIT_DEVTOOLS_URL 환경변수가 올바르게 설정됐는지도 확인하세요.',\n );\n }\n });\n\n return server;\n}\n\nfunction jsonResult(value: unknown) {\n return { content: [{ type: 'text' as const, text: JSON.stringify(value, null, 2) }] };\n}\n\n/**\n * Wraps `value` in a `ToolEnvelope` (when compat mode is off) and returns it\n * as a text content block. In dev-mode `env` is always `'mock'` and\n * `attached` is always `true` (the Vite dev server is the single implicit\n * \"attached\" page).\n *\n * When `AIT_MCP_COMPAT=chrome-devtools` the envelope is skipped and the raw\n * value is returned — identical to `jsonResult` (0.1.x back-compat).\n */\nfunction envelopeResult(tool: string, value: unknown) {\n const wrapped = wrapEnvelope(value, { tool, env: 'mock', attached: true });\n return { content: [{ type: 'text' as const, text: JSON.stringify(wrapped, null, 2) }] };\n}\n\n/** Builds the dev-mode server and connects it over stdio. */\nexport async function runDevServer(): Promise<void> {\n const server = createDevServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n"],"mappings":"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"}
|