Browse guides · Serverless MCPs

Serverless MCPs

A Serverless MCP is an MCP server whose code ClosedMCP hosts for you. You, or your AI app, write the code with ClosedMCP tools, test it in a sandbox and publish it. People in your company then use its tools through the ClosedMCP connection they already have. Nobody installs anything.

The app sleeps when nobody uses it and wakes on the next call, so the first call after a quiet period can take a few seconds.

What your app must do

  • Serve MCP over Streamable HTTP at /mcp on 0.0.0.0:8080, stateless.
  • Answer initialize and tools/list within 60 seconds of starting, or the new version does not go live.
  • Expect no internet. The app can reach its own database and nothing else. Everything that changes the app arrives as a tool call through ClosedMCP.
  • Finish each tool call within 5 minutes.

Who is calling

ClosedMCP signs each person in and tells your app who is calling with four headers:

HeaderValue
X-ClosedMCP-Userthe person's ClosedMCP ID (stable)
X-ClosedMCP-Emailtheir email
X-ClosedMCP-Orgtheir company's org ID
X-ClosedMCP-Clientthe AI app they use (display only)

ClosedMCP removes any Authorization header and any copy of these headers sent by someone else, then sets them itself. Inside a hosted app you can trust them.

Do not use the "Log in with ClosedMCP" SDK server wrapper in a hosted app; see The ClosedMCP library.

A minimal app (Python)

server.py:

python
from fastmcp import FastMCP
from fastmcp.server.dependencies import get_http_headers
import uvicorn

mcp = FastMCP("hello")

@mcp.tool
def hello() -> str:
    """Say hello to the person calling."""
    email = get_http_headers(include_all=True).get("x-closedmcp-email", "")
    return f"Hello {email}"

if __name__ == "__main__":
    uvicorn.run(mcp.http_app(path="/mcp", stateless_http=True), host="0.0.0.0", port=8080)

requirements.txt can be empty: the base image already has FastMCP, the MCP SDK and PostgreSQL drivers. Publish with entry_command: ["python", "server.py"] and dependency_lockfile: "requirements.txt".

A minimal app (Node)

server.js (CommonJS, so the packages in the base image are found):

js
const http = require("node:http");
const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
const { StreamableHTTPServerTransport } = require("@modelcontextprotocol/sdk/server/streamableHttp.js");

function build() {
  const server = new McpServer({ name: "hello", version: "1.0.0" });
  server.registerTool("hello", { description: "Say hello to the person calling." }, async (extra) => ({
    content: [{ type: "text", text: `Hello ${extra.requestInfo?.headers["x-closedmcp-email"] ?? ""}` }],
  }));
  return server;
}

http.createServer(async (req, res) => {
  if (req.url !== "/mcp" || req.method !== "POST") return res.writeHead(405).end();
  const server = build();
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  res.on("close", () => { transport.close(); server.close(); });
  await server.connect(transport);
  await transport.handleRequest(req, res);
}).listen(8080, "0.0.0.0");

A Node app needs package.json and package-lock.json (lockfile version 2 or newer) at the project root, even with no dependencies of its own. Publish with entry_command: ["node", "server.js"] and dependency_lockfile: "package-lock.json".

Build, test, publish

StepToolNotes
Createclosedmcp_create_projectlowercase name; it becomes the app name
Listclosedmcp_projects, closedmcp_company_projects, closedmcp_deleted_projectsyours, the company's (admins), and deleted ones you can restore
Renameclosedmcp_update_projectname or description
Write codeclosedmcp_write_fileseach call is one Git commit; 10 MiB in total
Read codeclosedmcp_read_files, closedmcp_list_files, closedmcp_history
Testclosedmcp_runa shell command on the latest commit, in the same image as production, for up to five minutes. Install dependencies and start or test your server in the same command.
Try privatelyclosedmcp_previewdeploys this commit so only you can call it
Publishclosedmcp_publishentry_command, dependency_lockfile, share_with, needed_database
Logsclosedmcp_hosted_logsbuild and runtime logs of a version

entry_command starts your server. Its first word must be node, npm, python or python3, and the file it names must be in the project.

share_with decides who gets the tools:

  • me: only you and your own AI apps.
  • team:<name> or group:<name>: members get the tools automatically.
  • company: everyone or only chosen people, as the company's apps policy says.

Company admins set the widest sharing allowed and who may build and publish. Change sharing later with closedmcp_share_app. Public publishing is not available.

People call your tools as <app>__<tool>; dashes in the app name become underscores.

Dependencies

  • Python: requirements.txt or requirements.lock with exact name==version pins. --hash=sha256: is optional, but if one line has a hash, every line needs one. Only prebuilt wheels install.
  • Node: package-lock.json version 2 or newer; every package at an exact version from registry.npmjs.org with an integrity hash. Install scripts do not run.
  • Your own packages win over the ones in the base image.

The base image has Node 22, Python 3.12, the MCP SDKs, FastMCP, the ClosedMCP SDKs, PostgreSQL drivers and psql.

Database

closedmcp_database_enable gives the project two PostgreSQL databases:

  • dev, used by closedmcp_run: psql works with no arguments, DATABASE_URL points at dev, and LIVE_DATABASE_URL is a read-only view of live.
  • live, used by the published app through DATABASE_URL.

Publish with needed_database: true. Schema changes are not copied from dev to live and are not undone by a rollback, so make your app create or migrate its own tables on start or on first use.

Settings

closedmcp_set_project_setting writes config or a secret; secret values cannot be read. The published app (and a preview) gets every setting as an environment variable of the same name, from the next publish on: a running version keeps the values it was published with, and a rollback brings back that version's values. closedmcp_run sandboxes do not get settings. Names use A-Z, 0-9 and _ and cannot override ClosedMCP's own variables (CLOSEDMCP_*, PG*, DATABASE_URL); at most 100 settings, values up to 8 KiB.

Versions and rollback

Each publish is a new version. It goes live only after it starts and answers; calls already running finish on the old version. closedmcp_hosted_rollback switches back to a kept version without rebuilding; watch closedmcp_project_versions until it says live or failed. If a rollback fails, the current version stays live. closedmcp_unpublish takes the app down now and keeps its versions.

Delete, restore, transfer

  • closedmcp_delete_project (with the exact name) stops the app now and removes it after seven days. Its data stays unless also_delete_data is true.
  • closedmcp_restore_project brings it back within seven days; publish again to go live.
  • closedmcp_transfer_project hands it to another company member.
  • When the creator of a company project leaves the company, the project moves to an admin.

Limits

WhatLimit
Source10 MiB; no symlinks, no .env files, no private keys
Build5 minutes, 1 CPU, 1 GiB memory
Image500 MiB
Test run5 minutes
Tool call5 minutes

Telling agents when something happens

Your app can let an AI agent wait for events, such as a new chat message, with event channels. For checks on a timer, use monitors.

For AI agents: this page as markdown is at /docs/serverless.md, and the closedmcp_guide tool returns it with topic serverless.