Discovery — When a page loads, the content script scans for HTML <link>/<meta> tags while the background worker fetches /.well-known/mcp.json, /mcp.txt, and checks response headers
Session Storage Broadcast — Discovery results are written to chrome.storage.session as a reactive notification bus. All UI surfaces (side panel, popup, options dashboard, DevTools panel) watch for changes via storage.watch() and update automatically — no polling or timeouts
Connection — When the user chats, the LLM bridge formats discovered tools and sends them to the AI provider
Invocation — Tool calls from the AI are checked against the permission policy, consent is requested if needed, and the call is forwarded to the MCP server
Proxy — The native messaging bridge exposes discovered servers to external MCP clients (e.g., Claude Code). list_discovered_servers reads from session storage; call_discovered_tool proxies through the extension's MCP client manager
Audit — Every tool invocation is logged with tool name, server, tier, timestamp, and consent decision
The extension supports multiple AI providers through a common adapter interface (lib/llm/). Each adapter (Claude, OpenAI, Bedrock) implements the same interface for sending messages and streaming responses, allowing the chat UI to work identically regardless of the configured provider.
Each tool implements a common BuiltinTool interface (types.ts) and is registered at startup. The LLM bridge checks the builtin registry before forwarding calls to external MCP servers.
Discovery state is stored in chrome.storage.session (RAM-backed, cleared on restart) via lib/storage/discovery-state.ts. The background service worker writes state at three points during discovery (start, success, error) and bumps a revision counter. UI surfaces use the useDiscoveryState() React hook from lib/hooks/useDiscoveryState.ts which watches for changes and re-renders automatically.
The extension bridge handler (entrypoints/background/native-bridge.ts) accepts requests from @enqr/bridge over the signed-in WebSocket tunnel. It handles four request types:
list_tools — returns builtin tool definitions
tool_call — executes a builtin tool
list_discovered_servers — returns all MCP servers from session storage
call_discovered_tool — proxies a tool call to a discovered MCP server via the extension's MCP client manager
It also dispatches three inspect-only debug tools surfaced through the MCP bridge:
The agentic chat path lives in entrypoints/background/llm-bridge.ts. Two callers share the same loop:
Side panel (entrypoints/sidepanel/pages/ChatPage.tsx) — streams via a chrome.runtime.connect() Port for incremental UI updates.
enquire__chat MCP tool (entrypoints/background/agent-runner.ts) — runs headless for external MCP clients; same machinery, fake port that captures the final assistant text.
The loop:
Resolve toolset — lib/llm/agent-tiers.ts:resolveAgentTools(allTools, tier, mode) filters the discovered + builtin tool list down to a per-turn surface based on the active tier × mode.
Build context — buildPlanContext(sessionToken) synthesises a hidden [enquire-context] user message containing: current FSM mode, active tab URL+title, episodic recall block (if any), and plan progress. Stripped from saved history via stripSyntheticMessages so it doesn't bloat the conversation.
Stream — call the configured adapter (lib/llm/{claude,openai,bedrock,webllm,transformers}-adapter.ts).
Empty-stream retry — if the model returns no text and no tool call, inject a corrective [enquire-retry] user message and re-stream once. After that, bail with a visible diagnostic.
Tool-call handling — for each tool_use from the model:
Check the dedup window (recentToolCalls Map, 60 s, keyed by conversationId:toolName:stableArgs).
Permission gate via permission-manager.ts (skipped for read-only or always-allowed tools).
Execute under withTimeout (30 s default, 60 s for nav/wait/screenshot/crawl tools).
Emit TOOL_CALL_START / TOOL_CALL_EXECUTING / TOOL_CALL_RESULT events on the port so the chat UI can render each call as its own timeline row.
Recurse — feed the tool results back into the model up to MAX_TOOL_DEPTH = 25 turns or until the agent calls done() or the loop guard trips on a repeat-pattern.
lib/builtin-tools/task-plan.ts keeps sessions: Map<token, SessionState> (mode, plan, page index) backed by chrome.storage.session. On module load it hydrates from storage; mutations write back via a 200 ms debounced persistSoon(). This survives MV3 service-worker recycle (~30 s idle) so a long task doesn't lose its plan halfway through.
getAgentState(sessionToken) aggregates {mode, plan, planText, progress, inProgress, pageIndexed} for the AgentStateBar UI and the get_agent_state MCP tool.
lib/storage/episodic-memory.ts — chrome.storage.local-backed list, capped at 500 entries.
Schema:{id, domain, url, title, summary, facts, createdAt, updatedAt, conversationId} keyed by eTLD+1 of the active tab.
Capture:doneHandler (in lib/builtin-tools/mode-router.ts) fires appendEpisodic whenever done(summary) runs on a tab with a URL. Fire-and-forget — never fails the tool.
Recall:buildPlanContext calls getEpisodicForDomain(domain, 3) and folds the newest three summaries into a "Prior visits to {domain}" block, capped at ~600 chars. Skipped when settings.episodicInject === false.
The flow closes a real failure mode: on first visit the agent rediscovers everything; on the second the surfaces it already learned about the domain are pre-loaded into context, so it doesn't re-run google_search for an answer that's on the open page.
lib/storage/procedures.ts — chrome.storage.local backed. The shape is intentionally close to the dashboard/API procedure shape so local and cloud procedures can round-trip cleanly once sync is hardened.
Editor: Options → Procedures (entrypoints/options/pages/ProceduresPage.tsx) — list grouped by domain, inline YAML editor, delete, Replay.
Replay flow:Replay button sends REPLAY_PROCEDURE { procedureId } to the SW. The handler opens the side panel via browser.sidePanel.open({ tabId }), then broadcasts PROCEDURE_REPLAY_REQUEST { procedure }. ChatPage listens, starts a fresh chat, and seeds the user message with the procedure body framed as a primer. The normal agent loop takes it from there.
Extension procedures are local-first today. The dashboard and API expose /procedures for cloud-visible procedure records, while extension-to-cloud procedure sync remains a hardening target.
entrypoints/offscreen/ then dials wss://bridge.enqr.dev/ws?token=<authToken> with reconnect. Once the tunnel is up:
External MCP clients hit https://bridge.enqr.dev/mcp with Authorization: Bearer <token>. The bridge looks up the user's tunnel via HttpVerifier (api.enqr.dev/auth/get-session) and routes tool calls into the extension.
Settings sync — lib/sync/cloud-sync.ts POSTs settings to api.enqr.dev so signed-in users see consistent config across devices.
API surface — api.enqr.dev validates every call with the same Better Auth bearer plugin and serves /procedures, /query-logs, /billing, and /account.
The side panel (entrypoints/sidepanel/) hosts the in-extension agentic chat. Notable components:
ChatPage — orchestrates the loop. Maintains a merged timeline of messages, debugEvents (mode swaps, tool refresh markers), and individual tool-call rows, all sorted by timestamp so the read order matches wall-clock order.
ActiveTabBar — pinned strip showing which Chrome tab the agent is driving. Empty state offers Adopt / New tab actions; the X detaches without closing.
AgentStateBar — pinned strip below the header showing [mode] · 2/4 steps · last_action. Click any segment for a popover (toolset, plan list, last tool input/result).
Per-conversation model picker — dropdown above the input. Overrides Settings for that chat only — every preset including transformers.js + Ollama variants is selectable.
Tool badge — shows 7/53 so the user understands "7 tools visible to the model under tier minimal × mode interact, 53 total exist." Click to expand a popover listing visible + hidden tools.