Record Scope
Record scope is the per-user data behind enum injection: the list of resource IDs a particular person may reach through a particular role. Enum injection is the mechanism; record scope is what feeds it.
Enum injection is defence in depth, not your authorisation boundary. Your API must still enforce ownership on every request — record scope stops the AI constructing an out-of-scope call, it does not stop a determined caller making one.
Where it comes from
| Identity model | Source | Set up by |
|---|---|---|
| Tenant Delegated | enum_scopes in your exchange, validate and refresh responses | your application, automatically |
| Token Exchange | x-mcp-bootstrap claim on the access token | your IdP, your backend, or an admin |
| Identity Brokering | x-mcp-bootstrap claim on the access token | your backend, or an admin |
Tenant Delegated needs nothing from this page. Your app already knows which records the user owns — adding an enum_scopes key to the JSON its exchange and validate endpoints return is the whole integration. The hub stores it at login (exchange) and replaces it whenever validate or refresh returns it. See the contract spec.
The rest of this page is for the two Keycloak-backed models, where the payload has to reach a token claim.
The Keycloak side (the attribute and the token mappers) is set up by the hub when a connection is provisioned. There is nothing to configure in Keycloak yourself.
Option 1 — your IdP emits the claim (Token Exchange)
The best answer where your identity provider supports it, because nobody maintains anything: the scope is computed per user at every login and can never drift from your database. It needs a federated IdP, so it applies to Token Exchange connections only.
Your IdP issues a token carrying:
{
"sub": "user-id",
"x-mcp-bootstrap": {
"accounts": [
{ "roles": [{ "role": "MANAGER", "saleIds": [5, 12, 47] }] }
]
}
}Most providers can do this with a hook that calls your API at token issuance — Okta inline hooks, Auth0 Actions, Entra claims mapping policies, or a Keycloak SPI. Dex cannot: its claim set is fixed, so use option 2 or 3 there.
For a SAML IdP, send it as an attribute named x-mcp-bootstrap whose value is that JSON as a string.
Then turn it on for the connection: Connections → the connection → Onboarding → Identity setup → "My identity provider sends each user's record scope", save, and provision. Provisioning adds the mapper that copies the claim into the hub at each sign-in; turning the option off and provisioning again removes it.
Your IdP then owns the value for its users. It is copied at every sign-in, so a change reaches the user at their next login — and anything set for them through option 2 or 3 is replaced at that login too. If your IdP sends no value for a user, their scope is cleared at that login, and tools that use record scope then refuse their calls until it is set again. The Members editor and the API both say so for these users (managedByIdp).
Option 2 — your backend pushes it
For providers that cannot emit custom claims, your application can tell LockMCP directly whenever a user's scope changes.
PUT /api/v1/record-scope
Authorization: Bearer lmcp_live_...
Content-Type: application/json
{
"email": "agent@yourcompany.com",
"scope": {
"accounts": [
{ "roles": [{ "role": "MANAGER", "saleIds": [5, 12, 47] }] }
]
}
}Identify the user by email or by userId — exactly one. Both are resolved within your workspace only: an API key can never write scope for another tenant's user, whatever it sends.
"scope": null clears the scope. Clearing does not unrestrict the user: wherever one of their roles carries an inject key, a missing scope works like an empty one, and those tools refuse every call until scope is set again. To lift a restriction, remove the inject key from the role's tool access instead.
Responses
| Status | Meaning | Retry? |
|---|---|---|
200 | Written. changed: false means it already matched. | — |
400 | Malformed payload, or over the 8 KB limit (measured as the scope sits in the access token, where each " is escaped, so text IDs cost a little more than numbers) | no |
401 / 403 | Key invalid, revoked, expired, or lacks members.manage | no |
404 | No such user in your workspace | no |
409 | User has no identity-provider account yet | after their first sign-in |
503 | Identity provider unavailable | yes, honour Retry-After |
A 200 may include unknownRoles. That means the payload named a role your workspace has no role for — most often a typo. It is not an error, but that entry will never apply to anyone.
A 200 also carries managedByIdp. When it names a connection, the user signs in through an identity provider that sends their record scope (option 1), so what you just wrote lasts only until their next sign-in.
Getting a key
A workspace admin issues one under Settings → API keys (it needs the Manage API keys permission). The full key is shown once and only its hash is stored, so it cannot be recovered — keep it in your secrets manager. To rotate, issue a second key, swap your configuration over, then revoke the first.
The key belongs to whoever issued it and can never do more than they can: it needs its owner to hold Manage members, and it stops working if the owner is deactivated. The list shows each key's owner and flags a key whose owner can no longer back it. Your LockMCP operator can also issue a key for you.
Call the API on the host you sign in to the dashboard with (for example https://www.lockmcp.ai). Another host that only redirects to it answers with an empty redirect, which most HTTP clients do not follow for a PUT.
Option 3 — an admin sets it in the dashboard
Members → select a member → Record scope.
Add a row per role and inject key, with the IDs that role may reach. Useful for small or slow-moving sets, for testing, and for providers that cannot emit claims and have no backend to integrate.
It is a fixed list maintained by a person, so it goes stale as records are created. Prefer options 1 or 2 wherever you can.
The section is unavailable for members who have never signed in — there is no identity-provider account to attach scope to until then.
Three things worth knowing
Changes are not instant, and this is not a kill switch. Scope reaches a running AI client at its next access-token refresh, usually within five minutes. A session already in flight keeps the scope it started with. To cut someone off immediately, revoke their session from the Sessions page instead.
Scope only applies to roles the member actually holds. Setting scope for MANAGER on someone who has no MANAGER role stores the payload and returns success, but nothing will ever read it. The role names must also match your workspace's role names exactly — the join is case-sensitive.
Renaming a role breaks stored scope silently. The payload refers to roles by name. Rename one in the dashboard and every stored scope for it stops matching, with no error anywhere. Re-set scope for affected members after a rename.
Related
- Enum Injection — the mechanism, and marking parameters with
x-mcp-inject - Tool Studio — configuring inject keys per tool and role
- Contract Spec —
enum_scopesfor Tenant Delegated