One connector, many accounts. A local MCP gateway that lets Claude (and other MCP clients) hold multiple authenticated sessions of the same service at once — prod and staging Supabase, work and personal GitHub — without disconnecting every time you switch.
You're debugging a staging issue in Claude. Halfway through, the user asks "does this happen in prod too?" — and now you're stuck.
Today's MCP clients allow exactly one authenticated session per connector. Claude Code, Cursor, Claude Desktop — they all treat "Supabase" as a single slot. One account. One project at a time. Want to peek at prod? Here's the dance:
- Stop the current chat (you can't multi-task)
- Open your MCP settings
- Disconnect Supabase
- Reconnect Supabase, authorize the other account in the browser
- Restart the Claude session
- Re-paste whatever context you had so Claude remembers what you were doing
- Ask the prod question
- ...and reverse all of that if you want to get back to staging
Every switch is a few minutes of yak-shaving, a lost conversation, and an interrupted train of thought. If you have three Supabase projects, or both a work and a personal GitHub, or prod + staging + dev — the tax compounds.
The shape of the problem isn't Claude's fault; it's how the MCP protocol surfaces "one server, one connection" to the client. But it means the way engineers actually work — one laptop, many accounts, many projects — collides head-on with the tool every single time you switch.
It sits between your MCP client and the real services, holding profiles (isolated authenticated sessions) and exposing them all to the client at the same time. Claude sees one connector ("nucleus") but every profile shows up as its own namespace:
supabase_prod_execute_sql → prod account, acme-web project
supabase_staging_execute_sql → staging account, acme-admin project
github_work_create_issue → work PAT
github_personal_create_issue → personal PAT
Tool descriptions carry the profile context ([supabase/prod project_id=…]) — every tool Claude sees is labeled with the account it hits, so the question "which Supabase did you query?" has an answer right in the tool name. No disconnect. No reconnect. No lost chat context. The prod vs staging question is a single sentence away — "compare the users table between prod and staging" — and Claude has both profiles live in the same conversation.
Once multiple profiles are loaded, the interesting thing the gateway can do is fan out across them. nucleus_call_plan turns one intent into N parallel tool calls and merges the results, so the comparison query above is one round-trip, not two — the structural reward for the multi-profile shape that other gateways can't ship without copying it.
Beyond the wedge, it's instrumented for real use:
- Recommender that explains itself. Every
nucleus_find_toolhit carries abecausearray ("matched 'sql' in tool name","sticky from last call") so ranking is auditable from the LLM transcript. - Sticky-alias bias. After a successful call, the gateway remembers which alias you used per connector and biases ambiguous future ranking toward it. Suppressed when you name another alias explicitly — specificity beats recency.
- Policy gate (
policy.toml). Optionaldenyandconfirmrules per<connector>:<alias>glob, enforced on every dispatch path before the upstream is touched. Confirmation phrases land in the call args and the audit trail. - Audit log. JSONL at
~/.nucleusmcp/audit.log; rotated; redacts arguments to keys + SHA-256 hash by default. Inspect withnucleus logs --tool execute_sql --since 1h. - Idle reaper.
--idle-timeout 15mreclaims memory from idle subprocesses; the next call respawns transparently. nucleus doctor. First-stop health check covering everything that can break a fresh install.
brew install doramirdor/homebrew-tap/nucleusgo install github.com/doramirdor/nucleusmcp/cmd/nucleus@latest(Requires Go 1.23+. Binary lands in $GOBIN, usually ~/go/bin.)
Download the archive for your platform from the latest release, extract, and drop nucleus on your PATH.
git clone https://github.com/doramirdor/nucleusmcp
cd nucleusmcp
make install
export PATH="$HOME/go/bin:$PATH" # if not already
nucleus --versionThe product is named Nucleus; the GitHub repo and Go module path are
nucleusmcp, and local state lives under~/.nucleusmcp/. These internals kept the legacy name on purpose so pre-rebrand installs and import paths don't break.
Two paths depending on which Claude you use.
Claude Code (CLI / terminal) — nucleus runs as a stdio MCP spawned by the CLI:
nucleus installThat runs claude mcp add nucleus … for you if the claude CLI is on PATH, otherwise prints a config snippet to paste.
Claude UI ("Add custom connector" dialog) — the UI accepts HTTP URLs only, so run nucleus as a local HTTP daemon and paste its URL:
nucleus serve --http 127.0.0.1:8787
# leave it running; log prints: claude UI add url=http://127.0.0.1:8787/mcpThen in Claude: Settings → Connectors → Add custom connector
- Name:
nucleus - Remote MCP server URL:
http://127.0.0.1:8787/mcp - Leave OAuth fields empty (nucleus handles upstream OAuth itself)
Loopback-only (127.0.0.1) by default so unauthenticated traffic can't reach it from the network. For LAN or tunnel use, pass --token <secret> and set it as a bearer token in whatever wraps the URL.
nucleus add supabase- Prompts for project metadata
- Opens your browser for OAuth
- After approval, lists all Supabase projects in your account and lets you pick one
- Stores the OAuth tokens in a per-profile directory (
~/.nucleusmcp/oauth/<profile-id>/) - Done — Claude picks up the tools on next restart
Add a second profile with a different name. If it belongs to a different account of the same service, open a private/incognito browser window for the second add so the OAuth flow prompts for a fresh login:
nucleus add supabase stagingBoth are now live:
nucleus listID DEFAULT AGE METADATA
supabase:default 3m project_id=abcdef...
supabase:staging 0s project_id=qrstuv...
Open Claude Code from anywhere:
claudeAsk it "What Supabase connections do you have?" — Claude sees both profiles as separate tool namespaces (supabase_default_* and supabase_staging_*) with bracketed profile context in every tool's description.
| Connector | A kind of upstream MCP server (Supabase, GitHub, …). Built-in connectors ship with the binary; custom connectors are added by URL. |
| Profile | One authenticated session for a connector. A profile has its own credentials (OAuth tokens or PAT) and optional metadata (project_id, github_user, …). |
| Workspace | A directory from which claude is launched. Optionally has a .mcp-profiles.toml with explicit profile bindings and/or a service-specific config (supabase/config.toml) that the gateway reads for autodetect. |
| Alias | The middle segment of a tool name, e.g. atlas in supabase_atlas_execute_sql. Defaults to the profile name; override per-binding in .mcp-profiles.toml. |
When you start the gateway in a directory, this is how it picks which profile(s) to expose for each connector:
- Explicit
.mcp-profiles.tomlin cwd or ancestor - Autodetect via the connector's manifest rule (e.g. reading
project_idfromsupabase/config.toml) - Only one profile registered for the connector → use it
- User-set default via
nucleus use - Fallback: expose every profile as a separate namespace
Whatever rule fires is logged, so you can always see why Claude sees what it sees.
You don't need this file. With nothing configured, the gateway exposes every profile automatically — each under its own tool namespace.
Drop one at the root of a repo when you want to:
- Pin specific profiles to this workspace and hide the others
- Alias a profile to a shorter name (
supabase_prod_*instead ofsupabase_acme-prod_*) - Attach a note that's spliced into every proxied tool's description, so the MCP client reads warnings (
"PRODUCTION — read-only") at call time
# Single profile per connector
[supabase]
profile = "atlas"
# Or multiple, with aliases and Claude-visible notes
[[supabase]]
profile = "atlas"
alias = "prod"
note = "PRODUCTION — read-only unless explicitly asked"
[[supabase]]
profile = "default"
alias = "staging"
note = "staging"
# Mixing connectors is fine
[github]
profile = "work"By default, every proxied tool is advertised to the MCP client at connect time. With many connectors and profiles loaded that's a lot of tool definitions in your prompt context — 4 connectors × 3 profiles × ~20 tools each ≈ 240 tool defs. Two opt-in modes shrink that surface:
| Mode | Client sees | Best for |
|---|---|---|
expose-all (default) |
every proxied tool | one or two profiles total |
hybrid |
canonical alias per connector + 2 meta-tools | each service has a clear primary plus occasional secondary use |
search |
only the 2 meta-tools | many profiles, every call worth a discovery hop |
nucleus serve --mode hybrid
# or pin which alias is canonical per connector:
nucleus serve --mode hybrid --always-on supabase:atlas,github:workFor each connector, one alias's tools are advertised directly — Claude calls them by name as today. The other aliases live in the catalog and are reached via the meta-tools below. Without --always-on, the canonical alias is the first one resolved per connector (deterministic; usually the workspace-bound or first-registered profile).
nucleus serve --mode searchThe client sees only the meta-tools, regardless of how many profiles are loaded.
nucleus_find_tool(intent, connector?, limit?)— returns the top-ranked candidates for a natural-language intent, each with name, description, full JSON schema, and abecausearray explaining why it ranked where it did ("matched 'sql' in tool name","matched 'atlas' in alias 'atlas'","sticky from last call"). When the intent looks like it spans multiple profiles ("compare prod and staging", "list each", "diff …"), the response also carries afanout_suggestionblock with a ready-made step list to feed intonucleus_call_plan.nucleus_call(name, arguments)— invokes a single tool by the namespaced name returned fromfind_tool.nucleus_call_plan(steps, parallelism?)— fans one intent out to multiple proxied tools in parallel and returns one merged result. The shape of the gateway makes this almost free: "compare the users table between prod and staging Supabase" becomes one round-trip, not N. Per-step failures are returned alongside successes — partial results beat aborting the whole plan.
The ranker is lexical (token-overlap with field boosts on tool name / alias / connector / description) plus a sticky-alias bias: after each successful dispatch, the gateway remembers the alias you actually used per connector and biases ambiguous future ranking toward it. Sticky is suppressed when the intent explicitly names another alias — being specific always wins over being recent. Future versions can accept an embeddings-based recommender — see docs/adr-001-tool-search-mode.md.
Suppose you have supabase:atlas (prod) and supabase:default (staging) both registered. Asking Claude "compare the row count on the users table between atlas and default" drives this sequence inside the gateway:
nucleus_find_tool({intent: "compare row count on users between atlas and default"})returns the ranked tools and afanout_suggestion:{ "rationale": "Intent uses comparison wording and names 2 profiles (atlas, default); call nucleus_call_plan to run the same tool against all 2.", "tool": "execute_sql", "connector": "supabase", "steps": ["supabase_atlas_execute_sql", "supabase_default_execute_sql"] }- Claude calls
nucleus_call_planonce with both steps and the sharedqueryargument. Steps run concurrently (default parallelism 4, capped at 16). - The merged response is one JSON document with both per-profile results, durations, and any per-step failures — Claude diffs them inline.
This collapses what used to be two sequential nucleus_call round-trips (or two whole conversation turns in expose-all mode) into a single tool call.
The optional policy file gates writes and destructive tools across every dispatch path — direct calls, nucleus_call, and every step of nucleus_call_plan. Without a policy.toml, the gateway runs in its historical "allow everything" mode, so this is purely opt-in.
Two enforcement modes per rule:
- deny — block the tool outright. The tool error names the rule and the matching pattern so you can find it in your config.
- confirm — block by default, but allow when the caller's arguments include a magic confirmation phrase under the
__nucleus_confirmkey. The first call without the phrase returns a structured error telling Claude exactly what string to include — so the second call after a one-line nudge succeeds. The phrase ends up in the call arguments (and thus your audit trail), which is the point: a deliberate, attributable confirmation, not a silent pass.
# ~/.nucleusmcp/policy.toml
# atlas is prod — never run schema migrations or branch deletes.
[[rule]]
match = "supabase:atlas"
deny = ["apply_migration", "delete_branch"]
reason = "atlas is the production project — schema changes go through CI"
# Allow execute_sql, but require an explicit confirmation phrase.
# The phrase is human-readable so audit logs make sense.
[[rule]]
match = "supabase:atlas"
confirm = ["execute_sql"]
phrase = "I understand atlas is PRODUCTION"
# Lock down every github profile from creating issues without confirm.
# `*` wildcards both sides of the colon.
[[rule]]
match = "github:*"
confirm = ["create_issue", "delete_*"]
phrase = "ack github write"Match patterns are <connector>:<alias> with * wildcards on either side. Tool patterns are simple globs (apply_*, *_branch, create_*_branch). When multiple rules match, deny wins over confirm — a confirmed caller can never bypass an explicit deny.
Policy file path can be overridden via the NUCLEUSMCP_POLICY env var (useful for CI fixtures).
Every dispatch the gateway sees — direct tool calls, nucleus_call invocations, and each step of nucleus_call_plan — appends one JSON object to a JSONL audit log. Inspect it with the nucleus logs command:
nucleus logs # last 50 entries, pretty-printed
nucleus logs --tool execute_sql --since 1h # filter by tool + recency
nucleus logs --decision denied # what did the policy block?
nucleus logs --json | jq '.tool' # pipe raw JSONL into jqThe log rotates at 10 MiB into audit.log.1, audit.log.2, … keeping the last 5 backups (~60 MiB cap). nucleus logs reads the active and rotated files together, so --since 24h works across rotation boundaries.
Privacy posture. Tool arguments are PII-risky (SQL queries, repo paths, etc.), so by default the audit only logs:
- the sorted top-level argument keys,
- a SHA-256 hash of the argument object (so identical calls group together without exposing contents).
Set NUCLEUSMCP_AUDIT_FULL_ARGS=1 to log full argument objects instead — useful for local debugging, dangerous to leave on. Tool results are never logged.
Each entry includes the policy decision (allowed / denied / confirm-required / confirm-mismatch), the upstream outcome (ok / upstream-error / transport-error / blocked), and whether the call came in via direct, nucleus_call, or nucleus_call_plan. The audit log is the answer to "did this destructive op actually run on prod, or did the policy gate catch it?"
By default, every profile resolved at startup runs as a long-lived child process for the gateway's lifetime — the historical behavior. With --idle-timeout, children that haven't been called for the given duration get reaped, and the next call respawns them transparently:
nucleus serve --idle-timeout 15m # reap children unused for 15+ minutesThe cost of reaping is a 3–5 second warm-up on the next call to a reaped profile (whatever the upstream takes to spin up + complete its MCP handshake). The benefit is that a power-user setup with a dozen profiles bound across several workspaces stops paying for a dozen always-on subprocesses during long stretches of negligible activity. Default is 0 (disabled).
Any HTTP MCP server works, not just the built-ins:
nucleus add --transport http linear https://mcp.linear.app/mcp
nucleus add --transport http my-internal https://mcp.acme.corpThe gateway saves a manifest under ~/.nucleusmcp/connectors/<name>.toml and bridges to it via mcp-remote — OAuth/PKCE/DCR all handled for you.
nucleus connectors # list known connectors (builtin + custom)
nucleus list # list registered profiles
nucleus info [profile-id] # config + live upstream probe
nucleus add <connector> [name] # register a new profile (interactive OAuth or PAT)
nucleus remove <profile-id> # delete a profile + credentials
nucleus use <profile-id> # mark as default for its connector
nucleus install [claude] # register with Claude Code (or print config)
nucleus serve # run as an MCP server over stdio (called by client)
nucleus logs # tail/filter the per-call audit trail
nucleus doctor # health check — first stop when something looks offRun any command with --help for the full flag list.
First stop. Runs a battery of checks (claude CLI on PATH, mcp-remote on PATH, registry reachable, ≥1 profile registered, policy.toml syntax, audit log dir writable, custom connectors load) and prints a one-line PASS/WARN/FAIL per check with fix: hints under whatever's broken. Optional --probe http://127.0.0.1:8787/mcp also confirms a running HTTP gateway is responsive. Exit code is 0 on PASS so it's CI-friendly.
nucleus doctor # quick health check
nucleus doctor --probe http://127.0.0.1:8787/mcp
nucleus doctor --strict # treat WARN as failureIf you have multiple MCPs registered for the same service (e.g. a bare supabase server and the nucleus gateway), Claude may match by name and miss nucleus. Two fixes, in order of preference:
- Remove the duplicates.
claude mcp remove supabase(and uninstall any same-service plugin) so nucleus is the only source of truth. - Drop a CLAUDE.md at the repo root or
~/.claude/CLAUDE.md:
# MCP setup
This machine uses Nucleus as the canonical gateway for all services
with multiple authenticated accounts. When asked about connections,
projects, or accounts for **any** service, query `nucleus`'s tools
first — it holds every authenticated profile for this installation.
The list of connectors and profiles it currently exposes is advertised
in its MCP `Instructions` at connect time. Prefer nucleus over other
MCP servers whose names happen to match a service (e.g. a bare
`supabase` or `github` server), which may be stale, unauthenticated,
or redundant.(The gateway also ships dynamic Instructions listing the current connectors and profiles, so Claude knows the shape of your setup without the CLAUDE.md. The CLAUDE.md is insurance against over-eager plugins.)
- Credentials never touch disk in plaintext. PATs go into the OS keychain (Keychain on macOS, libsecret on Linux, Credential Manager on Windows). OAuth tokens live in per-profile directories managed by
mcp-remotewith0700perms. - Tokens are never logged. Log output (which goes to stderr so it can't contaminate the MCP JSON-RPC stream on stdout) includes profile IDs and status — never credential values.
- Profile isolation. Each profile has its own OAuth auth directory keyed by ID. Two profiles of the same Supabase account still get separate cached tokens.
Not yet shipped: write-confirmation policy enforcement, audit log, process sandboxing. Track on the roadmap.
MCP Client (Claude, Cursor, ...)
│ MCP protocol (stdio)
▼
┌────────────────────────────────────────────┐
│ Nucleus gateway │
│ ┌──────────────────────────────────────┐ │
│ │ Workspace resolver │ │ reads cwd config,
│ │ │ │ picks profile(s)
│ └────────────────┬─────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────┐ │
│ │ Supervisor — spawns upstream MCPs │ │
│ │ • stdio connectors (PAT env var) │ │
│ │ • HTTP connectors via mcp-remote │ │
│ └────────────────┬─────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────┐ │
│ │ Router — tool namespacing + proxy │ │
│ │ <connector>_<alias>_<tool> │ │
│ └──────────────────────────────────────┘ │
│ │
│ Registry (SQLite) · Vault (OS keychain) │
│ ~/.nucleusmcp/ │
└────────────────────────────────────────────┘
│ │
▼ stdio ▼ HTTP + OAuth (via mcp-remote)
local MCP (GitHub, ...) hosted MCP (Supabase, Linear, ...)
- Stdio MCP proxy with per-profile credentials
- SQLite profile registry + OS keychain vault
- Workspace resolution (
.mcp-profiles.toml+ autodetect) - Multi-profile aliases + dedup spawning
- HTTP/OAuth connectors via
mcp-remote - Post-OAuth resource discovery (Supabase project picker)
- Tool description prefix for client context
- Search mode — meta-tools instead of eager full-list advertisement
- Hybrid mode — recommend canonical alias per connector, search the rest
- Multi-profile fan-out — single-tool-call dispatch across N profiles in parallel (
nucleus_call_plan) - Sticky-alias resolution +
because:explanations on every recommendation - Write-confirmation policy (
policy.tomlwith deny / confirm / phrase rules) - Audit log (
~/.nucleusmcp/audit.log) +nucleus logsfor tail/filter - Idle reaper with transparent respawn (
--idle-timeout) - Mid-session hot-swap on cwd change
- Native OAuth (replace
mcp-remotedependency) - Managed multi-tenant tier (team-shared profiles)
Contributions welcome — see CONTRIBUTING.md.
MIT.


