
A PrestaShop publica desde abril de 2026 um módulo MCP oficial, proprietário e ligado ao seu OAuth. Para manteres o controlo, escreve um servidor MCP em PHP que envolve a API Webservice com uma chave limitada a GET e algumas ferramentas que respondem a uma pergunta em vez de retransmitir a API. Conta com cerca de 650 linhas de PHP e três camadas de recusa independentes.
Ligar um agente de código a uma loja PrestaShop é tentador: liaria o catálogo, identificaria as fichas incompletas e as ruturas de stock no teu lugar. O risco não vem da sua imprudência, mas da superfície que lhe abres. Este tutorial constrói um servidor MCP em PHP que só expõe quatro leituras, e pelo qual nenhuma escrita pode passar.
Já existe um servidor MCP para o PrestaShop?
Sim, desde a primavera de 2026. A PrestaShop publica o seu próprio módulo, PrestaShop MCP Server, cuja documentação pública anuncia sem rodeios que a editora «holds all associated intellectual property rights» e concede uma licença pessoal, não exclusiva e não transferível. A página de introdução que abri a 7 de setembro de 2026 mostra uma última atualização a 30 de abril de 2026. Duas restrições contam para um programador: o módulo não é open source, e a sua autenticação «works exclusively with PrestaShop OAuth».
Prova datada e independente da sua existência, o pacote prestashop/ps-mcp-server-stubs publicado no Packagist na versão 1.0.3 a 9 de julho de 2026, licença proprietary, descrito como «IDE stubs for ps_mcp_server MCP attributes and exceptions». Os módulos de terceiros podem aliás enxertar aí as suas próprias ferramentas através de atributos PHP PsMcpTool, PsMcpSchema e PsMcpToolAnnotations.
Do lado da comunidade, a API do GitHub dá um quadro menos animador. Os quatro repositórios encontrados a 7 de setembro de 2026 não voltaram a receber um único commit depois da sua semana de criação.
| Repositório | Linguagem | Licença | Criado | Último push | Estrelas |
|---|---|---|---|---|---|
latinogino/prestashop-mcp | Python | MIT | 30/06/2025 | 30/06/2025 | 8 |
promokit/prestashop-mcp | TypeScript | nenhuma | 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 |
Nenhum foi executado aqui, e isso é deliberado: a escolha que conta joga-se entre superfície controlada e superfície sofrida, não entre oficial e comunitário. Um servidor que escreves cabe em três ficheiros PHP e sabes, linha a linha, o que ele sabe fazer.
Porquê o Webservice, e não a base de dados?
O PrestaShop expõe há muito uma API REST, o Webservice, «a CRUD API» segundo a documentação para programadores da versão 9. O seu interesse aqui está no controlo de acesso mais do que na riqueza do modelo, já que uma chave de trinta e dois caracteres recebe direitos por recurso e por método HTTP. A documentação di-lo sem rodeios: «you might want a user to have read and write access on some resources, but only read access on others».
A recusa não está portanto escrita no teu código PHP, onde um erro de programação a pode apagar. É aplicada pela loja, antes mesmo de o teu servidor existir. É a mesma lógica de defesa em profundidade descrita no artigo sobre as sandbox e as permissões dos agentes de código.
A chave cria-se no back-office (Parâmetros avançados > Webservice) ou por código, com a classe WebserviceKey e o seu método setPermissionForAccount(). Para o laboratório, criei-a em SQL já que não tinha um navegador no circuito. Os direitos concedidos: só GET, sobre nove recursos.
// Direitos atribuídos à chave do laboratório: nada além de GET.
$permissions = [];
foreach (['products', 'categories', 'stock_availables', 'orders', 'order_states',
'combinations', 'manufacturers', 'languages', 'currencies'] as $resource) {
$permissions[$resource] = ['GET' => 1];
}A verificação não se faz por palavra dada. Três pedidos bastam, com a chave a passar como nome de utilizador HTTP Basic com uma palavra-passe vazia:
# Recurso autorizado: 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}]}
# Recurso ausente dos direitos: 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"}]}
# Escrita num recurso no entanto autorizado em leitura: 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 devolve o mesmo 405. E o preço do produto 1 valia 23,90 € antes destes ensaios, 23,90 € depois. É o único controlo que vale.
Etapa 1, um cliente Webservice que só sabe ler
A segunda barreira está no código. A classe que fala com a loja só expõe um método público, get(): mesmo que a chave viesse um dia a receber direitos de escrita por erro, o servidor MCP não teria nenhuma forma de os usar. Ela também força output_format=JSON, veremos mais abaixo que este detalhe tem um custo.
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 . ':', // chave como nome de utilizador, palavra-passe vazia
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_FOLLOWLOCATION => false,
]);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
// 404 numa coleção filtrada = zero resultados, não uma falha.
if ($status === 404) {
return [];
}
if ($status !== 200) {
throw new RuntimeException(self::readableError((string) $body, $status));
}
return json_decode((string) $body, true) ?: [];
}
}O Webservice tem uma sintaxe de pedido própria, que é preciso verificar uma a uma antes de a escrever em código. Estas foram validadas no PrestaShop 9.1.4: display=[id,name,price] para escolher os campos, filter[name]=%[colibri]% para um «contém», filter[quantity]=[0,2000] para um intervalo, filter[id]=[1|2|3] para um «ou», sort=[price_DESC], e date=1 que deve acompanhar qualquer filtro sobre date_add.
Etapa 2, quatro ferramentas que respondem em francês, não em JSON
É aqui que se joga o essencial, e muitos servidores MCP falham-no. Uma ferramenta não deve despejar a API no contexto do modelo, deve responder a uma pergunta. As quatro ferramentas do servidor são: search_products, get_product, orders_summary e stock_alerts. Cada uma devolve um texto curto e, em paralelo, um structuredContent para o código que o queira reler.
get_product ilustra a ideia: em vez de entregar a ficha bruta, calcula o que o agente teria de deduzir sozinho.
// As lacunas que o agente deve ver de imediato, sem ter de raciocinar sobre o 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;Na loja de demonstração, a chamada devolve isto, um texto que um humano lê tão depressa como um modelo:
#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 videO ganho mede-se. No mesmo catálogo de dezanove produtos, eis o que aterra no contexto consoante o método escolhido.
| O que o agente recebe | Bytes |
|---|---|
GET /api/products?display=full (XML, formato por defeito) | 139 350 |
GET /api/products?display=full&output_format=JSON | 41 126 |
Texto devolvido pela ferramenta search_products | 1 137 |
Cento e vinte e duas vezes menos do que o XML em bruto. Um servidor MCP que se limita a reproduzir os pontos de entrada da API faz o cliente pagar a totalidade desta diferença, a cada chamada. O raciocínio está desenvolvido no guia sobre a criação de um servidor MCP em PHP, que também traz toda a teoria do protocolo que este tutorial presume já adquirida.
Etapa 3, o servidor HTTP: um token, uma origem, um método
O transporte cabe num ficheiro e três recusas. Um servidor MCP local escuta na loopback local, o que não o protege de uma página web aberta no navegador da mesma máquina: daí a verificação do cabeçalho Origin. Tendo a revisão 2026-07-28 da especificação eliminado o fluxo GET, tudo passa por POST. E o token bearer é distinto da chave Webservice: pode revogar-se sem tocar na loja.
// 1. Origin: sem esta verificação, uma página web aberta no navegador
// da máquina pode falar com o servidor local (requalificação de DNS).
$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. A revisão 2026-07-28 eliminou o fluxo GET: tudo passa por POST.
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
header('Allow: POST');
respond(405, ['jsonrpc' => '2.0', 'error' => ['code' => -32600, 'message' => 'POST uniquement.']]);
}
// 3. Token bearer. A chave Webservice nunca sai do servidor.
$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.'));
}Uma última subtileza merece ser notada: um erro de ferramenta não se devolve como erro JSON-RPC. A especificação distingue os «protocol errors» dos «tool execution errors», devendo estes últimos voltar no resultado com isError: true para que o modelo se possa corrigir. Pedir o produto 9999 devolve portanto um 200 HTTP contendo o texto «Produto 9999 não encontrado.».
O servidor lança-se com o servidor integrado do PHP:
php -S 127.0.0.1:8110 server.phpAs respostas abaixo foram todas registadas nesta instância.
| Pedido | Resposta |
|---|---|
GET /mcp | 405 |
POST /mcp sem token | 401, -32001 |
Origin: https://exemple-malveillant.test | 403 |
initialize (protocolVersion 2025-06-18) | 200, versão devolvida de forma idêntica |
tools/list | 200, quatro ferramentas, 1 928 bytes |
notifications/initialized (sem id) | 202, corpo vazio |
MCP-Protocol-Version: 1900-01-01 | 400, -32022 com a lista das versões geridas |
Ligar o Codex sem tocar na sua configuração
O Codex lê o seu ficheiro ~/.codex/config.toml, mas também aceita chaves de configuração ao voo com -c. É a boa forma de experimentar um servidor: nada é escrito em disco, e o ensaio não sobrevive ao comando. A impressão digital do meu config.toml era aliás idêntica antes e depois das quatro execuções abaixo. O token, esse, fica numa variável de ambiente em vez de na linha de comandos: a documentação do Codex prevê bearer_token_env_var para isso.
export MCP_TOKEN="…" # token do servidor MCP, nunca a chave 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."A minha primeira versão do servidor não passava. O Codex via bem o servidor, listava bem as quatro ferramentas, mas cada chamada parava em:
mcp: ps/stock_alerts (failed)
MCP tool call requires approval, but approval policy is neverO reflexo é procurar a definição do Codex que desbloqueia. É o sítio errado. Cruzei as duas variáveis em quatro execuções, mesmo comando, mesmo modelo, mesma loja, só o servidor mudava, com ou sem anotações.
| Anotações do servidor | default_tools_approval_mode | Resultado |
|---|---|---|
readOnlyHint | não especificado (por defeito) | completed |
readOnlyHint | "writes" | completed |
| nenhuma | não especificado (por defeito) | failed |
| nenhuma | "writes" | failed |
A coluna que decide é a das anotações, ou seja o protocolo, e não a definição do cliente. A especificação MCP prevê anotações facultativas que descrevem o comportamento de uma ferramenta, ao declarar readOnlyHint, o servidor diz ao cliente que nenhuma chamada altera o estado remoto, e o Codex acredita nisso sem que seja preciso ajustar-lhe seja o que for. Quatro linhas bastam.
// As quatro ferramentas são só de leitura: declara-se isso de uma vez por todas.
// É esta anotação que os clientes leem para decidir se é preciso pedir
// uma aprovação ao utilizador antes de cada chamada.
$readOnly = [
'readOnlyHint' => true,
'destructiveHint' => false,
'idempotentHint' => true,
'openWorldHint' => false,
];Com elas, o mesmo comando é bem-sucedido.
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.Duas chamadas de ferramenta, 11 263 tokens. Uma precisão de segurança impõe-se no entanto, e a própria especificação a formula: os clientes «MUST consider tool annotations to be untrusted unless they come from trusted servers». Uma anotação readOnlyHint é uma declaração do servidor, não uma garantia técnica. Esta era verdadeira porque tinha acabado de escrever o servidor. Não substitui nem a chave limitada a GET, nem o cliente PHP sem método de escrita. Vem depois, e para um servidor de terceiros não vale nada.
E no Claude Code?
A declaração faz-se num comando, com o token deixado sob a forma de variável para que o ficheiro seja versionável.
claude mcp add --scope project --transport http ps http://127.0.0.1:8110/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'O ficheiro .mcp.json escrito na raiz do projeto contém mesmo ${MCP_TOKEN}, e não o valor do token.
{
"mcpServers": {
"ps": {
"type": "http",
"url": "http://127.0.0.1:8110/mcp",
"headers": {
"Authorization": "Bearer ${MCP_TOKEN}"
}
}
}
}Um servidor de âmbito de projeto não fica no entanto ativo. claude mcp list mostra-o em espera, e a documentação explica porquê: «Claude Code prompts for approval in interactive sessions before using project-scoped servers from .mcp.json files». É o comportamento correto, um repositório clonado nunca deveria ligar um servidor sozinho.
ps: http://127.0.0.1:8110/mcp (HTTP) - ⏸ Pending approval (run `claude` to approve)A abordagem é a mesma que para um servidor MCP destinado ao WordPress, com a diferença de que o WordPress fornece agora uma API de abilities e um adaptador oficial, ao passo que o PrestaShop deixa o Webservice como base.
Duas armadilhas que o agente nunca verá
A raiz do Webservice em JSON responde 500. No PrestaShop 9.1.4 com PHP 8.5, GET /api/?output_format=JSON devolve um 500 quando a mesma raiz em XML devolve um 200. A falha 34794 do repositório PrestaShop descreve exatamente isto, «Uncaught TypeError: array_filter()» na saída JSON, e continua aberta: reportada a 9 de dezembro de 2023, última atividade a 10 de agosto de 2026. Nunca deixes portanto um agente descobrir os recursos disponíveis pela raiz JSON, codifica a lista.
O desfasamento de fuso horário é silencioso, e é o pior. O PrestaShop escreve as suas datas no fuso da loja (PS_TIMEZONE, aqui Europe/Paris), enquanto o PHP que executava o meu servidor corria em UTC. A minha primeira versão de orders_summary limitava a janela a now() - 30 dias: devolveu zero encomendas numa loja que contava cinco. Nenhum erro, nenhum aviso, um zero perfeitamente credível, que o agente teria reportado tal como estava. A correção consiste em limitar a dias completos.
// O PrestaShop escreve as suas datas no fuso da loja (PS_TIMEZONE), não no
// do processo PHP que executa este servidor. Limita-se portanto a dias
// completos, senão algumas horas de encomendas desaparecem sem aviso.
$from = (new DateTimeImmutable('today -' . $days . ' days'))->format('Y-m-d 00:00:00');
$to = (new DateTimeImmutable('tomorrow'))->format('Y-m-d 00:00:00');É a classe de erro mais cara com um agente: a que produz uma resposta plausível. Um servidor MCP que agrega deve ser testado sobre dados cuja contagem já se conhece de antemão.
Para que serve, na prática
Três usos fazem sentido com estas quatro ferramentas só de leitura. A auditoria de fichas primeiro: get_product já devolve a lista de lacunas, o agente só tem de percorrer e agrupar. A vigilância das ruturas depois, com stock_alerts chamado por um agente programado em vez de por um humano que abre o back-office. A preparação de um projeto de conteúdo por fim: identificar as cinquenta fichas a rever é um trabalho de leitura, reescrevê-las à escala é outro, que cabe a um módulo dedicado como o WizardAI.
Não acrescentes uma quinta ferramenta que escreve. No dia em que precisares disso, escreve um segundo servidor, com a sua chave, o seu token e a sua política de aprovação. Dois servidores separados valem mais do que um servidor cuja metade das ferramentas é perigosa.
A reter
- A PrestaShop publica de facto um módulo MCP oficial desde abril de 2026, proprietário e ligado ao seu OAuth, os quatro projetos comunitários encontrados estão todos abandonados desde a sua semana de criação.
- A segurança assenta em três camadas independentes: chave Webservice limitada a
GETsobre nove recursos, cliente PHP sem método de escrita, token MCP distinto e revogável. - Uma ferramenta deve responder a uma pergunta, não retransmitir uma API: 1 137 bytes de texto útil contra 139 350 bytes de XML em bruto para o mesmo catálogo.
- Sem anotação
readOnlyHint, o Codex recusa chamar uma ferramenta em modo não interativo, seja qual for a definição de aprovação. É o protocolo que desbloqueia, não o cliente, mas uma anotação continua a ser uma declaração do servidor, não uma garantia. - Testa as tuas agregações sobre dados cuja contagem conheces: um desfasamento de fuso devolve um zero credível que ninguém verificará.
Erros frequentes
GET /api/?output_format=JSON responde 500 no PrestaShop 9.1.4 com PHP 8.5 (falha aberta desde dezembro de 2023) enquanto a mesma raiz em XML responde 200. Codifica a lista de recursos em vez de a deixares descobrir.PS_TIMEZONE, o teu PHP pode estar a correr em UTC. A janela now() - 30 dias devolveu zero encomendas numa loja que contava cinco. Limita a dias completos.readOnlyHint, o Codex recusa a chamada em modo não interativo: MCP tool call requires approval, but approval policy is never. Verificado em quatro execuções cruzadas: nenhuma definição de default_tools_approval_mode substitui a anotação.bearer_token_env_var do lado do Codex e ${MCP_TOKEN} do lado do Claude Code, para que o ficheiro fique versionável.

