July 12, 2026 · 9 min read
If you're building with Next.js, adding MCP support means one new route handler. No separate process, no new infrastructure — just a POST /mcp endpoint that speaks JSON-RPC. Any agent that supports HTTP MCP transport — Claude.ai, Cursor, a custom agent — can connect to it directly.
This guide builds a minimal but complete MCP server: tool discovery, tool execution, and the correct response shapes the 2026 MCP spec expects.
A Next.js route handler at /api/mcp that exposes two tools:
get_post(id) — fetch a blog post by IDlist_posts() — list all postsThe pattern generalises to any tools you want to expose: database queries, API calls, search, whatever your product does.
In your Next.js project, create app/api/mcp/route.ts:
// app/api/mcp/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const body = await req.json()
const { method, params, id } = body
switch (method) {
case 'tools/list':
return NextResponse.json(toolsList(id))
case 'tools/call':
return NextResponse.json(await toolsCall(id, params))
default:
return NextResponse.json(methodNotFound(id, method))
}
}That's the router. Three cases: list tools, call a tool, or return an error for anything else.
The tools/list response tells agents what tools are available and how to call them. Each tool needs a name, description, and a JSON Schema for its inputs:
function toolsList(id: number | string) {
return {
jsonrpc: '2.0',
id,
result: {
tools: [
{
name: 'list_posts',
description: 'Return all published blog posts with their IDs and titles.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_post',
description: 'Fetch the full content of a blog post by its ID.',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'The post ID returned by list_posts',
},
},
required: ['id'],
},
},
],
},
}
}The tools/call handler receives the tool name and arguments, executes the logic, and returns a content array. The content array is how MCP returns text — each item has a type and text:
async function toolsCall(id: number | string, params: { name: string; arguments?: Record<string, unknown> }) {
const args = params.arguments ?? {}
try {
let text: string
if (params.name === 'list_posts') {
const posts = await db.post.findMany({
select: { id: true, title: true, publishedAt: true },
orderBy: { publishedAt: 'desc' },
})
text = posts.map(p => `${p.id}: ${p.title} (${p.publishedAt.toISOString().slice(0, 10)})`).join('\n')
} else if (params.name === 'get_post') {
const id = String(args.id).slice(0, 100) // sanitise
const post = await db.post.findUnique({ where: { id } })
if (!post) {
text = `Post not found: ${id}`
} else {
text = `# ${post.title}\n\n${post.content}`
}
} else {
return toolNotFound(id, params.name)
}
return {
jsonrpc: '2.0',
id,
result: { content: [{ type: 'text', text }] },
}
} catch (err) {
return {
jsonrpc: '2.0',
id,
error: { code: -32603, message: 'Internal error', data: String(err) },
}
}
}function methodNotFound(id: number | string, method: string) {
return {
jsonrpc: '2.0',
id,
error: { code: -32601, message: `Method not found: ${method}` },
}
}
function toolNotFound(id: number | string, name: string) {
return {
jsonrpc: '2.0',
id,
error: { code: -32602, message: `Unknown tool: ${name}` },
}
}Add a Link response header so agents and crawlers know your MCP server exists. In next.config.ts:
// next.config.ts
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Link',
value: '</api/mcp>; rel="mcp-server"',
},
],
},
]
},
}
export default nextConfigWith this header, any MCP client that fetches your homepage will discover your server automatically — no manual URL entry needed.
# List tools
curl -X POST http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Call a tool
curl -X POST http://localhost:3000/api/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_posts","arguments":{}}}'Then paste http://localhost:3000/api/mcp (or your deployed URL) into Claude.ai settings → Integrations, and your tools will appear in any Claude conversation.
Authorization: Bearer <token> validation if your tools touch private dataparams.arguments as untrusted user input; validate types and sanitise before any DB queryDon't want to build this yourself?
AgentReady gives any website an instant MCP server — crawling, chunking, embedding, and hosting the endpoint so AI agents can query your docs, pricing, or product pages without you writing any route handlers.
Try it free →