MCP Server
Route handler patterns and tool implementation for ChatGPT MCP integration
MCP Server Implementation
Table of Contents
- Route Handler Structure - Handler initialization pattern
- Request Flow - Tool discovery, invocation, and resource loading flows
- Tool Registration Pattern - Factory pattern and tool structure
- Resource Registration Pattern - Resource loader structure
- Widget HTML Loading - Module initialization HTML loading process
- Error Handling - Tool execution and validation errors
- Base URL Resolution - When and how to use baseURL constant
- CORS Handling - Middleware configuration
- References - File references for implementation details
Route Handler Structure
The MCP server is implemented as a Next.js Route Handler at app/mcp/route.ts.
Handler Initialization
The handler is initialized when app/mcp/route.ts module is evaluated:
- Load widget resources - each resource loader (e.g.,
loadArtifactViewer()) fetches and caches widget HTML - Register resources using
registerResource()helper - Register tools using
registerTool()helper, passing widget metadata - Configure authentication - tools can optionally require OAuth (see authentication.md for details)
See app/mcp/route.ts for the complete implementation.
Request Flow
1. Tool Discovery
ChatGPT → GET /mcp
↓
Handler responds with available tools and resources
↓
ChatGPT displays tools in UI2. Tool Invocation
ChatGPT → POST /mcp (tool call with tool name and input)
↓
Route handler extracts tool names from request body
↓
Checks if any tool requires authentication (see authentication.md)
↓
Validates auth token if required, extracts userId
↓
Tool handler executes with validated input and auth context
↓
Returns response with content, structuredContent, and _meta3. Resource Loading
ChatGPT → GET /mcp (resource read request)
↓
Handler returns pre-loaded HTML cached at module initialization
↓
ChatGPT renders widget in iframeTool Registration Pattern
Tools use a factory pattern. Each tool file in app/mcp/tools/ exports a factory function that returns an McpTool object with name, config, and handler.
Key requirements:
- Define Zod schemas for input/output validation
- Factory returns
McpTool<InputSchema, OutputSchema, InputType> - Handler uses
resolveAuthContext(context.authInfo)to extractuserIdfor authenticated tools - Handler returns
{ content, structuredContent, _meta }triple - Use
satisfies ToolHandler<InputType>for type safety
See app/mcp/tools/artifact-list.ts, artifact-view.ts, or artifact-create.ts for complete implementation examples.
Resource Registration Pattern
Resources represent widget HTML templates that tools can reference. Each resource file in app/mcp/resources/ exports a loader function that:
- Fetches widget HTML via
getAppsSdkCompatibleHtml(baseURL, '/page-route') - Returns
{ resource, metadata }whereresourceis forregisterResource()andmetadatais for tool_meta - Must use
mimeType: 'text/html+skybridge'(required by ChatGPT)
When creating a new widget/resource: Use app/mcp/resources/artifact-viewer.ts as an implementation reference.
Widget HTML Loading
Widget HTML is loaded when app/mcp/route.ts module is evaluated (before handling any requests), not per-request, via getAppsSdkCompatibleHtml(baseURL, '/page-route').
This function:
- Uses
baseURLfrombaseUrl.ts(resolved based onNODE_ENVand Vercel environment variables) - Fetches the server-rendered HTML from the Next.js page route via HTTP
- Returns the HTML string, which is stored in the resource handler closure for all subsequent requests
Critical: Widget pages must be fully server-side rendered. Client-side hydration happens in the ChatGPT iframe.
See app/mcp/lib/shared.ts (function getAppsSdkCompatibleHtml) for the implementation.
Error Handling
Authentication Errors
Authentication errors are handled by the MCP route handler - see authentication.md for details.
Validation Errors
Input validation happens automatically via Zod schemas in tool configurations. The MCP SDK validates input against inputSchema before invoking the tool handler.
Pattern:
- Define validation rules in the tool's
inputSchemaobject using Zod methods (.min(),.max(),.optional(), etc.) - SDK performs validation before handler execution
- Validation failures return structured error responses to ChatGPT
- Tool handlers can also throw errors, which the MCP SDK catches and converts to error responses
Example validation rules:
const inputSchema = {
email: z.string().email(),
age: z.number().int().min(0).max(150),
items: z.array(z.string()).min(1).max(100),
status: z.enum(['active', 'inactive']).optional()
};Base URL Resolution
When implementing MCP tools or resources that need the application's URL (for OAuth metadata, widget HTML loading, etc.), import baseURL from baseUrl.ts:
import { baseURL } from '@/baseUrl';Usage:
- Pass to resource loader functions (e.g.,
loadArtifactViewer(baseURL)) - Pass to
getAppsSdkCompatibleHtml(baseURL, '/page-route')when fetching widget HTML - Never hardcode URLs like
http://localhost:3000orhttps://yourapp.vercel.app
The baseURL constant resolves automatically based on environment (development vs production). See baseUrl.ts for implementation details.
CORS Handling
CORS middleware in middleware.ts runs on all routes (matcher: '/:path*').
Behavior:
- Handles OPTIONS preflight requests for all paths
- Adds CORS headers (
Access-Control-Allow-Origin: *) to all responses - Uses
clerkMiddleware()only for/mcproutes (for session/auth context) - All routes including
.well-known/*OAuth discovery endpoints receive CORS headers
See middleware.ts for the complete implementation.
References
app/mcp/route.ts- Main MCP handlerapp/mcp/lib/tool-registry.ts- Tool registration helperapp/mcp/lib/resource-registry.ts- Resource registration helperapp/mcp/lib/shared.ts- Shared utilities (createToolMeta, resolveAuthContext)app/mcp/tools/*.ts- Individual tool implementationsapp/mcp/resources/*.ts- Individual resource loadersbaseUrl.ts- Environment-based URL resolutionmiddleware.ts- CORS configurationnext.config.ts- Asset prefix configuration