Est.

MCP Tool Schema Design for Least Privilege

Senior Writer · · 11 min read
Cover illustration for “MCP Tool Schema Design for Least Privilege”
MCP Architecture · August 26, 2026 · 11 min read · 2,469 words

I've spent the last few months elbow-deep in MCP server configs, and here's the thing nobody tells you up front: least privilege isn't something the protocol hands you. It's a design choice you make three fields at a time, inside a tool schema that Anthropic built to be permissive by default. Get those three fields wrong on day one, and you're patching a production system later, at a cost that dwarfs whatever time you saved by rushing the schema in the first place.

Anthropic put out MCP in late 2024. Within months, OpenAI, Google DeepMind, and Microsoft had adopted it, along with thousands of smaller dev teams, and the Python and TypeScript SDKs now pull tens of millions of downloads a month. In December 2025, Anthropic donated the protocol to the Agentic AI Foundation under the Linux Foundation, with OpenAI and Block joining as co-founding members. That's a fast climb from one company's spec to a vendor-neutral standard. Worth sitting with, honestly, because who governs the spec changes what kind of guardrails you can expect from it later.

Underneath all that adoption, the protocol itself is pretty plain: JSON-RPC 2.0 messaging, a standard tool description schema, and an OAuth 2.1 layer that got formalized in the November 2025 spec. The current published version is 2025-11-25, and it defaults any schema missing a $schema field to JSON Schema 2020-12. For every tool, the spec asks for three things: a unique name, a functional description, and an inputSchema. That's the entire surface where privilege decisions get made. And the spec is upfront about what it won't do: it doesn't enforce security at the protocol level. That job falls on whoever builds the server. Anything the builder doesn't lock down is wide open by default.

Why ungoverned schemas produce over-privileged agents by default

Diagram: Over-Privileged vs. Least-Privilege: The Incident Rate Gap. Visualizes: A stark magnitude contrast between two deployment postures: organizations running over-privileged AI deployments see a 76% security incident rate; organizations…

Organizations running over-privileged AI deployments see a 76% security incident rate. Organizations that stick to least-privilege access see 17%. I don't think that gap is coincidence; it's structural.

Here's why: agents inherit the full permission scope of whatever application is orchestrating them. Once a session opens, it advertises every tool the server exposes, and nothing gets reviewed after that first handshake. A single session token can let an agent send email, query a database, read files, open support tickets, and push code to GitHub, all without a second look from anyone.

Research on MCP servers in the wild has turned up consistent authentication gaps. In a pass of 500-plus servers, 38% had no authentication whatsoever. Rogue server registration isn't some theoretical edge case sitting in a whitepaper somewhere; it's a live option in a lot of real deployments today.

Endor Labs looked at 2,614 MCP implementations and found the damage sitting in a few predictable spots. 82% used file-system operations open to path traversal. 67% exposed APIs that could be hit with code injection. 34% had command injection holes sitting in them, just waiting. Path traversal and injection happen because nobody defined input constraints in the schema and backed them up in code. The 30-plus CVEs filed in the first two months of 2026 alone show what happens when a permission-rich protocol ships faster than its own governance can keep up.

One more quirk worth naming: consent in MCP happens at the client level, a user approves the connection once, not at the tool-invocation level. Once that session is live, tool access is bounded only by whatever the server chose to advertise. Fixing that means writing privilege constraints into the schema before the server ever ships, not after.

The three schema fields where least-privilege decisions are actually made

Three fields, three separate levers. Each one carries its own share of the design burden, and none of them cover for the others.

Name. Between 1 and 128 characters, ASCII only, no spaces, no special characters, unique within the server. Sounds like paperwork, I know. But naming discipline is what keeps tool identities distinct. Call something do_thing and you've made access control opaque before you've written a line of logic.

Description. This is what the model reads to decide whether it should even call the tool. Write it too broadly, and you're inviting the agent to reach for it in situations it was never built for.

inputSchema. This is where the real mechanical work happens. Type declarations, string, integer, boolean, array, are your first line of defense against a model hallucinating garbage input. But the keywords doing the actual enforcing are minimum and maximum for numbers, pattern for strings, enum for a closed set of values, and oneOf/anyOf for options that shouldn't overlap.

