Repository navigation
MCP server face: let AI agents drive the real browser - #10
Merged
Merged
Conversation
Extend the single global --allow-domains list into layered policies:
- internal/policy: domain rules (allow/deny, *.sub, *), principals and a
JSON --policy-file defining named tokens with their own rules, an admin
flag and a scripts permission. --deny-domains adds a global denylist.
- Auth attaches the token's principal to the request context; --token is
the admin "default" principal.
- Tab ownership: non-admin tokens only see their own tabs, get their own
per-site default tabs, and cannot adopt existing tabs.
- core.RulesPolicy checks global ∩ requester ∩ owner ∩ session rules,
re-checks the final URL, and guards restricted tabs in the browser via
the DevTools Fetch domain so redirects, clicks and page script obey the
same rules.
- API: session rules on tab create, GET/PUT /tabs/{id}/policy, /whoami.
WebDriver: tether:options.policy, ownership on every command, and
Execute Script gated by the scripts permission.
- Docs: docs/POLICIES.md, SECURITY.md, ARCHITECTURE.md, README, example
policy file. Integration test drives a real Chrome as a restricted token.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfsJJgC4Jp8VqckdcChH4W
Replace the /mcp stub with a real Model Context Protocol server on the shared session core: - Streamable HTTP transport (MCP 2025-06-18, 2025-03-26, 2024-11-05): Mcp-Session-Id sessions, protocol-version checks, batches, 202 for notifications, DELETE to end a session. Inherits token auth, the loopback guard and --allow-domains from the server. - 16 browser tools: tabs/new/select/close, navigate, history, snapshot (interactive elements with stable backend-node refs plus page text), click, type, press_key, select_option, scroll, wait_for, eval, screenshot and in-tab fetch with the user's cookies. Tool annotations, isError results with stable codes, policy checked against the page's current URL, password values and Set-Cookie never returned, tool arguments never logged. - Each MCP session tracks a current tab and the tabs it opened; ending the session (or idling past --idle-timeout) closes only those. - `tether mcp`: stdio bridge for Claude Desktop and other stdio clients, local or across a tailnet, with ordered forwarding. - --mcp flag (default on) to turn the face off. Also fix screenshots of idle background tabs hanging forever (Chrome does not composite hidden tabs): probe briefly, then bring the tab to the front and retry, as ChromeDriver does. Affects WebDriver too. Docs: docs/MCP.md, README, ARCHITECTURE, SECURITY threat model, CHANGELOG. Tests: unit tests with the cdp fakes and a real headless Chrome integration test driving every major tool. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpbHKDrhpTtF3U8jAhD61N
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpbHKDrhpTtF3U8jAhD61N
8000
| lastUsed: s.now(), | ||
| } | ||
| s.admit(sess) | ||
| s.log.Info("mcp session started", "client", sess.client, "protocol", proto) |
Owner
Author
There was a problem hiding this comment.
This alert is a false positive. The only client-supplied value in this log line is clientInfo.name. At line 297 it passes through logx.Clean, which strips \r and \n and is the same sanitiser the session package uses for tab IDs. CodeQL can't see across that function call, so it doesn't count the value as sanitised. The other value, proto, can only be one of the server's own entries in supportedVersions. No log entries can be forged this way, so I'm leaving the code as it is.
Generated by Claude Code
…rint Close the remaining WebDriver gaps on top of new cdp.Tab primitives: - Switch To Frame / Parent Frame for same- and cross-site (out-of-process) frames at any depth, via cdp.Tab.InFrame and iframe auto-attach. - Get Element Shadow Root and Find From Shadow Root, including closed roots; shadow roots round-trip through scripts. - Alert commands and unhandledPromptBehavior; calls blocked by a dialog fail fast instead of hanging. - Multiple windows: New Window, popups joined automatically, switching, closing; the session ends with its last window. - Window rect, maximise, minimise and fullscreen, applied only to windows that hold nothing but Tether tabs. - File inputs via Element Send Keys plus Selenium's /se/file upload. - Print Page. - Focus emulation on attached tabs so input reaches background tabs with cross-site iframes promptly. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
POST/DELETE /session/{id}/actions dispatch key, mouse, pen, touch and
wheel input as real CDP events, tick by tick, with element/pointer/viewport
origins, animated moves and scrolls, click counting, US-layout Shift, and
input state that persists until Release Actions.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnJnokVdV3oFBMpLc45zcJ
…t click Chrome routes input into frames asynchronously, and on some versions a real click into a cross-site frame of a background tab never arrives. Arm a one-shot listener before clicking inside a frame, wait briefly for it, and click from script if the real click did not land. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Attaching to a tab that is closing can fail after Target.getTargetInfo succeeds (Page.enable: session not found). Map that to ErrNoSuchTab so faces answer 410/no such window instead of 500. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Resolves conflicts with #7 (console logs): Tab gets both Guard and Logs, the API keeps the policy routes and adds /tabs/{id}/logs, and docs list both features. Also makes TestConsoleLogs tolerate a late favicon 404 between the two WebDriver log reads (it asserted the second read was empty; it should assert no entry repeats). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
8000
Tab gains Mouse/Touch alongside Guard and Logs; the WebDriver session keeps the log cursor and the Actions input state; docs list everything. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts: # README.md # internal/cdp/cdptest/cdptest.go
Conflict resolution plus one fix the combination needs: popups and new windows now belong to the session that opened them. A popup is attached as the session's own tab (same owner, same session rules, guarded), so a non-admin token can switch to its popups, and a restricted session can no longer leave its allowlist through window.open or New Window. A popup that loaded a forbidden page before Tether attached is closed and the switch fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts: # CHANGELOG.md # docs/WEBDRIVER.md # internal/cdp/cdptest/cdptest.go # internal/webdriver/errors.go # internal/webdriver/webdriver.go # test/integration/integration_test.go
Conflict resolution: the fake tab serves MCP's OnCall/Props and the WebDriver wrappers; app wiring keeps per-token credentials and the optional MCP face; docs describe MCP alongside per-token policies. TestNavigateHonoursPolicy now simulates drift with SetURL, since the browser-side guard (from policies) refuses the navigation it used. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts: # README.md # internal/cdp/cdptest/cdptest.go # test/integration/integration_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Adeel · project thread
What
Before:
/mcpanswered501, and agents could only reach Tether through the proxy or WebDriver.After:
/mcpis a working Model Context Protocol server.claude mcp add --transport http tether http://127.0.0.1:8787/mcpgives an agent 16 tools to work in your real, logged-in Chrome: tabs (list/new/select/close), navigate, back/forward/reload, snapshot, click, type, press key, select option, scroll, wait for, eval, screenshot, and an in-tabfetchthat uses your cookies. Claude Desktop and other stdio clients connect withtether mcp, a bridge that also works across a tailnet.Mcp-Session-Id, protocol versions are checked, batches work, andDELETEends a session.[e42] button "Sign in". Refs are Chrome backend node IDs (the same scheme as WebDriver). A ref from a page that has since changed fails asstale_element.DELETE, or idle past--idle-timeout), only the tabs it opened are closed. Tabs it merely selected, such as the user's own, stay open.isErrorresults with stable codes (tab_gone,forbidden,stale_element,element_not_interactable, …) and a hint the model can act on.--mcp/TETHER_MCP(default on).Bug fix: a screenshot of an idle background tab used to hang until the request timed out, because Chrome doesn't paint hidden tabs. This affected WebDriver too.
cdp.Tab.Screenshotnow tries for 2s, then brings the tab to the front and tries again, which is what ChromeDriver does.Why
Adeel wants all the stubbed faces in the first release. MCP is the face the README pitch leads with ("give your AI agent your real, logged-in browser").
How
The MCP face stays thin. Every tool is a few calls into the core:
Sessions,Core.Navigate/Eval/Do,core.Policy, andcdp.Tab. Nothing ininternal/proxychanged.How tested
make checkequivalent: gofmt,go vet, golangci-lint v2.5.0 (0 issues),go test -race ./....internal/mcpcoverage is 86%.make test-integrationagainst real headless Chromium. The newTestMCPlogs in, snapshots, types and submits a form, uses eval and wait_for, goes back, clicks, presses keys, scrolls, fetches with cookies, takes viewport/element/full-page screenshots, lists tabs, and checks that ending the session closes its tab. The existing proxy and WebDriver tests also pass.tether serve --launch --headless --token …, and drove it throughtether mcp(navigate, snapshot, type, click, eval, screenshot, ping).docs/MCP.md(client setup, tools, limits, security). Also updated the README, ARCHITECTURE, SECURITY threat model, CHANGELOG and CONTRIBUTING.Security
/mcpsits behind the same token auth and loopback Host/Origin guard as every other face. A test covers this throughserver.New.--allow-domainsis enforced on navigate and fetch. It is also checked against the page's current URL before every page tool, so an agent that clicks its way off the allowlist can't read or act there. If the URL can't be parsed, the check refuses.Set-Cookieis never relayed, and tool arguments are never logged.docs/MCP.mdand the threat model.🤖 Generated with Claude Code
https://claude.ai/code/session_01HpbHKDrhpTtF3U8jAhD61N
Generated by Claude Code