├── .dockerignore ├── .env ├── .gitignore ├── .npmignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── demo1.png ├── common └── version.ts ├── index.ts ├── jest.config.js ├── operations ├── __tests__ │ └── router.test.ts └── router.ts ├── package.json ├── playground ├── .env.local ├── app │ ├── api │ │ ├── products │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ ├── categories │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── users │ │ │ ├── [id] │ │ │ └── route.ts │ │ │ └── route.ts │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── products │ │ ├── categories │ │ │ └── page.tsx │ │ └── page.tsx │ └── users │ │ └── page.tsx ├── lib │ ├── db.ts │ └── validation.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── tailwind.config.js ├── tsconfig.json └── types │ └── index.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── setup.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Version control 2 | .git 3 | .gitignore 4 | 5 | # Node.js 6 | node_modules 7 | npm-debug.log 8 | yarn-debug.log 9 | yarn-error.log 10 | 11 | # Build output 12 | dist 13 | 14 | # Environment variables 15 | .env 16 | .env.* 17 | 18 | # OS files 19 | .DS_Store 20 | Thumbs.db 21 | 22 | # IDE files 23 | .idea 24 | .vscode 25 | *.swp 26 | *.swo 27 | 28 | # Temporary files 29 | tmp 30 | temp -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | TRANSPORT=sse 2 | URL_BASE= 3 | PORT=4857 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist/**/*.test.js 2 | dist/jest.config.js 3 | **/**.test.js 4 | dist/**.test.js -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.12-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | # Copy package files first to leverage Docker cache 6 | COPY package.json package-lock.json* pnpm-lock.yaml* ./ 7 | 8 | # Install dependencies 9 | RUN npm install 10 | 11 | # Copy source code 12 | COPY . . 13 | 14 | # Build the application 15 | RUN npm run build 16 | 17 | FROM node:22.12-alpine AS release 18 | 19 | WORKDIR /app 20 | 21 | ENV NODE_ENV=production 22 | 23 | # Copy only necessary files from builder stage 24 | COPY --from=builder /app/dist /app/dist 25 | COPY --from=builder /app/package.json /app/package.json 26 | COPY --from=builder /app/package-lock.json* /app/package-lock.json* 27 | COPY --from=builder /app/pnpm-lock.yaml* /app/pnpm-lock.yaml* 28 | 29 | # Use a non-root user for security 30 | USER node 31 | 32 | ENTRYPOINT ["node", "dist/index.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Vertile 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js MCP Server 2 | 3 | ## Demo 4 | 5 | ![Router analysis demo](assets/demo1.png) 6 | 7 | ## Features 8 | 9 | - `get-routers-info` 10 | 11 | The Router Analyzer scans your Next.js app directory structure and extracts information about all API routes, including: 12 | 13 | - API paths 14 | - HTTP methods (GET, POST, PUT, DELETE, etc.) 15 | - Request parameters 16 | - Status codes 17 | - Request and response schemas 18 | 19 | ## Installation 20 | 21 | ```bash 22 | npm install next-mcp-server 23 | ``` 24 | 25 | Or if you're using pnpm: 26 | 27 | ```bash 28 | pnpm add next-mcp-server 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Command Line 34 | 35 | You can run the mcp server directly: 36 | 37 | ```bash 38 | npm run build 39 | node dist/index.js 40 | ``` 41 | 42 | ### Docker 43 | 44 | ```bash 45 | docker build -t mcp/next -f Dockerfile . 46 | docker run mcp/next -d 47 | ``` 48 | 49 | For cursor usage, define a `mcp.json` under `~/.cursor` or `[projectDir]/.cursor` 50 | 51 | ``` 52 | { 53 | "mcpServers": { 54 | "next.js": { 55 | "url": "http://localhost:4857/sse" 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | The `url` here could vary based on your .env settings within the project. 62 | 63 | 64 | ## Output 65 | 66 | The tool generates detailed information about each route: 67 | 68 | ```javascript 69 | [ 70 | { 71 | "filePath": "/path/to/your/app/api/test/route.ts", 72 | "implementationPath": "/path/to/your/app/api/test/route.ts", 73 | "apiPath": "/api/test", 74 | "handlers": [ 75 | { 76 | "method": "GET", 77 | "path": "/api/test", 78 | "functionSignature": "export async function GET(request: Request)", 79 | "description": "Get test data", 80 | "parameters": [], 81 | "statusCodes": [200] 82 | }, 83 | { 84 | "method": "POST", 85 | "path": "/api/test", 86 | "functionSignature": "export async function POST(request: Request)", 87 | "description": "Create test data", 88 | "parameters": [], 89 | "requestBodySchema": "{ name: string }", 90 | "statusCodes": [201, 400] 91 | } 92 | ] 93 | } 94 | ] 95 | ``` 96 | 97 | ## Development 98 | 99 | To run tests: 100 | 101 | ```bash 102 | npm run test 103 | ``` 104 | 105 | To run the mcp server locally: 106 | 107 | ```bash 108 | npm run build 109 | node dist/index.js 110 | ``` 111 | 112 | To run it from node_modules after `npm i`: 113 | 114 | ```bash 115 | node node_modules/next-mcp-server/dist/index.js 116 | ``` 117 | 118 | To run the playground: 119 | 120 | ```bash 121 | pnpm --filter playground dev 122 | ``` 123 | 124 | ## How It Works 125 | 126 | The tool: 127 | 128 | 1. Scans your Next.js app directory structure for route files 129 | 2. Analyzes each route file to extract HTTP methods, paths, parameters, etc. 130 | 3. Extracts documentation from comments 131 | 4. Returns a structured representation of all your API routes 132 | 133 | ## Restrictions 134 | 135 | 1. Due to the nature of accessing filesystem directory by path, it will not work if hosted over network 136 | 2. Only supports Next.js App router projects 137 | 138 | ## License 139 | 140 | MIT 141 | -------------------------------------------------------------------------------- /assets/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vertile-ai/next-mcp-server/382983e3b689ee0c7e3e74bd79fd6759ae30f747/assets/demo1.png -------------------------------------------------------------------------------- /common/version.ts: -------------------------------------------------------------------------------- 1 | export const VERSION = "0.0.1"; -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { 5 | CallToolRequestSchema, 6 | ListToolsRequestSchema, 7 | } from "@modelcontextprotocol/sdk/types.js"; 8 | import { z } from 'zod'; 9 | import { zodToJsonSchema } from 'zod-to-json-schema'; 10 | 11 | import * as router from './operations/router.js'; 12 | import { VERSION } from "./common/version.js"; 13 | import { resolvePort, resolveTransport, setupSSE } from "./setup.js"; 14 | 15 | const configureServer = (): Server => { 16 | const server = new Server( 17 | { 18 | name: "next-mcp-server", 19 | version: VERSION, 20 | }, 21 | { 22 | capabilities: { 23 | tools: {}, 24 | }, 25 | } 26 | ); 27 | 28 | 29 | server.setRequestHandler(ListToolsRequestSchema, async () => { 30 | return { 31 | tools: [ 32 | { 33 | name: "get_routers_info", 34 | description: "Get Pages details in the Next.js app.", 35 | inputSchema: zodToJsonSchema(router.GetRoutersInfoSchema), 36 | }, 37 | ], 38 | }; 39 | }); 40 | 41 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 42 | try { 43 | switch (request.params.name) { 44 | case "get_routers_info": { 45 | const args = router.GetRoutersInfoSchema.parse(request.params.arguments); 46 | const pagesInfo = await router.getRoutersInfo(args.projectDir); 47 | return { 48 | content: [{ type: "text", text: JSON.stringify(pagesInfo, null, 2) }], 49 | }; 50 | } 51 | 52 | default: 53 | throw new Error(`Unknown tool: ${request.params.name}`); 54 | } 55 | } catch (error) { 56 | if (error instanceof z.ZodError) { 57 | throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`); 58 | } 59 | throw error; 60 | } 61 | }); 62 | 63 | return server; 64 | } 65 | 66 | async function runServer() { 67 | const server = configureServer(); 68 | const transportData = resolveTransport(); 69 | console.error(`Using transport: ${transportData.type}`); 70 | console.error(`Transport source: ${transportData.source}`); 71 | 72 | if (transportData.type === 'sse') { 73 | // Set up Express server for SSE transport 74 | const app = await setupSSE(process.env.URL_BASE || '', server); 75 | // Start the HTTP server (port is only relevant for SSE transport) 76 | const portData = resolvePort(); 77 | const port = portData.port; 78 | console.error(`Port source: ${portData.source}`); 79 | app.listen(port, () => { 80 | console.error(`Waiting for connection to MCP server at http://localhost:${port}/sse`); 81 | }); 82 | } else { 83 | // Set up STDIO transport 84 | const transport = new StdioServerTransport(); 85 | console.error("Starting with STDIO transport"); 86 | await server.connect(transport); 87 | 88 | // Listen for SIGINT to gracefully shut down 89 | process.on('SIGINT', async () => { 90 | console.error('Shutting down...'); 91 | await transport.close(); 92 | process.exit(0); 93 | }); 94 | console.error("Next MCP Server running on stdio"); 95 | } 96 | } 97 | 98 | runServer().catch((error) => { 99 | console.error("Fatal error in main():", error); 100 | process.exit(1); 101 | }); -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest/presets/default-esm', 3 | testEnvironment: 'node', 4 | extensionsToTreatAsEsm: ['.ts'], 5 | moduleNameMapper: { 6 | '^(\\.{1,2}/.*)\\.js$': '$1', 7 | }, 8 | transform: { 9 | '^.+\\.tsx?$': [ 10 | 'ts-jest', 11 | { 12 | useESM: true, 13 | }, 14 | ], 15 | }, 16 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 17 | verbose: true, 18 | silent: false, 19 | }; -------------------------------------------------------------------------------- /operations/__tests__/router.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals'; 3 | import { getRoutersInfo } from '../router.js'; 4 | 5 | describe('NextJS Router Analysis', () => { 6 | // Capture console logs for verification 7 | let consoleLogSpy; 8 | let consoleErrorSpy; 9 | let consoleWarnSpy; 10 | 11 | const playgroundPath = path.resolve(process.cwd(), 'playground'); 12 | const unixPath = path.resolve(process.cwd(), 'playground').replace(/\\/g, '/') 13 | const windowsPath = path.resolve(process.cwd(), 'playground').replace(/\//g, '\\') 14 | const windowsPathWithDoubleBackslashes = windowsPath.replace(/\//g, '\\\\') 15 | 16 | // Create different format variations 17 | const projectDirsInput = [ 18 | playgroundPath, 19 | `${windowsPath}\\`, 20 | `\\${windowsPath}`, 21 | unixPath, 22 | `/${unixPath}`, 23 | `${unixPath}/`, 24 | windowsPath, 25 | windowsPathWithDoubleBackslashes, 26 | ]; 27 | 28 | // Filter out duplicates (since some formats might be identical depending on OS) 29 | const uniqueProjectDirs = [...new Set(projectDirsInput)]; 30 | 31 | beforeEach(() => { 32 | consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); 33 | consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 34 | consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); 35 | }); 36 | 37 | afterEach(() => { 38 | if (consoleLogSpy) consoleLogSpy.mockRestore(); 39 | if (consoleErrorSpy) consoleErrorSpy.mockRestore(); 40 | if (consoleWarnSpy) consoleWarnSpy.mockRestore(); 41 | }); 42 | 43 | test.each(uniqueProjectDirs)(`should analyze NextJS routes from %s`, async (projectDir) => { 44 | // Call the function we're testing 45 | const routesInfo = await getRoutersInfo(projectDir); 46 | 47 | // Expect routesInfo to be an array 48 | expect(Array.isArray(routesInfo)).toBe(true); 49 | 50 | // We should have at least logged information about the routes 51 | expect(consoleLogSpy).toHaveBeenCalled(); 52 | 53 | // Verify structure of returned route info 54 | expect(routesInfo.length).toBeGreaterThan(0); 55 | const firstRoute = routesInfo[0]; 56 | 57 | // Check that the structure matches our schema 58 | expect(firstRoute).toHaveProperty('filePath'); 59 | expect(firstRoute).toHaveProperty('implementationPath'); 60 | expect(firstRoute).toHaveProperty('apiPath'); 61 | expect(firstRoute).toHaveProperty('handlers'); 62 | expect(Array.isArray(firstRoute.handlers)).toBe(true); 63 | 64 | // Check handlers if they exist 65 | if (firstRoute.handlers.length > 0) { 66 | const firstHandler = firstRoute.handlers[0]; 67 | expect(firstHandler).toHaveProperty('method'); 68 | expect(firstHandler).toHaveProperty('path'); 69 | expect(firstHandler).toHaveProperty('functionSignature'); 70 | } 71 | }); 72 | 73 | test('should handle invalid directory gracefully', async () => { 74 | // Testing error handling with non-existent directory 75 | const nonExistentDir = path.resolve(process.cwd(), 'non-existent-dir'); 76 | 77 | // Expect the function to throw an error for invalid directory 78 | await expect(getRoutersInfo(nonExistentDir)).resolves.toEqual([]); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /operations/router.ts: -------------------------------------------------------------------------------- 1 | // next-route-analyzer.js 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { promisify } from 'util'; 5 | import { z } from 'zod'; 6 | const readdir = promisify(fs.readdir); 7 | const readFile = promisify(fs.readFile); 8 | 9 | // Define Zod schemas for route information 10 | export const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']); 11 | 12 | export const GetRoutersInfoSchema = z.object({ 13 | projectDir: z.string().describe('The directory of the Next.js project. Must be absolute path.') 14 | }); 15 | 16 | export const RouteParameterSchema = z.object({ 17 | name: z.string(), 18 | type: z.string(), 19 | description: z.string().optional(), 20 | required: z.boolean().default(true) 21 | }); 22 | 23 | export const RouteHandlerSchema = z.object({ 24 | method: HttpMethodSchema, 25 | path: z.string(), 26 | functionSignature: z.string(), 27 | description: z.string().optional(), 28 | parameters: z.array(RouteParameterSchema).default([]), 29 | requestBodySchema: z.string().optional(), 30 | responseType: z.string().optional(), 31 | statusCodes: z.array( 32 | z.object({ 33 | code: z.number(), 34 | description: z.string().optional() 35 | }) 36 | ).default([]) 37 | }); 38 | 39 | export const RouteInfoSchema = z.object({ 40 | filePath: z.string(), 41 | implementationPath: z.string(), 42 | apiPath: z.string(), 43 | handlers: z.array(RouteHandlerSchema).default([]), 44 | imports: z.array(z.string()).default([]), 45 | validationSchemas: z.array(z.string()).default([]) 46 | }); 47 | 48 | /** 49 | * Main function to analyze Next.js routes 50 | * @returns {Promise>>} - Array of route information 51 | */ 52 | async function analyzeNextRoutes(projectDir: string): Promise>> { 53 | const routeFiles = await findRouteFiles(projectDir); 54 | const routesInfo = []; 55 | 56 | for (const filePath of routeFiles) { 57 | try { 58 | let implementationContent: string | null = null; 59 | 60 | try { 61 | implementationContent = await readFile(filePath, 'utf8'); 62 | } catch (error) { 63 | console.warn(`Could not read implementation file ${filePath}: ${error.message}`); 64 | continue; 65 | } 66 | 67 | // Parse the route information 68 | const apiPath = extractApiPath(filePath, implementationContent); 69 | const handlers = extractRouteHandlers(implementationContent, apiPath); 70 | const imports = extractImports(implementationContent); 71 | const validationSchemas = extractValidationSchemas(implementationContent); 72 | 73 | const routeInfo = RouteInfoSchema.parse({ 74 | filePath, 75 | implementationPath: filePath, 76 | apiPath, 77 | handlers, 78 | imports, 79 | validationSchemas 80 | }); 81 | 82 | routesInfo.push(routeInfo); 83 | } catch (error) { 84 | console.error(`Error processing route file ${filePath}:`, error); 85 | } 86 | } 87 | 88 | return routesInfo; 89 | } 90 | 91 | 92 | /** 93 | * Find all route.ts/js files directly in the project directory 94 | * @param {string} dir - Project directory to search 95 | * @returns {Promise>} - Array of file paths 96 | */ 97 | async function findRouteFiles(dir: string): Promise> { 98 | const routeFiles: string[] = []; 99 | 100 | async function searchDir(currentDir: string) { 101 | try { 102 | const entries = await readdir(currentDir, { withFileTypes: true }); 103 | 104 | for (const entry of entries) { 105 | const fullPath = path.join(currentDir, entry.name); 106 | 107 | if (entry.isDirectory()) { 108 | // Skip node_modules, .git, .next, and other common directories to avoid excessive scanning 109 | if (['node_modules', '.git', '.next', 'dist', 'build', 'out'].includes(entry.name)) { 110 | continue; 111 | } 112 | await searchDir(fullPath); 113 | } else if (entry.name === 'route.ts' || entry.name === 'route.js' || 114 | entry.name === 'route.tsx' || entry.name === 'route.jsx') { 115 | // Only include routes inside the app directory 116 | if (fullPath.includes(`${path.sep}app${path.sep}`)) { 117 | routeFiles.push(fullPath); 118 | } 119 | } 120 | } 121 | } catch (error) { 122 | console.error(`Error reading directory ${currentDir}:`, error.message); 123 | } 124 | } 125 | 126 | await searchDir(dir); 127 | return routeFiles; 128 | } 129 | 130 | /** 131 | * Extract the path to the actual implementation file from a type file 132 | * @param {string} fileContent - Content of the route.ts type file 133 | * @returns {string|null} - Path to the implementation file or null 134 | */ 135 | function extractImplementationPath(fileContent: string): string | null { 136 | const pathRegex = /\/\/ File: (.+)/; 137 | const match = fileContent.match(pathRegex); 138 | 139 | if (match && match[1]) { 140 | // Convert Windows path if necessary 141 | return match[1].replace(/\\/g, path.sep); 142 | } 143 | 144 | return null; 145 | } 146 | 147 | /** 148 | * Extract the API path from the file path and implementation 149 | * @param {string} filePath - Path to the route.ts type file 150 | * @param {string} implementationContent - Content of the implementation file 151 | * @returns {string} - API path 152 | */ 153 | function extractApiPath(filePath: string, implementationContent: string): string { 154 | // Try to extract from comments in the implementation file first 155 | const commentPathRegex = /\/\/ (GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS) (\/api\/[^\s]+) -/; 156 | const commentMatch = implementationContent.match(commentPathRegex); 157 | 158 | if (commentMatch && commentMatch[2]) { 159 | return commentMatch[2]; 160 | } 161 | 162 | // Fall back to deriving from file path 163 | const pathParts = filePath.split(path.sep); 164 | const apiIndex = pathParts.indexOf('api'); 165 | 166 | if (apiIndex !== -1) { 167 | const routeParts = pathParts.slice(apiIndex, pathParts.length - 1); 168 | return '/' + routeParts.join('/'); 169 | } 170 | 171 | return ''; 172 | } 173 | 174 | /** 175 | * Extract route handlers from the implementation file 176 | * @param {string} content - Content of the implementation file 177 | * @param {string} apiPath - API path 178 | * @returns {Array} - Array of route handlers 179 | */ 180 | function extractRouteHandlers(content: string, apiPath: string): Array> { 181 | const handlers: Array> = []; 182 | const methodRegex = /export\s+async\s+function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(([^)]*)\)/g; 183 | const statusCodeRegex = /status:\s*(\d+)/g; 184 | const commentRegex = /\/\/\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([^\s]+)\s*-\s*([^\n]+)/g; 185 | 186 | // Extract descriptions from comments 187 | const descriptions: Record = {}; 188 | let commentMatch: RegExpExecArray | null; 189 | while ((commentMatch = commentRegex.exec(content)) !== null) { 190 | const method = commentMatch[1]; 191 | const description = commentMatch[3].trim(); 192 | descriptions[method] = description; 193 | } 194 | 195 | // Extract handlers 196 | let methodMatch: RegExpExecArray | null = null; 197 | while ((methodMatch = methodRegex.exec(content)) !== null) { 198 | const method = methodMatch[1] as z.infer; 199 | const functionSignature = methodMatch[0]; 200 | const parameterString = methodMatch[2]; 201 | 202 | // Extract parameters 203 | const parameters = extractParameters(parameterString); 204 | 205 | // Extract status codes 206 | const handlerContent = extractFunctionBody(content, methodMatch.index); 207 | const statusCodes = []; 208 | let statusMatch; 209 | while ((statusMatch = statusCodeRegex.exec(handlerContent)) !== null) { 210 | statusCodes.push({ 211 | code: parseInt(statusMatch[1]), 212 | description: getStatusCodeDescription(parseInt(statusMatch[1])) 213 | }); 214 | } 215 | 216 | // Extract request body schema if applicable 217 | const requestBodySchema = extractRequestBodySchema(handlerContent); 218 | 219 | // Extract response type 220 | const responseType = extractResponseType(handlerContent); 221 | 222 | handlers.push({ 223 | method, 224 | path: apiPath, 225 | functionSignature, 226 | description: descriptions[method] || undefined, 227 | parameters, 228 | requestBodySchema, 229 | responseType, 230 | statusCodes 231 | }); 232 | } 233 | 234 | return handlers; 235 | } 236 | 237 | /** 238 | * Extract parameters from a function signature 239 | * @param {string} parameterString - Parameter string from function signature 240 | * @returns {Array} - Array of parameters 241 | */ 242 | function extractParameters(parameterString: string): Array> { 243 | const parameters: Array> = []; 244 | 245 | // Split by comma, but handle nested objects 246 | const paramParts = []; 247 | let currentPart = ''; 248 | let braceCount = 0; 249 | 250 | for (let i = 0; i < parameterString.length; i++) { 251 | const char = parameterString[i]; 252 | 253 | if (char === '{') braceCount++; 254 | else if (char === '}') braceCount--; 255 | 256 | if (char === ',' && braceCount === 0) { 257 | paramParts.push(currentPart.trim()); 258 | currentPart = ''; 259 | } else { 260 | currentPart += char; 261 | } 262 | } 263 | 264 | if (currentPart.trim()) { 265 | paramParts.push(currentPart.trim()); 266 | } 267 | 268 | for (const part of paramParts) { 269 | // Parse parameter name and type 270 | const matches = part.match(/(\w+)\s*:\s*(.+)/); 271 | 272 | if (matches) { 273 | const [, name, type] = matches; 274 | parameters.push({ 275 | name, 276 | type: type.trim(), 277 | required: !type.includes('?') && !type.includes('undefined') 278 | }); 279 | } 280 | } 281 | 282 | return parameters; 283 | } 284 | 285 | /** 286 | * Extract the function body 287 | * @param {string} content - Content of the implementation file 288 | * @param {number} startIndex - Start index of the function 289 | * @returns {string} - Function body 290 | */ 291 | function extractFunctionBody(content: string, startIndex: number): string { 292 | // Find the opening bracket 293 | let openBracketIndex = content.indexOf('{', startIndex); 294 | if (openBracketIndex === -1) return ''; 295 | 296 | // Find the closing bracket (accounting for nested brackets) 297 | let bracketCount = 1; 298 | let endIndex = openBracketIndex + 1; 299 | 300 | while (bracketCount > 0 && endIndex < content.length) { 301 | const char = content[endIndex]; 302 | if (char === '{') bracketCount++; 303 | else if (char === '}') bracketCount--; 304 | endIndex++; 305 | } 306 | 307 | return content.substring(openBracketIndex, endIndex); 308 | } 309 | 310 | /** 311 | * Extract request body schema from a function body 312 | * @param {string} functionBody - Function body 313 | * @returns {string|undefined} - Request body schema 314 | */ 315 | function extractRequestBodySchema(functionBody: string): string | undefined { 316 | // Look for schema validation 317 | const schemaRegex = /(\w+)\.safeParse\(body\)/; 318 | const match = functionBody.match(schemaRegex); 319 | 320 | if (match && match[1]) { 321 | return match[1]; 322 | } 323 | 324 | return undefined; 325 | } 326 | 327 | /** 328 | * Extract response type from a function body 329 | * @param {string} functionBody - Function body 330 | * @returns {string} - Response type 331 | */ 332 | function extractResponseType(functionBody: string): string { 333 | if (functionBody.includes('NextResponse.json(')) { 334 | return 'JSON'; 335 | } else if (functionBody.includes('new NextResponse(')) { 336 | return 'Raw'; 337 | } else if (functionBody.includes('Response.json(')) { 338 | return 'JSON'; 339 | } else if (functionBody.includes('new Response(')) { 340 | return 'Raw'; 341 | } 342 | 343 | return 'Unknown'; 344 | } 345 | 346 | /** 347 | * Extract imports from the implementation file 348 | * @param {string} content - Content of the implementation file 349 | * @returns {Array} - Array of imports 350 | */ 351 | function extractImports(content: string): Array { 352 | const imports = []; 353 | const importRegex = /import\s+(.+?)\s+from\s+['"](.+?)['"];?/g; 354 | 355 | let match; 356 | while ((match = importRegex.exec(content)) !== null) { 357 | imports.push(`import ${match[1]} from '${match[2]}'`); 358 | } 359 | 360 | return imports; 361 | } 362 | 363 | /** 364 | * Extract validation schemas from the implementation file 365 | * @param {string} content - Content of the implementation file 366 | * @returns {Array} - Array of validation schema names 367 | */ 368 | function extractValidationSchemas(content: string): Array { 369 | const schemas: string[] = []; 370 | const schemaRegex = /(\w+)Schema\.safeParse/g; 371 | 372 | let match; 373 | while ((match = schemaRegex.exec(content)) !== null) { 374 | schemas.push(match[1]); 375 | } 376 | 377 | return [...new Set(schemas)]; // Remove duplicates 378 | } 379 | 380 | /** 381 | * Get a description for an HTTP status code 382 | * @param {number} code - HTTP status code 383 | * @returns {string} - Description 384 | */ 385 | function getStatusCodeDescription(code: number): string { 386 | const statusCodes: Record = { 387 | 200: 'OK - Successful request', 388 | 201: 'Created - Resource created successfully', 389 | 204: 'No Content - Request succeeded with no response body', 390 | 400: 'Bad Request - Invalid request data', 391 | 401: 'Unauthorized - Authentication required', 392 | 403: 'Forbidden - Permission denied', 393 | 404: 'Not Found - Resource not found', 394 | 500: 'Internal Server Error - Server error occurred' 395 | }; 396 | 397 | return statusCodes[code] || 'Unknown status code'; 398 | } 399 | 400 | /** 401 | * Run the analyzer and print the results 402 | */ 403 | export async function getRoutersInfo(projectDir: string) { 404 | try { 405 | // remove leading slashes or backslashes 406 | const cleanedProjectPath = projectDir.replace(/^[\/\\]+/, ''); 407 | const routesInfo = await analyzeNextRoutes(cleanedProjectPath); 408 | 409 | console.log('API Routes Analysis:'); 410 | console.log(JSON.stringify(routesInfo, null, 2)); 411 | 412 | // Also output a more human-readable summary 413 | console.log('\nAPI Routes Summary:'); 414 | for (const route of routesInfo) { 415 | console.log(`\n${route.apiPath}`); 416 | for (const handler of route.handlers) { 417 | console.log(` ${handler.method} - ${handler.description || 'No description'}`); 418 | console.log(` Parameters: ${handler.parameters.length ? handler.parameters.map(p => p.name).join(', ') : 'None'}`); 419 | console.log(` Status Codes: ${handler.statusCodes.map(s => s.code).join(', ')}`); 420 | } 421 | } 422 | 423 | return routesInfo; 424 | } catch (error) { 425 | console.error('Error analyzing routes:', error); 426 | throw error; 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-mcp-server", 3 | "version": "0.2.2", 4 | "description": "Help LLMs to understand your Next.js project better", 5 | "main": "dist/index.js", 6 | "author": "jazelly (xzha4350@gmail.com)", 7 | "license": "MIT", 8 | "type": "module", 9 | "workspaces": [ 10 | "playground" 11 | ], 12 | "files": [ 13 | "dist/", 14 | "LICENSE" 15 | ], 16 | "scripts": { 17 | "build": "shx rm -rf dist && tsc && shx chmod +x dist/*.js", 18 | "watch": "tsc --watch", 19 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" 20 | }, 21 | "keywords": [ 22 | "mcp", 23 | "nextjs", 24 | "mcp-server" 25 | ], 26 | "dependencies": { 27 | "@modelcontextprotocol/sdk": "1.7.0", 28 | "express": "^5.0.1", 29 | "zod": "^3.22.4", 30 | "zod-to-json-schema": "^3.23.5" 31 | }, 32 | "devDependencies": { 33 | "@jest/globals": "^29.7.0", 34 | "@types/express": "^5.0.1", 35 | "@types/jest": "^29.5.12", 36 | "@types/node": "^22", 37 | "jest": "^29.7.0", 38 | "shx": "^0.3.4", 39 | "ts-jest": "^29.1.2", 40 | "typescript": "^5.6.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /playground/.env.local: -------------------------------------------------------------------------------- 1 | # API Configuration 2 | API_URL=http://localhost:3000/api 3 | 4 | # Authentication 5 | JWT_SECRET=your-jwt-secret-key 6 | JWT_EXPIRES_IN=1h 7 | 8 | # Stripe Configuration (for webhook testing) 9 | STRIPE_SECRET_KEY=sk_test_your_test_key 10 | STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret 11 | 12 | # This is a mock environment file for the playground 13 | # In a real application, never commit this file to version control -------------------------------------------------------------------------------- /playground/app/api/products/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { productDb, categoryDb } from '@/lib/db'; 3 | import { ProductIdSchema, ProductUpdateSchema } from '@/lib/validation'; 4 | import { ApiResponse, Product, ProductUpdateInput } from '@/types'; 5 | 6 | interface RouteContext { 7 | params: { 8 | id: string; 9 | }; 10 | } 11 | 12 | /** 13 | * GET /api/products/[id] - Get a product by ID 14 | * @param request - The incoming request object 15 | * @param context - Contains route parameters 16 | * @returns {Promise} JSON response with product data 17 | */ 18 | export async function GET( 19 | request: NextRequest, 20 | context: RouteContext 21 | ): Promise>> { 22 | try { 23 | const { id } = context.params; 24 | 25 | // Validate ID format 26 | const validatedId = ProductIdSchema.safeParse({ id }); 27 | 28 | if (!validatedId.success) { 29 | return NextResponse.json( 30 | { 31 | error: 'Invalid product ID format', 32 | status: 400 33 | }, 34 | { status: 400 } 35 | ); 36 | } 37 | 38 | // Find product by ID 39 | const product = await productDb.getById(id); 40 | 41 | if (!product) { 42 | return NextResponse.json( 43 | { 44 | error: 'Product not found', 45 | status: 404 46 | }, 47 | { status: 404 } 48 | ); 49 | } 50 | 51 | return NextResponse.json( 52 | { 53 | data: product, 54 | status: 200 55 | }, 56 | { status: 200 } 57 | ); 58 | } catch (error) { 59 | console.error('Error fetching product:', error); 60 | return NextResponse.json( 61 | { 62 | error: 'Internal server error', 63 | status: 500 64 | }, 65 | { status: 500 } 66 | ); 67 | } 68 | } 69 | 70 | /** 71 | * PUT /api/products/[id] - Update a product 72 | * @param request - The incoming request object with updated product data 73 | * @param context - Contains route parameters 74 | * @returns {Promise} JSON response with updated product data 75 | */ 76 | export async function PUT( 77 | request: NextRequest, 78 | context: RouteContext 79 | ): Promise>> { 80 | try { 81 | const { id } = context.params; 82 | const body: ProductUpdateInput = await request.json(); 83 | 84 | // Validate ID format 85 | const validatedId = ProductIdSchema.safeParse({ id }); 86 | 87 | if (!validatedId.success) { 88 | return NextResponse.json( 89 | { 90 | error: 'Invalid product ID format', 91 | status: 400 92 | }, 93 | { status: 400 } 94 | ); 95 | } 96 | 97 | // Validate request body 98 | const validatedData = ProductUpdateSchema.safeParse(body); 99 | 100 | if (!validatedData.success) { 101 | return NextResponse.json( 102 | { 103 | error: 'Invalid request data', 104 | message: validatedData.error.message, 105 | status: 400 106 | }, 107 | { status: 400 } 108 | ); 109 | } 110 | 111 | // Check if category exists if categoryId is provided 112 | if (body.categoryId) { 113 | const category = await categoryDb.getById(body.categoryId); 114 | 115 | if (!category) { 116 | return NextResponse.json( 117 | { 118 | error: 'Category not found', 119 | status: 400 120 | }, 121 | { status: 400 } 122 | ); 123 | } 124 | } 125 | 126 | // Update product 127 | const updatedProduct = await productDb.update(id, validatedData.data); 128 | 129 | if (!updatedProduct) { 130 | return NextResponse.json( 131 | { 132 | error: 'Product not found', 133 | status: 404 134 | }, 135 | { status: 404 } 136 | ); 137 | } 138 | 139 | return NextResponse.json( 140 | { 141 | data: updatedProduct, 142 | message: 'Product updated successfully', 143 | status: 200 144 | }, 145 | { status: 200 } 146 | ); 147 | } catch (error) { 148 | console.error('Error updating product:', error); 149 | return NextResponse.json( 150 | { 151 | error: 'Internal server error', 152 | status: 500 153 | }, 154 | { status: 500 } 155 | ); 156 | } 157 | } 158 | 159 | /** 160 | * DELETE /api/products/[id] - Delete a product 161 | * @param request - The incoming request object 162 | * @param context - Contains route parameters 163 | * @returns {Promise} JSON response confirming deletion 164 | */ 165 | export async function DELETE( 166 | request: NextRequest, 167 | context: RouteContext 168 | ): Promise>> { 169 | try { 170 | const { id } = context.params; 171 | 172 | // Validate ID format 173 | const validatedId = ProductIdSchema.safeParse({ id }); 174 | 175 | if (!validatedId.success) { 176 | return NextResponse.json( 177 | { 178 | error: 'Invalid product ID format', 179 | status: 400 180 | }, 181 | { status: 400 } 182 | ); 183 | } 184 | 185 | // Delete product 186 | const deleted = await productDb.delete(id); 187 | 188 | if (!deleted) { 189 | return NextResponse.json( 190 | { 191 | error: 'Product not found', 192 | status: 404 193 | }, 194 | { status: 404 } 195 | ); 196 | } 197 | 198 | return NextResponse.json( 199 | { 200 | message: 'Product deleted successfully', 201 | status: 200 202 | }, 203 | { status: 200 } 204 | ); 205 | } catch (error) { 206 | console.error('Error deleting product:', error); 207 | return NextResponse.json( 208 | { 209 | error: 'Internal server error', 210 | status: 500 211 | }, 212 | { status: 500 } 213 | ); 214 | } 215 | } -------------------------------------------------------------------------------- /playground/app/api/products/categories/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { categoryDb } from '@/lib/db'; 3 | import { CategorySchema } from '@/lib/validation'; 4 | import { ApiResponse, Category } from '@/types'; 5 | 6 | /** 7 | * GET /api/products/categories - Get all product categories 8 | * @param request - The incoming request object 9 | * @returns {Promise} JSON response with categories data 10 | */ 11 | export async function GET( 12 | request: NextRequest 13 | ): Promise>> { 14 | try { 15 | const categories = await categoryDb.getAll(); 16 | 17 | return NextResponse.json( 18 | { 19 | data: categories, 20 | status: 200 21 | }, 22 | { status: 200 } 23 | ); 24 | } catch (error) { 25 | console.error('Error fetching categories:', error); 26 | return NextResponse.json( 27 | { 28 | error: 'Internal server error', 29 | status: 500 30 | }, 31 | { status: 500 } 32 | ); 33 | } 34 | } 35 | 36 | /** 37 | * POST /api/products/categories - Create a new category 38 | * @param request - The incoming request object with category data 39 | * @returns {Promise} JSON response with the created category 40 | */ 41 | export async function POST( 42 | request: NextRequest 43 | ): Promise>> { 44 | try { 45 | const body = await request.json(); 46 | 47 | // Validate request body 48 | const validatedData = CategorySchema.safeParse(body); 49 | 50 | if (!validatedData.success) { 51 | return NextResponse.json( 52 | { 53 | error: 'Invalid request data', 54 | message: validatedData.error.message, 55 | status: 400 56 | }, 57 | { status: 400 } 58 | ); 59 | } 60 | 61 | // Check if category name already exists 62 | const allCategories = await categoryDb.getAll(); 63 | const nameExists = allCategories.some( 64 | category => category.name.toLowerCase() === body.name.toLowerCase() 65 | ); 66 | 67 | if (nameExists) { 68 | return NextResponse.json( 69 | { 70 | error: 'Category name already exists', 71 | status: 409 72 | }, 73 | { status: 409 } 74 | ); 75 | } 76 | 77 | // Create new category 78 | const newCategory = await categoryDb.create(validatedData.data); 79 | 80 | return NextResponse.json( 81 | { 82 | data: newCategory, 83 | message: 'Category created successfully', 84 | status: 201 85 | }, 86 | { status: 201 } 87 | ); 88 | } catch (error) { 89 | console.error('Error creating category:', error); 90 | return NextResponse.json( 91 | { 92 | error: 'Internal server error', 93 | status: 500 94 | }, 95 | { status: 500 } 96 | ); 97 | } 98 | } -------------------------------------------------------------------------------- /playground/app/api/products/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { productDb, categoryDb } from '@/lib/db'; 3 | import { PaginationQuerySchema, ProductCreateSchema } from '@/lib/validation'; 4 | import { ApiResponse, Product, ProductCreateInput } from '@/types'; 5 | 6 | /** 7 | * GET /api/products - Get a list of products with pagination 8 | * @param request - The incoming request object 9 | * @returns {Promise} JSON response with products data 10 | */ 11 | export async function GET( 12 | request: NextRequest 13 | ): Promise>> { 14 | try { 15 | // Parse query parameters for pagination 16 | const url = new URL(request.url); 17 | const page = url.searchParams.get('page'); 18 | const limit = url.searchParams.get('limit'); 19 | 20 | const queryParams = PaginationQuerySchema.safeParse({ 21 | page: page ? parseInt(page) : 1, 22 | limit: limit ? parseInt(limit) : 10 23 | }); 24 | 25 | if (!queryParams.success) { 26 | return NextResponse.json( 27 | { 28 | error: 'Invalid query parameters', 29 | status: 400 30 | }, 31 | { status: 400 } 32 | ); 33 | } 34 | 35 | const { page: validPage, limit: validLimit } = queryParams.data; 36 | const { products, total } = await productDb.getAll(validPage, validLimit); 37 | 38 | return NextResponse.json( 39 | { 40 | data: { products, total }, 41 | status: 200 42 | }, 43 | { status: 200 } 44 | ); 45 | } catch (error) { 46 | console.error('Error fetching products:', error); 47 | return NextResponse.json( 48 | { 49 | error: 'Internal server error', 50 | status: 500 51 | }, 52 | { status: 500 } 53 | ); 54 | } 55 | } 56 | 57 | /** 58 | * POST /api/products - Create a new product 59 | * @param request - The incoming request object with product data 60 | * @returns {Promise} JSON response with the created product 61 | */ 62 | export async function POST( 63 | request: NextRequest 64 | ): Promise>> { 65 | try { 66 | const body: ProductCreateInput = await request.json(); 67 | 68 | // Validate request body 69 | const validatedData = ProductCreateSchema.safeParse(body); 70 | 71 | if (!validatedData.success) { 72 | return NextResponse.json( 73 | { 74 | error: 'Invalid request data', 75 | message: validatedData.error.message, 76 | status: 400 77 | }, 78 | { status: 400 } 79 | ); 80 | } 81 | 82 | // Check if category exists 83 | const category = await categoryDb.getById(body.categoryId); 84 | 85 | if (!category) { 86 | return NextResponse.json( 87 | { 88 | error: 'Category not found', 89 | status: 400 90 | }, 91 | { status: 400 } 92 | ); 93 | } 94 | 95 | // Create new product 96 | const newProduct = await productDb.create(validatedData.data); 97 | 98 | return NextResponse.json( 99 | { 100 | data: newProduct, 101 | message: 'Product created successfully', 102 | status: 201 103 | }, 104 | { status: 201 } 105 | ); 106 | } catch (error) { 107 | console.error('Error creating product:', error); 108 | return NextResponse.json( 109 | { 110 | error: 'Internal server error', 111 | status: 500 112 | }, 113 | { status: 500 } 114 | ); 115 | } 116 | } -------------------------------------------------------------------------------- /playground/app/api/users/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { userDb } from '@/lib/db'; 3 | import { UserIdSchema, UserUpdateSchema } from '@/lib/validation'; 4 | import { ApiResponse, User, UserUpdateInput } from '@/types'; 5 | 6 | interface RouteContext { 7 | params: { 8 | id: string; 9 | }; 10 | } 11 | 12 | /** 13 | * GET /api/users/[id] - Get a user by ID 14 | * @param request - The incoming request object 15 | * @param context - Contains route parameters 16 | * @returns {Promise} JSON response with user data 17 | */ 18 | export async function GET( 19 | request: NextRequest, 20 | context: RouteContext 21 | ): Promise>> { 22 | try { 23 | const { id } = context.params; 24 | 25 | // Validate ID format 26 | const validatedId = UserIdSchema.safeParse({ id }); 27 | 28 | if (!validatedId.success) { 29 | return NextResponse.json( 30 | { 31 | error: 'Invalid user ID format', 32 | status: 400 33 | }, 34 | { status: 400 } 35 | ); 36 | } 37 | 38 | // Find user by ID 39 | const user = await userDb.getById(id); 40 | 41 | if (!user) { 42 | return NextResponse.json( 43 | { 44 | error: 'User not found', 45 | status: 404 46 | }, 47 | { status: 404 } 48 | ); 49 | } 50 | 51 | return NextResponse.json( 52 | { 53 | data: user, 54 | status: 200 55 | }, 56 | { status: 200 } 57 | ); 58 | } catch (error) { 59 | console.error('Error fetching user:', error); 60 | return NextResponse.json( 61 | { 62 | error: 'Internal server error', 63 | status: 500 64 | }, 65 | { status: 500 } 66 | ); 67 | } 68 | } 69 | 70 | /** 71 | * PUT /api/users/[id] - Update a user 72 | * @param request - The incoming request object with updated user data 73 | * @param context - Contains route parameters 74 | * @returns {Promise} JSON response with updated user data 75 | */ 76 | export async function PUT( 77 | request: NextRequest, 78 | context: RouteContext 79 | ): Promise>> { 80 | try { 81 | const { id } = context.params; 82 | const body: UserUpdateInput = await request.json(); 83 | 84 | // Validate ID format 85 | const validatedId = UserIdSchema.safeParse({ id }); 86 | 87 | if (!validatedId.success) { 88 | return NextResponse.json( 89 | { 90 | error: 'Invalid user ID format', 91 | status: 400 92 | }, 93 | { status: 400 } 94 | ); 95 | } 96 | 97 | // Validate request body 98 | const validatedData = UserUpdateSchema.safeParse(body); 99 | 100 | if (!validatedData.success) { 101 | return NextResponse.json( 102 | { 103 | error: 'Invalid request data', 104 | message: validatedData.error.message, 105 | status: 400 106 | }, 107 | { status: 400 } 108 | ); 109 | } 110 | 111 | // Update user 112 | const updatedUser = await userDb.update(id, validatedData.data); 113 | 114 | if (!updatedUser) { 115 | return NextResponse.json( 116 | { 117 | error: 'User not found', 118 | status: 404 119 | }, 120 | { status: 404 } 121 | ); 122 | } 123 | 124 | return NextResponse.json( 125 | { 126 | data: updatedUser, 127 | message: 'User updated successfully', 128 | status: 200 129 | }, 130 | { status: 200 } 131 | ); 132 | } catch (error) { 133 | console.error('Error updating user:', error); 134 | return NextResponse.json( 135 | { 136 | error: 'Internal server error', 137 | status: 500 138 | }, 139 | { status: 500 } 140 | ); 141 | } 142 | } 143 | 144 | /** 145 | * DELETE /api/users/[id] - Delete a user 146 | * @param request - The incoming request object 147 | * @param context - Contains route parameters 148 | * @returns {Promise} JSON response confirming deletion 149 | */ 150 | export async function DELETE( 151 | request: NextRequest, 152 | context: RouteContext 153 | ): Promise>> { 154 | try { 155 | const { id } = context.params; 156 | 157 | // Validate ID format 158 | const validatedId = UserIdSchema.safeParse({ id }); 159 | 160 | if (!validatedId.success) { 161 | return NextResponse.json( 162 | { 163 | error: 'Invalid user ID format', 164 | status: 400 165 | }, 166 | { status: 400 } 167 | ); 168 | } 169 | 170 | // Delete user 171 | const deleted = await userDb.delete(id); 172 | 173 | if (!deleted) { 174 | return NextResponse.json( 175 | { 176 | error: 'User not found', 177 | status: 404 178 | }, 179 | { status: 404 } 180 | ); 181 | } 182 | 183 | return NextResponse.json( 184 | { 185 | message: 'User deleted successfully', 186 | status: 200 187 | }, 188 | { status: 200 } 189 | ); 190 | } catch (error) { 191 | console.error('Error deleting user:', error); 192 | return NextResponse.json( 193 | { 194 | error: 'Internal server error', 195 | status: 500 196 | }, 197 | { status: 500 } 198 | ); 199 | } 200 | } -------------------------------------------------------------------------------- /playground/app/api/users/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { userDb } from '@/lib/db'; 3 | import { PaginationQuerySchema, UserCreateSchema } from '@/lib/validation'; 4 | import { ApiResponse, User, UserCreateInput } from '@/types'; 5 | 6 | /** 7 | * GET /api/users - Get a list of users with pagination 8 | * @param request - The incoming request object 9 | * @returns {Promise} JSON response with users data 10 | */ 11 | export async function GET( 12 | request: NextRequest 13 | ): Promise>> { 14 | try { 15 | // Parse query parameters for pagination 16 | const url = new URL(request.url); 17 | const page = url.searchParams.get('page'); 18 | const limit = url.searchParams.get('limit'); 19 | 20 | const queryParams = PaginationQuerySchema.safeParse({ 21 | page: page ? parseInt(page) : 1, 22 | limit: limit ? parseInt(limit) : 10 23 | }); 24 | 25 | if (!queryParams.success) { 26 | return NextResponse.json( 27 | { 28 | error: 'Invalid query parameters', 29 | status: 400 30 | }, 31 | { status: 400 } 32 | ); 33 | } 34 | 35 | const { page: validPage, limit: validLimit } = queryParams.data; 36 | const { users, total } = await userDb.getAll(validPage, validLimit); 37 | 38 | return NextResponse.json( 39 | { 40 | data: { users, total }, 41 | status: 200 42 | }, 43 | { status: 200 } 44 | ); 45 | } catch (error) { 46 | console.error('Error fetching users:', error); 47 | return NextResponse.json( 48 | { 49 | error: 'Internal server error', 50 | status: 500 51 | }, 52 | { status: 500 } 53 | ); 54 | } 55 | } 56 | 57 | /** 58 | * POST /api/users - Create a new user 59 | * @param request - The incoming request object with user data 60 | * @returns {Promise} JSON response with the created user 61 | */ 62 | export async function POST( 63 | request: NextRequest 64 | ): Promise>> { 65 | try { 66 | const body: UserCreateInput = await request.json(); 67 | 68 | // Validate request body 69 | const validatedData = UserCreateSchema.safeParse(body); 70 | 71 | if (!validatedData.success) { 72 | return NextResponse.json( 73 | { 74 | error: 'Invalid request data', 75 | message: validatedData.error.message, 76 | status: 400 77 | }, 78 | { status: 400 } 79 | ); 80 | } 81 | 82 | // Check if email already exists 83 | const existingUsers = await userDb.getAll(); 84 | const emailExists = existingUsers.users.some(user => user.email === body.email); 85 | 86 | if (emailExists) { 87 | return NextResponse.json( 88 | { 89 | error: 'Email already in use', 90 | status: 409 91 | }, 92 | { status: 409 } 93 | ); 94 | } 95 | 96 | // Create new user 97 | const newUser = await userDb.create(validatedData.data); 98 | 99 | return NextResponse.json( 100 | { 101 | data: newUser, 102 | message: 'User created successfully', 103 | status: 201 104 | }, 105 | { status: 201 } 106 | ); 107 | } catch (error) { 108 | console.error('Error creating user:', error); 109 | return NextResponse.json( 110 | { 111 | error: 'Internal server error', 112 | status: 500 113 | }, 114 | { status: 500 } 115 | ); 116 | } 117 | } -------------------------------------------------------------------------------- /playground/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /playground/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | 3 | export const metadata = { 4 | title: 'Next.js', 5 | description: 'Generated by Next.js', 6 | } 7 | 8 | export default function RootLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode 12 | }) { 13 | return ( 14 | 15 | 16 |
17 | 25 |
26 |
27 | {children} 28 |
29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /playground/app/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |

