---
title: "MCP in Next.js"
description: "Integrating MCP servers with Next.js - both dev tooling and production domain servers"
url: "https://agentsurface.dev/docs/mcp-servers/nextjs-integration"
lastVerified: 2026-09-24
lastModified: 2026-09-25T05:46:01.000Z
---



## Summary [#summary]

Two distinct Next.js-MCP patterns: next-devtools-mcp (dev environment tooling) and mcp-handler (production domain servers). The first exposes your dev environment to Claude Desktop and Cursor via .mcp.json discovery. The second builds domain-specific MCP servers hosted inside a Next.js app. Both are useful but serve different purposes.

* next-devtools-mcp: local server, discovers the dev server's own `/_next/mcp` endpoint for errors, routes, and logs
* Setup: .mcp.json discovery in project root
* mcp-handler: production servers, domain-specific capabilities
* Use .mcp.json for local development; deploy domain servers separately
* Auto-discovery via .mcp.json works with Claude Desktop and Cursor

Next.js and MCP intersect in two completely different ways that are easy to conflate. The first is `next-devtools-mcp`, a tool that exposes your Next.js development environment to AI assistants. The second is building your own domain-specific MCP server hosted inside a Next.js application using `mcp-handler`. Both are useful, but they serve entirely different purposes.

The patterns below target Next.js 16 (App Router, Turbopack as the default bundler, Node 20+ floor) - the version this documentation site itself runs on.

## next-devtools-mcp: Dev Environment Tooling [#next-devtools-mcp-dev-environment-tooling]

`next-devtools-mcp` is a local MCP server that exposes your Next.js dev environment to Claude Desktop, Cursor, and other MCP-capable editors. It runs as a subprocess on your development machine alongside `next dev`. Agents connected to it can inspect errors, read routes, and query your Next.js application state without leaving the conversation.

**Setup:** Create a `.mcp.json` file in your project root:

```json
{
  "mcpServers": {
    "next-devtools": {
      "command": "npx",
      "args": ["-y", "next-devtools-mcp@latest"]
    }
  }
}
```

Next.js 16+ enables its own MCP endpoint by default at `http://localhost:3000/_next/mcp`. `next-devtools-mcp` scans common ports, discovers that endpoint on your running dev server, and connects automatically - no port flag or environment variable needed. Clients that support `.mcp.json` discovery (Claude Desktop, Cursor) will pick this up automatically when the project is opened.

**Tools exposed by next-devtools-mcp:**

| Tool           | Description                                                                                                                                                             |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nextjs_index` | Scans common ports for running Next.js 16+ dev servers and lists each one's runtime tools                                                                               |
| `nextjs_call`  | Invokes a runtime tool (`get_errors`, `get_logs`, `get_page_metadata`, `get_project_metadata`, `get_server_action_by_id`) on a discovered server, by port and tool name |
| `nextjs_docs`  | Points the agent at the version-matched docs bundled in `node_modules/next/dist/docs/`                                                                                  |
| `browser_eval` | Points the agent at the agent-browser CLI for browser automation                                                                                                        |

These tools are intentionally development-only. `next-devtools-mcp` does not run in production and does not expose any of your application's business logic or data.

## Building a Domain MCP Server with mcp-handler [#building-a-domain-mcp-server-with-mcp-handler]

If you want to expose your application's own capabilities through MCP - letting agents call your business logic, read your data, or trigger your workflows - you build a domain MCP server hosted inside your Next.js application.

Install the adapter:

```bash
pnpm add mcp-handler
```

**Basic route handler** in `app/mcp/route.ts`:

```typescript
import { createMcpHandler } from "mcp-handler";
import { z } from "zod";

const handler = createMcpHandler((server) => {
  server.tool(
    "get_project_status",
    "Returns the current status of a project by its slug.",
    {
      project_slug: z.string().describe("The URL slug of the project to look up"),
    },
    async ({ project_slug }) => {
      const project = await db.projects.findBySlug(project_slug);

      if (!project) {
        return {
          isError: true,
          content: [{ type: "text", text: `Project '${project_slug}' not found.` }],
        };
      }

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify({
              id: project.id,
              name: project.name,
              status: project.status,
              updated_at: project.updatedAt,
            }),
          },
        ],
      };
    },
  );
});

export const maxDuration = 60;

export { handler as GET, handler as POST, handler as DELETE };
```

`mcp-handler` exposes a single `/mcp` endpoint - `POST` for requests, `GET` for the server-to-client stream where the server supports one. See [Transport Selection](./transports) for how the underlying route works.

**Connecting clients:** Point your MCP client at `https://your-app.vercel.app/mcp`.

## Organizing a Larger Domain Server [#organizing-a-larger-domain-server]

For servers with many tools, keep the route handler thin and move tool registration into a factory function - the same pattern described in [Server Architecture](./architecture):

