MCP Documentation

Overview

What the MCP server is, where it lives, how it authenticates, and everything it can do.

What is MCP

The Model Context Protocol lets an AI assistant call real tools instead of guessing. Point a client at the Static.app MCP server and your assistant can list your sites, edit files, deploy a ZIP, and read form entries — in plain language, with your own API key.

Everything the server does runs through the Static.app REST API, so an MCP action and an API call have identical effects and permissions.

Requires a paid plan. The MCP server is included on paid plans. See Pricing.

Endpoint

One hosted server, SSE transport, JSON-RPC 2.0 — per the MCP specification (2025-03-26).

GET https://mcp.static.app/sse
EndpointMethodPurpose
/sseGETOpens the event stream and hands back a session message URL.
/message?sessionId={id}POSTSends JSON-RPC requests into an open session.
/uploadPOST / PUTStages a ZIP out of band. See Uploading.

Authentication

The server is multi-tenant: it holds no key of its own, so every client brings its own. Create one in Account → API — keys start with sk_. Three ways to pass it, in the order the server checks them:

MethodValueNotes
Authorization headerAuthorization: Bearer sk_xxxxRecommended.
Custom headerX-API-Key: sk_xxxxFor clients that cannot set Authorization.
Query parameter/sse?api_key=sk_xxxxEnds up in logs and browser history. Local development only.
Keep the key private. It has full access to your account. Never commit it — put it in an environment variable and reference it from the client config.

All tools

16 tools across three groups. Every one of them runs on our servers — none of them can see files on your machine, so content travels as arguments or through the upload endpoint.

ToolGroupWhat it does
list_sitesSitesList every site on the account.
get_siteSitesGet one site by PID.
get_site_filesSitesList a site's files.
create_site_from_archiveSitesCreate a site from a ZIP.
update_site_from_archiveSitesReplace a site's contents from a ZIP.
download_site_archiveSitesGet a download link for the whole site.
download_site_filesSitesGet a download link for selected files.
delete_siteSitesDelete a site.
write_site_filesFilesCreate or replace files from inline content.
read_site_fileFilesRead one file.
delete_site_filesFilesDelete files or directories.
create_uploadUploadingReserve a one-time upload slot for a ZIP.
list_formsFormsList a site's forms.
get_formFormsGet one form.
get_form_entriesFormsList submissions.
delete_form_entryFormsDelete a submission.

Site identifier (pid)

Every tool that touches a site takes its public 10-character identifier, e.g. 0aw4jtby1z — never the numeric database id. Run list_sites to see the PIDs on your account.

Setup

Connect a client in a couple of minutes. Every config below points at the same endpoint and differs only in how the client wants it written.

Claude Code

One command, run inside your project:

claude mcp add --transport sse static-app https://mcp.static.app/sse \
    --header "Authorization: Bearer sk_xxxx"

Or commit a .mcp.json to the project and keep the key in the environment:

{
    "mcpServers": {
        "static-app": {
            "command": "npx",
            "args": [
                "-y", "mcp-remote",
                "https://mcp.static.app/sse",
                "--header", "Authorization: Bearer ${STATIC_API_KEY}"
            ],
            "env": { "STATIC_API_KEY": "sk_xxxx" }
        }
    }
}

Claude Desktop

Claude Desktop speaks stdio, so mcp-remote bridges it to the hosted endpoint. Open Settings → Developer → Edit Config and add:

{
    "mcpServers": {
        "static-app": {
            "command": "npx",
            "args": [
                "-y", "mcp-remote",
                "https://mcp.static.app/sse",
                "--header", "Authorization: Bearer ${STATIC_API_KEY}"
            ],
            "env": { "STATIC_API_KEY": "sk_xxxx" }
        }
    }
}

Restart Claude Desktop to pick up the change.

Cursor

Open Settings → MCP Servers → Add Server:

  • Name: static-app
  • Type: sse
  • URL: https://mcp.static.app/sse
  • Headers: Authorization: Bearer sk_xxxx

Windsurf

Open Windsurf settings and add an MCP server with the same four values:

  • Name: static-app
  • Transport: sse
  • URL: https://mcp.static.app/sse
  • Headers: Authorization: Bearer sk_xxxx

n8n & custom clients

Any client that speaks MCP over SSE works. Point it at https://mcp.static.app/sse with the Authorization header, and it will discover all 16 tools on connect.

Building your own? The handshake is two calls: open the stream, then post JSON-RPC to the session URL the stream hands back in its first endpoint event.

curl -N https://mcp.static.app/sse \
    -H "Authorization: Bearer sk_xxxx"

# event: endpoint
# data: https://mcp.static.app/message?sessionId=f5df9141-...
If your client cannot hold an SSE connection open, use the REST API directly — it exposes the same capabilities without a session.

