# MCP reference

Everything an AI app (or you, writing one) needs to talk to Hystore: the
addresses, the OAuth sign-in step by step, the message format and every tool
with an example. To just connect a ready app, see
[Connect your AI](/en/code-theme/mcp/).

## Addresses

| What | Address |
|---|---|
| MCP server | `POST https://mcp.hystore.com.br/mcp` |
| Resource metadata (RFC 9728) | `GET https://mcp.hystore.com.br/.well-known/oauth-protected-resource/mcp` |
| Sign-in metadata (RFC 8414) | `GET https://mcp.hystore.com.br/.well-known/oauth-authorization-server` |
| App registration (RFC 7591) | `POST https://mcp.hystore.com.br/v1/oauth/register` |
| Authorization screen | `https://app.hystore.com.br/oauth/authorize` |
| Token | `POST https://mcp.hystore.com.br/v1/oauth/token` |
| Sign out (revoke) | `POST https://mcp.hystore.com.br/v1/oauth/revoke` |

Every route is also in the [OpenAPI](/openapi.json).

- Transport: **Streamable HTTP without sessions**. Each `POST /mcp` carries one
  JSON-RPC message and gets a JSON answer. `GET` and `DELETE` answer `405`.
- Protocol: `2025-11-25` (also `2025-06-18` and `2025-03-26`).
- Web pages (requests with an `Origin` header) cannot call `/mcp` or the
  token endpoint: call from a program, not from the browser.

## How the sign-in works

It is OAuth 2.1 with PKCE, the way the MCP specification asks. Ready apps do
all of it by themselves; the steps below show what happens, with commands you
can run in a terminal.

### 1. Find where to sign in

Without a token, `/mcp` answers `401` and points to the metadata:

```bash
curl -i -X POST https://mcp.hystore.com.br/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'
```

```http
HTTP/2 401
www-authenticate: Bearer resource_metadata="https://mcp.hystore.com.br/.well-known/oauth-protected-resource/mcp", scope="theme:read theme:write theme:publish-request"
```

The metadata says who handles the sign-in (`authorization_servers`), and the
sign-in server's metadata has the authorization, token and registration
addresses:

```bash
curl https://mcp.hystore.com.br/.well-known/oauth-authorization-server
```

```json
{
  "issuer": "https://mcp.hystore.com.br",
  "authorization_endpoint": "https://app.hystore.com.br/oauth/authorize",
  "token_endpoint": "https://mcp.hystore.com.br/v1/oauth/token",
  "registration_endpoint": "https://mcp.hystore.com.br/v1/oauth/register",
  "revocation_endpoint": "https://mcp.hystore.com.br/v1/oauth/revoke",
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "scopes_supported": ["theme:read", "theme:write", "theme:publish-request"]
}
```

### 2. Register the app

The app registers once and gets a public `client_id` (no secret):

```bash
curl -X POST https://mcp.hystore.com.br/v1/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name":"My app","redirect_uris":["http://127.0.0.1:8765/callback"]}'
```

```json
{
  "client_id": "hys_cl_...",
  "client_name": "My app",
  "redirect_uris": ["http://127.0.0.1:8765/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "none"
}
```

- The redirect address must be `https`, loopback `http` (`127.0.0.1`,
  `localhost`, `[::1]`) or an app's own scheme (`myapp:/callback`).
- The name shows on the authorization screen marked as *given by the app*.
- An app that registers and is never used is removed after 30 days.

### 3. Make the PKCE pair

```bash
VERIFIER=$(openssl rand -base64 48 | tr -d '=+/' | cut -c1-64)
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
```

### 4. Send the person to authorize

Open in the browser (one line):

```text
https://app.hystore.com.br/oauth/authorize?response_type=code&client_id=hys_cl_...&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&code_challenge=<CHALLENGE>&code_challenge_method=S256&scope=theme%3Aread%20theme%3Awrite%20theme%3Apublish-request&resource=https%3A%2F%2Fmcp.hystore.com.br%2Fmcp&state=<a random value>
```

The person signs in to the dashboard, picks the store and authorizes. The
browser comes back to your address with `?code=hys_ac_...&state=...`. Check
the `state`.

> [!WARNING]
> The code lasts **60 seconds** and works once. Using the same code again
> revokes everything it produced.

### 5. Exchange the code for a token

```bash
curl -X POST https://mcp.hystore.com.br/v1/oauth/token \
  -d grant_type=authorization_code \
  -d code=hys_ac_... \
  -d client_id=hys_cl_... \
  -d redirect_uri=http://127.0.0.1:8765/callback \
  -d code_verifier="$VERIFIER" \
  -d resource=https://mcp.hystore.com.br/mcp
```

```json
{
  "access_token": "hys_at_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "hys_rt_...",
  "scope": "theme:read theme:write theme:publish-request"
}
```

The access token lasts one hour and only opens `/mcp`.

### 6. Refresh and sign out

```bash
curl -X POST https://mcp.hystore.com.br/v1/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=hys_rt_...
```

Each refresh returns a new pair and invalidates the old one. Using a
`refresh_token` that was already exchanged revokes the whole session (someone
copied the token). The session lasts 30 days from the last use.

```bash
curl -X POST https://mcp.hystore.com.br/v1/oauth/revoke -d token=hys_rt_...
```

## Messages

Each call is a `POST /mcp` with the token and a JSON-RPC message:

