Skip to content

Commit 5d2ba7d

Browse files
committed
[mcp] get_routes mcp tool
1 parent 5312936 commit 5d2ba7d

File tree

16 files changed

+432
-1
lines changed

16 files changed

+432
-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+
28+
interface RouteInfo {
29+
route: string
30+
type: 'app' | 'page' | 'api'
31+
}
32+
33+
export function registerGetRoutesTool(
34+
server: McpServer,
35+
options: {
36+
projectPath: string
37+
nextConfig: NextConfigComplete
38+
pagesDir: string | undefined
39+
appDir: string | undefined
40+
}
41+
) {
42+
server.registerTool(
43+
'get_routes',
44+
{
45+
description:
46+
'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. Optional parameters: includeApi (boolean, default true) - whether to include API routes; routerType (string, default "all") - filter by "all", "app", or "pages".',
47+
},
48+
async (request) => {
49+
// Track telemetry
50+
mcpTelemetryTracker.recordToolCall('mcp/get_routes')
51+
52+
try {
53+
const includeApi =
54+
request.params?.includeApi !== undefined
55+
? request.params.includeApi
56+
: true
57+
const routerType =
58+
(request.params?.routerType as 'all' | 'app' | 'pages') || 'all'
59+
60+
const routes: RouteInfo[] = []
61+
62+
const { projectPath, nextConfig, pagesDir, appDir } = options
63+
64+
// Check if we have any directories to scan
65+
if (!pagesDir && !appDir) {
66+
return {
67+
content: [
68+
{
69+
type: 'text',
70+
text: 'No pages or app directory found in the project.',
71+
},
72+
],
73+
}
74+
}
75+
76+
const isSrcDir =
77+
(pagesDir && pagesDir.includes('/src/')) ||
78+
(appDir && appDir.includes('/src/'))
79+
80+
// Create valid file matcher for filtering
81+
const validFileMatcher = createValidFileMatcher(
82+
nextConfig.pageExtensions,
83+
appDir
84+
)
85+
86+
// Collect and process App Router routes if requested
87+
if (appDir && (routerType === 'all' || routerType === 'app')) {
88+
try {
89+
const { appPaths } = await collectAppFiles(appDir, validFileMatcher)
90+
91+
if (appPaths.length > 0) {
92+
const mappedAppPages = await createPagesMapping({
93+
pagePaths: appPaths,
94+
isDev: true,
95+
pagesType: PAGE_TYPES.APP,
96+
pageExtensions: nextConfig.pageExtensions,
97+
pagesDir,
98+
appDir,
99+
appDirOnly: pagesDir ? false : true,
100+
})
101+
102+
const { appRoutes, appRouteHandlers } = processAppRoutes(
103+
mappedAppPages,
104+
validFileMatcher,
105+
projectPath,
106+
isSrcDir || false
107+
)
108+
109+
// Add app page routes
110+
for (const { route } of appRoutes) {
111+
routes.push({
112+
route,
113+
type: 'app',
114+
})
115+
}
116+
117+
// Add app route handlers
118+
for (const { route } of appRouteHandlers) {
119+
routes.push({
120+
route,
121+
type: 'app',
122+
})
123+
}
124+
}
125+
} catch (error) {
126+
// Error collecting app routes - continue anyway
127+
}
128+
}
129+
130+
// Collect and process Pages Router routes if requested
131+
if (pagesDir && (routerType === 'all' || routerType === 'pages')) {
132+
try {
133+
const pagePaths = await collectPagesFiles(
134+
pagesDir,
135+
validFileMatcher
136+
)
137+
138+
if (pagePaths.length > 0) {
139+
const mappedPages = await createPagesMapping({
140+
pagePaths,
141+
isDev: true,
142+
pagesType: PAGE_TYPES.PAGES,
143+
pageExtensions: nextConfig.pageExtensions,
144+
pagesDir,
145+
appDir,
146+
appDirOnly: false,
147+
})
148+
149+
const { pageRoutes, pageApiRoutes } = processPageRoutes(
150+
mappedPages,
151+
projectPath,
152+
isSrcDir || false
153+
)
154+
155+
// Add page routes
156+
for (const { route } of pageRoutes) {
157+
routes.push({
158+
route,
159+
type: 'page',
160+
})
161+
}
162+
163+
// Add API routes if requested
164+
if (includeApi) {
165+
for (const { route } of pageApiRoutes) {
166+
routes.push({
167+
route,
168+
type: 'api',
169+
})
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)