AI & agents

A key that can't see the whole workspace

An MCP key you hand an agent isn't all-or-nothing. It carries one of three access levels, capped by what the workspace allows, and can be narrowed to named views with a role on each — so a key scoped to one table is refused every other table's data, reads included.

SchemaStack team26 Aug 2026Verified working · 26 Aug 2026Docs

Handing an AI agent a key to your database feels like handing it the keys to everything. It doesn't have to. An MCP key in SchemaStack is a narrow credential by construction: it belongs to one workspace, carries an access level, and can be pinned to the specific views you want the agent to touch — with a different role on each.

Key asks: FULLper-key access modeCeiling: READ_ONLYworkspace_mcp_configEffective: READ_ONLYthe lower of the twothen scopeview: Orders → VIEWERin scopeview: Salariesnot in scope → 404 / deniedNo scope rows means every view at the effective mode. One scope row means those views only —the key is refused any view it was not granted, reads included. Keys are hashed at rest; revoking one bites on the next request.
An MCP key's reach is the lower of what it asks for and what the workspace allows, narrowed again by the views it is scoped to. A FULL key under a READ_ONLY ceiling reads and no more; a key scoped to one view cannot see the others, even by UUID.

Three levels, capped by the workspace

A key is issued at one of three levels. Read-only can list your schema and query rows, and change nothing. Data access can read the schema and create, update and delete rows, but cannot alter the schema itself. Full can do both — change structure and change data.

Above every key sits a workspace ceiling, set by an admin. The key's effective power is the lower of the two: a full-access key under a read-only ceiling reads and no more. And the ceiling defaults to closed — a workspace with no MCP configuration rejects every key, rather than quietly allowing one. Turning MCP on is a decision someone makes, not a default you inherit.

Scoped to the views you name

That's the coarse dial. The fine one is view scoping. A key can be restricted to a list of views, each with its own role — viewer, editor, or admin. Leave the scope empty and the key reaches every view at its access level; name three views and those three are all it can see.

The enforcement is the part that matters, so it's worth being precise: a scoped key asked for a view it wasn't granted is refused — including reads. Knowing the view's identifier doesn't help; the answer is the same denial. A key scoped to your Orders view cannot query, export, or write your Salaries view, and cannot list it to discover that it exists. The scope is a wall, not a hint.

The key itself

A key is shown once, at creation, and never again — what SchemaStack keeps is a SHA-256 hash, so the plaintext isn't recoverable from our side. Each key carries an optional expiry, and revoking one bites on the very next request: revoked keys are filtered out at lookup, and nothing about a key is cached between calls, so there's no window where a cancelled key still works.

Revocation and rotation are the moves you'll actually use — create a new key, point the agent at it, revoke the old one — and none of them require the agent to be offline.

What it doesn't do (yet)

  • The tool list isn't filtered by the key. An agent connecting with a read-only, single-view key is still shown the full set of tools; it's refused the ones outside its reach when it calls them. The wall is on use, not on the menu.
  • A view scope is whole-view, not per-row. Granting access to a view grants it for every row. There's no "only this agent's rows" at the key level — for that, point the workspace at your own identity provider and let row-level security narrow each query by the caller's claims.
  • The trail records the call, not the contents. Every tool call is now recorded — which credential, which tool, which view it named, and whether it was allowed — and refusals are recorded alongside successes, because a key repeatedly bouncing off a view it has no scope for is how a mis-issued key announces itself. What it deliberately does not keep is the arguments: a single create_record carries a whole row, and storing it would turn an audit table into a second copy of your workspace living in our database rather than yours. So the trail can tell you a key wrote to Orders at 14:02; it cannot tell you what it wrote.
  • There's no UI for the trail yet. It's readable through the API (GET /api/workspaces/{uuid}/mcp-keys/activity, workspace-admin only) and that's all so far — no screen in the app, no export, no alerting when refusals spike, and no retention policy, so it grows until someone prunes it.
  • An mcp_ key still names no person. Its writes are attributed to the key, not to a human. An OAuth call records the client and the person who consented; a key records only itself.
  • The monthly request limit is per organisation, not per key, and is a fixed number rather than something you tune per agent.
  • One key, one workspace. There's no organisation-wide key that spans workspaces; a key is minted against a single workspace and stays there.

The AI integration guide lists the full tool set and the setup, and revoking an agent's access, instantly covers the other credential — the OAuth token an agent gets after a person consents — and how cutting it off works the same way.

Verified 26 Aug 2026: 98 tests, all green 2026-08-26 — McpApiKeyResourceSecurityTest (43 cases covering create/list/update/revoke, per-key access mode capped by the workspace ceiling, view-scope creation, and cross-organisation isolation), McpUnifiedPermissionTest, McpAccessControlTest and McpEndpointAuthIntegrationTest, all in metadata-test. Plus a new 15-case McpDataToolPermissionTest driving the live /mcp endpoint with real read-only and view-scoped keys, asserting the data and bulk tools refuse a reduced or out-of-scope key and still admit an authorized one. The audit trail added the same day is covered by McpAuditTrailTest (6 cases through the live endpoint, including that a refusal is recorded as DENIED and that a create_record's payload never reaches the trail) and McpToolsAreAuditedTest, which fails if any of the 57 tools would run without leaving a record..