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

# The FrontMCP Server

> The core FrontMCP server — start with the minimum and scale up with HTTP, sessions, logging, providers, and authentication.

<Note>
  This page covers **Server Mode** (HTTP). For embedded SDK usage or serverless handlers, see [Runtime Modes](/frontmcp/deployment/runtime-modes).
</Note>

FrontMCP servers are defined with a single decorator, `@FrontMcp({ ... })`. This page shows the **minimal config** and then every **top-level option** you can use. Deep dives live in the pages listed under *Servers*.

## Minimal server

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { FrontMcp } from '@frontmcp/sdk';
import MyApp from './my.app';

@FrontMcp({
  info: { name: 'My Server', version: '0.1.0' },
  apps: [MyApp],
})
export default class Server {}
```

**Required:**

* `info.name` (string)
* `info.version` (string)
* `apps` (at least one app)

Everything else is optional with sensible defaults.

***

## Full configuration (at a glance)

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  /** Required */
  info: {
    name: 'Expense MCP Server',
    version: '1.0.0',
    title?: 'Human title',
    websiteUrl?: 'https://example.com',
    icons?: Icon[], // MCP Icon[]
  },
  apps: [/* App classes */],
```

**Info field descriptions:**

| Field        | Description                           |
| ------------ | ------------------------------------- |
| `name`       | Server name shown in MCP discovery    |
| `version`    | Semantic version for clients to track |
| `title`      | Optional human-readable title         |
| `websiteUrl` | Link to documentation or homepage     |
| `icons`      | MCP Icon array for visual branding    |

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  /** Optional */
  serve?: true,                 // default true (auto-boot)
  splitByApp?: false,           // app composition mode
  providers?: [/* provider classes/factories/values */],

  http?: {
    port?: 3000,                // default: process.env.PORT or 3000
    entryPath?: '',             // MUST match PRM resourcePath in .well-known
    hostFactory?: /* custom host */,
    socketPath?: '/tmp/mcp.sock',  // Unix socket path (overrides port)
    cors?: CorsOptions | false, // default: permissive with built-in adapter (origin: true, credentials: false)
  },

  /** Transport & session lifecycle (NEW in v0.6) */
  transport?: {
    // Session lifecycle
    // sessionMode has been removed in v1.0 — configure via transport.protocol presets
    platformDetection?: { customOnly?: boolean },

    // Protocol configuration (use preset or custom config)
    protocol?: 'legacy' | 'modern' | 'stateless-api' | 'full' | ProtocolConfig,
    // default: 'legacy' (SSE + streamable + legacy SSE support)

    // Session persistence (Redis-backed, auto-enabled when redis is configured)
    persistence?: false | {
      defaultTtlMs?: 3600000,   // 1 hour
    },

    // Distributed mode
    distributedMode?: boolean | 'auto',  // default: false
  },

  /** Shared Redis config (NEW in v0.6) */
  redis?: {
    host: 'localhost',
    port?: 6379,
    password?: string,
    db?: 0,
    tls?: false,
    keyPrefix?: 'mcp:',
    defaultTtlMs?: 3600000,
  },

  /** Default ESM package loader config (affects all App.esm() apps) */
  loader?: {
    url?: string,           // Base URL for registry + bundle fetching
    registryUrl?: string,   // Separate registry URL (if different)
    token?: string,         // Bearer token for auth
    tokenEnvVar?: string,   // Env var name containing bearer token (takes precedence over `token`)
  },

  logging?: {
    level?: LogLevel.Info,      // Debug | VERBOSE | Info | Warn | Error | Off
    enableConsole?: true,       // default true
    prefix?: string,
    transports?: [/* custom log transports */],
  },

  /** Server-level default auth (omit if splitByApp: true) */
  auth?: (
    | { mode: 'public', ... }
    | { mode: 'transparent', ... }
    | { mode: 'local', ... }
    | { mode: 'remote', provider: 'https://auth.example.com', ... }
  ),
})
```

***

## Composition mode

FrontMCP can host **many apps**. Choose how they’re exposed:

* **Multi-App (default)**: `splitByApp: false`
  One server scope. You *may* configure **server-level `auth`** and all apps inherit it (apps can still override with app-level auth).

* **Split-By-App**: `splitByApp: true`
  Each app is isolated under its own scope/base path (for example `/billing`). Streamable HTTP, the `/message` SSE endpoint, and OAuth issuers reuse that scope automatically. **Server-level `auth` is disallowed**; configure auth per app. (See *Authentication → Overview*.)

<Tip>If you’re offering multiple products or tenants, `splitByApp: true` gives clean separation and per-app auth.</Tip>

***

## HTTP transport

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
http: {
  port?: number;                // default: process.env.PORT or 3000
  entryPath?: string;           // default ''; MUST match the PRM resourcePath in .well-known
  hostFactory?: FrontMcpServer | ((cfg) => FrontMcpServer);
  socketPath?: string;          // Unix socket path (overrides port)
  cors?: CorsOptions | false;   // default: permissive with built-in adapter (origin: true, credentials: false)
  routes?: HttpRouteConfig[];   // custom HTTP routes (see Custom HTTP routes below)
}
```

