Skip to main content
Glama
Feedthrough

Feedthrough

Official
by feedthrough

Feedthrough

Debug with AI — from inside your app.

Feedthrough injects a lightweight debug bridge into any running web page, then exposes everything — DOM state, console logs, network requests, and user interactions — as MCP tools. Any MCP-compatible AI agent can inspect and drive the page conversationally, in real time.

Browser (any)
 └── @feedthrough/core          ← injected into your page
      ├── console interceptor
      ├── fetch / XHR interceptor
      └── DOM inspector
      ↕  WebSocket
@feedthrough/mcp               ← MCP server, exposes tools over stdio
 └── Tools: click, fill, inspect_element, query_dom,
            get_console_logs, get_network_requests, …
      ↕  MCP protocol
Claude Code / Cursor / any MCP client

The name

Many physics and chemistry experiments run inside a sealed vacuum chamber, with all the air pumped out so nothing contaminates the experiment. The catch: you still need to control instruments inside the chamber and read their measurements, and the smallest air leak ruins the run. A feedthrough is the part that solves this — a specially engineered connector that carries electrical signals through the chamber wall while keeping the vacuum perfectly intact. You can't reach inside, but the feedthrough lets you observe and control what's happening in there anyway.

The parallel is exact: Feedthrough extracts runtime debug data from inside a running web app without disturbing it, and sends control signals back in — clicks, keystrokes, DOM queries — without breaking the execution environment.


Related MCP server: agnt

Why Feedthrough?

Every other browser MCP tool is an external observer — it controls the browser from outside via Puppeteer or CDP and only works in Chrome. Feedthrough is an embedded agent. It runs inside the page, so it sees:

  • Framework internals (React component trees, Redux store, custom globals)

  • Any browser, not just Chrome

  • Your existing dev workflow — no separate controlled browser to launch

  • Cypress's own browser context during test runs


Packages

Package

Description

@feedthrough/core

In-browser bridge — intercepts console, fetch, XHR; handles commands

@feedthrough/mcp

MCP server — bridges any MCP client to the browser via WebSocket

@feedthrough/cypress

Cypress adapter — auto-injects the bridge before each test page load

@feedthrough/playwright

Playwright adapter — injects the bridge via page.addInitScript()

@feedthrough/vite

Vite plugin for apps with a static index.html

@feedthrough/webpack

Webpack plugin — adds bridge as a global entry point

@feedthrough/nextjs

Next.js adapter — wraps next.config.ts with withFeedthrough()

@feedthrough/nuxt

Nuxt 3 module

@feedthrough/sveltekit

SvelteKit adapter — injects via the handle hook

@feedthrough/remix

Remix adapter — injects via a Vite dev server middleware


Framework support

Framework

Adapter

Notes

Vite + React / Vue / Solid / Preact

@feedthrough/vite

Static index.html — plugin uses transformIndexHtml

Next.js

@feedthrough/nextjs

Wraps the webpack config; dev only

Nuxt 3

@feedthrough/nuxt

Registers as a Nuxt module; dev only

SvelteKit

@feedthrough/sveltekit

handle hook with transformPageChunk; dev only

Remix

@feedthrough/remix

Vite dev server middleware; dev only

Webpack apps

@feedthrough/webpack

Global entry point; guards against production mode

Cypress

@feedthrough/cypress

window:before:load hook

Playwright

@feedthrough/playwright

page.addInitScript()


Quick start

1. Start the MCP server

npx @feedthrough/mcp

The server listens for browser connections on ws://127.0.0.1:8765 and exposes MCP tools on stdio. Override the port with FEEDTHROUGH_PORT=9000. If the port is already taken, the server steps up to the next free one rather than refusing to start — see Running several sessions at once.

2. Add it to your MCP client config

{
  "mcpServers": {
    "feedthrough": {
      "command": "npx",
      "args": ["@feedthrough/mcp"]
    }
  }
}

3. Inject the bridge into your page

Vite + React / Vue / Solid / Preact:

// vite.config.ts
import { feedthrough } from "@feedthrough/vite";
export default defineConfig({ plugins: [feedthrough()] });

Next.js:

// next.config.ts
import { withFeedthrough } from "@feedthrough/nextjs";
export default withFeedthrough()({ /* your next config */ });

