---
title: "How to serve an MCP server card on Next.js"
slug: mcp-server-card-nextjs
published: 2026-10-05T13:16:11.789156+00:00
updated: 2026-10-05T13:16:11.789156+00:00
author: "AIScan Content Team"
author_url: https://aiscan.site/about
category: "AI Readiness"
tags: MCP, server card, Next.js, well-known, check:P2, platform:nextjs
description: "Serve /.well-known/mcp/server-card.json on Next.js 16 with a static file or a route handler, and stop proxy.ts from redirecting the card to your /login page."
url: https://aiscan.site/blog/mcp-server-card-nextjs
---

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](/blog/publish-mcp-server-card) first for the schema question. This post covers only the Next.js mechanics for check P2, which [AIScan](https://aiscan.site/) 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:

```ts
// 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`:

```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`:

```ts
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:

```bash
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:

```bash
curl -si https://yoursite.com/.well-known/mcp/server-card.json
```

Expected output, trimmed:

```text
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](/guides). The [capabilities checks reference](/docs/checks/capabilities) lists P1 to P4 and E2, and the [Next.js platform page](/docs/platforms/nextjs) collects every Next.js guide. If you also want agents to read your pages as Markdown, see [Markdown for AI agents on Next.js](/blog/markdown-for-ai-agents-nextjs).