Verify the connection

Ask the assistant to list your sites. If tools are wired up correctly it calls list_sites and answers with real domains. To check by hand, post into the session URL from above:

curl -X POST "https://mcp.static.app/message?sessionId=YOUR_SESSION_ID" \
    -H "Authorization: Bearer sk_xxxx" \
    -H "Content-Type: application/json" \
    -d '{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": { "name": "list_sites", "arguments": {} }
    }'

The result arrives on the SSE stream, not in the POST response — that call returns an empty acknowledgement.

Site tools

List, inspect, deploy, and delete sites. Every one of these takes the site's pid, not its numeric id.

list_sites

Returns every site in the active workspace.

TOOL list_sites

Arguments

NameTypeRequiredDescription
No arguments.

Example result

[
    {
        "pid": "xxxxxxxxxx",
        "slug": "example-site",
        "name": "Example Site",
        "domain": "example.static.app",
        "status": true,
        "full_url": "https://example.static.app",
        "created_at": "2025-01-15T10:00:00Z",
        "updated_at": "2025-02-01T12:30:00Z"
    }
]

Ask your assistant

"Show me all my Static.app sites."

get_site

Returns a single site.

TOOL get_site

Arguments

NameTypeRequiredDescription
pidstringrequiredPublic identifier, e.g. 0aw4jtby1z.

get_site_files

Lists a site's files and directories, recursively from the root.

TOOL get_site_files

Arguments

NameTypeRequiredDescription
pidstringrequiredPublic identifier of the site.

Example result

{
    "status": "success",
    "current_path": "",
    "files": [
        { "name": "index.html", "path": "index.html", "is_dir": false, "size": 1234, "type": "html", "hash": "0123456789abcdef0123456789abcdef" },
        { "name": "images", "path": "images", "is_dir": true, "size": null, "type": "directory", "hash": null }
    ]
}

create_site_from_archive

Creates a new site from a ZIP archive. Give exactly one archive source — they are tried in the order below. The server cannot read files from your machine, so the archive arrives either through create_upload or from a public URL.

TOOL create_site_from_archive

Arguments

NameTypeRequiredDescription
upload_idstringoptionalPreferred. Id from create_upload once the ZIP has been PUT to its URL.
archive_urlstringoptionalURL to download the ZIP from.
archive_base64stringoptionalLegacy fallback. See Legacy base64.
namestringoptionalDesired subdomain, e.g. my-site. A random one is generated otherwise.

Example result

{
    "status": "success",
    "pid": "xxxxxxxxxx",
    "slug": "my-site",
    "url": "https://my-site.static.app"
}

update_site_from_archive

Replaces a site's contents with a new ZIP. To change a handful of files instead of the whole site, use write_site_files.

TOOL update_site_from_archive

Arguments

NameTypeRequiredDescription
pidstringrequiredSite to update.
upload_idstringoptionalPreferred. Id from create_upload.
archive_urlstringoptionalURL to download the ZIP from.
archive_base64stringoptionalLegacy fallback.

download_site_archive

Packs the whole site and returns a download link.

TOOL download_site_archive

Arguments

NameTypeRequiredDescription
pidstringrequiredSite to download.

Example result

{
    "status": "success",
    "url": "https://static.app/storage/zip/abcdef0123456789_download.zip"
}

download_site_files

Packs selected files and returns a download link.

TOOL download_site_files

Arguments

NameTypeRequiredDescription
pidstringrequiredSite to download from.
filesstringrequiredJSON array of paths, e.g. ["index.html", "css/style.css"].

delete_site

Deletes a site and everything in it. There is no undo.

TOOL delete_site

Arguments

NameTypeRequiredDescription
pidstringrequiredSite to delete.

File tools

Read, write, and delete individual files without redeploying the whole site.

write_site_files

Creates or replaces files by sending their contents inline. Files that do not exist yet are created, and missing directories are created along the way. Nothing needs to be on disk anywhere, so this is the tool that works against the hosted server.

TOOL write_site_files

Arguments

NameTypeRequiredDescription
pidstringrequiredSite to write to.
filesstringrequiredJSON array of {path, content, encoding}. Up to 200 entries.
create_onlybooleanoptionalFail with 409 rather than overwrite an existing file. Defaults to false.

File entry fields

FieldTypeRequiredDescription
pathstringrequiredRelative to the site root, e.g. css/style.css. Paths that walk outside the site are rejected.
contentstringrequiredThe file's contents. Max 200MB.
encodingstringoptional"text" (default) sends the content verbatim; "base64" for images and other binaries.

Example arguments

