A side-by-side look at the Feedthrough and Nodriver — undetected browser automation MCP servers: transport, auth, tools, quality signals, and the config to install either one in Claude, Cursor, Windsurf or VS Code.
Verdict: choose Feedthrough for browser automation work as a local process; choose Nodriver — undetected browser automation for browser automation as a local process. You can also run both side by side in one mcpServers block.
FEEDTHROUGH_PORT.get_instructions, connection_status, get_console_logs.list_frames, bypass_insecure_warning, cf_verify.Rows where the two servers differ are marked.
| Spec | ||
|---|---|---|
| Category | Browser Automation | Browser Automation |
| Quality signal | 61/100 · Good | 61/100 · Good |
| Install command | Confirmed | Confirmed |
| Transport | Local subprocess (stdio) | Local subprocess (stdio) |
| Auth | No auth required | Not declared |
| Pricing | Free | Not declared |
| Required env vars | FEEDTHROUGH_PORT | None listed |
| Verified owner | Community listing | Community listing |
| GitHub stars | 4 | 6 |
| Directory activity | 3 views 0 installs 0 upvotes | 4 views 0 installs 0 upvotes |
| Tools | 16 | 70 |
| Full listing | View Feedthrough | View Nodriver — undetected browser automation |
get_instructionsReturns 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.connection_statusCheck whether a browser with the Feedthrough bridge is currently connected. Returns connected flag and a list of open tabs (id, url, which is active). Call this first — every tool except get_instructions requires a connected browser.get_console_logsReturn 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.get_network_requestsReturn 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.query_domQuery 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.inspect_elementReturn 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').clickClick 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.fillSet 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.hoverHover 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.press_keyDispatch 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.get_htmlReturn 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.get_page_infoReturn basic page context: current URL, document title, readyState, viewport size, scroll position, and user agent. 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.list_framesList the page's frames, so their content can be reached at all.
Chrome's accessibility tree stops at a frame boundary, and
document.querySelector never crosses one, so anything inside an iframe —
payment fields, consent walls, embedded editors, CAPTCHA widgets — is
invisible to take_snapshot and query_selector by default.
Index 0 is always the main frame. Pass an index or frame id as the `frame`
argument of get_page_content, query_selector, evaluate_script,
wait_for_selector or take_snapshot to work inside one.bypass_insecure_warningClick through Chrome's "Your connection is not private" interstitial.
Use this when a navigation lands on an SSL/certificate warning page (expired
or self-signed certificate, hostname mismatch) instead of the site itself —
the snapshot then shows a warning page rather than the expected content.
This performs the Advanced -> Proceed click for you.
Has no effect on any other kind of page.cf_verifyAttempt to solve a Cloudflare "Verify you are human" challenge.
Use when a page is stuck on a Cloudflare interstitial — a checkbox widget,
or "Checking your browser before accessing". Drives nodriver's built-in
verification bypass, which locates the checkbox visually and clicks it.
Needs opencv, which is an optional extra: install nodriver-mcp[cf], or this
reports that it is missing rather than failing obscurely. Many challenges
clear by themselves after a few seconds, so wait_for(["some text from the
real page"]) is worth trying first — and this says so when it looks at the
page and finds no challenge at all.clickClick an element addressed by its snapshot uid.
This is the click you want in almost all cases — prefer it over click_at,
because a uid survives layout shifts and raw coordinates do not. The element
is scrolled into view first, so it need not be visible beforehand.
Sends real CDP input events, so the page sees `isTrusted=true`, which is the
entire point of an undetected driver. Because those are delivered by
coordinate, the click is aimed at a point that actually hits the element:
several points inside it are hit-tested first, since a sticky header or a
banner can cover the centre. If none of them reach it, `if_covered` decides
what happens, and the response always says which path was taken.
Two other situations force the scripted fallback regardless: a touch-emulated
target, where CDP mouse input can crash the renderer, and a CDP click that
times out or errors. Every step is bounded at 10s, so a wedged page cannot
hang the call.
On "unknown uid", take a fresh take_snapshot and retry with the new uid.click_atClick a raw viewport coordinate instead of an element.
Use `click` with a uid whenever the target shows up in a snapshot — it is
robust against layout shifts, this is not. Coordinates are for surfaces with
no addressable element: canvases, maps, video players, image hotspots.
The point must lie inside the current viewport; scroll_page or
scroll_to_selector first if it does not. Coordinates are in CSS pixels and
ignore the device pixel ratio, so they match what emulate/resize_page report.
Same input path as `click`: real CDP events, the same reported scripted
fallback, the same 10s bound per step.close_pageClose a single browser tab.
The last remaining tab cannot be closed — use close_browser to shut Chrome
down entirely. Closing the selected page clears the selection, so subsequent
tools fall back to the most recently opened tab.
Returns the remaining open pages, because indices shift when a tab closes:
re-read them from this response rather than reusing older ones.close_browserQuit Chrome entirely, closing every tab.
Unlike close_page (which always keeps one tab alive), this tears down the
whole browser. Chrome relaunches automatically on the next tool call with
the currently selected profile, so this is also how you apply pending launch
flags without switching profiles.
On an ephemeral temp profile (the default) this discards cookies, logins and
localStorage — save_session first if you need them back. Persistent profiles
created with create_profile keep everything.
It keeps the browser itself. The name, its selected profile and its launch
flags survive for the next relaunch, so the name goes on counting against the
12-browser limit. When an agent is finished for good, call shutdown_browser
instead: it also releases the name.dragDrag one element onto another (press, move, release).
Both uids must come from the same take_snapshot. Both ends are scrolled into
view and hit-tested first, and the pointer is moved in steps with the button
held, which is what mouse-driven sortables and sliders listen for.
This drives the mouse. Native HTML5 drag-and-drop — the dataTransfer kind —
is a separate protocol that a synthetic mouse does not trigger in Chrome; if
nothing happens on a page that uses it, that is why.emulateEmulate network, CPU, geolocation, user agent, color scheme or viewport.
Applies to the selected page and persists across navigations until
reset_emulation. Every parameter is independent — pass only what you want to
change, leave the rest at their defaults. Settings already in force are
carried over rather than dropped: a user_agent on its own keeps the client
hints a device preset established, and a viewport on its own keeps its touch
and mobile flags unless you clear them with `notouch` / `nomobile`.
To emulate a real phone or tablet, use emulate_device instead: it sets user
agent, client hints, viewport, DPR and touch as one coherent set, which
hand-assembled overrides here get wrong in ways anti-bot systems detect.
Turning `touch` on in the viewport also changes how `click` behaves — CDP
mouse input can crash a touch-emulated renderer, so clicks fall back to the
scripted path (`isTrusted=false`) for as long as touch is enabled.reset_emulationClear every emulation override on the selected page.
Resets in one call: network throttling, CPU throttling, geolocation, user
agent and client hints, color scheme, viewport, device pixel ratio, page
scale and touch emulation — back to the real browser defaults.
Use after emulate or emulate_device to make the page behave like ordinary
desktop Chrome again. Turning touch emulation off here also restores trusted
CDP clicks.emulate_deviceEmulate a phone or tablet with one internally consistent set of signals.
Preferred over assembling `emulate` parameters by hand for mobile work: user
agent, UA client hints (Sec-CH-UA-*), viewport, device pixel ratio, touch
support and Accept-Language are set together so they cannot contradict each
other — a mismatch between them is a classic automation tell.
Presets are pixel_7, pixel_7_landscape and ipad_air (aliases are listed in
the `device` parameter). ipad_air deliberately reports desktop-class Safari
with touch and sends no client hints, which is what a real iPad does.
Applies to the selected page and survives navigation. To have mobile signals
present on a page's very first request, pass `device` to new_page or
navigate_page instead of calling this afterwards. Undo with reset_emulation.evaluate_scriptRun JavaScript inside the page and get the result back as JSON.
The escape hatch for anything the other tools do not cover: reading computed
styles, calling a page's own JS API, or extracting structured data in a
single round trip instead of a dozen snapshot-and-click cycles.
Without `args` the function runs at page level in the main world. With
`args`, the given uids become real element references, which is how you
operate on one specific element from a snapshot.
Return values must be JSON-serialisable — DOM nodes, functions and circular
structures are not, so map them to plain values inside the function. A script
that throws comes back as a failed call carrying the JavaScript error, not as
a result you have to inspect for the word "Error".
Pass script_path to run a function kept in a .js file, and file_path to write
the result to disk instead of into the conversation.Paste either block (or both) into your client config, e.g. claude_desktop_config.json or ~/.cursor/mcp.json. We only show a config when the install command is confirmed.
{
"mcpServers": {
"feedthrough-feedthrough": {
"command": "npx",
"args": [
"-y",
"@feedthrough/mcp"
],
"env": {
"FEEDTHROUGH_PORT": "YOUR_FEEDTHROUGH_PORT_HERE"
}
}
}
}
{
"mcpServers": {
"nodriver-undetected-browser-automation": {
"command": "uvx",
"args": [
"nodriver-mcp"
]
}
}
}