Nuxt 3:

// nuxt.config.ts
export default defineNuxtConfig({ modules: ["@feedthrough/nuxt"] });

SvelteKit:

// src/hooks.server.ts
import { feedthroughHandle } from "@feedthrough/sveltekit";
import { sequence } from "@sveltejs/kit/hooks";
export const handle = sequence(feedthroughHandle);

Remix:

// vite.config.ts
import { feedthrough } from "@feedthrough/remix";
export default defineConfig({ plugins: [remix(), feedthrough()] });

Webpack:

// webpack.config.mjs
import { FeedthroughPlugin } from "@feedthrough/webpack";
export default { plugins: [new FeedthroughPlugin()] };

Cypress:

// cypress/support/e2e.ts
import { setupFeedthrough } from "@feedthrough/cypress";
setupFeedthrough();

Playwright:

// import test from the adapter instead of @playwright/test
import { test, expect } from "@feedthrough/playwright";

Or manually (any bundler):

// main.ts
if (import.meta.env.DEV) {
  import("@feedthrough/core").then(({ init }) => init());
}

4. Open your page and start asking

Once the bridge connects you'll see [feedthrough] tab connected in the MCP server output. For the simplest experience, keep a single tab open. Multiple tabs can connect at the same time and commands are routed to the most recently active one, but a single tab avoids any ambiguity.

Then ask your AI agent:

> What's on the page right now?
> Click the submit button and tell me what network requests fired
> Why is the counter showing the wrong value?

MCP tools

Tool

Description

get_instructions()

Usage guide — recommended workflow, tool ordering, and selector tips

query_dom(selector)

All elements matching a CSS selector

inspect_element(selector, properties?)

Tag, attributes, full bounding rect + inViewport, ancestor path, curated computed styles, overflow info (clipped/overflowing content), clipped-by-ancestor info, effective visibility (visible + hiddenReason, accounting for ancestors), occlusion (hittable + occludedBy), accessibility (a11y: role, name, states), pseudo ::before/::after content, live form state; properties reads extra CSS props by name

get_html(selector)

Raw outerHTML of a region (capped at 50 KB)

get_console_logs(limit?, levels?, match?, since?)

Console output (all methods) plus uncaught errors & promise rejections; filter by levels, match, or since timestamp

get_network_requests(filter?, since?)

Captured fetch + XHR — URL, method, status, duration, headers, request/response bodies (10 KB cap); narrow by filter or since

get_page_info()

URL, title, readyState, viewport size, scroll position, user agent, and which bridge this page is connected to

connection_status()

Connected tabs and which one is active, plus this server's name, version, and bound port

click(selector)

Click an element via native click() (fires click + default activation, not the pointer sequence)

fill(selector, value)

Set an input/textarea/select value (fires input + change, not keystrokes)

hover(selector)

Fire mouseover/mouseenter to mount hover UI (JS handlers, not CSS :hover)

press_key(selector, key)

Dispatch a key press — Enter, Escape, Tab, arrow keys, or a character

set_style(selector, properties)

Preview a visual fix — set inline CSS live (not saved to source)

set_attribute(selector, name, value)

Preview an attribute change — toggle disabled, swap a class, set aria-* (null removes)

set_text(selector, text)

Preview wording/label changes — replace an element's text

reset_overrides()

Undo every live set_style / set_attribute / set_text change

Live edit is a preview, not a save. set_style / set_attribute / set_text mutate the running DOM so the agent can show you a fix without a rebuild. They are not written to your source and reset on reload/HMR. The loop: the agent previews live, you confirm, then it edits the actual source to make it stick. Changes a framework owns (text, controlled attributes) may be overwritten on the next render — the tool result flags this so the agent can tell you.


Example app

examples/react-app is a small React app with three deliberate bugs — a good sandbox for trying out the diagnostic workflow:

# Terminal 1 — app
cd examples/react-app && pnpm dev    # http://localhost:5173

# Terminal 2 — MCP server
cd packages/mcp && node dist/index.js

Connect an AI agent and ask it to find what's wrong. The three bugs are all invisible from the UI but findable in under a minute via get_console_logs, get_network_requests, and query_dom.


