If you have built an MCP server, an A2A endpoint, a contact form API, or any other agent-facing route, you have likely encountered a CORS error. The browser console shows a red message: "Access to fetch at 'https://yourdomain.com/api/mcp' from origin 'https://agent.example.com' has been blocked by CORS policy." Your API works perfectly when called from your own site or from curl, but fails when called from a different domain.
This is the browser's same-origin policy doing its job. It is a security feature, not a bug. But it becomes a problem when you want AI agents, MCP clients, and other services running on different origins to access your endpoints. The solution is CORS headers: a set of HTTP response headers that tell the browser which cross-origin requests to allow.
This guide explains what CORS is, why AI agent endpoints need it, the exact headers you need to set, how to handle preflight OPTIONS requests, complete Next.js implementation patterns, security considerations for wildcard versus specific origins, and common debugging techniques. Use the AgentReady scanner to check your endpoint CORS configuration automatically.
What is CORS?
CORS stands for Cross-Origin Resource Sharing. It is a mechanism built into web browsers that restricts HTTP requests made from scripts running on one origin (domain + port + protocol) to a different origin. Without CORS, any website could make requests to your API and read the responses, which would be a serious security risk.
The same-origin policy
By default, browsers enforce the same-origin policy: JavaScript running on https://example.com can only make requests to https://example.com. A request from https://example.com to https://yourdomain.com/api/mcp is cross-origin and will be blocked unless the server at yourdomain.com explicitly allows it via CORS headers.
Two URLs have the same origin only if they share the same protocol, hostname, and port:
| URL A | URL B | Same Origin? |
|---|---|---|
https://example.com/a |
https://example.com/b |
Yes |
https://example.com |
http://example.com |
No (different protocol) |
https://example.com |
https://api.example.com |
No (different hostname) |
https://example.com |
https://example.com:8080 |
No (different port) |
Why AI agent endpoints need CORS
AI agents interact with your endpoints from various origins:
- Browser-based AI agents (Chrome AI, browser extensions) run in a browser context on a different origin than your API.
- MCP clients may be web applications running on their own domain that call your MCP server endpoints.
- A2A protocol requests come from other agents running on different domains, and some may originate from browser contexts.
- Web-based agent platforms (custom GPTs, agent builders) make requests from their own domains to your API.
- Development tools running on
localhostneed to call your production API during testing.
Without CORS headers, all of these requests fail in browser contexts. Server-to-server requests (like a Python script calling your API) bypass CORS entirely because CORS is a browser-only mechanism. But since you cannot control whether an agent operates in a browser or server context, you should always include CORS headers on your agent-facing endpoints.
The CORS headers explained
There are six CORS headers. You will use three of them on every endpoint, and the others for specific scenarios.
Access-Control-Allow-Origin
This is the most important CORS header. It tells the browser which origins are allowed to read the response.
Access-Control-Allow-Origin: * # Any origin
Access-Control-Allow-Origin: https://example.com # Specific origin
Wildcard (*) allows any origin. Use this for public, read-only endpoints where there is no sensitive data and no authentication. Good candidates: GET /api/ai/context, GET /api/mcp (server info), public search endpoints.
Specific origin allows only the named origin. You can only specify one origin per response. To allow multiple specific origins, you must dynamically check the Origin request header against an allowlist and echo back the matching origin.
Access-Control-Allow-Methods
Lists the HTTP methods the endpoint accepts from cross-origin requests:
Access-Control-Allow-Methods: GET, POST, OPTIONS
For most agent endpoints, you need GET (reading data), POST (submitting data, executing tools), and OPTIONS (preflight requests). Include only the methods your endpoint actually supports.
Access-Control-Allow-Headers
Lists the HTTP request headers that cross-origin requests can include:
Access-Control-Allow-Headers: Content-Type, Authorization
Content-Type is needed for JSON requests (application/json). Authorization is needed for Bearer token authentication. If your endpoint uses custom headers, list them here too.
Access-Control-Max-Age
How long (in seconds) the browser can cache the preflight response. This avoids sending an OPTIONS request before every single API call:
Access-Control-Max-Age: 86400 # Cache for 24 hours
Access-Control-Allow-Credentials
Required if your endpoint uses cookies or HTTP authentication. When set to true, you cannot use a wildcard for Access-Control-Allow-Origin; you must specify the exact origin:
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers
By default, cross-origin responses only expose a limited set of headers to JavaScript. If your response includes custom headers that the client needs to read, list them here:
Access-Control-Expose-Headers: X-Request-Id, X-RateLimit-Remaining
Preflight requests: OPTIONS handling
The browser sends a preflight request (an HTTP OPTIONS request) before the actual request when certain conditions are met. This is the most common source of CORS errors because developers forget to handle the OPTIONS method.
When does preflight happen?
The browser sends a preflight OPTIONS request when the actual request:
- Uses a method other than GET, HEAD, or POST
- Uses POST with a Content-Type other than
application/x-www-form-urlencoded,multipart/form-data, ortext/plain - Includes custom headers (like
Authorization)
Since AI agent requests almost always use Content-Type: application/json and often include an Authorization header, preflight requests will happen on virtually every agent endpoint. You must handle them.
The preflight flow
- The browser sends an
OPTIONSrequest withAccess-Control-Request-MethodandAccess-Control-Request-Headersheaders. - Your server responds with CORS headers and a 200 or 204 status code.
- The browser checks the CORS headers. If they allow the actual request, it proceeds.
- The actual request (GET, POST, etc.) is sent with the real payload.
- Your server processes the request and includes CORS headers in the response.
Next.js API route CORS patterns
Here are the patterns you need for Next.js App Router API routes. These cover the most common agent endpoint scenarios.
Pattern 1: Public endpoint with wildcard CORS
Use this for read-only, public endpoints like /api/ai/context or GET /api/mcp:
// app/api/ai/context/route.ts
import { NextResponse } from "next/server";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
export async function OPTIONS() {
return NextResponse.json({}, { headers: corsHeaders });
}
export async function GET() {
const context = {
name: "Your Product",
description: "What your product does",
services: ["Service A", "Service B"],
contact: "hello@yourdomain.com",
};
return NextResponse.json(context, { headers: corsHeaders });
}
Pattern 2: Authenticated endpoint with origin allowlist
Use this for endpoints that accept writes or use authentication, like your MCP server's POST handler:
// app/api/mcp/route.ts
import { NextRequest, NextResponse } from "next/server";
const ALLOWED_ORIGINS = [
"https://yourdomain.com",
"https://app.yourdomain.com",
"http://localhost:3000",
"http://localhost:3001",
];
function getCorsHeaders(origin: string | null) {
const allowedOrigin = origin && ALLOWED_ORIGINS.includes(origin)
? origin
: ALLOWED_ORIGINS[0];
return {
"Access-Control-Allow-Origin": allowedOrigin,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "86400",
};
}
export async function OPTIONS(req: NextRequest) {
const origin = req.headers.get("origin");
return NextResponse.json({}, { headers: getCorsHeaders(origin) });
}
export async function GET(req: NextRequest) {
const origin = req.headers.get("origin");
// Return server info (public)
return NextResponse.json(
{ name: "Your MCP Server", version: "1.0" },
{ headers: getCorsHeaders(origin) }
);
}
export async function POST(req: NextRequest) {
const origin = req.headers.get("origin");
const headers = getCorsHeaders(origin);
// Verify auth token
const authHeader = req.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401, headers }
);
}
// Process the MCP request...
const body = await req.json();
return NextResponse.json(
{ result: "processed" },
{ headers }
);
}
Pattern 3: A2A endpoint with open CORS
The A2A (Agent-to-Agent) protocol uses JSON-RPC 2.0 and needs to accept requests from any agent. Since A2A requests may come from both browser and server contexts, use a permissive CORS policy. Authentication is handled by Bearer tokens, not origin restrictions:
// app/api/agent/route.ts
import { NextRequest, NextResponse } from "next/server";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "86400",
};
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: corsHeaders,
});
}
export async function GET() {
// Return AgentCard
const agentCard = {
name: "Your Agent",
description: "What your agent does",
url: "https://yourdomain.com/api/agent",
version: "1.0.0",
capabilities: { streaming: false },
authentication: { schemes: ["bearer"] },
skills: [],
};
return NextResponse.json(agentCard, { headers: corsHeaders });
}
export async function POST(req: NextRequest) {
const body = await req.json();
// Process JSON-RPC 2.0 request
if (body.method === "tasks/send") {
const taskText = body.params?.message?.parts?.[0]?.text || "";
return NextResponse.json({
jsonrpc: "2.0",
id: body.id,
result: {
id: body.params?.id,
status: { state: "completed" },
artifacts: [{
parts: [{ type: "text", text: "Response here" }],
}],
},
}, { headers: corsHeaders });
}
return NextResponse.json(
{ jsonrpc: "2.0", id: body.id, error: { code: -32601, message: "Method not found" } },
{ headers: corsHeaders }
);
}
Security: wildcards versus specific origins
The decision between Access-Control-Allow-Origin: * and a specific origin depends on what the endpoint does and how it is authenticated.
When wildcards are safe
- Public read-only endpoints:
GET /api/ai/context,GET /api/mcp(server info). The data is public anyway. - Endpoints with Bearer token auth: If every request must include a valid
Authorization: Bearer <token>header, the token is the security boundary, not the origin. Note that you cannot use wildcards withAccess-Control-Allow-Credentials: true, but Bearer tokens sent via headers (not cookies) do not require credentials mode. - A2A endpoints: The whole point is to accept requests from any agent. Authentication is via Bearer tokens.
When to use an allowlist
- Cookie-based authentication: If your endpoint uses session cookies, you must use specific origins and set
Access-Control-Allow-Credentials: true. - Sensitive write operations: If an endpoint performs destructive actions (deleting data, making payments) and relies on ambient authority (cookies, IP-based auth), restrict origins.
- Admin endpoints: Internal tools and admin APIs should restrict origins to your own domains.
Dynamic origin validation
For endpoints that need specific origin validation but may receive requests from multiple known origins:
function getAllowedOrigin(requestOrigin: string | null): string {
const allowed = [
"https://yourdomain.com",
"https://app.yourdomain.com",
"http://localhost:3000",
];
if (requestOrigin && allowed.includes(requestOrigin)) {
return requestOrigin;
}
// Return the primary origin as default (this will block
// requests from non-allowed origins)
return allowed[0];
}
Global CORS configuration in next.config.js
For simple CORS headers that apply to all API routes, you can configure them globally in next.config.js. This avoids repeating CORS header code in every route file:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
// Apply to all API routes
source: "/api/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "*",
},
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
];
},
};
module.exports = nextConfig;
This handles simple CORS (adding headers to responses) but does not handle preflight OPTIONS requests. You still need an OPTIONS handler in each API route that receives non-simple requests. The next.config.js approach is best combined with per-route OPTIONS handlers for complete coverage.
MCP endpoint CORS configuration
MCP (Model Context Protocol) endpoints have specific CORS requirements depending on the client type. See the MCP Server Guide for the full implementation.
MCP server info (GET)
The GET handler returns server metadata and is public. Use wildcard CORS:
export async function GET() {
return NextResponse.json(
{
name: "your-mcp-server",
version: "1.0.0",
description: "Your MCP server description",
tools: [/* tool list */],
},
{
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
}
);
}
MCP tool execution (POST)
The POST handler executes tools and typically requires authentication. Use wildcard CORS since security is handled by the Bearer token, not origin restrictions. This is necessary because MCP clients run on diverse origins:
export async function POST(req: NextRequest) {
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
// Validate Bearer token
const auth = req.headers.get("authorization");
if (!auth?.startsWith("Bearer ")) {
return NextResponse.json(
{ error: "Missing or invalid authorization" },
{ status: 401, headers: corsHeaders }
);
}
// Process tool call...
return NextResponse.json(result, { headers: corsHeaders });
}
Common CORS errors and how to fix them
Error: "No 'Access-Control-Allow-Origin' header"
Cause: Your response does not include the Access-Control-Allow-Origin header.
Fix: Add the header to your response. Make sure it is included in both the OPTIONS handler and the actual GET/POST handler.
Error: "CORS policy: Response to preflight request doesn't pass"
Cause: Your OPTIONS handler is missing or returns the wrong status code.
Fix: Export an OPTIONS function from your route file that returns 200 or 204 with all CORS headers.
Error: "The value of 'Access-Control-Allow-Origin' must not be the wildcard '*' when credentials mode is 'include'"
Cause: You are using credentials: 'include' in the fetch request but returning Access-Control-Allow-Origin: *.
Fix: Either stop using credentials mode (use Bearer tokens via headers instead) or return the specific requesting origin instead of a wildcard.
Error: "Request header 'Authorization' is not allowed"
Cause: Authorization is not listed in Access-Control-Allow-Headers.
Fix: Add Authorization to the Access-Control-Allow-Headers value.
Error: works in curl but fails in browser
Cause: CORS is browser-only. Curl bypasses it entirely.
Fix: This confirms the issue is CORS, not your API logic. Add the appropriate CORS headers.
Debugging CORS issues
Step 1: Check the network tab
Open browser DevTools, go to the Network tab, and look for the failed request. If there is an OPTIONS request that fails, the problem is in your preflight handler. If the OPTIONS succeeds but the actual request fails, the problem is in your GET/POST handler's response headers.
Step 2: Test with curl
Simulate a preflight request to verify your OPTIONS handler works:
# Test OPTIONS preflight
curl -X OPTIONS https://yourdomain.com/api/mcp \
-H "Origin: https://agent.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-v 2>&1 | grep -i "access-control"
You should see your CORS headers in the response. If not, your OPTIONS handler is not working correctly.
Step 3: Test the actual request
# Test POST with Origin header
curl -X POST https://yourdomain.com/api/mcp \
-H "Origin: https://agent.example.com" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-token" \
-d '{"method": "tools/list"}' \
-v 2>&1 | grep -i "access-control"
Step 4: Check header casing
HTTP headers are case-insensitive, but some proxy configurations can strip headers based on casing. Use the exact casing shown in the examples: Access-Control-Allow-Origin, not access-control-allow-origin.
CORS and middleware
In Next.js, you can also set CORS headers in middleware. This is useful if you want to apply CORS to all API routes without modifying each one. However, middleware in Next.js runs on the Edge runtime, which has some limitations:
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(req: NextRequest) {
// Only apply CORS to API routes
if (!req.nextUrl.pathname.startsWith("/api/")) {
return NextResponse.next();
}
// Handle preflight
if (req.method === "OPTIONS") {
return new NextResponse(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "86400",
},
});
}
// Add CORS headers to the response
const response = NextResponse.next();
response.headers.set("Access-Control-Allow-Origin", "*");
response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
return response;
}
export const config = {
matcher: "/api/:path*",
};
Note: This middleware approach applies the same CORS policy to all API routes. If you need different policies for different endpoints (stricter for admin, permissive for public), use per-route handlers instead.
CORS for contact form endpoints
If your contact form API receives cross-origin requests from AI agents, it needs CORS headers. A contact form is a write endpoint (POST), but it typically does not require authentication (anyone should be able to submit an inquiry). Use wildcard CORS with validation on the input data rather than the origin:
// app/api/contact/route.ts
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
export async function OPTIONS() {
return NextResponse.json({}, { headers: corsHeaders });
}
export async function POST(req: NextRequest) {
// Input validation is your security layer, not CORS
const body = await req.json();
// ... validate, process, respond
return NextResponse.json({ success: true }, { headers: corsHeaders });
}
Testing CORS in development
During development, your Next.js app runs on http://localhost:3000. If you are testing cross-origin requests, you need a second origin. Options:
- Run a simple HTML page on a different port:
npx serve -p 3001serves static files on port 3001, creating a different origin. - Use the browser console: Open any website and run
fetch('http://localhost:3000/api/your-endpoint')from the console. This creates a cross-origin request. - Use a CORS testing tool: Websites like test-cors.org let you test CORS against any endpoint.
Always include http://localhost:3000 and http://localhost:3001 in your allowed origins during development. Remove them or gate them behind an environment variable check for production.
Frequently Asked Questions
What is CORS and why do AI agent endpoints need it?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that blocks requests from one domain to another unless the server explicitly allows it via response headers. AI agent endpoints need CORS headers because browser-based agents, A2A protocol requests, and MCP clients may call your API from a different origin than your website. Without proper CORS configuration, these requests fail silently in the browser.
Should I use a wildcard (*) for Access-Control-Allow-Origin?
For public, read-only endpoints like /api/ai/context or GET /api/mcp, a wildcard is appropriate and safe. For endpoints that accept writes or use cookie-based authentication, avoid wildcards. Instead, validate the Origin request header against an allowlist and echo back the specific origin. Bearer token authentication works fine with wildcards since the token itself is the security boundary.
What is a preflight request and when does it happen?
A preflight request is an HTTP OPTIONS request the browser sends before the actual request to check if the server allows cross-origin access. It happens when the request uses methods other than GET/HEAD/simple POST, includes custom headers like Authorization, or uses Content-Type: application/json. Your server must respond to OPTIONS with the correct CORS headers and a 200 or 204 status code.
How do I configure CORS in Next.js API routes?
Export an OPTIONS handler that returns CORS headers, and include the same headers in your GET/POST responses. Define the headers once as a constant object containing Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers, then spread them into every NextResponse.
Do server-to-server requests need CORS?
No. CORS is enforced by browsers only. Server-to-server requests (like a Python agent calling your API, or a Node.js MCP client) bypass CORS entirely. However, you should still include CORS headers on your agent endpoints because some agent requests originate from browser contexts, and the headers do not affect server-to-server calls.
What CORS headers does the A2A protocol require?
A2A (Agent-to-Agent) endpoints should allow POST and OPTIONS methods, accept Content-Type and Authorization headers, and set Access-Control-Allow-Origin: * since agents run on diverse origins. Since A2A uses JSON-RPC 2.0, the Content-Type will always be application/json, which triggers preflight requests that your OPTIONS handler must address.
How do I debug CORS errors?
Open browser DevTools Network tab and look for a failed OPTIONS request or a blocked response. Check: (1) Is your OPTIONS handler returning 200 or 204 with CORS headers? (2) Does Access-Control-Allow-Origin match the requesting origin? (3) Are all required methods and headers listed in the allow headers? (4) Test with curl to confirm your API works outside the browser context.
Can I set CORS headers globally in Next.js instead of per-route?
Yes. Add a headers() config to next.config.js with a source pattern matching your API routes. This applies CORS headers to all matching routes without repeating code. However, this only handles simple CORS. Preflight OPTIONS requests still need explicit handler functions in each API route that receives non-simple requests (JSON content type, Authorization headers).
By Paul Gosnell