Here's the anti-pattern I see constantly: someone writes "returns 1 through 10 results" in the description field and calls it a day. A strict JSON Schema consumer can't pull numeric bounds out of free text. It'll generate values outside the range you meant, because the constraint never actually existed as far as the machine's concerned. The fix is boring, but it works: write "ISO 8601 date, e.g., 2026-07-05," not just "date." Put maximum: 10 in the schema itself, not in the prose sitting next to it.

This matters because of how validation runs. There are two gates: the client-side gate, where the model provider checks its generated JSON against the schema before sending anything, and the server-side gate, where the server checks the incoming CallToolRequest. A constraint that only lives in description text is invisible to that first gate. As far as the client's concerned, it doesn't exist.

The 2025-06-18 spec added an optional outputSchema. Declare one, and anything in structuredContent has to match it. Output bounding is the half of least-privilege that most teams skip, probably because it reads like a data-shape problem rather than a security control. It's both, and treating it like only one of those things is how holes open up.

Schema drift is its own quiet tax. Skip a strict constraint, or mark a field optional by accident, and every automated consumer downstream, agents, CI pipelines, test harnesses, starts making its own assumptions about what's allowed. Those assumptions produce bad requests and behavior nobody predicted. Fixing a loose constraint after the tool's in production means versioning it, pushing the update to every client, and auditing every session that ran on the old version. Setting the enum correctly on day one is cheaper, and it isn't close.

How tool annotations extend the schema into runtime behavior signals

Annotations arrived in the 2025-03-26 spec revision. Five fields now: title (just a display name, no security weight), readOnlyHint (defaults to false), destructiveHint (defaults to true, covers destructive rather than additive updates), idempotentHint (defaults to false, meaning repeat calls with the same input produce no extra effect), and openWorldHint (flags a tool that talks to something outside the controlled environment).

These aren't cosmetic add-ons. Missing annotations account for 30% of rejections from the Claude Connector Directory, so annotation completeness is a gate you clear, not a box you check when you feel like it. Some major platforms now require readOnlyHint, destructiveHint, and openWorldHint on every tool descriptor submitted for app directories.

There's a hard limit baked into the design, though: annotations are hints, not contracts. The spec says so outright, clients have to treat annotations from untrusted servers as untrusted. A server can set readOnlyHint: true and then quietly delete files, and the protocol has no way to stop it or even flag the lie. MCP chose hints over enforced contracts on purpose, because enforcing contracts cleanly across a pool of untrusted third-party servers isn't something the protocol layer can pull off.

That gap shows up in real client behavior, not just spec language. Claude Code doesn't auto-approve tool calls based on readOnlyHint today, and a 2026 feature request to add that got closed as "not planned." The annotation exists; the trust signal downstream mostly doesn't. Where it does turn into real enforcement is at the proxy layer. MCPProxy, for instance, maps annotations into distinct permission variants, call_tool_read, call_tool_write, call_tool_destructive, closing the gap between a hint and something a system actually acts on.

Annotate everything correctly anyway. Directory compliance depends on it, human reviewers lean on it, and sooner or later some gateway sitting in the chain is going to start enforcing what today is just a suggestion.

Why OAuth scopes alone cannot enforce least privilege across tool chains

The June 2025 spec update made something explicit that a lot of people had been fuzzy on: MCP servers are OAuth Resource Servers, not Authorization Servers. The token gets issued somewhere else; the server's job is just to enforce it. November 2025 pushed further, making OAuth 2.1 mandatory for every remote MCP connection, PKCE mandatory for all clients, and it dropped implicit grant entirely while tightening redirect URI handling.

Scope design at the server level matters more than people give it credit for. The spec recommends servers include a scope parameter in the WWW-Authenticate header, nudging clients toward the narrowest scope that gets the job done. That's least privilege expressed right at the token boundary. But broad scopes get expensive fast when things go sideways. An attacker holding a token with files:*, db:*, admin:* can move laterally across data, chain privileges together, and good luck revoking that without forcing the entire surface to re-consent.

There's a second problem hiding underneath, and it's the one people miss: scope sprawl. Try to give every single MCP tool its own distinct OAuth scope, and you end up with proliferation that bloats every token payload and breaks the moment a tool changes shape. Scopes answer "what resource." They don't answer "when," "why," or "for whom." A low-privilege user chaining together several individually valid tool calls to produce one high-impact effect is completely invisible to a system that only checks scopes.

