# clicked.cloud MCP server

For agents. One remote MCP server lets your agent list sites, add SSH keys, deploy releases, roll back, search domains, edit DNS and start a checkout. It only ever reaches the sites of the account whose token it holds.

- URL: `https://clicked.cloud/mcp`
- Transport: Streamable HTTP, stateless, POST only (GET answers 405).
- Auth: **OAuth 2.1**, so a person connects by giving their app the URL and signing in with the link we email them. Nothing to copy.
  - An unauthenticated request gets `401` with `WWW-Authenticate: Bearer resource_metadata="https://clicked.cloud/.well-known/oauth-protected-resource/mcp", scope="mcp"`.
  - Authorisation server metadata: `https://clicked.cloud/.well-known/oauth-authorization-server`. Dynamic client registration at `/oauth/register` (public clients, `token_endpoint_auth_method: "none"`), `/oauth/authorize`, `/oauth/token` (form encoded), `/oauth/revoke`.
  - PKCE `S256` is required (`plain` is refused). Redirect URIs match exactly; loopback `http://127.0.0.1` / `http://localhost` match on any port (RFC 8252). `resource` must be `https://clicked.cloud/mcp`.
  - Access tokens (`cka_…`) last 1 hour; refresh tokens (`ckr_…`) rotate on every use and last 30 days from last use. Reusing an old refresh token disconnects the app.
  - The person sees each connected app in **Account → Security → Connected apps** and can disconnect it there.
- Or a token: `Authorization: Bearer ck_…`, made in the dashboard's **Account → Security** tab. It is shown once and expires after 30, 90 or 365 days (default 90), or never. An expired token gets `401 token_expired`.
- Browsers: a request with an `Origin` other than `https://clicked.cloud` is refused (403). Agent clients send no Origin and are unaffected.
- No token, or a revoked or expired one: HTTP 401 with `{"error":"unauthorized","docs":"…/docs/mcp.md"}` and the `WWW-Authenticate` header above.

## Add it

People: https://clicked.cloud/get-started/ has the same steps with pictures.

Claude (claude.ai, desktop and mobile apps): **Customize → Connectors → Add custom connector**, URL `https://clicked.cloud/mcp`, then **Connect**.

Claude Code, with our plugin (this server plus the deploy and domain skills), served from clicked.cloud:

```
/plugin marketplace add https://clicked.cloud/plugin/marketplace.json
/plugin install clicked@clicked
```

Or the server on its own:

```sh
claude mcp add --transport http --scope user clicked https://clicked.cloud/mcp
```

Then `/mcp` → clicked → sign in.

Cursor (`.cursor/mcp.json`), then sign in from Cursor Settings → MCP:

```json
{ "mcpServers": { "clicked": { "url": "https://clicked.cloud/mcp" } } }
```

Codex:

```sh
codex mcp add clicked --url https://clicked.cloud/mcp
codex mcp login clicked
```

A client that can't do OAuth can send a ck_ token instead: `--header "Authorization: Bearer ck_…"` (Claude Code) or `"headers": { "Authorization": "Bearer ck_…" }`.

## Tools

Read-only tools change nothing. Destructive tools change what is live or who can get in, so confirm with the human first.

| Tool | Does | Read-only | Destructive |
|---|---|---|---|
| `list_sites` | Your sites, status, live release, live URL | yes | no |
| `site_details` | SSH host/user/port, releases path, and the exact ssh, mkdir and rsync commands | yes | no |
| `add_ssh_key` | Adds a public key to a site (live with our hosting partner in about 10 minutes) | no | no |
| `list_ssh_keys` | Keys on a site and their status | yes | no |
| `remove_ssh_key` | Removes a key from a site | no | yes |
| `search_domains` | Available names and their yearly price | yes | no |
| `list_domains` | Your domains, expiry, attached site | yes | no |
| `get_dns` | DNS records for one of your domains | yes | no |
| `set_dns_record` | Adds an A, AAAA, CNAME or TXT record (MX only in the dashboard) | no | yes |
| `delete_dns_record` | Deletes one record | no | yes |
| `list_releases` | Releases for a site, which one is live | yes | no |
| `activate_release` | Registers an uploaded release and puts it live | no | yes |
| `rollback` | Puts the previous release back live | no | yes |
| `publish_site` | Sends a site's files from the chat and puts them live (no terminal needed) | no | yes |
| `deploy_status` | How a `publish_site` upload is going: queued, uploading, live or failed | yes | no |
| `start_checkout` | Returns a Stripe URL for the HUMAN to open and pay | no | no |
| `get_subscriptions` | What you pay for and when it renews | yes | no |

