The MCP spec hardened the handshake and left the invocation unguarded
The 2026-07-28 MCP revision fixed authorization everywhere except the boundary where an agent actually acts: the individual tool call.
On July 28, 2026, MCP shipped its biggest revision since launch. It made the protocol stateless, formalized extensions, and hardened authorization. This is a serious, well-run standards effort, and the security work in it is real: issuer validation per RFC 9207, credential binding to the issuing server, audience-bound tokens, enterprise IdP policy controls. Every one of those hardens the front door.
None of them guards the door after you walk through it.
The spec says so in its own words. Its authorization section states that authorization is OPTIONAL for MCP implementations, and that "the implementation details of the authorization server are beyond the scope of this specification." The security section is blunter: "While MCP itself cannot enforce these security principles at the protocol level..." What the 2026 revision hardened is the handshake: who gets in, and with what token. What it does not define is what a tool is allowed to do on each call, or a deny the server can produce when a call exceeds it.
This is the same gap that shows up everywhere agents act: every permission system ships a check primitive, none ships the map. With MCP it is sharper, because MCP is the default connector for agents talking to business systems, and the spec went out of its way to fix authorization everywhere except the invocation boundary.
What the spec actually covers
Walk the 2026-07-28 authorization spec and every requirement is about establishing and maintaining a session: client registration, PKCE, RFC 9207 issuer validation, RFC 8707 resource indicators, refresh token guidance, and a step-up flow that re-authorizes the client when an operation needs more scope.
The runtime path for insufficient permission is telling. When a client presents a token that lacks scope for an operation, the server responds 403 with error="insufficient_scope", and the client is expected to compute the union of its prior scopes and the challenge, then run a step-up authorization flow - a human, at a login page, approving an expanded grant. That is the entire runtime story. There is no server-side, per-invocation deny that says "this tool, on this call, with these arguments, is not permitted" and enforces it deterministically. Step-up consent decides what a user approves at login. It does nothing about what the tool does on call number 500 an hour later, with no human watching.
Others have named the same gap
Rock Lambros, writing in RockCyber Musings a week after the release, put it plainly: the six authorization SEPs are all about the handshake - "Not one defines a per-tool capability that the server has to check on every invocation, or a deny it has to be able to produce." A practitioner building an MCP gateway said the same thing more concisely: "MCP has no built-in concept of per-tool, per-user permissions."
And the consequence has been measured. RockCyber describes a June 2026 study of MCP-style runtimes: a connection-layer defense with human approval on every call blocked four of ten attack cases, while a runtime that enforced scope as an explicit execution-time invariant blocked all ten. The OAP paper from March 2026 measured the same shape at the tool level: 74.6% social-engineering success against a permissive agent, 0% across 879 attempts under a restrictive policy enforced before each call.
This is not a corner case. Excessive agency - an agent taking broader actions than its scope - is LLM03 in the OWASP GenAI LLM Top 10 2026, and AWS's Well-Architected GENSEC05-BP01 rates the risk high and notes the agent "has little knowledge beyond the prompt as to what behaviors are permitted or not."
The gap is architectural, and it has a shape
Every permission system ships a check primitive. AWS has SimulatePrincipalPolicy. Kubernetes has SelfSubjectAccessReview. GCP has testIamPermissions. Each answers: given a policy, is this principal allowed to do this thing? None ships the map: what permissions does this operation require in the first place? That mapping is the missing piece every enforcement layer needs and no provider publishes.
That gives the gap a structure, and the structure is four verbs per provider:
resolve(operation) -> required_permissions # the missing map
check(permissions) -> verdict # the provider's native API
generate(operations) -> policy # compose least privilege
verify(policy, operations) -> verdict # the provider's simulator
resolve is the only function you build. Everything else delegates to primitives the providers already ship. For MCP, the four verbs map directly onto what the spec does not have: a per-invocation check that resolves what a tool call requires, consults the agent's effective permissions, and can produce a deny.
A working implementation exists
None of this needs to be speculative. There is a live, public implementation of the map: Stigmer (https://stigmer.network/mcp) resolves boto3 operations to their required IAM actions from machine-readable sources, generates least-privilege policies, and verifies them against AWS's own SimulateCustomPolicy - confirming a generated policy grants what was intended and nothing extra.
What already exists: policy generators for humans
Prospective policy generation is not new. Salesforce's policy_sentry has done it since 2019 - it "allows users to create least-privilege IAM policies in a matter of seconds... scoped down according to access levels and resources." AWS's IAM Access Analyzer is the retrospective complement: start_policy_generation takes a principal and a CloudTrail trail with a start and end time, and infers permissions from what actually ran. One is preventive, the other reactive. Both are mature tools for a human authoring a policy ahead of time.
What those tools do not do is take an operation as the input. policy_sentry wants an access level and a resource ARN - a human says "Read on this bucket" and gets a policy. The gap is that an agent knows the API call it is about to make, not an abstract CRUD level it wants. s3.PutObject in, required actions out: that is the input shape nobody's generator takes, and it is the one an agent call path needs.
The map as data: an OPA bundle service
The same map ships as an OPA bundle - the format Open Policy Agent already consumes. OPA's Bundle Service API is a documented HTTP contract: a server exposes a gzipped tarball with policy and data, sets an ETag, and replies 304 Not Modified when nothing changed. A bundle is the exact shape of what Stigmer has: the operation-to-action map is the data; the rule that looks it up is a few lines of Rego.
So Stigmer's map is published at https://stigmer.network/opa/bundle.tar.gz, signed (.signatures.json, RS256) so OPA verifies it before activation. Any OPA instance subscribes with one config entry - a services and bundles block - and pulls the map automatically, refreshed on every poll, no reinstall. That reaches the ecosystems that already run the enforcement engine: Gatekeeper for Kubernetes admission, Conftest evaluating Terraform plan JSON in CI, OPAL, and the agent-gateway pattern from the InfoQ reference architecture, where the allow_actor map is hand-written and nothing derives what the policy should say. Stigmer is the input to the engine everyone already runs.
Three mechanisms from it show the enforcement shapes:
A pre-action authorization hook. Strands fires a BeforeToolCallEvent before every tool call. A hook there asks AWS's own simulator whether the current identity is allowed to perform the pending operation. On a deny it cancels the call and lists the missing permissions; on an unknown verdict it passes through by default or blocks under a fail-closed policy. That is a per-invocation check the agent frameworks currently lack, shipping in the strands-stigmer package today.
Scoped execution. use_aws runs every call against the ambient session and exposes no parameter that accepts scoped credentials, so nothing can narrow a single call. A drop-in tool, stigmer_use_aws, adds optional role_arn and session_policy: it assumes the role, applies the least-privilege policy, and the call's effective permissions are the intersection of the two. The scoping happens at client construction because that is the only place it can: an ambient session is fixed at process launch and cannot be narrowed after the fact.
A policy-as-code bundle. The OPA bundle is the third shape: the map published in the format enforcement engines consume, signed, and kept current by re-sync from the service definitions. Adoption is one config line in OPA - no package install, no pull request, no approval - and every subscriber gets updates automatically. It is the same four verbs, expressed as data plus a lookup rule instead of an HTTP API.
What this is and is not
It is the operation-level map, and the three enforcement shapes it enables: check-before-call, scope-the-call, and policy-as-code for engines that already enforce. Policy generators for humans exist - policy_sentry does this well. What no one ships is the map keyed to the operation an agent is about to call, delivered in the format an enforcement engine consumes at runtime.
It is not a silver bullet. A check before a tool call does not replace a least-privilege role, and a least-privilege role does not replace conditions or boundaries. The layers compose. What the MCP 2026-07-28 revision did - hardening the handshake, deliberately leaving the invocation to implementers - made that composition the implementer's job. The map and the per-invocation check are the parts of the job that were missing.
The front door is stronger than it was a month ago. The door behind it, where the agent acts on call number 500 with no human watching, is still open. It does not have to stay that way.