| Field         | Description                                                                          |
| ------------- | ------------------------------------------------------------------------------------ |
| `port`        | HTTP listening port (default: `process.env.PORT` or `3000`)                          |
| `entryPath`   | JSON-RPC entry path; must match `.well-known` discovery                              |
| `hostFactory` | Custom host implementation for advanced setups                                       |
| `socketPath`  | Unix socket path; when set, server listens on a socket instead of a TCP port         |
| `cors`        | CORS configuration (see [CORS](#cors) below)                                         |
| `routes`      | First-class custom HTTP routes (see [Custom HTTP routes](#custom-http-routes) below) |

* **Port**: listening port for Streamable HTTP. Defaults to `process.env.PORT` or `3000` when omitted.
* **entryPath**: your MCP JSON-RPC entry (`''` or `'/mcp'`). Must align with discovery.
* **hostFactory**: advanced — provide/construct a custom host implementation.
* **socketPath**: listen on a Unix socket instead of a TCP port. The entire HTTP feature set (streamable HTTP, SSE, elicitation, sessions) works unchanged over Unix sockets.
* **routes**: mount custom HTTP handlers on the same listener as the MCP endpoint (downloads, webhooks, extra health probes). See [Custom HTTP routes](#custom-http-routes).
* **Split-by-app scopes**: when `splitByApp` is enabled, clients hit `<entryPath>/<appId>` (for example `/mcp/billing`) and subscribe via `<entryPath>/<appId>/message`; FrontMCP handles the prefixing.

### CORS

CORS is configured via the `http.cors` option. It supports three modes:

| Value         | Behavior                                                                     |
| ------------- | ---------------------------------------------------------------------------- |
| `undefined`   | **Default** — permissive CORS enabled (`origin: true`, `credentials: false`) |
| `false`       | CORS disabled entirely (no CORS headers are sent)                            |
| `CorsOptions` | Custom CORS configuration                                                    |

```typescript theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
interface CorsOptions {
  origin?:
    | boolean               // true = reflect request origin
    | string                // single allowed origin
    | string[]              // multiple allowed origins
    | ((origin: string | undefined, cb: (err: Error | null, allow?: boolean) => void) => void);
  credentials?: boolean;    // allow cookies / auth headers (default: false)
  maxAge?: number;          // preflight cache duration in seconds
}
```

| Field         | Type                                        | Default | Description                                                                                                                                            |
| ------------- | ------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `origin`      | `boolean \| string \| string[] \| function` | —       | Allowed origins; `true` reflects the request `Origin` header. Defaults to `true` only via the built-in `DEFAULT_CORS` when `cors` is omitted entirely. |
| `credentials` | `boolean`                                   | `false` | Whether to allow credentials (cookies, authorization headers)                                                                                          |
| `maxAge`      | `number`                                    | —       | How long (in seconds) browsers may cache preflight responses                                                                                           |

<Warning>
  The permissive default (`origin: true`, `credentials: false`) only applies when using the **built-in Express adapter** (i.e. no `hostFactory`). When a custom `hostFactory` is provided, the factory receives the full `http` config (including `cors`) but is responsible for applying CORS itself — FrontMCP does not install CORS middleware automatically.
</Warning>

<Tip>
  An empty `cors: {}` object (without an `origin` key) will **not** enable CORS middleware — the adapter requires an explicit `origin` value. To enable permissive CORS, omit `cors` entirely or pass `cors: { origin: true }`.
</Tip>

<Info>
  FrontMCP automatically adds `Mcp-Session-Id` (alongside `WWW-Authenticate`) to the `Access-Control-Expose-Headers` response header when CORS is enabled. This ensures Streamable HTTP clients can read the session ID from cross-origin responses without additional configuration.
</Info>

<CodeGroup>
  ```ts Default (permissive) theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  @FrontMcp({
    http: { port: 3001 },
    // cors is undefined → permissive CORS (origin: true, credentials: false)
  })
  ```

  ```ts Disabled theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  @FrontMcp({
    http: {
      port: 3001,
      cors: false,  // No CORS headers
    },
  })
  ```

  ```ts Custom theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  @FrontMcp({
    http: {
      port: 3001,
      cors: {
        origin: ['https://app.example.com', 'https://admin.example.com'],
        credentials: true,
        maxAge: 86400,  // cache preflight for 24 hours
      },
    },
  })
  ```
</CodeGroup>

***

## Custom HTTP routes

`http.routes` mounts first-class custom HTTP handlers on the **same listener** as the MCP JSON-RPC endpoint. They share the configured CORS policy, body limits, and security middleware. Use them for byte delivery (file/stream/binary downloads), webhooks, health probes beyond `/health`, or any non-JSON-RPC surface the MCP channel cannot serve.

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
http: {
  routes: [
    // Public route (default) — no auth.
    {
      method: 'GET',
      path: '/download/:id',
      handler: (req, res) => {
        res.setHeader('Content-Type', 'application/pdf'); // see Content-Type note below
        res.status(200).send(loadPdfBytes(req.params.id));
      },
    },
    // Auth-gated route — runs the MCP `session:verify` flow first.
    {
      method: 'POST',
      path: '/webhooks/billing',
      auth: true,
      handler: (req, res) => {
        // req.authSession is populated when auth: true and verification passed
        res.status(202).json({ accepted: true, user: req.authSession?.user?.sub });
      },
    },
  ],
}
```

| Field     | Type                   | Default          | Description                                                                                |
| --------- | ---------------------- | ---------------- | ------------------------------------------------------------------------------------------ |
| `method`  | `HttpMethod`           | —                | `'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' \| 'OPTIONS' \| 'HEAD'`                   |
| `path`    | `string`               | —                | Express-style path (`/files/:id`); rejected at startup if it collides with a reserved path |
| `handler` | `ServerRequestHandler` | —                | `(req, res, next) => void \| Promise<void>`                                                |
| `auth`    | `boolean`              | `false` (public) | When `true`, gate the route behind the MCP `session:verify` flow                           |

`req` / `res` are exported as `ServerRequest` / `ServerResponse` from `@frontmcp/sdk`; `ServerRequestHandler` types the handler. Respond with `res.status(...).json(...)` / `res.send(...)`, or call `next()` to fall through.

### `auth` opt-in

Routes are **public by default**. With `auth: true`, the request runs through the same `session:verify` flow as the MCP endpoint:

* **Unauthorized** → `401` + `WWW-Authenticate` header.
* **Forbidden** (valid token, insufficient scope) → `403` + `WWW-Authenticate`.
* **Authorized** → the verified authorization is attached to `req.authSession` before the handler runs.

In **public auth mode** (or transparent with `allowAnonymous`), `auth: true` routes get an anonymous session and the handler still runs. Under modes with no anonymous fallback (e.g. `local` with `allowDefaultPublic: false`), unauthenticated requests receive the `401`.

### Reserved-path guard

<Warning>
  Custom paths that collide with FrontMCP's own surfaces are **rejected at startup** (fail-fast): the resolved MCP entry path (and its `/sse` + `/message` siblings, including split-by-app bases like `/mcp/billing`), anything under `/oauth/*` and `/.well-known/*`, and `/health` + `/metrics`. Pick a non-reserved path.
</Warning>

### Content-Type gotcha

<Warning>
  The built-in Express adapter defaults **every** response to `application/json; charset=utf-8`. HTML, binary, and streaming handlers MUST set their own content type via `res.setHeader('Content-Type', ...)` (or `res.type(...)`) **before** sending the body. `hostFactory` users are not affected — they own their Express app.
</Warning>

### Large payloads: prefer a custom route over `@Resource`

<Tip>
  When a tool needs to hand the client a **large** payload, return a [`resource_link`](/frontmcp/servers/resources) that points at a custom `http.routes` **GET** handler, and serve the bytes from that handler (it supports stream/binary delivery). A [`@Resource`](/frontmcp/servers/resources) rides the MCP JSON-RPC channel and is **not** out-of-band — large reads block the protocol stream and inflate token usage. The custom route delivers bytes on a separate HTTP request the client fetches directly.
</Tip>

***

## Transport

<Info>
  Transport configuration is a dedicated top-level `transport` property, separate from `auth`. It controls session lifecycle, protocol presets, and persistence.
</Info>

The `transport` config controls session lifecycle, protocol presets, and session persistence. Configure it at the server level or per-app when using `splitByApp: true`.

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  transport: {
    // Session lifecycle
    platformDetection: { customOnly: false },

    // Protocol preset (or custom config)
    protocol: 'legacy',  // default - SSE + streamable + legacy SSE support

    // Session persistence (auto-enabled when top-level redis is configured)
    persistence: {
      defaultTtlMs: 3600000,
    },

    // Distributed mode for serverless/multi-instance deployments
    distributedMode: false,
  },
})
```

### Session Lifecycle

| Option              | Default     | Description                                                                 |
| ------------------- | ----------- | --------------------------------------------------------------------------- |
| `platformDetection` | `undefined` | Platform detection config; set `customOnly: true` to skip built-in mappings |

### Protocol Presets

Use protocol presets for simplified configuration, or provide a custom config object for fine-grained control:

| Preset          | Description                                  | Legacy SSE | SSE | Streamable | JSON | Stateless | Strict Session |
| --------------- | -------------------------------------------- | ---------- | --- | ---------- | ---- | --------- | -------------- |
| `legacy`        | **Default** - Modern + legacy SSE support    | ✅          | ✅   | ✅          | ❌    | ❌         | ✅              |
| `modern`        | SSE + streamable HTTP with strict sessions   | ❌          | ✅   | ✅          | ❌    | ❌         | ✅              |
| `stateless-api` | No sessions, pure request/response           | ❌          | ❌   | ❌          | ❌    | ✅         | ❌              |
| `full`          | All protocols enabled, maximum compatibility | ✅          | ✅   | ✅          | ✅    | ✅         | ❌              |

<CodeGroup>
  ```ts Using a Preset theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  @FrontMcp({
    transport: {
      protocol: 'legacy',  // Default - backwards compatible
    },
  })
  ```

  ```ts Custom Protocol Config theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  @FrontMcp({
    transport: {
      protocol: {
        sse: true,
        streamable: true,
        json: true,        // Enable JSON-only responses
        stateless: false,
        legacy: true,
        strictSession: false,
      },
    },
  })
  ```
</CodeGroup>

### Protocol Options (for custom config)

| Option          | Default (legacy) | Description                                               |
| --------------- | ---------------- | --------------------------------------------------------- |
| `sse`           | `true`           | Enable SSE listener for server-initiated messages         |
| `streamable`    | `true`           | Enable streamable HTTP transport (POST with SSE response) |
| `json`          | `false`          | Enable JSON-only responses (stateful HTTP)                |
| `stateless`     | `false`          | Enable stateless HTTP mode (no session required)          |
| `legacy`        | `true`           | Enable legacy SSE transport for older clients             |
| `strictSession` | `true`           | Require session ID for streamable HTTP                    |

<Tip>
  Use `protocol: 'legacy'` (the default) for maximum backwards compatibility with older MCP clients. Use `protocol: 'modern'` when you only need to support newer Streamable HTTP clients.
</Tip>

### Session Persistence

Session persistence is **automatically enabled** when you configure top-level `redis`. Sessions are persisted to Redis/Vercel KV and transports can be recreated after server restart.

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  redis: {
    host: 'redis.example.com',
    port: 6379,
  },
  // persistence auto-enabled using global redis config
})
```

To explicitly disable persistence when redis is configured:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  redis: { host: 'localhost' },
  transport: {
    persistence: false,  // Explicitly disable
  },
})
```

To customize persistence TTL:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  redis: { host: 'localhost' },
  transport: {
    persistence: {
      defaultTtlMs: 7200000, // 2 hours (default: 1 hour)
    },
  },
})
```

| Option         | Default   | Description                          |
| -------------- | --------- | ------------------------------------ |
| `defaultTtlMs` | `3600000` | Session TTL in milliseconds (1 hour) |

<Info>
  Session persistence uses the top-level `redis` config. Configure `redis` once and it's automatically shared by `transport.persistence` and `auth.tokenStorage`.
</Info>

You can apply different transport policies per app when `splitByApp: true`. Sensitive apps stay stream-only with strict session IDs, while demo or health-check apps can enable stateful/stateless HTTP for easier automation.

***

## Redis

<Info>
  Redis configuration is a dedicated top-level `redis` property, shared by both `transport.persistence` and `auth.tokenStorage`.
</Info>

Configure Redis once at the server level for use across features:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  redis: {
    host: 'redis.example.com',
    port: 6379,
    password: 'secret',
    db: 0,
    tls: true,
    keyPrefix: 'mcp:',
    defaultTtlMs: 3600000,
  },
})
```

| Option         | Default    | Description                          |
| -------------- | ---------- | ------------------------------------ |
| `host`         | (required) | Redis server hostname                |
| `port`         | `6379`     | Redis server port                    |
| `password`     | -          | Optional authentication password     |
| `db`           | `0`        | Redis database index                 |
| `tls`          | `false`    | Enable TLS/SSL connection            |
| `keyPrefix`    | `'mcp:'`   | Prefix for all Redis keys            |
| `defaultTtlMs` | `3600000`  | Default TTL for cached data (1 hour) |

### Usage with Features

The `redis` config is automatically used by:

* **Session persistence** (`transport.persistence`) — stores session state for recovery
* **Token storage** (`auth.tokenStorage: { redis: { ... } }`) — stores refresh tokens securely

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@FrontMcp({
  redis: {
    host: 'localhost',
    keyPrefix: 'myapp:',
  },
  transport: {
    persistence: { defaultTtlMs: 3600000 },
  },
  auth: {
    mode: 'local',
    id: 'local',
    name: 'Local Auth',
    tokenStorage: { redis: { host: 'localhost' } },
  },
})
```

***

<Tip>
  Instead of individual flags, use a **protocol preset**: `'legacy'` (default), `'modern'`, `'stateless-api'`, or `'full'`.
</Tip>

***

## Logging

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
logging: {
  level?: LogLevel;        // default Info
  enableConsole?: boolean; // default true
  prefix?: string;
  transports?: LogTransportType[]; // custom sinks via @FrontMcpLogTransport
}
```

| Field           | Description                                               |
| --------------- | --------------------------------------------------------- |
| `level`         | Minimum log level: Debug, Verbose, Info, Warn, Error, Off |
| `enableConsole` | Whether to output to stdout (default: true)               |
| `prefix`        | Optional prefix for all log messages                      |
| `transports`    | Custom log transport implementations                      |

Use custom log transports for shipping logs to external systems; console remains on by default.

***

## Global providers

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
providers: [
  /* Provider classes/factories/values */
];
```

Define DI-style singletons available to all apps (and their tools/plugins). Provider lifetime scopes: `'global'` (default, shared across all requests) or `'context'` (one instance per execution context).

***

## Authentication (server level)

Server-level `auth` sets the **default** auth for all apps (unless `splitByApp: true`, where auth must be per-app).

### Remote OAuth (encapsulated external IdP)

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
auth: {
  mode: 'remote',
  provider: 'https://auth.example.com',   // OAuth provider base URL (required)
  clientId?: string,
  clientSecret?: string,
  scopes?: string[],
  providerConfig?: {
    dcrEnabled?: boolean,
    authEndpoint?: string,
    tokenEndpoint?: string,
    // ...other advanced provider fields
  },
  tokenStorage?: 'memory' | { redis: RedisConfig }, // default: 'memory'
  consent?: ConsentConfig,
  publicAccess?: PublicAccessConfig,
  refresh?: TokenRefreshConfig,
  expectedAudience?: string | string[],
}
```

### Local OAuth (built-in AS)

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
auth: {
  mode: 'local',
  local?: {                  // local signing config (optional)
    issuer?: string,
    signingKey?: JWK | Uint8Array,
    jwks?: JSONWebKeySet,
  },
  tokenStorage?: 'memory' | { redis: RedisConfig }, // default: 'memory'
  consent?: ConsentConfig,
  publicAccess?: PublicAccessConfig,
  allowDefaultPublic?: boolean, // default false
  anonymousScopes?: string[],   // default ['anonymous']
}
```

<Info>
  Apps can also define their own `auth` (and mark themselves `standalone`) to expose an isolated auth surface — useful
  when mixing public and private apps under one server.
</Info>

***

## Bootstrapping & discovery

* **Version safety**: on boot, FrontMCP checks that all `@frontmcp/*` packages are aligned and throws a clear “version mismatch” error otherwise.

<Warning>If you disable `serve`, you’re responsible for calling the core bootstrap yourself.</Warning>

***

## Common starting points

* **Single app, default everything**: minimal sample above.
* **Multiple apps, shared auth**: omit `splitByApp`, set server-level `auth`.
* **Isolated apps with per-app auth**: set `splitByApp: true`, configure `auth` in each app.

***

## Best Practices

**Do:**

* Start with minimal config and add options as needed
* Use `splitByApp: true` for multi-tenant deployments
* Use top-level `transport` config (not `auth.transport` or `session`)
* Use protocol presets (`'legacy'`, `'modern'`, etc.) instead of individual flags
* Configure `redis` at top-level for shared use across features (auto-enables persistence)
* Use Redis for session persistence in production deployments
* Set log level to `Info` or higher in production

**Don't:**

* Configure server-level `auth` when using `splitByApp: true`
* Use deprecated `auth.transport` or `session` — migrate to `transport`
* Disable `serve` unless you're managing bootstrap manually
* Use `protocol: 'stateless-api'` with short-lived upstream tokens
* Use `protocol: 'stateless-api'` in production without trust boundaries
* Leave `enableConsole: true` in containerized production (use transports)

***

Next up: learn how to structure **Apps**, **Tools**, **Resources**, and **Prompts** in the *Core Components* section.
