# Making a multi-connector MCP setup safe to act on

> When an agent holds several tenant connectors at once, the risk isn't data leakage (that's sealed server-side) — it's acting on the wrong tenant by mistake. The fix is layered: make the tenant visible, make it assertable, and gate the irreversible actions. Visibility alone is not enough.

URL: https://biloh.com.au/docs/engineering-notes/multi-tenant-mcp-safety
Category: Engineering notes | Audience: builder | Updated: 2026-06-25

An AI agent can hold **several Biloh connectors at once** — one per tenant, plus a platform-admin connector — and they look almost identical in the tool list. The danger is not that one connector reads another's data (that is sealed by a tenant-bound token, row-level security, and write gateways, independent of the agent). The danger is **intent**: calling the right tool on the *wrong* connector.

Biloh solves this in three layers, cheapest first: **make the tenant visible, make it assertable, and gate the actions you can't undo.** Visibility alone — the most common instinct — is not enough.

## Layer 1 — make the tenant visible

Two signals, on by default:

- **Connector identity.** Each connection advertises itself as `biloh · <Tenant> (<MODE>)`, where mode is `LIVE` (a real tenant), `TEST` (a tenant flagged as test data), or `PLATFORM` (the cross-tenant admin surface). Two real tenants are *both* `LIVE` — so the **name** distinguishes them and the **mode** separates a tenant from the platform.
- **A per-response stamp.** Every tool result carries `meta.tenant { id, name, mode }`. Because it is on *every* call, the last result an agent received always tells it which tenant it just touched.

A subtle implementation note: the MCP handler is built **once per process**, so the server's identifying info is otherwise static across connections. Per-connection identity is therefore applied by rewriting the protocol `initialize` *response* — fail-safe, so any anomaly returns the original handshake untouched rather than risking the connection.

## Layer 2 — make the tenant assertable (a misroute should *fail*)

Visibility helps a careful reader. It does nothing for a mistake already in flight. So a write can carry an optional `expected_tenant`: the tenant you *intend* to act on, by name or id.

If it doesn't match the connector you're on, the call returns `expected_tenant_mismatch` and the handler **never runs**. A misroute stops instead of silently going through. The argument rides the same central schema-augmentation as cross-tenant routing and is enforced once, at the single wrapper every tool call passes through — so it covers the whole tool surface without per-tool code.

> The principle: **data isolation and intent safety are different problems.** One is enforced by the database; the other has to be enforced by affordances the caller opts into.

## Layer 3 — gate what you can't undo

Two-phase confirm-gates protect the irreversible actions. The first call returns a **preview** plus a deterministic `confirm_token` and mutates nothing; only a second call carrying the matching token executes.

The scoping rule matters more than the mechanism: **gate by operation, not by keyword.** Archiving a tenant is irreversible — gated. Creating a tenant is gated too (it completes the lifecycle pair). Minting an access token is *revocable* — so it is deliberately **left ungated**. "Sounds dangerous" is not the test; "can't be undone" is.

A gate is only safe to add after an **audit** confirms nothing automated calls the tool — otherwise you break provisioning. In this build the audit showed zero code-level callers, so the gate had zero blast radius.

## What each layer is locked by

Every layer ships with a spec test written *before* the code, so the guarantee can't quietly regress:

- The mismatch guard has a test asserting a wrong `expected_tenant` returns the error code and the handler does not run.
- Each confirm-gate has a test asserting the no-token call previews and mutates nothing, a wrong token errors, and the matching token executes.
- The identity rewrite is fail-safe by construction and tested against a malformed handshake.

## Next steps

- The connection basics: [Connecting Biloh over MCP](/docs/reference/mcp-overview).
- How an agent finds the right tool in the first place: [Tool discoverability for agents](/docs/engineering-notes/tool-discoverability-for-agents).
