# Shopkit MCP server

The Shopkit MCP server connects AI assistants such as ChatGPT, Claude, Claude Code, Codex and Cursor to a Shopkit store. Once the merchant approves the connection, the assistant can read and update products, orders, customers, pages, the theme and the rest of the store through the [Shopkit API](https://shopk.it/developers/api), with the access the merchant granted.

```text
https://mcp.shopk.it/mcp
```

### Connecting a client

In every client the merchant signs in to Shopkit in the browser, picks the store and approves the access the assistant asks for.

#### ChatGPT

Add a custom connector (or an app) with the URL `https://mcp.shopk.it/mcp`. ChatGPT opens the Shopkit sign in page when the connector is added.

#### Claude (app and claude.ai)

Add a custom connector in Claude's settings with the URL `https://mcp.shopk.it/mcp`. Keep the authentication and OAuth client options Claude detects: it signs in with OAuth and identifies itself with its published client document (CIMD), so nothing needs to be registered in Shopkit.

#### Claude Code

```bash
claude mcp add --transport http shopkit https://mcp.shopk.it/mcp
claude mcp login shopkit
```

You can also sign in from inside Claude Code with the `/mcp` command.

#### Codex

```bash
codex mcp add shopkit --url https://mcp.shopk.it/mcp
codex mcp login shopkit
```

#### Cursor and other MCP clients

Add a remote (Streamable HTTP) server with the URL `https://mcp.shopk.it/mcp`. The client finds the authorization server through the discovery described in Authentication.

### Server address and transport

- **Endpoint:** `https://mcp.shopk.it/mcp`.
- **Transport:** Streamable HTTP. Every JSON-RPC message is a `POST` to the endpoint and the answer comes back as a single JSON response (`application/json`). The server keeps no sessions and does not open SSE streams, so a `GET` returns `405` with `Allow: POST`.
- **Protocol versions:** `2025-06-18` and `2025-11-25`. In `initialize` the server answers with the version the client asked for when it is one of these, and with `2025-11-25` otherwise. An `MCP-Protocol-Version` header with any other value returns `400`.
- **Methods:** `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list` and `resources/read`. Notifications (messages without `id`) are accepted with `202` and an empty body.
- **Capabilities:** tools, and resources for the ChatGPT app (see [ChatGPT app](#chatgpt-app)).

### Authentication

Every request needs an OAuth 2.0 access token in the `Authorization: Bearer` header, issued by the Shopkit authorization server for the resource `https://mcp.shopk.it/mcp`. The full flow (authorization code with PKCE, scopes, refresh and revocation) is described in [OAuth 2.0](https://shopk.it/developers/oauth).

MCP clients discover everything by themselves:

1. A request without a valid token returns `401` with a `WWW-Authenticate` header that points to the protected resource metadata. When a token was sent but is invalid, expired or revoked, the header also carries `error="invalid_token"`.

```text
WWW-Authenticate: Bearer resource_metadata="https://mcp.shopk.it/.well-known/oauth-protected-resource/mcp"
```

2. The Protected Resource Metadata (RFC 9728) at `https://mcp.shopk.it/.well-known/oauth-protected-resource/mcp` (also served without `/mcp`) names the resource, the authorization server, the supported scopes and the header as the only way to send the token. The supported scopes are only the ones the [tools](#tools) need. The consent for the MCP server offers all of them, whatever valid `scope` the client asks, so every client shows the merchant the same areas; the user grants those their permissions allow. A `scope` that is not valid fails with `invalid_scope` (see [OAuth 2.0](https://shopk.it/developers/oauth) for where errors go), and the token response's `scope` says what was granted.

```json
{
  "resource": "https://mcp.shopk.it/mcp",
  "authorization_servers": ["https://shopk.it"],
  "scopes_supported": ["orders:read", "orders:write", "products:read", "products:write", "..."],
  "bearer_methods_supported": ["header"]
}
```

3. The authorization server metadata (RFC 8414) at `https://shopk.it/.well-known/oauth-authorization-server` gives the authorize, token and revoke endpoints.

Clients such as ChatGPT, Claude, Claude Code and Codex identify themselves with a Client ID Metadata Document (CIMD): the `client_id` is an `https` URL that serves the client's metadata, so they connect without registering an app first. Any other client can use an app registered by the merchant under **Configurações › Developers › OAuth**.

Tokens are bound to their audience. A token issued for `https://mcp.shopk.it/mcp` only works on the MCP server and is refused by the API, and API tokens (issued for `https://api.shopk.it` or without `resource`) are refused by the MCP server.

### Tools

`tools/list` returns `shopkit_get_profile`, `shopkit_search`, one tool per store operation and, when the app is available, the three [ChatGPT app](#chatgpt-app) tools. It is the same list for every connection.

#### Store operations

Every operation of the [API v1 reference](https://shopk.it/developers/api) (OpenAPI) except the `DELETE` ones is a tool named after its `operationId`: products and their variants, extra options, tabs, quantity tiers and custom fields, categories, brands, orders and their PDF, shipments and shipping methods, customers, coupons, pages, blog posts, tags and authors, media, redirects, webhooks, stats, the store and the theme editor. A call runs that API operation, so validation, the user's permissions and store events work exactly as in the API. An operation added to the API becomes a tool too.

A record is read with its `*_get_by_id` tool, such as `{"name": "order_get_by_id", "arguments": {"id": 1042}}`, or with the list tool and `id`. A `DELETE` operation is not a tool: calling it returns `-32602` `Unknown tool` (see [Disabled actions](#disabled-actions)).

Each tool has its `inputSchema` (path and query parameters and, on `POST` and `PUT`, a `body` object; path parameters are required, unknown arguments are rejected, and on `PUT` no field of `body` is required, since it is a partial update), `securitySchemes` with the scope it needs, and `annotations` with boolean hints: `GET` tools are `readOnlyHint`; `PUT` tools are `destructiveHint`, since they overwrite data, as are `order_post` and `order_invoice_post` (they can e-mail the customer and issue invoices), `webhook_post` (it sends store data to a URL), `redirect_post` (it can send the storefront's visitors elsewhere) and `product_option_group_post` (it rebuilds the product's variants, deleting the ones that no longer match); `openWorldHint` marks the ones that reach outside the store (e-mails to the customer, invoicing, importing media from a public URL, webhook deliveries). A call without the scope is refused as described in [Errors](#errors).

#### shopkit_search

Searches Shopkit content with the same engine as the public [search](https://shopk.it/search?format=md): support articles, blog, features, apps, sell online guides and site pages, only published, each with an `excerpt` of the passage that matched (features also bring the `admin_url` of their page). Then come the store records the connection can read: orders, products, customers, pages and more. It takes one argument, `query`, and returns up to 20 short results (10 of Shopkit content, 10 of the store) with `type`, `id`, `title`, `summary`, `excerpt`, `url` and `admin_url`. The server instructions tell the assistant to use it before searching the web for any question about Shopkit. Store records only come from the areas the scopes and the user's permissions allow. Each authorization can run 30 searches per minute.

#### shopkit_get_profile

Takes no arguments and returns the store and user behind the connection: a stable `id` for that user in that store, `name`, `email` and `nickname` (the store name).

#### ChatGPT app

When the app is available, `tools/list` also returns three tools that make up the Shopkit app in ChatGPT, an [MCP App](https://github.com/modelcontextprotocol/ext-apps) whose page is the resource `ui://shopkit/app/v1.html` (`text/html;profile=mcp-app`, listed in `resources/list` and served by `resources/read`; any other URI returns `-32002`):

- `shopkit_app_open`: the store home, opened from the ChatGPT sidebar or next to a conversation (`_meta["openai/ui"]["entrypoints"]` `global` and `thread`). It is app-only (`_meta.ui.visibility: ["app"]`), so the assistant does not call it. It takes an optional `period` (7, 30 or 90 days, 30 by default) and returns, read through the API with the connection's access: sales, orders, sessions and conversion rate with the previous period, daily sales of both periods, pending orders, orders waiting for payment, products out of stock, abandoned carts, popular products, popular searches and traffic sources. A block the connection cannot read is left out and listed in `unavailable`; a read that failed is left out and listed in `failed`; neither is shown as zero. The buttons of the page send a request to the conversation (`ui/message`), so the work continues with the other tools. The page reads the home again when it is updated by hand, when it gets focus back more than 30 seconds after the last read, and when the period changes; a read that fails keeps the last data, marked as out of date.
- `shopkit_show_product`: shows a product (`id`) in a card next to the conversation: title, status, price, stock, categories, description, image and the links to the store and the admin. The assistant calls it after creating or changing a product.
- `shopkit_use_image`: gives the assistant the link of an image the user attached in the conversation (`_meta["openai/fileParams"]`), to put right away in the `images` of `product_post` or `product_put`. The link is temporary; Shopkit copies the image when it imports the product images.

#### Theme editor

With the `appearance` scope, the operations include the store's theme editor (the API's `/theme`), with the same validation, CSS compilation and fields as the visual editor: `theme_field_get` (fields that match `q`, with type, allowed values and current value), `theme_get` (all current values) and `theme_put`, which changes only the fields sent. Run it with `"mode": "dry_run"` to validate and compile without saving, and with `"publish"`, after the merchant confirms, to change the live store. Each publish keeps the theme as it was as a version in the editor, named after the app, so the merchant can go back.

#### Writes the assistant confirms first

Creating and changing orders (`order_post`, `order_put`, which can e-mail the customer), issuing invoices (`order_invoice_post`), creating webhooks (`webhook_post`), creating redirects (`redirect_post`) and adding option groups to a product (`product_option_group_post`) run, marked `destructive`, and their descriptions tell the assistant to confirm with the merchant first and why. `theme_put` says the same for `publish`.

#### Disabled actions

Deleting cannot be undone, so no `DELETE` operation is a tool: the list and record reads of what the API deletes say where the merchant deletes it in the Shopkit admin (the record's `admin_url`, the redirects, webhooks or media page, or the product's page), so the assistant gives that page. They remain available in the API.

### Results

- Successful JSON results come in `structuredContent` and, serialized, in `content[0].text`. `_meta["shopkit/http_status"]` carries the API status code.
- Lists come as `{"items": [...], "paging": {...}, "total_count": N}`. Operations with a `limit` parameter return 10 items unless you pass `limit` (up to 50); use the filters and `page` to go further. A list with no matches is `{"items": [], "total_count": 0}`; a page past the end is `{"items": []}`, without `total_count`, since the API does not count it there.
- Products, orders, customers, categories, brands, pages, blog posts and coupons include `admin_url`, the record's page in the Shopkit admin, both in lists and in single records.
- Empty fields of objects (`null`, empty strings and empty arrays) are left out. `0` and `false` are kept, and lists keep every position: a daily series has one value per day, `null` on a day without data.

The `initialize` response includes `instructions` for the assistant: which store it is connected to, how lists and `admin_url` work, when to use `shopkit_search`, where deleting is done and which writes to confirm with the user. Some clients, such as Claude, do not give these instructions to the model, so the confirmations and where to delete are also in the tool descriptions.

Changes made through the MCP server appear in the store's history as made by the user who approved the connection, "via" the assistant, with the origin `mcp` (also in the `X-Webhook-Resource-Origin` header of the store's webhooks).

### Errors

| Case | Response |
|---|---|
| Body is not valid JSON | JSON-RPC error `-32700` Parse error |
| Not a valid JSON-RPC 2.0 request | JSON-RPC error `-32600` Invalid Request |
| Unknown method | JSON-RPC error `-32601` Method not found |
| Unknown tool, arguments outside the `inputSchema` or missing path parameter | JSON-RPC error `-32602` with the reason, for example `Invalid params: arguments.limit must be integer` |
| The API answered with an error | Tool result with `isError: true`, the API response in `content` and its status in `_meta["shopkit/http_status"]` |
| The API answered with a body that is not JSON | Tool result with `isError: true`; for a write, the text says the change may have been applied and to look it up before repeating it |
| The tool needs a scope the connection does not have, and approving the connection again can give it | Tool result with `isError: true` and `_meta["mcp/www_authenticate"]` with a Bearer challenge carrying `error="insufficient_scope"` and the missing `scope`, so the client can ask the merchant to approve it |
| The tool needs a permission the user who connected it does not have in the store, or access the store removed in **Apps › Conectadas** | Tool result with `isError: true` and no challenge: the text says who can give it and where (the store's users page, or the app's page in **Apps › Conectadas**) |
| Missing, invalid, expired or revoked token | HTTP `401` with `WWW-Authenticate` |
| Unsupported `MCP-Protocol-Version` | HTTP `400` |
| `Origin` not allowed | HTTP `403` with `{"error": "origin_not_allowed"}` |

### Limits, CORS and permissions

- **Plan:** the store's plan must include the API. Tool calls count towards the plan's hourly API request limit, the same one the API uses.
- **Search:** `shopkit_search` has its own limit of 30 searches per minute for each authorization.
- **CORS:** browser requests are accepted only from `https://chatgpt.com`, `https://claude.ai` and `https://claude.com`; any other `Origin` gets `403`. The MCP specification asks servers to check `Origin` to prevent DNS rebinding attacks, and clients that call the server from their own servers send no `Origin`, so they are not affected.
- **Permissions:** a connection can do what both the approved scopes and the approving user's permissions in the store allow. If the user loses a permission or is deactivated, the connection loses it too.
- **Disconnecting:** the merchant sees every connected assistant under **Apps › Conectadas**, where they can change its access or disconnect it. The change applies to the next request.

### Privacy and support

What the connection does with store data, what Shopkit keeps and for how long is in the [privacy policy](https://shopk.it/privacy-policy#assistentes-ia). Privacy questions go to [privacy@shopk.it](mailto:privacy@shopk.it), and help with connecting an assistant to [support@shopk.it](mailto:support@shopk.it).

### Examples

Every request has the same headers:

```http
POST /mcp HTTP/1.1
Host: mcp.shopk.it
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2025-11-25
```

#### initialize

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

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-11-25",
    "capabilities": {"tools": {}, "resources": {}},
    "serverInfo": {"name": "shopkit", "version": "18.8.2"},
    "instructions": "Connected to the Shopkit store \"My Store\" (https://my-store.shopk.it), with the access this connection was granted. ..."
  }
}
```

#### tools/list

```json
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
```

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {"name": "shopkit_get_profile", "...": "..."},
      {"name": "shopkit_search", "...": "..."},
      {"name": "store_get", "...": "..."},
      {
        "name": "product_put",
        "title": "Update Product",
        "inputSchema": {
          "type": "object",
          "properties": {
            "id": {"type": "integer"},
            "body": {"type": "object", "properties": {"...": "..."}, "additionalProperties": false, "minProperties": 1}
          },
          "required": ["id", "body"],
          "additionalProperties": false
        },
        "securitySchemes": [{"type": "oauth2", "scopes": ["products:write"]}],
        "annotations": {"title": "Update Product", "readOnlyHint": false, "destructiveHint": true, "idempotentHint": true, "openWorldHint": false}
      },
      {"name": "...", "...": "..."}
    ]
  }
}
```

#### A list tool

```json
{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "product_get", "arguments": {"limit": 2}}}
```

```json
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [{"type": "text", "text": "{\"items\":[{\"id\":107,\"title\":\"Caneca de Cerâmica Azul\", ...}"}],
    "_meta": {"shopkit/http_status": 200},
    "isError": false,
    "structuredContent": {
      "items": [
        {
          "id": 107,
          "title": "Caneca de Cerâmica Azul",
          "reference": "CAN-AZUL-01",
          "price": 14.9,
          "price_formatted": "14,90 €",
          "status": 1,
          "status_alias": "active",
          "featured": true,
          "url": "https://my-store.shopk.it/product/caneca-de-ceramica-azul",
          "admin_url": "https://my-store.shopk.it/admin/products/detail/107"
        }
      ],
      "paging": {"next": "https://api.shopk.it/v1/product?limit=2&page=2"},
      "total_count": 27
    }
  }
}
```

#### An operation that is not a tool

```json
{"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "product_delete", "arguments": {"id": 107}}}
```

```json
{"jsonrpc": "2.0", "id": 5, "error": {"code": -32602, "message": "Unknown tool"}}
```

#### An API error

```json
{"jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": {"name": "product_get", "arguments": {"id": 999999999}}}
```

```json
{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "content": [{"type": "text", "text": "{\"message\":\"Not found\"}"}],
    "_meta": {"shopkit/http_status": 404},
    "isError": true
  }
}
```
