Skip to content

Commit 0bc9482

Browse files
committed
[mcp] get_routes mcp tool
1 parent 5312936 commit 0bc9482

File tree

16 files changed

+396
-1
lines changed

16 files changed

+396
-1
lines changed

packages/next/src/server/dev/hot-reloader-turbopack.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,9 @@ export async function createHotReloaderTurbopack(
777777
getMcpMiddleware({
778778
projectPath,
779779
distDir,
780+
nextConfig,
781+
pagesDir: opts.pagesDir,
782+
appDir: opts.appDir,
780783
sendHmrMessage: (message) => hotReloader.send(message),
781784
getActiveConnectionCount: () =>
782785
clientsWithoutRequestId.size + clientsByRequestId.size,

packages/next/src/server/dev/hot-reloader-webpack.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,6 +1673,9 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface {
16731673
getMcpMiddleware({
16741674
projectPath: this.dir,
16751675
distDir: this.distDir,
1676+
nextConfig: this.config,
1677+
pagesDir: this.pagesDir,
1678+
appDir: this.appDir,
16761679
sendHmrMessage: (message) => this.send(message),
16771680
getActiveConnectionCount: () =>
16781681
this.webpackHotMiddleware?.getClientCount() ?? 0,

packages/next/src/server/mcp/get-or-create-mcp-server.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import { registerGetErrorsTool } from './tools/get-errors'
44
import { registerGetPageMetadataTool } from './tools/get-page-metadata'
55
import { registerGetLogsTool } from './tools/get-logs'
66
import { registerGetActionByIdTool } from './tools/get-server-action-by-id'
7+
import { registerGetRoutesTool } from './tools/get-routes'
78
import type { HmrMessageSentToBrowser } from '../dev/hot-reloader-types'
9+
import type { NextConfigComplete } from '../config-shared'
810

911
export interface McpServerOptions {
1012
projectPath: string
1113
distDir: string
14+
nextConfig: NextConfigComplete
15+
pagesDir: string | undefined
16+
appDir: string | undefined
1217
sendHmrMessage: (message: HmrMessageSentToBrowser) => void
1318
getActiveConnectionCount: () => number
1419
getDevServerUrl: () => string | undefined
@@ -23,7 +28,7 @@ export const getOrCreateMcpServer = (options: McpServerOptions) => {
2328

2429
mcpServer = new McpServer({
2530
name: 'Next.js MCP Server',
26-
version: '0.1.0',
31+
version: '0.2.0',
2732
})
2833

2934
registerGetProjectMetadataTool(
@@ -43,6 +48,12 @@ export const getOrCreateMcpServer = (options: McpServerOptions) => {
4348
)
4449
registerGetLogsTool(mcpServer, options.distDir)
4550
registerGetActionByIdTool(mcpServer, options.distDir)
51+
registerGetRoutesTool(mcpServer, {
52+
projectPath: options.projectPath,
53+
nextConfig: options.nextConfig,
54+
pagesDir: options.pagesDir,
55+
appDir: options.appDir,
56+
})
4657

4758
return mcpServer
4859
}
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
/**
2+
* MCP tool for getting all routes that become entry points in a Next.js application.
3+
*
4+
* This tool discovers routes by scanning the filesystem directly. It finds all route
5+
* files in the app/ and pages/ directories and converts them to route paths.
6+
*
7+
* Returns routes grouped by router type:
8+
* - appRouter: App Router pages and route handlers
9+
* - pagesRouter: Pages Router pages and API routes
10+
*
11+
* Dynamic route segments appear as [id], [slug], or [...slug] patterns. This tool
12+
* does NOT expand getStaticParams - it only shows the route patterns as defined in
13+
* the filesystem.
14+
*/
15+
import type { McpServer } from 'next/dist/compiled/@modelcontextprotocol/sdk/server/mcp'
16+
import { mcpTelemetryTracker } from '../mcp-telemetry-tracker'
17+
import {
18+
collectAppFiles,
19+
collectPagesFiles,
20+
processAppRoutes,
21+
processPageRoutes,
22+
createPagesMapping,
23+
} from '../../../build/entries'
24+
import { createValidFileMatcher } from '../../lib/find-page-file'
25+
import { PAGE_TYPES } from '../../../lib/page-types'
26+
import type { NextConfigComplete } from '../../../server/config-shared'
27+
import z from 'next/dist/compiled/zod'
28+
29+
interface RouteInfo {
30+
route: string
31+
type: 'app' | 'page' | 'api'
32+
}
33+
34+
export function registerGetRoutesTool(
35+
server: McpServer,
36+
options: {
37+
projectPath: string
38+
nextConfig: NextConfigComplete
39+
pagesDir: string | undefined
40+
appDir: string | undefined
41+
}
42+
) {
43+
server.registerTool(
44+
'get_routes',
45+
{
46+
description:
47+
'Get all routes that will become entry points in the Next.js application by scanning the filesystem. Returns routes grouped by router type (appRouter, pagesRouter). Dynamic segments appear as [param] or [...slug] patterns. API routes are included in their respective routers (e.g., /api/* routes from pages/ are in pagesRouter). Optional parameter: routerType ("app" | "pages") - filter by specific router type, omit to get all routes.',
48+
inputSchema: {
49+
routerType: z.union([z.literal('app'), z.literal('pages')]).optional(),
50+
},
51+
},
52+
async (request) => {
53+
// Track telemetry
54+
mcpTelemetryTracker.recordToolCall('mcp/get_routes')
55+
56+
try {
57+
const routerType =
58+
request.routerType === 'app' || request.routerType === 'pages'
59+
? request.routerType
60+
: undefined
61+
62+
const routes: RouteInfo[] = []
63+
64+
const { projectPath, nextConfig, pagesDir, appDir } = options
65+
66+
// Check if we have any directories to scan
67+
if (!pagesDir && !appDir) {
68+
return {
69+
content: [
70+
{
71+
type: 'text',
72+
text: 'No pages or app directory found in the project.',
73+
},
74+
],
75+
}
76+
}
77+
78+
const isSrcDir =
79+
(pagesDir && pagesDir.includes('/src/')) ||
80+
(appDir && appDir.includes('/src/'))
81+
82+
// Create valid file matcher for filtering
83+
const validFileMatcher = createValidFileMatcher(
84+
nextConfig.pageExtensions,
85+
appDir
86+
)
87+
88+
// Collect and process App Router routes if requested
89+
if (appDir && (!routerType || routerType === 'app')) {
90+
try {
91+
const { appPaths } = await collectAppFiles(appDir, validFileMatcher)
92+
93+
if (appPaths.length > 0) {
94+
const mappedAppPages = await createPagesMapping({
95+
pagePaths: appPaths,
96+
isDev: true,
97+
pagesType: PAGE_TYPES.APP,
98+
pageExtensions: nextConfig.pageExtensions,
99+
pagesDir,
100+
appDir,
101+
appDirOnly: pagesDir ? false : true,
102+
})
103+
104+
const { appRoutes, appRouteHandlers } = processAppRoutes(
105+
mappedAppPages,
106+
validFileMatcher,
107+
projectPath,
108+
isSrcDir || false
109+
)
110+
111+
// Add app page routes
112+
for (const { route } of appRoutes) {
113+
routes.push({
114+
route,
115+
type: 'app',
116+
})
117+
}
118+
119+
// Add app route handlers
120+
for (const { route } of appRouteHandlers) {
121+
routes.push({
122+
route,
123+
type: 'app',
124+
})
125+
}
126+
}
127+
} catch (error) {
128+
// Error collecting app routes - continue anyway
129+
}
130+
}
131+
132+
// Collect and process Pages Router routes if requested
133+
if (pagesDir && (!routerType || routerType === 'pages')) {
134+
try {
135+
const pagePaths = await collectPagesFiles(
136+
pagesDir,
137+
validFileMatcher
138+
)
139+
140+
if (pagePaths.length > 0) {
141+
const mappedPages = await createPagesMapping({
142+
pagePaths,
143+
isDev: true,
144+
pagesType: PAGE_TYPES.PAGES,
145+
pageExtensions: nextConfig.pageExtensions,
146+
pagesDir,
147+
appDir,
148+
appDirOnly: false,
149+
})
150+
151+
const { pageRoutes, pageApiRoutes } = processPageRoutes(
152+
mappedPages,
153+
projectPath,
154+
isSrcDir || false
155+
)
156+
157+
// Add page routes
158+
for (const { route } of pageRoutes) {
159+
routes.push({
160+
route,
161+
type: 'page',
162+
})
163+
}
164+
165+
// Add API routes (always included as part of pages router)
166+
for (const { route } of pageApiRoutes) {
167+
routes.push({
168+
route,
169+
type: 'api',
170+
})
171+
}
172+
}
173+
} catch (error) {
174+
// Error collecting pages routes - continue anyway
175+
}
176+
}
177+
178+
if (routes.length === 0) {
179+
return {
180+
content: [
181+
{
182+
type: 'text',
183+
text: 'No routes found in the project.',
184+
},
185+
],
186+
}
187+
}
188+
189+
// Group routes by router type
190+
const appRoutes = routes
191+
.filter((r) => r.type === 'app')
192+
.map((r) => r.route)
193+
.sort()
194+
const pageRoutes = routes
195+
.filter((r) => r.type === 'page' || r.type === 'api')
196+
.map((r) => r.route)
197+
.sort()
198+
199+
// Format the output with grouped routes
200+
const output = {
201+
appRouter: appRoutes.length > 0 ? appRoutes : undefined,
202+
pagesRouter: pageRoutes.length > 0 ? pageRoutes : undefined,
203+
}
204+
205+
return {
206+
content: [
207+
{
208+
type: 'text',
209+
text: JSON.stringify(output, null, 2),
210+
},
211+
],
212+
}
213+
} catch (error) {
214+
return {
215+
content: [
216+
{
217+
type: 'text',
218+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
219+
},
220+
],
221+
}
222+
}
223+
}
224+
)
225+
}

packages/next/src/telemetry/events/build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export type McpToolName =
237237
| 'mcp/get_logs'
238238
| 'mcp/get_page_metadata'
239239
| 'mcp/get_project_metadata'
240+
| 'mcp/get_routes'
240241
| 'mcp/get_server_action_by_id'
241242

242243
export type EventMcpToolUsage = {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export async function GET(
2+
request: Request,
3+
{ params }: { params: { id: string } }
4+
) {
5+
return Response.json({ id: params.id })
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function BlogPost({ params }: { params: { slug: string } }) {
2+
return <div>Blog post: {params.slug}</div>
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Docs({ params }: { params: { slug: string[] } }) {
2+
return <div>Docs: {params.slug.join('/')}</div>
3+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default function RootLayout({
2+
children,
3+
}: {
4+
children: React.ReactNode
5+
}) {
6+
return (
7+
<html>
8+
<body>{children}</body>
9+
</html>
10+
)
11+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function HomePage() {
2+
return <div>Home</div>
3+
}

0 commit comments

Comments
 (0)