ChanlChanl
Tools & MCP

9 in 10 MCP Servers Skip OAuth. The July 28 Spec Just Landed.

Only 8.5% of MCP servers use OAuth. The July 28 spec landed, and it doesn't make auth mandatory. Here's what it actually requires and what to fix first.

DGDean GroverCo-founderFollow
July 4, 2026
13 min read
A Server Rack Visualization with Lock Icons Showing Secure and Unsecured MCP Server Connections

Your agent has 12 MCP servers connected. It can read customer records, look up order history, update the CRM, trigger refunds, and book follow-up appointments. You've been shipping features fast. The agent works. Customers are happy.

Then a security researcher sends you a report. One of your MCP servers, the one connecting to your internal knowledge base, has no auth. Anyone who can reach it on the network can query it directly, bypassing your agent entirely. The researcher found it by reading your agent's tool manifest, which is public because your chatbot widget is public.

You check the others. Three more are unprotected.

This isn't hypothetical. When BlueRock scanned roughly 7,000 public MCP servers, 41% required no authentication at all, and only 8.5% used OAuth. Of the ones that did authenticate, 53% leaned on a static API key, which is a shared secret with no expiry, no audience, and no way to tell two callers apart. The gap exists because MCP was designed for local development first, and most implementations never got hardened when they went to production.

The new MCP specification landed on July 28, 2026, and it rewrote how the protocol handles authorization. If you were treating that date as a deadline to prepare for, it has already passed. What follows is what actually changed, what the spec does and doesn't force you to do, and how to close the gap before someone else finds it for you.

Why Do So Few MCP Servers Have Auth?

MCP was built for one place first: a developer's own laptop. It launched as a way to give AI assistants access to local tools and data sources. The original use case was Claude Desktop connecting to local files, a local SQLite database, maybe a GitHub MCP server using a personal access token stored in config. Auth overhead made sense to skip, because the caller and the server were the same person on the same machine.

That mental model didn't scale. Teams started deploying MCP servers to production infrastructure. They connected them to CRMs, internal databases, ticket systems, and payment processors. They exposed them to agents running in the cloud, accessible from outside the local environment. The tooling moved to production; the auth model didn't.

The result is what you see in the registry numbers. Most MCP server implementations are forks of examples that started local. The auth scaffolding was never added because the original wasn't built for remote deployment.

If you're newer to MCP and want a primer on how the protocol works before diving into auth, the MCP overview covers the basics. The July 28 spec doesn't invent new concepts. OAuth 2.1 has been around for years. What it does is stop leaving the details to each implementer: it names the RFCs, and it turns the parts everyone was improvising into hard requirements.

What Does the July 28 Spec Actually Change?

Start with the part a lot of the coverage got wrong, including a few headlines that ran the week it shipped. The spec does not make authorization mandatory. It says plainly that authorization is OPTIONAL for MCP implementations, and that HTTP-based transports SHOULD conform to it. If you were bracing for a cutoff that hard-fails unauthenticated servers, there isn't one, and pretending otherwise just burns your credibility with the engineer you're trying to convince.

What did change is the bar you're held to once you do implement auth. There the language stops being polite. An MCP server is now formally an OAuth 2.1 resource server, and these are MUSTs.

RequirementRFCWho it bindsWhat it closes
Protected Resource MetadataRFC 9728Servers MUST implement it; clients MUST use it for discoveryClients guessing or hardcoding the wrong auth server
Resource Indicators (resource)RFC 8707Clients MUST send it on both authorization and token requestsA token for your KB server replayed against your CRM server
Audience validationRFC 8707 §2Servers MUST reject tokens not issued for themConfused-deputy and token passthrough
Client identityCIMD draft / RFC 7591Clients MUST hold a client_id before starting the flowUnknown callers invoking tools anonymously
Issuer validationRFC 9207Clients MUST validate iss before redeeming a codeAuthorization-server mix-up attacks

Protected Resource Metadata (RFC 9728) is the one hard obligation the spec puts on you as a server operator. You expose a discovery endpoint at /.well-known/oauth-protected-resource naming which authorization server issues valid tokens and what scopes exist. Clients read it instead of you hardcoding an auth server URL into every client config.

Resource Indicators (RFC 8707) require clients to include a resource parameter identifying the MCP server the token is for, using the server's canonical URI. Clients have to send it whether or not the authorization server actually supports it, which is a deliberately blunt rule: it means the parameter shows up in the wild fast enough that auth servers have to catch up. A token issued for your knowledge base server can't then be replayed against your CRM server.

Audience validation is the server-side half of that bargain, and it's the requirement most likely to be skipped, because everything appears to work without it. Your server MUST confirm the token was issued for it specifically, and MUST NOT accept or forward tokens meant for anything else.

Client identity works differently than it did. Clients still MUST obtain a client_id, but Dynamic Client Registration (RFC 7591) is now deprecated, kept only for backwards compatibility. The preferred mechanism is Client ID Metadata Documents, where the client uses an HTTPS URL as its client_id and the authorization server fetches the metadata from it. Worth being precise about who checks what: the authorization server validates client identity, not your MCP server. Your server's job is the token audience.

Issuer validation (RFC 9207) is the quietest addition and closes a real attack. Clients record the issuer from the authorization server's metadata, then compare the iss value returned in the authorization response before they exchange the code. Without it, an attacker who can steer a client to a lookalike authorization server can swap in their own code.

Together these mean a valid OAuth token isn't enough. It has to be issued for the exact server being called, by an issuer the client verified, on behalf of a client that holds a real identity.

The Three Planes You Actually Need to Secure

Securing MCP means locking three boundaries: who can call your server, what each caller can do once inside, and what your server is allowed to do in the downstream systems it reaches. Most teams only lock the first. There are two more, and all three matter in production.

Agent or MCP client Plane 1: Inbound auth Plane2 MCPServer Plane 3: Outbound auth
Three auth planes for production MCP deployments

Plane 1: Inbound auth validates that the caller holds a valid OAuth 2.1 token issued for your specific server. This is the plane the July spec writes rules for, and the audience check is the MUST inside it. Without any of it, every network-accessible caller can invoke your tools.

Plane 2: Access control governs what a validated caller is allowed to do. A customer service agent might be allowed to read customer records but not update billing information. A scheduling agent might be allowed to read and create calendar entries but not delete them. Scopes define the boundaries.

Plane 3: Outbound auth governs how your MCP server authenticates to the downstream systems it calls. Your CRM has its own API key. Your calendar API has its own OAuth credentials. Your payment processor has its own service account. If those credentials live in your MCP server's config unscoped, any caller who gets through planes 1 and 2 can trigger any action the downstream system allows, regardless of what scope they were granted.

Most incidents come from plane 3 being ignored. The OAuth layer at the front looks secure. But once a caller is inside, the outbound credentials are the same for everyone. A poorly scoped tool call can trigger a refund, send an email, or delete a record using the full service account permissions, not the narrower permissions the agent was supposed to have.

The unprotected knowledge base server from the opening is a plane 1 failure, and it's the one everybody can picture. Plane 3 is the quieter problem: it's what turns one over-permissioned token into a refund nobody authorized.

Implementing Inbound Auth for a Node.js MCP Server

Here's what adding OAuth 2.1 validation looks like in a typical TypeScript MCP server:

mcp-auth-middleware.ts·typescript
import { createRemoteJWKSet, jwtVerify } from "jose";
 
const JWKS = createRemoteJWKSet(
  new URL("https://your-auth-server.com/.well-known/jwks.json")
);
 
const MCP_SERVER_URI = "https://mcp.yourcompany.com/crm";
 
export async function verifyMcpToken(
  authHeader: string | undefined
): Promise<TokenClaims> {
  if (!authHeader?.startsWith("Bearer ")) {
    throw new Error("Missing or invalid Authorization header");
  }
 
  const token = authHeader.slice(7);
 
  const { payload } = await jwtVerify(token, JWKS, {
    // Reject any token whose audience isn't this exact server URI
    audience: MCP_SERVER_URI,
    issuer: "https://your-auth-server.com",
  });
 
  return payload as TokenClaims;
}
 
// In your MCP server request handler:
export async function handleMcpRequest(req: Request): Promise<Response> {
  let claims: TokenClaims;
 
  try {
    claims = await verifyMcpToken(req.headers.get("Authorization"));
  } catch {
    // A bare 401 is a dead end. The client needs to be pointed at the
    // Protected Resource Metadata document to discover the auth server.
    return new Response(JSON.stringify({ error: "Unauthorized" }), {
      status: 401,
      headers: {
        "Content-Type": "application/json",
        "WWW-Authenticate":
          `Bearer resource_metadata="${MCP_SERVER_URI}/.well-known/oauth-protected-resource"`,
      },
    });
  }
 
  // Pass validated claims into your tool handlers
  return handleToolCall(req, claims);
}

Two lines are doing the real work here. The audience option is the server-side enforcement of RFC 8707: the resource parameter binds a token to one target at issue time, and checking the aud claim is how you refuse everything else. Without it, a token minted for a different MCP server on the same auth server sails straight through.

The WWW-Authenticate header on the 401 is the other one, and it's the piece most implementations forget. It's how a client that has never seen your server discovers where to go get a token. Return a naked 401 and a compliant client has nowhere to go next.

Scoping Access per Tool

Once you have a validated caller identity, you need to decide what they're allowed to do. The simplest approach is scope-based access control at the tool level.

tool-scope-guard.ts·typescript
// Define which scope each tool requires
const TOOL_SCOPES: Record<string, string[]> = {
  "get_customer_record": ["crm:read"],
  "update_customer_email": ["crm:write"],
  "trigger_refund": ["payments:write", "crm:write"],
  "read_order_history": ["orders:read"],
  "cancel_order": ["orders:write"],
  "book_appointment": ["calendar:write"],
};
 
export function assertToolAccess(
  toolName: string,
  tokenScopes: string[]
): void {
  const required = TOOL_SCOPES[toolName];
  if (!required) {
    throw new Error(`Unknown tool: ${toolName}`);
  }
 
  const scopeSet = new Set(tokenScopes);
  const missing = required.filter((s) => !scopeSet.has(s));
 
  if (missing.length > 0) {
    throw new Error(
      `Insufficient scope for ${toolName}. Missing: ${missing.join(", ")}`
    );
  }
}

When an agent calls your trigger_refund tool, your server checks that the token includes both payments:write and crm:write. If the agent was only issued crm:read (maybe it's a lookup-only agent), the call is rejected before any business logic runs.

This separates your agent's role from your service account's permissions. Your CRM service account might have full admin access. The agent token only gets crm:read. Even if the agent is compromised or manipulated through prompt injection, it can't write to the CRM because the token scope prevents it.

Outbound Auth: Acting on Behalf of Your Users

Outbound auth means your MCP server calls downstream systems with user-specific credentials, not a single shared service account. This makes every action attributable to the user or session that triggered it, which is what compliance teams and audit logs actually need.

This third plane matters most in multi-user deployments. Your agent isn't acting on behalf of the MCP server itself. It's acting on behalf of a specific user who started the conversation.

When a customer calls in and your agent looks up their account, the audit log in your CRM should show that the lookup was performed by that specific customer's agent session, not by a generic service account. When the agent triggers a refund, the payment processor's log should show which session initiated it.

Token exchange (RFC 8693) supports this pattern. It's a separate OAuth extension rather than part of the MCP spec, so check that your authorization server implements it before designing around it. Your agent receives a token representing the user, and when it calls your MCP server, the server exchanges that token for a downstream credential that carries the user context forward.

outbound-token-exchange.ts·typescript
async function getDownstreamToken(
  userToken: string,
  targetService: "crm" | "payments" | "calendar"
): Promise<string> {
  const response = await fetch("https://your-auth-server.com/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
      subject_token: userToken,
      subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
      resource: serviceURI(targetService),
      scope: requiredScopeFor(targetService),
    }),
  });
 
  const { access_token } = await response.json();
  return access_token;
}

The user's identity flows through the call chain. The CRM sees which user's session triggered the action. The payment processor logs show the originating session. Your compliance team can answer "who authorized this refund?" without guessing.

Trust Chains in Multi-Agent Systems

When an orchestrator spawns subagents, each subagent should receive a scoped token for only its assigned task, not a copy of the orchestrator's full privileges. This is the principle of least privilege applied to agent delegation.

If your CX deployment uses an orchestrator-subagent pattern, token scoping becomes a trust chain problem. The orchestrator has broad access to provision resources and coordinate work. The subagents it spawns should have only the access they need for their specific task.

This is where the orchestrator-subagent pattern meets zero-trust delegation. When the orchestrator spawns a "lookup order history" subagent, that subagent should receive a token with orders:read scope, not the orchestrator's full token. Resource Indicators make this cleaner: you can bind a subagent's token to one MCP server, and it won't work anywhere else. That guarantee is only as good as the audience check on the receiving end, though, which is the MUST teams skip most often.

scoped-subagent-token.ts·typescript
async function issueSubagentToken(
  orchestratorToken: string,
  subagentTask: "order_lookup" | "refund_processing" | "appointment_booking"
): Promise<string> {
  const taskScopes: Record<string, string[]> = {
    order_lookup: ["orders:read"],
    refund_processing: ["payments:write", "orders:read"],
    appointment_booking: ["calendar:write"],
  };
 
  // Thin wrapper over the RFC 8693 exchange shown above
  return await tokenExchange({
    parentToken: orchestratorToken,
    scopes: taskScopes[subagentTask],
    // Scope token to the specific MCP server this subagent will call
    resource: mcpServerForTask(subagentTask),
  });
}

Short-lived tokens for subagents shrink the blast radius when one is compromised. A five-minute token for a lookup subagent can't be replayed after the task finishes.

One thing to get right: the caller doesn't choose that lifetime. RFC 8693 has no parameter for requesting a token TTL, so a short expiry is a policy you configure on the authorization server for that client and scope pair. Plenty of teams write the number into the request, watch it get ignored, and assume they have five-minute tokens when they're holding hour-long ones.

What Will Compliance Teams Ask?

Compliance teams want three things from your MCP auth setup: attributable access logs (who accessed what and when), documented access controls (who is allowed to do what), and evidence that those controls are actually enforced, not just documented. OAuth 2.1 with proper scope logging satisfies all three.

For HIPAA-covered deployments, every MCP tool call that accesses protected health information needs an audit log entry attributable to a specific caller identity. OAuth 2.1 makes this straightforward: log the sub claim from the token alongside the tool name, parameters, and timestamp. Your compliance team can then pull a full access log by patient ID. A log nobody reads is just storage, so pair it with monitoring and alerting on the tool calls that touch regulated data.

SOC 2 Type II auditors will check whether access to internal systems through your MCP servers is appropriately gated. If your MCP server connects to a database that holds customer PII and there's no access control, that's a finding. The gate doesn't have to be OAuth; it just has to be documented and enforced. OAuth 2.1 is the path of least resistance for satisfying this.

PCI-DSS applies if any of your MCP tools touch cardholder data. In that case, scope restrictions are not optional. Only agents and users with demonstrated need-to-access should have the relevant scopes, and access should be logged and reviewable.

The Chanl MCP integration logs every tool call with the caller identity, parameters, and response, which gives your compliance team the audit trail without requiring each downstream system to implement its own logging.

Where to Start Now That the Spec Has Shipped

The spec is out, your servers didn't change overnight, and nothing broke. That's the trap. Compliant clients will start expecting discovery metadata long before anything hard-fails, so the work is exactly what it was in June, minus the deadline that would have made you do it.

Start with an inventory. List every MCP server in production, what systems it connects to, and whether it has inbound auth at all. Anything reachable over the network without a token is your short list.

Then prioritize by blast radius, not by ease. Servers touching customer PII, financial data, or write paths into production go first. Internal knowledge bases and read-only sources can wait a sprint.

Third, add the Protected Resource Metadata endpoint. This is the one thing the spec squarely requires of servers, and it's also the cheapest item on the list. Expose /.well-known/oauth-protected-resource, point it at your authorization server, and compliant clients can find their way in without you shipping bespoke config to each of them.

For MCP tool management at scale, the inventory step is easier when your tools are centrally registered. If your tool catalog lives in a single place, you can audit auth coverage in one pass rather than hunting through individual server configs.

The spec raises the floor. It doesn't set a ceiling. Most of the patterns above are ordinary OAuth 2.1 and have been available for years, which is the uncomfortable part: nothing was stopping anyone from doing this a year ago.

The agent in the opening scenario had 12 MCP servers, and four turned out to be unprotected, because a researcher went looking before anyone on the team did. Run the inventory yourself this week and you get to be the person who finds them.

Audit your MCP tool coverage and access controls

Chanl's tool management gives you a central registry of every MCP server and tool your agents can call, with access logs and call history so you can see what's actually being used and who's calling it.

See MCP management
DG

Co-founder

Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.

Learn Agentic AI

Weekly. Patterns for shipping agents that work. MCP, scorecards, regression tests, prompts, model comparisons.

500+ builders subscribed

Frequently Asked Questions