Nothing here cancels a plan, deletes a site or moves a domain away. Cancelling is done by the human in the dashboard. Deleting a site and moving a domain away aren't offered yet; the human emails support@help.clicked.cloud.

The REST API holds the same line for tokens. A `ck_` or OAuth token gets `403 forbidden` on cancelling a plan, resetting a mailbox password, changing the profile, removing a card, the card portal, opening or answering a support ticket, adding or removing an MX record (it decides where the domain's email goes), and managing tokens or connected apps. Those need the person signed in at clicked.cloud in a browser. Deploying, rollback, SSH keys, DNS, domain search, checkout and reading status all work with a token.

## Publish with no terminal (Claude on the web)

Claude on claude.ai can't run ssh or rsync, so it sends the files through the chat instead.

1. `list_sites`, then pick the site. It must be `active`.
2. `publish_site {siteId, files: [...]}` with a few files per call (at most 100 files or 4 MB). Text files go as `content`; images, fonts and PDFs go as `contentBase64`. Paths are relative: `index.html`, `about/index.html`, `img/logo.png`. The first call returns a `releaseId`.
3. Send the rest in more `publish_site` calls with that same `releaseId`.
4. Ask the human, then send `last: true` (files can be empty). That replaces the live site.
5. `deploy_status {siteId}` every few seconds until `status` is `live`, then give the human the `liveUrl`. If it says `failed`, the live site was not changed; tell the human the `error`.
6. Something wrong? `rollback {siteId}`.

For people, the sentence to say to Claude is: "Publish this website to my clicked.cloud site, then tell me when it's live."

Limits: 500 files, 25 MB in total, 4 MB a file and 20 new publishes a day. Static files only: HTML, CSS, JS, images, fonts, PDF and video. No PHP or other server-side code (not even inside a name like `x.php.html`), and no hidden files except `.well-known/`.

Each web publish resets the site's FTP password to a random one and locks FTP again. Use SSH (above) if you need your own access.

## Deploy with MCP and a terminal

1. `site_details {siteId}`. If `deploy` is `null`, the site is still being set up; wait.
2. No working key? Read the user's `~/.ssh/id_ed25519.pub` (never the private key) and call `add_ssh_key`. Wait about 10 minutes.
3. Pick a release id: `date -u +%Y%m%dT%H%M%SZ` (for example `20261108T091500Z`).
4. Run the `mkdir` then the `rsync` command from `site_details`, with `<RELEASE_ID>` and `<BUILD_DIR>` filled in. Do not upload `.env` files or private keys.
5. **Only call `activate_release` if BOTH the mkdir and the rsync exited 0.** If either failed, stop and tell the human why. `Permission denied (publickey)` is normal for about 10 minutes after adding a key; retry later. Activating a release you did not just upload takes the live site down.
6. `activate_release {siteId, releaseId}`. Report the `liveUrl` from `list_sites`.
7. Something wrong? `rollback {siteId}`.

## Paying

`start_checkout` returns a URL. Give it to the human. Never open, fill in or pay a checkout yourself. One billing interval per cart: a domain renews yearly, so a cart with a domain is annual, or buy the domain in its own checkout (a monthly plan plus a domain answers `mixed_interval`).

## Errors

A failed tool call comes back with `isError: true` and the API's body, e.g. `{"status":404,"error":"not_found"}`. A site or domain that is not yours is always `404`, never `403`. Other codes: `409 conflict` (for example nothing to roll back), `409 no_package` (domain not attached to a site yet), `422 site_limit`, `422 mailbox_limit`, `429 rate_limited`.
