How to Deploy an MCP App to Production (May 2026)
Deploy your sunpeak MCP App to production.
TL;DR: Run pnpm build to compile your app, then pnpm start to run the production server. Export an auth() function from src/server.ts to authenticate requests (or use OAuth 2.1 for full production auth). Point ChatGPT or Claude at your /mcp endpoint. Any Node.js 20+ host works. The same build serves every MCP-compatible host.
Building an MCP App locally with pnpm dev is fast. Deploying it is a two-command operation, pnpm build then pnpm start, but there are details worth getting right before you point real AI hosts at your server: authentication, environment variables, streaming proxy config, testing before deploy, and connecting each host. This guide covers all of it.
If you’re building a cross-host MCP App that runs on ChatGPT, Claude, VS Code, Goose, and other hosts, the deployment process is the same for all of them. One build, one server, many hosts.
Step 1: Build for Production
Run pnpm build from your project root:
pnpm build
This does three things:
-
Compiles each resource in
src/resources/into a self-contained HTML file atdist/{name}/{name}.htmlwith a metadata sidecar atdist/{name}/{name}.json. The HTML has all JavaScript and CSS inlined. No external script tags, no CDN dependencies. This is what AI hosts load into iframes when your tool is called. The JSON contains the resource URI (with a cache-bust timestamp), title, description, and any_metaconfig (CSP, permissions). -
Compiles tool handlers from
src/tools/into Node.js ESM modules atdist/tools/{name}.js. -
Compiles
src/server.ts(if it exists) intodist/server.js. This is where yourauth()function and server config live.
If the build fails, fix the errors before proceeding. A failed build means either a TypeScript error in your resource components or a misconfigured tool file.
Check that dist/ was created with your resources, tools, and server entry:
dist/
├── contact/
│ ├── contact.html ← self-contained resource bundle
│ └── contact.json ← resource metadata (URI, title, _meta)
├── tools/
│ └── show-contact.js ← compiled tool handler
└── server.js ← compiled auth + server config
Step 2: Configure Authentication
Production MCP Apps need authentication. The approach depends on your use case.
Simple Token Auth
Create src/server.ts if you don’t have one. Export an auth() function that validates the incoming request and returns an AuthInfo object, or null to reject with a 401:
import type { IncomingMessage } from 'node:http';
import type { AuthInfo } from 'sunpeak/mcp';
export async function auth(req: IncomingMessage): Promise<AuthInfo | null> {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return null;
}
const user = await verifyToken(token);
if (!user) return null;
return {
token,
clientId: user.id,
scopes: user.scopes,
};
}
export const server = { name: 'My App', version: '1.0.0' };
The AuthInfo you return is passed to every tool handler as extra.authInfo:
export default async function (args: Args, extra: ToolHandlerExtra) {
const userId = extra.authInfo?.clientId;
const data = await db.getDataForUser(userId);
return { structuredContent: data };
}
OAuth 2.1 (Recommended for Production)
For apps that need user login through ChatGPT or Claude, implement the MCP OAuth 2.1 spec. Both hosts support PKCE-based authorization flows with discovery documents, token endpoints, and refresh tokens. This is the standard for production MCP Apps that handle user data.
If you don’t need authentication at all (a public tool that returns generic data), you can return an AuthInfo with an empty token:
export async function auth(req: IncomingMessage): Promise<AuthInfo | null> {
return { token: '', clientId: 'anonymous', scopes: [] };
}
Step 3: Set Environment Variables
Your tool handlers read configuration from process.env. Set these on your server before starting the process. Never commit secrets to your repository.
For a local production test:
DATABASE_URL=postgres://... API_KEY=sk-... pnpm start
In production, set environment variables through your hosting platform:
- Docker / docker-compose:
environment:block indocker-compose.yml, or-e KEY=VALUEflag - systemd:
Environment=KEY=VALUEin the service unit file - Fly.io:
fly secrets set DATABASE_URL=postgres://... - Railway: Project settings, Variables section
- AWS ECS: Task definition environment variables
Read them in tool handlers:
export default async function (args: Args, extra: ToolHandlerExtra) {
const db = new Client(process.env.DATABASE_URL);
// ...
}
Step 4: Start the Production Server
pnpm start
The MCP server starts on port 8000 by default. Set PORT to change it:
PORT=3000 pnpm start
Your MCP endpoint is at http://your-server:PORT/mcp. This is the URL you’ll give to ChatGPT, Claude, and any other MCP-compatible host.
The server handles the full MCP protocol over Streamable HTTP from this single endpoint:
- Tool and resource manifests, read by the host on connection
- Tool calls, validated against your Zod schemas, routed to your handler, and returned as structured content
- Resource HTML, the pre-built bundles from
dist/, served to host iframes when a tool returns structured content
Verify the server is running:
curl http://localhost:8000/health
Step 5: Reverse Proxy and TLS
AI hosts require your MCP endpoint to use HTTPS. In production, terminate TLS at a reverse proxy (nginx, Caddy, Cloudflare Tunnel) and forward traffic to pnpm start.
MCP uses Streamable HTTP, which includes streaming responses. Make sure your proxy does not buffer these. In nginx:
location /mcp {
proxy_pass http://localhost:8000;
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding on;
}
With Caddy, response buffering is off by default, so no extra config is needed.
With Cloudflare, set the route to “No Transform” to avoid response buffering.
Step 6: Test Before You Deploy
Run your test suite before every deploy. This catches rendering issues, tool handler bugs, and cross-host differences without a paid ChatGPT or Claude account:
pnpm test
The sunpeak inspector replicates both the ChatGPT and Claude runtimes, so your unit tests and E2E tests run against the same rendering environment your users will see. If tests pass, your resources render correctly in both hosts.
For automated deploys, add testing to your CI/CD pipeline:
- run: pnpm install
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm exec sunpeak test
- run: pnpm exec sunpeak build
The pre-submission testing checklist covers what to verify before you publish to the ChatGPT App Store or Claude Connectors Directory: tool annotations, test credentials, privacy compliance, display mode rendering, and CSP configuration.
Step 7: Connect to ChatGPT
In ChatGPT, go to User > Settings > Apps & Connectors > Create. Enter your server URL with the /mcp path:
https://my-app.example.com/mcp
ChatGPT fetches your tool manifest from /mcp and registers your tools. The next time a user asks something that triggers one of your tools, ChatGPT calls your tool handler and renders the resource HTML in an iframe inside the conversation.
If your server uses bearer token authentication, enter the token in the Authentication section of the connector setup form. For OAuth, configure the authorization URL, token URL, and client credentials. ChatGPT supports the full MCP OAuth 2.1 flow.
Step 8: Connect to Claude
In Claude, go to Settings > Connectors > Add custom connector. Enter the same server URL with the /mcp path:
https://my-app.example.com/mcp
Claude discovers your tools and resources automatically. If your server requires authentication, configure the bearer token or OAuth in the connector settings. For OAuth, allowlist both claude.ai and claude.com callback URLs. Missing one causes authentication failures for some users, and it’s a common reason Claude Connector Directory submissions get rejected.
One thing to know: Claude connects to your MCP server from Anthropic’s cloud infrastructure, not from the user’s browser. Your server needs to be reachable over the public internet. If you have firewall rules, make sure Anthropic’s IP ranges can reach your /mcp endpoint.
For Claude-specific deployment details (Cloudflare Workers, Vercel, callback URLs), see the deploying Claude Connectors guide.
Keeping the Server Running
Use a process manager to keep pnpm start running after deploys and restarts.
pm2:
pm2 start "pnpm start" --name my-mcp-app
pm2 save
pm2 startup
systemd:
[Unit]
Description=My MCP App
After=network.target
[Service]
WorkingDirectory=/opt/my-mcp-app
ExecStart=/usr/local/bin/pnpm start
Restart=always
Environment=PORT=8000
Environment=DATABASE_URL=postgres://...
[Install]
WantedBy=multi-user.target
Docker:
FROM node:20-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm exec sunpeak build
EXPOSE 8000
CMD ["pnpm", "exec", "sunpeak", "start"]
Deploying on Fly.io
Fly.io is a good fit for MCP Apps: global regions, automatic TLS, and straightforward Node.js deployments.
Create a fly.toml:
app = "my-mcp-app"
primary_region = "ord"
[build]
[http_service]
internal_port = 8000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
[[vm]]
size = "shared-cpu-1x"
memory = "256mb"
Add a Dockerfile using the example above, then deploy:
fly launch
fly secrets set DATABASE_URL=postgres://...
fly deploy
Your MCP endpoint will be at https://my-mcp-app.fly.dev/mcp. Point ChatGPT, Claude, or any MCP host to that URL.
Watch out for cold starts on serverless and auto-stop platforms. If your server takes too long to wake up, the host may time out the connection. On Fly.io, set min_machines_running = 1 in fly.toml to keep at least one instance warm. On other platforms, check whether you can configure minimum instances or keep-alive pings.
CI/CD: Build and Test Before Deploy
Add build and test steps to your CI/CD pipeline before deploying. Here’s a GitHub Actions workflow:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm exec sunpeak test
- run: pnpm exec sunpeak build
# Deploy dist/ and server files to your host
- name: Deploy to Fly.io
run: fly deploy
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
pnpm test runs both unit and E2E tests in the inspector against both the ChatGPT and Claude hosts before the build. If they pass, you know your resources render correctly in both environments before you ship. See the CI/CD guide for the full workflow and the complete testing guide for how to write the tests.
Smoke Testing After Deploy
After deploying, verify the server is healthy:
curl https://my-app.example.com/health
If it returns a response, your server is running and reachable. Open ChatGPT or Claude, trigger one of your tools, and confirm the resource renders correctly.
For ongoing monitoring, the error handling guide covers how to handle loading, error, and cancelled states in your resources so users see useful feedback when something goes wrong in production.
Get Started
npx sunpeak new
Further Reading
- MCP App Tutorial - build your first resource and tool from scratch
- MCP App Authentication: OAuth 2.1 - full auth setup for production
- Building One MCP App for ChatGPT and Claude - cross-host portability
- Pre-Submission Testing - validate before publishing to ChatGPT and Claude
- MCP App CI/CD: Run Your Tests in GitHub Actions
- Deploying Claude Connectors - Claude-specific deployment details
- MCP App Error Handling - handle loading, error, and cancelled states
- Security Testing for MCP Apps - auth flow, CSP, and input validation tests
- Deployment guide
- CLI reference: sunpeak build
- CLI reference: sunpeak start
- Server authentication
Frequently Asked Questions
What commands do I run to deploy an MCP App?
Run "pnpm build" to compile your resources and tools, then "pnpm start" to launch a production MCP server. The server listens on port 8000 by default and exposes your MCP endpoint at /mcp. Point any MCP-compatible host (ChatGPT, Claude, VS Code, Goose) to that URL to connect.
What does sunpeak build do?
"pnpm build" (which runs sunpeak build) compiles each resource in src/resources/ into a self-contained HTML bundle with all JavaScript and CSS inlined, plus a JSON metadata sidecar with the resource URI and config. It compiles tool handlers from src/tools/ into Node.js ESM modules and compiles src/server.ts (if present) into dist/server.js. The output goes into dist/. All steps must succeed before you can run pnpm start.
How do I authenticate users in a deployed MCP App?
For simple token auth, create src/server.ts and export an async auth() function that validates the Authorization header and returns an AuthInfo object or null to reject with a 401. For full OAuth 2.1 with PKCE, implement the discovery document, authorization, and token endpoints. ChatGPT and Claude both support the MCP OAuth 2.1 spec for production authentication.
What port does sunpeak start use?
"pnpm start" (which runs sunpeak start) listens on port 8000 by default. Set the PORT environment variable to override it. Your MCP endpoint is at http://your-host:PORT/mcp.
Do I need a special server to host an MCP App?
No. The production server is a standard Node.js HTTP server. You can host it on any platform that runs Node.js 20+: a VPS (DigitalOcean, Hetzner, Linode), a container on Fly.io or Railway, a serverless platform like AWS Lambda behind an API Gateway, or a managed Node.js service. The only requirement is that the /mcp endpoint is publicly reachable over HTTPS by the AI host.
How do I connect a deployed MCP App to ChatGPT?
In ChatGPT, go to User > Settings > Apps & Connectors > Create. Enter your server URL with the /mcp path (e.g., https://my-app.example.com/mcp). ChatGPT will fetch the tool and resource manifests and connect. For OAuth, configure the auth settings in the connector setup form.
How do I connect a deployed MCP App to Claude?
In Claude, go to Settings > Connectors > Add custom connector. Enter your server URL with the /mcp path. Claude will connect and discover your tools and resources automatically. If your server requires authentication, configure the bearer token or OAuth in the connector settings. Allowlist both claude.ai and claude.com OAuth callback URLs.
Should I test my MCP App before deploying?
Yes. Run pnpm test to execute unit and E2E tests against the sunpeak inspector, which replicates the ChatGPT and Claude runtimes locally. This catches rendering issues, tool handler bugs, and cross-host differences without paid accounts or burned credits. Add these tests to CI/CD so every push is validated before deploy.
What transport protocol do MCP Apps use?
MCP Apps use Streamable HTTP for communication. This is the standard MCP transport, which works on every hosting platform including serverless ones like Cloudflare Workers and Vercel Edge Functions. SSE transport is deprecated in the MCP spec. Make sure your reverse proxy does not buffer streaming responses.