MCPNext.jsAnthropicNode.js

Connecting Claude to Your Next.js App via MCP

How to build an MCP server alongside your Next.js app, expose your application data and actions as MCP tools, and connect Claude Desktop and the Claude API to your live app without writing separate API clients.

Deepak Kaushal··10 min read

Once you've built a Next.js app — whether it's a marketplace, a SaaS dashboard, or a content platform — you can expose its capabilities to Claude Desktop, Claude Code, and your own AI features via MCP without writing separate API clients or duplicating your data access layer. This guide shows how to build an MCP server that sits alongside your Next.js app and gives Claude full access to your application's tools and data.

Architecture

The setup has three layers. Your Next.js app handles the UI and business logic through API routes. The MCP server is a separate Node.js process that exposes your app's capabilities as MCP tools — either by calling your API routes or by importing your data access layer directly. Claude Desktop or Claude Code connects to the MCP server via stdio. For production deployments, the MCP server uses HTTP transport so remote clients can connect too.

Project Structure

my-nextjs-app/
  app/
    api/
      listings/route.js      ← Next.js API routes
      bookings/route.js
  lib/
    db.js                    ← shared data access
    schemas.js               ← shared Zod schemas
  mcp/
    index.js                 ← MCP server entry point
    tools/
      listings.js
      bookings.js
  package.json

Shared Zod Schemas

Define input schemas once in lib/schemas.js. Both your Next.js API routes and MCP tool definitions import from here — this keeps validation consistent across both surfaces and prevents the MCP layer from accepting inputs your API would reject.

// lib/schemas.js
import { z } from 'zod';

export const SearchListingsSchema = z.object({
  query: z.string().min(1),
  category: z.enum(['services', 'rentals', 'products']).optional(),
  maxPrice: z.number().positive().optional(),
  location: z.string().optional(),
});

export const CreateBookingSchema = z.object({
  listingId: z.string().uuid(),
  startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  message: z.string().max(500).optional(),
});

Building the MCP Server

The MCP server imports your shared schemas and data access functions. Tool handlers call into the same database layer your Next.js app uses — no duplication. For write operations (creating bookings, sending messages), call your Next.js API routes instead of the database directly to preserve your business logic and audit trail.

// mcp/index.js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { SearchListingsSchema, CreateBookingSchema } from '../lib/schemas.js';
import { searchListings, getListingById } from '../lib/db.js';

const server = new McpServer({
  name: 'my-nextjs-app-mcp',
  version: '1.0.0',
});

// Read: query the database directly
server.tool(
  'search_listings',
  'Search available listings. Returns matching results with ID, title, price, and location.',
  SearchListingsSchema.shape,
  async (args) => {
    const results = await searchListings(SearchListingsSchema.parse(args));
    return {
      content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
    };
  },
);

server.tool(
  'get_listing',
  'Get full details for a listing by its ID.',
  { id: { type: 'string', description: 'Listing UUID' } },
  async ({ id }) => {
    const listing = await getListingById(id);
    return {
      content: [{ type: 'text', text: listing ? JSON.stringify(listing, null, 2) : `Listing ${id} not found` }],
      isError: !listing,
    };
  },
);