Next.js API Routes Playground

7 | 8 |

9 | This is a simple playground for testing Next.js API routes. The following API endpoints are available: 10 |

11 | 12 |
13 |
14 |

User Endpoints

15 |
    16 |
  • GET /api/users - Get all users (with pagination)
  • 17 |
  • POST /api/users - Create a new user
  • 18 |
  • GET /api/users/[id] - Get a user by ID
  • 19 |
  • PUT /api/users/[id] - Update a user
  • 20 |
  • DELETE /api/users/[id] - Delete a user
  • 21 |
22 |
23 | 24 |
25 |

Product Endpoints

26 |
    27 |
  • GET /api/products - Get all products (with pagination)
  • 28 |
  • POST /api/products - Create a new product
  • 29 |
  • GET /api/products/[id] - Get a product by ID
  • 30 |
  • PUT /api/products/[id] - Update a product
  • 31 |
  • DELETE /api/products/[id] - Delete a product
  • 32 |
  • GET /api/products/categories - Get all product categories
  • 33 |
  • POST /api/products/categories - Create a new product category
  • 34 |
35 |
36 |
37 |
38 | ); 39 | } -------------------------------------------------------------------------------- /playground/app/products/categories/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | 5 | interface Category { 6 | id: string; 7 | name: string; 8 | description: string; 9 | createdAt: string; 10 | } 11 | 12 | export default function CategoriesPage() { 13 | const [categories, setCategories] = useState([]); 14 | const [loading, setLoading] = useState(true); 15 | const [error, setError] = useState(''); 16 | 17 | useEffect(() => { 18 | const fetchCategories = async () => { 19 | try { 20 | setLoading(true); 21 | const res = await fetch('/api/products/categories'); 22 | 23 | if (!res.ok) { 24 | throw new Error('Failed to fetch categories'); 25 | } 26 | 27 | const data = await res.json(); 28 | setCategories(data.data); 29 | } catch (err) { 30 | setError('Error fetching categories. Please try again.'); 31 | console.error(err); 32 | } finally { 33 | setLoading(false); 34 | } 35 | }; 36 | 37 | fetchCategories(); 38 | }, []); 39 | 40 | return ( 41 |
42 |
43 |

Product Categories

44 | 47 |
48 | 49 | {loading ? ( 50 |
51 |
52 |
53 | ) : error ? ( 54 |
55 | {error} 56 |
57 | ) : ( 58 |
59 | {categories.length === 0 ? ( 60 |

No categories found.

61 | ) : ( 62 | categories.map((category) => ( 63 |
64 |

{category.name}

65 |

{category.description || 'No description available'}

66 |
67 | 68 | Created: {new Date(category.createdAt).toLocaleDateString()} 69 | 70 | 73 |
74 |
75 | )) 76 | )} 77 |
78 | )} 79 |
80 | ); 81 | } -------------------------------------------------------------------------------- /playground/app/products/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import Link from 'next/link'; 5 | 6 | interface Product { 7 | id: string; 8 | name: string; 9 | price: number; 10 | description: string; 11 | categoryId: string; 12 | createdAt: string; 13 | } 14 | 15 | export default function ProductsPage() { 16 | const [products, setProducts] = useState([]); 17 | const [loading, setLoading] = useState(true); 18 | const [error, setError] = useState(''); 19 | const [page, setPage] = useState(1); 20 | const [total, setTotal] = useState(0); 21 | const limit = 10; 22 | 23 | useEffect(() => { 24 | const fetchProducts = async () => { 25 | try { 26 | setLoading(true); 27 | const res = await fetch(`/api/products?page=${page}&limit=${limit}`); 28 | 29 | if (!res.ok) { 30 | throw new Error('Failed to fetch products'); 31 | } 32 | 33 | const data = await res.json(); 34 | setProducts(data.data.products); 35 | setTotal(data.data.total); 36 | } catch (err) { 37 | setError('Error fetching products. Please try again.'); 38 | console.error(err); 39 | } finally { 40 | setLoading(false); 41 | } 42 | }; 43 | 44 | fetchProducts(); 45 | }, [page]); 46 | 47 | const totalPages = Math.ceil(total / limit); 48 | 49 | return ( 50 |
51 |
52 |

Products

53 | 56 |
57 | 58 | {loading ? ( 59 |
60 |
61 |
62 | ) : error ? ( 63 |
64 | {error} 65 |
66 | ) : ( 67 | <> 68 |
69 | {products.map((product) => ( 70 |
71 |

{product.name}

72 |

${product.price.toFixed(2)}

73 |

{product.description}

74 |
75 | 76 | View Details 77 | 78 |
79 |
80 | ))} 81 |
82 | 83 | {totalPages > 1 && ( 84 |
85 | 104 |
105 | )} 106 | 107 | )} 108 |
109 | ); 110 | } -------------------------------------------------------------------------------- /playground/app/users/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import Link from 'next/link'; 5 | 6 | interface User { 7 | id: string; 8 | name: string; 9 | email: string; 10 | role: string; 11 | createdAt: string; 12 | } 13 | 14 | export default function UsersPage() { 15 | const [users, setUsers] = useState([]); 16 | const [loading, setLoading] = useState(true); 17 | const [error, setError] = useState(''); 18 | const [page, setPage] = useState(1); 19 | const [total, setTotal] = useState(0); 20 | const limit = 10; 21 | 22 | useEffect(() => { 23 | const fetchUsers = async () => { 24 | try { 25 | setLoading(true); 26 | const res = await fetch(`/api/users?page=${page}&limit=${limit}`); 27 | 28 | if (!res.ok) { 29 | throw new Error('Failed to fetch users'); 30 | } 31 | 32 | const data = await res.json(); 33 | setUsers(data.data.users); 34 | setTotal(data.data.total); 35 | } catch (err) { 36 | setError('Error fetching users. Please try again.'); 37 | console.error(err); 38 | } finally { 39 | setLoading(false); 40 | } 41 | }; 42 | 43 | fetchUsers(); 44 | }, [page]); 45 | 46 | const totalPages = Math.ceil(total / limit); 47 | 48 | return ( 49 |
50 |
51 |

Users

52 | 55 |
56 | 57 | {loading ? ( 58 |
59 |
60 |
61 | ) : error ? ( 62 |
63 | {error} 64 |
65 | ) : ( 66 | <> 67 |
68 | 69 | 70 | 71 | 74 | 77 | 80 | 83 | 86 | 87 | 88 | 89 | {users.length === 0 ? ( 90 | 91 | 94 | 95 | ) : ( 96 | users.map((user) => ( 97 | 98 | 101 | 104 | 109 | 112 | 120 | 121 | )) 122 | )} 123 | 124 |
72 | Name 73 | 75 | Email 76 | 78 | Role 79 | 81 | Joined 82 | 84 | Actions 85 |
92 | No users found. 93 |
99 |
{user.name}
100 |
102 |
{user.email}
103 |
105 | 106 | {user.role} 107 | 108 | 110 | {new Date(user.createdAt).toLocaleDateString()} 111 | 113 | 114 | View 115 | 116 | 119 |
125 |
126 | 127 | {totalPages > 1 && ( 128 |
129 | 148 |
149 | )} 150 | 151 | )} 152 |
153 | ); 154 | } -------------------------------------------------------------------------------- /playground/lib/db.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a mock database service for the playground 3 | * In a real application, you would use a real database like PostgreSQL, MongoDB, etc. 4 | */ 5 | import { 6 | User, 7 | UserCreateInput, 8 | UserUpdateInput, 9 | Product, 10 | ProductCreateInput, 11 | ProductUpdateInput, 12 | Category, 13 | LoginInput 14 | } from '@/types'; 15 | 16 | // Mock data 17 | const users: User[] = [ 18 | { 19 | id: '550e8400-e29b-41d4-a716-446655440000', 20 | name: 'Admin User', 21 | email: 'admin@example.com', 22 | role: 'admin', 23 | createdAt: new Date('2023-01-01'), 24 | updatedAt: new Date('2023-01-01') 25 | }, 26 | { 27 | id: '550e8400-e29b-41d4-a716-446655440001', 28 | name: 'Test User', 29 | email: 'user@example.com', 30 | role: 'user', 31 | createdAt: new Date('2023-01-02'), 32 | updatedAt: new Date('2023-01-02') 33 | } 34 | ]; 35 | 36 | const categories: Category[] = [ 37 | { 38 | id: '550e8400-e29b-41d4-a716-446655440002', 39 | name: 'Electronics', 40 | description: 'Electronic devices and accessories' 41 | }, 42 | { 43 | id: '550e8400-e29b-41d4-a716-446655440003', 44 | name: 'Books', 45 | description: 'Books and publications' 46 | } 47 | ]; 48 | 49 | const products: Product[] = [ 50 | { 51 | id: '550e8400-e29b-41d4-a716-446655440004', 52 | name: 'Laptop', 53 | description: 'High-performance laptop', 54 | price: 1299.99, 55 | stock: 10, 56 | categoryId: '550e8400-e29b-41d4-a716-446655440002', 57 | createdAt: new Date('2023-01-03'), 58 | updatedAt: new Date('2023-01-03') 59 | }, 60 | { 61 | id: '550e8400-e29b-41d4-a716-446655440005', 62 | name: 'Programming Book', 63 | description: 'Learn programming with this book', 64 | price: 29.99, 65 | stock: 50, 66 | categoryId: '550e8400-e29b-41d4-a716-446655440003', 67 | createdAt: new Date('2023-01-04'), 68 | updatedAt: new Date('2023-01-04') 69 | } 70 | ]; 71 | 72 | // Helper function to generate UUID 73 | const generateUUID = (): string => { 74 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 75 | const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 76 | return v.toString(16); 77 | }); 78 | }; 79 | 80 | // User database functions 81 | export const userDb = { 82 | getAll: async (page = 1, limit = 10): Promise<{ users: User[], total: number }> => { 83 | const start = (page - 1) * limit; 84 | const end = start + limit; 85 | return { 86 | users: users.slice(start, end), 87 | total: users.length 88 | }; 89 | }, 90 | 91 | getById: async (id: string): Promise => { 92 | return users.find(user => user.id === id) || null; 93 | }, 94 | 95 | create: async (data: UserCreateInput): Promise => { 96 | const newUser: User = { 97 | id: generateUUID(), 98 | ...data, 99 | role: data.role || 'user', 100 | createdAt: new Date(), 101 | updatedAt: new Date() 102 | }; 103 | users.push(newUser); 104 | return newUser; 105 | }, 106 | 107 | update: async (id: string, data: UserUpdateInput): Promise => { 108 | const index = users.findIndex(user => user.id === id); 109 | if (index === -1) return null; 110 | 111 | const updatedUser = { 112 | ...users[index], 113 | ...data, 114 | updatedAt: new Date() 115 | }; 116 | 117 | users[index] = updatedUser; 118 | return updatedUser; 119 | }, 120 | 121 | delete: async (id: string): Promise => { 122 | const index = users.findIndex(user => user.id === id); 123 | if (index === -1) return false; 124 | 125 | users.splice(index, 1); 126 | return true; 127 | }, 128 | 129 | authenticate: async (credentials: LoginInput): Promise => { 130 | // In a real app, you would check password hash 131 | const user = users.find(u => u.email === credentials.email); 132 | return user || null; 133 | } 134 | }; 135 | 136 | // Product database functions 137 | export const productDb = { 138 | getAll: async (page = 1, limit = 10): Promise<{ products: Product[], total: number }> => { 139 | const start = (page - 1) * limit; 140 | const end = start + limit; 141 | return { 142 | products: products.slice(start, end), 143 | total: products.length 144 | }; 145 | }, 146 | 147 | getById: async (id: string): Promise => { 148 | return products.find(product => product.id === id) || null; 149 | }, 150 | 151 | create: async (data: ProductCreateInput): Promise => { 152 | const newProduct: Product = { 153 | id: generateUUID(), 154 | ...data, 155 | createdAt: new Date(), 156 | updatedAt: new Date() 157 | }; 158 | products.push(newProduct); 159 | return newProduct; 160 | }, 161 | 162 | update: async (id: string, data: ProductUpdateInput): Promise => { 163 | const index = products.findIndex(product => product.id === id); 164 | if (index === -1) return null; 165 | 166 | const updatedProduct = { 167 | ...products[index], 168 | ...data, 169 | updatedAt: new Date() 170 | }; 171 | 172 | products[index] = updatedProduct; 173 | return updatedProduct; 174 | }, 175 | 176 | delete: async (id: string): Promise => { 177 | const index = products.findIndex(product => product.id === id); 178 | if (index === -1) return false; 179 | 180 | products.splice(index, 1); 181 | return true; 182 | }, 183 | 184 | getByCategoryId: async (categoryId: string): Promise => { 185 | return products.filter(product => product.categoryId === categoryId); 186 | } 187 | }; 188 | 189 | // Category database functions 190 | export const categoryDb = { 191 | getAll: async (): Promise => { 192 | return categories; 193 | }, 194 | 195 | getById: async (id: string): Promise => { 196 | return categories.find(category => category.id === id) || null; 197 | }, 198 | 199 | create: async (data: { name: string; description?: string }): Promise => { 200 | const newCategory: Category = { 201 | id: generateUUID(), 202 | ...data 203 | }; 204 | categories.push(newCategory); 205 | return newCategory; 206 | } 207 | }; -------------------------------------------------------------------------------- /playground/lib/validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { UserRole } from '@/types'; 3 | 4 | // User schemas 5 | export const UserCreateSchema = z.object({ 6 | name: z.string().min(2, "Name must be at least 2 characters"), 7 | email: z.string().email("Invalid email address"), 8 | password: z.string().min(8, "Password must be at least 8 characters"), 9 | role: z.enum(['admin', 'user', 'guest'] as const).optional() 10 | }); 11 | 12 | export const UserUpdateSchema = z.object({ 13 | name: z.string().min(2, "Name must be at least 2 characters").optional(), 14 | email: z.string().email("Invalid email address").optional(), 15 | role: z.enum(['admin', 'user', 'guest'] as const).optional() 16 | }); 17 | 18 | export const UserIdSchema = z.object({ 19 | id: z.string().uuid("Invalid user ID format") 20 | }); 21 | 22 | // Product schemas 23 | export const ProductCreateSchema = z.object({ 24 | name: z.string().min(1, "Product name is required"), 25 | description: z.string().optional(), 26 | price: z.number().positive("Price must be a positive number"), 27 | stock: z.number().nonnegative("Stock cannot be negative"), 28 | categoryId: z.string().uuid("Invalid category ID format") 29 | }); 30 | 31 | export const ProductUpdateSchema = z.object({ 32 | name: z.string().min(1, "Product name is required").optional(), 33 | description: z.string().optional(), 34 | price: z.number().positive("Price must be a positive number").optional(), 35 | stock: z.number().nonnegative("Stock cannot be negative").optional(), 36 | categoryId: z.string().uuid("Invalid category ID format").optional() 37 | }); 38 | 39 | export const ProductIdSchema = z.object({ 40 | id: z.string().uuid("Invalid product ID format") 41 | }); 42 | 43 | // Category schemas 44 | export const CategorySchema = z.object({ 45 | name: z.string().min(1, "Category name is required"), 46 | description: z.string().optional() 47 | }); 48 | 49 | // Authentication schemas 50 | export const LoginSchema = z.object({ 51 | email: z.string().email("Invalid email address"), 52 | password: z.string().min(8, "Password must be at least 8 characters") 53 | }); 54 | 55 | export const RegisterSchema = LoginSchema.extend({ 56 | name: z.string().min(2, "Name must be at least 2 characters") 57 | }); 58 | 59 | // Webhook validation schemas 60 | export const StripeWebhookSchema = z.object({ 61 | id: z.string(), 62 | type: z.enum([ 63 | 'payment_intent.succeeded', 64 | 'payment_intent.failed', 65 | 'customer.subscription.created' 66 | ]), 67 | data: z.object({ 68 | object: z.object({ 69 | id: z.string(), 70 | amount: z.number().optional(), 71 | status: z.string().optional(), 72 | customer: z.string().optional() 73 | }) 74 | }) 75 | }); 76 | 77 | export const PaginationQuerySchema = z.object({ 78 | page: z.coerce.number().int().positive().default(1), 79 | limit: z.coerce.number().int().positive().default(10) 80 | }); -------------------------------------------------------------------------------- /playground/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /playground/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | // Experimental features 5 | experimental: { 6 | // This helps with monorepo setups 7 | externalDir: true, 8 | } 9 | } 10 | 11 | module.exports = nextConfig -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "scripts": { 4 | "dev": "next dev", 5 | "build": "next build", 6 | "start": "next start", 7 | "lint": "next lint" 8 | }, 9 | "dependencies": { 10 | "next": "^14.1.0", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^22", 16 | "@types/react": "^18.2.21", 17 | "@types/react-dom": "^18.2.7", 18 | "autoprefixer": "^10.4.16", 19 | "postcss": "^8.4.49", 20 | "tailwindcss": "^3.4.17" 21 | } 22 | } -------------------------------------------------------------------------------- /playground/postcss.config.js: -------------------------------------------------------------------------------- 1 | // postcss.config.js 2 | module.exports = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | } 7 | } -------------------------------------------------------------------------------- /playground/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './app/**/*.{js,ts,jsx,tsx,mdx}', 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: { 12 | 50: '#f0f9ff', 13 | 100: '#e0f2fe', 14 | 200: '#bae6fd', 15 | 300: '#7dd3fc', 16 | 400: '#38bdf8', 17 | 500: '#0ea5e9', 18 | 600: '#0284c7', 19 | 700: '#0369a1', 20 | 800: '#075985', 21 | 900: '#0c4a6e', 22 | 950: '#082f49', 23 | }, 24 | }, 25 | fontFamily: { 26 | sans: ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'], 27 | }, 28 | spacing: { 29 | '128': '32rem', 30 | '144': '36rem', 31 | }, 32 | borderRadius: { 33 | '4xl': '2rem', 34 | }, 35 | }, 36 | }, 37 | plugins: [], 38 | } 39 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } -------------------------------------------------------------------------------- /playground/types/index.ts: -------------------------------------------------------------------------------- 1 | // User related types 2 | export interface User { 3 | id: string; 4 | name: string; 5 | email: string; 6 | role: UserRole; 7 | createdAt: Date; 8 | updatedAt: Date; 9 | } 10 | 11 | export type UserRole = 'admin' | 'user' | 'guest'; 12 | 13 | export interface UserCreateInput { 14 | name: string; 15 | email: string; 16 | password: string; 17 | role?: UserRole; 18 | } 19 | 20 | export interface UserUpdateInput { 21 | name?: string; 22 | email?: string; 23 | role?: UserRole; 24 | } 25 | 26 | // Product related types 27 | export interface Product { 28 | id: string; 29 | name: string; 30 | description?: string; 31 | price: number; 32 | stock: number; 33 | categoryId: string; 34 | createdAt: Date; 35 | updatedAt: Date; 36 | } 37 | 38 | export interface ProductCreateInput { 39 | name: string; 40 | description?: string; 41 | price: number; 42 | stock: number; 43 | categoryId: string; 44 | } 45 | 46 | export interface ProductUpdateInput { 47 | name?: string; 48 | description?: string; 49 | price?: number; 50 | stock?: number; 51 | categoryId?: string; 52 | } 53 | 54 | export interface Category { 55 | id: string; 56 | name: string; 57 | description?: string; 58 | } 59 | 60 | // Authentication related types 61 | export interface LoginInput { 62 | email: string; 63 | password: string; 64 | } 65 | 66 | export interface RegisterInput extends LoginInput { 67 | name: string; 68 | } 69 | 70 | export interface AuthResponse { 71 | token: string; 72 | user: User; 73 | } 74 | 75 | // API response types 76 | export interface ApiResponse { 77 | data?: T; 78 | error?: string; 79 | message?: string; 80 | status: number; 81 | } 82 | 83 | // Webhook types 84 | export interface WebhookPayload { 85 | id: string; 86 | type: string; 87 | data: Record; 88 | createdAt: number; 89 | } 90 | 91 | export interface StripeWebhookPayload extends WebhookPayload { 92 | type: 'payment_intent.succeeded' | 'payment_intent.failed' | 'customer.subscription.created'; 93 | data: { 94 | object: { 95 | id: string; 96 | amount?: number; 97 | status?: string; 98 | customer?: string; 99 | } 100 | }; 101 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'playground' -------------------------------------------------------------------------------- /setup.ts: -------------------------------------------------------------------------------- 1 | import type { Express } from 'express' 2 | import express from 'express'; 3 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js' 4 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 5 | 6 | /** 7 | * Resolve port from command line args or environment variables 8 | * Returns port number with 8080 as the default 9 | * 10 | * Note: The port option is only applicable when using --transport=sse 11 | * as it controls the HTTP server port for SSE connections. 12 | */ 13 | export function resolvePort(): { port: number; source: string } { 14 | // Get command line arguments 15 | const args = parseCommandLineArgs(); 16 | 17 | // 1. Check command line arguments first (highest priority) 18 | if (args.port) { 19 | const port = parseInt(args.port, 10); 20 | return { port, source: 'command line argument' }; 21 | } 22 | 23 | // 2. Check environment variables 24 | if (process.env.PORT) { 25 | const port = parseInt(process.env.PORT, 10); 26 | return { port, source: 'environment variable' }; 27 | } 28 | 29 | // 3. Default to 4857 30 | return { port: 4857, source: 'default' }; 31 | } 32 | 33 | export async function setupSSE(base: string, server: Server): Promise { 34 | const app = express(); 35 | const transports = new Map() 36 | 37 | app.use(`${base}/sse`, async (req, res) => { 38 | const transport = new SSEServerTransport(`${base}/messages`, res) 39 | transports.set(transport.sessionId, transport) 40 | res.on('close', () => { 41 | transports.delete(transport.sessionId) 42 | }) 43 | await server.connect(transport) 44 | }) 45 | 46 | app.use(`${base}/messages`, async (req, res) => { 47 | if (req.method !== 'POST') { 48 | res.statusCode = 405 49 | res.end('Method Not Allowed') 50 | return 51 | } 52 | const query = new URLSearchParams(req.url?.split('?').pop() || '') 53 | const clientId = query.get('sessionId') 54 | if (!clientId || typeof clientId !== 'string') { 55 | res.statusCode = 400 56 | res.end('Bad Request due to missing sessionId') 57 | return 58 | } 59 | const transport = transports.get(clientId) 60 | if (!transport) { 61 | res.statusCode = 404 62 | res.end('Not Found due to invalid sessionId') 63 | return 64 | } 65 | 66 | await transport.handlePostMessage(req, res); 67 | }) 68 | return app; 69 | } 70 | 71 | 72 | /** 73 | * Parse command line arguments 74 | * @returns {Record} 75 | */ 76 | export function parseCommandLineArgs(): Record { 77 | // Check if any args start with '--' (the way tsx passes them) 78 | const args = process.argv.slice(2); 79 | const parsedManually: Record = {}; 80 | 81 | for (let i = 0; i < args.length; i++) { 82 | const arg = args[i]; 83 | if (arg.startsWith('--')) { 84 | const [key, value] = arg.substring(2).split('='); 85 | if (value) { 86 | // Handle --key=value format 87 | parsedManually[key] = value; 88 | } else if (i + 1 < args.length && !args[i + 1].startsWith('--')) { 89 | // Handle --key value format 90 | parsedManually[key] = args[i + 1]; 91 | i++; // Skip the next argument as it's the value 92 | } else { 93 | // Handle --key format (boolean flag) 94 | parsedManually[key] = 'true'; 95 | } 96 | } 97 | } 98 | 99 | // Just use the manually parsed args - removed parseArgs dependency for Node.js <18.3.0 compatibility 100 | return parsedManually; 101 | } 102 | 103 | /** 104 | * Resolve transport type from command line args or environment variables 105 | * Returns 'stdio' or 'sse', with 'stdio' as the default 106 | */ 107 | export function resolveTransport(): { type: 'stdio' | 'sse'; source: string } { 108 | // Get command line arguments 109 | const args = parseCommandLineArgs(); 110 | 111 | // 1. Check command line arguments first (highest priority) 112 | if (args.transport) { 113 | const type = args.transport === 'sse' ? 'sse' : 'stdio'; 114 | return { type, source: 'command line argument' }; 115 | } 116 | 117 | // 2. Check environment variables 118 | if (process.env.TRANSPORT) { 119 | const type = process.env.TRANSPORT === 'sse' ? 'sse' : 'stdio'; 120 | return { type, source: 'environment variable' }; 121 | } 122 | 123 | // 3. Default to localhost sse 124 | return { type: 'sse', source: 'default' }; 125 | } 126 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "rootDir": ".", 5 | "target": "es2020", 6 | "module": "NodeNext", 7 | "moduleResolution": "NodeNext", 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "isolatedModules": true, 14 | "baseUrl": ".", 15 | "noImplicitAny": false, 16 | "strictNullChecks": false, 17 | "strictFunctionTypes": false, 18 | "strictBindCallApply": false, 19 | "strictPropertyInitialization": false, 20 | "noImplicitThis": false, 21 | "useUnknownInCatchVariables": false, 22 | "alwaysStrict": false, 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "exactOptionalPropertyTypes": false, 26 | "noImplicitReturns": false, 27 | "noFallthroughCasesInSwitch": false, 28 | "noUncheckedIndexedAccess": false, 29 | "noImplicitOverride": false, 30 | "noPropertyAccessFromIndexSignature": false 31 | }, 32 | "include": [ 33 | "./**/*.ts", 34 | "./**/*.js", 35 | "**/*.test.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules", 39 | "dist", 40 | "dist/operations/router.js", 41 | "playground" 42 | ] 43 | } 44 | --------------------------------------------------------------------------------