# Use Apexly from AI agents (MCP)

Connect Claude Code, Cursor or any MCP client to Apexly's hosted MCP server and let your coding agent add, check, diagnose and remove custom domains with your API key.

Apexly runs a hosted [Model Context Protocol](https://modelcontextprotocol.io) server, so an AI coding agent can manage your customers' domains while it builds the feature with you: add a hostname, hand the customer the DNS record or the connect link, check why a domain is stuck, and remove it.

The server is built into the Apexly API, but the `mcp.apexly.dev` hostname goes live with the next production deploy: until then the snippets below will not connect. You can run it locally today at `http://localhost:4000/mcp` (see [Run it locally](#run-it-locally)).

## How it works

- **Endpoint:** `https://mcp.apexly.dev/mcp`, Streamable HTTP transport (stateless: no session to keep alive).
- **Auth:** your normal Apexly API key, sent as `Authorization: Bearer sk_live_…` or `sk_test_…`. Create one under **Keys** in the dashboard. There is no separate MCP login.
- **Scope:** exactly the same as the REST API. The agent sees only the domains of the key's app; another app's domain is `not_found`. A test key sees and creates test domains only, so start with `sk_test_…` while you try it out.
- **Limits:** every tool call counts against your app's [API rate limits](/docs/limits-and-pricing#rate-limits) (60 writes and 600 reads per minute), plus 600 MCP requests per minute per app.
- **Errors** come back as the same envelope as the API, `{ "error": { "code", "message", "fix", "docs" } }`, so the agent can read the fix and act on it.

## Claude Code

```bash
claude mcp add --transport http apexly https://mcp.apexly.dev/mcp \
  --header "Authorization: Bearer sk_test_…"
```

Then ask, for example: *"Add careers.acme.com as a custom domain and tell me what DNS record Acme needs."* Run `claude mcp list` to check the connection.

Keep the key out of the repository: `claude mcp add` stores it in your local Claude Code config by default. Use `--scope project` only with an environment variable, never a pasted key, in a shared `.mcp.json`:

```json
{
  "mcpServers": {
    "apexly": {
      "type": "http",
      "url": "https://mcp.apexly.dev/mcp",
      "headers": { "Authorization": "Bearer ${APEXLY_API_KEY}" }
    }
  }
}
```

## Cursor

Add the server to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for every project):

```json
{
  "mcpServers": {
    "apexly": {
      "url": "https://mcp.apexly.dev/mcp",
      "headers": { "Authorization": "Bearer ${env:APEXLY_API_KEY}" }
    }
  }
}
```

Set `APEXLY_API_KEY` in the environment Cursor starts from, and add `.cursor/mcp.json` to `.gitignore` if you paste a key into it instead.

## Other MCP clients

Any client that supports remote servers over Streamable HTTP with custom headers works:

- URL: `https://mcp.apexly.dev/mcp`
- Transport: Streamable HTTP (`POST` only; `GET` and `DELETE` answer 405 because the server keeps no sessions)
- Header: `Authorization: Bearer <your API key>`

A client that only speaks stdio can bridge with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote):

```bash
npx mcp-remote https://mcp.apexly.dev/mcp --header "Authorization: Bearer ${APEXLY_API_KEY}"
```

## Tools

| Tool | What it does | What happens next |
| --- | --- | --- |
| `create_domain` | Adds the customer's hostname (idempotent: an existing one comes back as is). Optional `metadata`, `apex_redirect`, `origin`. | The domain is `pending_dns`; the customer adds the record or opens the connect link. |
| `get_domain` | The domain object plus `status_explained`: one sentence on why it is in its status and the fix. | Poll it until `status` is `active`. |
| `list_domains` | Newest first, filter by `status` or hostname prefix; pass `next_cursor` back as `cursor`. | |
| `get_dns_instructions` | The exact record(s) and steps for the customer's DNS provider, detected from its nameservers. | Relay them to the customer. |
| `get_connect_url` | The [connect page](/docs/connect-page) link for the customer. | Send it to the domain owner. |
| `verify_domain` | Queues a DNS check now (one per 15 s per domain); on `cert_failed`, retries the certificate. | Call `get_domain` a few seconds later. |
| `diagnose_domain` | The [domain doctor](/docs/troubleshooting/domain-doctor), same as `GET /v1/domains/{id}/diagnose`: every check live (DNS at each resolver, other A and stale AAAA records, the Cloudflare proxy, CAA, the certificate, the origin, the `www` pair), each with what we saw and the fix, `blocking_check_id` (the one to fix first), plus `status_explained` and the expected record. 30 per minute per app. | Tell the customer what to change. |
| `delete_domain` | Removes the domain; it stops being served within 5 seconds. Needs `confirm: true`. | Agents are told to confirm with you first. |

The tools return the same fields as the [Domains API](/docs/api/domains); they never return API keys or signing secrets.

## Run it locally

With the API running (`pnpm dev`), point your client at `http://localhost:4000/mcp` with a key from your local dashboard:

```bash
claude mcp add --transport http apexly-local http://localhost:4000/mcp \
  --header "Authorization: Bearer sk_test_…"
```