Running several sessions at once

Two AI agent sessions on one machine each start their own Feedthrough MCP server, and only one of them can have port 8765. That is handled, but it is worth knowing how.

The server moves, and tells you where it went. On a busy port it steps up (8766, 8767, …) instead of failing. connection_status() reports the port it actually bound, and the agent passes that to the dev server it starts:

FEEDTHROUGH_PORT=8766 npm run dev

Every build-tool adapter — vite, webpack, nextjs, nuxt, sveltekit, remix — reads FEEDTHROUGH_PORT (or FEEDTHROUGH_URL for a full ws:// URL) in Node at config-load time and bakes the result into the injected bridge. So a committed, argument-free feedthrough() pairs correctly in any session, with no file edits. An explicit serverUrl option always wins over the environment.

If you start the dev server yourself, the environment is unset and the page falls back to 8765, which may be another session's server. Either export the port before starting it, or pin one per project (below).

Pinning a port per project. For a project you always work on in its own session, pin the port on both ends and neither has to think about it. In .mcp.json:

{
  "mcpServers": {
    "feedthrough": {
      "command": "npx",
      "args": ["@feedthrough/mcp"],
      "env": { "FEEDTHROUGH_PORT": "8770" }
    }
  }
}

and in the app's config, feedthrough({ serverUrl: "ws://localhost:8770" }).

Telling bridges apart. Each server picks a readable name at startup (quiet-olive-heron) and sends it to every page that connects. The page logs one line to the browser console, stores it on window.__feedthrough.server, and returns it from get_page_info(). The agent's own name comes from connection_status(). Two different names mean the tab is paired with another session's server — so "am I driving the right app?" is one call, not something you notice by watching the wrong window change.


Using with an AI agent

  1. connection_status() — confirm the bridge is connected before anything else

  2. get_console_logs() — errors and app output often identify the root cause immediately

  3. get_network_requests() — look for failed fetches, wrong URLs, or missing calls

  4. query_dom(selector) — find elements and check what's rendered

  5. inspect_element(selector) — deep-dive on a specific element

  6. click() / fill() — interact, then re-check logs and network

Project-memory snippet

Add this to whatever project-memory file your AI agent reads — CLAUDE.md for Claude Code, .cursor/rules/*.md for Cursor, and so on — to prime it with the right workflow:

## Debugging with Feedthrough

A Feedthrough MCP server is configured. When investigating UI bugs:

1. Call `connection_status()` first — fail fast if no browser is connected.
2. Check `get_console_logs()` before touching the DOM.
3. Check `get_network_requests()` for failed or missing API calls.
4. Use `query_dom` to orient yourself, `inspect_element` to dig into a specific element.
5. Interact with `click` / `fill`, then re-check logs.

Prefer element IDs as selectors — they're stable. Avoid long attribute selectors.

Sample system prompt

For one-off sessions with any MCP client:

You have access to the Feedthrough MCP server. It gives you live access to a running web app
from inside the browser — console logs, network requests, DOM state, and the ability to click
and fill inputs. Start by calling get_instructions() for the recommended workflow.

Security

v1 is local-only. Two guards enforce this:

  • Localhost binding — the WebSocket server binds to 127.0.0.1, so it is not reachable from other machines on the network.

  • Origin validation — each incoming WebSocket connection is checked against its Origin header. Loopback origins (localhost, 127.0.0.1, ::1) are always accepted, as is any host ending with an allowed suffix (default .test, so local dev domains like Laravel Valet's myapp.test connect out of the box). Override the suffix list with FEEDTHROUGH_ALLOWED_HOST_SUFFIXES (comma-separated; replaces the default — set it empty for loopback-only). Any other origin is rejected. A .test origin can only be presented by a page actually served from a .test host, which resolves locally, so this widens which local origins connect, not network reach.

What gets captured

Captured network requests include request and response bodies and headers, including Authorization, Cookie, and any other headers your app sends. That's intentional — debugging auth and session flows needs them. But the data does leave the page over the local WebSocket, flows through the MCP server, and reaches whichever AI agent you've connected. If that agent is cloud-backed, sensitive values reach the provider. Run Feedthrough only on dev machines and dev data. Do not inject @feedthrough/core into production builds.


Development

pnpm install       # install all workspace deps
pnpm build         # build all packages
pnpm typecheck     # typecheck all packages

Requires Node.js ≥ 22 and pnpm.

Releasing

Packages are versioned independently — bump only the package(s) you actually changed and leave the rest alone. Publishing to npm is handled by CI: the Publish to npm workflow runs on every published GitHub Release and publishes only the packages whose name@version isn't on npm yet, skipping the ones already published (via OIDC trusted publishing, no tokens).

To cut a release:

# 1. Bump the changed package(s) only
pnpm --filter @feedthrough/mcp exec npm version 0.1.1 --no-git-tag-version
# When bumping @feedthrough/mcp, also bump the version (and packages[].version) in
# packages/mcp/server.json to match — the MCP registry validates them against npm.
git add packages/mcp/package.json packages/mcp/server.json
git commit -m "Release @feedthrough/mcp 0.1.1"
git push

# 2. Create a GitHub Release (this triggers the publish workflow)
gh release create v0.1.1 --title "v0.1.1" --notes "..."

The workflow builds all packages and publishes only the newly bumped ones. It also publishes @feedthrough/mcp to the official MCP registry (io.github.feedthrough/feedthrough) via GitHub OIDC whenever the registry is missing the current version, so a failed registry publish can be retried by re-running the workflow (Actions tab, "Run workflow") with no version bump. Mark a release as a pre-release to skip publishing.


License

MIT — see LICENSE.

Available Tools

16 tools
clickA

Click an element by calling its native click(), which fires a click event and runs the default activation: following a link, toggling a checkbox or radio, submitting a form. Prefer an id selector (#submit-btn) for reliable targeting. Note it does NOT synthesize the preceding pointer/mouse sequence (pointerdown / mousedown / mouseup) or move focus, so a handler wired specifically to those events rather than to click won't fire; for keyboard-driven activation use press_key instead. Behavior: if the selector matches nothing the call returns an error; it does not scroll the element into view, and it does not wait for any resulting navigation, network, or re-render to settle, returning as soon as the click is dispatched. Observe the effect with a follow-up get_console_logs / get_network_requests / query_dom. Returns the tag and id of the clicked element.

Inputs: selector (required).

connection_statusA

Check whether a browser with the Feedthrough bridge is currently connected. Returns a connected flag, a list of open tabs (id, url, which is active), and a 'server' block identifying this bridge: its name, version, and the port it actually bound. That port matters — if another agent session's server already held the default 8765, this one moved up, and the page will not find it until the app's dev server is started with FEEDTHROUGH_PORT set to the reported port. When a page is connected but behaving as though someone else is driving it, compare this name with the 'server' field from get_page_info: different names mean the tab is paired with another session's bridge. Call this first — every tool except get_instructions requires a connected browser.

Inputs: no named parameters.

fillA

Set the value of an input, textarea, or select element. Focuses the element, assigns the value through the element's native value setter (so React/Vue controlled inputs register the change), then fires bubbling input and change events. The value is set in one shot, not typed character by character, so per-keystroke handlers (keydown / keypress / keyup / beforeinput) do NOT fire; to send Enter to submit or trigger a key shortcut, follow with press_key. Prefer an id selector (#search-input). If the selector matches nothing the call returns an error; it returns as soon as the events are dispatched and does not wait for downstream validation or re-renders. Returns the tag and the value that was set.

Inputs: value (required), selector (required).

get_console_logsA

Return console output captured since the bridge connected. Covers every console method — log/warn/error/info/debug plus dir, table, assert, trace, count, countReset, time/timeEnd/timeLog, group/groupCollapsed/groupEnd, and clear. Each entry has a 'level' (the closest of the five standard levels); rich methods also carry a 'method' field, and console.trace() plus failing console.assert() entries include a 'stack'. Uncaught exceptions and unhandled promise rejections are also captured (level 'error', method 'uncaught' / 'unhandledrejection') even though the app never logged them. When the app is noisy with framework or deprecation warnings, pass levels: ['error'] (or ['error', 'warn']) so the real errors aren't buried, and use 'match' to narrow by content. Pass 'since' (a ms timestamp from an earlier entry's 'ts', or Date.now() before an action) to see only what happened after that point. Read-only: it returns a passively captured buffer and neither clears the console nor changes the page. Always check this early — app errors and debug output often identify the root cause immediately.

Inputs: limit, match, since, levels.

get_htmlA

Return the outerHTML of an element (capped at 50 KB). Use this when the summarised query_dom output isn't enough and you need to see the actual markup/structure of a region. Read-only: it only reads the DOM and makes no changes, and it returns an error if the selector matches nothing.

Inputs: selector (required).

get_instructionsA

Returns the Feedthrough usage guide as a Markdown text document, with sections for the recommended workflow, tool-ordering tips, and selector advice. Read-only and takes no arguments; it does not touch the page or require a connected browser. Call it at the start of a debugging session if you are unfamiliar with Feedthrough or want a quick refresher.

Inputs: no named parameters.

get_network_requestsA

Return all fetch and XHR requests captured since the bridge connected, including URL, method, HTTP status, duration, request and response headers, and request and response bodies (bodies capped at 10 KB each — anything longer is truncated with a marker; binary responses are summarised). Use this to find failed requests (4xx/5xx), wrong URLs, slow calls, or to inspect what the app actually sent or received. Use 'filter' to narrow by URL/method and 'since' (a ms timestamp) to see only requests that fired after an action. Read-only: it returns a passively captured log and does not issue or modify any requests.

Inputs: since, filter.

get_page_infoA

Return basic page context: current URL, document title, readyState, viewport size, scroll position, user agent, and a 'server' block naming which Feedthrough bridge this page is connected to (name, port, version — null against a pre-0.4 bridge). Read-only and non-destructive: it only reads page state and makes no changes. Useful to orient at the start of a session or confirm a navigation happened. If several agent sessions are running on this machine, check 'server.name' against connection_status: a mismatch means this tab is talking to another session's server, so you are driving the wrong app.

Inputs: no named parameters.

hoverA

Hover over an element by dispatching synthetic, bubbling mouseover and mouseenter events from inside the page. This triggers JavaScript hover handlers (onMouseEnter / onMouseOver), so hover-only UI that mounts on hover (tooltips, popovers, dropdown and submenus) appears in the DOM; follow up with query_dom, get_html, or inspect_element to read what was revealed. Three limits to know: it does NOT activate the CSS :hover pseudo-class (that is driven by the real cursor, not synthetic events), so styles or content shown purely via :hover in CSS will not change; no mouseout / mouseleave is sent, so the hovered state stays until the app tears it down or you interact elsewhere; and the events are dispatched whether or not the element is visible or in the viewport (it is not scrolled into view), so a successful call does not by itself confirm anything rendered. If the selector matches nothing the call returns an error. Returns the tag of the hovered element.

Inputs: selector (required).

inspect_elementA

Return full details about a single element: tag, id, classes, all attributes, text content, bounding rect (top/right/bottom/left/width/height + page scroll and an inViewport flag), a compact ancestor 'path' (e.g. 'body > main > div#app > button.cta'), a curated set of computed styles (layout, box model, typography, positioning, flex/grid), an 'overflow' block when content is clipped/overflowing (scroll vs client size + per-axis x/y flags), a 'clipped' block when an ancestor's overflow cuts the element off (the clipping ancestor + which edges), an effective-visibility check ('visible' boolean, with a 'hiddenReason' such as 'ancestor div#modal display:none' or 'opacity:0' when not visible, accounting for ancestors), an occlusion check ('hittable' boolean from a center-point hit-test, with 'occludedBy' naming the element actually on top when something covers it), an 'a11y' block (resolved role, best-effort accessible name, and key states like expanded/checked/selected/disabled/hidden/tabindex), a 'pseudo' block with ::before/::after content when set (icon fonts, generated text), and live form state where applicable (an input's current value, checked, disabled, etc.). Pass 'properties' to additionally read any specific computed CSS properties by name — they come back under 'requested'. Use this to understand why an element looks wrong or isn't behaving as expected. Read-only: it only reads element state and never changes the page, and it returns an error if the selector matches nothing. Note: addEventListener-registered event handlers cannot be read from the page; only inline on* handler attributes appear (in 'attributes').

Inputs: selector (required), properties.

press_keyA

Dispatch a key press (keydown/keypress/keyup) on an element — e.g. Enter to submit a search, Escape to close a modal, Tab to move focus, or ArrowUp/ArrowDown in a list. Use named keys (Enter, Escape, Tab, Backspace, Delete, ArrowUp/Down/Left/Right) or a single character. Note: this fires key handlers but does NOT insert text into inputs — use 'fill' to set an input's value, then press_key for the submit/shortcut. If the selector matches nothing the call returns an error; it dispatches the key events and returns without waiting for any resulting navigation or re-render.

Inputs: key (required), selector (required).

query_domA

Query the page with a CSS selector and return a summary of every matching element (tag, id, classes, text content). Good for counting list items, checking what's rendered, or finding the right selector before calling inspect_element or click. Read-only: it only reads the DOM and never changes the page. Returns an empty list (not an error) when nothing matches, so it is also a safe existence check.

Inputs: selector (required).

reset_overridesA

Undo every set_style / set_attribute / set_text change the bridge has applied since it connected, restoring the original values. Best effort: elements the framework has since re-created may not roll back (a page reload always fully resets).

Inputs: no named parameters.

set_attributeA

Set or remove an attribute on an element to preview a change (toggle disabled, swap a class, set an aria-* attribute). Pass value=null to remove the attribute. Live preview only — not saved to source, resets on reload. If the attribute is one a framework controls (class, value, checked, disabled, …) the result includes a 'frameworkWarning' that it may be reverted on the next render — relay it. Reset with reset_overrides.

Inputs: name (required), value (required), selector (required).

set_styleA

Set one or more inline CSS properties on an element to PREVIEW a visual change live (e.g. shrink a label that doesn't fit, adjust padding or width). This edits the running DOM only — it is NOT saved to source and resets on reload — so tell the user it's a preview, and once they're happy, make the real change in the CSS/component source. Inline styles override the stylesheet and usually survive re-renders. The result includes a 'note' to relay; reset with reset_overrides.

Inputs: selector (required), properties (required).

set_textA

Replace an element's text content to preview wording/label changes. Live preview only — not saved to source, resets on reload. textContent is almost always framework-controlled, so the result includes a 'frameworkWarning' that React/Vue/etc. will likely overwrite it on the next render — relay that, and persist real changes in the source. Reset with reset_overrides.

Inputs: text (required), selector (required).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.3/5.0

    Scored across 16 tools

    Disambiguation4/5

    Most tools have clearly distinct purposes, but query_dom, inspect_element, and get_html all read from the DOM and could occasionally be confused. get_page_info and connection_status also share bridge-identity fields, though their primary responsibilities are different.

    Naming Consistency4/5

    The get_* and set_* prefixes create a strong, predictable pattern, and click/fill/hover/press_key are intuitive action verbs. connection_status breaks the get_ convention and query_dom/inspect_element are less pattern-aligned, but overall naming remains readable and consistent.

    Tool Count4/5

    At 16 tools, this is slightly above the ideal 3-15 range, but the breadth is justified by the server's browser-debugging scope. The count feels reasonable rather than bloated.

    Completeness4/5

    The toolset covers the core debugging lifecycle: reading DOM/console/network state, interacting with the page, previewing changes, and resetting overrides. Obvious gaps like explicit navigation, waiting for conditions, or screenshots are missing but are not fatal to the main workflows.

    Maintenance

    ActivityStale
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI assistants to debug frontend applications by providing direct access to browser DevTools, React state, DOM inspection, and runtime debugging capabilities. Bridges the gap between AI and complex web applications for autonomous debugging and issue resolution.
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Bridges AI coding agents with the browser to provide visual debugging, real-time error capture, screenshot capabilities, DOM inspection, and interactive wireframing through a reverse proxy with injected developer tools.
      47 npm
      22
      Apache 2.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI coding agents to access live console logs, errors, and network requests from web applications via a local WebSocket connection, without copying data to chat.
      9 npm
      19
      MIT
    • A
      license
      B
      quality
      D
      maintenance
      Enables AI agents to inspect and control a live Chromium browser for frontend debugging, providing console logs, network requests, DOM snapshots, and accessibility analysis.
      19
      12 npm
      MIT