# Vercel

> Deploy a FrontMCP server to Vercel with frontmcp build --target vercel: the files it writes, the vercel.json it makes, what the function serves, when it runs in production, and storing sessions in Vercel KV or Upstash.

Source: https://frontmcp.dev/reference/deployment/vercel

`frontmcp build --target vercel` bundles your server, FrontMCP and all, into one Node function, and writes it in the layout of Vercel's Build Output API, which routes every request to it. The function is FrontMCP's Express app in serverless mode: it serves MCP and `/healthz`, runs in production when `NODE_ENV` is `production`, as it is on Vercel, and keeps nothing between invocations unless you give it storage. Vercel KV, or Upstash's REST API, can hold its sessions.

```bash
frontmcp build --target vercel     # dist/vercel/ and .vercel/output/
vercel deploy --prebuilt           # upload .vercel/output
```

---

## Reference

### What the build writes

```text
dist/vercel/
  main.js, help-desk.app.js, …   your compiled code
  serverless-setup.js            sets FRONTMCP_SERVERLESS=1, FRONTMCP_DEPLOYMENT_MODE=serverless and FRONTMCP_HTTP_ENTRY_PATH
  index.js                       loads the setup, then your server, and exports the handler
  handler.cjs                    index.js and everything it imports that's installed, in one file (about 10 MB)
  queue.widget.tsx, …            your tools' *.widget.tsx and *.widget.jsx files, copied beside handler.cjs
.vercel/output/
  config.json                    { "version": 3, "routes": [{ "src": "/(.*)", "dest": "/index" }] }
  functions/index.func/
    handler.cjs, …               a copy of dist/vercel/
    .vc-config.json              { "runtime": "nodejs24.x", "handler": "handler.cjs", "launcherType": "Nodejs" }
    node_modules/                @vercel/kv and @swc/core, when your package.json lists them
vercel.json                      only when there's none
```

The generated `index.js`:

```js dist/vercel/index.js
require('./serverless-setup.js');
require('./main.js');
const { getServerlessHandlerAsync } = require('@frontmcp/sdk');

let handlerPromise = null;

module.exports = async function handler(req, res) {
  if (!handlerPromise) {
    handlerPromise = getServerlessHandlerAsync();
  }
  const app = await handlerPromise;
  return app(req, res);
};
module.exports.default = module.exports;
```

