Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project
Least Privilege
Summary
Grant each MCP server, each tool, and each credential the minimum capability required to do its job, and deny everything else by default. Least privilege is the smallest control surface that meaningfully reduces blast radius from a compromised server, a confused-deputy attack, or a successful prompt injection. On Vercel the pattern gets a serverless upgrade: OIDC federation can replace long-lived cloud keys entirely, and every remaining credential is scoped to one project and one environment by configuration, not by discipline.
Problem addressed
The cost of an MCP integration going wrong scales with what it can do. A server that holds a credential with full-tenant scope can, on compromise, take any action the credential can take, regardless of which tool was nominally exposed. A tool with an over-broad input schema can be coerced via prompt injection into operations the designer never intended. A host that shows the model every tool from every server makes it trivially possible for the model to call something the user did not have in mind.
Serverless sharpens the problem. Every MCP server on Vercel is a remote server behind a public URL, and the classic failure mode is a long-lived cloud key pasted into an environment variable, readable by anyone who can read project settings, valid for months after it leaks. Default-allow is the easy posture. Default-deny, with explicit, justified grants per capability and per credential, is the posture that holds up under attack.
When to use
Always. Least privilege is not a discretionary pattern; it is the baseline. The decision is not whether to apply it but where to draw each boundary.
In particular, apply it deliberately when:
- Provisioning the credential a Vercel Function will use against an upstream backend.
- Deciding which tools, resources, and prompts a server exposes at discovery.
- Filtering
tools/list,resources/list, andprompts/listper authenticated principal. - Choosing the scopes an access token must carry before a tool call is honored.
- Assigning environment variables to production, preview, and development targets.
- Granting “always allow” or session-scoped approvals from the host UI.
When not to use
There is no “don’t apply least privilege” case. Anti-pattern variants to avoid:
- Reusing a single high-privilege credential across many tools because credential provisioning is annoying.
- Exposing the whole backend API “for completeness” when only a handful of operations are needed.
- Returning the union of every server’s capabilities to every user because per-user filtering is harder than no filtering.
- Sharing one team-wide environment variable across every project and environment because per-project scoping takes a few more clicks.
- Letting preview deployments inherit the production credential because provisioning a second, weaker one felt like overkill.
Architecture / flow diagram
Diagram source (Mermaid)
flowchart TB
Host[Host] --> Client[MCP Client]
Client -->|Streamable HTTP| Fn[MCP Server Function]
Fn -->|short-lived exchanged token| Backend[Cloud backend]
B1[Per-tool schema and authz] -.enforced at.-> Fn
B2[Per-principal capability filter] -.enforced at.-> Fn
B3[Env-scoped credential config] -.enforced at.-> Fn
B4[Trust policy pins project and environment] -.enforced at.-> BackendEach boundary (B1 to B4) is enforced at the element it points to: the server constrains its own tool schemas, authorization, and per-principal listing; project- and environment-scoped configuration constrains what credential the function even holds; and the cloud-side trust policy constrains which deployments can obtain a credential at all.
Protocol implications
- The server declares its surface via the mandatory
server/discoverRPC and its discovery lists; only what is declared is callable. Declare less. MCP 2026-07-28 removed theinitializehandshake (SEP-2575): capabilities now travel in each request’s_metaand in theserver/discoverresult, so the declared surface is re-asserted on every exchange rather than negotiated once. - Each tool’s
inputSchemais itself a scoping mechanism: tighter schemas reject more adversarial inputs before any handler runs. Per SEP-1303 (adopted in 2025-11-25 and unchanged in 2026-07-28), surface an input-validation failure as a tool execution error, atools/callresult withisError: true, rather than a JSON-RPC protocol error, so the model can read the rejection and self-correct. tools/list,resources/list, andprompts/listresults must be filtered per authenticated principal. The protocol allows servers to vary the listed set; use that. A listing filter alone is not an access control: enforce the same decision again at call time.- The MCP authorization model binds access tokens to a single resource server via RFC 8707
resourceindicators; a token minted for one MCP server must not be accepted by, or forwarded to, another. Scoped tokens are least privilege applied to the connection itself. - 2026-07-28 removed protocol sessions: cross-call state is an explicit server-minted handle the client passes back as an ordinary tool argument (SEP-2567). A handle is a capability, so scope it like one: mint it bound to one principal and one job, enforce that binding server-side on every use, give it an expiry, and reject presentation by any other principal. A handle any caller can replay is a session cookie without the cookie jar.
- For locally-run stdio servers, roots are deprecated as of 2026-07-28 (SEP-2577); prefer explicit tool parameters, resource URIs, or server configuration to scope filesystem access. Where a stack still speaks roots during the deprecation window, declare the narrowest root the task requires.
- Sampling is deprecated as of 2026-07-28 (SEP-2577) in favor of direct LLM provider APIs. Where a stack still uses it during the deprecation window, sampling requests remain subject to host approval; deny by default and grant per-request.
- Destructive tools should be gated behind an authorization check distinct from the authentication the request arrived with, and marked with honest tool annotations so the host can demand explicit approval.
- Wire status: the pinned stack (
mcp-handler2.1.1 on@modelcontextprotocol/server2.0.0) serves the 2026-07-28 contract natively over Streamable HTTP and falls back to stateless 2025-11-25 Streamable HTTP for legacy clients; the SDKClientdefaults to that legacy handshake unless you opt in to modern version negotiation, so the examples’ in-memory test suites exercise only the legacy path.
Vercel mapping
- Trade static keys for OIDC federation. The strongest least-privilege move on Vercel is deleting the long-lived cloud key. With OIDC federation enabled, Vercel issues a short-lived signed token: the function token has a two hour TTL, and Vercel reuses a token for up to 90 minutes before minting a fresh one, so the remaining window keeps it valid through the longest invocation. In builds and local development it is exposed as the
VERCEL_OIDC_TOKENenvironment variable; in Vercel Functions it arrives on each request as thex-vercel-oidc-tokenheader, andgetVercelOidcToken()from@vercel/oidc(the helper moved out of@vercel/functions/oidc) reads it from whichever location applies. Your cloud provider trusts Vercel’s issuer and exchanges that token for temporary credentials; on AWS this issts:AssumeRoleWithWebIdentity, wrapped for you byawsCredentialsProviderfrom@vercel/oidc-aws-credentials-provider. Nothing long-lived exists to leak, rotate, or forget. - Pin the trust policy to the narrowest subject. The token’s
subclaim has the shapeowner:<team>:project:<project>:environment:production. An exact-match condition onsubandaudin the role’s trust policy means only that one project’s production deployments can assume the role. Wildcards (project:*,environment:preview) widen access; write them deliberately, never by default.AssumeRoleWithWebIdentityalso accepts an inline session policy, so a single role can be narrowed further per exchange. - One role per environment. Give production a role scoped to exactly the operations your tools expose, and give preview a separate role that is read-only, or no role at all. Environment separation on the cloud side is what makes environment separation on the Vercel side mean something.
- Scope environment variables per project and per environment. Vercel environment variables target production, preview, and development independently. A preview deployment is a public URL running unreviewed branch code; it should hold a lower-privilege credential than production, or none. Prefer project-scoped variables over team-wide shared ones: a shared variable is a shared blast radius.
- Make secrets write-only. Mark credentials as sensitive environment variables: the value cannot be decrypted or read back after creation, and Vercel redacts sensitive values that are 32 characters or longer (and always
VERCEL_OIDC_TOKENandVERCEL_AUTOMATION_BYPASS_SECRET) from build logs; a shorter secret is not redacted, so generate secrets long enough to qualify. Team owners can enforce this by policy so every new production and preview variable is sensitive by default. - Marketplace credentials are per-project by construction. Connecting a Marketplace resource (
vercel integration resource connect) injects that resource’s credentials into the connected project only, restrictable per environment with--environmentand prefixable to avoid collisions. The injected credential belongs to one resource, not to a team-wide master account; keep it that way by connecting resources per project rather than hand-copying one resource’s credentials across many. - Nothing secret in
NEXT_PUBLIC_*. Any variable with that prefix is compiled into the client bundle. It is not configuration; it is publication.
Security considerations
- Each tool must declare the upstream scopes or permissions it requires, and the server should refuse to start if the configured grants are missing or exceed the declared set. Excess is a finding, not a convenience. See Authorization & scoping.
- Default deny: any tool invocation whose authorization decision is indeterminate is rejected, not allowed. See Authorization & scoping.
- Filter capability listings per principal, and enforce the same decision at call time; do not assume the client will filter for you. See Authorization & scoping.
- Prefer exchanged short-lived credentials (OIDC federation) over static keys; where a static credential is unavoidable, give it a short expiry and a documented rotation flow. See Authentication.
- There is no per-function egress firewall on Vercel: the server’s code is its own outbound allowlist. Static IPs (Pro and Enterprise) give the project a fixed egress address a backend can allowlist, and Secure Compute (Enterprise-only) adds private connectivity, but neither filters what your code may call. Never construct upstream hosts or URLs from model-supplied input. See Trust boundaries.
- Preview deployments holding any real credential must be behind Deployment Protection, and preview credentials must be weaker than production’s. See Deployment posture.
- Approval state is scoped per server; do not infer approval across servers from prior grants. See Consent & user approval.
- “Always allow” and bulk-approval modes must be opt-in, time-bounded, and revocable. See Consent & user approval.
The cheapest credential to defend is the one that does not exist. Federate first; scope what remains.
Example implementation
examples/least-privilege-server(in the repository) - the in-process enforcement half of this page: explicit per-tool upstream scope declarations, refuse-to-start configuration validation that rejects a grant set which lacks or exceeds the declared scopes, a registration drift guard that fails closed if a tool is registered without declared scopes, per-principal least privilege at both layers (atools/listhandler installed withsetRequestHandlerthat answers each request from the verified principal inctx.http.authInfo, and call-time authorization inside every handler, becausetools/callresolves every registered tool regardless of what the listing showed, so a principal who cannot see a tool must also be unable to call it), and bounded inputs as a scoping mechanism:amountCentsis capped at 100000 per refund by the schema and then checked against the looked-up invoice in the handler, andinvoiceIdmust match^inv-[0-9]+$at no more than 64 characters, all advertised in the tool’sinputSchema. The principal comes from the bearer token verified bywithMcpAuth(a stub token table insrc/auth.ts), never from an argument. This page carries the credential story (OIDC federation, environment-scoped variables); the example enforces what the process can enforce about itself.examples/secure-tools-server(in the repository) - least privilege at the tool layer: tight per-argument input validation that rejects out-of-bounds input before any state change, default-deny authorization, and output minimization.examples/minimal-server(in the repository) - the smallest possible declared surface, as a contrast.
Trade-offs
| Pros | Cons |
|---|---|
| Smallest possible blast radius from any single compromise. | More roles, trust policies, and per-environment variables to provision and audit. |
| OIDC federation removes the long-lived key class of leak entirely. | One-time cloud-side setup (identity provider, trust policies) per team. |
| Default-deny rejects the failure modes you didn’t think of. | Up-front scoping work; harder to add capabilities ad hoc. |
| Per-principal filtering enables real multi-tenant deployments. | Listing endpoints must be principal-aware, which complicates caching. |
| Tight input schemas reject prompt-injection payloads before any handler runs. | Schema discipline must be maintained across every new tool. |
Related patterns
- trust-boundaries - least privilege is how each trust boundary is actually enforced.
- adapter - the credential an adapter holds must be scoped to its exposed tools, not the whole backend.
- sidecar - runtime isolation is least privilege applied to compute; combine with capability scoping for layered defense.
- facade - the central enforcement point for per-principal capability filtering across many backends, and the pattern most in need of per-backend credential scoping.
- query-vs-command - commands typically require broader credential scope than queries; split roles accordingly.
Vercel deployment (Terraform)
An illustrative Vercel expression of this pattern lives in terraform/patterns/least-privilege (in the repository): per-environment project environment variables, sensitive variables for anything secret, and access-group scoping, built with the official vercel/vercel provider; its README carries the cloud-side OIDC trust-policy example. It is tofu validate-checked, never applied in CI. See terraform/README.md (in the repository) for scope and caveats.
Bibliography
- Model Context Protocol Specification, Authorization, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization
- Model Context Protocol Specification, Tools, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/server/tools
- Model Context Protocol Specification, Roots (deprecated), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/client/roots
- Model Context Protocol Specification, Deprecated features, version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/deprecated
- Model Context Protocol, Security Best Practices - https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices
- Vercel Documentation, OpenID Connect (OIDC) Federation - https://vercel.com/docs/oidc
- Vercel Documentation, Connect to Amazon Web Services (AWS) - https://vercel.com/docs/oidc/aws
- Vercel Documentation, Environment variables - https://vercel.com/docs/environment-variables
- Vercel Documentation, Sensitive environment variables (build-log redaction applies to values of 32 characters or more) - https://vercel.com/docs/environment-variables/sensitive-environment-variables
- Vercel Documentation, Static IPs - https://vercel.com/docs/networking/static-ips
- Vercel Documentation, Secure Compute - https://vercel.com/docs/networking/secure-compute
- Vercel Documentation, Vercel Marketplace - https://vercel.com/docs/integrations
- AWS Security Token Service API Reference, AssumeRoleWithWebIdentity - https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
- OWASP Top 10 for Large Language Model Applications - https://owasp.org/www-project-top-10-for-large-language-model-applications/