Skip to main content

Tenant isolation

Every registry tool call over MCP passes through a facility-scope boundary guard before it reaches the tool. The guard either rewrites your arguments to your accessible facilities or rejects the whole call. It is fail-closed: an argument the guard cannot prove is in scope is never passed through. This is the layer that makes facilityId behave differently over MCP than it does for the in-app assistant, and the difference surprises people. Read the four behaviours before you hardcode a facility id.

What it applies to

On the registry path the guard runs after your arguments have been validated against the tool’s inputSchema and before the org guardrails and the idempotency reservation. A call rejected on the boundary therefore never evaluates a guardrail and never consumes an Idempotency-Key.

Your accessible set

The guard resolves an accessible facility set from the credential’s bound user — the user the API key or OAuth token belongs to — using their role, org unit, and facility: An empty set is not a pass-through. Every guarded call fails with:
If you see that, the credential’s bound user has no facility and no org unit to resolve — an operator has to fix the user’s assignment; nothing in the request can work around it.
API keys carry their own facility binding, which operators use to key the per-facility autonomy and tier-equivalence dials. The accessible facility set on this page comes from the bound user, so a key without a bound user cannot call a guarded tool at all — governed tools reject org-only credentials with “This tool requires an authenticated user identity”.

The four behaviours

(a) You omit facilityId → NexSpace injects your accessible facilities

Omitting the argument can never mean “all tenants”. The guard injects your accessible set in whatever shape the tool’s facilityId field accepts: the array when the field takes one, or the single id when the field takes a number and you have exactly one accessible facility.
A tool whose input schema has no facilityId field at all is treated as tenant-agnostic (a catalog or metadata listing) and its arguments pass through unchanged.

(b) You pass a facility you can access → honored

Your value is used as-is. No narrowing, no injection.

(c) You pass any id outside your scope → the whole call is rejected

Arrays are not silently narrowed. If you pass [3, 4, 99] and 99 is outside your scope, the entire call is rejected — you do not get results for 3 and 4. This differs from the in-app assistant, whose tool adapter clamps the request down to the in-scope ids and continues. The headless boundary never silently narrows, because a partially-narrowed result is indistinguishable from a complete one to a caller who cannot see the clamp.
The message lists the offending ids (as a JSON array) and your full accessible set in parentheses — enough to correct the call without another round-trip. A non-numeric value that slipped past schema validation is treated as out of scope by definition and appears in the same list.

(d) You can access several facilities but the tool takes a single facilityId → hard error

There is no way to represent “facilities 3 and 4” in a number field, and the guard will not pick one for you or widen the query:
Pick one of the listed ids and retry — or fan out one call per facility.

Recognizing a boundary rejection

All four rejections come back as -32003 (FORBIDDEN) with retryable: false, and all of them carry this exact suggestion:
That string is the reliable discriminator between a tenant-boundary rejection and the other -32003 causes — a scope gap carries required / granted, a guardrail carries guardrail, and a tier floor carries requiredTier / tierEquivalence. Boundary rejections carry none of those.

Practical guidance

  1. Prefer omitting facilityId. Injection (behaviour a) is the safe default, works for single- and multi-facility credentials, and needs no knowledge of ids.
  2. Discover ids before you use them. The search_facilities registry tool (facility:read) and the listFacilities verb (facilities:read) return facilities you can reach; a hardcoded id from another environment is behaviour (c) waiting to happen.
  3. Do not retry. retryable: false is accurate for all four — the call cannot succeed until the arguments or the credential change.
  4. Expect (d) on single-facility tools if your bound user spans an org. Either bind the credential’s user to one facility or always pass an explicit id.

See also

  • MCP — the full -32003 decision tree.
  • Governance — the guardrail layer that runs immediately after this guard.
  • Tool catalog — which tools take a facilityId.