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
POSTper JSON-RPC request, JSON responses. No sessions, no SSE stream (GETandDELETEanswer405). - Protocol versions:
2026-07-28(the current revision: stateless,server/discover,MCP-Protocol-Version,Mcp-MethodandMcp-Nameheaders) and the earlier2025-11-25,2025-06-18,2025-03-26(initializehandshake). Clients pick automatically; today's clients mostly speak2025-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 gets401withWWW-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
403withWWW-Authenticate: Bearer error="insufficient_scope", scope="…"(AMBITO_MANCANTE). Scopes are never implied:catalog:writedoes not includecatalog: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.
| 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
POSTcounts once, alsoinitialize,tools/listandserver/discover; a tool that reads two routes still counts once. Over the limit:429withRetry-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
Originheader 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 (
routemcp: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:
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.