route.ts

Complete generic Next.js API route example implementing MCP protocol with bearer token authentication, example tools, resources, and integration patterns for any application

route.tsv1.0.015.7 KB
route.ts(typescript)
1/**
2 * Generic Remote MCP Server Implementation
3 * 
4 * This is a complete example of a Next.js API route that implements the Model Context Protocol
5 * over HTTP using the mcp-handler package and Anthropic's MCP SDK.
6 * 
7 * Uses Vercel's MCP Adapter (mcp-handler): https://github.com/vercel/mcp-adapter
8 * 
9 * IMPORTANT - Application-Specific Components:
10 * This example shows integration patterns that should be adapted to your application:
11 * - MockIdentityService: Replace with your authentication system
12 * - MockSecurityService: Replace with your rate limiting/security middleware
13 * - QuickstartCommand: Demonstrates the Utaba Micro-Block Command pattern
14 *   (see utaba/main/patterns/micro-block/README.md for the full architecture)
15 * - ApiContext: Security context pattern - adapt to your middleware
16 * 
17 * Key Features:
18 * - Bearer token authentication
19 * - One example tool (quickstart)
20 * - One example resource (quickstart guide)
21 * - Proper error handling with MCP error codes
22 * - Rate limiting and security context integration
23 * - Generic patterns that can be adapted to any application
24 * 
25 * For guidance on implementing this example, see the UCM here:
26 * ucm:utaba/main/guidance/development/nextjs-remote-mcp-server.md
27 * 
28 */
29
30import { McpServer, ResourceMetadata } from "@modelcontextprotocol/sdk/server/mcp.js";
31import { 
32    McpError, 
33    TextContent, 
34    CallToolResult, 
35    ServerRequest, 
36    ServerNotification, 
37    ReadResourceResult 
38} from "@modelcontextprotocol/sdk/types.js";
39import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
40import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
41import { createMcpHandler, withMcpAuth } from "mcp-handler";
42import { NextRequest } from "next/server";
43
44// ================================
45// MCP Error Codes (Standard)
46// ================================
47enum McpErrorCode {
48    InvalidRequest = -32600,
49    MethodNotFound = -32601,
50    InvalidParams = -32602,
51    InternalError = -32603,
52    UnuserIdized = -32001,
53}
54
55// ================================
56// Type Definitions
57// ================================
58
59/**
60 * Authentication ticket containing user information and permissions
61 * This would typically come from your application's auth system
62 */
63interface AuthTicket {
64    isAuthenticated: boolean;
65    userId?: string;
66    permissions: string[];
67    // Add other user-specific data your application needs
68}
69
70/**
71 * Extended context information passed through MCP auth
72 */
73interface AuthExtra {
74    authTicket: AuthTicket;
75    request: NextRequest;
76    timestamp: string;
77}
78
79/**
80 * API Context for business operations
81 * This represents the security and request context for your business logic
82 */
83interface ApiContext {
84    securityContext: {
85        authTicket: AuthTicket;
86    };
87    requestContext: {
88        method: string;
89        timestamp: string;
90        activityType: string;
91    };
92}
93
94// ================================
95// Business Logic Interfaces
96// ================================
97
98/**
99 * Example command input for generating quickstart content
100 */
101interface QuickstartInput {
102    userId:string;
103    serverType: 'remote' | 'local';
104    includeExamples?: boolean;
105}
106
107/**
108 * Example command output
109 */
110interface QuickstartOutput {
111    markdown: string;
112    timestamp: string;
113}
114
115// ================================
116// Mock Business Services
117// ================================
118
119/**
120 * Mock identity service for token validation
121 * In a real implementation, this would integrate with your auth system
122 */
123class MockIdentityService {
124    async validateToken(bearerToken: string): Promise<AuthTicket | null> {
125        // Parse bearer token 
126        //Note: This example uses PAT, but in your system you should adapt this to suit.
127        const [userId, apiKey] = bearerToken.split(':');
128        
129        if (!userId || !apiKey) {
130            return null;
131        }
132
133        // Mock validation - in reality, check against your auth system
134        if (apiKey === 'demo-api-key' || apiKey === 'abcdef') {
135            return {
136                isAuthenticated: true,
137                userId: userId,
138                permissions: ['read', 'write']
139            };
140        }
141        
142        return null;
143    }
144}
145
146/**
147 * Mock service for creating API security context
148 * This would integrate with your rate limiting and security systems
149 */
150class MockSecurityService {
151    async createApiContext(
152        request: NextRequest,
153        activityType: string,
154        method: string,
155        authTicket: AuthTicket
156    ): Promise<ApiContext> {
157        // In a real implementation, this would:
158        // - Check rate limits
159        // - Log security events
160        // - Validate permissions
161        // - Create audit trails
162        
163        return {
164            securityContext: {
165                authTicket
166            },
167            requestContext: {
168                method,
169                timestamp: new Date().toISOString(),
170                activityType
171            }
172        };
173    }
174}
175
176/**
177 * Mock business command for generating quickstart content
178 */
179class QuickstartCommand {
180    constructor(private input: QuickstartInput) {}
181    
182    async execute(): Promise<QuickstartOutput> {
183        const examples = this.input.includeExamples ? `
184
185## Quick Examples
186
187\`\`\`typescript
188// Example API call
189const result = await fetch('/api/data');
190\`\`\`
191` : '';
192        
193        const markdown = `# Welcome to ${this.input.serverType === 'remote' ? 'Remote' : 'Local'} MCP Server
194
195Hello **${this.input.userId}**! 
196
197This is your quickstart guide for using the MCP server.
198
199## Getting Started
200
2011. Use the health check tool to verify connectivity
2022. Explore available tools with the list command
2033. Start building with your business logic
204
205${examples}
206
207## Available Operations
208
209- **Health Check**: Verify server status
210- **Resource Access**: Get documentation and guides
211- **Custom Tools**: Your application-specific functionality
212
213---
214*Generated at ${new Date().toISOString()}*`;
215
216        return {
217            markdown,
218            timestamp: new Date().toISOString()
219        };
220    }
221}
222
223// ================================
224// Service Instances
225// ================================
226const identityService = new MockIdentityService();
227const securityService = new MockSecurityService();
228
229// ================================
230// MCP Tool Implementations
231// ================================
232
233/**
234 * Quickstart tool - provides getting started information
235 */
236const quickstartTool = async (authInfo?: AuthInfo): Promise<CallToolResult> => {
237    const extra = authInfo?.extra as AuthExtra;
238    
239    // Create API context with security validation
240    const context = await securityService.createApiContext(
241        extra.request,
242        'quickstart',
243        'GET',
244        extra.authTicket
245    );
246    
247    // Execute business logic
248    const command = new QuickstartCommand({
249        userId: context.securityContext.authTicket.userId,
250        serverType: 'remote',
251        includeExamples: true
252    });
253    
254    const result = await command.execute();
255    return formatMcpResponse(result.markdown);
256};
257
258/**
259 * Health check tool - verifies system status
260 */
261const healthCheckTool = async (authInfo?: AuthInfo): Promise<CallToolResult> => {
262    const extra = authInfo?.extra as AuthExtra;
263    
264    const context = await securityService.createApiContext(
265        extra.request,
266        'health-check',
267        'GET',
268        extra.authTicket
269    );
270    
271    const healthData = {
272        status: "healthy",
273        timestamp: new Date().toISOString(),
274        version: "1.0.0",
275        userId: context.securityContext.authTicket.userId,
276        authenticated: true,
277        permissions: context.securityContext.authTicket.permissions
278    };
279
280    return formatMcpResponse(healthData);
281};
282
283// ================================
284// MCP Resource Implementations
285// ================================
286
287/**
288 * Read resource handler for MCP resources
289 */
290async function readResource(uri: string, authInfo?: AuthInfo): Promise<ReadResourceResult> {
291    const extra = authInfo?.extra as AuthExtra;
292    
293    const context = await securityService.createApiContext(
294        extra.request,
295        'read-resource',
296        'GET',
297        extra.authTicket
298    );
299    
300    switch (uri) {
301        case "app://quickstart":
302            // Generate personalized quickstart content
303            const command = new QuickstartCommand({
304                userId: context.securityContext.authTicket.userId,
305                serverType: 'remote',
306                includeExamples: true
307            });
308            
309            const result = await command.execute();
310            
311            return {
312                contents: [{
313                    uri: "app://quickstart",
314                    mimeType: "text/markdown",
315                    text: result.markdown
316                }]
317            };
318            
319        default:
320            throw new McpError(McpErrorCode.InvalidParams, `Resource not found: ${uri}`);
321    }
322}
323
324// ================================
325// Utility Functions
326// ================================
327
328/**
329 * Format response data for MCP protocol
330 */
331function formatMcpResponse(data: any): CallToolResult {
332    if (typeof data === 'string') {
333        return {
334            content: [{ type: 'text', text: data } as TextContent]
335        };
336    } else {
337        return {
338            content: [{ type: 'text', text: JSON.stringify(data, null, 2) } as TextContent]
339        };
340    }
341}
342
343/**
344 * Wrap errors in MCP format
345 */
346const wrapMcpError = (error: McpError): CallToolResult => {
347    return {
348        content: [{ 
349            type: "text", 
350            text: `Error: ${error.message}${error.data ? '\nData: ' + JSON.stringify(error.data) : ''}` 
351        }],
352        isError: true,
353    };
354};
355
356/**
357 * Execute tool with comprehensive error handling
358 */
359const executeTool = async (toolHandler: () => Promise<CallToolResult>): Promise<CallToolResult> => {
360    try {
361        return await toolHandler();
362    } catch (error) {
363        let mcpError: McpError;
364        
365        if (error instanceof McpError) {
366            mcpError = error;
367        } else if (error instanceof Error) {
368            mcpError = new McpError(McpErrorCode.InternalError, error.message);
369        } else {
370            mcpError = new McpError(McpErrorCode.InternalError, String(error));
371        }
372        
373        return wrapMcpError(mcpError);
374    }
375};
376
377/**
378 * Create auth-enabled tool wrapper
379 */
380const createAuthTool = (toolFn: (authInfo?: AuthInfo) => Promise<CallToolResult>) =>
381    async (args: any, extra: any) => {
382        return await executeTool(() => toolFn(extra.authInfo));
383    };
384
385// ================================
386// MCP Server Registration
387// ================================
388
389/**
390 * Register all tools with the MCP server
391 */
392const registerTools = (server: McpServer) => {
393    // Quickstart tool - provides getting started guidance
394    server.tool(
395        "quickstart",
396        "Get getting started guidance and system overview",
397        {
398            // No parameters needed for basic quickstart
399        },
400        createAuthTool(quickstartTool)
401    );
402
403    // Health check tool - verify system status
404    server.tool(
405        "health_check",
406        "Check server connectivity and authentication status",
407        {
408            // No parameters needed for health check
409        },
410        createAuthTool(healthCheckTool)
411    );
412
413    // Add more tools here following the same pattern:
414    // server.tool(
415    //     "tool_name",
416    //     "Tool description",
417    //     {
418    //         param: z.string().describe("Parameter description")
419    //     },
420    //     async ({ param }, extra) => {
421    //         return await executeTool(() => yourToolFunction(param, extra.authInfo));
422    //     }
423    // );
424};
425
426/**
427 * Register all resources with the MCP server
428 * Resources appear in Claude Desktop's '+' prompt button
429 */
430const registerResources = (server: McpServer) => {
431    server.resource(
432        "Getting Started Guide",
433        "app://quickstart",
434        {
435            description: "Interactive getting started guide with personalized examples and instructions",
436        } as ResourceMetadata,
437        (uri: URL, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => 
438            readResource(uri.toString(), extra.authInfo)
439    );
440
441    // Add more resources here:
442    // server.resource(
443    //     "Resource Name",
444    //     "app://resource-uri",
445    //     { description: "Resource description" } as ResourceMetadata,
446    //     (uri: URL, extra) => readResource(uri.toString(), extra.authInfo)
447    // );
448};
449
450// ================================
451// Authentication Implementation
452// ================================
453
454/**
455 * Verify bearer token and create auth context
456 * This integrates with your application's authentication system
457 */
458const verifyToken = async (
459    req: Request,
460    bearerToken?: string
461): Promise<AuthInfo | undefined> => {
462    if (!bearerToken) {
463        return undefined;
464    }
465
466    // Validate token with your identity service
467    const authTicket = await identityService.validateToken(bearerToken);
468    
469    if (!authTicket || !authTicket.isAuthenticated) {
470        return undefined;
471    }
472
473    // Create extra context for downstream operations
474    const extra: AuthExtra = {
475        authTicket,
476        request: req as NextRequest,
477        timestamp: new Date().toISOString(),
478    };
479
480    return {
481        token: bearerToken,
482        scopes: [".default"], // Your application's scope system
483        clientId: "mcp-client",
484        extra: extra as any
485    };
486};
487
488// ================================
489// MCP Handler Configuration
490// ================================
491
492/**
493 * Create the MCP handler with server configuration
494 */
495const handler = createMcpHandler(
496    (server: McpServer) => {
497        registerTools(server);
498        registerResources(server);
499    },
500    {
501        // Instructions shown to AI assistants
502        instructions: 'This MCP server provides access to application functionality. Start with the quickstart tool to understand available operations.',
503        
504        // Server capabilities
505        capabilities: {
506            auth: {
507                type: "bearer",
508                required: true,
509            },
510            tools: {
511                listChanged: false
512            },
513            resources: {
514                subscribe: false,
515                listChanged: false
516            },
517            logging: {},
518            experimental: {}
519        },
520        
521        // Server identification
522        serverInfo: {
523            name: 'Generic Remote MCP Server',
524            version: '1.0.0',
525        },
526    },
527    {
528        // HTTP endpoint configuration
529        streamableHttpEndpoint: '/api/mcp',
530        basePath: '/api'
531    }
532);
533
534/**
535 * Create authenticated handler with token verification
536 */
537const authHandler = withMcpAuth(handler, verifyToken, {
538    required: true,
539    requiredScopes: [".default"],
540    resourceMetadataPath: "/.well-known/oauth-protected-resource",
541});
542
543// ================================
544// Next.js API Route Exports
545// ================================
546
547export { authHandler as GET, authHandler as POST };
548
549/**
550 * Usage Instructions:
551 * 
552 * For comprehensive implementation guidance, see:
553 * utaba/main/guidance/development/nextjs-remote-mcp-server.md
554 * 
555 * 1. Deploy this route at /src/app/api/mcp/route.ts in your Next.js app
556 * 
557 * 2. Configure authentication by implementing your own IdentityService
558 * 
559 * 3. Add your business logic by creating commands/services
560 * 
561 * 4. Register additional tools and resources as needed
562 * 
563 * 5. Test with: curl -H "auhtorization: Bearer userId:demo-api-key" http://localhost:3000/api/mcp
564 * 
565 * 6. Connect MCP clients using: http://localhost:3000/api/mcp
566 * 
567 * Key Integration Points:
568 * - Replace MockIdentityService with your auth system
569 * - Replace MockSecurityService with your rate limiting/security
570 * - Add your business commands and services
571 * - Customize the namespace and permission model
572 * - Add monitoring and logging as needed
573 */

Metadata

Path
utaba/main/implementations/remote-mcp-server/route.ts
Namespace
utaba/main/implementations/remote-mcp-server
Author
utaba
Category
implementations
Technology
typescript
Version
1.0.0
MIME Type
application/typescript
Published
29-Aug-2025
Last Updated
29-Aug-2025