Search for "Next.js MCP server" and you land on two different jobs that share a name. Some people want their coding agent to see inside the Next.js app they are building: the errors on the page, the routes, the build problems. Others want to turn their Next.js app into an MCP server, so Claude or ChatGPT can call tools their site provides. Most pages we read cover one and blur the other, and many still show the old version of the library for the second job.
This guide covers both. We ran every command and code sample below on 14 November 2026, on Next.js 16.4.6, next-devtools-mcp 0.4.0, mcp-handler 2.4.0, @modelcontextprotocol/server 2.4.0 and zod 4.6.7, on macOS. Where the result differed from the docs, we say so.
Which one do you need?
| You want to | Use | Runs | The AI gets |
|---|---|---|---|
| Let Claude Code, Cursor or Codex debug the app you are writing | Built-in /_next/mcp plus next-devtools-mcp |
Your machine, next dev only |
Live errors, routes, logs, compile checks, version-matched docs |
| Give AI apps tools from your product (search, orders, docs) | Your own route with mcp-handler |
Wherever the app is deployed | The tools you write, nothing else |
| Both | Both, they do not conflict |
If you are not sure, you probably want the first. It takes one command and no code.
Part 1: connect your coding agent to a running Next.js app
What Next.js 16 exposes at /_next/mcp
Since Next.js 16, next dev answers MCP requests at /_next/mcp. It is on by default. We sent tools/list to it with curl and got nine tools back:
| Tool | What it returns | Needs |
|---|---|---|
get_errors |
Current errors: next.config validation, browser runtime errors, build errors with source-mapped stacks |
The page open in a browser for runtime errors |
get_logs |
Path to the dev log file with browser console and server output | |
get_page_metadata |
What contributed to the current page render | The page open in a browser |
get_project_metadata |
Project path and dev server URL | |
get_routes |
Every route grouped by appRouter and pagesRouter, dynamic segments as [param] |
|
get_server_action_by_id |
Source file and export name of a Server Action | |
get_compilation_issues |
Resolve errors, missing modules, transform errors across all routes | Turbopack |
compile_route |
Compile one route without requesting it | Turbopack |
get_request_insights |
Recent request timelines, fetches and cache behaviour | experimental.requestInsights |
The server called itself "Next.js MCP Server" version 0.2.0. On our two-page test app, get_routes returned exactly what we had: / and /api/mcp.
The compile tools are the quiet win. An agent that just edited three files can ask the running dev server whether they compile, instead of starting a full next build while the code is half done.
Two limits to know before you rely on it. It exists only in development: we built the same app and ran next start, and /_next/mcp returned 404. And it needs Next.js 16. On 15 there is nothing at that path; npx @next/codemod@latest upgrade latest is the upgrade route the tooling itself suggests.
To turn it off, set experimental.mcpServer to false in next.config.
Install next-devtools-mcp
Your agent does not talk to /_next/mcp directly. The next-devtools-mcp package is a small stdio server that finds running dev servers and forwards calls to them. Since version 0.4.0 it is a thin connector with four tools:
nextjs_indexlists the running Next.js dev servers and the tools each one exposes.nextjs_callruns one of those tools on a server, given itsportandtoolName.nextjs_docspoints the agent at the docs that ship inside your installed Next.js, innode_modules/next/dist/docs/, so answers match your version.browser_evalpoints the agent at theagent-browserCLI for checking pages in a real browser.
It needs Node.js 20.19 or newer. Pick your client:
Claude Code
claude mcp add next-devtools npx next-devtools-mcp@latest
Codex
codex mcp add next-devtools -- npx next-devtools-mcp@latest
On Windows 11, Codex also needs env = { SystemRoot="C:\\Windows", PROGRAMFILES="C:\\Program Files" } and startup_timeout_ms = 20_000 in .codex/config.toml, according to the README.
Gemini CLI
gemini mcp add next-devtools npx next-devtools-mcp@latest
Add -s user to install it for every project instead of the current one.
VS Code with Copilot
code --add-mcp '{"name":"next-devtools","command":"npx","args":["-y","next-devtools-mcp@latest"]}'
Cursor, or any client that reads a JSON file
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": ["-y", "next-devtools-mcp@latest"]
}
}
}
Put it in .mcp.json at the project root to share it with the team through git, or in ~/.cursor/mcp.json for Cursor alone.
All agents at once
npx add-mcp next-devtools-mcp@latest
add-mcp writes the entry for every coding agent it finds on the machine.
Check that it works
Start the dev server first. Then open the page you are working on in the browser, because get_errors and get_page_metadata read from live browser sessions. Now ask the agent something only the running app knows:
- "What errors does the app have right now?"
- "List the routes and tell me which ones are dynamic."
- "Does
/blog/[slug]compile?"
In Claude Code you will see calls like nextjs_index and then nextjs_call with toolName: "get_errors". If the agent answers from reading your files instead, it has not connected.
You can also check the endpoint without any agent:
curl -s -X POST http://localhost:3000/_next/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
A list of tools means Next.js is fine and any problem is between the agent and next-devtools-mcp.
What we hit: dev servers on other ports
Our test app ran on port 3417, because port 3000 was taken. nextjs_index did not find it. It found a different project that happened to be running on 3000.
Reading the package source explained why. On macOS and Linux, next-devtools-mcp 0.4.0 first looks for processes named exactly next-server. On our Mac the process is called next-server (v16.4.6), so that match failed, and the package fell back to probing ports 3000 to 3010. A dev server on any other port is invisible to it.
The fix is simple. nextjs_call takes the port directly, and it worked on 3417 as soon as we passed it:
{ "port": 3417, "toolName": "get_routes" }
So if you run more than one project, or use a custom port, tell the agent once: "the dev server is on port 3417". It will call the tool with that port. Keeping projects inside 3000 to 3010 also avoids the problem.
Part 2: build your own MCP server inside a Next.js app
This is the other meaning: your Next.js app becomes something Claude, Cursor or ChatGPT can connect to by URL. Typical tools are search over your content, a lookup in your database, or an action like creating a support ticket.
Vercel's mcp-handler does the protocol work. It turns a server definition into a plain (Request) => Response function, which is exactly what a Next.js Route Handler exports.
Version 2 changed a lot
Most tutorials still show version 1, with Redis, an SSE endpoint and a [transport] dynamic route. Version 2 is simpler, and the old snippets do not run on it:
| mcp-handler 1.x | mcp-handler 2.x | |
|---|---|---|
| SDK package | @modelcontextprotocol/sdk 1.x |
@modelcontextprotocol/server 2.x |
| zod | 3 | 4.2 or later |
| Node.js | 20 or later | |
| Transports | Streamable HTTP and SSE | Streamable HTTP only, stateless |
| Redis | Needed for SSE | Not used |
| Routing options | basePath, maxDuration, redisUrl and more |
Removed: mount the handler at the route you want |
If you stay on 1.x, keep @modelcontextprotocol/sdk at 1.26.0 or later. The mcp-handler README flags earlier SDK versions for a security vulnerability.
Install
npm install mcp-handler@^2 @modelcontextprotocol/server@^2 zod@^4
The route
Create app/api/mcp/route.ts. This one gives AI clients a search over blog posts. Swap the array for your database or CMS.
import { createMcpHandler } from "mcp-handler";
import { z } from "zod";
const posts = [
{ slug: "edge-caching", title: "Caching at the edge", body: "How we cache HTML at the edge for 10 minutes." },
{ slug: "image-sizes", title: "Picking image sizes", body: "Three widths cover most layouts." },
];
const handler = createMcpHandler(
(server) => {
server.registerTool(
"search_posts",
{
title: "Search posts",
description: "Find blog posts whose title or body contains the query.",
inputSchema: z.object({ query: z.string().min(2) }),
},
async ({ query }) => {
const q = query.toLowerCase();
const hits = posts.filter((p) => `${p.title} ${p.body}`.toLowerCase().includes(q));
return {
content: [{ type: "text", text: hits.length ? hits.map((p) => `${p.title} (/blog/${p.slug})`).join("\n") : "No posts found." }],
};
},
);
},
{ serverInfo: { name: "my-blog", version: "1.0.0" } },
);
export { handler as GET, handler as POST };
/api/mcp is only a convention. The handler never looks at the path, so app/mcp/route.ts works just as well. Give clients the full URL of whatever route you pick.
Set serverInfo. Without it, our server introduced itself to clients as "mcp-typescript server on vercel", which is what users see in their client's server list.
Test it with curl
With npm run dev running, these three requests are a full round trip. We ran them against the route above:
curl -s -X POST http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
The answer came back as one server-sent event with "serverInfo":{"name":"my-blog","version":"1.0.0"}. Replace the body with {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}} to see search_posts with its JSON Schema, and with this one to run the tool:
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_posts","arguments":{"query":"edge"}}}
We got Caching at the edge (/blog/edge-caching). A plain GET to the route answered 405, which is expected: version 2 is stateless and has no long-lived stream to open.
For a click-through view instead of curl, the MCP Inspector works too: npx @modelcontextprotocol/inspector, then connect to http://localhost:3000/api/mcp with the Streamable HTTP transport.
Connect a client
Clients that speak Streamable HTTP connect by URL.
Claude Code
claude mcp add --transport http my-blog http://localhost:3000/api/mcp
Cursor (~/.cursor/mcp.json)
{
"mcpServers": {
"my-blog": { "url": "http://localhost:3000/api/mcp" }
}
}
VS Code (.vscode/mcp.json)
{
"servers": {
"my-blog": { "type": "http", "url": "http://localhost:3000/api/mcp" }
}
}
Claude Desktop reads remote servers from Settings › Connectors. For a local URL in claude_desktop_config.json, bridge it with mcp-remote:
{
"mcpServers": {
"my-blog": { "command": "npx", "args": ["-y", "mcp-remote", "http://localhost:3000/api/mcp"] }
}
}
After you deploy, swap the localhost URL for the real one, such as https://your-app.vercel.app/api/mcp. Our config builder writes these snippets for 15 clients if you list your server there.
Protect it before you deploy
A deployed route is public. Anyone who finds the URL can call your tools, so add auth before the first deploy, not after.
withMcpAuth wraps the handler. Here is the version we tested, with one shared token from an environment variable:
import { createMcpHandler, withMcpAuth } from "mcp-handler";
// createMcpHandler(...) as above
const verifyToken = async (_req: Request, bearerToken?: string) => {
if (!bearerToken || bearerToken !== process.env.MCP_TOKEN) return undefined;
return { token: bearerToken, clientId: "team", scopes: ["posts:read"] };
};
const authed = withMcpAuth(handler, verifyToken, { required: true });
export { authed as GET, authed as POST };
required: true matters. Without it, a request with no valid token still passes through. With it, our unauthenticated call got:
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer error="invalid_token", error_description="No authorization provided", resource_metadata="http://localhost:3417/.well-known/oauth-protected-resource"
and the same call with Authorization: Bearer <token> got 200.
That resource_metadata link is how OAuth-capable clients find your sign-in. Serve it from app/.well-known/oauth-protected-resource/route.ts:
import { metadataCorsOptionsRequestHandler, protectedResourceHandler } from "mcp-handler";
const handler = protectedResourceHandler({ authServerUrls: ["https://auth.example.com"] });
const corsHandler = metadataCorsOptionsRequestHandler();
export { handler as GET, corsHandler as OPTIONS };
It answered {"resource":"http://localhost:3417","authorization_servers":["https://auth.example.com"]}. Point authServerUrls at your real identity provider. For real users, replace the shared token with verification of that provider's tokens, and check requiredScopes per route. The current spec revision, 2026-07-28, moves client registration from Dynamic Client Registration to Client ID Metadata Documents; that work happens in your authorization server, and your Next.js route does not change.
Inside a tool, the verified identity is on ctx.http?.authInfo, so a tool can filter results by the caller's account.
Problems and fixes
| What you see | Cause | Fix |
|---|---|---|
[error] No server info found from next-devtools-mcp |
No Next.js 16 dev server running, or it is outside ports 3000 to 3010 | Start npm run dev; on another port, give the agent the port for nextjs_call |
ERR_MODULE_NOT_FOUND for next-devtools-mcp/dist |
Stale npx cache | Clear the npx cache and restart the client |
404 on /_next/mcp |
Next.js 15 or older, next start, or experimental.mcpServer: false |
Upgrade, use next dev, or remove the flag |
| Agent answers from files, never calls tools | Client did not load the server | Restart the client and check its MCP list; in Claude Code run /mcp |
405 on GET /api/mcp |
Normal for mcp-handler 2 | Nothing to fix; clients use POST |
401 with a WWW-Authenticate header |
withMcpAuth with required: true and no token |
Send Authorization: Bearer ... or sign in through the client's OAuth flow |
Old tutorial code fails on redisUrl or basePath |
Written for mcp-handler 1 | Remove those options and mount the route where you want it |
| "The module was automatically converted to an EcmaScript module" on a fresh project | npm init added "type": "commonjs" to package.json |
Delete the type field; Next.js handles modules itself |
The last one cost us five minutes on a blank test project. create-next-app does not have the problem.
Next.js MCP servers in our directory
On 14 November 2026 the HostedMCP catalog held 19 active servers with Next.js in the name, and 4 of them passed our index rules. The two that matter for this guide:
| Server | Stars | Checks score | What it is |
|---|---|---|---|
| vercel/next-devtools-mcp | 836 | 89 | The connector from Part 1 |
| vercel-labs/mcp-handler | 699 | 86 | The library from Part 2 |
Star counts come from GitHub on our last refresh; the checks score is our automated review of repository, license, maintenance, supply chain and tool descriptions, with the evidence on each server's checks report. Most of the other 17 are starter templates or single-purpose tools with few stars. Check them the same way before you install one.
Security notes
The dev endpoint is development-only, but it is not private. While next dev runs, any process that can reach the port can read your project path, routes and log location. Do not bind the dev server to a shared network address while it holds secrets in its environment.
Your own route is a public API from the day you deploy it. Treat every tool like an endpoint: validate input with the schema, return only what the caller may see, and never put secrets or internal URLs in tool descriptions, because the model reads those descriptions and so does anyone who lists your tools. Our MCP security guide goes through the risks in order.
Where to go next
If you want the protocol details behind the route, how to build an MCP server builds one in plain TypeScript and Python. When your server is live, submit the repository and HostedMCP will list it with install snippets for each client and run its checks.