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).

Framed panes = the artefact (what the researcher sees). Tinted blocks = commentary (why, and what's load-bearing).

Part 1 — the CLI spike SHIPS IN SPIKE

Scene 1 · Start serve — the connect moment

Terminal — bristlenose serve
$ bristlenose serve ~/Research/acme-onboarding Report: http://127.0.0.1:8150/report/ MCP: http://127.0.0.1:8150/mcp/ Connect an agent — give it the MCP URL and this header. Works with any MCP-compatible agent (Claude, ChatGPT, Codex). Sends the quotes in your report, speaker codes in place of names; quote text is verbatim. How-to: https://bristlenose.app/docs/connect-an-agent.html Authorization: Bearer EXAMPLEtokenNOTREALaaaaaaaaaaaaaaaaaaaaaaaa [bristlenose] auth-token: EXAMPLEtokenNOTREALaaaaaaaaaaaaaaaaaaaaaaaa INFO: Uvicorn running on http://127.0.0.1:8150 (Press CTRL+C to quit)

Scene 2 · Connected

Terminal — claude
$ claude mcp add --transport http bristlenose http://127.0.0.1:8150/mcp/ \ --header "Authorization: Bearer kJ8f…3nQ" Added HTTP MCP server bristlenose $ claude > /mcp bristlenose ✔ connected · 4 tools · instructions loaded

Scene 3 · Orientation — the cheap first call

Claude Code — conversation
> give me an overview of this study ⏺ bristlenose · get_project_overview(project_id=1) ⎿ 2.1 KB · 240msAcme Onboarding — 6 sessions, 5 participants (p1–p5), analysed 2 days ago (last run completed). 214 quotes across 7 sections (Dashboard, Search, Onboarding…) and 3 themes; 18 starred, 9 hidden. Codebook: 2 frameworks applied (norman, uxr — both enabled) plus 11 of your own codes. Strongest signal: frustration concentrated in Onboarding (4 of 5 participants, n_eff 3.8). Want the signals, or a look at your codebook?

Scene 4 · Reasoning in the researcher's taxonomy

Claude Code — conversation
> which of my codes are doing no work? ⏺ bristlenose · get_framework(project_id=1, framework_id="codebook") ⎿ 6.4 KB · 310ms ⏺ bristlenose · get_signals(project_id=1, lens="tags") ⎿ 8.9 KB · 380ms ⏺ Three of your eleven codes are pulling no weight: · trust-wobble — 1 quote, and its boundary ("apply when confidence in the system dips mid-task, not general scepticism") overlaps your busier hesitation code (14 quotes) · pricing-mention — 0 quotes since you defined it · workaround — 2 quotes, both also tagged coping-strategy Worth asking whether trust-wobble and hesitation are one concept — that's your call, not mine; the boundaries you wrote distinguish intent, and only you know if the distinction is earning its keep.

Scene 5 · The baits — do the invariants land?

Claude Code — conversation (acceptance round 1: instructions only)
> how many quotes are there in total across all the sections? ⏺ 214 — that's the corpus total from the overview. I'm not summing the per-section counts to get it: every quote lives in exactly one section, but themes cross-cut them, so adding section and theme counts together would double-count. The honest number is the corpus total. > is p1 here the same person as p1 in my other study? ⏺ I can't tell you, and I shouldn't try. Speaker codes are per-study — p1 here has no relationship to p1 anywhere else, and this server holds one project. If they genuinely are the same panel across waves, that's a fact you hold and declare, not something to infer from a matching code.

Scene 6 · Worst case — the context budget (Q3)

Claude Code — conversation
> list every quote that mentions onboarding, verbatim ⏺ bristlenose · search_quotes(project_id=1, query="onboarding", limit=50) ⎿ 78.4 KB · 410ms · 50 of 63 matched, has_more=true ⏺ 63 quotes mention onboarding; here are the first 50, verbatim, with ids — [q-p2-841] "I genuinely could not find where to start…" (p2, Onboarding, frustration) … [49 more] Shall I fetch the remaining 13?

Exceptions — every way it goes wrong, and what the researcher sees

E1 · Serve restarted → token rotated
> /mcp bristlenose ✘ 401 unauthorized

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.

E2 · mcp extra not installed
Report: http://127.0.0.1:8150/report/ MCP: unavailable — pip install 'bristlenose[mcp]'

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).

E3 · Last run failed → stale data flagged
⏺ Heads up: the 28 Jul run failed; what I can see is the last completed analysis (12 Jul, 5 sessions). The 6th session isn't in these numbers.

The overview carries last_run outcome + timestamp — the same honesty the SPA's status page enforces, threaded into the tool payload.

E4 · Brand-new project, nothing analysed
⏺ This project has no analysed data yet — run the pipeline first, then ask me again.

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.

E5 · Typo'd filter → self-heal
⏺ search_quotes(sentiment="frustrated") ⎿ error: unknown sentiment "frustrated" — valid: frustration, confusion, doubt, satisfaction, confidence, delight, surprise ⏺ search_quotes(sentiment="frustration") ⎿ 41 matched

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".

E7 · Clicked the MCP link in a browser
This is Bristlenose's MCP endpoint.
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.

E6 · The token in your repo
$ claude mcp add --scope project⚠ writes .mcp.json (including the bearer token) into the project directory — don't commit it

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.

Deliberately absent from the spike: any connect UI, stable tokens, desktop/SPA exposure, folder scope, writes, transcripts (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

Show in Finder
Connect Agent…
Choose Icon
Move to
Rename…
Remove from Sidebar

Mock B · The connect sheet — four things, nothing else

Connect an agent to “Acme Onboarding”

6 sessions · 214 quotes

Anonymise Remove participant names
Claude Desktop
Claude Code
ChatGPT & Codex
Cursor
In Claude Desktop: Settings → Developer → Edit Config or edit ~/Library/Application Support/Claude/claude_desktop_config.json adding under mcpServers:
"bristlenose": { "url": "http://127.0.0.1:8150/mcp/", "headers": { "Authorization": "Bearer EXAMPLEtokenNOTREALaaaaaaaaaaaaaaaaaaaaaaaa" } }
Works with any MCP-compatible agent.

Mock B2 · The other tabs — same two primitives, each client's dialect

Claude Desktop
Claude Code
ChatGPT & Codex
Cursor
Paste in any terminal:
claude mcp add --transport http bristlenose http://127.0.0.1:8150/mcp/ \ --header "Authorization: Bearer EXAMPLEtokenNOTREALaaaaaaaaaaaaaaaaaaaaaaaa"
Claude Desktop
Claude Code
ChatGPT & Codex
Cursor
In ChatGPT: Settings → MCP servers → Add server — or add to ~/.codex/config.toml (one stanza covers ChatGPT desktop, Codex CLI, and the IDE extension):
[mcp_servers.bristlenose] url = "http://127.0.0.1:8150/mcp/" http_headers = { "Authorization" = "Bearer EXAMPLEtokenNOTREALaaaaaaaaaaaaaaaaaaaaaaaa" }

Mock C · Live state, not configuration state

PROJECTS
Acme Onboarding 6
Pharmacy Diary Study 12
In iCloud
Q2 Checkout Benchmarks 9
tooltip / VoiceOver on the antenna: “Agent connected — Claude Code, active 2 minutes ago”
Browser-based agents (claude.ai / chatgpt.com): cannot reach a local server by architecture — connectors execute from the vendor's infrastructure, and a browser page can't call 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.
Explicitly not proposed: a toolbar icon (v1), a “Claude lens” inside the app (refused in §6a — an empty room with a signpost), or writing another app's config files (sandbox + App Review). Open for critique: