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. The gap exists because MCP was designed for local development first, and most implementations never got hardened when they went to production.
On July 28, 2026, a new MCP specification ships that makes OAuth 2.1 mandatory for remote deployments. If you have MCP servers in production, here's what changes and what you need to do about it before an incident surfaces the gap 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 change doesn't introduce new concepts. OAuth 2.1 has existed for years. The change makes it explicit that remote deployments must use it, and it adds two RFCs so auth discovery and token scoping stop depending on manual configuration.
What Does the July 28 Spec Actually Change?
The core change is formal: MCP servers are now recognized as OAuth 2.1 resource servers. This has three concrete implications.
| Change | RFC | What it requires | Attack it closes |
|---|---|---|---|
| Protected Resource Metadata | RFC 9728 | Server exposes a discovery endpoint naming its auth server and scopes | Clients guessing or hardcoding the wrong auth server |
| Resource Indicators | RFC 8707 | Callers pass a resource parameter so tokens bind to one server | A token for your KB server replayed against your CRM server |
| Client registration | n/a | Clients present a client_id the server can validate | Unknown callers invoking tools anonymously |
Protected Resource Metadata (RFC 9728) requires your MCP server to expose a discovery endpoint at /.well-known/oauth-protected-resource that describes which authorization server issues valid tokens for your server and what scopes exist. Clients can discover this automatically without you hardcoding the auth server URL into every client configuration.
Resource Indicators (RFC 8707) require callers to include a resource parameter in their token requests specifying which MCP server the token is intended for. This prevents token reuse across servers. A token issued for your knowledge base MCP server can't be replayed against your CRM MCP server, even if both use the same auth server.
Client registration requires that MCP clients identify themselves with a client_id when calling servers. Servers can validate this against a registered client list and reject calls from unknown clients.
Together, these changes mean that a valid OAuth token isn't enough. The token has to be issued for the specific server being called, by an auth server the target server trusts, with a registered client identity. This closes the replay and token-reuse attack surfaces that an unvalidated token-per-deployment setup left open.
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.
Plane 1: Inbound auth validates that the caller holds a valid OAuth 2.1 token issued for your specific server. This is what the July spec mandates. Without it, any 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.
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:
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 {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
// Pass validated claims into your tool handlers
return handleToolCall(req, claims);
}The key line is the audience validation. RFC 8707 is what binds a token to a single target resource at issue time; validating the aud claim here is how you enforce that binding on the server side. Without this check, a token issued for a different MCP server on the same auth server would sail right through.
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.
// 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.
OAuth 2.1's token exchange (RFC 8693) supports this pattern. Your agent receives a token representing the user, and when it calls your MCP server, the server exchanges that token for a downstream service credential that carries the user context forward.
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 zero-trust multi-agent delegation pattern applies directly. 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. The Resource Indicators in the July 28 spec make this easier: you can issue a token scoped to a specific MCP server for a specific subagent, and that token literally cannot be used to call other servers.
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"],
};
return await tokenExchange({
parentToken: orchestratorToken,
scopes: taskScopes[subagentTask],
// Scope token to the specific MCP server this subagent will call
resource: mcpServerForTask(subagentTask),
// Short-lived: expires when the subtask completes
ttl: "5m",
});
}Short-lived tokens for subagents reduce the blast radius if a subagent is compromised. A 5-minute token for a lookup subagent can't be replayed after the task completes.
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 pull a full access log by patient ID.
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.
Preparing Before July 28
You have a few weeks. Here's where to spend them.
First, inventory your MCP servers. List every server in production, what systems it connects to, and whether it has inbound auth. Any server accessible over the network without auth needs to be addressed before the spec ships.
Second, prioritize by risk. Servers connecting to systems with customer PII, financial data, or write access to production systems first. Internal knowledge bases and read-only data sources can follow.
Third, add the Protected Resource Metadata endpoint. This is the discovery mechanism the spec requires. Your server needs to expose /.well-known/oauth-protected-resource before compliant clients will trust it.
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 change is about raising the floor, not setting a new ceiling. Most of the patterns above are standard OAuth 2.1 and have been available for years. The July 28 date is an opportunity to catch up on security work that should have happened when these servers went to production.
The agent in the opening scenario had 12 MCP servers and discovered four were unprotected because a researcher found them first. The better outcome is discovering it yourself, during an inventory pass, before anyone else does. That's what this week is for.
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 managementCo-founder
Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.
The Signal Briefing
Un email por semana. Cómo los equipos líderes de CS, ingresos e IA están convirtiendo conversaciones en decisiones. Benchmarks, playbooks y lo que funciona en producción.



