# GroceryAI MCP server · read-only, test environment

- HTML: https://groceryai.ai/developers/mcp · Back to the API reference: https://groceryai.ai/developers

A remote [Model Context Protocol](https://modelcontextprotocol.io) server that lets an AI agent (Claude Code, Claude
Desktop or claude.ai, Cursor, or any MCP client) read **the test environment of your shop**: how many products you have,
the products themselves, the state of the catalog, the test orders waiting to be taken over, your stores. It never writes.
The numbers come from the same code that serves the REST API (`GET /v1/...`), so the agent and your code always see the
same thing.

Italian version: [`mcp.md`](https://groceryai.ai/it/sviluppatori/mcp).

## Address

```
https://oxjqgjbiltoviwldailg.supabase.co/functions/v1/core-mcp
```

- Transport: **Streamable HTTP**, one `POST` per JSON-RPC request, JSON responses. No sessions, no SSE stream (`GET` and
  `DELETE` answer `405`).
- Protocol versions: **`2026-07-28`** (the current revision: stateless, `server/discover`, `MCP-Protocol-Version`,
  `Mcp-Method` and `Mcp-Name` headers) and the earlier `2025-11-25`, `2025-06-18`, `2025-03-26` (`initialize`
  handshake). Clients pick automatically; today's clients mostly speak `2025-11-25`.
- Protected resource metadata ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)), no key needed:
  `GET https://oxjqgjbiltoviwldailg.supabase.co/functions/v1/core-mcp/.well-known/oauth-protected-resource`. A request
  without a key gets `401` with `WWW-Authenticate: Bearer resource_metadata="…", scope="catalog:read orders:read"`.

## The key

Use a **secret test key** of your shop, `gai_test_…`, sent as `Authorization: Bearer gai_test_…` on every request.
Create it in the portal under **Developers** with the scopes of the tools you want:

| scope | tools |
|---|---|
| `catalog:read` | `count_products`, `list_products`, `get_catalog_status`, `list_stores` |
| `orders:read` | `list_orders` |

- **Live keys (`gai_live_…`) are refused** (`CHIAVE_LIVE_NON_AMMESSA`): this server serves only the test environment. For
  live data use the REST API from your own code.
- **Publishable keys (`gai_pk_…`) are refused** (`CHIAVE_PUBBLICABILE_NON_AMMESSA`): they have no scopes.
- A tool whose scope the key lacks answers `403` with `WWW-Authenticate: Bearer error="insufficient_scope",
  scope="…"` (`AMBITO_MANCANTE`). Scopes are never implied: `catalog:write` does not include `catalog:read`.
- Keep the key in an environment variable (here `GROCERYAI_TEST_KEY`), never in a file you commit. Revoke it in the portal
  when you no longer need it.

There is no OAuth sign-in yet: the key is a token you already have, like a Stripe agent key. The metadata document
therefore lists no `authorization_servers`; a client that finds no key does not start an OAuth flow, it reports the `401`.

## Add it to your agent

**Claude Code** (one command; the shell puts the key from your environment into the configuration):

```bash
claude mcp add --transport http groceryai https://oxjqgjbiltoviwldailg.supabase.co/functions/v1/core-mcp \
  --header "Authorization: Bearer $GROCERYAI_TEST_KEY"
```

To share it with a project without writing the key anywhere, put this in `.mcp.json` at the project root (Claude Code
expands `${GROCERYAI_TEST_KEY}` from the environment of whoever opens the project):

```json
{
  "mcpServers": {
    "groceryai": {
      "type": "http",
      "url": "https://oxjqgjbiltoviwldailg.supabase.co/functions/v1/core-mcp",
      "headers": { "Authorization": "Bearer ${GROCERYAI_TEST_KEY}" }
    }
  }
}
```

**Claude Desktop and claude.ai**: Customize > Connectors > Add > Add custom connector; URL as above; Authentication **No
sign in**; under **Request headers** add `Authorization` with value `Bearer gai_test_…`. The connector is called from
Anthropic's cloud, so the address must be reachable from the internet (it is).

