Skip to main content

Build for Production

Build your app for production using the sunpeak CLI:
This generates a dist/ directory with built resources and compiled tools:
Each .html file is a complete, self-contained app bundle — no external scripts or stylesheets. Tool files are compiled to standard ESM JavaScript with node_modules resolved at runtime.

Start the Production Server

This loads your compiled tool handlers, Zod schemas, and optional auth from dist/, registers them with the MCP protocol, and starts a Streamable HTTP server on port 8000.
The production server:
  • Registers each tool from dist/tools/ with its real handler and Zod schema (input validation)
  • Serves pre-built resource HTML from dist/{name}/{name}.html
  • Calls your auth() function from src/server.ts on every request (if present)
  • Uses MCP Streamable HTTP transport on a single /mcp endpoint
  • Provides a /health endpoint for load balancer probes and uptime monitoring
  • Handles graceful shutdown on SIGTERM/SIGINT

Testing Locally

Use sunpeak build and sunpeak start to test production behavior locally:
Then expose the MCP server with a tunnel (e.g., ngrok http 8000) and connect from ChatGPT.

Custom Server Setup

For full control over the HTTP server (custom middleware, CORS, health checks, etc.), use createMcpHandler to mount the MCP protocol handler on your own server. Unlike sunpeak start which auto-discovers from dist/, these handlers accept pre-loaded config objects — you load tools, resources, and auth yourself. See Production Server API for config types.
The handler manages Streamable HTTP sessions, auth propagation via req.auth, and CORS preflights for the /mcp path. For unmatched routes it does nothing, so your server handles them.

Serverless / Edge Deployment

For serverless and edge runtimes (Cloudflare Workers, Deno, Bun, Vercel Edge), use createHandler with stateless: true:
Unlike createMcpHandler, the web handler does not do path matching — it handles every request it receives. Mount it behind your own router.

Stateless vs Stateful Mode

By default, both createHandler and createMcpHandler use stateful mode: they track MCP sessions in an in-memory map and route requests by the mcp-session-id header. This works for single-instance servers and multi-instance deployments with sticky sessions (see Horizontal Scaling). Stateless mode (stateless: true) creates a fresh MCP server and transport for every request. No session map, no session ID validation. Use this when:
  • Serverless (Lambda, Workers, Vercel Edge) where each request may hit a different instance with no shared memory
  • Horizontally scaled deployments where configuring sticky sessions isn’t practical
In stateless mode, only POST is supported (no SSE streaming via GET). JSON responses are used by default, which is the right choice for serverless anyway.

Domain Resolution in Stateless Mode

sunpeak auto-computes resource domains from serverUrl based on which host is connecting (ChatGPT and Claude use different sandbox domain schemes). This requires knowing the host’s identity. ChatGPT sends user-agent: openai-mcp/1.0.0 and Claude sends user-agent: Claude-User on every MCP HTTP request. sunpeak detects these automatically, so domain resolution works in stateless mode without any extra configuration:
For custom MCP clients that don’t send identifiable headers, detectClientFromHeaders is exported from sunpeak/mcp so you can build your own detection logic in a custom handler. Both createMcpHandler and createHandler use JSON responses by default (enableJsonResponse: true), which works reliably across all environments including serverless. If you need SSE streaming (e.g., for progress updates on long-running tool calls), set enableJsonResponse: false in your config. See Production Server API for details.
For fully custom MCP server setups, sunpeak/mcp also re-exports registerAppTool, registerAppResource, and RESOURCE_MIME_TYPE from the MCP Apps SDK. See Server Helpers for the full list.

Production Operations

Health Checks

The built-in server exposes a GET /health endpoint that returns:
Use this for load balancer probes, Kubernetes liveness/readiness checks, and uptime monitoring. The endpoint is always unauthenticated.

Structured Logging

Pass --json-logs to output structured JSON lines instead of human-readable logs:
Each line is a JSON object with ts, level, and msg fields, plus contextual data:
This format is compatible with log aggregation tools like Datadog, CloudWatch, Loki, and Elasticsearch. Errors go to stderr, everything else to stdout.

Binding Interface

By default, the server binds to 0.0.0.0 (all interfaces). To restrict it to localhost only:

Process Management

sunpeak start handles SIGTERM and SIGINT for graceful shutdown (5-second drain timeout). It works with any process manager:

Reverse Proxy

For TLS, rate limiting, and request size limits, put sunpeak start behind a reverse proxy. The server’s CORS headers allow all origins by default, which is correct for MCP servers called by AI hosts from various origins.