```typescript
// lib/mcp/server.ts
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerProjectTools } from "./tools/projects.js";
import { registerDeploymentTools } from "./tools/deployments.js";
import { registerTeamTools } from "./tools/teams.js";

export interface McpContext {
  userId: string;
  orgId: string;
  scopes: string[];
}

export function registerTools(server: McpServer, ctx: McpContext): void {
  registerProjectTools(server, ctx);
  registerDeploymentTools(server, ctx);
  registerTeamTools(server, ctx);
}
```

```typescript
// app/mcp/route.ts
import { createMcpHandler, withMcpAuth } from "mcp-handler";
import { registerTools } from "@/lib/mcp/server";
import { validateToken } from "@/lib/auth/validate-token";

const mcpHandler = createMcpHandler(
  (server, req) => {
    const ctx = (req as any).mcpAuth;
    registerTools(server, ctx);
  },
  {
    basePath: "/mcp",
    capabilities: {
      tools: {},
    },
  },
);

async function authenticate(token: string) {
  return validateToken(token); // Returns { userId, orgId, scopes }
}

export const maxDuration = 60;

export const { GET, POST, DELETE } = withMcpAuth(mcpHandler, authenticate);
```

The `withMcpAuth` wrapper calls `authenticate` on every request, attaches the result to the request as `req.mcpAuth`, and rejects unauthenticated requests with a 401 before the handler runs.

## Stateless Mode Is Now Protocol-Level [#stateless-mode-is-now-protocol-level]

Next.js on Vercel runs as serverless functions - each invocation is a fresh process, with no shared in-memory state between requests - so the `mcp-handler` package has always defaulted to stateless mode when deployed on Vercel. Under the current `2026-07-28` MCP specification, every MCP deployment target is stateless by protocol: the protocol removes the `initialize` handshake and protocol-level sessions outright (SEP-2567, SEP-2575). A long-running container behind Streamable HTTP is in exactly the same position as a Vercel function - there is no session for either one to hold.

The pattern below - an identifier passed as an ordinary tool argument, with state kept in an external store - is what the spec's own "Stateful Tools" guidance describes as the correct way to carry state across calls now that MCP has no protocol-level session. Build it this way regardless of where the server runs.

For tools that require session state across calls, store state externally:

```typescript
import { createMcpHandler } from "mcp-handler";
import { kv } from "@vercel/kv";

const handler = createMcpHandler((server) => {
  server.tool(
    "continue_analysis",
    "Continues a multi-step analysis started with start_analysis. " +
      "Requires the session_id returned by start_analysis.",
    {
      session_id: z.string().describe("Session ID from start_analysis"),
      next_step: z.enum(["summarize", "compare", "export"]),
    },
    async ({ session_id, next_step }) => {
      const state = await kv.get<AnalysisState>(`analysis:${session_id}`);

      if (!state) {
        return {
          isError: true,
          content: [
            {
              type: "text",
              text: `Session ${session_id} not found or expired. Call start_analysis to begin a new analysis.`,
            },
          ],
        };
      }

      // Process next step using stored state
      const result = await processStep(state, next_step);

      // Update stored state
      await kv.set(`analysis:${session_id}`, result.newState, { ex: 3600 });

      return {
        content: [{ type: "text", text: JSON.stringify(result.output) }],
      };
    },
  );
});
```

## maxDuration and Fluid Compute [#maxduration-and-fluid-compute]

Long-running tools - those that process files, call slow external APIs, or run batch operations - need a `maxDuration` large enough for the operation to complete. Vercel Functions run on Fluid Compute by default, which gives the Hobby plan a 300-second (5-minute) default and maximum.

```typescript
// Route-level configuration
export const maxDuration = 300; // Hobby default and maximum under Fluid Compute
```

Pro and Enterprise default to 300 seconds too, but can raise `maxDuration` up to 800 seconds, or to 1800 seconds (30 minutes) through the extended-duration beta:

```json
{
  "functions": {
    "app/mcp/route.ts": {
      "maxDuration": 800
    }
  }
}
```

Fluid Compute keeps the function instance alive between requests and allows concurrent execution. This is what enables long-lived SSE connections on Vercel without the function timing out between SSE events.

<Callout type="info">
  Durations above 800 seconds need the extended-duration beta and Pro or Enterprise. For tools that
  may run indefinitely, use a streaming response pattern to heartbeat the connection while work
  proceeds in the background, rather than blocking the function for the full duration.
</Callout>

## Environment Variables for MCP in Next.js [#environment-variables-for-mcp-in-nextjs]

MCP route handlers run as server-side code and have access to all server-side environment variables. Do not prefix MCP-related secrets with `NEXT_PUBLIC_` - that exposes them to the browser bundle.

```bash
# .env.local (development)
MCP_AUTH_SECRET=your-bearer-token
DATABASE_URL=postgres://...
STRIPE_API_KEY=sk_test_...

# Never:
NEXT_PUBLIC_MCP_AUTH_SECRET=... # ← This goes into the browser bundle
```

Access them in your route handler via `process.env.VARIABLE_NAME` - they are available without any additional configuration in Next.js server components and route handlers.
