Większość poradników o Model Context Protocol zakłada, że budujesz serwer przed sklepem lub bazą danych, czymś z zamówieniami i stanem magazynowym. My zrobiliśmy rzecz mniej oczywistą: zamieniliśmy statyczną stronę marketingową w działający serwer MCP tylko do odczytu. Każdy klient MCP może teraz wysłać POST na pojedynczy punkt końcowy i zapytać, jakie usługi istnieją albo jak rozpocząć zapytanie, w typowanym JSON-RPC, bez parsowania ani jednej strony HTML.
To relacja praktyka z tego wdrożenia. Jest celowo wąska: dwa narzędzia tylko do odczytu, żadnej bazy danych, żadnych zapisów. Ta wąskość jest tu istotą, i większość ciekawych decyzji z niej wynika. Poniżej wyjaśniamy, dlaczego witryna treściowa w ogóle zyskuje na punkcie końcowym MCP, dlaczego ten punkt końcowy jest funkcją Cloudflare Pages, a nie trasą frameworka, dlaczego napisaliśmy JSON-RPC ręcznie zamiast sięgać po SDK, oraz jeden błąd z ukośnikiem końcowym, który po cichu psuje każdy POST, jeśli go przeoczysz.
Po co umieszczać serwer MCP na stronie marketingowej
Sklep wystawia MCP, bo agent musi wykonać akcję: sprawdzić stan magazynu, zbudować koszyk, złożyć zamówienie. Strona marketingowa nie ma koszyka. Po co więc powierzchnia narzędziowa?
Uczciwa odpowiedź to odkrywanie przez agentów. Nasza strona już wcześniej miała warstwę odkrywania dla agentów: plik llms.txt, zestaw dokumentów .well-known oraz kartę serwera MCP pod /.well-known/mcp/server-card.json. Ta karta opisywała serwer, jego transport i możliwości. Deklarowała nawet możliwość tools. Był jeden problem: za nią nic nie stało. Karta rozgłaszała serwer, który nie istniał. Agent, który poszedł za kartą i próbował się połączyć, nie dostawał nic.
To częsty stan tych plików. Zostają wygenerowane, zostają rozgłoszone w nagłówku Link, a potem wskazują na zasób, który nigdy nie powstał. Karta jest obietnicą bez implementacji.
Celem nie było więc wymyślenie nowej powierzchni. Było nim uczynienie istniejącej, rozgłaszanej powierzchni realną. Narzędzia wynikają wprost z tego, do czego służy strona marketingowa:
check_serviceszwraca katalog usług: id, nazwę, opis, kategorię i kanoniczny adres URL.request_quotezwraca zlokalizowany adres URL kontaktu oraz instrukcje przesłania zapytania.
Dwa narzędzia, oba tylko do odczytu. Agent może teraz wyliczyć, co robimy, i podać użytkownikowi właściwy kolejny krok, deterministycznie, zamiast zgadywać z prozy.
Decyzja architektoniczna: funkcja Pages, a nie trasa Astro
Strona jest zbudowana w Astro w trybie wyjścia statycznego. Nie ma adaptera SSR. Każda podstrona jest prerenderowana do HTML w czasie builda, a pliki w stylu API, które serwuje (JSON katalogu usług, profil agenta), to pliki statyczne w public/, a nie trasy serwerowe.
Ten jeden fakt napędza cały projekt. Dynamiczny punkt końcowy, który czyta ciało żądania i zwraca wyliczoną odpowiedź, nie może tu być trasą Astro, bo nie ma serwera, który by ją uruchomił. Dodanie go oznaczałoby przełączenie trybu wyjściowego na hybrydowy lub serwerowy i podpięcie adaptera, co jest dużą, ryzykowną zmianą w buildzie, który już produkuje tysiące podstron.
Strona jednak już uruchamia ręcznie napisane funkcje Cloudflare Pages: middleware obsługujące przekierowania oraz kilka małych handlerów API. To właśnie ten szew. Dynamiczny punkt końcowy MCP to po prostu jeszcze jedna funkcja Pages, functions/mcp.ts, stojąca obok pozostałych. Wdraża się w ramach zwykłego builda, nie wymaga adaptera i nic nie zmienia w sposobie renderowania podstron.
Wniosek się uogólnia. Gdy chcesz dodać jeden dynamiczny punkt końcowy do skądinąd statycznej strony, nie sięgaj po tryb serwerowy frameworka. Sięgnij po prymityw funkcji brzegowej, który twój host już oferuje. W Cloudflare Pages jest to funkcja. Kosztem jest jeden plik, a nie przebudowa modelu renderowania.
Ręcznie napisany JSON-RPC bije SDK przy dwóch narzędziach
Odruch, gdy czytasz “serwer MCP”, to zainstalować @modelcontextprotocol/sdk. Dla serwera stdio z wieloma narzędziami, zasobami i promptami ten odruch jest słuszny. Dla dwóch narzędzi tylko do odczytu na funkcji brzegowej już nie.
Protokół transmisji MCP to JSON-RPC 2.0. Serwer, który odpowiada klientowi MCP, musi obsłużyć niewielki zestaw metod:
initialize, gdzie serwer zwraca wersję protokołu, możliwości i tożsamość.tools/list, gdzie zwraca definicje narzędzi wraz z ich JSON Schema.tools/call, gdzie uruchamia nazwane narzędzie i zwraca wynik opakowany w treść MCP.- powiadomienia takie jak
notifications/initialized, które nie niosą id i nie oczekują odpowiedzi.
To cała powierzchnia dla serwera tylko do odczytu. Ręczna jej implementacja to mniej więcej 120 linii. Dyspozytor to zwykły switch:
switch (method) {
case "initialize":
return ok({ protocolVersion, capabilities: { tools: { listChanged: false } }, serverInfo });
case "tools/list":
return ok({ tools: TOOLS });
case "tools/call":
return ok({ content: [{ type: "text", text: callTool(name, args, services).text }] });
default:
return err(-32601, `Method not found: ${method}`);
}
Zważ to na wadze przeciw SDK. SDK jest zaprojektowane pod stdio i pod pełny protokół. Na środowisku brzegowym dziedziczysz jego założenia dotyczące bundlowania oraz jego maszynerię transportową dla funkcji, których nie używasz. Dla dwóch narzędzi odczytu zależność, o której musisz myśleć, jest gorszym kompromisem niż 120 linii, które w pełni kontrolujesz. To ta sama powściągliwość najpierw-tylko-do-odczytu, zastosowana do zależności: wystawiaj minimum, posiadaj minimum.
Jedną rzeczą, którą warto zrobić ręcznie starannie, jest kontrakt błędów. JSON-RPC ma zdefiniowane kody błędów, a klienci MCP ich oczekują: -32700 dla błędu parsowania, -32601 dla nieznanej metody, -32602 dla błędnych parametrów. Trafienie w nie sprawia, że ręcznie napisany serwer zachowuje się wobec rygorystycznego klienta jak prawdziwy.
Najpierw tylko do odczytu to decyzja o bezpieczeństwie, nie ograniczenie
Kuszące trzecie narzędzie to takie, które przechwytuje kontakt: bierze imię, e-mail i wiadomość, i przesyła je do nas mailem. Nie buduj tego jako otwartego narzędzia MCP.
Publiczny punkt końcowy z narzędziem zapisującym to cel dla spamu. Każdy w internecie może wywołać tools/call z submit_quote i sfałszowanym ładunkiem, i teraz twoja skrzynka, twój CRM czy baza danych są celem bez żadnego tarcia i bez człowieka w pętli. W chwili, gdy narzędzie zapisuje, punkt końcowy potrzebuje uwierzytelniania, ograniczania częstości i obsługi nadużyć, co jest dużą powierzchnią do obrony dla strony marketingowej.
Dlatego request_quote nic nie zapisuje. Zwraca zlokalizowany adres URL kontaktu oraz ustrukturyzowane wskazówki:
if (name === "request_quote") {
const lang = LOCALES.includes(String(args?.lang)) ? String(args.lang) : "en";
return { text: JSON.stringify({
contact_url: CONTACT_URLS[lang],
method: "web-form",
note: "Read-only endpoint. Submit the inquiry through the contact form; this tool does not send it for you.",
reply_time: "within one working day",
}, null, 2) };
}
Agent dostaje wszystko, czego potrzebuje, by poprowadzić użytkownika dalej: właściwą stronę kontaktu dla języka użytkownika oraz jasne stwierdzenie, że przesłanie odbywa się przez formularz. Człowiek pozostaje w pętli. Punkt końcowy nie ma czego nadużyć.
To nie jest słabszy projekt wymuszony ostrożnością. To ta sama postawa, którą polecamy klientom budującym MCP dla własnych systemów: najpierw tylko do odczytu, dodawaj zapisy dopiero za uwierzytelnianiem, gdy jest ku temu konkretny powód. Serwer tylko do odczytu jest szczery co do tego, czym jest, i można go bezpiecznie zostawić otwartym.
| Wybór projektowy | Otwarte narzędzie zapisu i odczytu | Przekazanie tylko do odczytu |
|---|---|---|
| Wymagane uwierzytelnianie | Tak | Nie |
| Powierzchnia dla spamu | Wysoka | Brak |
| Człowiek w pętli | Opcjonalnie | Zawsze |
| Kod do obrony | Ograniczanie częstości, walidacja, obsługa nadużyć | Brak |
| Dopasowanie do publicznej strony marketingowej | Słabe | Dobre |
Pułapka ukośnika końcowego, która porzuca ciało twojego POST-a
Ta kosztowała realny czas debugowania, więc warto ją wyłożyć wprost.
Nasza strona kanonizuje adresy URL z ukośnikiem końcowym. Middleware przekierowuje z kodem 301 każdą ścieżkę bez niego na wersję z ukośnikiem. Dla podstron to w porządku. Dla punktu końcowego JSON-RPC to cicha katastrofa.
POST /mcp trafia na przekierowanie i zwraca 301 na /mcp/. Wielu klientów HTTP, podążając za przekierowaniem, nie wysyła ponownie ciała POST-a albo obniża metodę. Klient MCP widzi pustą lub błędną odpowiedź i wnioskuje, że serwer jest zepsuty. Serwer jest sprawny. Żądanie po prostu nigdy nie dotarło ze swoim ciałem.
Naprawą nie jest walka z middleware. Jest nią rozgłaszanie adresu URL, który strona faktycznie serwuje. Karta serwera MCP oraz każde odwołanie w warstwie odkrywania wskazują na /mcp/, z ukośnikiem końcowym, tak że dobrze zachowujący się klient wysyła POST bezpośrednio na kanoniczny adres URL i nigdy nie dotyka przekierowania. Jeśli twój framework lub host normalizuje ukośniki, zdecyduj, która forma jest kanoniczna, i rozgłaszaj dokładnie tę formę wszędzie.
Działający punkt końcowy możesz zweryfikować tak samo jak my:
curl -s -X POST https://wppoland.com/mcp/ \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
POST bez ukośnika na ten sam host pokaże ci za to 301.
Jak to wpisuje się w gotowość na agentów i GEO
Generative engine optimization to głównie kwestia bycia czytelnym i weryfikowalnym dla modeli. Dane strukturalne, jasne encje, spójne twierdzenia na twoich własnych powierzchniach i tych zewnętrznych. Działający punkt końcowy MCP to mocna wersja tej czytelności: to nie wskazówka o twojej treści, to wywoływalny interfejs do niej.
Dwie rzeczy sprawiają, że jest wart niewielkiego wysiłku, nawet gdy adopcja jest wciąż wczesna. Po pierwsze, domyka lukę między tym, co rozgłasza warstwa odkrywania, a tym, co istnieje. Karta wskazująca na działający serwer to spójny sygnał; karta wskazująca na nic to sygnał zepsuty, a zepsute sygnały są gorsze niż ich brak. Po drugie, dla każdego, kto sprzedaje tę usługę, punkt końcowy jest dowodem. Budujemy serwery MCP dla klientów, a najbardziej wiarygodną demonstracją tego jest publiczny, tylko do odczytu serwer działający na naszej własnej domenie, obok otwartoźródłowego serwera MCP dla WooCommerce, który publikujemy.
Żaden duży dostawca AI nie zobowiązuje się dziś formalnie do czytania kart serwera MCP ani llms.txt. To uczciwe zastrzeżenie i mówimy je wprost. Punkt końcowy jest tani w utrzymaniu, czyni rozgłaszaną obietnicę prawdziwą i jest działającym artefaktem, a nie deklaracją. Te trzy razem przekraczają próg opłacalności.
Co świadomie pominęliśmy
Krótka lista, bo wiedza o tym, co build pomija, jest równie użyteczna jak wiedza o tym, co zawiera.
- Na razie brak narzędzia
get_case_studies. Agent może odczytać studia przypadków zllms.txti podlinkowanych stron. Dodamy narzędzie, gdy poprosi o nie realny klient, a nie wcześniej. - Brak możliwości
resourcesczyprompts. Serwer deklaruje jedynietools, bo tylko to implementuje. Deklarowanie możliwości, której nie serwujesz, jest gorsze niż jej pominięcie: klient wywołującyresources/listdostaje błąd zamiast czystego “niewspierane”. - Brak SDK, jak opisano wyżej.
- Brak zapisów, jak opisano wyżej.
Każde pominięcie to decyzja, a nie przeoczenie. Punkt końcowy robi dokładnie to, czego wymagają dwie intencje, i nic więcej, dlatego jest na tyle mały, by ogarnąć go w jednym posiedzeniu, i na tyle bezpieczny, by zostawić go otwartym na internet.
Dokąd dalej
Jeśli chcesz pełniejszej wersji tych decyzji, klaster wokół tego wpisu pokrywa sąsiedni grunt: budowa serwera MCP dla WooCommerce dla przypadku ze stanem i obsługą sklepu, MCP kontra REST: kiedy co wygrywa dla pytania, czy w ogóle potrzebujesz MCP, oraz wzorce uwierzytelniania MCP na moment, w którym faktycznie dodajesz narzędzie zapisujące i potrzebujesz uwierzytelniania. Komercyjna wersja tej pracy mieszka na stronie usługi tworzenie serwerów MCP.
Krótka wersja mieści się w jednym zdaniu: strona marketingowa może być serwerem MCP, punkt końcowy to jedna funkcja brzegowa, każde narzędzie utrzymuj jako tylko do odczytu, a rozgłaszaj adres URL, który twój host faktycznie serwuje.