Loading `main.js` with `FRONTMCP_SERVERLESS=1` makes `@FrontMcp` build [a request handler](https://frontmcp.dev/reference/sdk/frontmcp-instance#frontmcpinstancecreatehandlerconfig) instead of listening, and the first request waits for it.

The function's `node_modules/` holds the packages the bundle can't contain, when your `package.json` lists them: `@vercel/kv`, `@swc/core`, `esbuild`, `openai` and `@anthropic-ai/sdk`.

`frontmcp build` also copies the `*.widget.tsx` and `*.widget.jsx` files of your tools next to `handler.cjs`, in `dist/vercel/` and in the function's folder, and says `copied 1 widget source file (*.widget.tsx/jsx) to dist/vercel`: a tool's `file` widget is read when the tool is called, at `join(__dirname, …)`, and `__dirname` is the folder of `handler.cjs`. Other files that a widget imports aren't copied.

When there's no `vercel.json`, the build writes one and says `Generated vercel.json`:

```json vercel.json
{
  "version": 2,
  "buildCommand": "npx frontmcp build --target vercel",
  "installCommand": "npm install"
}
```

The package manager comes from the lockfile the build finds: `bun.lock` or `bun.lockb` gives `bunx frontmcp build --target vercel` and `bun install`, `pnpm-lock.yaml` gives `pnpm exec …` and `pnpm install`, `yarn.lock` gives `yarn frontmcp build --target vercel` and `yarn install`, and `package-lock.json`, or none, gives `npx` and `npm install`. The command builds the Vercel target itself, so Vercel doesn't run your project's `build` script, which builds whatever `deployments` lists. (Changed in 1.8.6: before, `buildCommand` was `npm run build`, that script.) A `vercel.json` that's already there is kept, and the build says `vercel.json already exists (skipping)`.

#### Packages the build needs

Only the ones your server uses. The bundle includes the optional packages FrontMCP can load when they're installed, and leaves the rest as `require()` calls that run only if your configuration asks for them, so a project made with `frontmcp create` builds as it is, without a warning. Changed in 1.9.3: in 1.9.2 the build stopped with `Module not found: Can't resolve '@upstash/redis'` until you installed that package.

Install `@vercel/kv` when you use [Vercel KV](#storage); without it the function logs `@vercel/kv is required for Vercel KV storage adapter. Install it with: npm install @vercel/kv` and keeps sessions in memory. See [what's in the bundle](https://frontmcp.dev/reference/deployment/production-build#whats-in-the-bundle).

> **Note**
In 1.8.6 the build stopped with `Module not found: Can't resolve '@frontmcp/storage-sqlite'` until you installed `@frontmcp/storage-sqlite`, `@frontmcp/observability`, `@opentelemetry/sdk-trace-base` and `@vercel/kv`, used or not.

### What the function serves

| Request | Answer |
| --- | --- |
| `POST` to the MCP endpoint | MCP, as FrontMCP's Node server answers it. The endpoint is `http.entryPath`, or `/`. |
| `GET /healthz`, `GET /health` | `200` `{ "status": "ok", "server", "runtime": { …, "deployment": "serverless", "env": "production" }, "uptime" }` |
| `GET /readyz` | `404`. Readiness checks are off in the `serverless` deployment mode. |
| Anything else | Express's `404` page, `Cannot POST /mcp` for example. |

`transport.http.path` in `frontmcp.config` reaches the function: the build writes it into `serverless-setup.js`, so a project made with `frontmcp create` serves `/mcp`, and the build ends with `Server will serve MCP at /mcp`. `http.entryPath` in `@FrontMcp` wins over it. See [the endpoint path](https://frontmcp.dev/reference/deployment/production-build#the-endpoint-path). (Before 1.8.7 the function served `/` whatever `frontmcp.config` said.)

#### Production, when `NODE_ENV` says so

The function reads `NODE_ENV` when it starts. Vercel sets it to `production` for its functions (this wasn't checked on Vercel), so a deployed function is in [production mode](https://frontmcp.dev/reference/auth/production#production-mode): clients on protocol versions before 2026-07-28 need `MCP_SESSION_SECRET` in its environment, and without it their `initialize` answers `500` with `{"error":"server_misconfigured","code":"SESSION_SECRET_REQUIRED",…}`. Clients on MCP 2026-07-28 are served without it (changed in 1.8.5). `local` and `remote` auth need `JWT_SECRET` too. Run it locally without `NODE_ENV=production` and `/healthz` says `"env": "development"`: the function makes up secrets and shows callers the messages of plain errors.

Changed in 1.8.7: the bundler used to write `NODE_ENV` into `handler.cjs` as `"production"`, so the function was always in production, and `NODE_ENV=development` at run time changed nothing.

At startup it logs `[Deployment] SSE transport is not supported in serverless deployments. Consider using protocol: "stateless-api" for serverless targets.` for any server that keeps the default `transport.protocol`, and, without storage, `No distributed storage backend detected in production. Using in-memory storage. Set REDIS_URL, UPSTASH_REDIS_REST_URL, or KV_REST_API_URL.`

### Storage

Nothing is shared between invocations of the function, or between its instances, unless you configure it. Clients on MCP 2026-07-28 don't need it: each request carries what it needs. Clients on older protocol versions keep a session, which lives in the instance that created it; the next request may reach another one and get `404` `session not initialized`.

`redis: { provider: "vercel-kv" }` stores sessions over Vercel KV's REST API:

| Option | Default | What it is |
| --- | --- | --- |
| `provider` | | `"vercel-kv"`. |
| `url` | `KV_REST_API_URL` | The REST API's address. |
| `token` | `KV_REST_API_TOKEN` | Its token. |
| `keyPrefix` | `"mcp:"` | Put before every key. Sessions are `mcp:session:<id>`. |
| `defaultTtlMs` | `3600000` | How long a session's key lives, in milliseconds, as with [Redis](https://frontmcp.dev/reference/deployment/redis#how-long-sessions-last). With `120000` the key's TTL was `120`. |

It needs the `@vercel/kv` package, which npm marks 3.0.0 deprecated. Upstash Redis serves the same REST API, so its `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` work as `url` and `token`.

Vercel KV has no publish and subscribe, which background tasks and pending elicitation questions need. FrontMCP 1.9.3 handles that so:

| Configuration | What happens |
| --- | --- |
| `redis: { provider: "vercel-kv" }`, or `KV_REST_API_URL` in the environment with no `redis` | The server starts, with sessions in KV, and logs `[tasks] Background tasks are disabled: Vercel KV has no pub/sub, which task results and cancellation require. Serving without tasks. Configure tasks.redis with Redis or Upstash to enable them.` |
| The same, with `tasks: { enabled: true }` | The handler fails while it loads, so no request is served: `TaskStoreNotSupportedError: Vercel KV is not supported for task stores`. |
| `redis: { provider: "vercel-kv" }`, with `elicitation: { enabled: true }` and no `elicitation.redis` | The same kind of failure: `ElicitationNotSupportedError: Vercel KV is not supported for elicitation stores`. So does `KV_REST_API_URL` in the environment with `elicitation: { enabled: true }` and no `redis` or `elicitation.redis`. |
| `elicitation: { enabled: true, redis: { host, port } }`, with Vercel KV for sessions or `KV_REST_API_URL` set | The server starts, with sessions in KV and pending questions in that Redis. |

> **Note**
In 1.8.6 a server with `redis: { provider: "vercel-kv" }`, or with `KV_REST_API_URL` in its environment, failed with `TaskStoreNotSupportedError` unless you set `tasks: { enabled: false }`, and `elicitation: { enabled: true }` failed with `ElicitationNotSupportedError` whenever `KV_REST_API_URL` was set, even when `elicitation.redis` named a real Redis. `tasks: { enabled: false }` still works, and silences the warning.

#### Caveats

- `frontmcp create --target vercel` writes the same `buildCommand` and `installCommand` as the build, and no `builds`. Before 1.8.6 it wrote `builds: [{ src: "dist/main.js", use: "@vercel/node" }]`, for a file the build doesn't write, and its build command was `npm run build`, which builds the targets in `deployments` and not Vercel's. The build keeps a `vercel.json` that's already there, so a project made by an older `create` keeps the old file: delete it and build again, or give it the `buildCommand` above.
- `sqlite` keeps sessions in a file on one machine, which the function's instances don't share: use KV, Upstash or a Redis they can reach.
- This site couldn't deploy to Vercel. Everything above was checked by building with `frontmcp` 1.9.3 in a project made with `frontmcp create`, installing, and running `.vercel/output/functions/index.func/handler.cjs` under Node's HTTP server, as Vercel's Node runtime calls it. KV was run against a local server that speaks Upstash's REST API, not against Vercel KV or Upstash themselves: the table of tasks and elicitation with 1.9.3, from a Node server with the same configuration, and two copies of the function sharing KV, a session opened on one served by the other, with 1.9.1. That Vercel sets `NODE_ENV=production` for its functions wasn't checked.

---

## Usage

### Deploying a server

Build:

```bash
npx frontmcp build --target vercel
```

```text
[build] Bundling for vercel...
  Created bundle: handler.cjs
[build] Creating vercel deployment structure...
  Created deployment output structure
  Generated vercel.json
Build completed.
```

Set the secrets in the project's environment variables on Vercel (`MCP_SESSION_SECRET` for clients before MCP 2026-07-28, and `JWT_SECRET` for `local` or `remote` auth), then upload the prebuilt output with Vercel's CLI, or let Vercel build it: it runs `buildCommand` from `vercel.json`, which is `npx frontmcp build --target vercel`, whatever `deployments` in `frontmcp.config` lists. A project made with `frontmcp create --target vercel` has that file. A project made with `--target node` has none until you run `npx frontmcp build --target vercel` once, which writes it: commit it. Without a `vercel.json`, the build command is whatever the Vercel project's settings say, by default your `build` script, and in a project made with `--target node` that builds the `node` target.

### Trying the function locally

The handler is a Node `(req, res)` function, so Node's own HTTP server can call it the way Vercel does:

```js serve-handler.cjs
const http = require("node:http");
const handler = require("./.vercel/output/functions/index.func/handler.cjs");

http.createServer((req, res) => handler(req, res)).listen(4000, "127.0.0.1");
```

```bash
NODE_ENV=production MCP_SESSION_SECRET="$(openssl rand -hex 32)" node serve-handler.cjs
curl http://127.0.0.1:4000/healthz
```

```json
{"status":"ok","server":{"name":"help-desk","version":"1.0.0"},"runtime":{"platform":"darwin","runtime":"node","deployment":"serverless","env":"production"},"uptime":3.85}
```

An MCP 2026-07-28 `tools/call` to the function's endpoint (`/mcp` in a project made with `frontmcp create`) returns the tool's result, with or without `MCP_SESSION_SECRET`. An older client's `initialize` needs the secret, or it's `500`, as on Vercel. Without `NODE_ENV=production` the function runs in development mode, which Vercel's doesn't.

### Storing sessions in Vercel KV or Upstash

```ts main.ts
import "reflect-metadata";
import { FrontMcp } from "@frontmcp/sdk";
import { HelpDesk } from "./help-desk.app";

@FrontMcp({
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  http: { entryPath: "/mcp" },
  redis: { provider: "vercel-kv" }, // KV_REST_API_URL and KV_REST_API_TOKEN
  tasks: { enabled: false }, // KV has no publish and subscribe
})
export default class Server {}
```

For Upstash, pass its REST address and token:

```ts
redis: { provider: "vercel-kv", url: process.env.UPSTASH_REDIS_REST_URL, token: process.env.UPSTASH_REDIS_REST_TOKEN },
```

The function logs `vercel-kv session store will be initialized for transport persistence` and `Session store connection validated successfully`, and a client's session is stored as `mcp:session:<id>`, so its next request works on any instance.

---

## Troubleshooting

### `Module not found: Can't resolve '@frontmcp/storage-sqlite'`

A build of 1.8.6 or earlier, which stopped at the first optional package that wasn't installed. Upgrade `frontmcp`; from 1.8.7 the build leaves such packages out. See [packages the build needs](#packages-the-build-needs).

### `Module not found: Can't resolve '@upstash/redis'`

A build of 1.9.2, which stopped on this optional package when it wasn't installed, as in a project from `frontmcp create`. Upgrade `frontmcp` to 1.9.3, which leaves it out, or `npm install @upstash/redis`. See [packages the build needs](#packages-the-build-needs).

### `TaskStoreNotSupportedError: Vercel KV is not supported for task stores`

The server uses Vercel KV and has `tasks: { enabled: true }`. KV has no publish and subscribe, which tasks need. Remove `tasks.enabled`, and the server runs without tasks (with a warning), or give tasks a Redis with `tasks.redis`. See [storage](#storage). (Before 1.8.7 this also happened without `tasks.enabled`, and `tasks: { enabled: false }` was the fix.)

### `ElicitationNotSupportedError: Vercel KV is not supported for elicitation stores`

`elicitation: { enabled: true }` with Vercel KV as the only store, or with `KV_REST_API_URL` in the environment and no store of its own. Give elicitation a Redis with publish and subscribe:

```ts main.ts
@FrontMcp({
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  redis: { provider: "vercel-kv" },
  elicitation: { enabled: true, redis: { host: process.env.REDIS_HOST!, port: 6379, password: process.env.REDIS_PASSWORD } },
})
export default class Server {}
```

With that the server starts, with sessions in KV and pending questions in Redis, whether or not `KV_REST_API_URL` is set. (Before 1.8.7 the error came whenever that variable was set, and you had to rename the KV variables.)

### `initialize` answers `500`, with `"error":"server_misconfigured"` and `SESSION_SECRET_REQUIRED`

`MCP_SESSION_SECRET` isn't set, the function runs in production (`NODE_ENV=production`), and a client on a protocol version before 2026-07-28 tried to open a session. The log says `Session secret is required for session ID encryption`.

### `Cannot POST /mcp`

The function serves another path: `http.entryPath`, else `transport.http.path` from `frontmcp.config`, else `/`. A build of 1.8.6 or earlier served `/` unless `http.entryPath` was set. Set `http: { entryPath: "/mcp" }` in `@FrontMcp`, or point the client at the path the build's last line names.

### `/readyz` answers `404`

Readiness checks are off in serverless mode. Point the platform's checks at `/healthz`. See [Health checks and metrics](https://frontmcp.dev/reference/deployment/health-and-metrics).

### Older clients lose their session between requests

The session lives in the instance that created it. Store sessions in [KV or Upstash](#storing-sessions-in-vercel-kv-or-upstash), and give every instance the same `MCP_SESSION_SECRET`: an id made under another one gets `404` `invalid session id`.
