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:
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:
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:
sync_create_room({ set_default: true }) auto-vaults and marks as default/manageWhen 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:
sync_read_context({ room: "any-room-in-vault" }) — vault resolves the tokensync_vault_list() to see all rooms and tokenssync_create_room() to make new rooms (which auto-vault)room + token for rooms not even in the vaultThe 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:
sync_vault_list() at the start to discover available roomssync_read_context({ room: "specific-room" }) for targeted readsFor 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:
sync_join_room({ name: "Claude", role: "assistant" })This is the main ergonomic gap right now. Solutions under consideration:
The /manage page (passkey-authenticated, no OAuth needed) provides direct control:
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.