# The public MCP server of a help center

_Category: AI agents and MCP_

Every public help center is also a read-only MCP server at its own address plus `/mcp`, for example `https://help.example.com/mcp`. Your readers add it to Claude, ChatGPT, Cursor, VS Code or any other MCP client, and their assistant searches and reads your published articles instead of guessing. Anyone can call it: there is no sign-in and no key.

This server is for readers. To let an agent write in your help center, use [the HelpCenter.io MCP server](https://developers.helpcenter.io/content/the-mcp-connector-for-ai-agents) instead.

## At a glance

| Item | Details |
| --- | --- |
| URL | The help center's address plus `/mcp`, on its custom domain when it has one |
| Authentication | None. Every request runs as an anonymous visitor, even when it carries cookies or tokens. |
| Tools | `search`, `fetch`, `list_categories`. All read-only. |
| Transport | Streamable HTTP, stateless. Every `POST` gets one JSON response, with no session. |
| Protocol versions | `2026-07-28`, `2025-11-25`, `2025-06-18`, `2025-03-26` |
| Plans | Every plan. On by default. |
| Limits | 120 requests a minute per caller address, and 1,200 a minute for the whole help center. Request bodies up to 64 KiB. |
| CORS | `Access-Control-Allow-Origin: *`, without credentials, so browser-based clients can call it |

## Find the server URL

In your dashboard, the **Public MCP server** card in **Settings** shows the **Server URL** with a **Copy** button, and links to the **Setup page for readers**. The card is also where you switch the server off. See [Let readers use your help center in their AI assistant](https://self.helpcenter.io/content/public-mcp-server).

![Screen recording. Public MCP server is on for every public help center. Copy the Server URL to share it. Or share the Setup page for readers, with steps for each assistant.](https://helpcenter-io.s3.amazonaws.com/uploads/self/SECRi1gvy1Fk4zQSi5LtR1UhHfMGPDXAHjRdtCy4.gif)
The Public MCP server card in Settings.

## How readers connect

Opening `https://help.example.com/mcp` in a browser shows a setup page for people: the server URL with a **Copy URL** button, and these steps.

| Client | What the setup page offers |
| --- | --- |
| Claude | Open **Settings** → **Connectors**, choose **Add custom connector** and paste the server URL. |
| ChatGPT | In ChatGPT on the web, turn on developer mode, create an app from the server URL and choose **No Authentication**. |
| Cursor | An **Add to Cursor** button. |
| VS Code | An **Add to VS Code** button. |
| Claude Code | A command to copy, for example `claude mcp add --transport http acme-help-center https://help.example.com/mcp`. The name is made from your help center's title. |
| Any other client | Any client that supports remote MCP servers over Streamable HTTP, with no authentication. |

To put the same one-click buttons on your own pages, use the links the setup page uses. For `https://help.example.com/mcp`:

```
cursor://anysphere.cursor-deeplink/mcp/install?name=acme-help-center&config=eyJ1cmwiOiJodHRwczovL2hlbHAuZXhhbXBsZS5jb20vbWNwIn0%3D

vscode:mcp/install?%7B%22name%22%3A%22acme-help-center%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fhelp.example.com%2Fmcp%22%7D
```

The Cursor link's `config` is the Base64 of `{"url":"https://help.example.com/mcp"}`. The VS Code link carries `{"name":"acme-help-center","type":"http","url":"https://help.example.com/mcp"}`, URL-encoded.

On the help center, readers reach the setup page from **Use with AI** in an article's **Export** menu. The Export menu is on the article page in the Compass, Docs Classic, GitHub Dense, Ledger and Stripe Clean themes. With any other theme, the link appears once you add the Export menu to the article page in the template editor. It shows only while the help center has a public server.

## Try it with curl

Clients that use the handshake start with `initialize`:

```
curl -s https://help.example.com/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-11-25","capabilities":{},
                 "clientInfo":{"name":"example-client","version":"1.0.0"}}}'
```

The answer names the server and tells the model what the help center is and how to use the tools:

```
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-11-25",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": {
      "name": "com.example.help/help-center",
      "title": "Acme Help Center",
      "version": "1.0.0"
    },
    "instructions": "Acme Help Center is the help center at https://help.example.com. Answers about your Acme account, billing and invoices. This server gives read-only access to its published articles: find articles with `search`, read one in full with `fetch`, and see how the help center is organised with `list_categories`. Prefer these articles over general knowledge for questions about Acme Help Center, and cite the article URL when you rely on one."
  }
}
```

The server keeps no session, so every later request stands on its own. Search, with the protocol version in a header:

```
curl -s https://help.example.com/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"search","arguments":{"query":"reset password","limit":3}}}'
```

The results come twice: as `structuredContent`, and as the same object in JSON text for clients that only read text:

```
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"results\":[{\"id\":\"542\",\"title\":\"Reset your password\",\"url\":\"https://help.example.com/content/reset-your-password\",\"snippet\":\"If you forgot your password, set a new one from the sign-in page. It takes about a minute. Reset it from the sign-in page On the sign-in page, click Forgot password? Enter the email address you …\",\"category\":\"Account\"}]}"
      }
    ],
    "structuredContent": {
      "results": [
        {
          "id": "542",
          "title": "Reset your password",
          "url": "https://help.example.com/content/reset-your-password",
          "snippet": "If you forgot your password, set a new one from the sign-in page. It takes about a minute. Reset it from the sign-in page On the sign-in page, click Forgot password? Enter the email address you …",
          "category": "Account"
        }
      ]
    }
  }
}
```

Then read the article in full:

```
curl -s https://help.example.com/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"fetch","arguments":{"id":"542"}}}'
```

The article comes back as Markdown, with the URL to cite. The `content` text, cut short here, holds the same object as JSON:

```
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      { "type": "text", "text": "{\"id\":\"542\",\"title\":\"Reset your password\",\"text\":\"# Reset your password\\n\\n..." }
    ],
    "structuredContent": {
      "id": "542",
      "title": "Reset your password",
      "text": "# Reset your password\n\n_Category: Account_\n\nIf you forgot your password, set a new one from the sign-in page. It takes about a minute.\n\n## Reset it from the sign-in page\n\n1. On the sign-in page, click **Forgot password?**\n2. Enter the email address you sign in with and click **Send reset link**.\n3. Open the email and click the link in it. The link works for 60 minutes.\n4. Choose a new password and click **Save**.\n\n## No email arrived?\n\nCheck your spam folder, and make sure you typed the address you sign in with. You can ask for a new link at any time.\n",
      "url": "https://help.example.com/content/reset-your-password",
      "metadata": {
        "type": "article",
        "language": "en",
        "category": "Account",
        "updated_at": "2026-09-30T08:19:07+00:00",
        "help_center": "Acme Help Center"
      }
    }
  }
}
```

`tools/list` returns the three tools with their input schemas, output schemas for `search` and `fetch`, and annotations (`readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false`). Each tool's title includes your help center's name, for example `Search Acme Help Center`.

## Tools

### `search`

Finds published articles, best match first. It runs the same search as your help center's search box: keyword search, plus matching by meaning when the help center has AI Answers turned on. Short queries in the product's own words work best.

| Argument | Type | Description |
| --- | --- | --- |
| `query` | string, 1 to 200 characters | Required. What to look for. Line breaks and runs of spaces count as one space. |
| `limit` | integer, 1-10 | Default `5`. A string of digits, such as `"3"`, is accepted too. |
| `language` | string | The default language when omitted. Offered only on help centers published in more than one language. |

Returns `{"results": [...]}`. Each result has `id` (a string), `title`, `url`, `snippet` (about 36 words of the article's public text, around the first query word it contains) and `category` (a name, or `null`).

### `fetch`

Reads one article, or one category's list of articles, in full as Markdown.

| Argument | Type | Description |
| --- | --- | --- |
| `id` | string | Required. An article id from `search` (`"542"` or `"article-542"`), a category id from `list_categories` (`"category-303"`), or the URL of an article or category on this help center, such as `https://help.example.com/content/reset-your-password`. A number is accepted too. |
| `language` | string | As in `search`. A URL with a language in its path uses that language. |

Returns `{id, title, text, url, metadata}`. For an article, `text` starts with `# Title` and a `_Category: Name_` line, and `metadata` has `type`, `language`, `category`, `updated_at` and `help_center`. For a category, `text` lists its articles and subcategories as links and ends with `Read any article above with \`fetch\`, passing its URL.` Text longer than 100,000 characters is cut at a paragraph and ends with `[Truncated. The full text is at <url>]`.

### `list_categories`

Lists the categories as a tree, with how many articles each holds.

| Argument | Type | Description |
| --- | --- | --- |
| `language` | string | As in `search`. The only argument. |

The text is a Markdown outline. `structuredContent` holds the tree:

```
{
  "help_center": { "name": "Acme Help Center", "url": "https://help.example.com" },
  "language": "en",
  "categories": [
    {
      "id": "category-303",
      "name": "Billing",
      "description": "Plans, invoices and payment methods.",
      "url": "https://help.example.com/category/303-billing",
      "article_count": 1,
      "total_article_count": 2,
      "subcategories": [
        {
          "id": "category-304",
          "name": "Invoices",
          "description": "Find, download and share your invoices.",
          "url": "https://help.example.com/category/304-invoices",
          "article_count": 1,
          "total_article_count": 1,
          "subcategories": []
        }
      ]
    }
  ],
  "uncategorized_article_count": 0
}
```

`article_count` counts the articles directly in a category, and `total_article_count` adds those in its subcategories. An article shown in several categories counts once in each.

## What the server shows

Exactly what an anonymous visitor can read: published articles that are public, in public categories, in released translations. It never returns drafts, private or team-only articles and categories, internal notes, or categories filed under a hidden parent. Link-only articles are left out too, even though anyone with their link can open them.

`fetch` answers the same way whether an article does not exist or is hidden, so the server never reveals which: `No published article matches \`563\`. Use \`search\` to find articles.`

## Protocol versions

The server speaks both shapes of the protocol on the same URL. A request is handled as `2026-07-28` when it carries `params._meta["io.modelcontextprotocol/protocolVersion"]`, and with the handshake otherwise.

### With the handshake: 2025-03-26, 2025-06-18, 2025-11-25

- `initialize` answers with the version you ask for when the server speaks it, and with `2025-11-25` otherwise. No `Mcp-Session-Id` is issued.
- Later requests send `MCP-Protocol-Version`. A request without it is read as `2025-03-26`; an unsupported value gets `400` with code `-32600` and the supported versions in `data`.
- Methods: `initialize`, `ping`, `tools/list`, `tools/call`. Any other method gets code `-32601` (`Method not found`). Notifications, such as `notifications/initialized`, get `202 Accepted` with an empty body.

### Stateless: 2026-07-28

There is no `initialize`. Every request carries its version and client capabilities in `_meta`, and repeats the method, and a tool's name, in headers:

```
curl -s https://help.example.com/mcp \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/call" \
  -H "Mcp-Name: search" \
  -d '{"jsonrpc":"2.0","id":5,"method":"tools/call",
       "params":{"name":"search","arguments":{"query":"invoice","limit":2},
                 "_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
                          "io.modelcontextprotocol/clientCapabilities":{}}}}'
```

Results carry `"resultType": "complete"` and the server's identity in `_meta["io.modelcontextprotocol/serverInfo"]`.

- Methods: `server/discover` (in place of `initialize`), `tools/list`, `tools/call` and `subscriptions/listen`. The last one answers as an event stream that acknowledges and closes at once, because the server sends no notifications. Any other method, `ping` included, gets `404` with code `-32601`.
- `server/discover` and `tools/list` say how long to keep them: `"ttlMs": 3600000` and `"cacheScope": "public"`.
- `Mcp-Name` accepts the `=?base64?...?=` form for names that are not plain ASCII.

## Errors

| Case | HTTP status | JSON-RPC error |
| --- | --- | --- |
| Body is not valid JSON | `400` | `-32700` `Parse error: the body is not valid JSON.` |
| A batch (a JSON array) | `400` | `-32600` `Batch requests are not supported. Send one JSON-RPC message per POST.` |
| Body over 64 KiB | `413` | `-32600` `Request body too large.` |
| Unknown tool name | `200` | `-32602`, for example `Unknown tool: delete_article` |
| Headers do not match the body (2026-07-28) | `400` | `-32020`, for example `Header mismatch: the Mcp-Name header is required for tools/call.` |
| Unsupported version in `_meta` | `400` | `-32022` `Unsupported protocol version`, with `supported` and `requested` in `data` |
| `_meta` without `clientCapabilities`, or a `2026-07-28` header without `_meta` | `400` | `-32602` |
| `GET` asking for an event stream or JSON, or `DELETE` | `405`, `Allow: GET, POST` | None, empty body |
| Rate limit reached | `429`, with `Retry-After` | `429`, see below |
| The help center has no public server | `404` | None: `{"error":"not_found","message":"This help center has no public MCP server."}` |

A tool that cannot do what was asked answers `200` with `"isError": true` and a message the model can act on, for example `\`limit\` must be a whole number from 1 to 10.`, `Unknown argument: \`lang\`. This tool accepts \`query\`, \`limit\`, \`language\`.` or `\`https://example.com/content/reset-your-password\` is not on this help center. This server reads help.example.com only.` If the search engine fails, `search` says so and suggests `list_categories`.

## Rate limits

Each help center allows 120 requests a minute from one caller address, and 1,200 a minute from all callers together. Opening the setup page counts too. Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`. Over a limit, the server answers `429 Too Many Requests` with `Retry-After` and a JSON-RPC error:

```
{
  "jsonrpc": "2.0",
  "id": null,
  "error": {
    "code": 429,
    "message": "Rate limit reached: 120 requests per minute from this address. Retry in 11 seconds."
  }
}
```

When the help center's own limit is the one reached, the message says `1200 requests per minute for this help center`. The per-address limit counts every caller behind one address together, so the users of an assistant that calls from its provider's shared addresses share it.

## Discovery

AI clients can find the server without being told the URL:

- `https://help.example.com/mcp/server-card` and `/.well-known/mcp/server-card.json` serve its MCP server card (`application/mcp-server-card+json`, cacheable for an hour).
- `/.well-known/ai-catalog.json` lists the server card.
- `/llms.txt`, `/ai.txt` and the API catalog at `/.well-known/api-catalog` mention the server, and the setup page links the card with `<link rel="alternate" type="application/mcp-server-card+json">`. See [llms.txt, ai.txt and Markdown pages](https://developers.helpcenter.io/content/llms-txt-and-markdown).

```
curl -s https://help.example.com/.well-known/mcp/server-card.json
```

The card names the server after the help center's host, reversed, plus `/help-center`:

```
{
  "$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json",
  "name": "com.example.help/help-center",
  "version": "1.0.0",
  "title": "Acme Help Center",
  "description": "Search and read the published articles of Acme Help Center.",
  "websiteUrl": "https://help.example.com",
  "remotes": [
    {
      "type": "streamable-http",
      "url": "https://help.example.com/mcp",
      "supportedProtocolVersions": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26"]
    }
  ]
}
```

The title, here `Acme Help Center`, is the default page title from your help center's SEO settings. See [SEO settings for your help center](https://self.helpcenter.io/content/seo-settings).

## When a help center has no public server

A help center offers the server only when an anonymous visitor could read it anyway. It has none when it is private or password-protected, limited to certain IP addresses, shown only inside your app with **JS-only access**, asking search engines not to index it (**Search engine indexing** off), switched off by its owner, or not live. Then `POST /mcp`, the server card and the AI catalog answer `404` with `{"error":"not_found","message":"This help center has no public MCP server."}`, `GET /mcp` shows the help center's not-found page, and llms.txt, ai.txt and the API catalog, where they are served, stop mentioning the server. The **Public MCP server** card in Settings says why.

## Usage

The **Public MCP server** card counts the requests AI assistants made in the last 30 days: searches, fetches and category listings. The count can lag a few minutes. It stores numbers only, not the queries, the articles read or who asked, and it is kept apart from your reader statistics: a `fetch` does not count as an article view.

## Related

- [llms.txt, ai.txt and Markdown pages](https://developers.helpcenter.io/content/llms-txt-and-markdown)
- [The HelpCenter.io MCP server](https://developers.helpcenter.io/content/the-mcp-connector-for-ai-agents)
- [Connect an AI assistant to your help center](https://developers.helpcenter.io/content/connect-an-ai-assistant-to-your-help-center)
