
PrestaShop wydaje od kwietnia 2026 oficjalny moduł MCP, własnościowy i powiązany z jego OAuth. Żeby zachować kontrolę, napisz serwer MCP w PHP, który owija API Webservice kluczem ograniczonym do GET i kilkoma narzędziami, które odpowiadają na pytanie, zamiast przekazywać API dalej. Licz się z około 650 liniami PHP i trzema niezależnymi warstwami odmowy.
Podłączenie agenta kodu do sklepu PrestaShop jest kuszące: czytałby katalog, wyszukiwał niekompletne karty produktów i braki magazynowe zamiast Ciebie. Ryzyko nie bierze się z jego nieostrożności, tylko z powierzchni, którą mu otwierasz. Ten tutorial buduje serwer MCP w PHP, który udostępnia tylko cztery odczyty, i przez który żaden zapis nie może przejść.
Czy istnieje już serwer MCP dla PrestaShop?
Tak, od wiosny 2026. PrestaShop wydaje własny moduł, PrestaShop MCP Server, którego publiczna dokumentacja wprost ogłasza, że wydawca „holds all associated intellectual property rights” i przyznaje licencję osobistą, niewyłączną i nieprzenoszalną. Strona wprowadzająca, którą otworzyłem 7 września 2026, pokazuje ostatnią aktualizację na 30 kwietnia 2026. Dla dewelopera liczą się dwa ograniczenia: moduł nie jest open source, a jego uwierzytelnianie „works exclusively with PrestaShop OAuth”.
Datowany i niezależny dowód jego istnienia: pakiet prestashop/ps-mcp-server-stubs opublikowany na Packagist w wersji 1.0.3 9 lipca 2026, licencja proprietary, opisany jako „IDE stubs for ps_mcp_server MCP attributes and exceptions”. Moduły zewnętrzne mogą zresztą doczepiać do niego własne narzędzia przez atrybuty PHP PsMcpTool, PsMcpSchema i PsMcpToolAnnotations.
Po stronie społeczności API GitHuba daje mniej zachęcający obraz. Cztery repozytoria znalezione 7 września 2026 nie otrzymały ani jednego commita po tygodniu od utworzenia.
| Repozytorium | Język | Licencja | Utworzone | Ostatni push | Gwiazdki |
|---|---|---|---|---|---|
latinogino/prestashop-mcp | Python | MIT | 30/06/2025 | 30/06/2025 | 8 |
promokit/prestashop-mcp | TypeScript | brak | 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 |
Żaden nie został tu uruchomiony, i to celowo: wybór, który się liczy, rozgrywa się między powierzchnią opanowaną a narzuconą, nie między oficjalnym a społecznościowym. Serwer, który sam piszesz, mieści się w trzech plikach PHP, i wiesz, linia po linii, co on potrafi.
Dlaczego Webservice, a nie baza danych?
PrestaShop od dawna udostępnia API REST, Webservice, „a CRUD API” według dokumentacji deweloperskiej wersji 9. Jego wartość tutaj tkwi w kontroli dostępu bardziej niż w bogactwie modelu, bo klucz liczący trzydzieści dwa znaki otrzymuje uprawnienia na zasób i na metodę HTTP. Dokumentacja mówi to wprost: „you might want a user to have read and write access on some resources, but only read access on others”.
Odmowa nie jest więc zapisana w Twoim kodzie PHP, gdzie błąd programistyczny mógłby ją usunąć. Jest egzekwowana przez sklep, zanim Twój serwer w ogóle zaistnieje. To ta sama logika obrony w głąb, co ta opisana w artykule o sandboksie i uprawnieniach agentów kodu.
Klucz tworzy się w back-office (Parametry zaawansowane > Webservice) albo przez kod, klasą WebserviceKey i jej metodą setPermissionForAccount(). Do laboratorium stworzyłem go w SQL, bo nie miałem przeglądarki w pętli. Przyznane uprawnienia: tylko GET, na dziewięciu zasobach.
// Uprawnienia przyznane kluczowi laboratorium: nic poza GET.
$permissions = [];
foreach (['products', 'categories', 'stock_availables', 'orders', 'order_states',
'combinations', 'manufacturers', 'languages', 'currencies'] as $resource) {
$permissions[$resource] = ['GET' => 1];
}Weryfikacja nie odbywa się na słowo. Wystarczą trzy żądania, klucz przekazywany jako nazwa użytkownika HTTP Basic z pustym hasłem:
# Zasób autoryzowany: 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}]}
# Zasób nieobecny w uprawnieniach: 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"}]}
# Zapis na zasobie mimo to autoryzowanym do odczytu: 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 zwraca to samo 405. A cena produktu 1 wynosiła 23,90 € przed tymi próbami, 23,90 € po. To jedyna kontrola, która się liczy.
Krok 1, klient Webservice, który potrafi tylko czytać
Druga bariera jest w kodzie. Klasa, która rozmawia ze sklepem, udostępnia tylko jedną metodę publiczną, get(): nawet gdyby klucz pewnego dnia przez pomyłkę otrzymał uprawnienia do zapisu, serwer MCP nie miałby żadnego sposobu, żeby z nich skorzystać. Wymusza też output_format=JSON, zobaczymy dalej, że ten szczegół ma swoją cenę.
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 . ':', // klucz jako identyfikator, puste hasło
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_FOLLOWLOCATION => false,
]);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
// 404 na filtrowanej kolekcji = zero wyników, nie awaria.
if ($status === 404) {
return [];
}
if ($status !== 200) {
throw new RuntimeException(self::readableError((string) $body, $status));
}
return json_decode((string) $body, true) ?: [];
}
}Webservice ma własną składnię zapytań, którą trzeba sprawdzić element po elemencie, zanim wpisze się ją w kod. Te zostały zweryfikowane na PrestaShop 9.1.4: display=[id,name,price], żeby wybrać pola, filter[name]=%[colibri]% dla „zawiera”, filter[quantity]=[0,2000] dla przedziału, filter[id]=[1|2|3] dla „lub”, sort=[price_DESC], i date=1, które musi towarzyszyć każdemu filtrowi na date_add.
Krok 2, cztery narzędzia, które odpowiadają po francusku, nie w JSON
To tutaj rozgrywa się sedno sprawy, i wiele serwerów MCP to przegapia. Narzędzie nie powinno przelewać API wprost do kontekstu modelu, powinno odpowiadać na pytanie. Cztery narzędzia serwera to: search_products, get_product, orders_summary i stock_alerts. Każde zwraca krótki tekst i, równolegle, structuredContent dla kodu, który chciałby go odczytać ponownie.
get_product ilustruje ten pomysł: zamiast dostarczać surową kartę produktu, oblicza to, co agent musiałby inaczej wydedukować sam.
// Braki, które agent musi zobaczyć od razu, bez rozumowania nad JSON-em.
$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 sklepie demonstracyjnym wywołanie zwraca to, tekst, który człowiek czyta tak samo szybko jak model:
#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 videZysk da się zmierzyć. Na tym samym katalogu dziewiętnastu produktów, oto co ląduje w kontekście, zależnie od przyjętej metody.
| Co otrzymuje agent | Bajty |
|---|---|
GET /api/products?display=full (XML, format domyślny) | 139 350 |
GET /api/products?display=full&output_format=JSON | 41 126 |
Tekst zwrócony przez narzędzie search_products | 1 137 |
Sto dwadzieścia dwa razy mniej niż surowy XML. Serwer MCP, który zadowala się odtwarzaniem punktów wejścia API, każe klientowi płacić za całą tę różnicę, przy każdym wywołaniu. Rozumowanie jest rozwinięte w przewodniku o tworzeniu serwera MCP w PHP, który niesie też całą teorię protokołu, którą ten tutorial zakłada za znaną.
Krok 3, serwer HTTP: token, origin, metoda
Transport mieści się w jednym pliku i trzech odmowach. Lokalny serwer MCP nasłuchuje na pętli lokalnej, co nie chroni go przed stroną internetową otwartą w przeglądarce tej samej maszyny: stąd weryfikacja nagłówka Origin. Ponieważ rewizja 2026-07-28 specyfikacji zniosła strumień GET, wszystko idzie przez POST. A token bearer jest odrębny od klucza Webservice: można go odwołać, nie dotykając sklepu.
// 1. Origin: bez tej weryfikacji strona internetowa otwarta w przeglądarce
// tej maszyny może rozmawiać z lokalnym serwerem (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. Rewizja 2026-07-28 zniosła strumień GET: wszystko idzie przez POST.
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
header('Allow: POST');
respond(405, ['jsonrpc' => '2.0', 'error' => ['code' => -32600, 'message' => 'POST uniquement.']]);
}
// 3. Token bearer. Klucz Webservice nigdy nie opuszcza serwera.
$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.'));
}Ostatnia subtelność zasługuje na odnotowanie: błąd narzędzia nie zwraca się jako błąd JSON-RPC. Specyfikacja rozróżnia „protocol errors” od „tool execution errors”, te drugie muszą wracać w wyniku z isError: true, żeby model mógł się poprawić. Zapytanie o produkt 9999 zwraca więc 200 HTTP zawierające tekst „Produit 9999 introuvable.”.
Serwer uruchamia się wbudowanym serwerem PHP:
php -S 127.0.0.1:8110 server.phpWszystkie odpowiedzi poniżej zostały odnotowane na tej instancji.
| Żądanie | Odpowiedź |
|---|---|
GET /mcp | 405 |
POST /mcp bez tokena | 401, -32001 |
Origin: https://exemple-malveillant.test | 403 |
initialize (protocolVersion 2025-06-18) | 200, wersja zwrócona identycznie |
tools/list | 200, cztery narzędzia, 1 928 bajtów |
notifications/initialized (bez id) | 202, puste ciało |
MCP-Protocol-Version: 1900-01-01 | 400, -32022 z listą obsługiwanych wersji |
Podłączyć Codex, nie dotykając jego konfiguracji
Codex czyta swój plik ~/.codex/config.toml, ale akceptuje też klucze konfiguracji w locie, przez -c. To dobry sposób na wypróbowanie serwera: nic nie jest zapisywane na dysku, a próba nie przeżywa komendy. Suma kontrolna mojego config.toml była zresztą identyczna przed i po czterech wykonaniach poniżej. Token z kolei zostaje w zmiennej środowiskowej zamiast w linii poleceń: dokumentacja Codex przewiduje na to bearer_token_env_var.
export MCP_TOKEN="…" # token serwera MCP, nigdy klucz 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."Moja pierwsza wersja serwera nie przechodziła. Codex widział serwer, wyświetlał listę czterech narzędzi, ale każde wywołanie zatrzymywało się na:
mcp: ps/stock_alerts (failed)
MCP tool call requires approval, but approval policy is neverOdruchem jest szukanie ustawienia Codex, które odblokowuje. To złe miejsce. Skrzyżowałem obie zmienne na czterech wykonaniach, ta sama komenda, ten sam model, ten sam sklep, zmieniał się tylko serwer, z adnotacjami albo bez.
| Adnotacje serwera | default_tools_approval_mode | Wynik |
|---|---|---|
readOnlyHint | nieokreślone (domyślne) | completed |
readOnlyHint | "writes" | completed |
| brak | nieokreślone (domyślne) | failed |
| brak | "writes" | failed |
Kolumną, która decyduje, jest ta z adnotacjami, czyli protokół, a nie ustawienie klienta. Specyfikacja MCP przewiduje opcjonalne annotations opisujące zachowanie narzędzia, deklarując readOnlyHint, serwer mówi klientowi, że żadne wywołanie nie modyfikuje zdalnego stanu, a Codex mu wierzy, bez konieczności czegokolwiek ustawiać. Wystarczą cztery linie.
// Cztery narzędzia są tylko do odczytu: deklarujemy to raz na zawsze.
// To tę adnotację czytają klienci, żeby zdecydować, czy trzeba poprosić
// użytkownika o zatwierdzenie przed każdym wywołaniem.
$readOnly = [
'readOnlyHint' => true,
'destructiveHint' => false,
'idempotentHint' => true,
'openWorldHint' => false,
];Z nimi ta sama komenda kończy się sukcesem.
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.Dwa wywołania narzędzia, 11 263 tokeny. Konieczne jest jednak jedno zastrzeżenie bezpieczeństwa, i sama specyfikacja je formułuje: klienci „MUST consider tool annotations to be untrusted unless they come from trusted servers”. Adnotacja readOnlyHint to deklaracja serwera, a nie techniczna gwarancja. Ta akurat była prawdziwa, bo właśnie napisałem ten serwer. Nie zastępuje ani klucza ograniczonego do GET, ani klienta PHP bez metody zapisu. Przychodzi po nich, i dla zewnętrznego serwera nic nie jest warta.
A w Claude Code?
Deklaracja odbywa się w jednej komendzie, z tokenem pozostawionym w formie zmiennej, żeby plik dało się wersjonować.
claude mcp add --scope project --transport http ps http://127.0.0.1:8110/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Plik .mcp.json zapisany w katalogu głównym projektu zawiera rzeczywiście ${MCP_TOKEN}, a nie wartość tokena.
{
"mcpServers": {
"ps": {
"type": "http",
"url": "http://127.0.0.1:8110/mcp",
"headers": {
"Authorization": "Bearer ${MCP_TOKEN}"
}
}
}
}Serwer o zakresie projektu nie jest jednak przez to aktywny. claude mcp list pokazuje go jako oczekujący, a dokumentacja wyjaśnia dlaczego: „Claude Code prompts for approval in interactive sessions before using project-scoped servers from .mcp.json files”. To właściwe zachowanie, sklonowane repozytorium nigdy nie powinno samo podłączać serwera.
ps: http://127.0.0.1:8110/mcp (HTTP) - ⏸ Pending approval (run `claude` to approve)Podejście jest takie samo jak przy serwerze MCP przeznaczonym dla WordPressa, z tą różnicą, że WordPress dostarcza dziś API abilities i oficjalny adapter, podczas gdy PrestaShop pozostawia Webservice jako fundament.
Dwie pułapki, których agent nigdy nie zobaczy
Korzeń Webservice w JSON odpowiada 500. Na PrestaShop 9.1.4 z PHP 8.5, GET /api/?output_format=JSON zwraca 500, podczas gdy ten sam korzeń w XML zwraca 200. Defekt 34794 z repozytorium PrestaShop opisuje dokładnie to, „Uncaught TypeError: array_filter()” na wyjściu JSON, i wciąż jest otwarty: zgłoszony 9 grudnia 2023, ostatnia aktywność 10 sierpnia 2026. Nigdy więc nie pozwól agentowi odkrywać dostępnych zasobów przez korzeń JSON, zakoduj listę na sztywno.
Rozbieżność stref czasowych jest cicha, i to najgorsze. PrestaShop zapisuje swoje daty w strefie czasowej sklepu (PS_TIMEZONE, tutaj Europe/Paris), podczas gdy PHP wykonujący mój serwer działał w UTC. Moja pierwsza wersja orders_summary ograniczała okno do now() - 30 jours: zwróciła zero zamówień na sklepie, który miał ich pięć. Żadnego błędu, żadnego ostrzeżenia, doskonale wiarygodne zero, które agent zgłosiłby bez zmian. Poprawka polega na ograniczeniu do pełnych dni.
// PrestaShop zapisuje swoje daty w strefie czasowej sklepu (PS_TIMEZONE), nie w
// strefie procesu PHP, który wykonuje ten serwer. Ograniczamy więc do pełnych
// dni, inaczej kilka godzin zamówień znika bez śladu.
$from = (new DateTimeImmutable('today -' . $days . ' days'))->format('Y-m-d 00:00:00');
$to = (new DateTimeImmutable('tomorrow'))->format('Y-m-d 00:00:00');To najkosztowniejsza klasa błędu przy pracy z agentem: taka, która produkuje wiarygodną odpowiedź. Serwer MCP, który agreguje dane, musi być testowany na danych, których liczbę znasz z góry.
Do czego to konkretnie służy
Z tymi czterema narzędziami tylko do odczytu utrzymują się trzy zastosowania. Najpierw audyt kart produktów: get_product od razu zwraca listę braków, agentowi zostaje tylko przejrzeć i pogrupować. Potem nadzór nad brakami magazynowymi, z stock_alerts wywoływanym przez zaprogramowanego agenta zamiast przez człowieka otwierającego back-office. Wreszcie przygotowanie projektu treściowego: wyłowienie pięćdziesięciu kart do poprawy to praca polegająca na czytaniu, przepisanie ich na dużą skalę to już coś innego, co należy do dedykowanego modułu, takiego jak WizardAI.
Nie dodawaj piątego narzędzia, które zapisuje. Gdy pewnego dnia będziesz go potrzebować, napisz drugi serwer, z własnym kluczem, własnym tokenem i własną polityką zatwierdzania. Dwa osobne serwery są lepsze niż jeden serwer, którego połowa narzędzi jest niebezpieczna.
Co warto zapamiętać
- PrestaShop rzeczywiście wydaje oficjalny moduł MCP od kwietnia 2026, własnościowy i powiązany z jego OAuth, cztery znalezione projekty społecznościowe są wszystkie porzucone od tygodnia od ich utworzenia.
- Bezpieczeństwo trzyma się na trzech niezależnych warstwach: klucz Webservice ograniczony do
GETna dziewięciu zasobach, klient PHP bez metody zapisu, odrębny i odwoływalny token MCP. - Narzędzie musi odpowiadać na pytanie, nie przekazywać API dalej: 1 137 bajtów użytecznego tekstu wobec 139 350 bajtów surowego XML dla tego samego katalogu.
- Bez adnotacji
readOnlyHintCodex odmawia wywołania narzędzia w trybie nieinteraktywnym, niezależnie od ustawienia zatwierdzania. To protokół odblokowuje, nie klient, ale adnotacja pozostaje deklaracją serwera, nie gwarancją. - Testuj swoje agregacje na danych, których liczbę znasz: rozbieżność stref czasowych daje wiarygodne zero, którego nikt nie zweryfikuje.
Częste błędy
GET /api/?output_format=JSON odpowiada 500 na PrestaShop 9.1.4 z PHP 8.5 (defekt otwarty od grudnia 2023), podczas gdy ten sam korzeń w XML odpowiada 200. Zakoduj listę zasobów na sztywno, zamiast dawać ją odkrywać.PS_TIMEZONE, Twój PHP może działać w UTC. Okno now() - 30 dni zwróciło zero zamówień na sklepie, który miał ich pięć. Ograniczaj do pełnych dni.readOnlyHint, Codex odmawia wywołania w trybie nieinteraktywnym: MCP tool call requires approval, but approval policy is never. Zweryfikowane na czterech skrzyżowanych wykonaniach: żadne ustawienie default_tools_approval_mode nie zastępuje adnotacji.bearer_token_env_var po stronie Codex i ${MCP_TOKEN} po stronie Claude Code, żeby plik pozostał możliwy do wersjonowania.

