---
title: "Theme Dev MCP server"
description: "Connect an AI client to the FirearmCart theme development MCP server — transport, tokens, abilities, and team scoping"
---

> API Reference Index
> Fetch the complete API index at: https://docs.firearmcart.com/api/llms.txt
> Use this file to discover all available pages before exploring further.

# Theme Dev MCP server

FirearmCart runs a Model Context Protocol server that lets an AI client browse, edit, screenshot and publish your store's Liquid themes. It is a sibling of the v1 REST API, not part of it: same host, same bearer tokens, different protocol.

```
POST https://api.firearmcart.com/mcp/themes
```

The server advertises itself as **FirearmCart Theme Dev** and exposes **33 tools**, **3 resources** and **2 prompts**. Nothing here is reachable over `/v1` — there is no REST equivalent for theme files.

| Property | Value |
|----------|-------|
| Endpoint | `https://api.firearmcart.com/mcp/themes` |
| Method | `POST` only |
| Transport | Streamable HTTP (JSON-RPC 2.0) |
| Auth | `Authorization: Bearer <token>` — the same store token the REST API uses |
| Abilities | `themes:read` and/or `themes:write` |
| Rate limit | 240 requests/minute per store, in its own bucket |
| Protocol versions | `2025-11-25` (default), `2025-06-18`, `2025-03-26`, `2024-11-05` |

---

## Transport

The endpoint speaks **Streamable HTTP**, the MCP transport where every client message is an HTTP `POST` carrying a JSON-RPC envelope.

Three things about this deployment are worth knowing before you configure a client:

- **`POST` is the only usable method.** `GET` and `DELETE` on the endpoint are registered, but they answer `405` with `Allow: POST` and nothing else. There is no server-initiated SSE channel to listen on, so a client that insists on opening a `GET` stream before it will talk to you cannot connect.
- **Those `405`s are unauthenticated.** The auth, ability and throttle middleware are attached to the `POST` route only. A `GET` without a token returns `405`, never `401` — so a probe that expects a `401` to confirm the endpoint exists will read the wrong signal.
- **Responses are `application/json`.** The server replies `Content-Type: application/json` for ordinary requests and only switches to `text/event-stream` when a tool streams its output. Send `Accept: application/json, text/event-stream` as the MCP spec requires; the server reorders that header internally so `application/json` wins content negotiation.

A session id is returned in an `MCP-Session-Id` response header on `initialize`. The server does not persist sessions and does not reject later requests that omit the header.

---

## Get a token

The MCP server uses the same tokens as the REST API. Mint one at **Profile → API Tokens** (`/user/api-tokens`) on `https://app.firearmcart.com`, after switching to the store you want the token to act on.

