MCP server
spawnpoint is agent-native: it ships a first-class Model Context Protocol server. Your agent connects to it and deploys itself.
Endpoint
- URL:
https://app.getspawnpoint.com/mcp. (Running the binary yourself for development? Your local origin plus/mcp.) - Transport: Streamable HTTP (the current hosted-server standard)
- Auth: OAuth 2.1 (PKCE, dynamic client registration), advertised from the endpoint via
.well-knowndiscovery. A long-livedspk_live_…API token as a bearer credential also works.
Connect
curl -fsSL https://getspawnpoint.com/install | bashIt detects Claude Code, Codex CLI, Cursor, opencode, GitHub Copilot CLI, VS Code
Copilot, and jcode, and sets up each one it finds. Limit it to one with
SPAWNPOINT_AGENT=claude|codex|cursor. Point it at a local dev server with
SPAWNPOINT_URL=http://localhost:8080.
Then sign in once, in the browser, for each agent. Find yours below.
Claude Code
- Run the installer: It also adds the spawnpoint plugin, so "deploy this" works as a skill.
curl -fsSL https://getspawnpoint.com/install | bash - Restart Claude Code.
- Run
/mcp, choose spawnpoint, choose Authenticate, and approve in the browser.
Then say "deploy this" and you get a link back.
Cursor
- Run the installer: It writes Cursor's MCP config for you.
curl -fsSL https://getspawnpoint.com/install | bash - Run
cursor-agent mcp login spawnpoint, or open Cursor's MCP settings and approve spawnpoint.
Do not write Cursor's MCP config by hand. The installer puts it in the right place with the right endpoint, and hand-editing still leaves the browser sign-in to do.
Codex CLI
- Run the installer: It runs
curl -fsSL https://getspawnpoint.com/install | bashcodex mcp addfor you. - Run
codex mcp login spawnpoint.
opencode
- Run the installer:
curl -fsSL https://getspawnpoint.com/install | bash - Run
opencode mcp auth spawnpoint.
GitHub Copilot CLI
- Run the installer:
curl -fsSL https://getspawnpoint.com/install | bash - Run
copilot, use any spawnpoint tool, and approve the browser sign-in when it prompts.
VS Code Copilot
- Run the installer:
curl -fsSL https://getspawnpoint.com/install | bash - Open Copilot Chat in agent mode, start the spawnpoint server, and sign in from the browser prompt.
jcode
- Run the installer: It writes the spawnpoint entry into
curl -fsSL https://getspawnpoint.com/install | bash~/.jcode/mcp.json. - Use any spawnpoint tool;
mcp-remoteopens the browser sign-in on first use.
jcode speaks stdio-only MCP today, so the installer bridges it to spawnpoint's
hosted endpoint through the mcp-remote npm package, which needs Node
(npx) on your PATH. On a headless machine with no browser, create an
API token and add
"--header" and "Authorization: Bearer spk_live_your_token" to the
entry's args instead.
Any other MCP client
The installer does not know your client, so this is the one case where you configure it yourself. Every OAuth-capable MCP client needs only the URL, and will prompt you to sign in:
{
"mcpServers": {
"spawnpoint": {
"url": "https://app.getspawnpoint.com/mcp"
}
}
}
Wiring a supported agent by hand, if you must:
claude mcp add --transport http --scope user spawnpoint <url>/mcp,
codex mcp add spawnpoint --url <url>/mcp,
opencode mcp add spawnpoint --url <url>/mcp,
copilot mcp add spawnpoint <url>/mcp,
code --add-mcp '{"name":"spawnpoint","type":"http","url":"<url>/mcp"}'.
Environments with no browser
Clients that cannot do OAuth send a token instead:
{
"mcpServers": {
"spawnpoint": {
"url": "https://app.getspawnpoint.com/mcp",
"headers": { "Authorization": "Bearer spk_live_your_token" }
}
}
}
Every call is scoped to the account that authorized it.
Copilot coding agent (the cloud agent working GitHub issues) has no browser, so it uses
a token: create an API token, store it as a Copilot
agents secret named COPILOT_MCP_SPAWNPOINT_TOKEN, and add spawnpoint to the
repository's MCP configuration (Settings → Copilot → MCP servers) with an
Authorization header that references it.
Tools
Thirteen tools, one job each:
deploy_project: deploy an app, get a public URLcreate_upload: mint a one-time URL to upload files instead of inlining themget_project: poll one project's status, health, and errorget_project_logs: read a project's deploy or runtime logswatch_deploy: follow a deploy step by step and return when it settles; what to call afterdeploy_projectset_visibility: flip a project between private and publicshare_project: let one person into a private project, by emailunshare_project: revoke one viewerlist_projects: list your projects and their URLsterminate_project: stop a project and free its URLwhoami: which account this session is connected to, with balance and burnschedule_teardown: schedule (or cancel) a project's automatic terminationadd_credit: get a Stripe payment link to top up the prepaid balance
deploy_project
Input:
name: a short project nameruntime:static,node, orpython(defaultstatic)entrypoint: file to run fornode/python(e.g.server.js,main.py)files: array of{ path, content }making up the app, inlineupload_id: an id fromcreate_upload, instead offilesenv: optional environment variablesvisibility:restricted(the default: only you and emails you share with) orpublic(anyone with the link)teardown_in: optional; terminate automatically after this long (45m,2h,1d; 5m to 30d). Move or cancel later withschedule_teardown
deploy_project({
"name": "invoice-tool",
"runtime": "node",
"entrypoint": "server.js",
"files": [
{ "path": "server.js", "content": "require('http').createServer(...)" },
{ "path": "package.json", "content": "{ \"name\": \"invoice-tool\" }" }
]
})
// → { "project_id": "proj_…", "url": "https://spawn-….run.openrelay.inc", "status": "spawning" }
The call returns immediately; provisioning finishes in the background. Poll
get_project until the status leaves spawning.
create_upload
Inline files are right for a small app the agent just wrote. For
files that already exist on disk (or any binary asset), inlining means the agent
regenerates every byte as output, which is slow and costs tokens. The upload path
moves the bytes over HTTP instead:
create_upload()
// → { "upload_id": "upl_…", "upload_url": "https://…/api/uploads/upl_…",
// "command": "tar czf - --exclude .git --exclude node_modules . | curl -fsS -T - \"…\"", "note": "…" }
Run the returned command from the app's directory (it tars and
uploads in seconds), then call deploy_project with
upload_id instead of files. The URL takes a gzipped tar,
works exactly once, and expires in 30 minutes; the bundle limit is 20 MiB, and
.git and node_modules must be excluded (dependencies
install on the machine from package.json or
requirements.txt). A rejected archive does not spend the URL: fix the
tar and re-run the command.
set_visibility
set_visibility({ "project_id": "proj_…", "visibility": "public" })
// → { "project_id": "proj_…", "visibility": "public", "share_url": "https://…" }
New projects are private by default: only you and the emails you share
with (verified by a one-time code, no account needed) can open the link. Flip to
public to let anyone with the link view; flip back any time. The share
URL never changes either way, and a flip takes effect immediately.
share_project
share_project({ "project_id": "proj_…", "email": "friend@example.com" })
// → { "project_id": "proj_…", "shared_with": ["friend@example.com"],
// "share_url": "https://…", "note": "an invite email with the link is on its way to …" }
Adds one person to a private project's allow list. They get an invite email with the link, and the first time they open it they verify with a one-time code sent to that address: no account needed. Takes effect immediately. Up to 50 people per project; re-adding someone already on the list is harmless and sends no second email.
unshare_project
unshare_project({ "project_id": "proj_…", "email": "friend@example.com" })
// → { "project_id": "proj_…", "shared_with": [], "note": "… can no longer open the link" }
Removes one viewer. Revocation is instant: the gate checks the list on every request.
get_project
get_project({ "project_id": "proj_…" })
// → { "project_id": "proj_…", "name": "…", "status": "running", "url": "https://…", "health": "ok" }
// on a failed deploy, status is "error" and an "error" field says why
get_project_logs
get_project_logs({ "project_id": "proj_…" })
// → { "kind": "push", "log": "=== upload ===\n=== install ===\n…" }
get_project_logs({ "project_id": "proj_…", "kind": "runtime" })
// → { "kind": "runtime", "log": "<the app's journal tail>" }
push (the default) is the last deploy's combined output: upload,
dependency install, and start. When a deploy errors, the installer's own output is
usually the explanation. runtime fetches the last 200 lines of the
app's journal live from the machine. A note field explains an empty
log (nothing recorded yet, or the machine is gone).
list_projects
list_projects()
// → { "projects": [ { "project_id": "proj_…", "name": "…", "status": "running", "url": "https://…", "health": "ok" } ] }
terminate_project
terminate_project({ "project_id": "proj_…" })
// → { "status": "terminated" }
Runtimes
Your app listens on port 8080. spawnpoint runs it for you:
- static: files are served as a static site.
- node:
node <entrypoint>(your server binds:8080). - python:
python3 <entrypoint>(your server binds:8080).
Dependencies install automatically: ship requirements.txt (python) or
package.json (node) at the bundle root and spawnpoint installs them on the
machine before starting the app, so Flask, Express, and friends just work. Apps run
under a supervisor that restarts them after crashes and machine reboots.
https://app.getspawnpoint.com/mcp for you, and sign-in is one browser approval. The beta is open: sign in with your email and deploy.