Un server Model Context Protocol remoto con cui un agente AI (Claude Code, Claude Desktop o claude.ai, Cursor, o qualunque client MCP) legge l'ambiente di prova del tuo negozio: quanti prodotti hai, i prodotti, lo stato del catalogo, gli ordini di prova da prendere in carico, i punti vendita. Non scrive mai. I numeri vengono dallo stesso codice che serve l'API REST (GET /v1/...): l'agente e il tuo codice vedono sempre la stessa cosa.
Versione inglese: mcp.en.md.
L'indirizzo
https://oxjqgjbiltoviwldailg.supabase.co/functions/v1/core-mcp- Trasporto: Streamable HTTP, una
POSTper ogni richiesta JSON-RPC, risposte JSON. Nessuna sessione, nessuno stream SSE (GETeDELETErispondono405). - Versioni del protocollo:
2026-07-28(la revisione corrente: senza stato,server/discover, le intestazioniMCP-Protocol-Version,Mcp-MethodeMcp-Name) e le precedenti2025-11-25,2025-06-18,2025-03-26(con la stretta di manoinitialize). Il client sceglie da solo; i client di oggi parlano per lo piu'2025-11-25. - I metadati della risorsa protetta (RFC 9728), senza chiave:
GET https://oxjqgjbiltoviwldailg.supabase.co/functions/v1/core-mcp/.well-known/oauth-protected-resource. Una richiesta senza chiave riceve401conWWW-Authenticate: Bearer resource_metadata="…", scope="catalog:read orders:read".
La chiave
Si usa una chiave segreta di prova del negozio, gai_test_…, mandata come Authorization: Bearer gai_test_… in ogni richiesta. Si crea nel portale, in Sviluppatori, con gli ambiti degli strumenti che servono:
| ambito | strumenti |
|---|---|
catalog:read | count_products, list_products, get_catalog_status, list_stores |
orders:read | list_orders |
- Le chiavi live (
gai_live_…) si rifiutano (CHIAVE_LIVE_NON_AMMESSA): questo server serve solo l'ambiente di prova. Per i dati veri si usa l'API REST dal proprio codice. - Le chiavi pubblicabili (
gai_pk_…) si rifiutano (CHIAVE_PUBBLICABILE_NON_AMMESSA): non hanno ambiti. - Uno strumento di cui la chiave non ha l'ambito risponde
403conWWW-Authenticate: Bearer error="insufficient_scope", scope="…"(AMBITO_MANCANTE). Gli ambiti non si sottintendono:catalog:writenon contienecatalog:read. - La chiave sta in una variabile d'ambiente (qui
GROCERYAI_TEST_KEY), mai in un file che si committa. Quando non serve piu' la si revoca dal portale.
L'accesso con OAuth non c'e' ancora: la chiave e' un gettone che hai gia', come la chiave «agente» di Stripe. Per questo i metadati non elencano authorization_servers: un client che non trova la chiave non avvia un flusso OAuth, riporta il 401.
Aggiungerlo al tuo agente
Claude Code (un comando; la shell mette nella configurazione la chiave presa dal tuo ambiente):
claude mcp add --transport http groceryai https://oxjqgjbiltoviwldailg.supabase.co/functions/v1/core-mcp \
--header "Authorization: Bearer $GROCERYAI_TEST_KEY"Per condividerlo con un progetto senza scrivere la chiave da nessuna parte, in .mcp.json alla radice del progetto (Claude Code espande ${GROCERYAI_TEST_KEY} dall'ambiente di chi apre il progetto):
{
"mcpServers": {
"groceryai": {
"type": "http",
"url": "https://oxjqgjbiltoviwldailg.supabase.co/functions/v1/core-mcp",
"headers": { "Authorization": "Bearer ${GROCERYAI_TEST_KEY}" }
}
}
}Claude Desktop e claude.ai: Customize > Connectors > Add > Add custom connector; l'indirizzo qui sopra; Authentication No sign in; in Request headers si aggiunge Authorization con valore Bearer gai_test_…. Il connettore lo chiama il cloud di Anthropic, quindi l'indirizzo deve essere raggiungibile da internet (lo e').
Cursor: in ~/.cursor/mcp.json (tutti i progetti) o .cursor/mcp.json (un progetto):
{
"mcpServers": {
"groceryai": {
"url": "https://oxjqgjbiltoviwldailg.supabase.co/functions/v1/core-mcp",
"headers": { "Authorization": "Bearer ${env:GROCERYAI_TEST_KEY}" }
}
}
}Poi si chiede: «quanti prodotti ho?». L'agente chiama count_products.
Gli strumenti
Tutti di sola lettura (readOnlyHint: true), in ordine alfabetico; ognuno esegue un GET dell'API REST con la stessa chiave, lo stesso ambito e lo stesso ambiente.
| strumento | che cosa rende | argomenti | equivalente REST |
|---|---|---|---|
count_products | {environment, products, available} | nessuno | GET /v1/catalog/freshness (products, available); uguale a sfogliare GET /v1/products fino in fondo |
get_catalog_status | {freshness, feed}: ultimo aggiornamento, eta', cadenza dichiarata, stale, il feed programmato e il suo ultimo giro | nessuno | GET /v1/catalog/freshness + GET /v1/catalog/feed |
list_orders | {orders}, dal piu' vecchio | status (new di serie, acked), limit (1-200, di serie 50) | GET /v1/orders |
list_products | {products, next_cursor}, in ordine di sku | sku, gtin, limit (1-200, di serie 50), cursor, store_code | GET /v1/products |
list_stores | {stores} | nessuno | GET /v1/stores |
Ogni risultato porta i dati due volte: in structuredContent e come testo JSON in content. Un argomento sbagliato (nome sconosciuto, fuori limite, fuori elenco) o un errore della rotta (per esempio PUNTO_VENDITA_SCONOSCIUTO per uno store_code che non c'e') torna come risultato dello strumento con isError: true e il messaggio, cosi' l'agente si corregge da solo; l'errore della rotta e' la stessa busta dell'API REST (error.code, error.doc_url, request_id).
I titoli e le descrizioni dei prodotti sono dati del negozio: un agente li tratta come dati, non come istruzioni.
I limiti
- 120 richieste al minuto per chiave (il limite della chiave, lo stesso contatore dell'API REST). Ogni
POSTconta una volta, ancheinitialize,tools/listeserver/discover; uno strumento che legge due rotte conta comunque una volta. Oltre il limite:429conRetry-After. - Corpo fino a 1 MiB; una richiesta JSON-RPC per
POST(niente lotti). - Solo l'ambiente di prova, solo letture. Gli strumenti che scrivono e l'accesso OAuth 2.1 arriveranno in una versione successiva.
- Le richieste con l'intestazione
Originsi rifiutano (403) se l'origine non e' ammessa esplicitamente: il server e' per gli agenti, non per le pagine web. - Ogni richiesta autenticata finisce nel registro d'audit dell'API del tuo account (
routemcp:tools/call:<strumento>, esito, durata), come le chiamate REST.
Gli errori
Gli errori JSON-RPC portano error.data.code (stabile, gli stessi codici dell'API REST dove esistono) e, dove il codice ha una pagina, error.data.doc_url.
| HTTP | error.code | error.data.code | quando |
|---|---|---|---|
| 401 | -32001 | CHIAVE_ASSENTE, CHIAVE_MALFORMATA, CHIAVE_SCONOSCIUTA, CHIAVE_REVOCATA, CHIAVE_SCADUTA, CHIAVE_INCOERENTE | la chiave |
| 401 | -32001 | CHIAVE_LIVE_NON_AMMESSA, CHIAVE_PUBBLICABILE_NON_AMMESSA | una chiave che questo server non serve |
| 403 | -32003 | AMBITO_MANCANTE (required_scopes), TENANT_SOSPESO | l'ambito, l'account |
| 403 | -32005 | ORIGINE_NON_AMMESSA | un'Origin non ammessa |
| 429 | -32004 | LIMITE_RICHIESTE (retry_after) | il limite di richieste |
| 400 | -32020 | MCP-Protocol-Version, Mcp-Method o Mcp-Name non corrispondono al corpo (2026-07-28) | |
| 400 | -32022 | versione del protocollo non servita; error.data.supported elenca quelle servite | |
| 404 | -32601 | metodo non servito (2026-07-28) |
Esempi provati
Tutti e due gli esempi li esegue contro il server tests/mcp/esempi.mjs (su un negozio sintetico di 437 prodotti).
Con curl, una sola richiesta senza stato, nella revisione corrente:
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"}}}'La risposta: {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","content":[{"type":"text","text":"…"}],"structuredContent":{"environment":"test","products":437,"available":291},"isError":false,"_meta":{…}}}.
Con l'SDK TypeScript ufficiale (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();Sul negozio sintetico stampa {"environment":"test","products":437,"available":291}; sul tuo, i tuoi numeri.