Table of contents
Next.js will serve an MCP server card without any plugin, but two things catch people out: where the file lives, and the proxy file that quietly redirects the request to your login page. This guide uses Next.js 16.3.8 with the App Router, and every response below was captured from a real next build and next start.
The card is the JSON document at /.well-known/mcp/server-card.json that tells a crawler or scanner where your MCP server lives. It is still a draft, so read Publish an MCP Server Card So Agents Can Find Your Tools first for the schema question. This post covers only the Next.js mechanics for check P2, which AIScan grades.
Quick summary
| If you want to | Do this | Time |
|---|---|---|
| Serve the card with the least code | Put the JSON in public/.well-known/mcp/server-card.json | 2 min |
| Control headers or build the card from code | Add app/.well-known/mcp/server-card.json/route.ts | 5 min |
| Keep a login redirect off the card | Exclude .well-known in the proxy.ts matcher | 2 min |
| Prove it works | Run npx aiscan-cli yoursite.com and look at check P2 | 1 min |
Pick a route: static file or route handler
Both routes work on Next.js 16.3.8. The difference is what you control.
Option 1: a static file. Create public/.well-known/mcp/server-card.json. Next.js served it with Content-Type: application/json; charset=UTF-8 and Cache-Control: public, max-age=0. You cannot set CORS headers on it without adding a headers() rule in next.config.ts.
Option 2: a route handler. Create the folder app/.well-known/mcp/server-card.json/ and put a route.ts in it. A folder name that starts with a dot and ends in .json is legal, and next build listed it as ○ /.well-known/mcp/server-card.json, a static route:
// app/.well-known/mcp/server-card.json/route.ts
export const dynamic = "force-static";
const card = {
serverInfo: { name: "example-mcp", version: "1.0.0" },
description: "Search the example catalog",
transport: { type: "streamable-http", url: "https://example.com/mcp" },
tools: [{ name: "search", description: "Search the catalog" }],
};
const headers = {
"Access-Control-Allow-Origin": "*",
"Cache-Control": "public, max-age=3600",
};
export function GET() {
return Response.json(card, { headers });
}
export function OPTIONS() {
return new Response(null, { status: 204, headers });
}
Replace the example values with your real server name, endpoint and tools. The field names above follow the serverInfo and transport shape that AIScan's own P2 guidance shows. They are a convention, not a ratified schema.
The OPTIONS export matters for browser-based clients. Without it, a preflight request to the route returned 405 Method Not Allowed with Allow: GET, HEAD.
Route handlers set Content-Type: application/json for you when you return Response.json(). A hand-built new Response(JSON.stringify(card)) would not, so use Response.json().
Keep the proxy off the card
Next.js 16 renamed the middleware.ts file to proxy.ts. If yours redirects visitors who have no session cookie, it also redirects the card, because a typical matcher catches every path except _next/static and a few assets. I tested this with a six-line proxy.ts:
// proxy.ts (the broken version)
import { NextResponse, type NextRequest } from "next/server";
export function proxy(req: NextRequest) {
if (!req.cookies.get("session")) return NextResponse.redirect(new URL("/login", req.url));
return NextResponse.next();
}
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };
With that file in place, both the route handler and the static file answered 307 Temporary Redirect with location: /login. A scanner following the redirect lands on an HTML page, which is not a card.
Fix: add .well-known to the negative lookahead in matcher:
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.well-known).*)"],
};
After a rebuild the card returned 200 with content-type: application/json, while / still redirected to /login. The .well-known exclusion also covers /.well-known/mcp-registry-auth, the one-line domain-verification file the sibling guide describes.
Verify the card
Run AIScan first. From any terminal:
npx aiscan-cli yoursite.com
Find check P2 · MCP Server Card in the Capabilities group. It should show as passing. If it reports a 404 or a non-JSON response, work through the errors below.
By hand, the equivalent is one request:
curl -si https://yoursite.com/.well-known/mcp/server-card.json
Expected output, trimmed:
HTTP/2 200
content-type: application/json
access-control-allow-origin: *
cache-control: public, max-age=3600
{"serverInfo":{"name":"example-mcp","version":"1.0.0"},"description":"Search the example catalog", ...}
Two more behaviours I measured. A request to /.well-known/mcp/server-card.json/ with a trailing slash returned 308 Permanent Redirect back to the path without it, so link to the version without the slash. And /.well-known/mcp/nope.json returned a real 404, so a missing card will not pass as a soft 200.
When the card still fails
Redirect to a login page. The proxy.ts matcher above. Check with curl -si and look for a location: header.
404 on the file. The folder must sit directly under app/ (or public/) and be spelled .well-known. A well-known folder without the dot serves /well-known/....
Deployed host rewrites or blocks dot paths. Some CDNs and reverse proxies drop paths that begin with a dot. If next start serves the card and production does not, the proxy in front of Next.js is the cause.
Where to go next
Run npx aiscan-cli yoursite.com, confirm check P2, then browse the other fixes in the AIScan guides. The capabilities checks reference lists P1 to P4 and E2, and the Next.js platform page collects every Next.js guide. If you also want agents to read your pages as Markdown, see Markdown for AI agents on Next.js.
Frequently asked questions
Where does the MCP server card go in a Next.js project?
Either public/.well-known/mcp/server-card.json for a static file, or app/.well-known/mcp/server-card.json/route.ts for a route handler. Both are served at /.well-known/mcp/server-card.json on Next.js 16.3.8.
Does Next.js allow a folder named .well-known in the app directory?
Yes. next build listed /.well-known/mcp/server-card.json as a static route. Only folders starting with an underscore are treated as private.
Why does my server card redirect to /login?
A proxy.ts (formerly middleware.ts) matcher that covers every path also covers .well-known. Add .well-known to the negative lookahead in config.matcher, rebuild and curl the card again.
Why does curl show a 307 redirect instead of JSON?
The 307 with a location: /login header is your proxy file redirecting a request that has no session cookie. Exclude .well-known from its matcher.
Why does a browser-based client get a 405 from my card route?
A route handler that exports only GET answers a CORS preflight with 405 and Allow: GET, HEAD. Export an OPTIONS function that returns 204 with the same CORS headers.
Should I use a static file or a route handler?
Use the static file if the card never changes and you need no CORS headers. Use a route handler to set headers or build the card from your own config.
Does the trailing slash matter on the card URL?
Yes. A request with a trailing slash got a 308 redirect to the path without it. Publish and link the version without the slash.
How do I confirm AIScan check P2 passes?
Run npx aiscan-cli yoursite.com and read check P2 in the Capabilities group. By hand, curl -si https://yoursite.com/.well-known/mcp/server-card.json should return 200 with a JSON content type.
Related guides
Platform League Table: WordPress, Next.js, Astro and Shopify Across 728 Sites
Every few weeks someone asks which platform produces the most agentready sites. WordPress, Next.js, Astro and Shopify all have fans, and a ranked table is the answer people expect. We have the data…
How to Publish an OpenAPI File AI Agents Can Find
An agent that wants to call your API has to know what it can ask for. An OpenAPI file answers that in one fetch: every path, every parameter, every response shape, in JSON a program can read.…
Redirect www and http to one canonical host, the way an agent needs it
Type http://www.example.com/pricing into a terminal and count the hops it takes to reach the page. On a clean setup the answer is one, a single 301 straight to the canonical address. On many sites it…
How to return a real 404 with a recoverable body on Next.js
Next.js has a builtin 404 path, and it works until a loading.tsx file sits in the same route segment. Then a missing record returns HTTP 200 with a "not found" page and a noindex tag. Agents that…
