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.
Build with your AI app
Install the closedmcp-builder skill first: it tells your AI app how to build, test and publish a Serverless MCP. Download closedmcp-builder.zip (or the single file at https://closedmcp.com/skills/closedmcp-builder/SKILL.md) and unzip it into:
- Claude Code:
~/.claude/skills/<name>/(or.claude/skills/in a repo) - Codex CLI:
${CODEX_HOME:-~/.codex}/skills/<name>/or~/.agents/skills/<name>/(or.agents/skills/in a repo) - claude.ai: Settings > Capabilities > Skills, upload
<name>.zip
closedmcp-basics.zip installs the same way and helps anyone whose AI app uses ClosedMCP. Then describe the MCP you want and say "Deploy this MCP".
Start here
Choose the destination before creating the project. Ask only if the user has not said who should use it. For a named group, call closedmcp_group_list first, or closedmcp_group_create if it does not exist. Keep its returned id.
Pass that ID as group on every project call: create, files, settings, test, preview, publish, versions, logs, and sharing. You can do this through your current personal/company connection; you do not have to reconnect to build for a group. Each call checks current membership and the group's own permissions and AI-app approval. A connection already scoped to one group cannot target another group.
For example, after creating work-related with ID g_…:
{"name":"work-chat","template":"python","group":"g_…"}Use the returned project ID and the same group for all following calls. Check workspace.id and workspace.name in the results. Omitting group uses the connected workspace; creating a group does not switch it. Never publish personally as a substitute for a group the user requested.
When publishing, use share_with: "group:work-related" with that group ID. share_with selects the audience inside the selected workspace; it does not move a personal project. Group members connect using the returned workspace.mcp_url to discover and call the published tools. Building for a group does not add that group's native tools to a personal connection.
If approval is required, show the returned group-specific review link and stop until its owner/admin approves. Do not ask for a key in chat, retry into personal storage, or say reconnecting alone fixes a permission failure.
1. closedmcp_create_project with a lowercase name and template python (the default) or typescript. The project starts as a working app with one example tool, hello, already served the way ClosedMCP calls it. 2. Add your tools to server.py or server.ts with closedmcp_write_files. Keep the serve(...) line at the end. 3. closedmcp_test: it starts the app in the sandbox and checks it the way ClosedMCP checks a new version, in about 10 to 30 seconds. Pass call to try one tool. Green means the version will go live. 4. closedmcp_preview to try it privately, then closedmcp_publish.
What your app must do
- Serve MCP over Streamable HTTP at
/mcpon0.0.0.0:8080, stateless. Not stdio:mcp.run()with no arguments andStdioServerTransportread stdin, which a hosted app does not have, so the app exits and never goes live. Preview and publish refuse source that only talks stdio (passforce: trueif the check is wrong). With FastMCP without the starter, passhost="0.0.0.0", port=8080and run it withtransport="streamable-http"; its defaults are stdio and127.0.0.1:8000. - Answer
initializeandtools/listwithin 180 seconds of starting, or the new version does not go live.PORT=8080andHOST=0.0.0.0are set for you. - 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.
A minimal app (Python)
The python template writes an app like this one. It ends with serve(mcp) from closedmcp.hosted (SDK 0.3.0 or later), which does what the uvicorn.run line below does. server.py:
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)
The typescript template writes server.ts, which Node 22 runs as is with node server.ts (it strips the types). It ends with serve(build) from @closedmcp/sdk/hosted (SDK 0.3.0 or later), which does what the HTTP server below does. In JavaScript, server.js (CommonJS, so the packages in the base image are found):
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. template: python (default), typescript or none |
| 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 | |
| Check | closedmcp_test | installs dependencies, starts the app, then MCP initialize, tools/list and an optional call over HTTP, as the platform does before a version goes live. Returns passed, the tools and the app's log. entry_command and dependency_lockfile default to the starter's |
| Test | closedmcp_run | a shell command on the latest commit, in the same image as production. The sandbox lasts five minutes and the command is stopped after 285 seconds (exit code 124). Install dependencies and start or test your server in the same command. A long run streams progress every 15 s, so apps that give up on a silent request after about a minute, like Claude Code, keep waiting. |
| Try privately | closedmcp_preview | deploys this commit so only you can call it |
| Publish | closedmcp_publish | entry_command, dependency_lockfile, share_with, needed_database; force skips the stdio check |
| Logs | closedmcp_hosted_logs | build and runtime logs of a version; the newest one by default, including one that failed |
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>orgroup:<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.
Move an existing MCP to a group
Use closedmcp_move_project, or Serverless MCPs → project → Move to a group in the console. Do not rebuild a second copy to change its workspace.
1. Find the destination ID with closedmcp_group_list. From your personal/company connection, call closedmcp_move_project with project, destination_group and share_with: "me" or "group". For a project already in a group, also pass its source group ID. This returns a preview. 2. Review the named source, destination and access change. Source workspace users lose access; the chosen destination audience gains it. Destination admins can manage the app. Repeat with the returned project ID, the same arguments and the unexpired confirmation to apply. Changing the project or its settings requires a fresh preview. 3. Verify the returned destination. Use that group's MCP connection for native tools, and its group ID on subsequent management calls. Moving does not switch your current connection.
The project owner must belong to both workspaces and be allowed to deploy in the destination. Company export also requires company admin. An AI app must be approved in both workspaces. A group-bound connection cannot move between groups: use the personal/company connection or the signed-in console.
The move preserves project/app IDs, Git history, app settings and keys, development/live database data and retained versions. It changes workspace ownership and sharing without copying personal OAuth connections, MCP keys, sessions or old grants. Calls use destination workspace and user IDs. Existing records remain, but an app that keys its own records or permissions by the old IDs may need an app-specific migration; the move does not rewrite application tables. Retained versions use the new audience after rollback. Name conflicts refuse without overwriting anything. Active deployments, container services, externally shared databases and workspace-specific connector credentials must be resolved first; the tool reports the dependency. closedmcp_transfer_project changes owner within one workspace instead.
Two groups may expose the same MCP and tool names. Each connection is scoped to its own group: equal names do not merge projects, credentials or databases. Use the connection labelled for the intended group. If both MCPs connect to the same external service/account, that service's data may still be shared.
People call your tools as <app>__<tool>; dashes in the app name become underscores.
Dependencies
- Python:
requirements.txtorrequirements.lockwith exactname==versionpins.--hash=sha256:is optional, but if one line has a hash, every line needs one. Only prebuilt wheels install. - Node:
package-lock.jsonversion 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:psqlworks with no arguments,DATABASE_URLpoints at dev, andLIVE_DATABASE_URLis 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. A version that fails says why and what to do next in the failure of closedmcp_project_versions and closedmcp_hosted_list, for example startup: app exited (code 0) for a stdio server; failureMessage is the full sentence. An app that exits or listens on the wrong port fails within seconds, not minutes. 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 unlessalso_delete_datais true.closedmcp_restore_projectbrings it back within seven days; publish again to go live.closedmcp_transfer_projecthands 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 in total; the command itself 285 seconds |
| 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. 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.