Enquire docs
    Enquire docs
    Getting startedUser guide
    ArchitectureBrowser toolsProcedures
    Claude Code integrationExtension guide
    DeploymentRoadmapBenchmarks
    Concepts

    Architecture

    System design, tiered router, procedure mode, trust boundaries.

    Components

    The browser extension is built with WXT (Web Extension Toolkit) and React.

    ┌──────────────────────────────────────────────────────────────┐
    │                       Browser Extension                       │
    │                                                               │
    │  ┌──────────┐  ┌──────────────┐  ┌──────────────────┐        │
    │  │  Popup   │  │   Options    │  │    Side Panel     │        │
    │  │          │  │  (Dashboard) │  │  ┌─────────────┐  │        │
    │  │ Quick    │  │  Overview    │  │  │  AI Chat    │  │        │
    │  │ status   │  │  Discovery   │  │  │  Tool calls │  │        │
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    Edit on GitHub

    User guide

    Day-to-day use of the Enquire Chrome extension's side panel.

    Browser tools

    Auto-generated reference for the 31 MCP tools the bridge exposes — input shape, constraints, and descriptions.

    On this page

    ComponentsData FlowTech StackLLM Adapter PatternBuiltin Tools ArchitectureShared UI ArchitectureEvent-Driven Discovery StateNative Messaging BridgeAgent Loop (Side Panel + enquire__chat)FSM modes + tiersPlan persistenceEpisodic MemoryProcedures (v0)Cloud Connection (app.enqr.dev)Side-Panel Surfaces
    │ └──────────┘ │ Chat │ │ │ Servers │ │ │
    │ │ Permissions │ │ │ Permissions│ │ │
    │ ┌──────────┐ │ Audit Log │ │ └─────────────┘ │ │
    │ │ DevTools │ │ Settings │ └──────────────────────┘ │
    │ │ Panel │ │ Bridge │ │
    │ │ State │ └──────────────┘ │
    │ │ Servers │ │
    │ └──────────┘ ┌──────────────────────┐ │
    │ │ Content Script │ │
    │ │ Link/meta scanning │ │
    │ └──────────┬───────────┘ │
    │ │ messages │
    │ ┌────────────────────────▼──────────────────────────────┐ │
    │ │ Background Service Worker │ │
    │ │ ┌───────────┐ ┌────────────┐ ┌─────────┐ │ │
    │ │ │ Discovery │ │ MCP Client │ │ LLM │ │ │
    │ │ │ Manager │ │ Manager │ │ Bridge │ │ │
    │ │ ├───────────┤ ├────────────┤ ├─────────┤ │ │
    │ │ │ Session │ │ Header │ │ OAuth │ │ │
    │ │ │ Storage │←─│Interceptor │ │ Handler │ │ │
    │ │ │ (reactive)│ └────────────┘ └─────────┘ │ │
    │ │ ├───────────┤ │ │
    │ │ │ Permission│ ┌────────────┐ │ │
    │ │ │ Manager │ │ Native │ │ │
    │ │ ├───────────┤ │ Bridge │──── localhost:3789 │ │
    │ │ │ Audit │ │ (MCP proxy)│ │ │
    │ │ │ Logger │ └────────────┘ │ │
    │ │ └───────────┘ │ │
    │ └───────────────────────────────────────────────────────┘ │
    └──────────────────────────────────────────────────────────────┘

    Data Flow

    1. 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
    2. 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
    3. Connection — When the user chats, the LLM bridge formats discovered tools and sends them to the AI provider
    4. 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
    5. 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
    6. Audit — Every tool invocation is logged with tool name, server, tier, timestamp, and consent decision

    Tech Stack

    ComponentTechnology
    Extension frameworkWXT 0.20
    UIReact 19, Tailwind CSS 4, Headless UI
    State managementZustand
    AI providersAnthropic SDK, OpenAI SDK, AWS Bedrock (aws4fetch)
    MCP client@modelcontextprotocol/sdk
    Schema validationZod
    BuildVite
    TestsVitest

    LLM Adapter Pattern

    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.

    Builtin Tools Architecture

    Builtin tools (lib/builtin-tools/) are browser-native capabilities that don't require an external MCP server. They use a registry pattern:

    • Registry (registry.ts) — registers and looks up tools by name
    • Google Search (google-search.ts) — performs web searches via the browser
    • Page Crawl (crawl-page.ts) — extracts content from the current page
    • Tab Manager (tab-manager.ts) — manages browser tabs

    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.

    Shared UI Architecture

    UI components are organized in lib/ui/ for reuse across all entry points (side panel, options dashboard, DevTools panel):

    • lib/ui/components/ — atomic components (ServerCard, ToolCallCard, ChatMessage, ChatInput, TokenUsageBar, StatusIndicator, ToolBadge, ToolConsentDialog)
    • lib/ui/sections/ — composite page sections (SettingsForm, ToolsView, PermissionsView, AuditLogView)

    Entry-point pages are thin wrappers around shared sections. The compact prop controls layout density (side panel = compact, options = full-width).

    Event-Driven Discovery State

    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.

    Native Messaging Bridge

    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:

    • get_agent_state — current FSM mode, plan, recent tool calls, retry counter
    • get_plan — formatted plan text for the active session
    • get_session_log — rolling 500-entry persistent session log

    These let an external MCP client (Claude Code, Cursor) poll mid-run state without waiting for the next streamed result.


    Agent Loop (Side Panel + enquire__chat)

    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:

    1. 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.
    2. 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.
    3. Stream — call the configured adapter (lib/llm/{claude,openai,bedrock,webllm,transformers}-adapter.ts).
    4. 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.
    5. 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.
    6. 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.

    FSM modes + tiers

    The agent runs as a small finite-state machine:

    ModePurposeKey tools
    planOrient and write a planupdate_plan, get_plan, page_digest, find
    interactClick, type, scrollclick, type, find_and_click, press_key, scroll, fill_form
    navigateURL/tab changesnavigate, open_tab, switch_tab, list_tabs
    inspectRead the pageread_page, page_digest, find, extract, get_a11y_tree
    searchWeb search/crawlgoogle_search, fetch_links, crawl_page
    devtoolsConsole/networkget_console_log, clear_console, capture_console

    A control category — request_mode, done — is always present so the agent can transition modes or end a step regardless of where it is.

    Tiers cap the per-turn surface so smaller models don't drown:

    TierSurface
    minimal≤7 tools per mode (gemma4:e2b, qwen3:1.7b — ≤2B params)
    standard≤11 tools per mode (gemma4:e4b, qwen3:4b — 3-4B class)
    extendedfull mode surface (qwen2.5:7b, deepseek-r1 — 7-14B)
    fullevery tool every turn — FSM disabled (Claude/GPT-4o/Gemini)

    The whitelist lives in lib/llm/agent-tiers.ts:TIER_MODE_TOOLS. auto mode picks a tier based on model name (autoTierForModel).

    Plan persistence

    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.


    Episodic Memory

    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.
    • Viewer: Options → Memory (entrypoints/options/pages/MemoryPage.tsx) — search, group-by-domain, per-row delete, export JSON, clear all.

    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.


    Procedures (v0)

    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.

    • Schema: {id, name, domain, bodyYaml, contentHash, createdAt, updatedAt, lastUsedAt, totalRuns, successRate}.
    • 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.


    Cloud Connection (app.enqr.dev)

    When Settings → Cloud → Sign in is clicked, the extension performs a hosted OAuth flow against the same Better Auth instance the dashboard uses:

    ext settings ──► chrome.identity.launchWebAuthFlow
                      https://app.enqr.dev/sign-in?redirect_uri=...&state=...
                           │
                           ▼ (Better Auth issues bearer token)
                      redirect_uri returns ?token=…&user_id=…
                           │
                           ▼
                      settings.authToken / authUserId persisted
                      cloudMode = true
                      relayUrl  = wss://bridge.enqr.dev/ws (derived)

    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.

    Four production domains:

    DomainPurpose
    enqr.devMarketing site
    app.enqr.devDashboard — sign-in, setup page, procedures viewer
    bridge.enqr.devWebSocket relay + /mcp HTTP endpoint for external clients
    api.enqr.devREST API for procedures, queries, account

    Local dev runs all four against localhost per DEV_SETUP.md.


    Side-Panel Surfaces

    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.

    See also: Extension guide | User guide | Browser tools | Deployment