What actually works: OAuth scopes set the outer wall, and server-side RBAC does the fine-grained work inside it. ListTools becomes an RBAC gate in this setup. A read-only user never even sees the write or delete tools in the list, so the tool is invisible before it's ever callable, which beats hoping the model just doesn't try. Permissions get modeled at the tool level, read versus write, regular versus admin, instead of lumped into one blanket service-level grant.

The OWASP Top 10 for LLM Applications 2025 names excessive agency as a critical risk, and it breaks into three root causes that map onto MCP almost one-to-one: excessive functionality (tools the agent can reach but doesn't need for its task), excessive permissions (broader privileges than the task requires), and excessive autonomy (high-impact actions happening with nobody watching). Push this into ABAC or PBAC, and policies start factoring in user role, department, time of day, IP address, letting a system permit read-only shell commands like ls or cat while blocking something like rm -rf for a non-admin agent, with the MCP server reporting the exact sub-action to a Policy Decision Point that makes the call.

Token scope and schema constraints compounding: how the layers interact in practice

Diagram: Four Locks, One Door: How Least-Privilege Layers Interact. Visualizes: Visualize four distinct security layers that must all work together to enforce least privilege in MCP deployments: (1) Schema constraints — type declarations, enum…

Picture an agent holding a token scoped down to calendar:read, with the schema constraining the date-range parameter to a fixed maximum window and readOnlyHint set correctly. The ListTools handler hides every write-calendar tool from this session entirely. Four layers, each doing its own job, and each one blind to failures in the others.

Get the OAuth scope right but leave the schema under-constrained, and you've still got a valid token sitting right next to a path traversal bug or an injection point. Get the schema right but set the wrong annotation, and a downstream gateway misclassifies the tool and hands out the wrong permission tier. Get the annotation right but skip RBAC filtering on ListTools, and the tool just sits there, visible to users who should never have known it existed in the first place.

Think of least privilege as four separate locks on the same door: schema constraints, annotations, OAuth scopes, RBAC filtering. Each one's necessary. None of them, alone, is enough. Session-scoped and time-bounded tokens add a fifth dimension worth mentioning too, since a token that expires fast limits the damage from a compromised session even if everything upstream was already misconfigured.

Get all four right at design time and the payoff compounds. Each correctly built layer shrinks what's left for the layers above it to catch, which means quieter audit logs and fewer false alarms tripping runtime monitors when something actually does go wrong. So the real question becomes practical fast: who's holding all four layers consistent across a catalog of MCP servers that grows every single quarter?

What a central gateway provides that per-server schema discipline alone cannot

Discipline at the individual server level matters, but it doesn't touch the catalog problem. Every new MCP server added without a registry entry is an identity nobody's accounting for, and unaccounted identities are exactly the ones that get exploited first.

MCPManager, a Usercentrics product for auditing and controlling AI data access via MCP, is one example of a governance layer built to hold that catalog together. Without a gateway sitting over the whole catalog, things break in predictable ways. Schema constraints defined tightly on one server mean nothing to a session that happens to route through a different server running looser rules. Annotation completeness can't be checked across a catalog without something central keeping score. RBAC policies written per-server drift apart over time; a role that means "read-only" on one server can quietly mean "write access" on another. Audit logs, scattered one per server, force incident response teams to piece together what an agent actually touched only after the fact, once the damage is already done.

That timing gap is the real cost here. A log you read after something's gone wrong is a postmortem. Real-time visibility into what an agent is touching while it's happening is the actual safety net, and no amount of per-server logging gets you real-time correlation across a session spanning multiple systems.

A central gateway closes these gaps in a few specific ways. One registry, so every MCP server and every tool identity gets registered before an agent can ever reach it. Policy enforcement at the gateway layer, so schema constraints and annotation-based permission tiers apply the same way no matter how carelessly one particular server built its own rules. Dynamic tool filtering, where ListTools responses get shaped by the authenticated agent's actual role instead of whatever half-built RBAC logic one server happens to have lying around. And real-time observability: which tool got called, with what parameters, by which agent identity, at what time, correlated across the whole session instead of locked away in one server's isolated log file.

Schema discipline is where least privilege starts. A gateway is where it survives once you're running more than a handful of servers at once, and in my experience, that threshold comes a lot sooner than most teams plan for.

Sources

  1. workos.com
  2. labs.cloudsecurityalliance.org
  3. waxell.ai
  4. modelcontextprotocol.io
  5. apxml.com
Filed underMCP Architecture

More in MCP Architecture