Skip to main content
Glama

Next.js MCP Server

by vertile-ai
router.ts13.5 kB
// next-route-analyzer.js import fs from 'fs'; import path from 'path'; import { promisify } from 'util'; import { z } from 'zod'; const readdir = promisify(fs.readdir); const readFile = promisify(fs.readFile); // Define Zod schemas for route information export const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']); export const GetRoutersInfoSchema = z.object({ projectDir: z.string().describe('The directory of the Next.js project. Must be absolute path.') }); export const RouteParameterSchema = z.object({ name: z.string(), type: z.string(), description: z.string().optional(), required: z.boolean().default(true) }); export const RouteHandlerSchema = z.object({ method: HttpMethodSchema, path: z.string(), functionSignature: z.string(), description: z.string().optional(), parameters: z.array(RouteParameterSchema).default([]), requestBodySchema: z.string().optional(), responseType: z.string().optional(), statusCodes: z.array( z.object({ code: z.number(), description: z.string().optional() }) ).default([]) }); export const RouteInfoSchema = z.object({ filePath: z.string(), implementationPath: z.string(), apiPath: z.string(), handlers: z.array(RouteHandlerSchema).default([]), imports: z.array(z.string()).default([]), validationSchemas: z.array(z.string()).default([]) }); /** * Main function to analyze Next.js routes * @returns {Promise<Array<z.infer<typeof RouteInfoSchema>>>} - Array of route information */ async function analyzeNextRoutes(projectDir: string): Promise<Array<z.infer<typeof RouteInfoSchema>>> { const routeFiles = await findRouteFiles(projectDir); const routesInfo = []; for (const filePath of routeFiles) { try { let implementationContent: string | null = null; try { implementationContent = await readFile(filePath, 'utf8'); } catch (error) { console.warn(`Could not read implementation file ${filePath}: ${error.message}`); continue; } // Parse the route information const apiPath = extractApiPath(filePath, implementationContent); const handlers = extractRouteHandlers(implementationContent, apiPath); const imports = extractImports(implementationContent); const validationSchemas = extractValidationSchemas(implementationContent); const routeInfo = RouteInfoSchema.parse({ filePath, implementationPath: filePath, apiPath, handlers, imports, validationSchemas }); routesInfo.push(routeInfo); } catch (error) { console.error(`Error processing route file ${filePath}:`, error); } } return routesInfo; } /** * Find all route.ts/js files directly in the project directory * @param {string} dir - Project directory to search * @returns {Promise<Array<string>>} - Array of file paths */ async function findRouteFiles(dir: string): Promise<Array<string>> { const routeFiles: string[] = []; async function searchDir(currentDir: string) { try { const entries = await readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { // Skip node_modules, .git, .next, and other common directories to avoid excessive scanning if (['node_modules', '.git', '.next', 'dist', 'build', 'out'].includes(entry.name)) { continue; } await searchDir(fullPath); } else if (entry.name === 'route.ts' || entry.name === 'route.js' || entry.name === 'route.tsx' || entry.name === 'route.jsx') { // Only include routes inside the app directory if (fullPath.includes(`${path.sep}app${path.sep}`)) { routeFiles.push(fullPath); } } } } catch (error) { console.error(`Error reading directory ${currentDir}:`, error.message); } } await searchDir(dir); return routeFiles; } /** * Extract the path to the actual implementation file from a type file * @param {string} fileContent - Content of the route.ts type file * @returns {string|null} - Path to the implementation file or null */ function extractImplementationPath(fileContent: string): string | null { const pathRegex = /\/\/ File: (.+)/; const match = fileContent.match(pathRegex); if (match && match[1]) { // Convert Windows path if necessary return match[1].replace(/\\/g, path.sep); } return null; } /** * Extract the API path from the file path and implementation * @param {string} filePath - Path to the route.ts type file * @param {string} implementationContent - Content of the implementation file * @returns {string} - API path */ function extractApiPath(filePath: string, implementationContent: string): string { // Try to extract from comments in the implementation file first const commentPathRegex = /\/\/ (GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS) (\/api\/[^\s]+) -/; const commentMatch = implementationContent.match(commentPathRegex); if (commentMatch && commentMatch[2]) { return commentMatch[2]; } // Fall back to deriving from file path const pathParts = filePath.split(path.sep); const apiIndex = pathParts.indexOf('api'); if (apiIndex !== -1) { const routeParts = pathParts.slice(apiIndex, pathParts.length - 1); return '/' + routeParts.join('/'); } return ''; } /** * Extract route handlers from the implementation file * @param {string} content - Content of the implementation file * @param {string} apiPath - API path * @returns {Array} - Array of route handlers */ function extractRouteHandlers(content: string, apiPath: string): Array<z.infer<typeof RouteHandlerSchema>> { const handlers: Array<z.infer<typeof RouteHandlerSchema>> = []; const methodRegex = /export\s+async\s+function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(([^)]*)\)/g; const statusCodeRegex = /status:\s*(\d+)/g; const commentRegex = /\/\/\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([^\s]+)\s*-\s*([^\n]+)/g; // Extract descriptions from comments const descriptions: Record<string, string> = {}; let commentMatch: RegExpExecArray | null; while ((commentMatch = commentRegex.exec(content)) !== null) { const method = commentMatch[1]; const description = commentMatch[3].trim(); descriptions[method] = description; } // Extract handlers let methodMatch: RegExpExecArray | null = null; while ((methodMatch = methodRegex.exec(content)) !== null) { const method = methodMatch[1] as z.infer<typeof HttpMethodSchema>; const functionSignature = methodMatch[0]; const parameterString = methodMatch[2]; // Extract parameters const parameters = extractParameters(parameterString); // Extract status codes const handlerContent = extractFunctionBody(content, methodMatch.index); const statusCodes = []; let statusMatch; while ((statusMatch = statusCodeRegex.exec(handlerContent)) !== null) { statusCodes.push({ code: parseInt(statusMatch[1]), description: getStatusCodeDescription(parseInt(statusMatch[1])) }); } // Extract request body schema if applicable const requestBodySchema = extractRequestBodySchema(handlerContent); // Extract response type const responseType = extractResponseType(handlerContent); handlers.push({ method, path: apiPath, functionSignature, description: descriptions[method] || undefined, parameters, requestBodySchema, responseType, statusCodes }); } return handlers; } /** * Extract parameters from a function signature * @param {string} parameterString - Parameter string from function signature * @returns {Array} - Array of parameters */ function extractParameters(parameterString: string): Array<z.infer<typeof RouteParameterSchema>> { const parameters: Array<z.infer<typeof RouteParameterSchema>> = []; // Split by comma, but handle nested objects const paramParts = []; let currentPart = ''; let braceCount = 0; for (let i = 0; i < parameterString.length; i++) { const char = parameterString[i]; if (char === '{') braceCount++; else if (char === '}') braceCount--; if (char === ',' && braceCount === 0) { paramParts.push(currentPart.trim()); currentPart = ''; } else { currentPart += char; } } if (currentPart.trim()) { paramParts.push(currentPart.trim()); } for (const part of paramParts) { // Parse parameter name and type const matches = part.match(/(\w+)\s*:\s*(.+)/); if (matches) { const [, name, type] = matches; parameters.push({ name, type: type.trim(), required: !type.includes('?') && !type.includes('undefined') }); } } return parameters; } /** * Extract the function body * @param {string} content - Content of the implementation file * @param {number} startIndex - Start index of the function * @returns {string} - Function body */ function extractFunctionBody(content: string, startIndex: number): string { // Find the opening bracket let openBracketIndex = content.indexOf('{', startIndex); if (openBracketIndex === -1) return ''; // Find the closing bracket (accounting for nested brackets) let bracketCount = 1; let endIndex = openBracketIndex + 1; while (bracketCount > 0 && endIndex < content.length) { const char = content[endIndex]; if (char === '{') bracketCount++; else if (char === '}') bracketCount--; endIndex++; } return content.substring(openBracketIndex, endIndex); } /** * Extract request body schema from a function body * @param {string} functionBody - Function body * @returns {string|undefined} - Request body schema */ function extractRequestBodySchema(functionBody: string): string | undefined { // Look for schema validation const schemaRegex = /(\w+)\.safeParse\(body\)/; const match = functionBody.match(schemaRegex); if (match && match[1]) { return match[1]; } return undefined; } /** * Extract response type from a function body * @param {string} functionBody - Function body * @returns {string} - Response type */ function extractResponseType(functionBody: string): string { if (functionBody.includes('NextResponse.json(')) { return 'JSON'; } else if (functionBody.includes('new NextResponse(')) { return 'Raw'; } else if (functionBody.includes('Response.json(')) { return 'JSON'; } else if (functionBody.includes('new Response(')) { return 'Raw'; } return 'Unknown'; } /** * Extract imports from the implementation file * @param {string} content - Content of the implementation file * @returns {Array<string>} - Array of imports */ function extractImports(content: string): Array<string> { const imports = []; const importRegex = /import\s+(.+?)\s+from\s+['"](.+?)['"];?/g; let match; while ((match = importRegex.exec(content)) !== null) { imports.push(`import ${match[1]} from '${match[2]}'`); } return imports; } /** * Extract validation schemas from the implementation file * @param {string} content - Content of the implementation file * @returns {Array<string>} - Array of validation schema names */ function extractValidationSchemas(content: string): Array<string> { const schemas: string[] = []; const schemaRegex = /(\w+)Schema\.safeParse/g; let match; while ((match = schemaRegex.exec(content)) !== null) { schemas.push(match[1]); } return [...new Set(schemas)]; // Remove duplicates } /** * Get a description for an HTTP status code * @param {number} code - HTTP status code * @returns {string} - Description */ function getStatusCodeDescription(code: number): string { const statusCodes: Record<number, string> = { 200: 'OK - Successful request', 201: 'Created - Resource created successfully', 204: 'No Content - Request succeeded with no response body', 400: 'Bad Request - Invalid request data', 401: 'Unauthorized - Authentication required', 403: 'Forbidden - Permission denied', 404: 'Not Found - Resource not found', 500: 'Internal Server Error - Server error occurred' }; return statusCodes[code] || 'Unknown status code'; } /** * Run the analyzer and print the results */ export async function getRoutersInfo(projectDir: string) { try { // remove leading slashes or backslashes const cleanedProjectPath = projectDir.replace(/^[\/\\]+/, ''); const routesInfo = await analyzeNextRoutes(cleanedProjectPath); console.log('API Routes Analysis:'); console.log(JSON.stringify(routesInfo, null, 2)); // Also output a more human-readable summary console.log('\nAPI Routes Summary:'); for (const route of routesInfo) { console.log(`\n${route.apiPath}`); for (const handler of route.handlers) { console.log(` ${handler.method} - ${handler.description || 'No description'}`); console.log(` Parameters: ${handler.parameters.length ? handler.parameters.map(p => p.name).join(', ') : 'None'}`); console.log(` Status Codes: ${handler.statusCodes.map(s => s.code).join(', ')}`); } } return routesInfo; } catch (error) { console.error('Error analyzing routes:', error); throw error; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vertile-ai/next-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server