MCP
Auth & scoping
Scoped API keys, OAuth for MCP clients, tool allowlists and how secrets stay out of context.
In the cloud, MCP and agent access is authenticated with API keys. A key can be scoped to one group and an allowlist of tools, so the key you hand an agent reaches only what it needs, and your workspace key never leaves your server. MCP clients you use yourself can instead sign in with OAuth and get the same one-group scope.
On your own machine there is no auth: primitives dev serves http://localhost:7777/mcp/<group> with no token, as it serves REST. See Local access without a token.
Credentials
| Credential | Prefix | Scope | Where it lives |
|---|---|---|---|
| Workspace API key (cloud) | pk_ | Every group and every tool: create, list and destroy groups, create, delete, rename, move, pause and resume primitives, read the audit trail, store secrets | Your server only. Created in the dashboard at app.useprimitives.com |
| Scoped API key (cloud) | pk_ | One group or every group, and optionally a list of tools | Given to the agent or MCP client. Created in the dashboard |
| Root token (local, optional) | any | Account-level calls, for primitives dev --token <secret>. Without it, localhost needs no auth | Your machine |
| OAuth access token (cloud) | po_ | One group and the tools picked on the consent page, /mcp only | Held by the MCP client that signed in; refreshed with a pr_ refresh token |
Each is sent as Authorization: Bearer <credential>.
Scoped API keys
An API key is scoped when you create it under API keys in the dashboard:
- Group: one named group, or every group in the workspace (including ones created later).
- Tools: every tool, or a list of tools and globs such as
db_queryorfs_*.primitive_listis always available, so you don't need to list it.
A key with every group and every tool is a workspace key. Use it for the code that provisions groups. Anything narrower is a scoped key: give each agent its own, limited to its group and the tools it needs. To cut an agent off, revoke its key in the dashboard.
Picking the group on /mcp
https://mcp.useprimitives.com/mcp is the same URL for every group, so the key decides which group a session reaches:
- A key scoped to one group always reaches that group.
- A key for every group names one in the
X-Primitives-Groupheader. Without it,/mcpanswers400 invalid_request. - A header naming a group outside the key's scope answers
403 group_mismatch.
claude mcp add --transport http primitives https://mcp.useprimitives.com/mcp \
--header "Authorization: Bearer $PRIMITIVES_API_KEY" \
--header "X-Primitives-Group: agent-42"Over REST the path already names the group (/v1/groups/{group}/…) or the primitive (/v1/primitives/{id}), so no header is needed.
OAuth for MCP clients
In the cloud, https://mcp.useprimitives.com/mcp supports the MCP authorization spec, so a client can connect with only the URL. See Cloud, sign in with OAuth for client setup.
- A request without a token answers
401withWWW-Authenticate: Bearer resource_metadata="https://mcp.useprimitives.com/.well-known/oauth-protected-resource/mcp". That document names the authorization server,https://app.useprimitives.com/api/auth. - The client registers itself (dynamic client registration) and sends you to sign in. Public clients only, and PKCE with
S256is required. - On the consent page you pick one workspace, one group and the tools. The page shows the app's name and where it sends you back; the name is chosen by the app, so check the address.
- The client gets an access token (
po_, valid for an hour, bound to the MCP URL) and a refresh token (pr_, 30 days, replaced on every use). The client stores and refreshes them; you never handle them.
An OAuth token works like a group-scoped API key on /mcp: the same allowlist, scoping rules and plan limits apply, and X-Primitives-Group can't name another group. It doesn't work on the REST API.
Access stops at once when you click Disconnect under API keys → Connected apps, or when you leave the workspace. Any member of the workspace can see and disconnect its apps. To change the group or tools, disconnect and connect again.
Scoping rules
These apply to scoped API keys and OAuth tokens alike:
- One group. A key scoped to one group reaches only that group and the primitives in it. A primitive in another group (
/v1/primitives/{id}) answers404 not_found, so its existence isn't leaked;GET /v1/primitiveslists only the key's group; another group's/v1/groups/{group}paths fail with403 group_mismatch. - Allowlisted tools only. Tools outside the allowlist are left out of
tools/listand rejected withtool_not_allowed. The one exception isprimitive_list: every key can always list its group's primitives, over MCP and REST, so agents can discover what to pass asprimitive. Over REST the same allowlist gates each endpoint: for example,fs_readcovers reading files,fs_writecovers writes, moves and deletes, and a key withoutdb_execmay only run statements that don't write. - One tool at a time. Each MCP call is narrowed to the tool being called, so
db_querystays read-only even when the key also allowsdb_exec. - Every primitive in the group. A key reaches every database, filesystem, queue, memory, vault, key-value store and object store in its group, including ones created later. Over MCP the agent sees them with
primitive_listand addresses one by id or name with theprimitiveargument. Withprimitive_createon its allowlist it can add primitives to its group; withprimitive_snapshotandprimitive_forkit can snapshot, restore and fork them within its own group. - No escalation. Scoped keys can't create, list or destroy groups, delete, rename, move, pause or resume primitives, read the audit trail, or store, delete or grant secrets: those return
403 forbiddenand need a workspace key. A scoped key can't fork into another group; your server forks across groups with its workspace key if it wants to.
Unknown, malformed and revoked keys all fail the same way, with 401 invalid_token.
Local access without a token
primitives dev started without --token needs no auth on localhost, for REST and MCP alike:
/mcp/<group>takes no token. The group comes from the path (a valid group name, or400 invalid_request) and is created on first use. Every tool is allowed;?tools=kv_*,storage_*narrows them with the same patterns asprimitives mcp --tools. Calls go through the same checks as a scoped key's, narrowed to one tool at a time and limited to that group, and the audit trail records their actor aslocal.- Stdio too.
primitives mcp --group <name>serves one group over stdio with the same checks. See Stdio clients. - Localhost only. Requests addressed to another host (DNS rebinding) and cross-site requests from web pages get
403 forbidden. An MCP session belongs to the group path that started it; its id answers404anywhere else. - Root token. With
primitives dev --token <secret>, send the same value as the Bearer token; requests without it answer401 invalid_token.
?tools= keeps a local agent to the tools you list, but on localhost anything that can reach the port can open any group. Use scoped API keys in the cloud when the agent isn't trusted.
How secrets stay out of context
The vault is the one primitive designed so the model never sees the data:
- Your server stores the value in one of the group's vaults (
group.vault('prod').set) and grants it a host and method policy (.grant). - The agent calls
vault_listand sees names, versions and grants, such asGITHUB_TOKEN. - The agent calls
vault_usewith"auth": { "bearer": "GITHUB_TOKEN" }. The engine checks the grant, injects the value into the outgoing request, and returns the response with the value redacted.
The value never appears in tool arguments, tool results or logs, so it can't leak through the model's context, a prompt injection or a transcript. See Vault.
Signed URLs are capabilities
storage_sign (and presign over REST) turns the key's access into a URL that needs no credential at all. Anyone holding the URL can download, or upload to, that one object until it expires, even after the key that signed it is revoked. Grant storage_sign only to agents you'd let share or accept files on your behalf, keep expires_in short, and call revokeSignedUrls() on the store to invalidate every URL it has issued. Forking a store or moving it to another group also invalidates its URLs. See Signed URLs.
Recommendations
- Give each agent its own scoped API key, limited to its group and the tools it needs. Never hand an agent a workspace key.
- Start with the read-only tools and add write tools as the agent needs them.
- Keep keys in environment variables, not in checked-in config files. Reference them as
${env:PRIMITIVES_API_KEY}(Cursor) or$PRIMITIVES_API_KEY(Claude Code). - Name keys after the agent. The audit trail records each call's actor as
key:<id>. - Revoke a key in the dashboard as soon as its agent no longer needs it, or if you believe it's exposed.
- For apps you use yourself, such as Claude or Cursor, prefer OAuth over pasting a key: nothing long-lived ends up in a config file, and you can disconnect the app from the dashboard.