Give your agent real tools, each with its own safety edge
An integration appearing in the tool catalog doesn't mean this machine already has the matching runtime, credentials, or system permission.
Browser: drive real pages through Chrome DevTools Protocol#
Browser lets your agent operate a real web page — for UI inspection, interaction verification, and tasks that need actual browser context.
Browser automation is a personal experimental capability, off by default. You have to enable it explicitly in Settings' advanced section or your personal config.toml — a project's configuration can never turn it on for you. While it's off, browser doesn't even appear in the tool_search catalog, and a direct call is rejected by the backend.
How it runs#
kxen probes for a locally installed Google Chrome, Chromium, or Microsoft Edge in that order, and drives it through Chrome DevTools Protocol using an isolated headless profile. chromiumoxide gives it a separate, temporary user-data-dir — it never attaches to a browser window you already have open, and never inherits your everyday profile's cookies. kxen doesn't download or bundle a browser itself.
Before you can use it, you need:
- Google Chrome, Chromium, or Microsoft Edge already installed locally.
- kxen able to start a headless debugging instance.
- No conflicting process holding the target debug state.
Capabilities#
Browser can navigate pages, read visible state, click, type, and run page-level checks. Exact available actions follow the current Browser tool contract.
Network boundary#
The initial navigation URL goes through a scheme and network-address check first. Once Chrome starts, all HTTP and HTTPS traffic is pinned through kxen's local proxy with no direct fallback — this boundary covers redirects, iframes, subresources like images and scripts, in-page fetch, and WebSockets alike.
The proxy resolves the target domain exactly once, checks every address that resolution returns, and only connects using an already-checked specific address. If any result is loopback, private, link-local, CGNAT, unspecified, or multicast, the whole request is rejected before connecting. HTTPS uses a CONNECT tunnel, and the TLS SNI and HTTP Host stay the original domain — so pinning the address never requires weakening certificate validation. DNS rebinding can't redirect an already-checked request to a different address.
Resolution, headers, DNS results, connection time, idle time, and bidirectional traffic all have hard limits. Request/response body and tunnel traffic are capped at 64 MiB per direction; a streaming request body without Content-Length fails closed. Exceeding a limit or a proxy error fails the connection closed. After every non-close action, kxen still re-checks the top-level URL, and closes the entire browser instance if it finds a non-HTTP(S) or restricted target.
Browser stays experimental — not because iframes, subresources, or DNS rebinding are uncontrolled, but because web data, external interaction, and local browser automation genuinely need careful authorization on their own. It can never be used to route around Web or MCP network restrictions.
Data boundary#
Browser's isolated profile can retain login state and personal data formed during automation, but it's never the same as your everyday Chrome profile. Page content and interaction results can also enter the current model provider's context. The agent only operates within a task scope you explicitly authorized — submitting forms and other external writes still need to be treated by risk.
Compared to Web Search#
Web & Search is for extracting a known page, discovering sources, and getting search summaries. Browser is for real interfaces and interaction. When you only need material, prefer the smaller read surface.
LSP: structured code intelligence#
LSP gives the agent structured code information. It complements file search and text reading — it doesn't replace your compiler, tests, or your project's own static checks.
Supported languages today#
Rust, TypeScript, JavaScript, Python, Go.
The matching language server has to already be installed, and discoverable on the kxen app process's PATH.
Supported actions today#
Diagnostics, hover, definition, references, symbols.
kxen lazily starts a language server per language on first call, and later calls reuse the running process for the current Workspace.
Graceful degradation#
When a server is missing, initialization fails, or a file type isn't registered, LSP simply returns unavailable — basic Session work, file tools, and model calls continue normally.
Workspace boundary#
A language service is rooted at the current Workspace. After switching projects, diagnostics and symbol results must come from the new working directory — never a stale index from another repository.
Verification#
LSP diagnostics are a point-in-time result from the language service. You should still run your project's own build, lint, and test commands after any code change.
MCP: connect local or remote Model Context Protocol servers#
MCP connects kxen to external tools, resources, and prompts. Every server has its own transport, authentication, tool policy, and running state.
Once connected, kxen follows the MCP nextCursor to pull every page of tools/list, resources/list, and prompts/list, deduplicating by tool name, resource URI, and prompt name. To bound a misbehaving server that returns pages forever, each catalog type reads at most 100 pages and 10,000 entries; if the cursor loops or a later page fails, kxen keeps the catalog it already validated and logs the reason in diagnostics.
Transport#
kxen supports:
- stdio
- Streamable HTTP
- SSE
A stdio server starts from a local command, with its working directory fixed to the current Workspace. The subprocess doesn't inherit your full app environment: kxen runs env_clear first, keeping only HOME, LANG, LC_ALL, LC_CTYPE, LOGNAME, PATH, SHELL, TMPDIR, and USER, then layers on whatever env the configuration explicitly declares. A remote server uses a URL, optionally with fixed headers or OAuth configuration. Configured Remote MCP URLs and OAuth metadata overrides must use https://, and a URL can never embed a username or password. OAuth discovery's authorization, token-exchange, and dynamic-registration endpoints also reject plain public HTTP — only local protocol testing is allowed to use loopback HTTP. Remote MCP never advertises the roots capability by default, and never sends the current Workspace path to a remote server; even an unsolicited roots/list request from a remote server only ever gets an empty list back.
Streamable HTTP and SSE are Remote MCP, off by default — they only load once you explicitly enable them in personal Settings or config.toml. A project's configuration can never turn this on for you. A remote call sends tool arguments to that server; turning the toggle off immediately reloads the current Workspace's MCP runtime and removes the remote server. Reload and restart close the old transport, and wake up any call still waiting on that old connection.
Scope#
User-level MCP is visible across every Workspace. Project MCP belongs to the current Workspace — a project server with the same name overrides the user-level one.
Project MCP only loads once the Workspace is trusted. For a project's stdio server, beyond Workspace trust, you also separately approve the exact canonical absolute command, args, cwd, and env. Env vars that can redirect which executable or runtime loads — PATH, NODE_OPTIONS, PYTHONPATH, RUBYOPT, PERL5OPT, LD_*, DYLD_*, and similar — are rejected outright. The approval panel shows ordinary env values as-is; sensitive env only shows the key and a SHA-256 digest. Without an approval channel, or if you deny it, it fails closed. Within the same process, an approval is reused by matching the exact configuration fingerprint — any change to command, args, cwd, or any env value triggers a fresh prompt. If a same-named project server isn't approved, kxen can still fall back to the user-level server.
Tool policy#
Every MCP tool can be set to:
- allow
- ask
- deny
ask goes to Approval before the call. deny never executes, no matter what the model requests. MCP tools also still pass through role permissions and the general Safety layer.
MCP configuration loads fail-closed. A server key must be 1 to 32 ASCII bytes matching [A-Za-z0-9_-], and can't contain __. If the file is unreadable, the JSON is broken, a server definition is invalid, or toolPolicies isn't allow/ask/deny, that reload fails explicitly and the current runtime is kept — an error is never interpreted as an empty configuration, and an invalid policy is never silently treated as the default allow. A remote tools/list also individually validates provider-safe tool names, description size, and a restricted inputSchema; an invalid entry logs a diagnostic and is skipped on its own, without polluting the rest of that batch.
Resources and prompts#
A server that declares the Resources capability provides a read-only directory and read tool. The resource directory paginates by cursor, 20 entries per page by default and 100 maximum, so anything beyond the first 20 entries is still discoverable and readable. Binary resources are never expanded directly into model context.
A server that declares the Prompts capability provides a prompt directory and a prompts/get tool. The directory retains each prompt's argument definitions and required flags; a call first validates that the prompt exists, that required arguments are present, and that argument values are strings. prompts/get's result comes back to the agent as JSON, bound by the same 50,000-character single-output cap as any other MCP tool.
Where you see it#
Settings' advanced section shows MCP server status, tool count, and resource count. On first open, it waits for the current Workspace's MCP runtime to finish its initial load, instead of misreporting a not-yet-loaded state as unconfigured; if a project stdio server is waiting for approval, the persistent approval panel shows that request at the same time. You can initiate remote interactive authorization, poll for the authorization result, and manually restart a server.
Credentials#
Remote tokens, OAuth refresh tokens, OAuth client secrets, and fixed sensitive headers should never be committed to a project repository. OAuth data lives in the app data directory's mcp-oauth.json, with 0600 file permissions, and updates through fsync and rename — it's local, permission-restricted JSON, not the macOS Keychain. A token is bound to the configured scope, server name, and exact canonical resource endpoint: host case and default port get normalized, URL fragments are ignored, and a path or query change means it's never reused. So a same-named project server never inherits a user-level token, and changing host, port, path, or query means re-authorizing. An older token saved only by server name or only by URL origin fails closed for lacking this identity evidence, and needs re-authorization. Project configuration can describe a connection, but explicitly rejects oauth.clientSecret and any Authorization, Cookie, API-key, token, secret, or credential-class header — those secrets can only live in your user-level auth store or personal configuration. Before enabling Remote MCP, confirm the server's operator, data retention, and access scope.
Failure boundary#
A running server process doesn't mean the MCP handshake has completed. When diagnosing, check transport, authentication, initialization, tool listing, and the actual call result separately.
An ordinary request waits 30 seconds by default; initialize waits 60 seconds. On timeout, when you stop the Session, or when the call's future gets dropped, kxen removes the local pending request and best-effort sends notifications/cancelled. That stops the local wait, but can't guarantee the server rolls back a side effect that already happened externally.
When a transport closes, stdio first sends SIGTERM to the isolated process group, escalates to SIGKILL after 800 ms, and waits to reap the direct child process, avoiding leaked shell grandchildren. A remote reader stops, and Streamable HTTP best-effort deletes the server session. Network or server failure can still leave the remote cleanup result as UNKNOWN.