MCP dla PrestaShop: podłączyć agenta bez oddawania mu kluczy

Serwer MCP w PHP, który owija API Webservice PrestaShop czterema narzędziami tylko do odczytu, napisany i przetestowany na sklepie 9.1.4, potem podłączony do Codex i zadeklarowany w Claude Code.

MCP dla PrestaShop: podłączyć agenta bez oddawania mu kluczy
Szybka odpowiedź

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.

php
// 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:

bash
# 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ę.

php
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.

php
// 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:

markdown
#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 vide

Zysk 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.

php
// 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:

bash
php -S 127.0.0.1:8110 server.php

Wszystkie 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.

bash
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:

markdown
mcp: ps/stock_alerts (failed)
MCP tool call requires approval, but approval policy is never

Odruchem 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.

php
// 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.

markdown
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ć.

bash
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.

json
{
  "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.

markdown
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.

php
// 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 GET na 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 readOnlyHint Codex 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

Odkrywanie zasobów przez korzeń JSON 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ć.
Ograniczanie agregacji do bieżącej godziny PrestaShop zapisuje swoje daty w 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.
Zapomnienie o adnotacjach narzędzia Bez 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.
Dawanie kluczowi Webservice więcej niż GET Uprawnienia ustawia się na zasób i na metodę. Klucz, który potrafi zapisywać, czyni bezużyteczną całą staranność włożoną w kod PHP.
Wpisywanie tokena w komendę albo w .mcp.json Używaj bearer_token_env_var po stronie Codex i ${MCP_TOKEN} po stronie Claude Code, żeby plik pozostał możliwy do wersjonowania.

Claude CodeCodexMCPPHPPrestaShopSécurité

Damien Flandrin Web developer od 2010 roku, twórca Gekkode i Email Impact. Każdy artykuł jest sprawdzany na prawdziwym projekcie przed publikacją. Kontakt
Newsletter

Nowe testy, poradniki i projekty, e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.