{
  "markdown": "# Feedthrough\n\n**Debug with AI — from inside your app.**\n\nFeedthrough injects a lightweight debug bridge into any running web page, then exposes everything\n— DOM state, console logs, network requests, and user interactions — as MCP tools. Any\nMCP-compatible AI agent can inspect and drive the page conversationally, in real time.\n\n```\nBrowser (any)\n └── @feedthrough/core          ← injected into your page\n      ├── console interceptor\n      ├── fetch / XHR interceptor\n      └── DOM inspector\n      ↕  WebSocket\n@feedthrough/mcp               ← MCP server, exposes tools over stdio\n └── Tools: click, fill, inspect_element, query_dom,\n            get_console_logs, get_network_requests, …\n      ↕  MCP protocol\nClaude Code / Cursor / any MCP client\n```\n\n---\n\n## The name\n\nMany physics and chemistry experiments run inside a sealed vacuum chamber, with all the air\npumped out so nothing contaminates the experiment. The catch: you still need to control\ninstruments inside the chamber and read their measurements, and the smallest air leak ruins\nthe run. A feedthrough is the part that solves this — a specially engineered connector that\ncarries electrical signals through the chamber wall while keeping the vacuum perfectly intact.\nYou can't reach inside, but the feedthrough lets you observe and control what's happening in\nthere anyway.\n\nThe parallel is exact: Feedthrough extracts runtime debug data from inside a running web app\nwithout disturbing it, and sends control signals back in — clicks, keystrokes, DOM queries —\nwithout breaking the execution environment.\n\n---\n\n## Why Feedthrough?\n\nEvery other browser MCP tool is an **external observer** — it controls the browser from outside\nvia Puppeteer or CDP and only works in Chrome. Feedthrough is an **embedded agent**. It runs\n*inside* the page, so it sees:\n\n- Framework internals (React component trees, Redux store, custom globals)\n- Any browser, not just Chrome\n- Your existing dev workflow — no separate controlled browser to launch\n- Cypress's own browser context during test runs\n\n---\n\n## Packages\n\n| Package | Description |\n|---|---|\n| [`@feedthrough/core`](packages/core) | In-browser bridge — intercepts console, fetch, XHR; handles commands |\n| [`@feedthrough/mcp`](packages/mcp) | MCP server — bridges any MCP client to the browser via WebSocket |\n| [`@feedthrough/cypress`](packages/cypress) | Cypress adapter — auto-injects the bridge before each test page load |\n| [`@feedthrough/playwright`](packages/playwright) | Playwright adapter — injects the bridge via `page.addInitScript()` |\n| [`@feedthrough/vite`](packages/vite) | Vite plugin for apps with a static `index.html` |\n| [`@feedthrough/webpack`](packages/webpack) | Webpack plugin — adds bridge as a global entry point |\n| [`@feedthrough/nextjs`](packages/nextjs) | Next.js adapter — wraps `next.config.ts` with `withFeedthrough()` |\n| [`@feedthrough/nuxt`](packages/nuxt) | Nuxt 3 module |\n| [`@feedthrough/sveltekit`](packages/sveltekit) | SvelteKit adapter — injects via the `handle` hook |\n| [`@feedthrough/remix`](packages/remix) | Remix adapter — injects via a Vite dev server middleware |\n\n---\n\n## Framework support\n\n| Framework | Adapter | Notes |\n|---|---|---|\n| Vite + React / Vue / Solid / Preact | `@feedthrough/vite` | Static `index.html` — plugin uses `transformIndexHtml` |\n| Next.js | `@feedthrough/nextjs` | Wraps the webpack config; dev only |\n| Nuxt 3 | `@feedthrough/nuxt` | Registers as a Nuxt module; dev only |\n| SvelteKit | `@feedthrough/sveltekit` | `handle` hook with `transformPageChunk`; dev only |\n| Remix | `@feedthrough/remix` | Vite dev server middleware; dev only |\n| Webpack apps | `@feedthrough/webpack` | Global entry point; guards against production mode |\n| Cypress | `@feedthrough/cypress` | `window:before:load` hook |\n| Playwright | `@feedthrough/playwright` | `page.addInitScript()` |\n\n---\n\n## Quick start\n\n### 1. Start the MCP server\n\n```bash\nnpx @feedthrough/mcp\n```\n\nThe server listens for browser connections on `ws://127.0.0.1:8765` and exposes MCP tools on\nstdio. Override the port with `FEEDTHROUGH_PORT=9000`. If the port is already taken, the server\nsteps up to the next free one rather than refusing to start — see\n[Running several sessions at once](#running-several-sessions-at-once).\n\n### 2. Add it to your MCP client config\n\n```json\n{\n  \"mcpServers\": {\n    \"feedthrough\": {\n      \"command\": \"npx\",\n      \"args\": [\"@feedthrough/mcp\"]\n    }\n  }\n}\n```\n\n### 3. Inject the bridge into your page\n\n**Vite + React / Vue / Solid / Preact:**\n\n```ts\n// vite.config.ts\nimport { feedthrough } from \"@feedthrough/vite\";\nexport default defineConfig({ plugins: [feedthrough()] });\n```\n\n**Next.js:**\n\n```ts\n// next.config.ts\nimport { withFeedthrough } from \"@feedthrough/nextjs\";\nexport default withFeedthrough()({ /* your next config */ });\n```\n\n**Nuxt 3:**\n\n```ts\n// nuxt.config.ts\nexport default defineNuxtConfig({ modules: [\"@feedthrough/nuxt\"] });\n```\n\n**SvelteKit:**\n\n```ts\n// src/hooks.server.ts\nimport { feedthroughHandle } from \"@feedthrough/sveltekit\";\nimport { sequence } from \"@sveltejs/kit/hooks\";\nexport const handle = sequence(feedthroughHandle);\n```\n\n**Remix:**\n\n```ts\n// vite.config.ts\nimport { feedthrough } from \"@feedthrough/remix\";\nexport default defineConfig({ plugins: [remix(), feedthrough()] });\n```\n\n**Webpack:**\n\n```ts\n// webpack.config.mjs\nimport { FeedthroughPlugin } from \"@feedthrough/webpack\";\nexport default { plugins: [new FeedthroughPlugin()] };\n```\n\n**Cypress:**\n\n```ts\n// cypress/support/e2e.ts\nimport { setupFeedthrough } from \"@feedthrough/cypress\";\nsetupFeedthrough();\n```\n\n**Playwright:**\n\n```ts\n// import test from the adapter instead of @playwright/test\nimport { test, expect } from \"@feedthrough/playwright\";\n```\n\n**Or manually (any bundler):**\n\n```ts\n// main.ts\nif (import.meta.env.DEV) {\n  import(\"@feedthrough/core\").then(({ init }) => init());\n}\n```\n\n### 4. Open your page and start asking\n\nOnce the bridge connects you'll see `[feedthrough] tab connected` in the MCP server output.\nFor the simplest experience, keep a single tab open. Multiple tabs can connect at the same time\nand commands are routed to the most recently active one, but a single tab avoids any ambiguity.\n\nThen ask your AI agent:\n\n```\n> What's on the page right now?\n> Click the submit button and tell me what network requests fired\n> Why is the counter showing the wrong value?\n```\n\n---\n\n## MCP tools\n\n| Tool | Description |\n|---|---|\n| `get_instructions()` | Usage guide — recommended workflow, tool ordering, and selector tips |\n| `query_dom(selector)` | All elements matching a CSS selector |\n| `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 |\n| `get_html(selector)` | Raw outerHTML of a region (capped at 50 KB) |\n| `get_console_logs(limit?, levels?, match?, since?)` | Console output (all methods) plus uncaught errors & promise rejections; filter by `levels`, `match`, or `since` timestamp |\n| `get_network_requests(filter?, since?)` | Captured fetch + XHR — URL, method, status, duration, headers, request/response bodies (10 KB cap); narrow by `filter` or `since` |\n| `get_page_info()` | URL, title, readyState, viewport size, scroll position, user agent, and which bridge this page is connected to |\n| `connection_status()` | Connected tabs and which one is active, plus this server's name, version, and bound port |\n| `click(selector)` | Click an element via native `click()` (fires click + default activation, not the pointer sequence) |\n| `fill(selector, value)` | Set an input/textarea/select value (fires input + change, not keystrokes) |\n| `hover(selector)` | Fire mouseover/mouseenter to mount hover UI (JS handlers, not CSS `:hover`) |\n| `press_key(selector, key)` | Dispatch a key press — Enter, Escape, Tab, arrow keys, or a character |\n| `set_style(selector, properties)` | Preview a visual fix — set inline CSS live (not saved to source) |\n| `set_attribute(selector, name, value)` | Preview an attribute change — toggle disabled, swap a class, set aria-* (`null` removes) |\n| `set_text(selector, text)` | Preview wording/label changes — replace an element's text |\n| `reset_overrides()` | Undo every live `set_style` / `set_attribute` / `set_text` change |\n\n**Live edit is a preview, not a save.** `set_style` / `set_attribute` / `set_text` mutate the\nrunning DOM so the agent can show you a fix without a rebuild. They are *not* written to your\nsource and reset on reload/HMR. The loop: the agent previews live, you confirm, then it edits the\nactual source to make it stick. Changes a framework owns (text, controlled attributes) may be\noverwritten on the next render — the tool result flags this so the agent can tell you.\n\n---\n\n## Example app\n\n`examples/react-app` is a small React app with three deliberate bugs — a good sandbox for\ntrying out the diagnostic workflow:\n\n```bash\n# Terminal 1 — app\ncd examples/react-app && pnpm dev    # http://localhost:5173\n\n# Terminal 2 — MCP server\ncd packages/mcp && node dist/index.js\n```\n\nConnect an AI agent and ask it to find what's wrong. The three bugs are all invisible from the\nUI but findable in under a minute via `get_console_logs`, `get_network_requests`, and `query_dom`.\n\n---\n\n## Running several sessions at once\n\nTwo AI agent sessions on one machine each start their own Feedthrough MCP server, and only one\nof them can have port 8765. That is handled, but it is worth knowing how.\n\n**The server moves, and tells you where it went.** On a busy port it steps up (8766, 8767, …)\ninstead of failing. `connection_status()` reports the port it actually bound, and the agent\npasses that to the dev server it starts:\n\n```bash\nFEEDTHROUGH_PORT=8766 npm run dev\n```\n\nEvery build-tool adapter — vite, webpack, nextjs, nuxt, sveltekit, remix — reads `FEEDTHROUGH_PORT`\n(or `FEEDTHROUGH_URL` for a full `ws://` URL) in Node at config-load time and bakes the result into\nthe injected bridge. So a committed, argument-free `feedthrough()` pairs correctly in any session,\nwith no file edits. An explicit `serverUrl` option always wins over the environment.\n\n**If you start the dev server yourself**, the environment is unset and the page falls back to\n8765, which may be another session's server. Either export the port before starting it, or pin\none per project (below).\n\n**Pinning a port per project.** For a project you always work on in its own session, pin the port\non both ends and neither has to think about it. In `.mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"feedthrough\": {\n      \"command\": \"npx\",\n      \"args\": [\"@feedthrough/mcp\"],\n      \"env\": { \"FEEDTHROUGH_PORT\": \"8770\" }\n    }\n  }\n}\n```\n\nand in the app's config, `feedthrough({ serverUrl: \"ws://localhost:8770\" })`.\n\n**Telling bridges apart.** Each server picks a readable name at startup (`quiet-olive-heron`) and\nsends it to every page that connects. The page logs one line to the browser console, stores it on\n`window.__feedthrough.server`, and returns it from `get_page_info()`. The agent's own name comes\nfrom `connection_status()`. Two different names mean the tab is paired with another session's\nserver — so \"am I driving the right app?\" is one call, not something you notice by watching the\nwrong window change.\n\n---\n\n## Using with an AI agent\n\n### Recommended workflow\n\n1. `connection_status()` — confirm the bridge is connected before anything else\n2. `get_console_logs()` — errors and app output often identify the root cause immediately\n3. `get_network_requests()` — look for failed fetches, wrong URLs, or missing calls\n4. `query_dom(selector)` — find elements and check what's rendered\n5. `inspect_element(selector)` — deep-dive on a specific element\n6. `click()` / `fill()` — interact, then re-check logs and network\n\n### Project-memory snippet\n\nAdd this to whatever project-memory file your AI agent reads — `CLAUDE.md` for Claude Code,\n`.cursor/rules/*.md` for Cursor, and so on — to prime it with the right workflow:\n\n```markdown\n## Debugging with Feedthrough\n\nA Feedthrough MCP server is configured. When investigating UI bugs:\n\n1. Call `connection_status()` first — fail fast if no browser is connected.\n2. Check `get_console_logs()` before touching the DOM.\n3. Check `get_network_requests()` for failed or missing API calls.\n4. Use `query_dom` to orient yourself, `inspect_element` to dig into a specific element.\n5. Interact with `click` / `fill`, then re-check logs.\n\nPrefer element IDs as selectors — they're stable. Avoid long attribute selectors.\n```\n\n### Sample system prompt\n\nFor one-off sessions with any MCP client:\n\n```\nYou have access to the Feedthrough MCP server. It gives you live access to a running web app\nfrom inside the browser — console logs, network requests, DOM state, and the ability to click\nand fill inputs. Start by calling get_instructions() for the recommended workflow.\n```\n\n---\n\n## Security\n\nv1 is local-only. Two guards enforce this:\n\n- **Localhost binding** — the WebSocket server binds to `127.0.0.1`, so it is not reachable\n  from other machines on the network.\n- **Origin validation** — each incoming WebSocket connection is checked against its `Origin` header.\n  Loopback origins (`localhost`, `127.0.0.1`, `::1`) are always accepted, as is any host ending\n  with an allowed suffix (default `.test`, so local dev domains like Laravel Valet's `myapp.test`\n  connect out of the box). Override the suffix list with `FEEDTHROUGH_ALLOWED_HOST_SUFFIXES`\n  (comma-separated; replaces the default — set it empty for loopback-only). Any other origin is\n  rejected. A `.test` origin can only be presented by a page actually served from a `.test` host,\n  which resolves locally, so this widens *which local origins* connect, not network reach.\n\n### What gets captured\n\nCaptured network requests include request and response **bodies and headers**, including\n`Authorization`, `Cookie`, and any other headers your app sends. That's intentional — debugging\nauth and session flows needs them. But the data does leave the page over the local WebSocket,\nflows through the MCP server, and reaches whichever AI agent you've connected. If that agent is\ncloud-backed, sensitive values reach the provider. Run Feedthrough only on dev machines and dev\ndata. Do not inject `@feedthrough/core` into production builds.\n\n---\n\n## Development\n\n```bash\npnpm install       # install all workspace deps\npnpm build         # build all packages\npnpm typecheck     # typecheck all packages\n```\n\nRequires Node.js ≥ 22 and pnpm.\n\n## Releasing\n\nPackages are versioned **independently** — bump only the package(s) you actually changed and leave\nthe rest alone. Publishing to npm is handled by CI: the `Publish to npm` workflow runs on every\npublished GitHub Release and publishes only the packages whose `name@version` isn't on npm yet,\nskipping the ones already published (via OIDC trusted publishing, no tokens).\n\nTo cut a release:\n\n```bash\n# 1. Bump the changed package(s) only\npnpm --filter @feedthrough/mcp exec npm version 0.1.1 --no-git-tag-version\n# When bumping @feedthrough/mcp, also bump the version (and packages[].version) in\n# packages/mcp/server.json to match — the MCP registry validates them against npm.\ngit add packages/mcp/package.json packages/mcp/server.json\ngit commit -m \"Release @feedthrough/mcp 0.1.1\"\ngit push\n\n# 2. Create a GitHub Release (this triggers the publish workflow)\ngh release create v0.1.1 --title \"v0.1.1\" --notes \"...\"\n```\n\nThe workflow builds all packages and publishes only the newly bumped ones. It also publishes\n`@feedthrough/mcp` to the [official MCP registry](https://registry.modelcontextprotocol.io)\n(`io.github.feedthrough/feedthrough`) via GitHub OIDC whenever the registry is missing the current\nversion, so a failed registry publish can be retried by re-running the workflow (Actions tab,\n\"Run workflow\") with no version bump. Mark a release as a pre-release to skip publishing.\n\n---\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n",
  "bytes": 16235,
  "sha": "07ebaf5d078c24aa1ef5966bd4f4a07f96bd1283a2a36c60025eff5bb3c43d65",
  "repo_slug": "feedthrough/feedthrough",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_feedthrough_feedthrough_4ea6a38d/readme"
}