Telemetry from SynapBus #27345 (gemini-home, 2026-04-30) confirmed the activation funnel cliff is Step 3 → Step 4 (78.2% → 11.7%): users add upstream servers but fail to wire mcpproxy into their AI IDE.
Spec 046 v2 reframes the wizard as a persistent, idempotent setup surface with three tabs (Clients · Servers · Verify), a top-pinned sidebar entry with an animated badge, and a passive Verify tab that flips green automatically when any MCP client makes its first successful request.
/api/v1/onboarding/state extended with first_mcp_client_ever, mcp_clients_seen_ever, incomplete_tab_count; sourced from Spec 044's existing ActivationStore — no new BBolt buckets.OnboardingWizard.vue rewritten to a 3-tab idempotent surface; SidebarNav.vue gains the top-pinned Setup entry; the Dashboard's v1 "Run setup wizard" button is removed (FR-V11).On a fresh install the wizard auto-pops as a 3-tab modal (Clients · Servers · Verify). Sidebar shows 'Setup' top-pinned with the appropriate badge count above Dashboard.
Wizard auto-popped. Three tabs visible. Sidebar Setup entry shows badge above Dashboard, with sparkles icon and gradient background.
Each detected client config is a section with header (name + count + monospace path). Server rows are visibly nested under their parent: 28-32px left padding, a vertical guide line, and lighter font weight than the section title — so the eye instantly distinguishes 'Claude Code (User)' from the bare 'mcpproxy' row below it. Sticky footer's two action buttons are equal-width (`min-w-[180px]`) so they read as a balanced pair, not primary/afterthought. Default state: all checkboxes unchecked, footer says 'Select at least one server to import', both action buttons disabled.
Sections render with bold name + count + monospace path. Server rows are indented (32px left) with a thin vertical guide line showing the parent-child relationship. Section header background is subtly tinted to separate it from server rows. Both 'Import as active' and 'Import & quarantine' buttons are visibly equal width.
When the same server name is selected from 2+ sources, both conflicting copies show an inline warning-color pill with the renamed target (`→ mcpproxy_claude_desktop`, `→ mcpproxy_claude_code`). Footer shows 'N selected · M renamed'. Non-conflicting servers carry no pill. Backend receives the rename map per source and applies it before AddServer is called.
Both `mcpproxy` rows display the warning-color rename pill with the correct target name. Footer reads '2 selected · 2 renamed'. Both action buttons enabled. Out-of-band curl after import confirms `mcpproxy_claude_desktop` and `mcpproxy_claude_code` persist with `quarantined=true`.
Clicking 'Import & quarantine' fires one POST per source in parallel with the appropriate server_names + rename. Toast confirms. Top header now reflects new server count. List refreshes — imported servers drop out (those whose source name matches the imported entry). Footer returns to the disabled empty-selection state.
Toast: 'Import complete · 2 servers'. Top header switched to '2 / 5 Servers · 754 Tools'. Sections refreshed. Footer reset to disabled state. Title line: 'You're all set up.'.
Verify tab shows a passive waiting state with a 📡 icon and 'Waiting for your first request' message. Below, a 'TRY ONE OF THESE PROMPTS' list of three sample queries — each annotated with the built-in mcpproxy tool it exercises (`retrieve_tools`, `upstream_servers`, `quarantine_security`). Below that, a 'RECENT ACTIVITY' panel with a 'View all in Activity Log →' link and an empty-state message that explains every MCP request is observable here and on the Activity Log page.
All three sections render: header with 'Listening…' indicator, three sample prompts with built-in tool annotations on the right, RECENT ACTIVITY header with 'View all in Activity Log →' link. (Empty-state placeholder is present below the link when no activity has been recorded.)
After a real `initialize` and `tools/call` arrive at /mcp, the Verify tab flips to ✅ green showing 'Recognized:
Verify tab body: ✅ 'Round-trip verified · Recognized: claude-code'. Three sample-prompt rows with built-in tool annotations. RECENT ACTIVITY panel now lists 5 live records with yellow ! status badges (blocked-by-quarantine, expected behaviour for newly-imported quarantined servers): `gcore-mcp-server:waap_get_account_overview`, `:waap_tags_ls`, `:waap_statistics_get_usage_series`, `:waap_organizations_ls`, `:waap_ip_info_metr_ls` — all 'just now'.
After all three tabs are satisfied, the sidebar Setup entry stays visible but quiet (or muted with a check). It stops competing for attention while remaining easy to revisit.
Wizard dismissed; sidebar Setup row settles into the post-engagement state. Dashboard takes the focus row beneath.
On a fresh page load after engagement (state.engaged=true and should_show_wizard=false), the wizard
Fresh page load: dialog has no `open` attribute (Pinia store wizardOpen=false). Sidebar Setup row renders as 'Setup ✓'. Click on the Setup row flips wizardOpen=true and the dialog [open] attribute appears within ~700ms; one of the three tab panels is visible immediately after.
Toggling a section's select-all checkbox OFF must uncheck every server row in that section AND revert the sticky footer to the disabled-empty-selection copy 'Select at least one server to import'.
Clicking select-all twice (on then off) returns the footer text to 'Select at least one server to import'. Visible in the screenshot at the bottom of the modal body.
When a section has N >= 2 servers and exactly one of them is ticked, the section's select-all checkbox renders the HTML indeterminate state (`input.indeterminate === true`). This communicates partial selection without forcing the user to open the section to count rows.
After ticking exactly one child row in the first section with >=2 servers, the section header's input element reports `indeterminate=true` via direct property probe. Visual: the checkbox renders with the dash-style indeterminate glyph.
The 'View all in Activity Log →' link inside the Verify tab's RECENT ACTIVITY panel must navigate to the full Activity Log page (/activity) so users can deep-dive when the wizard preview is not enough.
Clicking the link inside the wizard navigates to `/ui/activity`. URL contains '/activity' after the click; the wizard modal is replaced by the Activity Log view in the screenshot (sidebar Activity Log entry highlighted).
Clicking the Docker isolation checkbox in the Servers tab must update the visible checkbox state immediately and the underlying `dockerIsolationDefault` reactive ref. Toggle should also persist to the runtime config via the patch endpoint so subsequent loads reflect the new state.
Pre-condition reset via PATCH /api/v1/config/docker-isolation to enabled=false. After a single in-wizard click, the checkbox flips to checked (FE state truthful). NOTE: the wizard's onToggleDockerIsolation routes through POST /api/v1/config/apply with a wrapped payload (`{config: …}`) which the backend rejects with a tools_limit validation error — so the UI flips locally but server-side persistence is best-effort. The dedicated PATCH endpoint works correctly when called directly. Filed as a follow-up: wizard should call api.setDockerIsolationEnabled (PATCH) instead of patchConfig+applyConfig.
Clicking the 'Quarantine new tools' checkbox in the Servers tab must flip the checkbox visually so the user gets clear feedback about the change in default protection.
Click toggles checked → unchecked (or vice versa) within ~2s. Same caveat as scenario 12 applies for backend persistence: onToggleQuarantine routes through patchConfig+applyConfig with a wrapped payload that the backend rejects, so the change is FE-local only.
Opening the Servers tab without ticking any checkboxes and clicking the footer's Close button must NOT add any servers to the upstream list. The bulk-import buttons must remain disabled while selectedCount === 0.
Pre-click: 'Import & quarantine' button reports disabled. Server count via /api/v1/servers is identical before and after Close. The wizard dismisses cleanly back to the sidebar.
Driver: Playwright 1.x (Chromium 1217, headless),
run serially against a fresh test-only mcpproxy on
127.0.0.1:18081. Each post-engagement test (8–14)
gets an isolated browser context and re-opens the wizard via the
sidebar Setup row.
./scripts/run-oauth-e2e.sh
bootstraps a separate test OAuth server + builds binaries; estimated
runtime exceeds the 5-minute budget. Existing pass status carries
over from the previous sweep on this branch.
Notable finding (scenarios 12–13): the wizard's
Docker-isolation and Quarantine toggles route through
POST /api/v1/config/apply with a wrapped payload
({config: merged}), but the backend's
handleApplyConfig decodes the body directly into
config.Config — so the wrap is treated as an empty
config and validation rejects it (tools_limit error). The
UI flips locally (good visual feedback) but server-side persistence
via this path is best-effort. The dedicated
PATCH /api/v1/config/docker-isolation endpoint persists
correctly when called directly. Recommended follow-up: route the
wizard's two toggles through api.setDockerIsolationEnabled
(already exists) and an analogous PATCH endpoint for
quarantine_enabled.
Latest correction: rename "Quarantine new tools" → "Quarantine new servers", move both toggles into a collapsible "Runtime isolation and MCP server quarantine" panel inside the sticky non-scrollable footer, expand each toggle with security context + docs.mcpproxy.app/security links, gate "Import & quarantine" on the quarantine toggle, and bold the "Recommended." / "Important:" copy.
On entering the Servers tab the security panel is collapsed and shows only its summary line: '▸ Runtime isolation and MCP server quarantine' with a hint 'global settings — apply to every imported server' on the right. The panel sits in the sticky non-scrollable footer between the import list and the action buttons.
Panel renders collapsed with summary visible above the action footer. List of detected client configs above is independently scrollable; security panel + buttons stay pinned.
Expanding the panel reveals two cards: Docker isolation (with a 'Docker detected' or 'Docker not detected' badge) and Quarantine new servers (with a 'Recommended' badge). Each card has a long description explaining the security model, an inline 'Learn more' link to docs.mcpproxy.app/security/{docker-isolation,quarantine}, and the Quarantine card emphasizes 'Recommended.' and 'Important:' in bold to flag the AI-agent-can-add-servers risk. When Docker is missing, an inline install hint links to Docker Desktop.
Both toggles checked by default. Docker shows green 'Docker detected' badge. Quarantine shows primary 'Recommended' badge. Both Learn-more links point to docs.mcpproxy.app/security/. Quarantine description leads with bold 'Recommended.' and 'Important:' clauses, and references the security scanners on the Servers page.
With Quarantine new servers ON and at least one server selected, both 'Import as active' and 'Import & quarantine' are enabled.
Footer reads '1 selected' (or higher). Both action buttons fully interactive.
Toggling Quarantine new servers off persists the new value via patchConfig and immediately disables the 'Import & quarantine' button (with a hover hint 'Re-enable Quarantine new servers above to use this option'). 'Import as active' stays enabled because the user may still want to opt into the trust path explicitly.
'Settings saved · Updated' toast confirms persistence. 'Import & quarantine' button is greyed out / disabled. 'Import as active' remains lit and clickable. Quarantine checkbox visibly unchecked.
Re-checking Quarantine flips the toggle on, persists, and the 'Import & quarantine' button becomes interactive again.
Toggle re-checked. 'Import & quarantine' button is enabled. Behaviour is fully reversible.
Collapsing the panel hides the toggle cards but preserves the underlying config values. The action footer remains visible.
Panel summary visible; toggle cards hidden. Action footer remains pinned with the same selection count.