@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/cli.js
CHANGED
|
@@ -848,6 +848,51 @@ var AutoDevtoolsOpener = class {
|
|
|
848
848
|
}
|
|
849
849
|
};
|
|
850
850
|
//#endregion
|
|
851
|
+
//#region src/mcp/envelope.ts
|
|
852
|
+
/**
|
|
853
|
+
* Returns `true` when `AIT_MCP_COMPAT=chrome-devtools` is set, which bypasses
|
|
854
|
+
* envelope wrapping and returns raw payloads (0.1.x back-compat).
|
|
855
|
+
*/
|
|
856
|
+
function isCompatMode() {
|
|
857
|
+
return process.env.AIT_MCP_COMPAT === "chrome-devtools";
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Maps `McpEnvironment` to `EnvelopeEnv`. After #307 these are the same
|
|
861
|
+
* union (`mock | relay-dev | relay-live`), so this is identity — kept as a
|
|
862
|
+
* named export for surface stability if envelope env diverges in the future.
|
|
863
|
+
*/
|
|
864
|
+
function toEnvelopeEnv(env) {
|
|
865
|
+
return env;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Wraps `data` in a `ToolEnvelope<T>` **unless** compat mode is active, in
|
|
869
|
+
* which case `data` is returned as-is.
|
|
870
|
+
*
|
|
871
|
+
* Use this at every tool call-site in `debug-server.ts` and `server.ts`.
|
|
872
|
+
*
|
|
873
|
+
* @example
|
|
874
|
+
* ```ts
|
|
875
|
+
* return jsonResult(wrapEnvelope(listPages(connection, tunnel), {
|
|
876
|
+
* tool: 'list_pages',
|
|
877
|
+
* env: resolveEnvironment(),
|
|
878
|
+
* attached: connection.listTargets().length > 0,
|
|
879
|
+
* }));
|
|
880
|
+
* ```
|
|
881
|
+
*/
|
|
882
|
+
function wrapEnvelope(data, ctx) {
|
|
883
|
+
if (isCompatMode()) return data;
|
|
884
|
+
return {
|
|
885
|
+
ok: true,
|
|
886
|
+
data,
|
|
887
|
+
meta: {
|
|
888
|
+
tool: ctx.tool,
|
|
889
|
+
env: toEnvelopeEnv(ctx.env),
|
|
890
|
+
attached: ctx.attached,
|
|
891
|
+
contentType: ctx.contentType ?? "json"
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
//#endregion
|
|
851
896
|
//#region src/mcp/environment.ts
|
|
852
897
|
/**
|
|
853
898
|
* Returns `true` when the environment is any relay variant (`relay-dev` or
|
|
@@ -2013,6 +2058,83 @@ function warnPassthrough(name) {
|
|
|
2013
2058
|
}
|
|
2014
2059
|
SIGNATURES.map((s) => s.name);
|
|
2015
2060
|
//#endregion
|
|
2061
|
+
//#region src/mcp/totp.ts
|
|
2062
|
+
/**
|
|
2063
|
+
* RFC 6238 TOTP implementation (Node.js, node:crypto only).
|
|
2064
|
+
*
|
|
2065
|
+
* External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
|
|
2066
|
+
* to keep the dependency surface minimal. This hand-roll is ~30 lines and
|
|
2067
|
+
* covers exactly what relay-side auth needs.
|
|
2068
|
+
*
|
|
2069
|
+
* Algorithm summary (RFC 6238 + RFC 4226):
|
|
2070
|
+
* T = floor(now / 30) — 30-second time step counter
|
|
2071
|
+
* K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
|
|
2072
|
+
* MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
|
|
2073
|
+
* offset = MAC[19] & 0x0f
|
|
2074
|
+
* code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
|
|
2075
|
+
*
|
|
2076
|
+
* Security note (keep this comment accurate):
|
|
2077
|
+
* The baked-in secret in a dogfood build is extractable from the bundle by a
|
|
2078
|
+
* determined reverse engineer. This mechanism raises the bar from
|
|
2079
|
+
* "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
|
|
2080
|
+
* Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
|
|
2081
|
+
* blocked; deliberate reverse engineering is not. See threat model in
|
|
2082
|
+
* src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
|
|
2083
|
+
*
|
|
2084
|
+
* SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
|
|
2085
|
+
* log, error message, or string visible outside this module. Only boolean
|
|
2086
|
+
* pass/fail and reason enum values are safe to surface.
|
|
2087
|
+
*/
|
|
2088
|
+
/** Time step window in seconds (RFC 6238 default). */
|
|
2089
|
+
const TIME_STEP = 30;
|
|
2090
|
+
/** Number of digits in the generated code. */
|
|
2091
|
+
const DIGITS = 6;
|
|
2092
|
+
/**
|
|
2093
|
+
* Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
|
|
2094
|
+
* clock time.
|
|
2095
|
+
*
|
|
2096
|
+
* @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
|
|
2097
|
+
* bytes). Must be the output of `generateAttachToken()` or compatible.
|
|
2098
|
+
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
2099
|
+
* @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
|
|
2100
|
+
*/
|
|
2101
|
+
function generateTotp(secret, when = Date.now()) {
|
|
2102
|
+
const key = Buffer.from(secret, "hex");
|
|
2103
|
+
const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
|
|
2104
|
+
const counterBuf = Buffer.alloc(8);
|
|
2105
|
+
const hi = Math.floor(counter / 4294967296);
|
|
2106
|
+
const lo = counter >>> 0;
|
|
2107
|
+
counterBuf.writeUInt32BE(hi, 0);
|
|
2108
|
+
counterBuf.writeUInt32BE(lo, 4);
|
|
2109
|
+
const mac = createHmac("sha1", key).update(counterBuf).digest();
|
|
2110
|
+
const offset = mac[19] & 15;
|
|
2111
|
+
return (((mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255) % 10 ** DIGITS).toString().padStart(DIGITS, "0");
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Verifies a TOTP code against the secret, accepting ±`skew` time steps to
|
|
2115
|
+
* tolerate clock drift between the relay host and the client device.
|
|
2116
|
+
*
|
|
2117
|
+
* Uses `timingSafeEqual` for constant-time comparison to prevent timing
|
|
2118
|
+
* side-channel attacks.
|
|
2119
|
+
*
|
|
2120
|
+
* @param secret - Hex-encoded shared secret.
|
|
2121
|
+
* @param code - The 6-digit code to verify (string or numeric).
|
|
2122
|
+
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
2123
|
+
* @param skew - Number of adjacent steps to accept on either side. Default 1
|
|
2124
|
+
* (accepts T-1, T, T+1 — a 90-second acceptance window).
|
|
2125
|
+
* @returns `true` if the code matches any accepted step, `false` otherwise.
|
|
2126
|
+
*/
|
|
2127
|
+
function verifyTotp(secret, code, when = Date.now(), skew = 1) {
|
|
2128
|
+
const normalised = String(code).padStart(DIGITS, "0");
|
|
2129
|
+
if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
|
|
2130
|
+
const candidateBuf = Buffer.from(normalised, "utf8");
|
|
2131
|
+
for (let delta = -skew; delta <= skew; delta++) {
|
|
2132
|
+
const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
|
|
2133
|
+
if (timingSafeEqual(Buffer.from(expected, "utf8"), candidateBuf)) return true;
|
|
2134
|
+
}
|
|
2135
|
+
return false;
|
|
2136
|
+
}
|
|
2137
|
+
//#endregion
|
|
2016
2138
|
//#region src/mcp/tools.ts
|
|
2017
2139
|
/** Static MCP tool descriptors (name + JSONSchema) for the full debug tool surface. */
|
|
2018
2140
|
const DEBUG_TOOL_DEFINITIONS = [
|
|
@@ -2048,7 +2170,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
2048
2170
|
},
|
|
2049
2171
|
{
|
|
2050
2172
|
name: "build_attach_url",
|
|
2051
|
-
description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Turns an `ait deploy --scheme-only` URL (intoss-private://…?_deploymentId=<uuid>) into a self-attaching deep link by splicing in debug=1 and the live relay URL for this session. Returns the deep link JSON and a unicode QR of that deep link. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Requires the tunnel to be up — call list_pages first. If the tunnel is not up, restart the MCP server: `npx @ait-co/devtools devtools-mcp`. Set wait_for_attach=true to block until the phone scans and a page attaches (polls listTargets up to 30 s by default), then returns the attached page info too. On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). Requires MCP_ENV=relay (set automatically when
|
|
2173
|
+
description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Turns an `ait deploy --scheme-only` URL (intoss-private://…?_deploymentId=<uuid>) into a self-attaching deep link by splicing in debug=1 and the live relay URL for this session. Returns the deep link JSON and a unicode QR of that deep link. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Requires the tunnel to be up — call list_pages first. If the tunnel is not up, restart the MCP server: `npx @ait-co/devtools devtools-mcp`. Set wait_for_attach=true to block until the phone scans and a page attaches (polls listTargets up to 30 s by default), then returns the attached page info too. On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). Requires MCP_ENV=relay-dev or relay-live (set automatically in debug-mode default).\n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>) — the URL is single-use for that 30-second step. The response includes a `totp` field with `expiresAt` (ISO timestamp). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh one-time URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.",
|
|
2052
2174
|
inputSchema: {
|
|
2053
2175
|
type: "object",
|
|
2054
2176
|
properties: {
|
|
@@ -2101,7 +2223,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
2101
2223
|
},
|
|
2102
2224
|
{
|
|
2103
2225
|
name: "measure_safe_area",
|
|
2104
|
-
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay\"` field so consumers can identify provenance without inspecting payload values. Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires a page to be attached — call list_pages first.",
|
|
2226
|
+
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-live\"` field so consumers can identify provenance without inspecting payload values. Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires a page to be attached — call list_pages first.",
|
|
2105
2227
|
inputSchema: {
|
|
2106
2228
|
type: "object",
|
|
2107
2229
|
properties: {},
|
|
@@ -2143,7 +2265,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
2143
2265
|
},
|
|
2144
2266
|
{
|
|
2145
2267
|
name: "call_sdk",
|
|
2146
|
-
description: "Calls a dogfood SDK method via the window.__sdkCall bridge (exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). On env
|
|
2268
|
+
description: "Calls a dogfood SDK method via the window.__sdkCall bridge (exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). On env 3/4 (real device relay) this hits the real SDK; on env 1 (local mock) it hits the mock SDK. (env 2 PWA does not inject the SDK — call_sdk is not available there.) Requires the relay to be attached — call list_pages first. Returns {ok: true, value} on success or {ok: false, error} on failure. If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], the result also includes `recentException` for crash triage. Returns a clear error if window.__sdkCall is not available (non-dogfood bundle) — redeploy via dogfood channel: `ait build && aitcc app deploy`.\n\nSECURITY: method name, args, and result value are not redacted — never include secrets.\n\nLIVE guard: when running against a live/production relay (relay-live env, MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that the SDK call may affect real users. Without it the call is rejected with a structured error. mock and relay-dev sessions are unaffected.\n\nIMPORTANT — 인자 시그니처 (잘못된 인자로 호출하면 토스 앱 crash 위험):\n setDeviceOrientation: call_sdk(\"setDeviceOrientation\", [{ type: \"landscape\" }]) // NOT \"landscape\"\n setIosSwipeGestureEnabled: call_sdk(\"setIosSwipeGestureEnabled\", [{ isEnabled: false }])\n setSecureScreen: call_sdk(\"setSecureScreen\", [{ enabled: true }])\n setScreenAwakeMode: call_sdk(\"setScreenAwakeMode\", [{ enabled: true }])\n getOperationalEnvironment: call_sdk(\"getOperationalEnvironment\", [])\n getPlatformOS: call_sdk(\"getPlatformOS\", [])\n getDeviceId: call_sdk(\"getDeviceId\", [])\n getLocale: call_sdk(\"getLocale\", [])\n getNetworkStatus: call_sdk(\"getNetworkStatus\", [])\n getSchemeUri: call_sdk(\"getSchemeUri\", [])\n requestReview: call_sdk(\"requestReview\", [])\n closeView: call_sdk(\"closeView\", [])",
|
|
2147
2269
|
inputSchema: {
|
|
2148
2270
|
type: "object",
|
|
2149
2271
|
properties: {
|
|
@@ -2197,7 +2319,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
2197
2319
|
},
|
|
2198
2320
|
{
|
|
2199
2321
|
name: "get_diagnostics",
|
|
2200
|
-
description: "Returns a single-call server status snapshot so the agent can diagnose \"why is this not working?\" without calling multiple tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), environment (kind: mock|relay-dev|relay-live, env: mock|relay backward-compat, reason, liveGuardActive: true when relay-live LIVE guard is active), serverLockHolder (pid + startedAt from the lock file, or null), nextRecommendedAction ({tool, reason} or null — the single next tool to call). All fields are nullable — missing data is null, not an error. debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. Tier C (both mock and relay). Call this first when debugging session state.",
|
|
2322
|
+
description: "Returns a single-call server status snapshot so the agent can diagnose \"why is this not working?\" without calling multiple tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), environment (kind: mock|relay-dev|relay-live, env: mock|relay backward-compat, reason, liveGuardActive: true when relay-live LIVE guard is active), serverLockHolder (pid + startedAt from the lock file, or null), nextRecommendedAction ({tool, reason} or null — the single next tool to call; in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). All fields are nullable — missing data is null, not an error. debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. Tier C (both mock and relay). Call this first when debugging session state.",
|
|
2201
2323
|
inputSchema: {
|
|
2202
2324
|
type: "object",
|
|
2203
2325
|
properties: { recent_errors_limit: {
|
|
@@ -2364,20 +2486,50 @@ function listPages(connection, tunnel) {
|
|
|
2364
2486
|
* URL plus this session's live relay. Throws if the tunnel is not up yet (no
|
|
2365
2487
|
* relay URL to splice in) — the caller surfaces that as a tool error.
|
|
2366
2488
|
*
|
|
2489
|
+
* When `AIT_DEBUG_TOTP_SECRET` is set, generates the current TOTP code and
|
|
2490
|
+
* splices it as `at=<code>` into the attach URL. The code is valid for one
|
|
2491
|
+
* 30-second time step (±1 skew accepted by the relay, so the effective window
|
|
2492
|
+
* is up to 90 s). If the scan happens after `totp.expiresAt`, call
|
|
2493
|
+
* `build_attach_url` again to get a fresh code.
|
|
2494
|
+
*
|
|
2367
2495
|
* Also validates the scheme URL's authority. A suspicious authority (empty,
|
|
2368
2496
|
* "web", "localhost", etc.) is surfaced as a non-fatal `authorityWarning` on
|
|
2369
2497
|
* the result so the caller can show a helpful hint without blocking the link
|
|
2370
2498
|
* generation (the warning is consistent with how other validation in
|
|
2371
2499
|
* `buildDeepLinkAttachUrl` works — hard errors for relay, soft warning for
|
|
2372
2500
|
* the scheme authority which is in the caller's input, not ours to own).
|
|
2501
|
+
*
|
|
2502
|
+
* SECRET-HANDLING: `totpSecret` (if provided) is used only to compute a code
|
|
2503
|
+
* and must never appear in any log, error message, or output outside of the
|
|
2504
|
+
* spliced `at=` param in `attachUrl`.
|
|
2505
|
+
*
|
|
2506
|
+
* @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL.
|
|
2507
|
+
* @param tunnel - Current tunnel status from the running debug server.
|
|
2508
|
+
* @param totpSecret - Optional hex-encoded TOTP secret (from
|
|
2509
|
+
* `AIT_DEBUG_TOTP_SECRET`). When provided, the current code is spliced into
|
|
2510
|
+
* the attach URL as `at=<code>`.
|
|
2373
2511
|
*/
|
|
2374
|
-
function buildAttachUrl(schemeUrl, tunnel) {
|
|
2512
|
+
function buildAttachUrl(schemeUrl, tunnel, totpSecret) {
|
|
2375
2513
|
if (!tunnel.up || tunnel.wssUrl === null) throw new Error("tunnel-down: cloudflared 터널이 안 떠 있습니다. MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.");
|
|
2376
2514
|
const authorityWarning = validateSchemeAuthority(schemeUrl) ?? void 0;
|
|
2515
|
+
let totpCode;
|
|
2516
|
+
let totpMeta;
|
|
2517
|
+
if (totpSecret !== void 0 && totpSecret !== "") {
|
|
2518
|
+
const now = Date.now();
|
|
2519
|
+
totpCode = generateTotp(totpSecret, now);
|
|
2520
|
+
const STEP_SECONDS = 30;
|
|
2521
|
+
const expiresAtMs = (Math.floor(now / 1e3 / STEP_SECONDS) + 1) * STEP_SECONDS * 1e3;
|
|
2522
|
+
totpMeta = {
|
|
2523
|
+
enabled: true,
|
|
2524
|
+
ttlSeconds: STEP_SECONDS,
|
|
2525
|
+
expiresAt: new Date(expiresAtMs).toISOString()
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2377
2528
|
return {
|
|
2378
|
-
attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl),
|
|
2529
|
+
attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl, totpCode),
|
|
2379
2530
|
relayUrl: tunnel.wssUrl,
|
|
2380
|
-
...authorityWarning !== void 0 ? { authorityWarning } : {}
|
|
2531
|
+
...authorityWarning !== void 0 ? { authorityWarning } : {},
|
|
2532
|
+
...totpMeta !== void 0 ? { totp: totpMeta } : {}
|
|
2381
2533
|
};
|
|
2382
2534
|
}
|
|
2383
2535
|
/**
|
|
@@ -2996,7 +3148,8 @@ function readDevtoolsVersion() {
|
|
|
2996
3148
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
2997
3149
|
*
|
|
2998
3150
|
* Branch rules (evaluated in priority order):
|
|
2999
|
-
* 1. tunnel.up === false
|
|
3151
|
+
* 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)
|
|
3152
|
+
* 1b. tunnel.up === false AND env is mock → wait_for_page (local target: tunnel-less is normal)
|
|
3000
3153
|
* 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)
|
|
3001
3154
|
* 3. pages has entry + crashDetectedAt non-null → build_attach_url (re-attach after crash)
|
|
3002
3155
|
* 4. otherwise → null (session looks healthy)
|
|
@@ -3004,7 +3157,12 @@ function readDevtoolsVersion() {
|
|
|
3004
3157
|
* Pure — does not throw; receives the final assembled snapshot fields.
|
|
3005
3158
|
*/
|
|
3006
3159
|
function computeNextRecommendedAction(tunnel, pages, env) {
|
|
3007
|
-
if (!tunnel.up)
|
|
3160
|
+
if (!tunnel.up) if (!isRelayEnv(env)) {
|
|
3161
|
+
if (pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
|
|
3162
|
+
tool: "wait_for_page",
|
|
3163
|
+
reason: "local Chromium spawn 직후 — 페이지 로드를 기다리거나 list_pages를 재호출하세요 (local 모드는 tunnel이 없는 게 정상입니다)"
|
|
3164
|
+
};
|
|
3165
|
+
} else return {
|
|
3008
3166
|
tool: "restart",
|
|
3009
3167
|
reason: "tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart"
|
|
3010
3168
|
};
|
|
@@ -3069,83 +3227,6 @@ async function getDiagnostics(input) {
|
|
|
3069
3227
|
};
|
|
3070
3228
|
}
|
|
3071
3229
|
//#endregion
|
|
3072
|
-
//#region src/mcp/totp.ts
|
|
3073
|
-
/**
|
|
3074
|
-
* RFC 6238 TOTP implementation (Node.js, node:crypto only).
|
|
3075
|
-
*
|
|
3076
|
-
* External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
|
|
3077
|
-
* to keep the dependency surface minimal. This hand-roll is ~30 lines and
|
|
3078
|
-
* covers exactly what relay-side auth needs.
|
|
3079
|
-
*
|
|
3080
|
-
* Algorithm summary (RFC 6238 + RFC 4226):
|
|
3081
|
-
* T = floor(now / 30) — 30-second time step counter
|
|
3082
|
-
* K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
|
|
3083
|
-
* MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
|
|
3084
|
-
* offset = MAC[19] & 0x0f
|
|
3085
|
-
* code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
|
|
3086
|
-
*
|
|
3087
|
-
* Security note (keep this comment accurate):
|
|
3088
|
-
* The baked-in secret in a dogfood build is extractable from the bundle by a
|
|
3089
|
-
* determined reverse engineer. This mechanism raises the bar from
|
|
3090
|
-
* "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
|
|
3091
|
-
* Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
|
|
3092
|
-
* blocked; deliberate reverse engineering is not. See threat model in
|
|
3093
|
-
* src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
|
|
3094
|
-
*
|
|
3095
|
-
* SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
|
|
3096
|
-
* log, error message, or string visible outside this module. Only boolean
|
|
3097
|
-
* pass/fail and reason enum values are safe to surface.
|
|
3098
|
-
*/
|
|
3099
|
-
/** Time step window in seconds (RFC 6238 default). */
|
|
3100
|
-
const TIME_STEP = 30;
|
|
3101
|
-
/** Number of digits in the generated code. */
|
|
3102
|
-
const DIGITS = 6;
|
|
3103
|
-
/**
|
|
3104
|
-
* Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
|
|
3105
|
-
* clock time.
|
|
3106
|
-
*
|
|
3107
|
-
* @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
|
|
3108
|
-
* bytes). Must be the output of `generateAttachToken()` or compatible.
|
|
3109
|
-
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
3110
|
-
* @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
|
|
3111
|
-
*/
|
|
3112
|
-
function generateTotp(secret, when = Date.now()) {
|
|
3113
|
-
const key = Buffer.from(secret, "hex");
|
|
3114
|
-
const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
|
|
3115
|
-
const counterBuf = Buffer.alloc(8);
|
|
3116
|
-
const hi = Math.floor(counter / 4294967296);
|
|
3117
|
-
const lo = counter >>> 0;
|
|
3118
|
-
counterBuf.writeUInt32BE(hi, 0);
|
|
3119
|
-
counterBuf.writeUInt32BE(lo, 4);
|
|
3120
|
-
const mac = createHmac("sha1", key).update(counterBuf).digest();
|
|
3121
|
-
const offset = mac[19] & 15;
|
|
3122
|
-
return (((mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255) % 10 ** DIGITS).toString().padStart(DIGITS, "0");
|
|
3123
|
-
}
|
|
3124
|
-
/**
|
|
3125
|
-
* Verifies a TOTP code against the secret, accepting ±`skew` time steps to
|
|
3126
|
-
* tolerate clock drift between the relay host and the client device.
|
|
3127
|
-
*
|
|
3128
|
-
* Uses `timingSafeEqual` for constant-time comparison to prevent timing
|
|
3129
|
-
* side-channel attacks.
|
|
3130
|
-
*
|
|
3131
|
-
* @param secret - Hex-encoded shared secret.
|
|
3132
|
-
* @param code - The 6-digit code to verify (string or numeric).
|
|
3133
|
-
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
3134
|
-
* @param skew - Number of adjacent steps to accept on either side. Default 1
|
|
3135
|
-
* (accepts T-1, T, T+1 — a 90-second acceptance window).
|
|
3136
|
-
* @returns `true` if the code matches any accepted step, `false` otherwise.
|
|
3137
|
-
*/
|
|
3138
|
-
function verifyTotp(secret, code, when = Date.now(), skew = 1) {
|
|
3139
|
-
const normalised = String(code).padStart(DIGITS, "0");
|
|
3140
|
-
if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
|
|
3141
|
-
const candidateBuf = Buffer.from(normalised, "utf8");
|
|
3142
|
-
for (let delta = -skew; delta <= skew; delta++) {
|
|
3143
|
-
const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
|
|
3144
|
-
if (timingSafeEqual(Buffer.from(expected, "utf8"), candidateBuf)) return true;
|
|
3145
|
-
}
|
|
3146
|
-
return false;
|
|
3147
|
-
}
|
|
3148
|
-
//#endregion
|
|
3149
3230
|
//#region src/mcp/tunnel.ts
|
|
3150
3231
|
/**
|
|
3151
3232
|
* cloudflared quick tunnel + attach banner for the debug-mode MCP server.
|
|
@@ -3518,7 +3599,7 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
|
|
|
3518
3599
|
* naturally via `enableDomains`). The tier only controls visibility.
|
|
3519
3600
|
*/
|
|
3520
3601
|
function createDebugServer(deps) {
|
|
3521
|
-
const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, defaultEnv } = deps;
|
|
3602
|
+
const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, defaultEnv, totpSecret } = deps;
|
|
3522
3603
|
const resolveEnvironment = getEnvDep ?? (() => getEnvironment({
|
|
3523
3604
|
connection,
|
|
3524
3605
|
defaultEnv
|
|
@@ -3530,7 +3611,7 @@ function createDebugServer(deps) {
|
|
|
3530
3611
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
3531
3612
|
const server = new Server({
|
|
3532
3613
|
name: "ait-debug",
|
|
3533
|
-
version: "0.1.
|
|
3614
|
+
version: "0.1.48"
|
|
3534
3615
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
3535
3616
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
3536
3617
|
const env = resolveEnvironment();
|
|
@@ -3574,7 +3655,7 @@ function createDebugServer(deps) {
|
|
|
3574
3655
|
if (name === "get_diagnostics") try {
|
|
3575
3656
|
const rawLimit = request.params.arguments?.recent_errors_limit;
|
|
3576
3657
|
const recentErrorsLimit = typeof rawLimit === "number" && rawLimit > 0 ? rawLimit : 10;
|
|
3577
|
-
|
|
3658
|
+
const result = await getDiagnostics({
|
|
3578
3659
|
tunnel: getTunnelStatus(),
|
|
3579
3660
|
connection,
|
|
3580
3661
|
env: resolveEnvironment(),
|
|
@@ -3582,7 +3663,9 @@ function createDebugServer(deps) {
|
|
|
3582
3663
|
collector,
|
|
3583
3664
|
readLock: readServerLock,
|
|
3584
3665
|
recentErrorsLimit
|
|
3585
|
-
})
|
|
3666
|
+
});
|
|
3667
|
+
const attached = connection.listTargets().length > 0;
|
|
3668
|
+
return envelopeResult$1(result, name, resolveEnvironment(), attached);
|
|
3586
3669
|
} catch (err) {
|
|
3587
3670
|
return errorResult(err, name);
|
|
3588
3671
|
}
|
|
@@ -3609,7 +3692,7 @@ function createDebugServer(deps) {
|
|
|
3609
3692
|
return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
|
|
3610
3693
|
};
|
|
3611
3694
|
try {
|
|
3612
|
-
const { attachUrl, relayUrl, authorityWarning } = buildAttachUrl(schemeUrl, getTunnelStatus());
|
|
3695
|
+
const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(schemeUrl, getTunnelStatus(), totpSecret);
|
|
3613
3696
|
const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
|
|
3614
3697
|
const header = "This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).";
|
|
3615
3698
|
const guiAvailable = canOpenBrowser();
|
|
@@ -3618,7 +3701,8 @@ function createDebugServer(deps) {
|
|
|
3618
3701
|
const qrHeadless = await renderQr(attachUrl);
|
|
3619
3702
|
const headlessText = `${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
|
|
3620
3703
|
attachUrl,
|
|
3621
|
-
relayUrl
|
|
3704
|
+
relayUrl,
|
|
3705
|
+
...totp ? { totp } : {}
|
|
3622
3706
|
}, null, 2)}\n\n${qrHeadless}`;
|
|
3623
3707
|
if (!waitForAttach) return { content: [{
|
|
3624
3708
|
type: "text",
|
|
@@ -3654,7 +3738,8 @@ function createDebugServer(deps) {
|
|
|
3654
3738
|
};
|
|
3655
3739
|
const shortText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
3656
3740
|
relayUrl,
|
|
3657
|
-
openResult
|
|
3741
|
+
openResult,
|
|
3742
|
+
...totp ? { totp } : {}
|
|
3658
3743
|
}, null, 2)}\n\n브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`;
|
|
3659
3744
|
if (!waitForAttach) return { content: [{
|
|
3660
3745
|
type: "text",
|
|
@@ -3693,7 +3778,8 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3693
3778
|
const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
|
|
3694
3779
|
attachUrl,
|
|
3695
3780
|
relayUrl,
|
|
3696
|
-
openResult
|
|
3781
|
+
openResult,
|
|
3782
|
+
...totp ? { totp } : {}
|
|
3697
3783
|
}, null, 2)}\n\n${qr}`;
|
|
3698
3784
|
if (!waitForAttach) return { content: [{
|
|
3699
3785
|
type: "text",
|
|
@@ -3721,7 +3807,8 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3721
3807
|
const qr = await renderQr(attachUrl);
|
|
3722
3808
|
const baseText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
3723
3809
|
attachUrl,
|
|
3724
|
-
relayUrl
|
|
3810
|
+
relayUrl,
|
|
3811
|
+
...totp ? { totp } : {}
|
|
3725
3812
|
}, null, 2)}\n\n${qr}`;
|
|
3726
3813
|
if (!waitForAttach) return { content: [{
|
|
3727
3814
|
type: "text",
|
|
@@ -3756,7 +3843,9 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3756
3843
|
if (connection instanceof ChiiCdpConnection) try {
|
|
3757
3844
|
await connection.refreshTargets();
|
|
3758
3845
|
} catch {}
|
|
3759
|
-
|
|
3846
|
+
const pagesData = listPages(connection, getTunnelStatus());
|
|
3847
|
+
const attached = connection.listTargets().length > 0;
|
|
3848
|
+
return envelopeResult$1(pagesData, name, resolveEnvironment(), attached);
|
|
3760
3849
|
}
|
|
3761
3850
|
return classifyEnableDomainError(err, name);
|
|
3762
3851
|
}
|
|
@@ -3768,11 +3857,14 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3768
3857
|
return jsonResult$1({ exceptions: listExceptions(connection, typeof rawLimit === "number" && rawLimit > 0 ? rawLimit : 50) });
|
|
3769
3858
|
}
|
|
3770
3859
|
case "list_network_requests": return jsonResult$1(listNetworkRequests(connection));
|
|
3771
|
-
case "list_pages":
|
|
3860
|
+
case "list_pages": {
|
|
3772
3861
|
if (connection instanceof ChiiCdpConnection) try {
|
|
3773
3862
|
await connection.refreshTargets();
|
|
3774
3863
|
} catch {}
|
|
3775
|
-
|
|
3864
|
+
const listPagesData = listPages(connection, getTunnelStatus());
|
|
3865
|
+
const listPagesAttached = connection.listTargets().length > 0;
|
|
3866
|
+
return envelopeResult$1(listPagesData, name, resolveEnvironment(), listPagesAttached);
|
|
3867
|
+
}
|
|
3776
3868
|
case "get_dom_document": return jsonResult$1(await getDomDocument(connection));
|
|
3777
3869
|
case "take_snapshot": return jsonResult$1(await takeSnapshot(connection));
|
|
3778
3870
|
case "take_screenshot": {
|
|
@@ -3783,7 +3875,11 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3783
3875
|
mimeType: shot.mimeType
|
|
3784
3876
|
}] };
|
|
3785
3877
|
}
|
|
3786
|
-
case "measure_safe_area":
|
|
3878
|
+
case "measure_safe_area": {
|
|
3879
|
+
const safeAreaData = await measureSafeArea(connection, resolveEnvironment());
|
|
3880
|
+
const safeAreaAttached = connection.listTargets().length > 0;
|
|
3881
|
+
return envelopeResult$1(safeAreaData, name, resolveEnvironment(), safeAreaAttached);
|
|
3882
|
+
}
|
|
3787
3883
|
case "evaluate": {
|
|
3788
3884
|
const expression = request.params.arguments?.expression;
|
|
3789
3885
|
if (typeof expression !== "string" || expression === "") return mcpError("evaluate: expression 인자가 비어 있습니다. 평가할 JavaScript 표현식을 전달하세요.");
|
|
@@ -3798,7 +3894,8 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3798
3894
|
if (isLiveRelayEnv(env) && request.params.arguments?.confirm !== true) return liveGuardError("call_sdk");
|
|
3799
3895
|
const sdkResult = await callSdk(connection, sdkName, sdkArgs);
|
|
3800
3896
|
if (!sdkResult.ok && typeof sdkResult.error === "string" && sdkResult.error.startsWith("sdk-absent:")) return sdkAbsentError("call_sdk");
|
|
3801
|
-
|
|
3897
|
+
const callSdkAttached = connection.listTargets().length > 0;
|
|
3898
|
+
return envelopeResult$1(sdkResult, name, resolveEnvironment(), callSdkAttached);
|
|
3802
3899
|
}
|
|
3803
3900
|
default: return unknownTool(name);
|
|
3804
3901
|
}
|
|
@@ -3814,6 +3911,22 @@ function jsonResult$1(value) {
|
|
|
3814
3911
|
text: JSON.stringify(value, null, 2)
|
|
3815
3912
|
}] };
|
|
3816
3913
|
}
|
|
3914
|
+
/**
|
|
3915
|
+
* Wraps `value` in a `ToolEnvelope` (when compat mode is off) and returns it
|
|
3916
|
+
* as a text content block. When `AIT_MCP_COMPAT=chrome-devtools` is set the
|
|
3917
|
+
* envelope is skipped and the raw value is returned — identical to `jsonResult`.
|
|
3918
|
+
*/
|
|
3919
|
+
function envelopeResult$1(value, tool, env, attached) {
|
|
3920
|
+
const wrapped = wrapEnvelope(value, {
|
|
3921
|
+
tool,
|
|
3922
|
+
env,
|
|
3923
|
+
attached
|
|
3924
|
+
});
|
|
3925
|
+
return { content: [{
|
|
3926
|
+
type: "text",
|
|
3927
|
+
text: JSON.stringify(wrapped, null, 2)
|
|
3928
|
+
}] };
|
|
3929
|
+
}
|
|
3817
3930
|
function unknownTool(name) {
|
|
3818
3931
|
return mcpError(`알 수 없는 tool: ${name}`);
|
|
3819
3932
|
}
|
|
@@ -3975,7 +4088,8 @@ async function runDebugServer(options = {}) {
|
|
|
3975
4088
|
return qrServer;
|
|
3976
4089
|
},
|
|
3977
4090
|
diagnosticsCollector,
|
|
3978
|
-
defaultEnv: "relay-dev"
|
|
4091
|
+
defaultEnv: "relay-dev",
|
|
4092
|
+
...process.env.AIT_DEBUG_TOTP_SECRET ? { totpSecret: process.env.AIT_DEBUG_TOTP_SECRET } : {}
|
|
3979
4093
|
});
|
|
3980
4094
|
const transport = new StdioServerTransport();
|
|
3981
4095
|
let closed = false;
|
|
@@ -4311,6 +4425,29 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
4311
4425
|
},
|
|
4312
4426
|
availableIn: "both"
|
|
4313
4427
|
},
|
|
4428
|
+
{
|
|
4429
|
+
name: "build_attach_url",
|
|
4430
|
+
description: "Turns an `ait deploy --scheme-only` URL into a self-attaching deep link for a real device. NOT available in dev-mode — requires a live cloudflared relay (Tier B, relay-only). To use this tool: restart the MCP server with `--mode=debug` (or omit --mode) and set MCP_ENV=relay, then call build_attach_url to generate the QR for phone scanning. See: https://docs.aitc.dev/guides/debug-relay",
|
|
4431
|
+
inputSchema: {
|
|
4432
|
+
type: "object",
|
|
4433
|
+
properties: {
|
|
4434
|
+
scheme_url: {
|
|
4435
|
+
type: "string",
|
|
4436
|
+
description: "The intoss-private:// URL from `ait deploy --scheme-only`."
|
|
4437
|
+
},
|
|
4438
|
+
wait_for_attach: {
|
|
4439
|
+
type: "boolean",
|
|
4440
|
+
description: "If true, block until a page attaches."
|
|
4441
|
+
},
|
|
4442
|
+
open_in_browser: {
|
|
4443
|
+
type: "boolean",
|
|
4444
|
+
description: "If true (default), open the QR PNG in the OS browser."
|
|
4445
|
+
}
|
|
4446
|
+
},
|
|
4447
|
+
required: ["scheme_url"]
|
|
4448
|
+
},
|
|
4449
|
+
availableIn: "relay"
|
|
4450
|
+
},
|
|
4314
4451
|
{
|
|
4315
4452
|
name: "evaluate",
|
|
4316
4453
|
description: "Evaluates an arbitrary JavaScript expression via CDP Runtime.evaluate. NOT available in dev-mode (no CDP connection). Switch to `--mode=local` or `--mode=debug` for CDP access.",
|
|
@@ -4401,6 +4538,12 @@ const CDP_ONLY_TOOL_NAMES = new Set([
|
|
|
4401
4538
|
"list_exceptions"
|
|
4402
4539
|
]);
|
|
4403
4540
|
/**
|
|
4541
|
+
* Tier B tools — relay-only per RFC #277.
|
|
4542
|
+
* Listed in dev-mode tool surface (issue #323) so agents get a hand-off hint
|
|
4543
|
+
* toward `--mode=debug` instead of "Unknown tool".
|
|
4544
|
+
*/
|
|
4545
|
+
const TIER_B_TOOL_NAMES = new Set(["build_attach_url"]);
|
|
4546
|
+
/**
|
|
4404
4547
|
* Builds the `list_pages` dev-mode shim response.
|
|
4405
4548
|
* Returns the Vite dev URL as a single-entry page list with `devMode: true`.
|
|
4406
4549
|
*/
|
|
@@ -4515,13 +4658,14 @@ function createDevServer(deps = {}) {
|
|
|
4515
4658
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
4516
4659
|
const server = new Server({
|
|
4517
4660
|
name: "ait-devtools",
|
|
4518
|
-
version: "0.1.
|
|
4661
|
+
version: "0.1.48"
|
|
4519
4662
|
}, { capabilities: { tools: {} } });
|
|
4520
4663
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
4521
4664
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4522
4665
|
const name = request.params.name;
|
|
4523
4666
|
if (!DEV_TOOL_NAMES.has(name)) return mcpError(`알 수 없는 tool: ${name}`);
|
|
4524
4667
|
if (CDP_ONLY_TOOL_NAMES.has(name)) return mcpError(`${name}: ${CDP_UNAVAILABLE_IN_DEV_MODE}`);
|
|
4668
|
+
if (TIER_B_TOOL_NAMES.has(name)) return tierRejectionError(name, "relay", "mock", "dev-mode — Vite HTTP endpoint, no CDP/relay connection. `--mode=debug` (または `devtools-mcp` without --mode) + MCP_ENV=relay로 재시작하세요.");
|
|
4525
4669
|
try {
|
|
4526
4670
|
const effective = name === "devtools_get_mock_state" ? "AIT.getMockState" : name;
|
|
4527
4671
|
if (isAitToolName(effective)) switch (effective) {
|
|
@@ -4531,13 +4675,13 @@ function createDevServer(deps = {}) {
|
|
|
4531
4675
|
default: return mcpError(`알 수 없는 tool: ${name}`);
|
|
4532
4676
|
}
|
|
4533
4677
|
switch (name) {
|
|
4534
|
-
case "list_pages": return
|
|
4535
|
-
case "get_diagnostics": return
|
|
4536
|
-
case "measure_safe_area": return
|
|
4678
|
+
case "list_pages": return envelopeResult("list_pages", buildDevListPagesResult(devtoolsUrl));
|
|
4679
|
+
case "get_diagnostics": return envelopeResult("get_diagnostics", await buildDevDiagnostics(devtoolsUrl, stateEndpoint, (url) => fetch(url)));
|
|
4680
|
+
case "measure_safe_area": return envelopeResult("measure_safe_area", await buildDevMeasureSafeArea(aitSource));
|
|
4537
4681
|
case "call_sdk": {
|
|
4538
4682
|
const sdkName = request.params.arguments?.name;
|
|
4539
4683
|
if (typeof sdkName !== "string" || sdkName === "") return mcpError("call_sdk: name 인자가 비어 있습니다. 호출할 메서드 이름을 전달하세요.");
|
|
4540
|
-
return
|
|
4684
|
+
return envelopeResult("call_sdk", await buildDevCallSdk(sdkName, aitSource));
|
|
4541
4685
|
}
|
|
4542
4686
|
default: return mcpError(`알 수 없는 tool: ${name}`);
|
|
4543
4687
|
}
|
|
@@ -4553,6 +4697,26 @@ function jsonResult(value) {
|
|
|
4553
4697
|
text: JSON.stringify(value, null, 2)
|
|
4554
4698
|
}] };
|
|
4555
4699
|
}
|
|
4700
|
+
/**
|
|
4701
|
+
* Wraps `value` in a `ToolEnvelope` (when compat mode is off) and returns it
|
|
4702
|
+
* as a text content block. In dev-mode `env` is always `'mock'` and
|
|
4703
|
+
* `attached` is always `true` (the Vite dev server is the single implicit
|
|
4704
|
+
* "attached" page).
|
|
4705
|
+
*
|
|
4706
|
+
* When `AIT_MCP_COMPAT=chrome-devtools` the envelope is skipped and the raw
|
|
4707
|
+
* value is returned — identical to `jsonResult` (0.1.x back-compat).
|
|
4708
|
+
*/
|
|
4709
|
+
function envelopeResult(tool, value) {
|
|
4710
|
+
const wrapped = wrapEnvelope(value, {
|
|
4711
|
+
tool,
|
|
4712
|
+
env: "mock",
|
|
4713
|
+
attached: true
|
|
4714
|
+
});
|
|
4715
|
+
return { content: [{
|
|
4716
|
+
type: "text",
|
|
4717
|
+
text: JSON.stringify(wrapped, null, 2)
|
|
4718
|
+
}] };
|
|
4719
|
+
}
|
|
4556
4720
|
/** Builds the dev-mode server and connects it over stdio. */
|
|
4557
4721
|
async function runDevServer() {
|
|
4558
4722
|
const server = createDevServer();
|