Source picker, per-query scope toggling, and API key encryption for MCP connections

Gadoes adds a full source-management layer on top of the MCP backend: a database schema for server registries, AES-256-GCM key storage, a region-grouped picker UI, and a per-message scope filter that restricts which MCP tools the LLM can call.

searchchat-ui

Three chunks land here as a unit. The database side (migration 002_mcp_servers.sql) seeds a registry of 12 sources grouped by region - United States (CourtListener, GovInfo), Gulf (Al-Meezan, Saudi MoJ), EU (EUR-Lex, Légifrance, BOE), arbitration (ICSID, ITALAW, UNCITRAL), and others. The table schema distinguishes tier-1 active sources from tier-2/3 "coming soon" entries visible in the picker with a locked badge. Per-user connections live in mcp_connections, with api_key stored as AES-256-GCM ciphertext and a key_version column for rotation tracking. Gadoes wires this through a GET/POST/DELETE /user/mcp-connections REST surface, encrypting on write and decrypting on read via encryption.ts, keyed on MCP_KEY_ENCRYPTION_KEY. There's also an env-fallback path in getMcpApiKey() so self-hosters can supply keys without requiring per-user DB entries.

The picker component (SourcePickerPopover) renders sources grouped by region with state badges - Always on, Connected, Connect, Permission required - plus a search input and region glyphs. It closes on outside click or Escape. A SourcesPill button in ChatInput.tsx shows the active count and opens the popover.

The per-query scope feature adds an mcpScope?: string[] | null field to both POST /chat and POST /projects/:projectId/chat. Inside runLLMStream(), a scopeSet is applied after the existing open-circuit filter: servers not in the scope list are excluded from the active tool definitions passed to the LLM. A null or absent mcpScope means no restriction. The frontend implements this with a useScopeState hook that tracks per-message vs. sticky modes, toggling sources on and off with toggleSource and a clearScope call after send.

One practical note: the 12-source seed catalogue is specific to Gadoes's product. If you take this, you'll want to replace it with your own list. The is_admin column added to user_profiles in migration 003 is consumed downstream by the telemetry dashboard, so plan for that dependency.

So what Worth a look if you're building MCP source management into your fork and want AES-256-GCM key storage plus per-query tool scoping in one batch. The encryption pattern is solid and self-contained. Skip if you're not using Supabase auth or the `user_profiles` layout this assumes - the DB migrations are not easily detached from that setup.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

6 commits from Gadoes/dispumike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
da870cef Chunk 6: Source Connections DB + User Settings UX + Key Encryption Gadoes 2026-05-02 ↗ GitHub
commit body
- Add migrations: mcp_servers (seed data for 12 sources), mcp_connections,
  user_profiles column additions (courtlistener_api_key, govinfo_api_key, is_admin)
- Implement AES-256-GCM encryption in backend/src/lib/mcp/encryption.ts
  (encryptApiKey / decryptApiKey, key from MCP_KEY_ENCRYPTION_KEY env var)
- Add REST API: GET/POST/DELETE /user/mcp-connections with key encryption on write
- Extend getUserApiKeys() and add getMcpApiKey() with env fallback pattern
- Add UserApiKeys.courtlistener and .govinfo fields to llm types
- Create LegalSourcesPage frontend settings page (grouped by region, toggle +
  API key input per server, tier-based 'coming soon' badge)
- Add 'Legal Sources' tab to account settings layout
- 13 backend unit tests (encryption round-trip, format, edge cases)
- 13 frontend component tests all passing; tsc --noEmit clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bb72208c Merge chunk-6-source-connections into main Gadoes 2026-05-02 ↗ GitHub
488ea9b4 Chunk 7: Source Picker Popover UI Gadoes 2026-05-02 ↗ GitHub
commit body
- Create SourcePickerPopover component with search, region grouping, state
  badges (Always on / Connected / Connect / Permission required), region_glyph
  display, and checkbox hooks for Chunk 8 scope toggling
- Create SourcesPill component showing 'Sources · N active' in chat toolbar
- Integrate SourcesPill + SourcePickerPopover into ChatInput.tsx
- Closes on outside click and Escape key
- Footer 'Add custom MCP server' placeholder (disabled for v1)
- 16 component tests covering popover open/close, region order, search
  filtering, state badges, region_glyph, and checkbox callbacks
- tsc --noEmit passes; all 51 frontend tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
c8e13dac Merge chunk-7-source-picker into main Gadoes 2026-05-02 ↗ GitHub
64ec30ab Chunk 8: Per-Query Scope Toggling Gadoes 2026-05-02 ↗ GitHub
commit body
Backend:
- Add mcpScope?: string[] | null to POST /chat and POST /projects/:projectId/chat
- Pass mcpScope to runLLMStream(); filter MCP server tools when scope is set
  (scopeSet filter applied after open-circuit filter, empty array = no filter)

Frontend:
- Create useScopeState hook: activeSources, scopeMode (per-message/sticky),
  toggleSource, clearScope, afterSend, mcpScopePayload
- Add mcpScope? field to streamChat() and streamProjectChat()
- Create ScopeIndicator component: 'Scope: N sources for this thread' + Clear

Tests:
- 7 backend unit tests: mcpScope filtering, open-circuit interaction
- 12 frontend unit tests: useScopeState hook (toggle, sticky/per-message modes,
  clearScope, afterSend, mcpScopePayload computation)
- All 57 backend tests and 63 frontend tests passing
- tsc --noEmit clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
e1df3784 Merge chunk-8-scope-toggling into main Gadoes 2026-05-02 ↗ GitHub

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-30.md from inside the repo you want the changes in.

⬇ Download capture-thread-30.md