Architecture Overview
Server Runtime
Rendering Pipeline
Client Navigation
Caching and Export
Development Tools
Build and Configuration
Ecosystem Packages
Testing Infrastructure
How It Works
The following files were used as context for generating this wiki page:
The Next.js Model Context Protocol (MCP) tool integration provides a standardized interface for AI agents and external clients to interact directly with a running Next.js development server and build pipeline. By exposing structured tools over HTTP and streamable transport layers, the MCP server allows external agents to inspect project and page metadata, enumerate routes, trigger on-demand route compilation, query compilation diagnostics, and retrieve error states or development logs without requiring manual browser navigation.
The Next.js Model Context Protocol (MCP) server instance is managed via a lazy initialization lifecycle linked directly to the development server's hot reloaders. When experimental MCP server support is enabled via configuration (experimental.mcpServer), the dev server registers HTTP middleware inside both Turbopack and Webpack hot reloaders. This middleware intercepts incoming requests matching the /_next/mcp prefix, connects an isolated streamable transport, and delegates execution to the core MCP server instance.
The lifecycle of the MCP server begins with getOrCreateMcpServer, which guards against duplicate initialization by retaining a module-level mcpServer singleton reference. Upon first invocation, it instantiates an McpServer with the name 'Next.js MCP Server' and version '0.2.0', registering the foundational tools such as get-project-metadata, get-errors, get-page-metadata, get-logs, get-server-action-by-id, and get-routes. Turbopack-specific capabilities like get-compilation-issues and compile-route are conditionally attached when their corresponding options are supplied.
HTTP requests entering the dev server flow through getMcpMiddleware, which performs path filtering, connection cleanup, and request body parsing before handing the payload off to the MCP SDK transport layer.
When an incoming HTTP request hits the Next.js development server, the middleware pipeline executes a precise sequence of checks and operations to handle JSON-RPC messaging.
getMcpMiddleware receives req (IncomingMessage), res (ServerResponse), and next (() => void).pathname starts with /_next/mcp. If it does not match, control immediately falls through by invoking next().getOrCreateMcpServer(options) is called to retrieve or initialize the McpServer instance with its registered tools.StreamableHTTPServerTransport is instantiated with sessionIdGenerator: undefined.'close' listener is bound to the response object (res) to guarantee that transport.close() is invoked if the connection drops prematurely.mcpServer.connect(transport) establishes the session bridge, after which parseBody(req, 1024 * 1024) parses up to 1 megabyte of incoming JSON-RPC payload data.transport.handleRequest(req, res, parsedBody) processes the execution request. If an exception occurs and headers have not yet been sent, a 500 status code with a JSON-RPC formatted error object (code: -32000, message: 'Internal server error') is written to the response.Note
The compile_route tool and compilation issue subscriptions are exclusively available when running under Turbopack (getTurbopackProject). When initializing the MCP middleware inside the Webpack hot reloader, compile_route is intentionally omitted from the server options.
The Model Context Protocol (MCP) server provides tools to discover entry routes across app/ and pages/ directories and trigger targeted on-demand route compilation without executing live HTTP requests. These capabilities allow clients to inspect application structure, warm module graphs, measure build latencies, and perform memory benchmarking.
get_routesThe get_routes tool scans the project filesystem directly to locate all route files in the App Router and Pages Router directories, returning them grouped by router type.
When invoked, the tool records telemetry via mcpTelemetryTracker.recordToolCall('mcp/get_routes'), validates that at least one directory exists, and independently executes discoverRoutes for both App and Pages routers in parallel to ensure a failure in one router does not block the other.
Note
Dynamic route segments are returned as defined in the filesystem (e.g., [id], [slug], [...slug]). The get_routes tool does not expand generateStaticParams or dynamic parameters.
compile_routeThe compile_route tool triggers on-demand compilation through the development server handler, simulating the code path executed when a user first visits a route.
Execution follows a strict validation and error-handling flow:
mcpTelemetryTracker.recordToolCall('mcp/compile_route') logs the tool invocation.routeSpecifier or path must be provided, returning an error response if both or neither are supplied.compileRoute({ routeSpecifier, path }) is awaited. On success, it returns { routeSpecifier, issues }.ENOENT error returns { notFound: true, input }, while general compilation failures return the serialized error message.Warning
Providing both routeSpecifier and path, or omitting both parameters entirely, results in an immediate isError: true JSON response requiring exactly one argument.
The Model Context Protocol (MCP) server integrates tools for inspecting development diagnostics, specifically focusing on Turbopack compilation errors across all routes and locating the Next.js development log files. These capabilities allow an AI agent to proactively analyze codebases without requiring a live browser session.
The get_compilation_issues tool builds the module graph for every application endpoint and collects diagnostics directly from Turbopack, covering module-not-found errors, syntax issues, and transform failures.
export function registerGetCompilationIssuesTool(
server: McpServer,
getProject: () => Project | undefined
) {
server.registerTool(
'get_compilation_issues',
{
description:
'Build the module graph for all routes and return all compilation issues (resolve errors, missing modules, transform errors, etc.). Does not require a browser session. Covers all routes proactively.',
inputSchema: {},
},
async () => {
mcpTelemetryTracker.recordToolCall('mcp/get_compilation_issues')
try {
const project = getProject()
if (!project) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error:
'Turbopack project is not available. This tool requires the Turbopack bundler.',
}),
},
],
}
}
const { issues } = await project.getAllCompilationIssues()
const formattedIssues = formatCompilationIssues(issues)
return {
content: [
{
type: 'text',
text: JSON.stringify({ issues: formattedIssues }),
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error),
}),
},
],
}
}
}
)
}Note
Unlike get_errors, which relies on an active browser session to reflect the runtime error overlay, get_compilation_issues evaluates all project routes proactively through Turbopack without opening a browser.
The get_logs tool exposes the filesystem path to the Next.js development log file, enabling agents to read browser console logs and development events directly.
export function registerGetLogsTool(server: McpServer, distDir: string) {
server.registerTool(
'get_logs',
{
description:
'Get the path to the Next.js development log file. Returns the file path so the agent can read the logs directly.',
},
async () => {
// Track telemetry
mcpTelemetryTracker.recordToolCall('mcp/get_logs')
try {
const logFilePath = join(distDir, 'logs', 'next-development.log')
// Check if the log file exists
try {
await stat(logFilePath)
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Log file not found at ${logFilePath}.`,
}),
},
],
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
logFilePath,
}),
},
],
}
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Error getting log file path: ${error instanceof Error ? error.message : String(error)}`,
}),
},
],
}
}
}
)
}Warning
If the development log file has not yet been initialized under {nextConfig.distDir}/logs/next-development.log, the tool catches the filesystem stat error and returns an explicit JSON error payload stating that the log file was not found.
The error reporting and collection subsystem combines Next.js instance-level configuration validation errors, build errors, and browser runtime errors into structured output via the Model Context Protocol (MCP). The get_errors tool orchestrates error retrieval by checking active browser connections, dispatching bidirectional HMR communication requests, combining route-specific overlay states with global instance errors, and invoking source-mapped stack trace inspection.
export const NextInstanceErrorState: {
nextConfig: unknown[]
} = {
nextConfig: [],
}Note
NextInstanceErrorState captures global instance errors that are not associated with a specific browser session or route, such as validation errors within next.config.js.
Retrieving runtime browser errors requires communication between the MCP server and connected browser sessions using Hot Module Replacement (HMR) messaging. The communication utility manages pending requests with unique identifiers, connection counts, and timeout handlers.
export function createBrowserRequest<T>(
messageType: HMR_MESSAGE_SENT_TO_BROWSER,
sendHmrMessage: (message: HmrMessageSentToBrowser) => void,
getActiveConnectionCount: () => number,
timeoutMs: number
): Promise<BrowserResponse<T>[]> {
const connectionCount = getActiveConnectionCount()
if (connectionCount === 0) {
return Promise.resolve([])
}
const requestId = `mcp-${messageType}-${nanoid()}`
const responsePromise = new Promise<BrowserResponse<T>[]>(
(resolve, reject) => {
const timeout = setTimeout(() => {
const pending = pendingRequests.get(requestId)
if (pending && pending.responses.length > 0) {
resolve(pending.responses as BrowserResponse<T>[])
} else {
reject(
new Error(
`Timeout waiting for response from frontend. The browser may not be responding to HMR messages.`
)
)
}
pendingRequests.delete(requestId)
}, timeoutMs)
pendingRequests.set(requestId, {
responses: [],
expectedCount: connectionCount,
resolve: resolve as (value: BrowserResponse<unknown>[]) => void,
reject,
timeout,
})
}
)
sendHmrMessage({
type: messageType,
requestId,
} as HmrMessageSentToBrowser)
return responsePromise
}The execution flow for gathering browser error state follows a precise sequence:
get_errors tool execution invokes getActiveConnectionCount() and records telemetry via mcpTelemetryTracker.recordToolCall('mcp/get_errors').createBrowserRequest() generates a unique requestId prefixed with mcp-, registers a pending request entry mapped by ID, and initializes a timer for DEFAULT_BROWSER_REQUEST_TIMEOUT_MS (5000ms).sendHmrMessage() transmits the HMR_MESSAGE_SENT_TO_BROWSER.REQUEST_CURRENT_ERROR_STATE message containing the requestId to the browser frontend.handleErrorStateResponse() which invokes handleBrowserPageResponse().handleBrowserPageResponse() pushes the received error state and URL into the pending request's responses array. Once responses.length >= expectedCount, it clears the timeout, resolves the promise, and purges the pending map entry.Warning
If zero browser connections are active when get_errors runs, the tool returns an immediate JSON message instructing the user to open the application in a browser, bypassing the HMR request step entirely.
To make stack traces readable during debugging, Next.js patches error inspection routines for Node.js (patchErrorInspectNodeJS) and Edge Lite (patchErrorInspectEdgeLite) runtimes. Stack frames are parsed, mapped back to original source files via source map consumers, and cleaned up by ignoring framework or Node internal frames.
export function patchErrorInspectNodeJS(
errorConstructor: ErrorConstructor
): void {
const inspectSymbol = Symbol.for('nodejs.util.inspect.custom')
errorConstructor.prepareStackTrace = prepareUnsourcemappedStackTrace
// @ts-expect-error -- TODO upstream types
errorConstructor.prototype[inspectSymbol] = function (
depth: number,
inspectOptions: util.InspectOptions,
inspect: typeof util.inspect
): string {
// avoid false-positive dynamic i/o warnings e.g. due to usage of `Math.random` in `source-map`.
return workUnitAsyncStorage.exit(() => {
const newError = sourceMapError(this, inspectOptions)
const originalCustomInspect = (newError as any)[inspectSymbol]
// Prevent infinite recursion.
// { customInspect: false } would result in `error.cause` not using our inspect.
Object.defineProperty(newError, inspectSymbol, {
value: undefined,
enumerable: false,
writable: true,
})
try {
return inspect(newError, {
...inspectOptions,
depth,
})
} finally {
;(newError as any)[inspectSymbol] = originalCustomInspect
}
})
}
}Caution
Both error patching functions execute inside workUnitAsyncStorage.exit() to prevent false-positive dynamic I/O warnings caused by internal dependencies such as random number generation in the source-map library.
Next.js Model Context Protocol (MCP) tooling exposes runtime page segment trees, absolute project filepaths, developer server URLs, and Server Action mappings by communicating directly with active browser sessions and reading internal build manifests. This introspection layer enables AI assistants and debugging clients to examine the live structure of an application without manual DOM inspection or guesswork.
The get_page_metadata tool queries connected browser sessions for runtime page segment data via WebSocket communication. When invoked, it checks active client connections, issues an HMR_MESSAGE_SENT_TO_BROWSER.REQUEST_PAGE_METADATA payload, converts the returned trie into structured page segments, and formats the metadata grouped by session URL and router type.
export function registerGetPageMetadataTool(
server: McpServer,
sendHmrMessage: (message: HmrMessageSentToBrowser) => void,
getActiveConnectionCount: () => number
) {
server.registerTool(
'get_page_metadata',
{
description:
'Get runtime metadata about what contributes to the current page render from active browser sessions.',
inputSchema: {},
},
async (_request) => {
// Track telemetry
mcpTelemetryTracker.recordToolCall('mcp/get_page_metadata')
try {
const connectionCount = getActiveConnectionCount()
if (connectionCount === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error:
'No browser sessions connected. Please open your application in a browser to retrieve page metadata.',
}),
},
],
}
}
const responses = await createBrowserRequest<SegmentTrieData>(
HMR_MESSAGE_SENT_TO_BROWSER.REQUEST_PAGE_METADATA,
sendHmrMessage,
getActiveConnectionCount,
DEFAULT_BROWSER_REQUEST_TIMEOUT_MS
)
// ... conversion and formattingDuring trie conversion, convertSegmentTrieToPageMetadata traverses the segment tree recursively. Nodes containing a value push a PageSegment record containing the node's type, pagePath, and boundaryType into the output array before visiting child nodes.
function convertSegmentTrieToPageMetadata(data: SegmentTrieData): PageMetadata {
const segments: PageSegment[] = []
if (data.segmentTrie) {
// Traverse the trie and collect all segments
function traverseTrie(node: SegmentTrieNode): void {
if (node.value) {
segments.push({
type: node.value.type,
pagePath: node.value.pagePath,
boundaryType: node.value.boundaryType,
})
}
for (const childNode of Object.values(node.children)) {
if (childNode) {
traverseTrie(childNode)
}
}
}
traverseTrie(data.segmentTrie)
}
return {
segments,
routerType: data.routerType,
}
}Warning
If getActiveConnectionCount() returns 0, get_page_metadata immediately returns an error JSON object without attempting browser communication. Users must open the application in a browser session to populate active connections.
The get_project_metadata tool evaluates project path availability and dev server URLs, returning absolute paths and endpoints for MCP clients. Complementing this, get_server_action_by_id inspects compiled build output to locate Server Actions by their unique string identifier within server-reference-manifest.json.
export function registerGetActionByIdTool(server: McpServer, distDir: string) {
server.registerTool(
'get_server_action_by_id',
{
description:
'Locates a Server Action by its ID in the server-reference-manifest.json. Returns the filename and export name for the action.',
inputSchema: {
actionId: z.string(),
},
},
async (request) => {
// Track telemetry
mcpTelemetryTracker.recordToolCall('mcp/get_server_action_by_id')
try {
const { actionId } = request
if (!actionId) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'actionId parameter is required',
}),
},
],
}
}
const manifestPath = join(
distDir,
'server',
'server-reference-manifest.json'
)When an action ID is supplied, the tool parses both node and edge records inside the server reference manifest. If an exported name starts with the inline action prefix $$RSC_SERVER_ACTION_, the returned function name is reported as 'inline server action'; otherwise, the actual export name is preserved.
const manifest: ServerReferenceManifest = JSON.parse(manifestContent)
// Search in node entries
if (manifest.node && manifest.node[actionId]) {
const entry = manifest.node[actionId]
const isInlineAction =
entry.exportedName.startsWith(INLINE_ACTION_PREFIX)
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
actionId,
runtime: 'node',
filename: entry.filename,
functionName: isInlineAction
? 'inline server action'
: entry.exportedName,
layer: entry.layer,
workers: entry.workers,
},
null,
2
),
},
],
}
}Tip
Segment sorting in formatPageMetadata prioritizes layout segments (0), boundary types (1), page segments (2), and fallback types (3), with tie-breaking handled alphabetically via localeCompare on pagePath.
The MCP telemetry and trace server infrastructure records tool call invocation metrics and exposes native Turbopack profiling spans via an integrated Model Context Protocol server. The telemetry tracker manages an in-memory usage map associating each feature name with its execution count, which can be flushed and recorded through standard telemetry events.
class McpTelemetryTracker {
private usageMap = new Map<McpToolName, number>()
recordToolCall(toolName: McpToolName): void {
const current = this.usageMap.get(toolName) || 0
this.usageMap.set(toolName, current + 1)
}
getUsages(): McpToolUsage[] {
return Array.from(this.usageMap.entries()).map(([featureName, count]) => ({
featureName,
invocationCount: count,
}))
}
reset(): void {
this.usageMap.clear()
}
hasUsage(): boolean {
return this.usageMap.size > 0
}
}
export const mcpTelemetryTracker = new McpTelemetryTracker()Note
When recordMcpTelemetry is called with a telemetry instance, it retrieves active tool usages via getMcpTelemetryUsage(), loads the build telemetry events module dynamically, and iterates through generated events to record each invocation count.
The Turbopack trace server CLI (startTurboTraceServerCli) loads native SWC bindings, starts a background trace server handle on a WebSocket port, and sets up an MCP server instance registered with the query_spans tool.
export async function startTurboTraceServerCli(
file: string,
port: number | undefined,
mcpPort: number | undefined
) {
const wsPort = port ?? DEFAULT_WS_PORT
const httpPort = mcpPort ?? wsPort + 1
let bindings = await loadBindings()
let handle = bindings.turbo.startTurbopackTraceServerHandle(file, wsPort)
const mcpServer = new McpServer({
name: 'Next.js Trace Server MCP',
version: '0.1.0',
})
mcpServer.registerTool(
'query_spans',
{
description: 'Query spans from a turbopack trace file...',
inputSchema: {
parent: z.string().optional(),
aggregated: z.boolean().optional(),
sort: z.enum(['value', 'name']).optional(),
search: z.string().optional(),
page: z.number().optional(),
outputType: z.enum(['markdown', 'json']).optional(),
},
},
(args) => {
const result = bindings.turbo.queryTraceSpans(handle, {
parent: args.parent,
aggregated: args.aggregated ?? true,
sort: args.sort,
search: args.search,
page: args.page ?? 1,
})
// Returns spans, page, totalPages, and totalCount
}
)
}Client interactions with the trace server are mediated by the queryTraceCli utility, which constructs a JSON-RPC request targeting the /mcp HTTP endpoint and parses Server-Sent Events (SSE) data streams to extract response text.
export async function queryTraceCli(options: QueryTraceOptions): Promise<void> {
const port = options.port ?? DEFAULT_MCP_PORT
const args: Record<string, unknown> = {}
if (options.parent !== undefined) args.parent = options.parent
if (options.aggregated !== undefined) args.aggregated = options.aggregated
if (options.sort !== undefined) args.sort = options.sort
if (options.search !== undefined) args.search = options.search
if (options.page !== undefined) args.page = options.page
if (options.json) args.outputType = 'json'
const requestBody = JSON.stringify({
jsonrpc: '2.0',
method: 'tools/call',
params: {
name: 'query_spans',
arguments: args,
},
id: 1,
})
const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
},
body: requestBody,
})
const body = await res.text()
for (const line of body.split('\n')) {
if (!line.startsWith('data: ')) continue
const msg = JSON.parse(line.slice('data: '.length))
const text = msg.result?.content?.find((c) => c.type === 'text')?.text
if (text !== undefined) {
process.stdout.write(text)
return
}
}
}