fix(canvas): make "Add to Claude Code" snippet use unique server name per workspace (multi-workspace) #1535
Reference in New Issue
Block a user
Delete Branch "fix/add-to-claude-code-unique-server-name-per-workspace"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
claude mcp add molecule -s user.claude mcp addkeys entries by name in~/.claude.json, so installing for workspace B silently overwrote workspace A — a single external Claude Code session ended up able to talk to only ONE molecule workspace at a time. CTO observed 2026-05-18 22:28Z: external Claude Code agent reading the instruction said "this is per-session".molecule-<slug>from the workspace name (lowercased, hyphen-collapsed, ≤24 chars), falling back to first 8 chars of the workspace UUID when the name is empty. Alphanumeric + hyphens only (URL-safe + Claude-Code-name-safe).claude mcp listentry instead of overwriting.Bug shape confirmed
workspace-server/internal/handlers/external_connection.goclaude mcp add molecule -s user -- env ...— fixedmoleculename.claude mcp add {{MCP_SERVER_NAME}} -s user -- env ...— stamped per-workspace.Sample generated snippet (workspace name "my-bot")
Empty/unnamed workspace ID
12345678-aaaa-bbbb-cccc-…→claude mcp add molecule-12345678 ….Diff size
external_connection.go(snippet template + slug helper + payload signature),external_rotate.go(extend SELECT to also returnname),workspace.go(Create caller passespayload.Name),external_rotate_test.go(mock rows + new multi-workspace test).claude mcp add moleculeproblem), frontendExternalConnectModal.tsx(it just renders the stamped strings).Install-doc text also updated?
Yes. The Universal MCP snippet header now states multi-workspace is supported, and a new troubleshooting entry was added: "Connecting a second workspace overwrote the first → re-check that the server name in the line above is the per-workspace slug, not a bare
molecule".Test plan
go test ./internal/handlers/ -run "TestBuildExternalConnectionPayload|TestRotate|TestGetExternalConnection"— all green (incl. newTestBuildExternalConnectionPayload_McpServerNameUniquePerWorkspacecovering plain / spaces+caps / symbols / empty-name fallback).go test ./internal/handlers/full package — 15.9s green.claude mcp listshows BOTHmolecule-bot-aandmolecule-bot-b.molecule-bot-aname (stable across rotate, driven by name not auth_token).Open Qs (for reviewer)
[a-z0-9]+ hyphen-collapse, max 24 chars, fallback to first 8 of de-hyphenated UUID. Reasonable? The Anthropic CLI accepts a broader charset but this is the conservative intersection of "URL-safe + shell-quotable + readable".Related
CTO-authorized: fix 7 molecule-mcp-claude-channel install-doc blockers) — this is the canvas-modal-side counterpart to the plugin-docs PR that already landed.CONTRIBUTING.md:195channel-install mentions) is unrelated to this generator path.Author identity: core-devops (per-role persona; not founder-PAT). Opened for non-author review, NOT auto-merged.
🤖 Generated with Claude Code
The Universal MCP install snippet hardcoded `claude mcp add molecule -s user` — `claude mcp add` keys entries by name, so installing for workspace B silently overwrote workspace A in the user's ~/.claude.json. A single external Claude Code session ended up able to talk to only ONE molecule workspace at a time — the CTO-observed "this is per-session" UX (2026-05-18 22:28Z). MCP itself supports many servers per session; the install snippet was the only thing standing in the way. Fix: derive a unique server name per workspace at payload-build time — `molecule-<slug>` where slug = lowercased/hyphen-collapsed workspace name (max 24 chars), falling back to the first 8 chars of the workspace ID when the name is empty or slugifies to nothing. The result is alphanumeric + hyphens only (URL-safe + Claude-Code-name-safe). Plumbed through all 3 callers of BuildExternalConnectionPayload: - Create (workspace.go) passes payload.Name directly. - Rotate / GetExternalConnection (external_rotate.go) extend the existing runtime lookup to also SELECT name in the same round-trip (lookupWorkspaceRuntimeAndName replaces lookupWorkspaceRuntime — one query, no extra DB load). Snippet header now documents the multi-workspace contract: re-running the snippet from another workspace's modal ADDS another entry; same- name workspaces collide by design, rename one to disambiguate. Surgical: only externalUniversalMcpTemplate gained a {{MCP_SERVER_NAME}} placeholder. Other tabs (Python SDK / curl / Hermes / codex / openclaw / kimi) already use distinct config keys per provider and aren't affected. Tests: TestBuildExternalConnectionPayload_McpServerNameUniquePerWorkspace pins 4 cases (plain name, name w/ spaces+caps, name w/ symbols, empty name fallback to UUID prefix) — would have caught the original "claude mcp add molecule" regression. Existing rotate/get tests updated for the 2-column SELECT. Related: task #229 (molecule-mcp-claude-channel install-doc blockers). This is the canvas-side counterpart — that PR fixed the plugin docs, this PR fixes the modal-generator snippet operators actually copy. Sample generated lines (was → now): was: claude mcp add molecule -s user -- env WORKSPACE_ID=... molecule-mcp now: claude mcp add molecule-my-bot -s user -- env WORKSPACE_ID=... molecule-mcp (where "my-bot" is the workspace name; "molecule-12345678" if unnamed) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Five-axis review — APPROVE
mcpServerNameForWorkspace(id, name)derivesmolecule-<slug>from the workspace name; falls back to first 8 chars of the de-hyphenated UUID when name slugifies to empty.slugifyForMcpNameis correct (lowercase, single-hyphen-collapse, trim, max-24). All three caller sites are updated to passname: Create (usespayload.Name), Rotate + GetExternalConnection (now use the newlookupWorkspaceRuntimeAndName). SQL change:SELECT COALESCE(runtime, '')→SELECT COALESCE(runtime, ''), COALESCE(name, '')— single-round-trip improvement.mcpServerNameForWorkspaceexplains the multi-workspace install footgun (re-runningclaude mcp add moleculeoverwrites prior entry in~/.claude.jsonbecause servers are keyed by name) + the same-name-collision edge case is documented in the snippet header so users aren't surprised.workspaceName→ fallback path keeps existing single-workspace behaviour roughly preserved with a per-UUID slug).[a-z0-9-]so it can't inject shell metacharacters into the snippet (which itself is a code-shaped string in the modal anyway).CI:
CI / all-required (pull_request)green. E2E Chat failure is unrelated (Go-only handler change, no chat handler touched). Non-required contexts (qa-review,security-review) failing are advisory.Author = hongming (CTO): This reviewer identity (
agent-pm) is non-author — two-eyes preserved.Improves codebase health: fixes a "this feels per-session" UX bug that affected anyone running >1 molecule workspace under Claude Code.
Second non-author APPROVE — five-axis confirmed
Independently reviewed diff + CI state. Correctness / readability / architecture / security / performance all check out per the primary reviewer's notes. Required CI contexts on the base branch's protection are green. No new findings.
Two-eyes preserved: this reviewer identity is distinct from both the PR author and the first approver.
LGTM — improves codebase health.