API keys vs. OAuth for your MCP server

Of MCP servers that require credentials, 53% use a bare static API key or personal access token instead of OAuth — only 8.5% implement real OAuth. Roughly 40% of publicly exposed MCP servers have no authentication at all. Static API keys aren't a fringe choice; they're the default.

Why API keys became the default

It's not that MCP server authors don't know OAuth exists. It's that a static key is the fastest thing that works: generate a random string, check it against an environment variable, ship. Real OAuth usually means standing up an authorization server, a consent screen, token issuance and refresh, and — for MCP specifically — supporting Dynamic Client Registration (RFC 7591), since MCP clients register themselves programmatically rather than through a manual developer-portal signup. Almost no existing OAuth provider supports DCR out of the box, so teams that want to do OAuth "properly" hit a wall before they even get started, and fall back to an API key instead.

The concrete risks of a static API key

  • No expiry or rotation. A key that was valid on day one is still valid years later unless someone remembers to rotate it — and rotation usually means manually updating every client that uses it.
  • No scoping.It's all-or-nothing. A key that can read one resource can typically call every tool your MCP server exposes.
  • Full-access blast radius if leaked. A key committed to a public repo, logged by accident, or pasted into the wrong chat window hands over everything it can do, to anyone, until someone notices.
  • No standard revocation mechanism. There's no common way to invalidate one compromised key without either rotating the shared secret for everyone or shipping custom revocation logic yourself.

What real OAuth adds

OAuth 2.1 doesn't just replace the string you check — it changes the shape of the problem:

  • Short-lived access tokens. mcpauth issues tokens with a 1-hour default expiry, so a leaked token is only useful for a limited window instead of indefinitely.
  • Scopes. Tokens carry specific scopes rather than blanket access, so a client only gets what it actually needs.
  • Per-client revocation. A single compromised client's token can be revoked via POST /api/oauth/revoke without touching every other client.
  • Standard discovery. Clients find every endpoint they need from one well-known URL — /.well-known/oauth-authorization-server — instead of your API-key scheme being documented (or undocumented) ad hoc.

The actual migration cost

This is usually the part that scares people off, and it's the part mcpauth is built to shrink. Swapping a hand-rolled API key check for OAuth backed by mcpauth is close to a one-line change in your middleware:

Before

// Hand-rolled API key check
app.use("/mcp", (req, res, next) => {
  const key = req.headers["x-api-key"];

  if (!key || key !== process.env.MCP_API_KEY) {
    return res.status(401).json({ error: "unauthorized" });
  }

  // No scopes, no expiry, no per-client identity.
  // Anyone with this one string has full access, forever,
  // until you rotate it and update every client by hand.
  next();
});

After

import { mcpAuth } from "getmcpauth";

// Real OAuth 2.1: short-lived tokens, per-client identity,
// scopes, and revocation — verified by mcpauth, cached
// in-process so it doesn't add a round-trip per tool call.
app.use(
  "/mcp",
  mcpAuth({ registrationSecret: process.env.MCPAUTH_SECRET })
);

mcpAuth() wraps the official @modelcontextprotocol/sdk's requireBearerAuthmiddleware, so requests without a valid token are rejected with a spec-correct 401 before they ever reach your handlers — and successful checks are cached in-process for 30 seconds by default, so a chatty agent conversation doesn't add a network round-trip to every tool call. Following the quickstart, the whole change takes about 10 minutes.