Agent Mode (MCP)
Agent Mode is how an AI agent works with your KeepKey. The goal is a wallet flow driven end to end: find and open the site, drive the dApp’s interface, decode what the dApp is actually asking for, explain it in plain terms, and walk you through the review. You sign on the device. The agent does the work and the explaining; you and the KeepKey remain the signing authority.
MCP (Model Context Protocol) is a standard protocol in which a server publishes a catalog of callable tools that clients discover with tools/list and invoke with tools/call. KeepKey publishes its tools over MCP, so an agent needs no custom transport to reach them. MCP standardizes the plumbing, not the setup: you still need the KeepKey Vault running, a pairing key for the agent to present, and Agent Mode switched on. Being able to speak MCP does not by itself grant an agent access to your wallet.
Availability
Agent Mode is under active development and is not in the latest published extension release. The five introspection tools described below are implemented and merged to the development branch; the browser-driving tools are an open pull request and do not work at all yet. If your extension’s Settings has no Agent Mode (MCP) section, your build does not have it.
The device is the gate
An agent can navigate, click, type, read state, and explain. It can assemble a transaction and drive the extension’s side of an approval right up to the moment of signing. What it cannot do is press the button on your KeepKey. Signing requires confirmation on the device itself, and no MCP tool can perform that press.
This is the invariant worth holding onto. It is not that today’s tools happen to be read-only — that is a fact about this version, not a promise about the product. The tools will grow. The button will not move.
Two honest qualifications. The confirmation is per transaction, not per signature: a multi-input Bitcoin transaction produces several signatures from one confirmation. And the button proves that a human approved; it does not vouch for what the agent composed. Read the device screen rather than treating the press as a formality — particularly for payloads the device cannot render meaningfully, such as raw-byte message signing.
Why the tools live in the extension
A browser-automation tool that launches its own throwaway profile has no extension in that profile, so it cannot exercise a dApp as a wallet user. That is a fact about the profile, not about your machine: the vault runs as a separate process and keeps its pairings in an on-disk database, and the KeepKey is a USB device attached to the host. Both are there regardless of which browser is running.
Pointed at your real Chrome, a general-purpose tool with JavaScript access can already read what any page can read — the extension injects its provider into every page, so eth_accounts and eth_chainId are available to page script without any bridge. What it cannot reach is the extension’s own internal state: the approval queue, the provider-call log, the connected-origins registry, and the bridge and service-worker internals. Those live in the background service worker’s memory, and they are what the bex_* tools add.
There is also a hard constraint. Playwright and Puppeteer cannot run inside the extension at all: the background bundles to a single IIFE with no Node builtins. Driving the browser from within the extension is the only way to put the driving and the wallet-side observation in one process.
What works today
Five introspection tools: bex_status, bex_accounts, bex_pending_requests, bex_connected_sites, and bex_logs. They report device, account, and session state.
The browser-driving tools that would complete the flow described above are in development and are not available. Agent Mode is off by default, and the agent endpoints accept only local, non-browser callers.
Setup
Agent Mode connects an MCP-speaking agent — Claude Code, Claude Desktop, or any other MCP client — to your wallet over localhost. Connecting an agent does not hand it signing power.
Prerequisites
- KeepKey Vault running, with its local server on
http://localhost:1646. The vault hosts the MCP endpoint, not the extension — a Chrome MV3 extension cannot listen on a port. The extension connects outbound to the vault over a WebSocket. - The extension paired with the vault. The existing pairing key is the bearer token your agent presents. There is no separate MCP key and no separate MCP pairing flow.
Steps
- Open the extension side panel and go to Settings → Agent Mode (MCP).
- Turn on Enable MCP. It is off by default. While it is off, the extension opens no socket.
- Press Copy agent config. This copies a ready-to-run
claude mcp addcommand with your pairing key already embedded. If the extension is not paired yet, the button tells you so instead of copying. - Paste it into your terminal.
The copied command has this shape, with <KEY> replaced by your real pairing key:
claude mcp add --transport http keepkey http://localhost:1646/mcp --header "Authorization: Bearer <KEY>"For other MCP clients (.mcp.json, Claude Desktop), configure it manually:
{
"mcpServers": {
"keepkey": {
"type": "http",
"url": "http://localhost:1646/mcp",
"headers": { "Authorization": "Bearer YOUR_PAIRING_KEY" }
}
}
}The copied string contains your pairing key. Treat it like a password: paste it only into your own agent, and never into a chat, an issue, or a screenshot.
Verify it works
Ask your agent to call bex_status. A healthy response reports:
deviceConnected: true— your KeepKey is plugged in and respondingvaultPaired: true— the extension holds a pairing keywalletInitialized: true— the wallet is loaded and ready to answer account queriesbridge: "up"— the extension’s outbound WebSocket to the vault is live
bex_status answers even when the bridge is down, so it is the right first call when something looks wrong. If it reports bridge: "down", Enable MCP is off or the extension has not reconnected yet — reconnect backoff runs from 5 seconds up to 60 seconds. If vaultPaired is false, re-pair the extension with the vault and copy the config again, since the key will have changed.
Introspection tools
These five tools are the current surface. They read state and observe traffic, and none of them can sign.
Transport and authentication
The MCP endpoint lives in the KeepKey Vault, not in the extension. Agents speak Streamable HTTP to POST http://localhost:1646/mcp. The server identifies itself with serverInfo.name set to keepkey-vault.
There is no MCP session. Each POST is an independent JSON-RPC request, authenticated on its own. Clients conventionally send initialize first to negotiate a protocol version, but it establishes no state and is not what grants access — tools/list and tools/call are answered without it. The server’s own protocol version is 2025-06-18, and it accepts 2025-06-18, 2025-03-26, and 2024-11-05, echoing back the version you request when it is one of those and otherwise answering with its own.
POST /mcp requires an Authorization: Bearer <pairing key> header, using the existing vault pairing key. A request with no token or a bad one returns 401. Two checks run before authentication: a non-loopback caller is rejected with 403 loopback only, and any request carrying an Origin or Sec-Fetch-Site header is rejected with 403 /mcp is not reachable from a browser. That second check excludes all browsers, including same-origin and loopback ones, because the vault serves browser content at its own localhost origin — so a localhost-origin allowlist would not be a boundary. Browsers cannot strip either header; non-browser agents send neither.
The tools
| Tool | What it returns | Arguments |
|---|---|---|
bex_status | bridge, extension version, keepkeyState and keepkeyStateName, deviceConnected, vaultPaired, walletInitialized, activeEvmNetwork (chainId, networkId, name) or null, swStartedAt, bridgeConnectedAt | none |
bex_accounts | Per-chain request_accounts result in the raw shape a dApp receives, plus pubkeys. A chain that fails returns an error field instead of blocking the others | chains (optional array; defaults to ethereum, bitcoin, bitcoincash, dogecoin, litecoin, dash, thorchain, mayachain, cosmos, osmosis, ripple) |
bex_pending_requests | The live approval queue: key, dApp-supplied id, method, params, origin, chain, requestedAt | none |
bex_connected_sites | Origins that have made provider requests, with chains touched and firstSeen / lastSeen | none |
bex_logs | Ring buffer of provider requests and responses (ts, origin, chain, method, params, result or errorCode / errorMessage, durationMs) | pattern, since, limit (all optional) |
What they are good for
bex_pending_requests is the decode-and-review step. When a dApp asks you to approve something, the exact method and parameters land in this queue, and the agent can read them and explain what the site is actually requesting before you look at the device. Address entries by key, not id — id is dApp-supplied and two tabs commonly collide on the same value.
bex_logs exposes provider traffic for debugging a stuck connection: filter by pattern (a case-insensitive regex matched against method, origin, chain, and error message), by since (epoch milliseconds), or cap with limit (default 100). The buffer holds the most recent 500 entries and long results are clipped.
bex_accounts is not an inert read. It routes through the same path page traffic takes, so calling it can trigger wallet initialization. The default chain list is limited to chains whose request_accounts is a cache read, so it does not require a device round-trip.
All of these registries are in-memory and are wiped when the MV3 service worker restarts. Compare swStartedAt across calls to detect a wipe.
Browser-driving tools
Not available. These nine tools live on an open pull request and are not merged. Even once merged they stay inert until a vault change lands — see Why they do not work yet. Nothing in this section works today, and none of it is callable: the vault’s
/mcpadvertises the five introspection tools only, and rejects any other name.
Introspection tells an agent what the wallet sees. Browser driving is what would let it do the rest of the job: open the site, drive the dApp UI, and walk you through what the dApp is asking for.
The proposed tools
| Tool | Arguments | What it does |
|---|---|---|
bex_tabs | action (list | create | close | select), tabId?, url? | List, open, close or focus tabs. Defaults to list. |
bex_navigate | url, tabId? | Navigate a tab and wait for load. |
bex_snapshot | tabId? | Accessibility snapshot of interactive elements, each with a [ref=eN] handle. |
bex_find | text, regex?, tabId? | Search the snapshot by accessible name; returns only hits and their refs. |
bex_click | ref, element, tabId? | Click an element by ref. |
bex_type | ref, element, text, submit?, tabId? | Type into a field, replacing its value. submit presses Enter. |
bex_select | ref, element, value, tabId? | Choose an option in a select. |
bex_read_page | selector?, maxChars?, tabId? | Rendered text, defaulting to main then body. |
bex_screenshot | tabId?, quality? | JPEG of the visible viewport, quality clamped to 1-100, default 60. Focuses the tab first, since Chrome can only capture a window’s active tab. |
element on the acting tools is a human-readable description of the target. Omitting tabId uses the active tab.
The ref model
The agent never writes a CSS selector and never clicks a coordinate. bex_snapshot returns an accessibility-shaped listing where every interactive element carries an opaque handle, and the agent acts on those handles:
page https://app.example.com/swap — "Swap"
- heading "Swap" [level=1]
- button "Connect Wallet" [ref=e1]
- textbox "You pay" [ref=e2] value="0"
- button "Swap" [ref=e3] [disabled]bex_click with ref: "e1" clicks Connect Wallet. Refs are re-minted on every snapshot, so re-snapshot after anything that changes the page; a stale ref is rejected rather than guessed at. This is the convention Microsoft (Playwright MCP) and Google (Chrome DevTools MCP) arrived at independently.
Screenshots are for visual verification only. You cannot act on one — there are no refs in a JPEG, and no coordinate clicking. Use bex_snapshot to find things.
Making it work on real dApps
Wallet UIs break naive automation, so the design accounts for how they are actually built. Open shadow roots are pierced, because web3modal and other Lit-based connect modals live inside them (closed roots remain invisible). Clicks dispatch a full pointer sequence rather than a bare click, because Radix and Headless UI listen on pointerdown. Typing goes through the prototype’s native value setter, which defeats React’s value tracker — a direct value assignment is a no-op to React. Disabled elements refuse clicks, since “the Swap button is disabled until you enter an amount” is exactly the gating an agent run should surface rather than bypass.
This costs no new Chrome permissions (<all_urls> and tabs are already granted) and no new dependencies. It does not use chrome.debugger, so there is no “extension is debugging this browser” infobar.
Why they do not work yet
The vault’s /mcp endpoint hardcodes its tool catalog and wraps every tool result as a text block. So tools/list never advertises the new tools, tools/call rejects them at the tool-name guard before the bridge is consulted, and bex_screenshot has nowhere to put a JPEG — MCP carries images as image content blocks with data and mimeType.
The fix is roughly twenty lines: make /mcp a dumb pipe that proxies tools/list to the extension and passes content blocks through verbatim. The extension would then own its whole tool surface, and future tools would ship without a vault release. Both changes have to land — the pull request and the vault change — before any of this section describes reality.
Security: the signing boundary
Signing requires confirmation on the KeepKey itself. An agent can read state, explain what a dApp is asking for, and — when browser driving lands — click through the dApp on your behalf. It can carry a transaction all the way to the point of signing. It cannot press the button.
The safety promise is the device confirmation, not “the tools are read-only”. Read-only describes today’s surface and nothing more.
One scope note: this guarantee is about physical KeepKey hardware. The vault also supports an opt-in, user-installed emulator that runs firmware as a local library and confirms with an on-screen click instead of a button press. Emulator signing carries no hardware guarantee and should not be treated as equivalent to device signing.
Off by default
Agent Mode is stored under keepkey-agent-mode and defaults to off. While it is off, the extension opens no socket and exposes nothing. The toggle is the real data gate, not the tool list.
To turn it off, open the side panel, go to Settings → Agent Mode (MCP), and switch off Enable MCP. The extension closes its WebSocket immediately.
With the bridge down, bex_accounts, bex_pending_requests, bex_connected_sites and bex_logs return an error result carrying bridge_disconnected. bex_status is deliberately exempt and returns a normal result reporting bridge: "down", so an agent can always get a truthful liveness answer.
How /mcp is guarded
The MCP endpoint lives in the KeepKey Vault at http://localhost:1646/mcp and applies three independent checks, in this order:
- Loopback only. A non-loopback caller is rejected with 403 before anything else.
- No browsers. A request carrying an
OriginorSec-Fetch-Siteheader is rejected with 403, including same-origin and loopback ones. - Bearer token.
POSTrequires a valid pairing key, or returns 401.
The agent endpoints are protected by these explicit per-route checks rather than by the socket binding: the vault process itself listens on all interfaces, so other endpoints such as /api/health are reachable from your local network. The bridge itself is outbound to loopback, and the shipped introspection tools make no outbound internet calls. If browser-driving lands, an agent will be able to navigate your browser to arbitrary internet sites — that traffic is your browser’s, not the bridge’s.
The pairing key is a secret
The token is your existing vault pairing key, not a separate MCP credential. Copy agent config embeds it in the command it copies. It is loopback scoped, so it is not usable from another machine, but treat it like any other secret: paste it into your own agent, never share it, never commit it. To rotate it, re-pair the extension with the vault.
What an agent can see when enabled
With Agent Mode on, an agent can read your addresses and accounts across chains, your device status, the approvals a dApp is currently waiting on, the sites you have connected, and your provider traffic logs. That is privacy relevant. It is a view of your wallet’s activity, handed to whatever agent holds the key.
When browser driving lands
The browser-driving tools are in development. When they ship, an agent will be able to act in your browser as you: open tabs, fill forms, click through a dApp. That is real power, and it is worth stating plainly rather than reassuring you.
Be clear about what the device button does and does not cover. It gates signing on the KeepKey. It does not gate anything else an agent can reach in a browser where you are already logged in — an exchange withdrawal, a hot wallet, an email password reset. Those paths never touch the device. The button is the guarantee for your KeepKey-held funds; it is not a general guarantee about everything an agent does in your Chrome. Enable browser driving with that in mind, and only for agents you would trust with a logged-in browser session.
Troubleshooting
Almost every failure is one of two things: a missing or stale bearer token, or Agent Mode being off in the extension.
401 {"error":"No bearer token — pair first via POST /auth/pair"}
The request reached the vault with no Authorization header. /mcp has no anonymous mode. Re-add the server with the header, and check that your MCP client actually forwards headers from its config.
401 with a key supplied
The body reads {"error":"Invalid or expired API key — re-pair via POST /auth/pair"}. The key is not a currently valid vault pairing key — usually a config copied before the extension re-paired. Re-pair the extension with the vault, then use Settings → Agent Mode (MCP) → Copy agent config to get the live key and update your MCP config. That config contains a secret; do not commit it.
403 {"error":"/mcp is not reachable from a browser"}
The request carried an Origin or Sec-Fetch-Site header. /mcp refuses all browser callers, even with a valid key and even from localhost. Call it from a local process — your agent CLI — not from a web page or a devtools console.
403 {"error":"loopback only"}
The request came from another machine. The agent endpoints accept loopback callers only. Run your agent on the same computer as the vault.
Connection refused on port 1646
KeepKey Vault is not running. Start it and retry. The MCP endpoint is served by the vault, not by the extension.
Tools answer bridge_disconnected
The message is KeepKey extension is not connected to the vault bridge (is the BEX running with Agent mode enabled?). The vault is up, but nothing is on the extension end of the bridge. Check, in order: Enable MCP is on in side-panel Settings (it defaults to off), Chrome is running with the extension loaded, and the extension is paired with the vault. bex_status still answers while the bridge is down, reporting bridge: "down" — use it to confirm. Note the JSON field is error, not code, and the same value appears if the extension is not installed, not yet paired, or its service worker has been evicted.
bex_accounts comes back empty or full of errors
The wallet is not initialized: device unplugged or locked, or the vault is not paired. Per-chain entries return as {chain, error} with Wallet not initialized. Plug in and unlock the device, open the side panel so initialization runs, then retry.
bex_click, bex_navigate and friends are not listed, or are rejected
Expected. The browser-driving tools are still in development. tools/list advertises the five introspection tools only, and calling one of the others returns JSON-RPC error -32602 unknown tool: bex_click. There is nothing to fix on your side.
POST /mcp drops the socket instead of returning a status
Seen as fetch failed or UND_ERR_SOCKET. Older vault builds could fail to render the 401 for /mcp. The underlying cause is still one of the two 401s above — check your Authorization header first, then update the vault.
Related
- Browser Extension — the extension Agent Mode inspects
- Settings → API Servers — the pairing and API bridge security model
- Verifying Transactions — the device confirmation the agent cannot perform