Development¶
Setup from a checkout¶
git clone https://github.com/junyeong-nero/kitsune ~/workspace/kitsune
cd ~/workspace/kitsune
uv sync
Register the checkout instead of the PyPI package so the client runs your working tree directly:
claude mcp add --scope user kitsune -- uv run --directory ~/workspace/kitsune kitsune
In JSON or TOML configs, use "command": "uv" with
["run", "--directory", "/Users/you/workspace/kitsune", "kitsune"]. Load the
extension from the checkout's extension/manifest.json (that is the developer
build). To try the signed variant locally, build it and load
build/extension-signed/manifest.json the same way:
uv run python scripts/build_extension.py signed
To test the installed-package path of a branch, build both packages and run the server from their wheels; rebuild after you edit files:
uv build --all-packages
uvx --from dist/kitsune_browser_mcp-<version>-py3-none-any.whl \
--with dist/kitsune_browser-<version>-py3-none-any.whl kitsune
(uvx --from ./packages/kitsune-browser-mcp kitsune instead runs the working tree's
code, which uv installs editable.)
Tests¶
Run the tests from the repository root:
uv run python -m unittest discover -s tests -q
for test in tests/*.cjs; do node "$test"; done
The Python regression suite covers the MCP tool surface, snapshots, navigation
waits, and bridge lifecycle; the Node harnesses cover content-script behavior
such as refs, form filling, and password redaction. For an end-to-end check, load the extension in a
supported browser and follow Try it; unit tests alone do not verify
your live browser connection. File reproducible bugs in
GitHub Issues, including the
browser version, bridge build from browser_status, tool calls, and expected
versus actual behavior. Use a minimal public or local page and redact secrets.
Tool definitions are resent with every model request, so their size is tracked:
uv run python scripts/tool_schema_size.py reports the compact JSON size of each
core tool in two views: the full definition, and the definition with its description
cut to the first paragraph, as some clients (Claude Code --bare) send it. A test fails
until --update records a change in tests/tool_schema_sizes.json. The same test fails
when the core total grows past TOTAL_BUDGETS or FIRST_PARAGRAPH_BUDGETS, or any
single tool past TOOL_BUDGET, or the server instructions of a mode past
INSTRUCTIONS_BUDGETS; raise a budget only deliberately. The core tools are
Playwright MCP's, published verbatim, and tests/test_playwright_tools.py checks the
default tools/list against src/kitsune/playwright_tools.json. The Kitsune-owned tools'
published input schemas omit the title keywords pydantic generates (KitsuneMCP.list_tools), and
tests/test_tool_schema_titles.py checks that they still accept and reject the same
arguments. They also publish an optional parameter whose default is null as its plain
type instead of anyOf: [type, null], and omit null and "" defaults. Calls are
still validated against the pydantic models, so an explicit null still works and
behaves as omitted; tests/test_tool_schema_optionals.py checks this for every tool.
Every client sends input schemas in full, so parameters are documented there
(Annotated[..., Field(description=...)]), not in Args: blocks. A tool's first
paragraph says what it does and what a caller must know. The Kitsune-owned action tools
share one contract (ref, element, wait, expect, confirm_token, first; confirm_token is the
approval fallback for clients without elicitation), documented once in
browser_check's parameter descriptions; the other action tools say in their first
paragraph that ok means dispatched, name expect and point to browser_check rather
than repeating it. tab_id is described wherever it is accepted.
tests/test_tool_first_paragraph.py checks these rules for every Kitsune-owned tool.
Real-browser suites¶
The opt-in real-browser suites (tests/test_real_browser*.py) run against a real
Firefox, Zen or LibreWolf. They cover action races, tab unloads and reconnects
(test_real_browser.py), tabs opened by target=_blank clicks with popups allowed
and with the default popup blocker (test_real_browser_opened_tabs.py), every
read tool on empty 404 and 500 pages (test_real_browser_error_pages.py), and
which form or button an action picks on a page with repeated controls
(test_real_browser_action_targets.py). They are skipped unless KITSUNE_E2E_BROWSER
names the browser binary. A full run takes several minutes, depending on the host
and browser; run one suite when iterating:
KITSUNE_E2E_BROWSER=/Applications/Zen.app/Contents/MacOS/zen \
uv run python -m unittest discover -s tests -p 'test_real_browser*.py' -v
Use -p test_real_browser_error_pages.py (or another file name) to run one suite.
Each test class starts a headless browser on a new temporary profile and HOME, plus
its own bridge and fixture site on spare ports, and kills them afterwards. Your
browser profiles and running bridge are not touched. The suites run a copy of the
extension whose bridge port is rewritten to the test bridge. The browser does not
start if any script in the copy still names port 47652, or if the test bridge would
use that port. The shared harness, tests/real_browser_harness.py, documents how to
add a suite, including how to choose the profile prefs (browser defaults or
popups allowed).
To keep evidence from a failing run, set KITSUNE_E2E_ARTIFACTS to a directory:
KITSUNE_E2E_BROWSER=/Applications/Zen.app/Contents/MacOS/zen \
KITSUNE_E2E_ARTIFACTS=/tmp/kitsune-e2e-artifacts \
uv run python -m unittest discover -s tests -p 'test_real_browser*.py' -v
Each failing test, or failing class setup, writes one directory under
kitsune-e2e-<UTC time>-<pid>/<module.Class>/ and prints its path to stderr.
timeline.jsonl puts the harness's bridge requests, the fixture server's request
events and the browser's top-level navigation events (load start/stop, location
changes, tab open/close) on one wall clock, so a slow page load can be told apart from
a slow fixture reply. For each fixture request it records when the request was
received and then either request-handled (the handler returned and the server
flushed its reply) or request-failed with the exception type (for example
BrokenPipeError after the browser dropped the connection; the error still reaches
the server). Both are server-side only and do not show that the browser received the
reply. Only bridge requests sent through the harness's call() are in the timeline;
open_tab (/api/new-tab) is, but suites that send harness._request directly
(test_real_browser.py, test_real_browser_protected_tabs.py,
test_real_browser_submit_form.py, test_real_browser_connection_diagnostics.py)
appear only in bridge.log. Harness and fixture events share one buffer of 4000
events per test process; summary.json reports how many events recorded since the
class started were evicted (droppedSinceClassStart, with complete: false when
any were) and, separately, all evictions in the process (droppedInProcess).
browser.log and bridge.log hold the last 256 KiB of the owned browser's and
bridge's output, resources.json the load average and ps samples of the test runner,
the bridge and the browser's process group, and summary.json what was collected. Passing classes write nothing; a class
keeps at most five failures; nothing under the directory is ever deleted by the
harness. The timeline and summaries hold no headers, request bodies or page contents,
and URLs in them keep only a loopback origin and path. The two log tails are copied as
the owned processes wrote them, with the bridge token, Authorization, Bearer and
token= values redacted; with the variable set, the owned bridge writes its log
unbuffered so that it is current when a test fails. Without the variable the suites
run exactly as before.
Releasing¶
To publish a new version to PyPI, see Releasing.
Layout¶
extension/ WebExtension (manifest v2, Gecko)
packages/kitsune-browser-mcp/ The MCP server's PyPI package: pins kitsune-browser[mcp], adds the commands
src/kitsune/ The kitsune-browser package (the repository root's pyproject.toml)
server.py MCP server: registers the tools on FastMCP, within KITSUNE_CAPS and KITSUNE_MODE
tools.py The agent tools (Kitsune's own) without MCP code; playwright_tools.py has the core group
browser.py Python API (kitsune.connect, Browser, Tab); errors.py has its exceptions
bridge_client.py Bridge requests and connection errors; starts the bridge on demand
bridge.py Bridge daemon (HTTP API + WebSocket)
workflows/ Named workflows for browser_workflow
docs/ Documentation site (mkdocs.yml), API reference, scraping playbooks
Credits¶
Kitsune started as a fork of ZenLink
and ZenLink-MCP by Jay-Quan
(MIT). Changes since the fork: token auth, Host and origin checks, no CORS,
on-demand shared daemon, one package, browser_* tool names, ~/.kitsune data
directory, macOS support, mcp<2 pin.
License¶
MIT. See LICENSE.