Skip to content

Commit 7d762a8

Browse files
committed
docs(planning): add server account clones implementation plan
Documents UI-assisted multi-account installs (Option 2) with optional first-class instances deferred to a later phase. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent d519a79 commit 7d762a8

1 file changed

Lines changed: 268 additions & 0 deletions

File tree

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
# Server Account Clones (UI-Assisted Multi-Account)
2+
3+
**Last Updated:** May 23, 2026
4+
**Status:** Planning — decisions locked, not started
5+
**Branch:** TBD — file after planning review
6+
**Base branch:** `main`
7+
**Issue:** TBD — file after planning review
8+
**Depends on:** None (orthogonal to session meta-tools; benefits from but does not require PR #154)
9+
**Unblocks:** Personal MCP migration (`jsg-tech-check/docs/setup/mcpmux-server-migration.md`) — Gmail, Sheets, PostHog ×2, Firebase ×4, and other single-account stdio servers
10+
11+
---
12+
13+
## Problem
14+
15+
McpMux installs servers once per `(space_id, server_id)` — enforced by `UNIQUE(space_id, server_id)` on `installed_servers`, `credentials`, and `outbound_oauth_clients`. That model works when one Space maps to one account (Personal vs S2H vs GAIT), but breaks down when a user needs **two accounts for the same MCP in the same Space**:
16+
17+
| Server type | Example need | Current workaround |
18+
| ----------- | ------------ | ------------------ |
19+
| Native multi-account | Google Workspace (`user_google_email` per call) | One install — works today |
20+
| Single-account stdio | PostHog personal + work in Personal Space | Hand-edit user space JSON with suffixed IDs (`posthog-personal`, `posthog-work`) |
21+
| Single-account OAuth HTTP | Two Notion workspaces in one Space | Same JSON hack or split Spaces |
22+
| Env-at-startup servers | Firebase ×4 projects | Four manual JSON entries with different env paths |
23+
24+
The workaround **works** — custom entries with unique IDs get separate processes, credential rows, and tool prefixes — but it is undiscoverable, error-prone, and regresses the "click Install" UX. Users migrating from a 40-entry `~/.cursor/mcp.json` hit this immediately.
25+
26+
Spaces remain the canonical answer for **context-level** separation (work vs personal repos). This feature targets **account-level** duplication inside a Space when context splitting is wrong or insufficient.
27+
28+
---
29+
30+
## Decisions
31+
32+
| # | Decision | Choice | Rationale |
33+
| - | -------- | ------ | --------- |
34+
| 1 | Primary approach | **Option 2: UI-assisted clone** — new `server_id` + `manual_entry`, no schema migration | Highest value-to-effort. Removes JSON-editing pain without touching OAuth/credential layer mid-migration. |
35+
| 2 | Schema change | **Defer Option 3** (`instance_label` column) to optional Phase 5 | Architecturally cleaner long-term, but 2–3 weeks of migration risk across 5+ tables. Ship clones first; revisit when migration volume justifies it. |
36+
| 3 | Clone identity | **`{base_server_id}-{suffix}`** where suffix is user-chosen (default suggestions: `work`, `personal`, `prod`) | Satisfies unique constraint. Hyphen suffix only — underscores are stripped by `normalize_server_id` and reserved as the tool-name delimiter. |
37+
| 4 | Definition source | **Copy `cached_definition` from source install** into clone at creation time | Clone is self-contained for offline/gateway startup. Registry updates do not auto-propagate — acceptable tradeoff for v1; document in UI. |
38+
| 5 | Prefix / alias | **Auto-set alias = suffix** (e.g. `posthog-work` → tools prefixed `posthog-work_*`) | Reuses `PrefixCacheService` first-come assignment. User can override alias in configure step. |
39+
| 6 | Credentials | **Never copy secrets** — clone starts with empty `input_values` / no OAuth; user configures fresh | Prevents accidental credential sharing. Clone wizard opens configure flow immediately after create. |
40+
| 7 | Source tracking | **`InstallationSource::ManualEntry`** + optional `cloned_from: Option<String>` metadata on `InstalledServer` | Distinguishes registry installs from clones in UI (`SourceBadge`). `cloned_from` is display-only in v1 — not a FK. |
41+
| 8 | Registry dedup UX | **"Add another account" disabled when source is already a clone-of-clone** (max depth 1) or when suffix collision detected | Prevents unbounded ID sprawl (`posthog-work-work-work`). Clones clone from registry/original only. |
42+
| 9 | Spaces unchanged | **No change to Space model** — clone is per-Space like any install | Work/personal split via Spaces stays documented as primary pattern; clones are the escape hatch. |
43+
44+
---
45+
46+
## The Model
47+
48+
### What a clone is
49+
50+
A clone is a **new `InstalledServer` row** in the same Space as the source, with:
51+
52+
```text
53+
InstalledServer {
54+
server_id: "{base_id}-{suffix}", // e.g. "posthog-work"
55+
server_name: "{display} ({suffix})", // e.g. "PostHog (work)"
56+
cached_definition: <copy from source>,
57+
input_values: {}, // empty — user fills in configure step
58+
source: ManualEntry,
59+
cloned_from: Some("{base_id}"), // new optional field, v1 display-only
60+
enabled: false, // same default as registry install
61+
}
62+
```
63+
64+
Prefix resolution treats the clone as an independent server. Tool names become `{alias}_{tool}` (e.g. `posthog-work_capture_event`).
65+
66+
### What a clone is NOT
67+
68+
- Not a second OAuth session on the same `server_id` row
69+
- Not a runtime credential swap (stdio env is fixed at process spawn)
70+
- Not a registry duplicate — the registry still has one definition for `posthog`; clones are local installs
71+
72+
### Composition with existing patterns
73+
74+
```text
75+
Multi-account need?
76+
├─ MCP has per-call account param (Google Workspace)
77+
│ └─ ONE install — no clone needed
78+
├─ Accounts map to repo context (Personal / S2H / GAIT)
79+
│ └─ Spaces — no clone needed
80+
└─ Two+ accounts in SAME Space, single-account MCP
81+
└─ Clone via "Add another account" (this feature)
82+
```
83+
84+
---
85+
86+
## Architecture
87+
88+
```
89+
┌─────────────────────────────────────────────────────────────────┐
90+
│ My Servers UI │
91+
│ │
92+
│ [PostHog ▼] Connected │
93+
│ ├─ Configure / Logs / … │
94+
│ └─ "Add another account…" ─────────────────────┐ │
95+
└────────────────────────────────────────────────────│────────────┘
96+
97+
98+
┌──────────────────────────────────┐
99+
│ CloneAccountModal │
100+
│ • suffix input (work/personal/…) │
101+
│ • alias preview (posthog-work) │
102+
│ • collision check │
103+
└──────────────────────────────────┘
104+
105+
106+
┌──────────────────────────────────┐
107+
│ ServerAppService::clone_server │
108+
│ 1. validate unique server_id │
109+
│ 2. copy cached_definition │
110+
│ 3. set alias in definition JSON │
111+
│ 4. install as ManualEntry │
112+
│ 5. emit ServerInstalled │
113+
└──────────────────────────────────┘
114+
115+
┌────────────────────────────────┴───────────────┐
116+
▼ ▼
117+
installed_servers row credentials row(s)
118+
(new server_id) (empty until configure)
119+
120+
121+
PrefixCache assigns alias on first enable/connect
122+
123+
124+
Separate stdio process / OAuth flow with clone's creds
125+
```
126+
127+
- **No gateway routing changes** — clone is a distinct `server_id`; existing prefix cache + routing already handle multiple servers in one Space.
128+
- **FeatureSets** see clones as separate servers — user adds `posthog-work` to a FeatureSet independently. Future Option 3 could add "all instances of server X" grouping.
129+
- **Meta tools** (`mcpmux_enable_server`) already accept any `server_id` string — clones work with session enable once the user knows the suffixed ID. Phase 3 adds optional `cloned_from` hint in `mcpmux_list_servers` response.
130+
131+
---
132+
133+
## Files to create
134+
135+
| File | Purpose |
136+
| ---- | ------- |
137+
| `apps/desktop/src/features/servers/CloneAccountModal.tsx` | Suffix input, alias preview, collision feedback, submit → Tauri command |
138+
| `apps/desktop/src/lib/api/serverClone.ts` | TS wrappers: `cloneServer`, `suggestCloneSuffix`, `isCloneIdAvailable` |
139+
| `apps/desktop/src-tauri/src/commands/server_clone.rs` | Tauri commands delegating to `ServerAppService::clone_server` |
140+
| `tests/rust/tests/integration/server_clone.rs` | Clone creates distinct install, empty creds, unique prefix, collision rejection |
141+
| `docs/planning/server-account-clones.md` | This doc |
142+
143+
## Files to modify
144+
145+
| File | Change |
146+
| ---- | ------ |
147+
| [`crates/mcpmux-core/src/domain/installed_server.rs`](../../crates/mcpmux-core/src/domain/installed_server.rs) | Add optional `cloned_from: Option<String>`. Builder `with_cloned_from`. |
148+
| [`crates/mcpmux-core/src/application/server.rs`](../../crates/mcpmux-core/src/application/server.rs) | `clone_server(space_id, source_server_id, suffix, alias_override?)` — copy definition, derive new ID, install as `ManualEntry`. |
149+
| [`crates/mcpmux-storage/src/repositories/installed_server_repository.rs`](../../crates/mcpmux-storage/src/repositories/installed_server_repository.rs) | Serialize/deserialize `cloned_from` (new nullable column or JSON in existing row — see Phase 1). |
150+
| [`crates/mcpmux-storage/src/migrations/`](../../crates/mcpmux-storage/src/migrations/) | New migration: `cloned_from TEXT` nullable on `installed_servers`. |
151+
| [`apps/desktop/src/features/servers/ServerActionMenu.tsx`](../../apps/desktop/src/features/servers/ServerActionMenu.tsx) | Add "Add another account…" action; hidden for clones-of-clones. |
152+
| [`apps/desktop/src/features/servers/ServersPage.tsx`](../../apps/desktop/src/features/servers/ServersPage.tsx) | Wire modal, group clones visually under source (optional Phase 2 polish). |
153+
| [`apps/desktop/src/components/SourceBadge.tsx`](../../apps/desktop/src/components/SourceBadge.tsx) | Badge variant for cloned servers ("Clone of posthog"). |
154+
| [`apps/desktop/src-tauri/src/lib.rs`](../../apps/desktop/src-tauri/src/lib.rs) | Register `clone_server`, `suggest_clone_suffix`, `is_clone_id_available` commands. |
155+
| [`crates/mcpmux-gateway/src/services/meta_tools/tools.rs`](../../crates/mcpmux-gateway/src/services/meta_tools/tools.rs) | (Phase 3) Optional `cloned_from` field in `mcpmux_list_servers` payload. |
156+
| [`docs/guide/servers.mdx`](../../docs/guide/servers.mdx) | Document multi-account patterns: Spaces vs native params vs clones. |
157+
158+
---
159+
160+
## Phasing
161+
162+
### Phase 1 — Core clone API + storage
163+
164+
**Effort:** ~1 day
165+
166+
- [ ] Migration: `cloned_from TEXT NULL` on `installed_servers`
167+
- [ ] `InstalledServer.cloned_from` field + repo round-trip
168+
- [ ] `ServerAppService::clone_server`:
169+
- Load source install + definition from `cached_definition`
170+
- Derive `new_id = "{base}-{suffix}"` using same normalization as `UserServerEntry::normalize_server_id`
171+
- Reject if `(space_id, new_id)` exists or source is missing
172+
- Patch definition `alias` to suffix (or user override)
173+
- Install via existing `install()` path with `ManualEntry` + `with_cloned_from(source_id)`
174+
- [ ] Unit tests: happy path, collision, missing source, suffix normalization (no underscores)
175+
- [ ] Tauri command `clone_server(space_id, source_server_id, suffix, alias?)`
176+
177+
**Outcome:** `clone_server` from Tauri creates a disabled `posthog-work` install with copied definition, empty creds, and `cloned_from = "posthog"`. Verifiable via `list_installed_servers` and SQLite inspection. No UI yet.
178+
179+
### Phase 2 — Clone wizard UI
180+
181+
**Effort:** ~1 day
182+
183+
- [ ] `CloneAccountModal` — suffix field with suggestions (`work`, `personal`, `prod`, `staging`), live alias preview, inline collision error
184+
- [ ] `ServerActionMenu` → "Add another account…" on registry and manual installs (not on clones)
185+
- [ ] Post-clone flow: open existing `ConfigEditorModal` for credential entry before enable
186+
- [ ] `SourceBadge` shows clone lineage
187+
- [ ] Optional: collapsed "Accounts" group on `ServersPage` when `cloned_from` matches same base (visual only, no schema)
188+
189+
**Outcome:** User clicks "Add another account" on PostHog, enters suffix `work`, gets `posthog-work` card in My Servers, configures API key, enables — tools appear as `posthog-work_*` in gateway. No JSON editing.
190+
191+
### Phase 3 — Meta-tool + docs surfacing
192+
193+
**Effort:** ~0.5 day
194+
195+
- [ ] `mcpmux_list_servers` returns optional `cloned_from` for clone rows
196+
- [ ] `docs/guide/servers.mdx` section: "Multiple accounts" — decision tree (Spaces / native param / clone)
197+
- [ ] Migration doc update in `jsg-tech-check` with concrete clone targets (PostHog, Gmail, Sheets, Firebase)
198+
199+
**Outcome:** LLM manifest shows clone lineage. Docs explain when to clone vs use a Space. Migration checklist has explicit suffix naming convention.
200+
201+
### Phase 4 — Validation + edge cases
202+
203+
**Effort:** ~0.5 day
204+
205+
- [ ] Integration test: two clones in one Space, distinct prefixes, both connect with different env
206+
- [ ] Uninstall clone does not affect source
207+
- [ ] Uninstall source warns if clones exist (list dependents, offer bulk uninstall)
208+
- [ ] Prefix collision: two different registry servers cannot claim same alias (existing behavior — verify clones don't break it)
209+
- [ ] `pnpm validate` + targeted Rust/TS tests
210+
211+
**Outcome:** Clone lifecycle is safe through install → configure → enable → uninstall. Source/uninstall warnings prevent orphaned expectations.
212+
213+
### Phase 5 — (Optional) First-class instances (Option 3)
214+
215+
**Effort:** ~2–3 weeks — **defer until clone UX proves demand**
216+
217+
- [ ] Schema: replace `UNIQUE(space_id, server_id)` with `UNIQUE(space_id, server_id, instance_label)` on `installed_servers`, `credentials`, `outbound_oauth_clients`, `server_features`
218+
- [ ] `instance_label` default `"default"` for existing rows; migration backfills
219+
- [ ] UI: one registry card with N instance sub-cards instead of flat clone list
220+
- [ ] FeatureSet member type: `ServerInstance { server_id, instance_label }` for grouped grants
221+
- [ ] Data migration: existing clones (`posthog-work`) → `(posthog, instance_label=work)`
222+
- [ ] Log paths, OAuth refresh, event payloads gain instance dimension
223+
224+
**Outcome:** Registry server is the template; instances are first-class. Clone IDs like `posthog-work` become legacy format migrated to structured instances. Only pursue if Phase 1–4 adoption shows ID-suffix sprawl or FeatureSet pain.
225+
226+
---
227+
228+
## Out of scope
229+
230+
| Item | Reason |
231+
| ---- | ------ |
232+
| Auto-sync clone definition when registry updates | Requires shared definition store or periodic refresh job. Defer; document "clone may drift from registry" in UI. Option 3 addresses properly. |
233+
| Credential copy / "duplicate with same secrets" | Security footgun. User always re-enters creds on clone. |
234+
| Runtime account switching on one process | Impossible for stdio env-at-startup servers. Not McpMux's layer to fix. |
235+
| Wrapper MCP shims per backend | Per-server maintenance burden (Option 5 from brainstorm). Rejected. |
236+
| Cross-Space clone | Install separately per Space — already works via Spaces. "Clone to another Space" is a nice follow-up, not v1. |
237+
| Tool-level account selection injection | Would require MCP spec / client header support. Out of scope. |
238+
239+
---
240+
241+
## Key files referenced
242+
243+
| File | Why |
244+
| ---- | --- |
245+
| [`crates/mcpmux-core/src/application/server.rs`](../../crates/mcpmux-core/src/application/server.rs) | `install()` uniqueness check — clone must use a new `server_id`. |
246+
| [`crates/mcpmux-core/src/domain/config.rs`](../../crates/mcpmux-core/src/domain/config.rs) | `normalize_server_id` / `normalize_alias` — suffix rules (no underscores). |
247+
| [`crates/mcpmux-storage/src/migrations/001_initial.sql`](../../crates/mcpmux-storage/src/migrations/001_initial.sql) | Current `UNIQUE(space_id, server_id)` constraints clone works around. |
248+
| [`crates/mcpmux-gateway/src/services/prefix_cache.rs`](../../crates/mcpmux-gateway/src/services/prefix_cache.rs) | Prefix assignment for clone's alias at connect time. |
249+
| [`apps/desktop/src/features/servers/ServersPage.tsx`](../../apps/desktop/src/features/servers/ServersPage.tsx) | Primary UI surface for install/configure/enable flow. |
250+
| [`apps/desktop/src/components/ConfigEditorModal.tsx`](../../apps/desktop/src/components/ConfigEditorModal.tsx) | Reused post-clone credential entry. |
251+
| [`docs/guide/spaces.mdx`](../../docs/guide/spaces.mdx) | Canonical work/personal separation — clones complement, not replace. |
252+
253+
---
254+
255+
## Related documentation
256+
257+
- [`docs/planning/dynamic-mcp-toggle-meta-tools.md`](./dynamic-mcp-toggle-meta-tools.md) — session enable works with clone `server_id`s once user knows the suffixed name; Phase 3 links them.
258+
- [`docs/guide/servers.mdx`](../../docs/guide/servers.mdx) — server management baseline; gets multi-account section in Phase 3.
259+
- [`docs/guide/spaces.mdx`](../../docs/guide/spaces.mdx) — primary pattern for context-level account separation.
260+
- Personal migration tracker: `jsg-tech-check/docs/setup/mcpmux-server-migration.md` — consuming checklist for PostHog, Gmail, Sheets, Firebase clones.
261+
262+
---
263+
264+
## Reconciliation
265+
266+
This doc is the source of truth for server account clones. When implementation starts, update **Status** and **Branch** at the top. Phase 5 remains optional — do not block Phases 1–4 on it.
267+
268+
**Decision record (May 23, 2026):** Option 2 (UI-assisted clone) selected over status quo, first-class instances (deferred Phase 5), per-client credential override (rejected), and wrapper meta-servers (rejected). Brainstorm source: Cursor session on multi-account MCP patterns.

0 commit comments

Comments
 (0)