{
    "pid": "xxxxxxxxxx",
    "files": "[{\"path\": \"index.html\", \"content\": \"<!doctype html><h1>Hello</h1>\"}, {\"path\": \"img/logo.png\", \"content\": \"iVBORw0KGgo...\", \"encoding\": \"base64\"}]"
}

Example result

{
    "status": "success",
    "written": 2,
    "files": [
        { "name": "index.html", "path": "index.html", "size": 34, "hash": "0123456789abcdef0123456789abcdef" },
        { "name": "logo.png", "path": "img/logo.png", "size": 2048, "hash": "89abcdef0123456789abcdef01234567" }
    ],
    "errors": []
}
Text content travels as-is, so HTML, CSS, and JS carry no encoding overhead. Reach for base64 only for binaries.

Errors

CodeWhen
409create_only was set and every file already exists.
422Nothing could be written — unsupported extension, invalid path, or bad base64.

Ask your assistant

"Add a privacy.html page to site xxxxxxxxxx with a short placeholder policy."

"Change the heading colour in css/style.css on site xxxxxxxxxx to #6551E0."

read_site_file

Returns one file's contents. Text comes back verbatim; binaries come back base64-encoded with encoding set accordingly.

TOOL read_site_file

Arguments

NameTypeRequiredDescription
pidstringrequiredSite to read from.
pathstringrequiredRelative to the site root, e.g. css/style.css.

Example result

{
    "status": "success",
    "path": "css/style.css",
    "encoding": "text",
    "size": 14,
    "hash": "fedcba9876543210fedcba9876543210",
    "content": "body{margin:0}"
}

Errors

CodeWhen
400path is missing or points outside the site.
404Site or file not found.
413File is larger than 1MB — use download_site_files instead.

delete_site_files

Removes files or directories. Directories go recursively.

TOOL delete_site_files

Arguments

NameTypeRequiredDescription
pidstringrequiredSite to delete from.
pathsstringrequiredJSON array of paths, e.g. ["old.html", "assets/legacy"].

A note on local paths

The hosted server runs on our infrastructure and shares no filesystem with you, so there is no tool that takes a path on your machine. Anything that has to travel does so as content or over HTTP:

You want toUse
Change or add a filewrite_site_files — send the contents
Deploy a ZIP you have locallycreate_upload, then upload_id
Deploy a ZIP already onlinearchive_url

Self-hosting the server yourself? Set LOCAL_FILE_ACCESS=true and it additionally exposes upload_site_files plus an archive_path argument, which read from the machine the server runs on.

Uploading

How to get content into a site without paying for it twice — once in bandwidth, once in tokens.

Which method to use

The server runs remotely, so nothing reads from your disk. Two questions decide the rest: are you changing a few files or replacing the whole site, and does your client have a shell?

SituationUseCost in tokens
Editing pages, styles, adding a filewrite_site_filesThe file's own text, nothing more.
Replacing a whole site from a ZIPcreate_upload + upload_idNone — the archive never enters the conversation.
The ZIP is already hosted somewherearchive_urlNone.
Client has no shell and no hostingarchive_base64Roughly 1.4 tokens per byte of ZIP.

create_upload

Reserves a one-time slot and hands back a URL to PUT the archive to, plus a ready-to-run curl command. The file goes straight from your disk to the server, so its size costs nothing in context.

TOOL create_upload

Arguments

NameTypeRequiredDescription
file_namestringoptionalName for the archive. Defaults to archive.zip.

Example result

{
    "upload_id": "0e9837a760c5062279ea57107d8f0a1c",
    "upload_url": "https://mcp.static.app/upload/0e9837a760c5062279ea57107d8f0a1c",
    "file_name": "site.zip",
    "expires_at": "2026-08-25T12:57:32Z",
    "max_size": 204800000,
    "single_use": true
}

Then PUT the archive

curl -X PUT --data-binary @site.zip \
    -H "Authorization: Bearer sk_xxxx" \
    https://mcp.static.app/upload/0e9837a760c5062279ea57107d8f0a1c

Then deploy it

{
    "pid": "xxxxxxxxxx",
    "upload_id": "0e9837a760c5062279ea57107d8f0a1c"
}
Single use. The staged file is deleted the moment it is deployed, and expires after 30 minutes regardless. Only the API key that reserved a slot can write to it or deploy it.

Upload endpoint

The tool is a convenience — any HTTP client can drive the endpoint directly. POST stages a file and returns a fresh id in one step; PUT fills an id that create_upload handed out.

POST https://mcp.static.app/upload
PUT https://mcp.static.app/upload/{upload_id}

Example request

curl -X POST --data-binary @site.zip \
    -H "Authorization: Bearer sk_xxxx" \
    "https://mcp.static.app/upload?file_name=site.zip"

