MCP spike — the whole UX surface
The frame: researchers get two offerings — 1. the report, one link in a browser, two modes: read it (shipped) and ask it (the Chat lens, folding into the report UX — sibling workstream); 2. stay in your favourite agent, all local (Claude Desktop, Claude Code, Codex). This page is offering 2 — Part 1 as the §9a spike ships it (CLI only, by the dedicated-extra decision), Part 2 the proposed bn.app exposure (later cycle, gated on the stable per-project token).
Part 1 — the CLI spike SHIPS IN SPIKE
Scene 1 · Start serve — the connect moment
Scene 2 · Connected
- Only two primitives exist — the URL and the Authorization header; every client is a dialect of writing them down. The spike prints one dialect (Claude Code) because both primitives are already on screen for everything else.
- Claude Desktop is JSON by hand, and the destination pane is confirmed (30 Jul screenshot): Settings → Developer → Local MCP servers → Edit Config, with View Logs right there for diagnosing a failed connect. The narrowed acceptance-verify item: does that config take a
url+headersentry directly, or does our snippet become themcp-remoteshim spelled as command+args? Record which in the §9a results. - The token rotates on every serve restart — accepted papercut, see exception E1.
Scene 3 · Orientation — the cheap first call
- Counts and structure only — no quote bodies. The overview is the "cheap orienting call" (§4) and the discovery surface for framework ids.
- The last-run line is the honesty block (review finding 26): a failed run would be flagged right here.
Scene 4 · Reasoning in the researcher's taxonomy
"codebook"reserved id at work: live codes, TagPrompt boundaries, usage counts — the assistant reasons inside the researcher's taxonomy instead of inventing one.- Report-view semantics everywhere: hidden quotes and unreviewed AutoCode proposals don't exist here; edited text wins.
- Note the posture: merge is suggested as a question — merge-tags stays a human action (§3b).
Scene 5 · The baits — do the invariants land?
- This is spike question 2 being measured: round 1 runs with the invariants in
instructionsonly (no inline notes in payloads), so compliance is attributable. - If the model ignores them, the doc's predicted fallback kicks in: invariants move into tool responses, noted in §9a results.
Scene 6 · Worst case — the context budget (Q3)
- Perf measured this ceiling on the real 484-quote corpus: 45–86 KB ≈ 12–22k tokens at limit=50. Text is never truncated; the count is capped and pagination is explicit.
- Every call emits
mcp_tool | tool=… | elapsed_ms=… | result_bytes=…— Q3 gets answered from the log, not vibes.
Exceptions — every way it goes wrong, and what the researcher sees
By design (per-instance token). Fix is the printed command again: re-run
claude mcp add from the fresh serve output. Documented, not engineered around.
The guarded import catches ImportError only; the stdout line tells the truth instead of advertising a dead endpoint. Auth prefix stays on regardless (fail closed).
The overview carries last_run outcome + timestamp — the same honesty the SPA's
status page enforces, threaded into the tool payload.
Zero Project rows → the honest message, not zeros pretending to be an analysis. Unknown project id (rows exist) is a different error that lists valid ids.
Closed vocabularies error loudly and list the valid values — the error message teaches the model, which retries correctly. An empty result never masquerades as "no data".
It speaks to AI agents (Claude Code, Claude Desktop), not to browsers.
Your report is at 127.0.0.1:8150/report/ · To connect an agent, copy the command from the terminal running
bristlenose serve.Terminals auto-linkify the MCP URL and people click without reading. A browser-shaped GET
(Accept: text/html) gets this unauthenticated, zero-secret page instead of raw
{"detail":"Unauthorized"} — the status page's honesty, applied to /mcp/.
Protocol traffic (POST + bearer) is untouched.
Documented in the acceptance notes: project-scoped client config puts the token in a committable file. Default scope keeps it in the user's own config.
get_transcript is omitted —
the single biggest PII-volume reduction, as a decision not an oversight).Part 2 — proposed bn.app surfaces PROPOSAL · LATER CYCLE
Per design-mcp-server.md §6a. Hard prerequisite: the stable per-project token — with the spike's rotating token, any UI here would break weekly by design. Everything below is sandbox-legal (clipboard + save panel; we never write another app's config).
Mock A · The way in — right-click, not chrome
- §6a's approved shape: a context-menu action on the project row. No toolbar icon in v1 — the action isn't "here", it's making Bristlenose connectable.
- Redrawn against the shipped menu (Gruber): the real project menu has no "Open", does have Choose Icon / Move to ▸ / Rename…, plus up to five conditional groups above. Connect Agent… joins Show in Finder's group — both are "send this project somewhere outside Bristlenose" — costing zero new separator groups (HIG: ~three groups max).
- Menu-bar twin required (HIG: every context item lives in the menu bar too): Project ▸ Connect Agent… alongside the existing rename/archive/delete mirrors in MenuCommands.swift.
Mock B · The connect sheet — four things, nothing else
Connect an agent to “Acme Onboarding”
6 sessions · 214 quotes
- Post-critique shape: counts line · the shipped Anonymise row · client tabs — connect-only. The disclosure sentence and the starter prompts are both removed for clarity (decided 30 Jul): the Anonymise caption carries the disclosure's meaning, the prompts move to the manual + acceptance script, and either returns only if the cohort shows confusion or lands in empty chats with nothing to ask. The CLI connect moment keeps its dim one-liner — no control is visible there to speak for itself. Footer is the standard Mac modal pair, bottom-right, outside the config field: Cancel + a primary named by its verb — Copy Config here, Copy Command on the Claude Code tab. Return fires the primary.
- Noun swept to "agent" everywhere (menu, title, body, CLI, fallback line) — decided; "Connect to Claude…" stays rejected as vendor-locked.
- Fixed geometry across tabs (decided 30 Jul): the header block — title, counts, Anonymise — is identical for every agent, the window is one size, and the config area is sized to the largest dialect (Claude Desktop's six-line JSON) so switching tabs never reflows or jumps. Only the panel content and the primary's verb change. The Parthenon rule: geometry is fixed, content bends.
- Tabs build as a real NSTabView (the tabs decision survives; the right class brings native metrics, Ctrl-Tab, accessibility). First detected pre-selected; three honesty states — detected / unknown (Claude Code: CLI presence unknowable from the sandbox) / not-found (greyed but never disabled). Detection renders as words, not a dot — the green dot died for meaning "installed" here and "connected" in Mock C with one colour.
- Anonymise = the shipped Export-popover row, verbatim ("Anonymise" / "Remove participant names" / switch — Martin's screenshot): one control, one word family across Export, clips, Miro, and MCP. On by default here (exports default off — different egress class). Quietly stricter than export: moderator names aren't sent either — the grounding layer never reads Person rows.
- Copy = the shipped
.bordered .small doc.on.docicon-button pattern (documented at LLMSettingsView.swift:897; ⧉ stands in here). One treatment, all labelled Copy, only the panel's prominent. Full token, shown and selectable — decided; never "looks copyable, isn't". The config path click-reveals in Finder (NSWorkspace.activateFileViewerSelecting, sandbox-legal). - Sheet stays a sheet — decided: nothing in the parent app is blocked behind it that this flow needs. 560pt so the payload never scrolls sideways; type on the macOS ladder (15/13/11); headroom check at
de/jabefore first build. - The ChatGPT & Codex tab is one tab, not three (verified 30 Jul): desktop app, Codex CLI, and IDE extension share
~/.codex/config.toml, so one TOML stanza covers all —url+http_headers = { "Authorization" = "Bearer …" }(the static-header form;codex mcp addhas no header flag and env vars don't reach a Finder-launched app). Detection: ChatGPT.app bundle via LaunchServices, same honesty tiering. - Desktop's end-state is the Extensions pane, not JSON (Martin's steer, 30 Jul, screenshots): ship
Bristlenose.mcpb— icon + thin stdio→HTTP proxy that reads a serve-written handshake file (fresh token + port each start), dissolving token rotation for Desktop entirely. The sandbox-legal big button: bn.app writes its bundled .mcpb to a user location andNSWorkspace.opens it — Desktop runs its own install flow. This tab's Edit-Config copy is the spike-era truth and the forever-fallback; when .mcpb ships, the Desktop tab's primary becomes Install Extension.
Mock B2 · The other tabs — same two primitives, each client's dialect
- Dialects verified, not remembered: Claude Code against the installed CLI itself (
claude mcp add --help, 30 Jul — HTTP transport +--headerexactly as shown). ChatGPT & Codex against learn.chatgpt.com (30 Jul research): oneconfig.tomlcovers ChatGPT desktop, Codex CLI, and the IDE extension;codex mcp addexists but has no static-header flag, and its env-var route doesn't reach a Finder-launched app, so the stanza is the reliable form; ChatGPT desktop wants Restart after Save. - The footer primary's verb follows the tab — Copy Command / Copy Config — the classic Mac primary-names-the-action pattern. Cursor's greyed tab still opens; its panel would carry its own
mcp.jsondialect.
Mock C · Live state, not configuration state
- Redrawn against the real cell (
ProjectSidebarOutline.swift:1655-1693): the shipped project row is a two-line cell — title line = user-chosen icon · name · session count trailing; subtitle line = optional MessageKind prefix glyph · status text · the subtitle-right slot, whose occupancy is a documented precedence chain: run ring > in-flight-copy ring >icloudglyph > empty. The agent badge is a new case in that chain, not a new column: ring > copy > agent > icloud. - Cloud + agent coexistence: today they cannot co-occur — an iCloud-evicted (dataless) project cannot be the served project (dataless reads hang; the served corpus is materialised by definition), so slot precedence is honest, not lossy. During a run the ring rightly wins the slot (transient + actionable, hover-× cancel); the agent glyph returns when it clears. If folder scope ever serves evicted rows, the slot grows a two-glyph cluster, agent leading.
- The glyph:
antenna.radiowaves.left.and.rightatsubtitleGlyphConfigsize, tinted accent (palette accent / Apple blue) — transmitting-now semantics for "an outside model is reading this project", shape-distinct from the outlineicloudat 11pt, and the tint does real work: accent = live egress vs secondaryLabel = passive storage state (the shipped cloud tint). Rejected:link(hyperlink-overloaded),sparkles(AI-branding, not connection state),app.connected.to.app.below.fill(muddy at subtitle size), any bare dot (colour-alone; green =MessageKind.success). - The badge means connected now — handshook within the last ~60s, debounced, instant appear/disappear (reduce-motion safe) — never "configured". The row never names the client; the tooltip,
accessibilityLabel, and the reopened sheet do. The glyph's context menu is where Disconnect lives. Clean rows stay clean (Schema E) — the subtitle line exists only while there is state to show. Nowhere else in v1 (not the SPA, not the report); the plumbing precedent is the sidebar's existing per-project serve reads.
127.0.0.1 either. Per §6a this is stated plainly, no apology, no degraded web
path, and no tunnelling as a product path (OpenAI's Secure MCP Tunnel exists; routing
participants' data through it is the same refusal). The desktop story got stronger, not weaker
— verified 30 Jul 2026: ChatGPT desktop is now a local MCP host too (Settings → MCP servers;
one ~/.codex/config.toml shared with Codex CLI and the IDE extension). The manual
sentence: use a local app — Claude Desktop, Claude Code, ChatGPT desktop, Codex — and the in-app
Chat lens is the conversational surface for researchers with no local agent at all.Does the sheet ever show the token?Decided 30 Jul: full token, shown and selectable — if it looks copyable, it is.Where does “disconnect” live?Answered: the trailing badge's context menu.Does the badge name the client?Answered: symbol only; the client is named in the tooltip, accessibilityLabel, and the reopened sheet.- When folder scope lands: does the sheet grow a scope picker, or does the folder row get its own Connect Agent…?
bristlenose mcp --client. Theclaude mcp addcommand moves to the acceptance script. Consequence: the manual's connect-an-agent section is a build deliverable — the URL must resolve before anything ships./mcp/is load-bearing — the bare path 307s and the paste fails.print()(Rich eats[bristlenose]).bristlenose[mcp](installation = the opt-in; no--mcprun flag — an unmounted-vs-mounted endpoint with no token handed out behaves identically). At beta the block likely quiets to the singleMCP:line, with the full snippet behind the futurebristlenose mcpcommand (§6) — where a--client claude|desktopdialect picker belongs.