# MCP (AI assistants)

Connect Claude, ChatGPT, Cursor, VS Code, Windsurf, Gemini CLI or any other MCP client to your PBN.LTD account: ask
in plain words ("which of my sites are offline?", "add a TXT record to example.com", "why is my site paused?") and
the assistant uses the same operations as this API, with the same permissions and limits.

**Server URL:** `https://app.pbn.ltd/mcp` (live)

Authentication: the assistant opens a PBN.LTD login and asks you to approve access (OAuth). Clients that do not
support OAuth can send an API key instead: `Authorization: Bearer pbn_...` (use a scoped key). Everything the
assistant does is in the call log of that key/app, and you can disconnect it at any time on [API keys](/api/keys/).

## Claude (claude.ai and Claude Desktop)

1. Settings > **Connectors** (or Customize > Connectors) > **+** > **Add custom connector**.
2. Name: `PBN.LTD`; URL: `https://app.pbn.ltd/mcp`. Click **Add**, then **Connect** and approve on PBN.LTD.

Vendor guide: [Get started with custom connectors using remote MCP](https://support.claude.com/en/articles/11175166-getting-started-with-custom-connectors-using-remote-mcp)
and [Connect to remote MCP servers](https://modelcontextprotocol.io/docs/develop/connect-remote-servers).

## Claude Code

```bash
claude mcp add --transport http pbn https://app.pbn.ltd/mcp
# then run /mcp in Claude Code, pick "pbn" and log in; or use a key:
claude mcp add --transport http pbn https://app.pbn.ltd/mcp --header "Authorization: Bearer $PBN_API_KEY"
```

Vendor guide: [Connect Claude Code to tools via MCP](https://code.claude.com/docs/en/mcp).

## ChatGPT

1. Settings > Security and login: switch on **Developer mode**.
2. Open Plugins (Apps) > **+**, give it the name PBN.LTD, choose the public endpoint option and enter `https://app.pbn.ltd/mcp`.
3. Approve access on PBN.LTD when asked.

Vendor guides: [MCP in ChatGPT](https://developers.openai.com/api/docs/mcp/) and
[Connect a server to ChatGPT](https://developers.openai.com/plugins/deploy/connect-chatgpt).

## Cursor

Add to `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project):

```json
{"mcpServers": {"pbn": {"url": "https://app.pbn.ltd/mcp"}}}
```

To use a key instead of OAuth: `{"mcpServers": {"pbn": {"url": "https://app.pbn.ltd/mcp", "headers": {"Authorization":
"Bearer pbn_..."}}}}`. Vendor guide: [Model Context Protocol (MCP) in Cursor](https://cursor.com/docs/context/mcp).

## VS Code (GitHub Copilot)

Command Palette > **MCP: Add Server** > HTTP, URL `https://app.pbn.ltd/mcp` - or in `.vscode/mcp.json`:

```json
{"servers": {"pbn": {"type": "http", "url": "https://app.pbn.ltd/mcp"}}}
```

Vendor guide: [Add and manage MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers).

## Windsurf

In Cascade's MCP settings (`mcp_config.json`):

```json
{"mcpServers": {"pbn": {"serverUrl": "https://app.pbn.ltd/mcp"}}}
```

Vendor guide: [Cascade MCP](https://docs.windsurf.com/windsurf/cascade/mcp).

## Gemini CLI

```bash
gemini mcp add --transport http pbn https://app.pbn.ltd/mcp
# or with a key:
gemini mcp add --transport http --header "Authorization: Bearer $PBN_API_KEY" pbn https://app.pbn.ltd/mcp
```

Vendor guide: [MCP servers with Gemini CLI](https://geminicli.com/docs/tools/mcp-server/).

## Other MCP clients

Any client that supports remote MCP servers over Streamable HTTP works: give it `https://app.pbn.ltd/mcp` and either let it do the
OAuth login or send an `Authorization: Bearer pbn_...` header. About MCP: [modelcontextprotocol.io](https://modelcontextprotocol.io/).

## What the assistant can do

The tools mirror the API reference one to one (tool `sites_list` = `GET /sites`, `dns_create` = `POST
/sites/{site_id}/dns`, ...). Actions that delete or overwrite (delete a site, restore, reinstall, delete files) are
marked destructive, so the assistant asks you before running them. A scoped key or a limited OAuth approval limits
the assistant exactly like any other key.


## What the server is

| | |
|---|---|
| Server URL | `https://app.pbn.ltd/mcp` (POST only) |
| Transport | Streamable HTTP |
| Protocol revisions | `2026-07-28`, `2025-11-25`, `2025-06-18`, `2025-03-26` |
| Tools | 85 |
| Resources | 4 fixed, 5 templates, one per docs page |
| Prompts | 12 |
| Authentication | OAuth 2.1 (PKCE, dynamic client registration) or an API key as `Authorization: Bearer pbn_...` |
| Server name / version | `pbn.ltd` 1.0.0 |

The endpoint serves both shapes of the protocol on the same URL. Revision `2026-07-28` made MCP stateless
(no `initialize` handshake, no sessions, per-request metadata, `server/discover`); revisions `2025-11-25`,
`2025-06-18` and `2025-03-26` use the `initialize` handshake. You do not have to care which one your client
speaks - it is detected from the request.

Because the server holds no session, `GET` and `DELETE` on `https://app.pbn.ltd/mcp` answer `405`, no `Mcp-Session-Id` is
issued, and streams are not resumable. Nothing is lost by that: every answer is complete in its own response.

## Authentication

Two ways in. Both end up as the same thing: a key with scopes, an owner, a call log and a rate limit.

**1. OAuth 2.1 (what a connector does for you).** Point the client at `https://app.pbn.ltd/mcp` and it will find the login by
itself: the first call answers `401` with

```
WWW-Authenticate: Bearer realm="PBN.LTD", resource_metadata="https://app.pbn.ltd/.well-known/oauth-protected-resource"
```

From there the client reads its [protected-resource metadata](https://app.pbn.ltd/.well-known/oauth-protected-resource)
(RFC 9728), then the [authorization-server metadata](https://app.pbn.ltd/.well-known/oauth-authorization-server)
(RFC 8414), registers itself (RFC 7591 dynamic client registration), and opens
`https://app.pbn.ltd/oauth/authorize` in your browser. You log in as yourself, see exactly which permissions the app is
asking for, and approve or refuse. PKCE with `S256` is required, the `resource` parameter (RFC 8707) is honoured,
authorization codes last 60 seconds, access tokens 1 hour and refresh tokens 90 days with rotation.

**2. An API key.** Create one at [API keys](https://app.pbn.ltd/api/keys/), give it only the permissions the assistant needs,
and put it in the client's configuration as the `Authorization: Bearer pbn_...` header. Use this for clients that
have no OAuth support, and for anything running unattended.

Either way you can end the connection at any moment: revoke the key (or the app) on the
[API keys](https://app.pbn.ltd/api/keys/) page and the next call fails. A key created through delegated access also dies the
moment that access is withdrawn.

## What you can ask for

The tools cover the whole job, not only reading:

- **"Build me a site."** Create a site of any type, upload a built site as a zip, set its home page, then fetch
  the pages and prove they really serve.
- **"Publish this post."** Write a post or a page and put it on the site - WordPress, Joomla, Drupal, PrestaShop,
  OpenCart, Grav, MediaWiki, and Static HTML / PHP hosting too (those get a page built in the look of the site's
  own pages, with a blog index and `sitemap.xml` kept up to date). Pictures upload with it.
- **"Upload these images."** Straight into the WordPress media library, or into the site's own files on the other
  types, and you get the address back to use in a page.
- **"Write me a plugin - and test it."** Write the files into the site, run PHP's own syntax check over them,
  switch the plugin on, fetch the page, read the PHP error log, and switch it off again if anything broke.
  Themes work the same way.
- **"Keep the blog going."** Look at the AI auto-posting campaigns, pause or resume one, or ask one to write now.
- **"Why is this site down?"** Health checks, the error log, the access log, usage, nameservers and DNS.
- **"What do I owe?"** Plan, invoices, payment due, add-ons.
- **"Publish it to Netlify as well."** The sites you deploy to third-party hosts, and a redeploy or a rollback.

Everything is done with the account's own permissions, and anything that replaces or deletes is marked
destructive so your assistant asks you first.

## Permissions (scopes)

An assistant can only do what the connection was granted. `tools/list` returns **only the tools the connection may
actually use**, so the assistant never offers something it cannot do. Ask for the narrowest set that does the job:

- `account:read` - Profile, plan, site slots, limits and usage, frozen state and why.
- `account:write` - Refresh usage measurements.
- `billing:read` - Subscription status, payment due, invoices and invoice PDFs, add-ons.
- `sites:read` - List and read sites: status, nameservers, CDN, indexation, SEO, platform, usage, health.
- `sites:write` - Create sites, change settings and PHP version, purge cache, reinstall, recheck health, run Site Cleaner.
- `sites:delete` - Delete sites (kept separate on purpose).
- `sites:login` - One-click admin login links (WordPress, Joomla, Drupal and the other applications).
- `dns:read` - Read DNS records and nameserver status.
- `dns:write` - Add, change and delete DNS records.
- `backups:read` - List backups and get download links.
- `backups:write` - Create backups, restore a backup, delete a backup.
- `files:read` - List folders and read files of a site.
- `files:write` - Upload, create folders, rename and delete files of a site.
- `logs:read` - Error log and access log of a site.
- `tickets:read` - Read your support tickets.
- `tickets:write` - Open tickets and reply to them.
- `content:read` - Read posts, pages and the auto-posting campaigns of the account.
- `content:write` - Publish and change posts and pages on any site type, upload pictures, and start or pause auto-posting campaigns.
- `kb:read` - Search the knowledge base (needs no scope; listed for completeness).

A `:write` scope includes the matching `:read`. `sites:delete` and `sites:login` are deliberately separate from
`sites:write`, so an assistant can be allowed to change a site without being able to delete it or to log in as its
administrator. If a tool needs a permission the connection does not have, an OAuth connection is asked to step up
(`403` with `WWW-Authenticate: ... error="insufficient_scope"`), and an API-key connection gets a tool error that
names the missing scope.

## Limits

MCP calls share the API's limits exactly - they are the same requests. The defaults are **300 requests a
minute**, **10000 an hour** and **90 writes a minute** per key or per connected app, with
`X-RateLimit-*` headers on the answer; see [Rate limits and sizes](https://app.pbn.ltd/api/docs/rate-limits). They were set
for exactly this kind of work: an assistant building a site or writing a plugin makes a burst of small writes.
A bulk upload counts as **one** request however many files are inside it, so `files_upload_archive` is the way to
put a built site up, not one `files_write` per file. A tool that hits the limit returns a tool error saying how
long to wait - a well-behaved assistant waits rather than retrying at once. File uploads are limited to
25 MB and file reads to 5 MB (the exact numbers for your key are in the `meta_limits` tool).

Every tool call is written to the call log of the key that made it, with the site it touched, and write calls keep
what was asked for as an audit trail. You can read it on your key's activity page.

## Tools

85 tools, one for every operation of the REST API plus `jobs_wait`. The complete reference - every
argument, what comes back, and a worked example for each - is on the
[MCP tool reference](https://app.pbn.ltd/api/docs/mcp-tools) page.

- **Account** - 4 tools
- **Billing** - 3 tools
- **Sites** - 7 tools
- **Site tools** - 14 tools
- **DNS records** - 5 tools
- **Backups** - 6 tools
- **Files** - 7 tools
- **Logs** - 2 tools
- **Support tickets** - 5 tools
- **Knowledge base** - 3 tools
- **Jobs** - 3 tools
- **Content and publishing** - 13 tools
- **Building, plugins and themes** - 13 tools

Tools are named after the API endpoints with dots replaced by underscores, so `sites_list` is `GET /sites` and
`dns_create` is `POST /sites/{site_id}/dns` (paths relative to `https://app.pbn.ltd/api/v1`). Each one carries a
JSON-Schema for its input **and** for its output, so a client can check the answer, and behaviour annotations
(`readOnlyHint`, `destructiveHint`, `idempotentHint`) so your assistant knows what needs your say-so first.

Answers carry `structuredContent` (the JSON the API would return) as well as a text block, so an assistant can
work with the data rather than re-reading prose.

### Things that finish later

Creating a site, taking or restoring a backup, deleting a site, reinstalling and publishing DNS all start a job.
The tool answers immediately with a `job` block; `jobs_wait` then waits for it (up to 5 minutes at a time) and
`jobs_get` / `jobs_list` show it on demand. If the client sends a `progressToken`, those calls stream
`notifications/progress` while they wait, so nothing looks frozen.

## Resources

Read-only context an assistant can pull in without spending a tool call:

| URI | What it holds |
|---|---|
| `pbn://account` | Who this connection belongs to and what the connected key may do. |
| `pbn://account/limits` | Site slots, subscription state, the default per-site limits, paused sites and the add-ons on the account. |
| `pbn://billing` | Subscription, payment due, next renewal and the add-ons that are being paid for. |
| `pbn://sites` | Every site on the account with its status, type, platform and nameserver state. |
| `pbn://site/{site_id}` | One site in full: status, type, CDN, HTTPS, PHP version, nameservers, SEO. |
| `pbn://site/{site_id}/health` | Is the site up, what the last checks found and what is wrong. |
| `pbn://site/{site_id}/dns` | The DNS records published for this site. |
| `pbn://invoice/{number}` | One invoice as a PDF. |
| `pbn://docs/{slug}` | A page of the PBN.LTD API / MCP documentation as Markdown. Slugs: see pbn://docs/index. |

`pbn://docs/index` lists a `pbn://docs/<page>` resource for every page of this documentation, so an assistant can
read the API reference itself before answering a question about it.

## Prompts

Ready-made jobs. In Claude they appear under the **+** menu (or as `/` commands) once the connector is added;
other clients show them in a command palette.

| Prompt | What it does |
|---|---|
| **Diagnose a site** (`diagnose_site`) | Work out why a site is down, slow, showing an error or paused, and say what to do about it. |
| **Check a site is ready to go live** (`going_live_check`) | Check the nameservers, DNS records, CDN and HTTPS of a site that is waiting to go live, and give the customer the exact steps left. |
| **Create a new site** (`create_site`) | Create a site of any type (WordPress, Joomla, Drupal, PrestaShop, OpenCart, Grav, MediaWiki, PHP hosting or static HTML) and follow it through to ready. |
| **Clean up a site** (`clean_up_site`) | Use Site Cleaner to find what is filling a site up, and clean it after showing the customer what would go. |
| **Review my account** (`account_review`) | A plain-language review of the whole account: plan, what is owed, slots, usage, paused sites, sites that are not live and anything that needs attention. |
| **Answer a billing question** (`billing_questions`) | Answer a question about the plan, an invoice, a payment or a renewal from the real billing record, with the invoice links. |
| **Read the error log and fix it** (`fix_from_error_log`) | Read a site's error log, explain the errors in plain words and propose the safest fix (never changing a file without asking first). |
| **Back up or restore a site** (`backup_and_restore`) | Take a backup now, or restore one - with the confirmation and the warnings a restore needs. |
| **Build me a site** (`build_site`) | Create a site of any type, put the pages on it, set the home page and check that it really serves - end to end. |
| **Write and publish a post** (`publish_post`) | Write a post or page and publish it to a site of ANY type, with its pictures, and check the published page. |
| **Make me a plugin** (`make_plugin`) | Write a WordPress plugin (or theme) into the site, check it, switch it on and prove the site still works - and take it back off if it does not. |
| **Find help / open a ticket** (`get_help`) | Search the PBN.LTD knowledge base for the customer's question and, only if it is not answered there, open a support ticket with the right detail in it. |

## Try it without a client

Everything below works with a plain API key. Replace `$PBN_API_KEY` with a key from
[API keys](https://app.pbn.ltd/api/keys/).

List the tools:

```bash
curl -s https://app.pbn.ltd/mcp \
  -H "Authorization: Bearer $PBN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```

Call one (this one is free of side effects - it is the cheapest way to prove a key works):

```bash
curl -s https://app.pbn.ltd/mcp \
  -H "Authorization: Bearer $PBN_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"account_me","arguments":{}}}'
```

Search your sites:

```bash
curl -s https://app.pbn.ltd/mcp \
  -H "Authorization: Bearer $PBN_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"sites_list","arguments":{"search":"example.com","limit":5}}}'
```

Read a resource:

```bash
curl -s https://app.pbn.ltd/mcp \
  -H "Authorization: Bearer $PBN_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"pbn://account"}}'
```

The handshake a client does first, if you want to see it (revision `2025-11-25`):

```bash
curl -s https://app.pbn.ltd/mcp \
  -H "Authorization: Bearer $PBN_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-11-25",
       "capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
```

…and the same thing on revision `2026-07-28`, which needs no handshake at all:

```bash
curl -s https://app.pbn.ltd/mcp \
  -H "Authorization: Bearer $PBN_API_KEY" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2026-07-28" -H "Mcp-Method: server/discover" \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{
       "io.modelcontextprotocol/protocolVersion":"2026-07-28",
       "io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1"},
       "io.modelcontextprotocol/clientCapabilities":{}}}}'
```

On `2026-07-28` the `MCP-Protocol-Version`, `Mcp-Method` and (for `tools/call`, `resources/read` and
`prompts/get`) `Mcp-Name` headers must agree with the body, or the answer is `400` with JSON-RPC error `-32020`.
An unsupported version is `400` with `-32022` and the list of versions we do support; an unknown method is `404`
with `-32601`.

## More clients

Anything that speaks remote MCP over Streamable HTTP works. A few more, with their own documentation:

- **Zed** - Settings > AI > MCP Servers > Add Server > Add Remote Server (`context_servers` in `settings.json`,
  a `url` and optional `headers`): [MCP in Zed](https://zed.dev/docs/ai/mcp).
- **MCP Inspector** (the reference client, for checking a connection):
  [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector).
- Anything else: point it at `https://app.pbn.ltd/mcp` and let it do the OAuth login, or give it the
  `Authorization: Bearer pbn_...` header. The protocol itself: [modelcontextprotocol.io](https://modelcontextprotocol.io/).

## Checking the connection yourself

The reference client from the MCP project lists the tools and calls one without any assistant in the way:

```bash
npx @modelcontextprotocol/inspector --cli https://app.pbn.ltd/mcp --transport http   --header "Authorization: Bearer $PBN_API_KEY" --method tools/list

npx @modelcontextprotocol/inspector --cli https://app.pbn.ltd/mcp --transport http   --header "Authorization: Bearer $PBN_API_KEY"   --method tools/call --tool-name account_me --format json
```

Leave out `--cli` for the graphical version in a browser, which also walks through the OAuth login.

## When something is wrong

| What you see | What it means |
|---|---|
| `401` with `WWW-Authenticate` | No key, a revoked or expired key, or a token issued for something else. Reconnect, or check the key on the API keys page. |
| `403` `insufficient_scope` | The connection was not granted that permission. An OAuth client will ask you to approve it; a key has to be edited. |
| A tool answer with `isError` | The call reached us and was refused or failed - the text says why and what to do (wrong site id, site busy, plan expired, over a limit). |
| `429` / a rate-limit tool error | Too many calls for this key in the last minute or hour. Wait the seconds it names. |
| `503` `mcp_disabled` | The MCP server is switched off for maintenance. The REST API is unaffected. |

## What it cannot do

- It never sees your PBN.LTD password, and it cannot change your password, your e-mail address or your payment
  method.
- It cannot reach another customer's account, and a key restricted to certain sites cannot see the others.
- It cannot spend money: nothing here buys a plan, an add-on or a domain.
- It can delete sites, files and backups if you grant those permissions - grant them only if you mean to, and
  keep a backup.
