Extension protocol
The L0 probe and the L1 MCP session: wire format, state machine, invariants and limits.
On this page
This is the contract between an MCP host (Lumo) and any Chrome extension that serves tools. It has exactly two layers: one custom message that answers whether the provider is usable, and standard MCP for everything else.
Two layers#
An MCP initialize cannot describe "installed but disabled" or "not authorized" without failing the handshake, which leaves the host unable to tell the user what to fix. L0 exists to express those states; L1 stays completely standard.
Layer 1: MCP over a runtime port#
- The port name is fixed:
lumo:mcp. The probe channel is fixed:lumo:probe. - JSON-RPC,
initialize, capability negotiation,tools/listandtools/callare handled by the official MCP SDK on both sides. - A port closing does not mean the service disappeared: MV3 may recycle the service worker. The host keeps the logical session and cached tool list, then reconnects lazily.
- Reconnection performs a fresh
initialize. The provider creates oneMcpServerper port and must not replay an old initialize result.
State machine#
| Probe result | Host state | User-visible outcome |
|---|---|---|
| sendMessage rejects | error | Extension not installed, or no agent endpoint. |
| Timeout at 1500ms | error | The original error message. |
ok: false | error | The protocol error message. |
| Missing service or status | error | Probe response is incomplete. |
enabled: false | error | Agent access is turned off in the provider. |
authorized: false + pending | error | Waiting for your approval in the provider. |
authorized: false | error | The caller is not on the allowlist. |
ready: false | error | The provider's own reason message. |
| All pass | connected | Tools appear, with counts. |
Invariants#
- Fixed names and version:
lumo:probe,lumo:mcp, probe version1. isExtensionId()is the only ID validator:/^[a-p]{32}$/. Every input path must use it.- A probe returns
ok: trueeven for a policy failure; only a protocol failure returnsok: false. - Authorization compares
sender.id.client.nameandservice.nameare display-only. - L1 requires
enabled && authorized.readydescribes capability, not permission. - The provider re-checks policy on every tool call, so a switch takes effect immediately.
- A closed port is not a deleted service; reconnect with a fresh initialize.
- L0 carries metadata only. Tools and business data move over L1 after authorization.
Tool namespacing and limits#
| Rule | Value | Reason |
|---|---|---|
| Maximum tools per provider | 64 | Keeps the model-visible catalogue bounded. |
| Namespace length | ≤ 16 | Leaves room for the tool name. |
| Tool name length | ≤ 40 | Provider-side constraint. |
| Combined length | ≤ 57 | Below OpenAI's 64-character function-name limit. |
| Reserved namespaces | browser, page, file, network, devtools, webmcp, ext | Prevents collisions with built-ins. |
| Name collisions | First wins | Never silently overwrite another server's tool. |
Audit and rate limits#
- Every write is audited with tool name, sender ID, result, target UUID and a hashed argument digest.
- Script source code never enters the audit log.
- Writes use a sliding window of 30 calls per 60 seconds per caller.
- A business failure is returned as a successful MCP call with
isError: true, so the model can read the reason instead of losing the turn.
Do not widen these limits casually
The 57-character total is derived from a hard provider limit. Raising the namespace or tool budget without checking every model integration breaks tool calling in ways that look like model failures.
Implementing a provider#
- 1
Declare the boundary
Add
externally_connectableto the manifest.ids: ['*']is required while the host is unpacked, because its ID changes on every install. - 2
Register both listeners synchronously
Call
chrome.runtime.onMessageExternalandonConnectExternalat the top level of the background script, not inside an async callback. - 3
Write a buffered transport
Implement the MCP
Transportinterface over the port, buffering inbound messages until authorization completes. - 4
Create one server per port
Register tools with the official SDK and shape the instructions with the current policy.
- 5
Store and re-check policy
Persist endpoint, allowlist, write and review switches; re-resolve them at the start of every tool call.
Lumo Userscripts is the reference implementation: lib/agent/protocol.ts, probe.ts, transport.ts, mcp-server.ts, tools.ts, policy.ts and audit.ts. The host side lives in lib/mcp/extension-*.ts.