**Cursor**: in `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):

```json
{
  "mcpServers": {
    "groceryai": {
      "url": "https://oxjqgjbiltoviwldailg.supabase.co/functions/v1/core-mcp",
      "headers": { "Authorization": "Bearer ${env:GROCERYAI_TEST_KEY}" }
    }
  }
}
```

Then ask: «how many products do I have?». The agent calls `count_products`.

## Tools

All tools are read-only (`readOnlyHint: true`), listed in alphabetical order, and each one runs a `GET` of the REST API
with the same key, scope and environment.

| tool | what it returns | arguments | REST equivalent |
|---|---|---|---|
| `count_products` | `{environment, products, available}` | none | `GET /v1/catalog/freshness` (`products`, `available`); equals paging `GET /v1/products` to the end |
| `get_catalog_status` | `{freshness, feed}`: last update, age, declared cadence, `stale`, the scheduled feed and its last run | none | `GET /v1/catalog/freshness` + `GET /v1/catalog/feed` |
| `list_orders` | `{orders}`, oldest first | `status` (`new` default, `acked`), `limit` (1-200, default 50) | `GET /v1/orders` |
| `list_products` | `{products, next_cursor}`, ordered by `sku` | `sku`, `gtin`, `limit` (1-200, default 50), `cursor`, `store_code` | `GET /v1/products` |
| `list_stores` | `{stores}` | none | `GET /v1/stores` |

Every result carries the data twice, as `structuredContent` and as JSON text in `content`. A wrong argument (unknown name,
out of range, not in the list) or an error of the route (for example `PUNTO_VENDITA_SCONOSCIUTO` for an unknown
`store_code`) comes back as a tool result with `isError: true` and the message, so the agent can correct itself; the
error of a route is the same envelope as the REST API (`error.code`, `error.doc_url`, `request_id`).

Product titles and descriptions are your shop's data: an agent should treat them as data, not as instructions.

## Limits

- **120 requests per minute per key** (the key's own limit, the same counter as the REST API). Every `POST` counts once,
  also `initialize`, `tools/list` and `server/discover`; a tool that reads two routes still counts once. Over the limit:
  `429` with `Retry-After`.
- Body up to 1 MiB; one JSON-RPC request per `POST` (no batches).
- Only the test environment, only reads. Writing tools and OAuth 2.1 sign-in will come in a later version.
- Requests that carry an `Origin` header are refused (`403`) unless the origin is explicitly allowed: the server is meant
  for agents, not for web pages.
- Every authenticated request is recorded in the API audit log of your account (`route` `mcp:tools/call:<tool>`, status,
  duration), like REST calls.

## Errors

JSON-RPC errors carry `error.data.code` (stable, the same codes as the REST API where they exist) and, where the code has a
page, `error.data.doc_url`.

| HTTP | `error.code` | `error.data.code` | when |
|---|---|---|---|
| 401 | -32001 | `CHIAVE_ASSENTE`, `CHIAVE_MALFORMATA`, `CHIAVE_SCONOSCIUTA`, `CHIAVE_REVOCATA`, `CHIAVE_SCADUTA`, `CHIAVE_INCOERENTE` | the key |
| 401 | -32001 | `CHIAVE_LIVE_NON_AMMESSA`, `CHIAVE_PUBBLICABILE_NON_AMMESSA` | a key this server does not serve |
| 403 | -32003 | `AMBITO_MANCANTE` (`required_scopes`), `TENANT_SOSPESO` | the scope, the account |
| 403 | -32005 | `ORIGINE_NON_AMMESSA` | an `Origin` that is not allowed |
| 429 | -32004 | `LIMITE_RICHIESTE` (`retry_after`) | the rate limit |
| 400 | -32020 | | `MCP-Protocol-Version`, `Mcp-Method` or `Mcp-Name` do not match the body (`2026-07-28`) |
| 400 | -32022 | | protocol version not served; `error.data.supported` lists the served ones |
| 404 | -32601 | | method not served (`2026-07-28`) |

## Tested examples

Both examples are run against the server by `tests/mcp/esempi.mjs` (on a synthetic shop of 437 products).

With `curl`, one stateless request in the current revision:

<!-- esempio: curl -->
```bash
curl -sS "$GROCERYAI_MCP_URL" \
  -H "Authorization: Bearer $GROCERYAI_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/call" \
  -H "Mcp-Name: count_products" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"count_products","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}'
```
<!-- /esempio -->

The answer: `{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","content":[{"type":"text","text":"…"}],"structuredContent":{"environment":"test","products":437,"available":291},"isError":false,"_meta":{…}}}`.

With the official TypeScript SDK (`npm install @modelcontextprotocol/sdk`):

<!-- esempio: js -->
```js
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const client = new Client({ name: 'my-integration', version: '1.0.0' });
await client.connect(new StreamableHTTPClientTransport(new URL(process.env.GROCERYAI_MCP_URL), {
  requestInit: { headers: { Authorization: `Bearer ${process.env.GROCERYAI_TEST_KEY}` } },
}));
const result = await client.callTool({ name: 'count_products', arguments: {} });
console.log(JSON.stringify(result.structuredContent));
await client.close();
```
<!-- /esempio -->

It prints `{"environment":"test","products":437,"available":291}` on the synthetic shop; on yours, your numbers.