Example response

{
    "status": "success",
    "upload_id": "0e9837a760c5062279ea57107d8f0a1c",
    "file_name": "site.zip",
    "size": 5000000,
    "sha256": "1df9cfae4840f7c4eef12dbd4691c043ce4e247130817053c55f1aa04eddd971",
    "expires_at": "2026-08-25T12:57:32Z"
}

Errors

CodeWhen
401No API key on the request.
403The id belongs to a different API key.
404Unknown or expired id.
409Content was already uploaded for this id.
413Body is over the 200MB ceiling.
503Staging is disabled on the server — use archive_url instead.

Legacy base64

archive_base64 still works and is checked last. It exists for clients that cannot make an HTTP request of their own — but base64 adds a third on top of the raw bytes, and every one of them passes through the model, so a 2MB archive costs roughly 700k tokens. Prefer upload_id whenever a shell is available.

# macOS
base64 -i site.zip | tr -d '\n'

# Linux
base64 -w 0 site.zip

Form tools

Read the forms on a site and work through their submissions.

list_forms

Returns every form on a site.

TOOL list_forms

Arguments

NameTypeRequiredDescription
pidstringrequiredPublic identifier of the site.

get_form

Returns a single form with its fields.

TOOL get_form

Arguments

NameTypeRequiredDescription
pidstringrequiredPublic identifier of the site.
form_idstringrequiredId of the form.

get_form_entries

Returns submissions for a form, newest first.

TOOL get_form_entries

Arguments

NameTypeRequiredDescription
pidstringrequiredPublic identifier of the site.
form_idstringrequiredId of the form.
limitnumberoptionalHow many entries to return.
offsetnumberoptionalOffset for pagination.

Ask your assistant

"Summarise the last 20 contact form submissions on site xxxxxxxxxx."

delete_form_entry

Deletes one submission.

TOOL delete_form_entry

Arguments

NameTypeRequiredDescription
pidstringrequiredPublic identifier of the site.
form_idstringrequiredId of the form.
entry_idstringrequiredId of the entry to delete.

Troubleshooting

The failures that actually come up, and what each one means.

401 Unauthorized

  • Check the key is current and has not been revoked in Account → API.
  • The header needs the Bearer prefix: Authorization: Bearer sk_xxxx.
  • In a config file, confirm the environment variable actually resolved — a literal ${STATIC_API_KEY} reaching the server reads as a bad key.
  • The MCP server is on paid plans. On a free plan the key authenticates but the account has no access.

"Site not found"

  • Pass the PID — the 10-character public id like 0aw4jtby1z — not the numeric database id.
  • The site has to belong to the account behind the key. Run list_sites to see which ones do.
  • Switching workspaces changes which sites are visible.

Passing a path from your machine

The hosted server shares no filesystem with you, so a path like /Users/me/site.zip means nothing to it. An older client that still sends archive_path gets an error explaining what to use instead; one that calls upload_site_files gets tool not found, since that tool is not registered here at all.

  • Changing files → write_site_files, with the contents inline.
  • Deploying an archive → create_upload, PUT the ZIP, then upload_id.
  • Archive already hosted → archive_url.
  • Running the server yourself → LOCAL_FILE_ACCESS=true brings the local-path tools back.

Tools not showing up

  • Restart the client after editing its MCP config — most read it only at startup.
  • Claude Desktop logs the handshake: ~/Library/Logs/Claude/mcp*.log on macOS.
  • Cannot find module 'mcp-remote' — use npx -y mcp-remote, which installs it on the spot.
  • A healthy connection lists 16 tools. Fewer means the client cached an older session.

Connection errors

  • The URL is exactly https://mcp.static.app/sse.
  • SSE holds one long-lived connection. Proxies that buffer responses or cut idle connections break it.
  • Test the endpoint on its own: curl -N https://mcp.static.app/sse -H "Authorization: Bearer sk_xxxx". It should print an event: endpoint line straight away.

Upload rejected

CodeMeaningFix
403The id belongs to another key.Use the same key that called create_upload.
404Unknown or expired id.Slots live 30 minutes and are deleted once deployed. Reserve a new one.
409Content already uploaded for this id.A slot takes one file. Reserve another to re-upload.
413Over the 200MB ceiling.Split the deployment, or drop large assets and add them with write_site_files.
503Staging is switched off on the server.Deploy with archive_url, or ask us to point UPLOAD_DIR at a writable path.
422Nothing could be written.Check the errors array — usually an unsupported extension or a path leaving the site root.

Still stuck

The same operations are available over HTTP — reproducing a failure against the REST API tells you whether the problem is the client or the account. If it is neither, contact support.