// Write: call the Next.js API route to preserve business logic
server.tool(
  'create_booking',
  'Create a new booking for a listing. Validates availability and creates the transaction.',
  CreateBookingSchema.shape,
  async (args) => {
    const body = CreateBookingSchema.parse(args);
    const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/bookings`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
    const data = await res.json();
    return {
      content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
      isError: !res.ok,
    };
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);

Adding a Script to package.json

Add a dedicated script for the MCP server so Claude Desktop can launch it without knowing your project structure.

{
  "scripts": {
    "dev": "next dev",
    "mcp": "node mcp/index.js"
  }
}

Claude Desktop Configuration

Point Claude Desktop at your MCP server script. Pass environment variables for the database URL and app URL so the server can connect to both.

{
  "mcpServers": {
    "my-nextjs-app": {
      "command": "node",
      "args": ["/Users/you/projects/my-nextjs-app/mcp/index.js"],
      "env": {
        "DATABASE_URL": "postgres://localhost:5432/myapp",
        "NEXT_PUBLIC_APP_URL": "http://localhost:3000"
      }
    }
  }
}

Deploying as an HTTP MCP Server

For a deployed app where teammates or clients want to use Claude Desktop against real data, switch to the HTTP transport. Run this as a separate service alongside your Next.js deployment — Railway, Render, and Fly.io work well for this.

// mcp/server-http.js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import { registerTools } from './tools/index.js';

const app = express();
app.use(express.json());

// Simple bearer token auth
app.use((req, res, next) => {
  if (req.headers.authorization !== `Bearer ${process.env.MCP_SECRET}`) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
});

app.all('/mcp', async (req, res) => {
  const server = new McpServer({ name: 'my-nextjs-app-mcp', version: '1.0.0' });
  registerTools(server);

  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => crypto.randomUUID(),
  });
  res.on('close', () => transport.close());
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

app.listen(process.env.PORT || 3001);

Exposing App Resources

Resources let Claude read your app's data as context without invoking a tool. A resource for your listings catalogue means Claude can ask 'read all categories' and get a structured response it can reason over without triggering a search call.

import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';

// Static resource: all categories
server.resource(
  'categories',
  'app://categories',
  { mimeType: 'application/json' },
  async () => {
    const categories = await getCategories();
    return {
      contents: [
        {
          uri: 'app://categories',
          mimeType: 'application/json',
          text: JSON.stringify(categories),
        },
      ],
    };
  },
);

// Dynamic resource: individual listing by ID
server.resource(
  'listing',
  new ResourceTemplate('app://listings/{id}', { list: undefined }),
  { mimeType: 'application/json' },
  async (uri, { id }) => {
    const listing = await getListingById(id);
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: 'application/json',
          text: JSON.stringify(listing),
        },
      ],
    };
  },
);

Using Your MCP Server with the Claude API

For programmatic use — an AI feature inside your Next.js app — connect to your own MCP server from a Route Handler and pass its tools to the Claude API. This means your AI features always use the same tool definitions as your Claude Desktop integration.

// app/api/ai-assist/route.js
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import Anthropic from '@anthropic-ai/sdk';

export async function POST(req) {
  const { message } = await req.json();

  // Connect to your local MCP server
  const mcpClient = new Client({ name: 'nextjs-app', version: '1.0.0' });
  const transport = new StdioClientTransport({
    command: 'node',
    args: ['./mcp/index.js'],
  });
  await mcpClient.connect(transport);

  // Discover tools from MCP
  const { tools: mcpTools } = await mcpClient.listTools();
  const anthropicTools = mcpTools.map((t) => ({
    name: t.name,
    description: t.description,
    input_schema: t.inputSchema,
  }));

  const anthropic = new Anthropic();
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 1024,
    tools: anthropicTools,
    messages: [{ role: 'user', content: message }],
  });

  // Handle tool calls
  for (const block of response.content) {
    if (block.type === 'tool_use') {
      await mcpClient.callTool({ name: block.name, arguments: block.input });
    }
  }

  await mcpClient.close();
  return Response.json({ response: response.content });
}

What to Expose via MCP vs What to Keep in the App

Not every capability should be an MCP tool. Expose tools that benefit from AI reasoning: search and filtering, complex queries, multi-step workflows. Keep direct in the app: simple CRUD with known inputs, payment flows (too risky to expose without user confirmation UX), and anything with irreversible consequences that the app's UI already handles safely. MCP is an AI interface, not a general-purpose API replacement.

Further Reading

Frequently Asked Questions

Why use MCP instead of just calling my API from a Claude tool use integration?

Direct tool use requires you to define the same tools in every application that uses them. MCP defines tools once in a server that any MCP client can discover and call — Claude Desktop, Claude Code, your own app, and any future MCP-compatible client. It also gives you a local dev experience where Claude Desktop can interact with your app without any server deployment.

Do I need to run a separate process for the MCP server alongside my Next.js app?

For the stdio transport (Claude Desktop / Claude Code), yes — the MCP server runs as a separate Node.js process. For the Streamable HTTP transport, you can run the MCP server as a standalone Express app or as a separate route in a Node.js server that proxies to your Next.js app. Next.js Route Handlers can't easily host an MCP server directly because they're stateless edge functions.

How do I authenticate MCP tool calls against my Next.js app?

For local stdio servers, pass credentials via environment variables in the claude_desktop_config.json. For HTTP MCP servers, implement standard HTTP auth — Bearer tokens in the Authorization header. The MCP spec supports an OAuth 2.1 flow for remote server authentication, which is what you'd use for user-specific data access in a deployed app.

Can multiple Claude Desktop users share one MCP HTTP server?

Yes. An HTTP MCP server can handle multiple concurrent clients with session isolation. Each connection gets its own session ID and state. For user-specific data (like 'show me my listings'), implement proper authentication so each session only accesses its own user's data.

Should MCP tools call my Next.js API routes or hit the database directly?

For simple reads, hitting the database directly from the MCP server is fine and faster. For writes and actions, call your Next.js API routes — this ensures your business logic, validation, and audit logging run consistently whether the action comes from the UI or Claude. Never duplicate business logic between the MCP layer and your app.

How do I keep MCP tool definitions in sync with my Next.js API?

Define a shared schema (Zod is ideal) that your Next.js API routes and MCP tools both import. The input validation stays consistent and the MCP tool descriptions remain accurate as your API evolves. If a route changes shape, the shared schema update immediately flags the MCP tool's handler.

What happens when my Next.js app is updated — do I need to restart the MCP server?

For stdio transport, yes — Claude Desktop restarts your MCP server process when it reconnects. For HTTP transport, the MCP server is a separate process and only needs restart if its own code changes. Tool implementations that call your Next.js API routes automatically pick up app updates without an MCP server restart.

More Articles

Need help with this?

I'm available for Sharetribe Flex, Shopify, Next.js, and AI integration projects.

Get In Touch