
PrestaShop pubblica dall'aprile 2026 un modulo MCP ufficiale, proprietario e legato al suo OAuth. Per mantenere il controllo, scrivi un server MCP in PHP che avvolge l'API Webservice con una chiave limitata a GET e alcuni strumenti che rispondono a una domanda invece di rilanciare l'API. Conta circa 650 righe di PHP e tre livelli di rifiuto indipendenti.
Collegare un agente di codice a un negozio PrestaShop è allettante: leggerebbe il catalogo, individuerebbe le schede incomplete e le rotture di stock al posto tuo. Il rischio non viene dalla sua imprudenza, ma dalla superficie che gli apri. Questo tutorial costruisce un server MCP in PHP che espone solo quattro letture, e attraverso il quale nessuna scrittura può passare.
Esiste già un server MCP per PrestaShop?
Sì, dalla primavera 2026. PrestaShop pubblica il proprio modulo, PrestaShop MCP Server, la cui documentazione pubblica annuncia esplicitamente che l’editore «holds all associated intellectual property rights» e concede una licenza personale, non esclusiva e non trasferibile. La pagina di introduzione che ho aperto il 7 settembre 2026 mostra un ultimo aggiornamento al 30 aprile 2026. Due vincoli contano per uno sviluppatore: il modulo non è open source, e la sua autenticazione «works exclusively with PrestaShop OAuth».
Prova datata e indipendente della sua esistenza, il pacchetto prestashop/ps-mcp-server-stubs pubblicato su Packagist in versione 1.0.3 il 9 luglio 2026, licenza proprietary, descritto come degli «IDE stubs for ps_mcp_server MCP attributes and exceptions». I moduli di terze parti possono peraltro innestarvi i propri strumenti tramite attributi PHP PsMcpTool, PsMcpSchema e PsMcpToolAnnotations.
Sul fronte della community, l’API di GitHub offre un quadro meno incoraggiante. I quattro repository trovati il 7 settembre 2026 non hanno più ricevuto un solo commit dopo la loro settimana di creazione.
| Repository | Linguaggio | Licenza | Creato | Ultimo push | Stelle |
|---|---|---|---|---|---|
latinogino/prestashop-mcp | Python | MIT | 30/06/2025 | 30/06/2025 | 8 |
promokit/prestashop-mcp | TypeScript | nessuna | 12/07/2025 | 16/07/2025 | 2 |
florinel-chis/prestashop-mcp | Python | MIT | 24/11/2025 | 24/11/2025 | 9 |
100peck/prestashop-mcp-server | TypeScript | MIT | 01/03/2026 | 02/03/2026 | 0 |
Nessuno è stato eseguito qui, ed è volontario: la scelta che conta si gioca tra superficie controllata e superficie subita, non tra ufficiale e community. Un server che scrivi tu sta in tre file PHP e sai, riga per riga, cosa sa fare.
Perché il Webservice, e non il database?
PrestaShop espone da tempo un’API REST, il Webservice, «a CRUD API» secondo la documentazione per sviluppatori della versione 9. Il suo interesse qui sta nel controllo degli accessi più che nella ricchezza del modello, dato che una chiave di trentadue caratteri riceve diritti per risorsa e per metodo HTTP. La documentazione lo dice senza giri di parole: «you might want a user to have read and write access on some resources, but only read access on others».
Il rifiuto non è quindi scritto nel tuo codice PHP, dove un errore di programmazione può cancellarlo. È applicato dal negozio, prima ancora che il tuo server esista. È la stessa logica di difesa in profondità descritta nell’articolo sulle sandbox e i permessi degli agenti di codice.
La chiave si crea nel back-office (Parametri avanzati > Webservice) o via codice, con la classe WebserviceKey e il suo metodo setPermissionForAccount(). Per il laboratorio, l’ho creata in SQL perché non avevo un browser nel giro. I diritti concessi: solo GET, su nove risorse.
// Diritti attribuiti alla chiave del laboratorio: nient'altro che GET.
$permissions = [];
foreach (['products', 'categories', 'stock_availables', 'orders', 'order_states',
'combinations', 'manufacturers', 'languages', 'currencies'] as $resource) {
$permissions[$resource] = ['GET' => 1];
}La verifica non si fa sulla parola. Bastano tre richieste, con la chiave passata come nome utente HTTP Basic e una password vuota:
# Risorsa autorizzata: 200
curl -s -u "$PS_WS_KEY:" \
"http://127.0.0.1:8097/api/products?output_format=JSON&limit=3"
{"products":[{"id":1},{"id":2},{"id":3}]}
# Risorsa assente dai diritti: 401
curl -s -u "$PS_WS_KEY:" "http://127.0.0.1:8097/api/customers?output_format=JSON"
{"errors":[{"code":26,"message":"Resource of type \"customers\" is not allowed
with this authentication key"}]}
# Scrittura su una risorsa pur autorizzata in lettura: 405
curl -s -X PUT -u "$PS_WS_KEY:" "http://127.0.0.1:8097/api/products/1"
<code><![CDATA[25]]></code>
<message><![CDATA[Method PUT is not allowed for the resource products
with this authentication key]]></message>DELETE restituisce lo stesso 405. E il prezzo del prodotto 1 valeva 23,90 € prima di queste prove, 23,90 € dopo. È l’unico controllo che conti.
Fase 1, un client Webservice che sa solo leggere
La seconda barriera è nel codice. La classe che parla con il negozio espone un solo metodo pubblico, get(): anche se la chiave ricevesse un giorno diritti di scrittura per errore, il server MCP non avrebbe alcun modo di servirsene. Impone anche output_format=JSON, vedremo più avanti che questo dettaglio ha un costo.
final class PrestaShopWebservice
{
public function __construct(
string $baseUrl,
private string $key,
private int $timeout = 10
) {
$this->baseUrl = rtrim($baseUrl, '/');
}
public function get(string $resource, array $query = []): array
{
$query['output_format'] = 'JSON';
$url = $this->baseUrl . '/api/' . ltrim($resource, '/') . '?' . http_build_query($query);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPGET => true,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $this->key . ':', // chiave come nome utente, password vuota
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_FOLLOWLOCATION => false,
]);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
// 404 su una collezione filtrata = zero risultati, non un guasto.
if ($status === 404) {
return [];
}
if ($status !== 200) {
throw new RuntimeException(self::readableError((string) $body, $status));
}
return json_decode((string) $body, true) ?: [];
}
}Il Webservice ha una sintassi di richiesta propria, da verificare una per una prima di scriverla nel codice. Queste sono state validate su PrestaShop 9.1.4: display=[id,name,price] per scegliere i campi, filter[name]=%[colibri]% per un «contiene», filter[quantity]=[0,2000] per un intervallo, filter[id]=[1|2|3] per un «o», sort=[price_DESC], e date=1 che deve accompagnare ogni filtro su date_add.
Fase 2, quattro strumenti che rispondono in linguaggio naturale, non in JSON
Qui si gioca l’essenziale, e molti server MCP lo mancano. Uno strumento non deve riversare l’API nel contesto del modello, deve rispondere a una domanda. I quattro strumenti del server sono: search_products, get_product, orders_summary e stock_alerts. Ognuno restituisce un testo breve e, in parallelo, uno structuredContent per il codice che volesse rileggerlo.
get_product illustra l’idea: invece di consegnare la scheda grezza, calcola ciò che l’agente dovrebbe altrimenti dedurre da solo.
// Le mancanze che l'agente deve vedere subito, senza ragionare sul JSON.
$gaps = [];
if ($summary['meta_title'] === '') {
$gaps[] = 'méta-titre vide';
}
if ($summary['meta_description'] === '') {
$gaps[] = 'méta-description vide';
}
if ($longLen < 300) {
$gaps[] = 'description longue de ' . $longLen . ' caractères';
}
if ($summary['reference'] === '') {
$gaps[] = 'référence absente';
}
$summary['gaps'] = $gaps;Sul negozio dimostrativo, la chiamata restituisce questo, un testo che un umano legge tanto in fretta quanto un modello:
#1 T-shirt imprimé colibri (réf. demo_1)
Prix HT : 23,90 € · Stock : 2400 · Actif : oui
Méta-titre : (vide)
Description courte : 94 caractères · longue : 367 caractères
À corriger : méta-titre vide, méta-description videIl guadagno si misura. Sullo stesso catalogo di diciannove prodotti, ecco cosa finisce nel contesto a seconda del metodo scelto.
| Cosa riceve l’agente | Byte |
|---|---|
GET /api/products?display=full (XML, formato predefinito) | 139.350 |
GET /api/products?display=full&output_format=JSON | 41.126 |
Testo restituito dallo strumento search_products | 1.137 |
Centoventidue volte meno del XML grezzo. Un server MCP che si limita a riprodurre i punti di ingresso dell’API fa pagare al client la totalità di questa differenza, a ogni chiamata. Il ragionamento è sviluppato nella guida sulla creazione di un server MCP in PHP, che porta anche tutta la teoria del protocollo che questo tutorial suppone acquisita.
Fase 3, il server HTTP: un token, un’origine, un metodo
Il trasporto sta in un file e tre rifiuti. Un server MCP locale ascolta sul loopback locale, il che non lo protegge da una pagina web aperta nel browser della stessa macchina: da qui la verifica dell’header Origin. La revisione 2026-07-28 della specifica avendo eliminato il flusso GET, tutto passa da POST. E il token bearer è distinto dalla chiave Webservice: lo si può revocare senza toccare il negozio.
// 1. Origin: senza questa verifica, una pagina web aperta nel browser
// della macchina può parlare al server locale (DNS rebinding).
$origin = $_SERVER['HTTP_ORIGIN'] ?? null;
if ($origin !== null && !in_array($origin, $origins, true)) {
respond(403, ['jsonrpc' => '2.0', 'error' => ['code' => -32600, 'message' => 'Origine refusée.']]);
}
// 2. La revisione 2026-07-28 ha eliminato il flusso GET: tutto passa da POST.
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
header('Allow: POST');
respond(405, ['jsonrpc' => '2.0', 'error' => ['code' => -32600, 'message' => 'POST uniquement.']]);
}
// 3. Token bearer. La chiave Webservice non lascia mai il server.
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$sent = preg_match('/^Bearer\s+(.+)$/i', trim((string) $header), $m) === 1 ? $m[1] : '';
if ($mcpToken !== '' && !hash_equals($mcpToken, $sent)) {
header('WWW-Authenticate: Bearer');
respond(401, jsonrpcError($id, -32001, 'Jeton MCP absent ou invalide.'));
}Una sottigliezza finale merita di essere notata: un errore di strumento non si restituisce come errore JSON-RPC. La specifica distingue i «protocol errors» dai «tool execution errors», questi ultimi dovendo tornare nel risultato con isError: true perché il modello possa correggersi. Chiedere il prodotto 9999 restituisce quindi un 200 HTTP contenente il testo «Produit 9999 introuvable.».
Il server si avvia con il server integrato di PHP:
php -S 127.0.0.1:8110 server.phpLe risposte qui sotto sono state tutte rilevate su questa istanza.
| Richiesta | Risposta |
|---|---|
GET /mcp | 405 |
POST /mcp senza token | 401, -32001 |
Origin: https://exemple-malveillant.test | 403 |
initialize (protocolVersion 2025-06-18) | 200, versione restituita identica |
tools/list | 200, quattro strumenti, 1.928 byte |
notifications/initialized (senza id) | 202, corpo vuoto |
MCP-Protocol-Version: 1900-01-01 | 400, -32022 con la lista delle versioni gestite |
Collegare Codex senza toccare la sua configurazione
Codex legge il suo file ~/.codex/config.toml, ma accetta anche chiavi di configurazione al volo con -c. È il modo giusto per provare un server: nulla viene scritto su disco, e la prova non sopravvive al comando. L’impronta del mio config.toml era peraltro identica prima e dopo le quattro esecuzioni qui sotto. Il token, invece, resta in una variabile d’ambiente piuttosto che nella riga di comando: la documentazione di Codex prevede bearer_token_env_var per questo.
export MCP_TOKEN="…" # token del server MCP, mai la chiave Webservice
codex exec \
-c 'mcp_servers.ps.url="http://127.0.0.1:8110/mcp"' \
-c 'mcp_servers.ps.bearer_token_env_var="MCP_TOKEN"' \
-c 'mcp_servers.ps.default_tools_approval_mode="writes"' \
-m gpt-6-astra -s read-only --skip-git-repo-check \
"Avec les outils ps, trouve la référence en alerte de stock sous 150 unités,
puis dis ce qui manque dans sa fiche produit."La mia prima versione del server non passava. Codex vedeva bene il server, elencava bene i quattro strumenti, ma ogni chiamata si fermava su:
mcp: ps/stock_alerts (failed)
MCP tool call requires approval, but approval policy is neverIl riflesso è cercare l’impostazione di Codex che sblocca. È il posto sbagliato. Ho incrociato le due variabili su quattro esecuzioni, stesso comando, stesso modello, stesso negozio, solo il server cambiava, con o senza annotazioni.
| Annotazioni del server | default_tools_approval_mode | Risultato |
|---|---|---|
readOnlyHint | non precisato (predefinito) | completed |
readOnlyHint | "writes" | completed |
| nessuna | non precisato (predefinito) | failed |
| nessuna | "writes" | failed |
La colonna che decide è quella delle annotazioni, cioè il protocollo, e non l’impostazione del client. La specifica MCP prevede delle annotazioni facoltative che descrivono il comportamento di uno strumento, dichiarando readOnlyHint, il server dice al client che nessuna chiamata modifica lo stato remoto, e Codex gli crede senza che si debba regolare nulla. Bastano quattro righe.
// I quattro strumenti sono in sola lettura: lo si dichiara una volta per tutte.
// È questa annotazione che i client leggono per decidere se chiedere
// un'approvazione all'utente prima di ogni chiamata.
$readOnly = [
'readOnlyHint' => true,
'destructiveHint' => false,
'idempotentHint' => true,
'openWorldHint' => false,
];Con esse, lo stesso comando riesce.
mcp: ps/stock_alerts (completed)
mcp: ps/get_product (completed)
Référence : demo_21 — Pack Mug + Affiche encadrée (ID 15).
Stock : 100 unités, sous le seuil de 150.
Fiche incomplète : méta-titre, méta-description et description longue absents.Due chiamate di strumento, 11.263 token. Una precisazione di sicurezza si impone comunque, e la specifica stessa la formula: i client «MUST consider tool annotations to be untrusted unless they come from trusted servers». Un’annotazione readOnlyHint è una dichiarazione del server, non una garanzia tecnica. Questa era vera perché ero stato io a scrivere il server. Non sostituisce né la chiave limitata a GET, né il client PHP senza metodo di scrittura. Viene dopo, e per un server di terze parti non vale nulla.
E in Claude Code?
La dichiarazione si fa con un comando, con il token lasciato come variabile perché il file sia versionabile.
claude mcp add --scope project --transport http ps http://127.0.0.1:8110/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Il file .mcp.json scritto alla radice del progetto contiene effettivamente ${MCP_TOKEN}, e non il valore del token.
{
"mcpServers": {
"ps": {
"type": "http",
"url": "http://127.0.0.1:8110/mcp",
"headers": {
"Authorization": "Bearer ${MCP_TOKEN}"
}
}
}
}Un server con ambito progetto non è comunque attivo. claude mcp list lo mostra in attesa, e la documentazione spiega perché: «Claude Code prompts for approval in interactive sessions before using project-scoped servers from .mcp.json files». È il comportamento giusto, un repository clonato non dovrebbe mai collegare un server da solo.
ps: http://127.0.0.1:8110/mcp (HTTP) - ⏸ Pending approval (run `claude` to approve)L’approccio è lo stesso di un server MCP destinato a WordPress, con la differenza che WordPress fornisce ormai un’API di abilities e un adattatore ufficiale, laddove PrestaShop lascia il Webservice come base.
Due trappole che l’agente non vedrà mai
La radice del Webservice in JSON risponde 500. Su PrestaShop 9.1.4 con PHP 8.5, GET /api/?output_format=JSON restituisce un 500 quando la stessa radice in XML restituisce un 200. Il difetto 34794 del repository PrestaShop descrive esattamente questo, «Uncaught TypeError: array_filter()» in uscita JSON, ed è ancora aperto: segnalato il 9 dicembre 2023, ultima attività il 10 agosto 2026. Non lasciare quindi mai che un agente scopra le risorse disponibili tramite la radice JSON, codifica la lista.
Lo sfasamento di fuso orario è silenzioso, ed è il peggiore. PrestaShop scrive le sue date nel fuso orario del negozio (PS_TIMEZONE, qui Europe/Paris), mentre il PHP che eseguiva il mio server girava in UTC. La mia prima versione di orders_summary delimitava la finestra su now() - 30 jours: ha restituito zero ordini su un negozio che ne contava cinque. Nessun errore, nessun avviso, uno zero perfettamente credibile, che l’agente avrebbe riportato tale e quale. La correzione consiste nel delimitare su giornate intere.
// PrestaShop scrive le sue date nel fuso orario del negozio (PS_TIMEZONE), non in
// quello del processo PHP che esegue questo server. Ci si delimita quindi su giornate
// intere, altrimenti alcune ore di ordini spariscono senza una parola.
$from = (new DateTimeImmutable('today -' . $days . ' days'))->format('Y-m-d 00:00:00');
$to = (new DateTimeImmutable('tomorrow'))->format('Y-m-d 00:00:00');È la classe di errore più costosa con un agente: quella che produce una risposta plausibile. Un server MCP che aggrega deve essere testato su dati di cui conosci il conteggio in anticipo.
A cosa serve, concretamente
Tre usi reggono con questi quattro strumenti in sola lettura. L’audit delle schede per primo: get_product restituisce già la lista delle mancanze, l’agente non deve far altro che scorrere e raggruppare. Il monitoraggio delle rotture poi, con stock_alerts chiamato da un agente programmato piuttosto che da un umano che apre il back-office. La preparazione di un cantiere di contenuti infine: individuare le cinquanta schede da rivedere è un lavoro di lettura, riscriverle su scala è un altro, che spetta a un modulo dedicato come WizardAI.
Non aggiungere un quinto strumento che scrive. Il giorno in cui ne avrai bisogno, scrivi un secondo server, con la sua chiave, il suo token e la sua politica di approvazione. Due server separati valgono meglio di un server la cui metà degli strumenti è pericolosa.
Cosa ricordare
- PrestaShop pubblica effettivamente un modulo MCP ufficiale dall’aprile 2026, proprietario e legato al suo OAuth, i quattro progetti della community trovati sono tutti abbandonati dalla loro settimana di creazione.
- La sicurezza sta in tre livelli indipendenti: chiave Webservice limitata a
GETsu nove risorse, client PHP senza metodo di scrittura, token MCP distinto e revocabile. - Uno strumento deve rispondere a una domanda, non rilanciare un’API: 1.137 byte di testo utile contro 139.350 byte di XML grezzo per lo stesso catalogo.
- Senza annotazione
readOnlyHint, Codex rifiuta di chiamare uno strumento in modalità non interattiva, qualunque sia l’impostazione di approvazione. È il protocollo che sblocca, non il client, ma un’annotazione resta una dichiarazione del server, non una garanzia. - Testa le tue aggregazioni su dati di cui conosci il conteggio: uno sfasamento di fuso orario dà uno zero credibile che nessuno verificherà.
Errori frequenti
GET /api/?output_format=JSON risponde 500 su PrestaShop 9.1.4 con PHP 8.5 (difetto aperto da dicembre 2023) mentre la stessa radice in XML risponde 200. Codifica la lista delle risorse invece di farla scoprire.PS_TIMEZONE, il tuo PHP magari gira in UTC. La finestra now() - 30 jours ha restituito zero ordini su un negozio che ne contava cinque. Delimita su giornate intere.readOnlyHint, Codex rifiuta la chiamata in modalità non interattiva: MCP tool call requires approval, but approval policy is never. Verificato su quattro esecuzioni incrociate: nessuna impostazione di default_tools_approval_mode sostituisce l'annotazione.bearer_token_env_var lato Codex e ${MCP_TOKEN} lato Claude Code, perché il file resti versionabile.

