{"_id":"@audioeye/testing-sdk-mcp","_rev":"4-bc46927f88dee16ee8649658fea1ed3c","name":"@audioeye/testing-sdk-mcp","dist-tags":{"bootstrap":"6.0.4-bootstrap.0","latest":"6.1.0"},"versions":{"6.0.4-bootstrap.0":{"name":"@audioeye/testing-sdk-mcp","version":"6.0.4-bootstrap.0","author":{"name":"AudioEye Developer Experience"},"license":"SEE LICENSE IN LICENSE","_id":"@audioeye/testing-sdk-mcp@6.0.4-bootstrap.0","maintainers":[{"name":"aenetops","email":"netops@audioeye.com"},{"name":"jkalberer","email":"jkalberer@audioeye.com"}],"homepage":"https://developer.audioeye.com","bugs":{"url":"https://github.com/audioeye/testing-sdk/issues"},"bin":{"audioeye-mcp":"dist/bin/audioeye-mcp.js"},"dist":{"shasum":"3d2bc0324c591ec70cc0ff88199305e372bc15e4","tarball":"https://registry.npmjs.org/@audioeye/testing-sdk-mcp/-/testing-sdk-mcp-6.0.4-bootstrap.0.tgz","fileCount":36,"integrity":"sha512-GItfJoNrE7n0+YZXhIT9qgWXlaIsiZnwf7NwQTuobKWpJavtkhpoBWAwpqCxwcvPLdprGpvlzIk8Q4TzQ5BQxA==","signatures":[{"sig":"MEYCIQDAYlrStvOfgtjlWeIpE/57CkwHrsJW4pFSpyVAT2v+vgIhAPSxTxpk3B5t7NUKFdPIYFfbuT180ZWEaPGOrpQhwDGK","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":3743345},"main":"dist/index.cjs","type":"module","_from":"file:/Users/jkalberer/Desktop/audioeye-mcp-npmjs-bootstrap/audioeye-testing-sdk-mcp-6.0.4-bootstrap.0.tgz","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"mcpName":"com.audioeye/testing-sdk-mcp","_npmUser":{"name":"jkalberer","email":"jkalberer@audioeye.com"},"_resolved":"/Users/jkalberer/Desktop/audioeye-mcp-npmjs-bootstrap/audioeye-testing-sdk-mcp-6.0.4-bootstrap.0.tgz","_integrity":"sha512-GItfJoNrE7n0+YZXhIT9qgWXlaIsiZnwf7NwQTuobKWpJavtkhpoBWAwpqCxwcvPLdprGpvlzIk8Q4TzQ5BQxA==","deprecated":"Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.","repository":{"url":"git+https://github.com/audioeye/testing-sdk.git","type":"git","directory":"packages/testing-sdk-mcp"},"_npmVersion":"11.8.0","description":"Scans live pages with the AudioEye testing SDK and maps accessibility issues to JSX source.","directories":{},"_nodeVersion":"24.13.1","dependencies":{"zod":"^3.25.76","find-up":"^7.0.0","p-retry":"^6.2.1","process":"^0.11.10","lodash-es":"^4.18.1","p-timeout":"^6.1.4","@noble/curves":"^2.2.0","normalize-url":"^8.1.1","puppeteer-core":"^25.3.0","chrome-launcher":"^1.2.1","write-file-atomic":"^6.0.0","@modelcontextprotocol/sdk":"^1.29.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/testing-sdk-mcp_6.0.4-bootstrap.0_1785533226984_0.7809126649934552","host":"s3://npm-registry-packages-npm-production"}},"6.1.0":{"name":"@audioeye/testing-sdk-mcp","version":"6.1.0","author":{"name":"AudioEye Developer Experience"},"license":"SEE LICENSE IN LICENSE","_id":"@audioeye/testing-sdk-mcp@6.1.0","maintainers":[{"name":"aenetops","email":"netops@audioeye.com"},{"name":"jkalberer","email":"jkalberer@audioeye.com"}],"homepage":"https://developer.audioeye.com","bugs":{"url":"https://github.com/audioeye/testing-sdk/issues"},"bin":{"audioeye-mcp":"dist/bin/audioeye-mcp.js"},"dist":{"shasum":"1bce0904728e998fbb828e90dc0c7dce2fcf0e79","tarball":"https://registry.npmjs.org/@audioeye/testing-sdk-mcp/-/testing-sdk-mcp-6.1.0.tgz","fileCount":36,"integrity":"sha512-KDU4ssaMavqTsNqCruoh5Xkt1qzWrxU65RR3iNf3lQ26VVM6O+GVRA+ELK3JUa3asV5XFnfnPwZjHv8xRkzOMA==","signatures":[{"sig":"MEUCIQCrTHEH6p6Fu4VTYiYYMN8rd65rFQsMWkvYjPjOsH31IAIgSOwBCrZYGMhH+/9MWyYGgeUwzfY+sGWKPwjY9BsP55Y=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":4804716},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.mjs","engines":{"node":">=22.12"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"mcpName":"com.audioeye/testing-sdk-mcp","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:eee56314-c373-4f60-9072-6e4f84dd78c7"}},"repository":{"url":"git+https://github.com/audioeye/testing-sdk.git","type":"git","directory":"packages/testing-sdk-mcp"},"_npmVersion":"11.17.0","description":"Scans live pages with the AudioEye rules engine and maps accessibility issues to JSX source.","directories":{},"_nodeVersion":"24.19.0","dependencies":{"zod":"^3.25.76","execa":"^10.0.1","semver":"^7.8.5","find-up":"^8.0.0","p-retry":"^6.2.1","process":"^0.11.10","lodash-es":"^4.18.1","p-timeout":"^6.1.4","jsonc-parser":"^3.3.1","@noble/curves":"^2.3.0","normalize-url":"^9.0.1","puppeteer-core":"^25.8.0","chrome-launcher":"^1.2.1","write-file-atomic":"^6.0.0","@modelcontextprotocol/sdk":"^1.30.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/testing-sdk-mcp_6.1.0_1788458006834_0.6548304103838389","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-07-31T21:27:06.821Z","modified":"2026-09-24T20:55:16.320Z","6.0.4-bootstrap.0":"2026-07-31T21:27:07.191Z","6.1.0":"2026-09-03T17:53:27.073Z"},"bugs":{"url":"https://github.com/audioeye/testing-sdk/issues"},"author":{"name":"AudioEye Developer Experience"},"license":"SEE LICENSE IN LICENSE","homepage":"https://developer.audioeye.com","repository":{"url":"git+https://github.com/audioeye/testing-sdk.git","type":"git","directory":"packages/testing-sdk-mcp"},"description":"Scans live pages with the AudioEye rules engine and maps accessibility issues to JSX source.","maintainers":[{"name":"aenetops","email":"netops@audioeye.com"},{"name":"jkalberer","email":"jkalberer@audioeye.com"}],"readme":"# @audioeye/testing-sdk-mcp\n\nA Model Context Protocol server that drives a live browser to scan a URL with the AudioEye testing SDK, attaches JSX source locations to each issue, and exposes the results as MCP tools so AI coding agents can propose source-level fixes.\n\nGetting started is three commands:\n\n```sh\nnpm install -g @audioeye/testing-sdk-mcp\naudioeye-mcp setup   # register with every detected MCP host\naudioeye-mcp login   # sign in via your browser\n```\n\n…then ask your agent to `/mcp__audioeye__scan-and-fix` your dev server.\n\n## Requirements\n\n- **Node.js ≥ 22.** We support every Node.js LTS line that has not reached end-of-life — currently 22 and 24. (Node 20 reached end-of-life in April 2026.) Claude Desktop users installing the `.mcpb` bundle don't need Node at all — Claude Desktop bundles its own runtime.\n- **An AudioEye account with testing-SDK access.** The SDK is licensed — it verifies a short-lived grace token before every scan and re-checks online at most once every 24h. Sign in once with `audioeye-mcp login` (see [Sign in](#sign-in)); without credentials, every tool that touches the AudioEye engine — opening the controlled browser (its page setup injects the engine), scans, verify-fix, a11y facts — fails closed with `SdkLicenseError` before any browser work. Rule metadata lookups work without credentials (their cache is only populated by licensed scans).\n\n## Install\n\n`@audioeye/testing-sdk-mcp` is published to the public npm registry — no registry configuration or install token needed. Install it globally so the `audioeye-mcp` bin lands on your `PATH`:\n\n```sh\nnpm install -g @audioeye/testing-sdk-mcp\n```\n\nThe server also runs over stdio via `npx -y @audioeye/testing-sdk-mcp`, but prefer the global install for host registration: `npx -y` re-resolves the package against whatever directory the MCP host launches from, and a workspace whose `package.json` carries a conflicting npm `overrides` block kills the server at startup (`EOVERRIDE` — hosts just show \"Connection closed\"). [`setup`](#one-time-mcp-host-setup) registers the installed bin's absolute path to sidestep this.\n\nRegister it under the key `audioeye` — hosts prefix tool and prompt names with it (`mcp__audioeye__scan`, …). The [setup command](#one-time-mcp-host-setup) does this for you; manual per-host options are listed there too.\n\n## Sign in\n\n```sh\naudioeye-mcp login\n```\n\n`login` prints a short pairing code, opens your browser to the AudioEye portal's device-approval page, and waits. You confirm the code and click **Approve** in the portal (signed in as your AudioEye user); the credentials then land on your machine at `~/.config/audioeye/credentials.json` (`%APPDATA%\\audioeye\\credentials.json` on Windows), permissions `0600`. No secret is ever printed to the terminal or pasted by hand.\n\n- **`audioeye-mcp whoami`** — shows the active identity, and which source it came from (environment variables win over the stored credentials).\n- **`audioeye-mcp logout`** — deletes the stored credentials. Environment credentials are untouched.\n\nIf you also use the AudioEye CLI (`aetest`), `aetest login` is the same flow writing the same store — sign in once, both work.\n\nOn hosts with no terminal (Claude Desktop's `.mcpb` install), sign in from the chat with the [`audioeye_login`](#tools) tool instead — same flow, same credential store, no Node or terminal required.\n\nFor non-interactive environments (CI), skip `login` and provide credentials as environment variables — see [CI](#ci).\n\n## One-time MCP host setup\n\nRun the bundled setup command once per machine:\n\n```sh\naudioeye-mcp setup\n```\n\nWith no flags this is `setup --all-detected --global`: it registers the server into **every detected host** at machine-wide scope — for VS Code that means the user-profile `mcp.json` (read by GitHub Copilot and every other native-MCP consumer), never a file inside your repo. To configure only specific hosts, pass a flag per host — host flags switch off the all-detected default:\n\n```sh\naudioeye-mcp setup --claude --global            # Claude Code only, machine-wide (`claude mcp add --scope user` + ~/.claude permissions.allow)\naudioeye-mcp setup --claude                     # Claude Code only, project-scoped (`claude mcp add` + ./.claude permissions.allow)\naudioeye-mcp setup --cursor --vscode --windsurf --zed --codex --gemini\n```\n\n`--global` affects Claude Code (user-scope registration + `~/.claude/settings.json`), VS Code (user-profile `mcp.json` instead of the project `./.vscode/mcp.json`), and gemini (`mcp add --scope user` instead of the CLI's project-scope default); the other hosts' config files are inherently user-scoped.\n\nNot sure what it will touch? `audioeye-mcp setup --dry-run` prints every host's detection result (including which binary a vendor CLI resolved to) and exactly what would be registered, without writing anything.\n\nThe registered server command is the absolute path of the `audioeye-mcp` bin that ran `setup` (via `node` on Windows). When `setup` itself runs from the npx cache (`npx -y @audioeye/testing-sdk-mcp setup`), that path is evictable, so it registers the `npx -y @audioeye/testing-sdk-mcp` form instead — which breaks in workspaces with a conflicting npm `overrides` block (see [Install](#install)). Install globally first.\n\nTwo registration strategies, per host:\n\n- **vendor-cli** (`--claude`, `--codex`, `--gemini`): shells out to the host's own `mcp add` command — preferred, because that CLI owns the shape of its config. When the CLI isn't on `PATH`, a standalone install is tried next (claude's native-installer paths `~/.local/bin/claude`, `~/.claude/local/claude`); a CLI that ships only inside another app's bundle is never spawned — running a bundled binary standalone makes macOS Gatekeeper flag it as malware and quarantine it. If that still finds nothing, the host's config file is **written directly** instead, into the same file the CLI would have:\n  - **claude** → `~/.claude.json` when it (or `~/.claude`) exists: a non-destructive merge into `mcpServers` (with `--global`) or the per-project `projects[<cwd>].mcpServers` map, leaving the rest of that managed file untouched.\n  - **codex** → `~/.codex/config.toml` when `~/.codex` exists (the same file the ChatGPT app, the VS Code extension, and the CLI all read): a surgical edit of its `[mcp_servers.audioeye]` table that leaves every other table, comment, and formatting choice untouched.\n  - **gemini** → `settings.json` (also read by Gemini Code Assist's agent mode) at the scope the CLI would use — `~/.gemini/` with `--global`, else the project's `./.gemini/`.\n- **config-write** (`--cursor`, `--vscode`, `--windsurf`, `--zed`): a non-destructive JSON merge into the host's own config file (`~/.cursor/mcp.json`; `./.vscode/mcp.json`, or the user-profile `mcp.json` with `--global`; `~/.codeium/windsurf/mcp_config.json`; `~/.config/zed/settings.json`). Never writes an `env` block — credentials come from the SDK credential store, not host configs. If our `audioeye` entry has drifted from what we'd write, it's overwritten and reported `updated` — that key is ours to own.\n\nClaude Code additionally **appends `mcp__audioeye__*` to `permissions.allow`** in `settings.json`, so AudioEye's tools stop prompting for permission on every call.\n\n`--all-detected` includes VS Code only together with `--global` (the bare-`setup` default), where the write goes to the user profile. Without `--global` the vscode write would land inside the project tree (committable) off a weak signal — a `./.vscode` directory exists in most repos regardless of editor — so pass `--vscode` explicitly for a project-scoped install.\n\nSafe to re-run: config-write hosts (including claude/codex/gemini via their fallback) report `already-present`/`updated` with no duplicate entries; when a vendor CLI *is* on `PATH`, re-running its own idempotent `mcp add` may just report success again rather than `already-present` (codex/gemini don't signal \"already registered\" — they just silently succeed). Reload your MCP client (Claude Code: Developer → Reload Window) after running it. If any host fails, `setupCli` exits non-zero while still leaving successfully-configured hosts in place.\n\nRead-only AudioEye tools (`audioeye_scan`, `audioeye_get_a11y_facts`, `audioeye_get_rule_metadata`, `audioeye_get_source_context`) also carry `readOnlyHint: true` annotations per the MCP spec — hosts that auto-approve read-only tools (some IDE integrations, future Claude Code versions) skip the prompt without needing the allowlist at all.\n\n### Manual registration\n\nIf you'd rather register by hand (or your host isn't covered by `setup`), [`docs/install-links.md`](./docs/install-links.md) has the generated one-liners and deeplinks:\n\n- **Cursor / VS Code** — one-click install deeplinks.\n- **Claude Code / Codex / Gemini CLIs:**\n\n  ```sh\n  claude mcp add audioeye -- npx -y @audioeye/testing-sdk-mcp\n  codex mcp add audioeye -- npx -y @audioeye/testing-sdk-mcp\n  # gemini's grammar differs: the command goes before `--` (`mcp add <name> <command> -- <args>`)\n  gemini mcp add audioeye npx -- -y @audioeye/testing-sdk-mcp\n  ```\n\n  With a global install, registering the bin (`claude mcp add audioeye -- audioeye-mcp`) is sturdier than the npx form — see [Install](#install).\n\n- **Claude Desktop** — install the `audioeye-mcp-<version>.mcpb` bundle (Settings → Extensions; drag the file in). The bundle ships its own dependencies and runs on Claude Desktop's bundled Node.js.\n\nKeep the registration key `audioeye` — it prefixes the tool and prompt names hosts display (`/mcp__audioeye__scan`, …).\n\n## CI\n\nIn CI (and any non-interactive environment), provide the license credentials as environment variables instead of the browser sign-in. A stored browser sign-in takes precedence when present; the env vars are the fallback (and also kick in when the stored credentials fail validation):\n\n- **`AUDIOEYE_TESTING_SDK_CLIENT_ID`** — your AudioEye testing-SDK client id.\n- **`AUDIOEYE_TESTING_SDK_CLIENT_TOKEN`** — the matching token.\n\nBoth come from the AudioEye portal (My Account → Testing SDK → CI credentials). Store them in your CI secret store and export them into the job environment. When a host config needs them inline, use the host's variable expansion so the committed file references only the variable _name_ (Claude Code's `.mcp.json` [variable expansion](https://docs.claude.com/en/docs/claude-code/mcp#environment-variable-expansion) shown):\n\n```json\n{\n  \"mcpServers\": {\n    \"audioeye\": {\n      \"command\": \"audioeye-mcp\",\n      \"env\": {\n        \"AUDIOEYE_TESTING_SDK_CLIENT_ID\": \"${AUDIOEYE_TESTING_SDK_CLIENT_ID}\",\n        \"AUDIOEYE_TESTING_SDK_CLIENT_TOKEN\": \"${AUDIOEYE_TESTING_SDK_CLIENT_TOKEN}\"\n      }\n    }\n  }\n}\n```\n\n**Don't commit the raw token**, and don't pass secrets as CLI flags — arguments are visible to any local process via `ps` and leak into shell history; a child process's environment is only readable by the same user.\n\n## Tools\n\n- **`audioeye_open_browser({ url? })`** — launch (or focus) a headed Chrome window with a persistent profile at `~/.cache/audioeye-mcp/profile/`. Login state survives between scans. Idempotent.\n- **`audioeye_scan({ url?, runOptions?, waitForReadyMs?, persistTo? })`** — run the SDK scan in-page, walk the framework's component tree to attach a JSX source location (`fileName:lineNumber:columnNumber`) to each failing element, and return a slim grouped JSON result. **Trimmed rule metadata** (`title`, `description`, `sourceFixGuidance`, `severity`, …) is **embedded in the response** under `metadata[ruleCode]` for every rule that failed — no follow-up `audioeye_get_rule_metadata` call needed. Pass `persistTo: \".agent/a11y-scan.json\"` (or any workspace-relative path) to have the server write the response to disk and return `persistedTo` so the agent can read it back later without re-emitting the JSON.\n- **`audioeye_get_rule_metadata({ ruleCodes })`** — look up rule metadata for codes from a _previous_ scan whose response is no longer in your context. New scans embed metadata directly; this tool is only needed to backfill from earlier turns.\n- **`audioeye_get_source_context({ source, contextLines? })`** — read ±N lines around a `DebugSource` from a previous scan. Workspace-bounded (rejects paths outside `AUDIOEYE_MCP_WORKSPACE` / `process.cwd()`).\n- **`audioeye_get_a11y_facts({ cssSelector })`** — compute the assistive-tech facts (accessible name, role, resolved aria-* attrs, nearest landmark, nearest focusable ancestor, `inheritedAtHidden`) for an element on the currently-open page.\n- **`audioeye_verify_fix({ ruleCode, source, url? })`** — re-scan after applying a fix and report whether the targeted `(ruleCode, fileName, lineNumber)` failure is gone within ±15 lines (tolerant to JSX shifts).\n- **`audioeye_close_browser()`** — graceful shutdown; profile preserved.\n- **`audioeye_login()`** — sign in without a terminal, for hosts that have none (Claude Desktop's `.mcpb` install). The first call returns a pairing code and the portal's device-approval link; approve it in the browser and call again to check progress. A background poll writes the shared credential store on success, so the CLI and every other host are signed in too. The entitlement token is never returned to the host.\n\n## Staying up to date\n\nAt boot the server checks the npm registry (channel-aware: an `alpha` install compares against the `alpha` dist-tag, a release against `latest`) at most once every 24h, cached under the SDK cache dir. The check is best-effort — it never blocks the server, and a failed check simply reports no update.\n\nWhen a newer version is published, the \"newer version available\" note rides along on every tool result until you update (it never corrupts the JSON — it's an extra markdown block). Update with `npm install -g @audioeye/testing-sdk-mcp@<channel>`.\n\n## Bundled prompts\n\nThree composable prompts ship with the server, registered via the standard MCP `prompts/list` API — every host that surfaces MCP prompts exposes them under its own naming with no setup step (Claude Code: `/mcp__audioeye__scan`, `/mcp__audioeye__fix`, `/mcp__audioeye__scan-and-fix`):\n\n- **`scan`** (takes an optional `url` argument) — scans the live URL with the AudioEye testing SDK and summarizes failures (with JSX source locations where available). The summary tables are server-rendered and deterministic — the agent presents the response's `summaryMarkdown` verbatim. No code edits. Persists the scan to `.agent/a11y-scan.json` for the fix flow to read back.\n- **`fix`** — proposes source-level fixes against an EXISTING scan response — either in conversation context, or persisted at `.agent/a11y-scan.json` from a prior session. Re-displays the persisted scan's server-rendered `summaryMarkdown` instead of rebuilding tables. Does not run a scan.\n- **`scan-and-fix`** (takes an optional `url` argument) — full end-to-end flow: open browser → wait for login → scan → summarize → propose source-level fixes (only on user approval) → verify. The scan summary shown to the user is the server-rendered, deterministic `summaryMarkdown` from the scan response. **This replaces the previous `/fix-a11y` slash command.**\n\nThe prompt bodies compose from the Markdown chunks under [`prompts/_partials/`](./prompts/_partials/) — single source of truth, no drift between commands. **Customers do not need to copy a `SKILL.md` into every repo** — updates to the prompts land via `npm update`, not per-repo hand edits.\n\n## Configuration\n\nA project-local `.audioeye-mcp.json` (discovered by walking up from the workspace root — `AUDIOEYE_MCP_WORKSPACE` when set, otherwise `process.cwd()`) suppresses known-irrelevant failures:\n\n```json\n{\n  \"ignore\": [\n    {\n      \"cssSelector\": \"footer.TanStackRouterDevtools\",\n      \"comment\": \"Dev-only widget; not shipped to prod.\"\n    },\n    {\n      \"ruleCode\": \"Iframe_Name_Missing\",\n      \"cssSelector\": \"iframe#cb-master-frame\",\n      \"comment\": \"Vendor SDK iframe — vendor-owned.\"\n    },\n    {\n      \"fileNameContains\": \"node_modules/\",\n      \"comment\": \"Don't try to patch dependencies.\"\n    }\n  ]\n}\n```\n\nEach entry's fields are AND'd; multiple entries are OR'd. The scan response includes the resolved config path and per-rule hit counts so reviewers can audit what's filtered.\n\n`cssSelector` is evaluated in the page against the live DOM (`element.closest`), so one field covers both the failing element itself and anything nested inside a matching container.\n\n## Environment variables\n\n**License (CI fallback — interactive machines should use [`audioeye-mcp login`](#sign-in) instead):**\n\n- **`AUDIOEYE_TESTING_SDK_CLIENT_ID`** / **`AUDIOEYE_TESTING_SDK_CLIENT_TOKEN`** — see [CI](#ci). Stored device credentials take precedence when present; the env vars are used when there is no sign-in or the stored credentials fail validation. With neither the env vars nor a device sign-in present, every browser-opening, scan, verify-fix, and a11y-facts call fails closed with `SdkLicenseError`.\n\n**Optional (behavior):**\n\n- **`AUDIOEYE_MCP_PROFILE_DIR`** — override the persistent profile location. Defaults to `~/.cache/audioeye-mcp/profile`.\n- **`AUDIOEYE_MCP_HEADLESS`** — set to `1` or `true` to run Chrome headless. Defaults to headed (so the user can authenticate interactively).\n- **`AUDIOEYE_MCP_WORKSPACE`** — repo root used by `audioeye_scan`'s `persistTo`, `audioeye_get_source_context`, and `.audioeye-mcp.json` discovery. Defaults to `process.cwd()`. Set this when the host's working directory differs from the repo root.\n- **`AUDIOEYE_MCP_CONFIG_PATH`** — explicit absolute path to a config file, bypassing the workspace-root walk.\n\n## Limitations\n\nSymlink containment for `audioeye_scan`'s `persistTo` writes and `audioeye_get_source_context` reads is enforced via POSIX `O_NOFOLLOW` (and a `realpath` containment check on the parent dir). On Windows hosts the `O_NOFOLLOW` flag is a no-op in some Node versions, so symlink containment there is best-effort — the `realpath` check still applies, but the final-segment open will follow a symlink. Run the MCP server on macOS or Linux for full containment.\n\n### Supported React versions for JSX source mapping\n\nThe scan resolves JSX source for failures via seven fallback paths (in order, per fiber):\n\n1. `fiber._debugSource` — React 16 / 17 (with `@babel/plugin-transform-react-jsx-source`) and React 18.0–18.2 stable.\n2. `fiber._debugStack` — React 19 (the dev runtime captures `new Error()` at the JSX call site; the SDK parses the first user frame).\n3. WeakMap side-channel populated by a `jsxDEV`/`jsxsDEV` wrapper installed on webpack chunk globals — covers React canaries that drop both `_debugSource` and `_debugStack` (notably the build Next.js 14 pins).\n4. `React.cloneElement` source-preservation hook — propagates the WeakMap entry from the original element's props to the cloned props, so wrapper / slot-composition patterns (MUI / Joy `<IconButton component={NextLink}>`, Storyblok link wrappers) keep their source.\n5. Babel classic-transform fallback: read `props.__source` directly when present.\n6. **V8 `[[FunctionLocation]]` + source-map translation** — when the in-page walk captures a component function reference but no source location, the Node-side server queries CDP for the function's compiled position, fetches the bundle's source map, and VLQ-decodes the mapping. Matches via this path are tagged `confidence: 'passthrough'` with `passthroughReason: 'component-function-location'`. Lets prod builds work as long as source maps ship.\n7. **Last-resort generic fallback** — `Function.prototype.toString()` on the component type. The dev JSX transform (SWC and Babel both) stamps the literal `{fileName, lineNumber, columnNumber}` source object inside the compiled component body; we regex it out. File-level precision only (the `lineNumber` is the first JSX call in the body, not the specific failing element), so matches via this path are tagged `confidence: 'passthrough'` with `passthroughReason: 'function-body-source'`. The agent reads the file to locate the precise line.\n\n#### React Server Components (Next.js App Router) caveat\n\nJSX rendered by **React Server Components** — that is, files in a Next.js App Router project that do _not_ opt into `'use client'` — runs server-side and is serialized into the React Flight payload stripped of `__source` info. The client-side fiber tree only sees the `'use client'` boundary; everything upstream is opaque from the browser.\n\nAs a result, the SDK cannot map failures in server-rendered DOM back to their authoring site. The fiber walk will find a \"nearest\" source, but it's typically the wrapper boundary (e.g. a top-level `<ThemeProvider>` or `<JoyCssVarsProvider>`), not the file that authored the failing element.\n\nTo detect this, the SDK classifies each `sourceMapped` match with a `confidence` field:\n\n- `confidence: 'high'` — the match is the real authoring site. Propose normally.\n- `confidence: 'passthrough'` — the match is a wrapper / provider / `cloneElement` shim and probably **not** the real authoring site. The scan response includes a `passthroughReason` label (`no-named-component-in-walk`, `children-passthrough-line`, `context-provider-line`, `cloneElement-call-line`) so the agent can warn the user precisely.\n\nTo restore precise source mapping for an RSC-rendered component, mark its file with `'use client'` so it executes client-side; the JSX dev runtime will then run in the browser and the SDK's interception path captures source normally.\n\nThe `summary.failuresWithSourceHighConfidence` metric is the one to gate auto-propose flows on — `summary.failuresWithSource` includes passthrough matches and can read healthy even when most failures point at the wrong file.\n\n## When this tool doesn't help\n\nA pre-flight check — if your project sits mostly in one of these buckets, expect few actionable source-level diffs.\n\n- **Non-React projects (Vue, Svelte, Angular, Solid, plain HTML).** `audioeye_scan` still runs and reports failures, but every failure lands in `unmapped` because there's no JSX fiber tree to walk — which also means we never capture a compiled position to feed the existing source-map resolver. Even when your prod bundle ships source maps, we have no anchor to translate them from. Pluggable framework adapters (tracked under [Roadmap](#roadmap)) are the path to unlocking source-map recovery here.\n- **CMS-rendered content (Storyblok, Contentful, Sanity, Optimizely, Builder.io).** Even on React projects, alt text, heading text, and link names authored in a CMS live in the CMS database, not in source. The scan tags these with `confidence: 'passthrough'` and `passthroughReason: 'cms-rendered-content:<CMS>'`, and the fix flow directs you to fix in the CMS admin. No code diff is proposed.\n- **Production / minified builds.** The dev JSX transform (which stamps `_debugSource` / `_debugStack` and friends) is dev-only. In prod builds, those signals are gone. **If your prod bundle ships source maps**, the MCP's Node-side V8 `[[FunctionLocation]]` + source-map translation (see `componentFunctionLookup.ts`) recovers the original source — failures land with `confidence: 'passthrough'` and `passthroughReason: 'component-function-location'`, which the agent surfaces as a per-row warning. **If source maps are absent**, every prod failure goes to `unmapped` and `sourceMappingDiagnostics` flags `reactDevTransformMissing` so the agent can tell the user to either re-enable the dev transform or ship source maps.\n- **Third-party iframes (ReCAPTCHA, HubSpot, Intercom, Drift, Optimizely banners).** The DOM inside is rendered by code you don't own. Failures bucket as `thirdPartyIframe`; the fix is a vendor change or runtime patching via AudioEye's overlay, not a source diff.\n- **Shadow DOM.** The in-page scanner can't traverse a closed shadow root, and the React fiber tree doesn't extend into them. Failures bucket as `shadowDom`.\n- **React Server Components without `'use client'`.** See the [RSC caveat](#react-server-components-nextjs-app-router-caveat). On App Router projects that don't opt components into `'use client'`, most failures surface as `passthrough` with `no-named-component-in-walk` — useful as a signal, not actionable as a code diff. Source maps don't help here either — the missing data is the server-side fiber tree, not source-map translation.\n\n## Roadmap\n\n- **Multi-framework source mapping** — see [`docs/framework-support.md`](./docs/framework-support.md). The package currently only resolves source locations for React (via fiber `_debugSource`). Preact, SolidJS, Inferno, Vue, Svelte, and Angular are planned via a pluggable adapter API with per-project framework caching.\n\n## Lifecycle note\n\nThe AudioEye-controlled Chrome window is parented to the MCP server process the host (Claude Code, VS Code, etc.) spawned. When that process exits, Chrome exits with it — by design. The MCP server is a single-session tool, not a long-running service, and shutting Chrome down with the host avoids leaving an orphaned browser running across editor restarts. The persistent profile at `~/.cache/audioeye-mcp/profile/` keeps your login state for the next session.\n\n## License\n\nProprietary — see [`LICENSE`](./LICENSE). The SDK and any accompanying source code are not open source; use is subject to AudioEye's terms of service or your separate agreement with AudioEye.\n","readmeFilename":"README.md"}