New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@audioeye/testing-sdk-mcp

Package Overview
Dependencies
Maintainers
2
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@audioeye/testing-sdk-mcp

Scans live pages with the AudioEye testing SDK and maps accessibility issues to JSX source.

bootstrap
Source
npmnpm
Version
6.0.4-bootstrap.0
Version published
Weekly downloads
25
-26.47%
Maintainers
2
Weekly downloads
 
Created
Source

@audioeye/testing-sdk-mcp

A 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.

Getting started is two commands:

npx -y @audioeye/testing-sdk-mcp setup --all-detected   # register with your MCP hosts
npx -y @audioeye/testing-sdk-mcp login                  # sign in via your browser

…then ask your agent to /mcp__audioeye__scan-and-fix your dev server.

Requirements

  • 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.
  • 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); without credentials the scan fails closed with SdkLicenseError. Scan-independent tools (rule metadata, a11y facts) work without credentials.

Install

@audioeye/testing-sdk-mcp is published to the public npm registry — no registry configuration or install token needed. The server runs over stdio via npx:

npx -y @audioeye/testing-sdk-mcp

Prefer a global install if you'd rather have the audioeye-mcp bin on your PATH:

npm install -g @audioeye/testing-sdk-mcp

Register it under the key audioeye — hosts prefix tool and prompt names with it (mcp__audioeye__scan, …). The setup command does this for you; manual per-host options are listed there too.

Sign in

audioeye-mcp login

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.

  • audioeye-mcp whoami — shows the active identity, and which source it came from (environment variables win over the stored credentials).
  • audioeye-mcp logout — deletes the stored credentials. Environment credentials are untouched.

If you also use the AudioEye CLI (aetest), aetest login is the same flow writing the same store — sign in once, both work.

On hosts with no terminal (Claude Desktop's .mcpb install), sign in from the chat with the audioeye_login tool instead — same flow, same credential store, no Node or terminal required.

For non-interactive environments (CI), skip login and provide credentials as environment variables — see CI.

One-time MCP host setup

Run the bundled setup command once per project (or once per machine with --global, Claude Code only — registers with claude mcp add --scope user instead of the default project scope) to register the server into your MCP host(s). Pass a flag per host, or --all-detected to configure every detected host except VS Code (see below):

audioeye-mcp setup --claude                     # registers via `claude mcp add` + permissions.allow
audioeye-mcp setup --cursor --vscode --windsurf --zed --codex --gemini
audioeye-mcp setup --all-detected               # detects and configures whatever's installed (not vscode)

Two registration strategies, per host:

  • vendor-cli (--claude, --codex, --gemini): shells out to the host's own mcp add command. Skipped with skipped-not-detected if that CLI isn't on PATH.
  • config-write (--cursor, --vscode, --windsurf, --zed): a non-destructive JSON merge into the host's own config file (~/.cursor/mcp.json, ./.vscode/mcp.json, ~/.codeium/mcp_config.json, ~/.config/zed/settings.json respectively). Windsurf reads ~/.codeium/mcp_config.json; an older ~/.codeium/windsurf/mcp_config.json may still exist on your machine from a previous Windsurf version, but current Windsurf does not read it. 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.

Claude Code additionally appends mcp__audioeye__* to permissions.allow in settings.json, so AudioEye's tools stop prompting for permission on every call.

--vscode is excluded from --all-detected: its "detection" is just a ./.vscode directory, which exists in most repos regardless of editor, and the config write lands inside the project tree (committable). Pass --vscode explicitly when you want it.

Safe to re-run: config-write hosts report already-present/updated with no duplicate entries; vendor-cli hosts (claude/codex/gemini) re-run their own idempotent mcp add, which 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.

Read-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.

Manual registration

If you'd rather register by hand (or your host isn't covered by setup), docs/install-links.md has the generated one-liners and deeplinks:

  • Cursor / VS Code — one-click install deeplinks.

  • Claude Code / Codex / Gemini CLIs:

    claude mcp add audioeye -- npx -y @audioeye/testing-sdk-mcp
    codex mcp add audioeye -- npx -y @audioeye/testing-sdk-mcp
    gemini mcp add audioeye -- npx -y @audioeye/testing-sdk-mcp
    
  • 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.