```bash
curl -X POST https://mcp.hystore.com.br/mcp \
  -H "Authorization: Bearer hys_at_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2025-11-25" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

### initialize

```json
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": { "name": "my-app", "version": "1.0.0" } } }
```

The answer has `serverInfo` (`hystore`), the `tools` capability and
`instructions`: a short text that tells the AI how to work with the theme
(read the guide first, edit the draft, show the preview, ask to publish).

### tools/call

```json
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": { "name": "read_theme_file", "arguments": { "path": "sections/hero.liquid" } }
}
```

```json
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "content": [{ "type": "text", "text": "path: sections/hero.liquid\nsha256: 3f1c...\n\n<section class=\"hero\">..." }],
    "structuredContent": { "path": "sections/hero.liquid", "content": "<section class=\"hero\">...", "sha256": "3f1c..." }
  }
}
```

A tool that refuses something answers with `"isError": true` and the reason
in the text (and in `structuredContent` when there are details). Protocol
errors (unknown method, a tool that does not exist) come back as a JSON-RPC
`error`.

## Tools

Argument names are in `snake_case`. Scope is the permission the connection
needs.

### read_theme_guide

Reads the theme guide (in English). Without arguments it returns the whole
guide; with `section` (part of a section title, such as `Sections`, `Data` or
`Apps`), only that part. Scope: `theme:read`.

```json
{ "name": "read_theme_guide", "arguments": { "section": "Sections" } }
```

### get_store

The store's name and address and this connection's permissions. No
arguments. Scope: `theme:read`.

### list_theme_files

The draft's files with `sha256`, size and the draft `revision`. No
arguments. Scope: `theme:read`.

### read_theme_file

One draft file with its `sha256`, which you send back as `base_sha` when
writing. Scope: `theme:read`.

```json
{ "name": "read_theme_file", "arguments": { "path": "layout/theme.liquid" } }
```

### write_theme_files

Creates, changes or deletes (`content: null`) up to 100 files at once: all or
nothing. Scope: `theme:write`.

```json
{
  "name": "write_theme_files",
  "arguments": {
    "changes": [
      { "path": "sections/black-friday.liquid", "content": "<section class=\"bf\">...</section>\n{% schema %}{\"name\":\"Black Friday\",\"settings\":[]}{% endschema %}", "base_sha": null },
      { "path": "templates/home.json", "content": "{\"sections\":{\"bf\":{\"type\":\"black-friday\"}},\"order\":[\"bf\"]}", "base_sha": "9a0b..." },
      { "path": "sections/old-banner.liquid", "content": null }
    ],
    "expected_revision": 12
  }
}
```

- `base_sha`: the `sha256` you read. If the file changed since then, nothing
  is written and the answer has `conflict`. `null` means "the file must not
  exist yet".
- `expected_revision` (optional): the draft `revision` you read; if another
  write got there first, nothing is written.
- A file with an error comes back in `problems`, with `path`, `line`, `code`
  and `message`. Nothing is written.

Success:

```json
{ "revision": 13, "files": [{ "path": "sections/black-friday.liquid", "sha256": "..." }], "warnings": [] }
```

Refused:

```json
{ "refused": true, "problems": [{ "path": "sections/black-friday.liquid", "line": 3, "code": "html_event_handler", "message": "Event attributes (onclick, onload...) are not allowed. Use data-hystore-* components.", "severity": "error" }] }
```

### check_theme

Checks the whole draft (the same check that runs before publishing): errors
and warnings with file and line. No arguments. Scope: `theme:read`.

### get_preview_link

A 24-hour link that opens the draft on the store's real address and reloads
by itself after each change. Give it to the person to look at. No arguments.
Scope: `theme:read`.

### request_publish

Asks to publish the current draft; someone with permission approves in
Customization > Code theme. Scope: `theme:publish-request`.

```json
{ "name": "request_publish", "arguments": { "note": "Black Friday banner at the top of the home" } }
```

### list_versions, read_version_file, restore_version

The last 30 published versions; a file as it was in a version
(`content: null` when that version did not have it); and bringing a version
back to the **draft** (the live store does not change until a publish is
approved). Scopes: `theme:read` to read, `theme:write` to restore.

```json
{ "name": "read_version_file", "arguments": { "version": 3, "path": "styles/theme.css" } }
```

```json
{ "name": "restore_version", "arguments": { "version": 3 } }
```

### list_assets, upload_asset, delete_asset

Images and fonts of the draft, with their public address. Use them in the
theme as `{{ 'name.webp' | asset_url }}` or `url(assets/name.webp)`. Scopes:
`theme:read` to list, `theme:write` to upload and delete.

```json
{ "name": "upload_asset", "arguments": { "path": "assets/banner.webp", "base64": "UklGRl..." } }
```

PNG, JPEG, WebP, GIF and AVIF up to 3 MB; WOFF and WOFF2 up to 1 MB. The type
is read from the content, not the name. SVG is not accepted as a media file.

### read_catalog

What a visitor sees of the store: active categories and products (id, name,
slug, price in cents, image). Use the ids with `data-hystore-add-to-cart` and
the slugs with `{% load products, category: '...' %}`. No arguments. Scope:
`theme:read`.

## Limits

- 120 reads and 30 writes per minute per connection; above that, `429` with
  `Retry-After` (in seconds).
- Files: 256 KB each, 400 files, 5 MB of text. Media: 100 files, 40 MB.
- One write carries up to 100 changes.

## Security

- The app token only opens `/mcp`. The keys and sessions of the
  [hystore tool](/en/code-theme/cli/) do not open `/mcp`, and the app token
  does not open the [theme API](/en/code-theme/api/).
- No tool touches orders, payments, players, coupons or settings, and none
  publishes by itself.
- Text that comes from the store (product names, descriptions) is treated as
  data, never as an instruction to the AI.
