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.
frontmcp build --target vercel # dist/vercel/ and .vercel/output/
vercel deploy --prebuilt # upload .vercel/outputReference
What the build writes
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:
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 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:
{
"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; 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.
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. (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: 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. 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. |
Caveats
frontmcp create --target vercelwrites the samebuildCommandandinstallCommandas the build, and nobuilds. Before 1.8.6 it wrotebuilds: [{ src: "dist/main.js", use: "@vercel/node" }], for a file the build doesn't write, and its build command wasnpm run build, which builds the targets indeploymentsand not Vercel's. The build keeps avercel.jsonthat's already there, so a project made by an oldercreatekeeps the old file: delete it and build again, or give it thebuildCommandabove.sqlitekeeps 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
frontmcp1.9.3 in a project made withfrontmcp create, installing, and running.vercel/output/functions/index.func/handler.cjsunder 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 setsNODE_ENV=productionfor its functions wasn't checked.
Usage
Deploying a server
Build:
npx frontmcp build --target vercel[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:
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");NODE_ENV=production MCP_SESSION_SECRET="$(openssl rand -hex 32)" node serve-handler.cjs
curl http://127.0.0.1:4000/healthz{"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
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:
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.
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.
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. (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:
@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.
Older clients lose their session between requests
The session lives in the instance that created it. Store sessions in KV or Upstash, and give every instance the same MCP_SESSION_SECRET: an id made under another one gets 404 invalid session id.