Tick **`themes:read`**, **`themes:write`**, or both. Neither ability appears in the pre-ticked read-only default, so you have to select them deliberately. See [Authentication](/api/authentication#abilities) for the token model and the token format.

Your store must also pass the API access gate — the plan must include **API & Webhooks**, or the store must hold the API access add-on. The gate runs on MCP requests exactly as it runs on `/v1`. See [Authentication](/api/authentication#api-access-gate).

---

## Connecting a client

### The raw contract

If you are wiring this up by hand, or your client's config format is not covered below, this is everything the server needs:

| | |
|---|---|
| URL | `https://api.firearmcart.com/mcp/themes` |
| Method | `POST` |
| `Authorization` | `Bearer <your-token>` |
| `Content-Type` | `application/json` |
| `Accept` | `application/json, text/event-stream` |

There is no OAuth flow on this server. It does not publish authorization-server metadata, and an unauthenticated request answers with `WWW-Authenticate: Bearer realm="mcp", error="invalid_token"` rather than a discovery URL. A client that only knows how to obtain a token by OAuth cannot bootstrap itself here — paste a token in.

### Claude Code

```bash
claude mcp add --transport http fc-themes https://api.firearmcart.com/mcp/themes \
  --header "Authorization: Bearer $FIREARMCART_TOKEN"
```

### Clients that take a JSON server list

Most desktop and IDE MCP clients keep a JSON file of servers, and most of them accept a remote HTTP server in roughly this shape:

```json
{
  "mcpServers": {
"fc-themes": {
  "type": "http",
  "url": "https://api.firearmcart.com/mcp/themes",
  "headers": {
    "Authorization": "Bearer YOUR_TOKEN_HERE"
  }
}
  }
}
```

> **Note:** The exact key names differ between clients — some spell the transport `"transport"` rather than `"type"`, some nest headers differently, and some do not support remote servers at all without a local bridge. We cannot promise a schema we do not control. Check your client's own documentation and map it onto the raw contract above; the only values that come from FirearmCart are the URL and the `Authorization` header.

### Verify with curl

The handshake is plain JSON-RPC, so you can prove a token works before touching a client config:

```bash
curl -sS https://api.firearmcart.com/mcp/themes \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
  "protocolVersion": "2025-11-25",
  "capabilities": {},
  "clientInfo": { "name": "curl", "version": "1.0" }
}
  }'
```

A working token returns a result whose `serverInfo.name` is `FirearmCart Theme Dev` and whose `instructions` field carries the server's full authoring brief. Swap `"method": "initialize"` for `"method": "tools/list"` to see the tools your token can actually reach — which is not always all 33. That is the next section.

---

## Which ability connects, which ability gates

This distinction is the one thing most likely to confuse you, so it is worth being precise.

Connecting requires **any one** of the two theme abilities, not both. `themes:read`
alone is enough to open a session; `themes:write` alone is too.

Practically:

- A token holding **only `themes:read`** connects successfully. It is not a second-class connection; the handshake, resources and prompts all work.
- A token holding **only `themes:write`** also connects.
- A token holding **neither** is rejected at the route with `403 {"message": "Invalid ability provided."}`.

Write access is then enforced per tool, in two layers:

1. **The 15 write tools hide themselves.** Each one's `shouldRegister()` checks the token for `themes:write`. A read-only token's `tools/list` returns **18 tools, not 33** — the write tools are absent from the listing rather than present-and-failing. If you are debugging a "the agent says that tool doesn't exist" report, check the token's abilities first.
2. **The tools re-check on call.** Reaching a write tool anyway returns an error result: `This token lacks themes:write. Create a token with Themes: Write ability.`

One tool sits slightly off this grid. `screenshot-theme` renders a preview and uploads the image, so it is not annotated read-only the way the other 17 read tools are — but it is **not** write-gated either, and a `themes:read` token can call it. Screenshotting is how an agent verifies its own work, so a read-only token can still run the full inspect-and-report loop.

---

## Team scoping

Everything here inherits the REST API's identity model: the authenticated principal is a **store**, not a person. Read [Authentication](/api/authentication#the-token-belongs-to-a-store-not-a-user) if you have not.

Every tool resolves the store from the token and constrains its query to that store's rows before doing anything else. There is no store argument on any tool and no way to reach another store's themes:

- Theme lookups match on `team_id` plus the id or UUID you passed. A theme that exists but belongs elsewhere is indistinguishable from one that does not exist: `Theme not found for this team. Call list-themes first.`
- Blog articles have no store column of their own, so article tools resolve the blog first and reach articles only through it. The scoping cannot be bypassed by passing an article id directly.
- A token that authenticates but does not resolve to a store is rejected by the tool itself with an unauthenticated error, telling you to provide a store API token.

If you run several stores, you need one token per store, and one configured MCP server per store.

---

## Rate limits

The MCP server has **its own bucket**: 240 requests per minute, keyed by store. Agent sessions make many small tool calls, so it is four times the `/v1` allowance.

| | Limit | Keyed by |
|---|---|---|
| Authenticated | 240/minute | Store |
| Unauthenticated | 10/minute | Client IP |

It does not draw on, and is not drawn on by, the `/v1` budget of 60/minute. Exhausting one leaves the other untouched. See [Rate limits](/api/rate-limits#other-buckets).

Note that the unit is **requests**, not tool calls. A client that batches several JSON-RPC messages into one `POST` spends one slot.

---

## Failures that are not JSON-RPC

Tool-level problems come back as ordinary MCP error results, and your client will render them. Middleware-level rejections happen before the MCP server runs at all, so they arrive as plain HTTP with a framework error body — which most clients surface as a bare connection failure.

| Status | Body | Cause |
|--------|------|-------|
| `401` | `{"message": "Unauthenticated."}` | Missing, mistyped or revoked token. Also carries `WWW-Authenticate: Bearer realm="mcp", error="invalid_token"`. |
| `403` | `{"error": "forbidden", "message": "API access is not enabled for this team."}` | The store's plan has no API access and it holds no add-on. |
| `403` | `{"error": "subscription_expired", "message": "Your subscription has expired. Please renew to continue using the API."}` | Subscription suspended after a failed payment, or past its cancellation date. |
| `403` | `{"message": "Invalid ability provided."}` | The token holds neither `themes:read` nor `themes:write`. |
| `405` | *(empty, with `Allow: POST`)* | The request used `GET` or `DELETE`. |
| `429` | Standard throttle response, with `Retry-After` | 240/minute exceeded. |

If a client reports "server disconnected" or "failed to initialize" with no further detail, replay the handshake with the curl command above — the HTTP status tells you which row you are in.

---

## How the server expects to be used

The `initialize` response ships a long `instructions` block that your agent receives automatically. Its shape, condensed:

1. **`list-themes`, then never build against the active theme.** The active theme is the live storefront. Use an inactive library copy, or call `duplicate-theme` to make one. File-write tools refuse the active theme unless you pass `allow_active: true`.
2. **Read the `liquid-guidelines` resource before writing any Liquid.** This is not optional advice; it is the platform-specific rulebook, and the failure modes it covers are silent ones. See [Resources & prompts](/api/mcp/resources).
3. **Inventory before authoring.** `list-theme-files` and `get-section-schema` first — configuring an existing section through `templates/*.json` beats writing a new one.
4. **Save, then look.** Writes are validated (Liquid, JSON and schema) and versioned; `screenshot-theme` on desktop, then a tablet and mobile pass.
5. **Revert cheaply.** `list-file-versions` and `restore-file-version` when a change made things worse.
6. **Publish last.** `publish-theme` when the merchant is ready; the previously active theme is deactivated automatically.

The brief also covers what a theme reads but does not own. Menus, policies and plugin sections are store data with their own tools. Collection filters are a theme setting: which specification groups appear as filters on collection pages is trimmed with `list-collection-filters` and `set-collection-filters`, never by editing the facets snippets or hiding filter groups with CSS.

The full tool surface is on [Tools](/api/mcp/tools), and the reference material the server hands your agent is on [Resources & prompts](/api/mcp/resources).

---

## Related documentation

- [Tools](/api/mcp/tools) — all 33 tools, their arguments and their guardrails
- [Resources & prompts](/api/mcp/resources) — the 3 resources and 2 prompts the server ships
- [Authentication](/api/authentication) — the store-scoped token model and the API access gate
- [Rate limits](/api/rate-limits) — how the MCP bucket sits alongside the `/v1` buckets
- [Themes](/themes) — the merchant-facing theme editor these tools write into

Source: https://docs.firearmcart.com/api/mcp/index.mdx
