> ## Documentation Index
> Fetch the complete documentation index at: https://sunpeak.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Production Server

> API reference for production MCP server handlers.

<Badge color="yellow">sunpeak API</Badge>

## Overview

sunpeak provides three ways to run a production MCP server, from zero-config to full control:

| Approach           | Use case                                                                  |
| ------------------ | ------------------------------------------------------------------------- |
| `sunpeak start`    | Zero-config — loads tools, resources, and auth from `dist/` automatically |
| `createMcpHandler` | Node.js — mount the MCP handler on your own Express/Fastify/http server   |
| `createHandler`    | Web Standard — Cloudflare Workers, Deno, Bun, Vercel Edge                 |

`sunpeak start` auto-discovers tools from `dist/tools/`, resources from `dist/{name}/`, and auth from `dist/server.js`, then passes them to `startProductionHttpServer` internally.

`createMcpHandler` and `createHandler` accept these as config objects — you load and pass them yourself. This gives full control over how tools, resources, and auth are provided.

All three use [MCP Streamable HTTP](/mcp-apps/mcp/overview#transports) transport on a single `/mcp` endpoint.

## `createMcpHandler`

Creates a Node.js request handler for MCP over Streamable HTTP.

```ts theme={null}
import { createMcpHandler } from 'sunpeak/mcp';
import type { ProductionServerConfig } from 'sunpeak/mcp';

const handler = createMcpHandler(config);
// handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>
```

The handler responds to `POST /mcp`, `GET /mcp`, `DELETE /mcp`, and `OPTIONS /mcp`. For unmatched paths it does nothing, so you can chain it with your own routes.

### Example: Express

```ts theme={null}
import express from 'express';
import { createMcpHandler } from 'sunpeak/mcp';

const app = express();
app.get('/health', (req, res) => res.json({ ok: true }));

// tools: ProductionTool[], resources: ProductionResource[], auth: AuthFunction
const mcpHandler = createMcpHandler({ tools, resources, auth });
app.use((req, res, next) => {
  mcpHandler(req, res).then(() => {
    if (!res.headersSent) next();
  });
});

app.listen(3000);
```

### `ProductionServerConfig`

<ResponseField name="name" type="string" default="sunpeak-app">
  Server name reported to hosts during [MCP handshake](/mcp-apps/mcp/overview#connection-lifecycle).
</ResponseField>

<ResponseField name="version" type="string" default="0.1.0">
  Server version reported to hosts.
</ResponseField>

<ResponseField name="serverInfo" type="ServerConfig">
  Full server identity. Overrides `name` and `version` when provided. Supports additional fields like `title`, `description`, `websiteUrl`, `icons`, and `instructions` (sent in the MCP `initialize` response so hosts can inject it into the model's system prompt). See [Server Entry](/app-framework/tools/server-entry#server-object-optional) for the full field list.
</ResponseField>

<ResponseField name="tools" type="ProductionTool[]" required>
  Tool registrations. Each has a `name`, `tool` config (optional resource link, title, description), optional `schema` (Zod shape for input validation), and `handler` function. Tools without a `resource` are registered as plain MCP tools (no UI).
</ResponseField>

<ResponseField name="resources" type="ProductionResource[]" required>
  Resource registrations. Each has a `name`, `uri`, `html` (self-contained HTML string), and optional `_meta`.
</ResponseField>

<ResponseField name="auth" type="(req: IncomingMessage) => AuthInfo | null">
  Auth function called on every request. Return `AuthInfo` to authenticate, `null` to reject with 401. See [Server Entry](/app-framework/tools/server-entry).
</ResponseField>

<ResponseField name="serverUrl" type="string">
  Public URL of the MCP server (e.g. `'https://example.com/mcp'`). Used to auto-compute a default `_meta.ui.domain` for resources that don't specify one. Without this, resources without an explicit domain may trigger host warnings (e.g. ChatGPT's "Widget domain is not set").
</ResponseField>

<ResponseField name="enableJsonResponse" type="boolean" default="true">
  Respond with JSON instead of SSE streams. Recommended for serverless environments (Lambda, Workers, Vercel Edge) where holding open SSE connections is unreliable. Set to `false` if you need SSE streaming for long-running tool calls.
</ResponseField>

<ResponseField name="stateless" type="boolean" default="false">
  Enable stateless mode for serverless and horizontally-scaled deployments. When `true`, every request creates a fresh MCP server instance with no session tracking. The host is identified from HTTP headers (`User-Agent`, `x-anthropic-client`, `x-openai-session`) on every request. See [Serverless Deployment](/app-framework/guides/deployment#serverless--edge-deployment) and [Horizontal Scaling](/app-framework/guides/deployment#horizontal-scaling).
</ResponseField>

## `createHandler`

Creates a Web Standard request handler for serverless and edge runtimes.

```ts theme={null}
import { createHandler } from 'sunpeak/mcp';
import type { WebHandlerConfig } from 'sunpeak/mcp';

const handler = createHandler(config);
// handler: (req: Request) => Promise<Response>
```

Unlike `createMcpHandler`, this handler does **not** do path matching — it handles every request it receives. Mount it behind your own router.

### Example: Cloudflare Worker

```ts theme={null}
import { createHandler } from 'sunpeak/mcp';

const handler = createHandler({ tools, resources, auth });
export default { fetch: handler };
```

### Example: Hono

```ts theme={null}
import { Hono } from 'hono';
import { createHandler } from 'sunpeak/mcp';

const app = new Hono();
const handler = createHandler({ tools, resources });
app.all('/mcp', (c) => handler(c.req.raw));
export default app;
```

### `WebHandlerConfig`

Same shape as `ProductionServerConfig` (including `enableJsonResponse` and `stateless`), except `auth` takes a Web Standard `Request`:

<ResponseField name="auth" type="(req: Request) => AuthInfo | null">
  Auth function for Web Standard environments. Return `AuthInfo` to authenticate, `null` to reject with 401.
</ResponseField>

## `startProductionHttpServer`

The built-in HTTP server used by `sunpeak start`. A convenience wrapper around `createMcpHandler` that adds a health check endpoint (`/health`), root page, favicon, and graceful shutdown.

```ts theme={null}
import { startProductionHttpServer } from 'sunpeak/mcp';

// Simple: port only
startProductionHttpServer(config, 3000);

// Full options
startProductionHttpServer(config, { port: 3000, host: '127.0.0.1' });
```

The second argument accepts either a port number or an `HttpServerOptions` object:

<ResponseField name="port" type="number" default="8000">
  HTTP port to listen on.
</ResponseField>

<ResponseField name="host" type="string" default="0.0.0.0">
  Host/interface to bind to. Use `127.0.0.1` to restrict to localhost.
</ResponseField>

For most projects, use `sunpeak start` instead of calling this directly.

## `setJsonLogging`

Enable structured JSON logging for all production server log output.

```ts theme={null}
import { setJsonLogging } from 'sunpeak/mcp';

setJsonLogging(true);
```

When enabled, all log messages are written as JSON lines (`{"ts":"...","level":"info","msg":"..."}`) to stdout (info/warn) or stderr (errors). This is useful for log aggregation tools like Datadog, CloudWatch, or Loki. The `sunpeak start --json-logs` flag calls this automatically.

## See Also

<CardGroup cols={2}>
  <Card horizontal title="sunpeak start" icon="play" href="/app-framework/cli/start">
    Zero-config production server CLI.
  </Card>

  <Card horizontal title="Deployment Guide" icon="rocket" href="/app-framework/guides/deployment">
    Full deployment walkthrough with custom server examples.
  </Card>
</CardGroup>
