Back to the API reference

The GroceryAI MCP server

Connect an AI agent to your shop's test environment: the address, the test key, the commands for Claude Code, Claude Desktop and Cursor.

This page in Markdown

A remote Model Context Protocol 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.

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

scopetools
catalog:readcount_products, list_products, get_catalog_status, list_stores
orders:readlist_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):

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

{
  "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):

{
  "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.

toolwhat it returnsargumentsREST equivalent
count_products{environment, products, available}noneGET /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 runnoneGET /v1/catalog/freshness + GET /v1/catalog/feed
list_orders{orders}, oldest firststatus (new default, acked), limit (1-200, default 50)GET /v1/orders
list_products{products, next_cursor}, ordered by skusku, gtin, limit (1-200, default 50), cursor, store_codeGET /v1/products
list_stores{stores}noneGET /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.

HTTPerror.codeerror.data.codewhen
401-32001CHIAVE_ASSENTE, CHIAVE_MALFORMATA, CHIAVE_SCONOSCIUTA, CHIAVE_REVOCATA, CHIAVE_SCADUTA, CHIAVE_INCOERENTEthe key
401-32001CHIAVE_LIVE_NON_AMMESSA, CHIAVE_PUBBLICABILE_NON_AMMESSAa key this server does not serve
403-32003AMBITO_MANCANTE (required_scopes), TENANT_SOSPESOthe scope, the account
403-32005ORIGINE_NON_AMMESSAan Origin that is not allowed
429-32004LIMITE_RICHIESTE (retry_after)the rate limit
400-32020MCP-Protocol-Version, Mcp-Method or Mcp-Name do not match the body (2026-07-28)
400-32022protocol version not served; error.data.supported lists the served ones
404-32601method 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:

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"}}}'

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

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();

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

Back to the API reference