HystoreWiki

5 min readView as Markdown

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.

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.

  • 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:

Terminal
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:

Terminal
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):

Terminal
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

Terminal
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):

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.

5. Exchange the code for a token

Terminal
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

Terminal
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.

Terminal
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:

Terminal
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.

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 do not open /mcp, and the app token does not open the 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.