How MCP clients discover which room to act in and which token to authenticate with — and what users and agents can do beyond the default.
sync is multi-room. A single user might have tokens for dozens of rooms. But MCP clients (Claude, ChatGPT, Claude Code) send tool calls with no ambient context — each tools/call is a stateless JSON-RPC request. The client doesn't inherently know which room you mean when you say "read the context" or "send a message."
sync-mcp solves this with a three-tier resolution chain: explicit parameters beat vault lookup, which beats the default.
When any tool is called, sync-mcp resolves the room and token in this order:
1. Explicit params → tool call includes { room: "...", token: "..." }
2. Vault lookup → OAuth user has a token stored for the named room
3. Default room → OAuth user has a vault entry marked is_default=true
Each tier shadows the ones below it. If you pass room and token explicitly, the vault is never consulted. If you pass just room (no token), the vault is searched for the best token for that room. If you pass neither, the default room is used.
Every tool accepts optional room and token parameters. When both are provided, they're used directly — no vault, no OAuth, no default. This is the escape hatch that makes sync-mcp backward-compatible with unauthenticated usage: anyone with a room token can use any tool without signing in.
When this matters:
- Scripted usage where you have tokens in env vars
- Accessing a room you haven't vaulted
- Overriding the default for a one-off operation
- Non-OAuth clients that can't do the passkey flow
If you pass room but not token, sync-mcp searches your vault for tokens belonging to that room. It picks the highest-authority token available:
room token > agent token > view token
This means you can say sync_read_context({ room: "workspace" }) without remembering or pasting a token. The vault resolves it.
When this matters:
- Switching between rooms in conversation: "now read the workspace room"
- Agents that know room names but shouldn't handle raw tokens
- Human users who want to name rooms, not paste token strings
If you pass neither room nor token, sync-mcp falls back to whatever vault entry is marked is_default=true. This is the zero-parameter path — you just call sync_read_context() and get your default room.
Only one vault entry can be the default at a time. Setting a new default clears the old one. The default is set in three places:
- Room creation —
sync_create_room({ set_default: true })auto-vaults and marks as default - Management UI — the ☆/★ toggle on room tokens at
/manage - OAuth consent — the room picker dropdown during the OAuth flow
When an MCP client triggers the OAuth flow (by hitting the 401 on POST /mcp), the user is taken to the authorization page. After passkey authentication, the consent screen now shows:
┌─────────────────────────────────────┐
│ Grant "Claude" access? │
│ │
│ Scope: sync:rooms │
│ • Read room context and state │
│ • Invoke actions and send messages │
│ • Manage your token vault │
│ │
│ Active room for this client: │
│ ┌─────────────────────────────┐ │
│ │ ▼ workspace (current default)│ │
│ │ oauth-live-test │ │
│ │ All rooms (vault default) │ │
│ └─────────────────────────────┘ │
│ │
│ Sets your default room. The client │
│ will use this room when no room is │
│ specified. │
│ │
│ [Deny] [Allow] │
└─────────────────────────────────────┘
Selecting a room sets it as the vault default before issuing the auth code. The MCP client then gets a token that, when used with parameterless tool calls, resolves to that room.
What "All rooms (vault default)" means: don't change the default. Whatever was previously set stays. The client can still access any room by name — the picker only controls which room is used when no room is specified.
What this does NOT do: it does not restrict the client to that room. The OAuth token grants access to the full vault. The client can always pass room: "other-room" explicitly or the LLM can be prompted to use a different room. The picker is purely about convenience — which room the client lands in by default.
Everything in the vault. The OAuth access token maps to a user identity, and that identity owns the entire vault. The default room is a UX convenience, not a security boundary. An authenticated client can:
- Call
sync_read_context({ room: "any-room-in-vault" })— vault resolves the token - Call
sync_vault_list()to see all rooms and tokens - Call
sync_create_room()to make new rooms (which auto-vault) - Call any tool with explicit
room+tokenfor rooms not even in the vault
The only thing the default controls is what happens when the client (or the LLM) calls a tool with no parameters.
The vault stores three types of tokens, each with different authority:
| Type | Prefix | Authority | Typical use |
|---|---|---|---|
| Room | room_ | Full admin — read/write state, register actions, manage agents | Room owner, admin tools |
| Agent | as_ | Agent-scoped — read context, invoke actions, send messages as that agent identity | Participating as a named agent |
| View | view_ | Read-only — read context, state, messages | Observers, dashboards, monitoring |
When vault-resolving, sync-mcp picks the highest authority available: room > agent > view. This means if you have both a room token and an agent token for the same room, parameterless calls use the room token (full admin). If you want to act as your agent specifically, pass the room name — or better, the vault could be extended to let you pick preferred authority level.
Current limitation: there's no way to say "default to my agent token, not my room token." The vault always escalates to maximum authority. This is fine for single-user scenarios but matters for multi-agent setups where you want Claude to act as agent:claude not as the room admin.
The zero-parameter path is the primary UX. Humans don't want to paste tokens or remember room IDs. They want to say:
"What's the current state?"
And have it just work. The default room makes this possible. When they want to switch:
"Read the context of the workspace room"
The LLM extracts room: "workspace" and the vault resolves the token. No token management needed in conversation.
Room picker at OAuth time is the key ergonomic: it lets the human choose their working context before the conversation even starts. Re-authenticating (disconnect + reconnect the MCP integration) lets them change the default at any time.
Agents benefit from explicit parameters. A well-prompted agent might:
- Call
sync_vault_list()at the start to discover available rooms - Use
sync_read_context({ room: "specific-room" })for targeted reads - Fall back to the default for general operations
For Claude Code with --scope project, the OAuth consent room picker lets you set a per-project default when you authenticate. Different projects can connect to different rooms.
When multiple agents share a room, the from field on messages and actions identifies who did what. Currently, messages sent via the room admin token show from: null because the room token has no agent identity. For proper agent attribution:
- Join the room as an agent:
sync_join_room({ name: "Claude", role: "assistant" }) - The agent token gets auto-vaulted
- But vault resolution picks the room token (higher authority) over the agent token
This is the main ergonomic gap right now. Solutions under consideration:
- Preferred token type per room — let users mark which token type to prefer for vault resolution
- Agent-aware defaulting — if an agent token exists, prefer it over room token for action invocation (but use room token for admin operations like registering actions)
- Scoped OAuth tokens — issue OAuth tokens that are bound to a specific room + token type, rather than granting vault-wide access
The /manage page (passkey-authenticated, no OAuth needed) provides direct control:
- ★ default badge on the current default room token
- ☆ default button on other room tokens to switch the default
- dash ↗ link on every token — opens the sync dashboard with that room + token pre-filled
- copy — copies the raw token to clipboard
- revoke — deletes the vault entry (doesn't invalidate the sync token itself, just removes it from your vault)
The dashboard URL format is https://sync.parc.land/?room={id}#token={token} — the token is in the fragment (never sent to the server in the URL), which is marginally better for security than putting it in a query parameter.
| Tool call | Resolution | Notes |
|---|---|---|
sync_read_context() | Default room, best token | Zero-parameter path |
sync_read_context({ room: "X" }) | Room "X", vault lookup | Vault picks best token for X |
sync_read_context({ room: "X", token: "tok" }) | Room "X", explicit token | Vault bypassed entirely |
sync_send_message({ body: "hi" }) | Default room, best token | from depends on token type |
sync_create_room({ id: "new" }) | N/A (creates new) | Auto-vaults room + view tokens |
sync_vault_list() | N/A (reads vault) | Shows all rooms and tokens |
The design principle: make the common case effortless, keep the explicit case possible. Most interactions should need zero parameters. Power users and automated agents can always override.