# 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: | Header | Value | |---|---| | `X-ClosedMCP-User` | the person's ClosedMCP ID (stable) | | `X-ClosedMCP-Email` | their email | | `X-ClosedMCP-Org` | their company's org ID, the slug (e.g. `dacme_io`) | | `X-ClosedMCP-Client` | the 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](/docs/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 | Step | Tool | Notes | |---|---|---| | Create | `closedmcp_create_project` | lowercase name; it becomes the app name | | List | `closedmcp_projects`, `closedmcp_company_projects`, `closedmcp_deleted_projects` | yours, the company's (admins), and deleted ones you can restore | | Rename | `closedmcp_update_project` | name or description | | Write code | `closedmcp_write_files` | each call is one Git commit; 10 MiB in total | | Read code | `closedmcp_read_files`, `closedmcp_list_files`, `closedmcp_history` | | | Test | `closedmcp_run` | a 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 privately | `closedmcp_preview` | deploys this commit so only you can call it | | Publish | `closedmcp_publish` | `entry_command`, `dependency_lockfile`, `share_with`, `needed_database` | | Logs | `closedmcp_hosted_logs` | build 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:` or `group:`: 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 `__`; 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 | What | Limit | |---|---| | Source | 10 MiB; no symlinks, no `.env` files, no private keys | | Build | 5 minutes, 1 CPU, 1 GiB memory | | Image | 500 MiB | | Test run | 5 minutes | | Tool call | 5 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](/docs/event-channels). For checks on a timer, use [monitors](/docs/monitors).