Keep the registration key audioeye — it prefixes the tool and prompt names hosts display (/mcp__audioeye__scan, …).

CI

In CI (and any non-interactive environment), provide the license credentials as environment variables instead of the browser sign-in — they take precedence over the credential store everywhere:

  • AUDIOEYE_TESTING_SDK_CLIENT_ID — your AudioEye testing-SDK client id.
  • AUDIOEYE_TESTING_SDK_CLIENT_TOKEN — the matching token.

Both 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 shown):

{
  "mcpServers": {
    "audioeye": {
      "command": "audioeye-mcp",
      "env": {
        "AUDIOEYE_TESTING_SDK_CLIENT_ID": "${AUDIOEYE_TESTING_SDK_CLIENT_ID}",
        "AUDIOEYE_TESTING_SDK_CLIENT_TOKEN": "${AUDIOEYE_TESTING_SDK_CLIENT_TOKEN}"
      }
    }
  }
}

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.

Tools

  • 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.
  • 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.
  • 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.
  • 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()).
  • 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.
  • 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).
  • audioeye_close_browser() — graceful shutdown; profile preserved.
  • 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.

Bundled prompts

Three 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):

  • 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.
  • 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.
  • 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.

The prompt bodies compose from the Markdown chunks under 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.

Configuration

A 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:

{
  "ignore": [
    {
      "cssSelector": "footer.TanStackRouterDevtools",
      "comment": "Dev-only widget; not shipped to prod."
    },
    {
      "ruleCode": "Iframe_Name_Missing",
      "cssSelector": "iframe#cb-master-frame",
      "comment": "Vendor SDK iframe — vendor-owned."
    },
    {
      "fileNameContains": "node_modules/",
      "comment": "Don't try to patch dependencies."
    }
  ]
}

Each 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.

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.

Environment variables

License (CI override — interactive machines should use audioeye-mcp login instead):

  • AUDIOEYE_TESTING_SDK_CLIENT_ID / AUDIOEYE_TESTING_SDK_CLIENT_TOKEN — see CI. When set, they take precedence over the stored device credentials. With neither the env vars nor a device sign-in present, every scan fails closed with SdkLicenseError.

Optional (behavior):

  • AUDIOEYE_MCP_PROFILE_DIR — override the persistent profile location. Defaults to ~/.cache/audioeye-mcp/profile.
  • AUDIOEYE_MCP_HEADLESS — set to 1 or true to run Chrome headless. Defaults to headed (so the user can authenticate interactively).
  • 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.
  • AUDIOEYE_MCP_CONFIG_PATH — explicit absolute path to a config file, bypassing the workspace-root walk.

Limitations

Symlink 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.

Supported React versions for JSX source mapping

The scan resolves JSX source for failures via seven fallback paths (in order, per fiber):

  • fiber._debugSource — React 16 / 17 (with @babel/plugin-transform-react-jsx-source) and React 18.0–18.2 stable.
  • fiber._debugStack — React 19 (the dev runtime captures new Error() at the JSX call site; the SDK parses the first user frame).
  • 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).
  • 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.
  • Babel classic-transform fallback: read props.__source directly when present.
  • 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.
  • 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.

React Server Components (Next.js App Router) caveat

JSX 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.

As 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.

To detect this, the SDK classifies each sourceMapped match with a confidence field:

  • confidence: 'high' — the match is the real authoring site. Propose normally.
  • 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.

To 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.

The 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.

When this tool doesn't help

A pre-flight check — if your project sits mostly in one of these buckets, expect few actionable source-level diffs.

  • 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) are the path to unlocking source-map recovery here.
  • 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.
  • 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.
  • 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.
  • 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.
  • React Server Components without 'use client'. See the RSC 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.

Roadmap

  • Multi-framework source mapping — see 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.

Lifecycle note

The 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.

License

Proprietary — see 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.

FAQs

Package last updated on 31 Jul 2026

Related posts