Horizontal Scaling

MCP uses session-based communication by default. Each initialize handshake creates a session, and all subsequent requests include an mcp-session-id header that must route back to the same server instance. For multi-instance deployments behind a load balancer, you have two options: Sticky sessions (recommended for long-lived servers) — Configure your load balancer to route requests with the same mcp-session-id header to the same backend instance:
  • AWS ALB: Enable target group stickiness
  • nginx: Use ip_hash or sticky cookie
  • Cloudflare: Enable session affinity
  • Kubernetes: Set session affinity annotations on your Ingress
Stateless mode (recommended for serverless) — Disable session tracking entirely. Each request creates a fresh server instance, so any backend can handle any request:
Or with a custom server:
See Serverless / Edge Deployment for details on domain resolution in stateless mode.

What Not to Worry About

These are handled at the infrastructure layer, not by sunpeak start:
  • TLS/HTTPS — Use a reverse proxy (nginx, Caddy, Cloudflare)
  • Rate limiting — Configure at the proxy or CDN level
  • Request body limits — Handled by the proxy or platform
For full control over the HTTP server (custom middleware, metrics, etc.), use createMcpHandler to mount the MCP handler on your own Express, Fastify, or http server.

Publish to ChatGPT

ChatGPT apps are now submitted and published as plugins. The app itself remains an MCP-backed app, so your sunpeak server, tools, and UI resources do not change. The plugin is the package used for review and distribution. Use the OpenAI Platform plugin submission portal when your production server is ready. OpenAI’s plugin submission guide is the source of truth for the review flow.

Prerequisites

  • Your MCP server is deployed at a public production HTTPS URL.
  • Your resources are built with sunpeak build.
  • The submitter has Apps Management: Write access for the OpenAI Platform organization. Organization owners already have this access.
  • The publisher has completed individual or business identity verification in the same organization and project used for submission.
  • You can serve the domain verification token at /.well-known/openai-apps-challenge on the MCP hostname or an allowed parent hostname.
  • If sign-in is required, reviewer credentials work without MFA, SMS, email confirmation, or private-network access.
For local development, first enable Developer mode from the bottom-left user menu under Settings > Security and login > Developer mode. Then create the app from the ChatGPT homepage under Plugins > +. Public submission is a separate flow in the OpenAI Platform. If the app already exists in ChatGPT, submit its MCP server from scratch. The portal does not accept an existing ChatGPT app ID.

Prepare the submission

Collect these materials before opening the portal:
  • Plugin name, short and long descriptions, logo, category, website, support URL, privacy policy URL, and terms URL.
  • Production MCP server URL, authentication details, and reviewer credentials when needed.
  • A content security policy listing the exact domains the app fetches from.
  • Clear tool names, descriptions, input schemas, output shapes, and accurate readOnlyHint, openWorldHint, and destructiveHint annotations for every tool.
  • Starter prompts that show realistic workflows.
  • Exactly five positive test cases and three negative test cases, including expected behavior and any fixture data.
  • Country or region availability and release notes.
Review tool responses before submission. Remove auth secrets, debug payloads, unneeded personal data, internal identifiers, and user-related fields that your privacy policy does not disclose.

Submit the MCP-backed plugin

  1. Open the plugin submission portal and select Create plugin.
  2. Choose With MCP for an app-only plugin or an app-plus-skills plugin.
  3. Complete the public listing and publisher identity fields.
  4. Enter the production MCP server URL, configure authentication, and provide demo credentials when required.
  5. Add the app’s content security policy.
  6. Complete domain verification if the portal asks for it. The challenge endpoint must return only the exact token as plain text.
  7. Select Scan Tools, fix any server or metadata errors, deploy the fixes, and scan again.
  8. Add starter prompts, five positive tests, three negative tests, availability, release notes, and policy attestations.
  9. Select Submit for Review.
Submission starts the review. After OpenAI approves the plugin, publish it from the portal when you are ready. Published apps appear with other plugins in the shared Plugins Directory in ChatGPT and Codex. There is no separate Apps Directory.

Production Checklist

  • Set an exact CSP (Content Security Policy) for your resources.
  • Configure CORS if serving from a different domain.
  • Set up HTTPS for secure communication.
  • Verify that tool annotations match real behavior.
  • Confirm that the domain challenge and reviewer credentials work from the public internet.
  • Check that listing URLs and publisher identity match.
See OpenAI’s build plugins guide if you also want to package skills with the app or test the app through a local plugin manifest.