Lokalny system RAG, który zbudowałem na początku roku, ma serwer MCP na porcie 7861 z narzędziami rag_search, rag_save i kilkoma narzędziami do grafu wiedzy. Dla Claude Code działającego na tym samym komputerze albo w sieci lokalnej działa to dobrze. Z telefonu albo z claude.ai w przeglądarce na innym urządzeniu jest bezużyteczny, bo nigdy nie musiał wychodzić poza moją sieć.
Chciałem, żeby samo claude.ai mogło odpytywać tę bazę wiedzy jako custom connector. Nie VPN, nie przekierowany port na routerze, tylko URL, który claude.ai może dodać i wobec którego może się uwierzytelnić z dowolnego miejsca. Okazało się, że to większa robota, niż sugeruje fraza “dodaj connector”.
Łańcuch
Cztery przeskoki między claude.ai a kontenerem, który faktycznie wykonuje pracę.
graph LR
A["claude.ai
custom connector"] -->|"OAuth 2.1 + DCR"| B["mcp.tomakl.dev
AI Controls MCP Portal"]
B -->|"service token bypass"| C["rag-mcp.tomakl.dev
Cloudflare Access app"]
C -->|"Cloudflare tunnel"| D["rag-mcp container
port 7861"]
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#f3e5f5,stroke:#6a1b9a
style C fill:#fff3e0,stroke:#e65100
style D fill:#c8e6c9,stroke:#2e7d32
mcp.tomakl.dev to Cloudflare “AI Controls” MCP Server Portal. rag-mcp.tomakl.dev to osobna, zwykła aplikacja Cloudflare Access chroniąca właściwe źródło. Kontener za nią to ten sam z pierwszego posta, nadal na porcie 7861, nadal osiągalny przez tunel Cloudflare, więc na moim routerze żaden port nigdy nie musi być przekierowany. Trzy z tych czterech elementów już istniały. Doprowadzenie claude.ai do faktycznego połączenia przez wszystkie z nich zajęło tydzień wieczorów.
Serwer nie ma pojęcia, że to wszystko istnieje
W pierwszym poście wspomniałem, że integracja MCP przez Gradio była wygodna, ale krucha, i że pewnie kiedyś przejdę na coś bardziej przewidywalnego. To “kiedyś” właśnie nastąpiło. Serwer to teraz mały serwis w Pythonie oparty na FastMCP i ma dokładnie zero wbudowanego uwierzytelniania. Żadnego sprawdzania klucza API, żadnego ciasteczka sesji, nic. Jeśli możesz dobić do portu 7861, możesz wywołać rag_save i zapisać coś w mojej bazie wiedzy.
To celowe uproszczenie, nie przeoczenie. Cała ochrona żyje na brzegu Cloudflare, nie w aplikacji. Zadaniem aplikacji jest szybko i poprawnie odpowiadać na wywołania MCP. Uwierzytelnianie to problem kogoś innego, a w tym układzie tym kimś innym jest Cloudflare Access.
Dlaczego ściana logowania nie wystarczy
Moja pierwsza próba była oczywista: postawić rag-mcp.tomakl.dev za standardową aplikacją Cloudflare Access z jedną polityką, dopuścić mój e-mail, gotowe. Dokładnie tak chroniona jest interfejs wyszukiwania dla ludzi i tam działa świetnie, bo jestem człowiekiem klikającym przez przekierowanie w przeglądarce.
Konfiguracja custom connectora w claude.ai tak nie działa. Interfejs connectora daje pole na URL i, jeśli serwer wymaga uwierzytelnienia, przepływ OAuth. Nie ma pola na statyczny bearer token ani na własny nagłówek. A claude.ai nie może przeklikać się przez interaktywną stronę logowania tak jak ja, bo po drugiej stronie tego konkretnego żądania nie siedzi człowiek przy klawiaturze. Trafia na ścianę logowania i się zatrzymuje.
To nie jest błąd w mojej konfiguracji. To znana luka, zgłoszona jako otwarty issue w repozytorium claude-ai-mcp na GitHubie Anthropic (issue #10, wciąż otwarty w połowie 2026). Zwykła ściana logowania Access jest zbudowana dla ludzi. Connector claude.ai człowiekiem nie jest.
Dwie polityki i portal
Rozwiązanie ma trzy części i wszystkie trzy musiały działać naraz, żeby cokolwiek zadziałało.
Po pierwsze, aplikacja Access na rag-mcp.tomakl.dev dostaje drugą politykę obok istniejącej “dopuść mój e-mail”. Ta nowa to polityka service tokenu, decyzja typu non_identity, a nie sprawdzenie tożsamości. Istnieje wyłącznie po to, żeby konkretny zautomatyzowany klient mógł przedstawić client ID i secret zamiast się logować.
Po drugie, AI Controls MCP Server Portal na mcp.tomakl.dev jest skonfigurowany, żeby dobijać do tego źródła za pomocą service tokenu. Backend portalu nigdy nie widzi strony logowania. Uwierzytelnia się jako service token, przepływa obok ściany, przez którą moja własna sesja przeglądarki przechodzi interaktywnie, i przekazuje żądanie MCP dalej.
Po trzecie, i to jest część, która faktycznie liczy się dla claude.ai, portal dostarcza OAuth. Źródło nie ma własnego, to serwer FastMCP bez żadnej powierzchni uwierzytelniania. To portal wystawia /.well-known/oauth-authorization-server, obsługuje dynamiczną rejestrację klienta pod /register i uruchamia endpointy /authorize i /token, których oczekuje przepływ connectora claude.ai. Claude.ai rozmawia z portalem po OAuth. Portal rozmawia z prawdziwym źródłem po HTTP uwierzytelnionym service tokenem. Żadna ze stron nie musi wiedzieć, jak druga faktycznie działa.
Same dane uwierzytelniające service tokenu są wysyłane jako niestandardowe nagłówki:
| |
skonfigurowane pod typem uwierzytelniania “Custom headers” serwera MCP w AI Controls, wcale nie jako ustawienie OAuth. To najprostszy możliwy mechanizm siedzący pod tym najbardziej wymyślnym.
Błąd, który pochłonął wieczór
Za każdym razem, gdy odtwarzałem obiekt serwera MCP w AI Controls, co zdarzyło się więcej niż raz, zanim dopracowałem polityki, zapisywał się cicho z pustą listą allowed_idps. Sama polityka Access wyglądała w panelu na całkowicie poprawną. Dobry e-mail, dobra decyzja, dobra aplikacja. A mimo to claude.ai nie mogło się połączyć, zgłaszając “No allowed servers available, check your Zero Trust policies”, czyli dokładnie taki komunikat błędu, który każe wracać do ponownego czytania polityki, która nigdy nie była problemem.
Spędziłem więcej czasu, niż chciałbym przyznać, sprawdzając tę politykę od nowa, zanim znalazłem prawdziwy mechanizm: interfejs panelu w ogóle nie pokazuje allowed_idps jako czegoś, co można zobaczyć albo poprawić bezpośrednio. Jedyny sposób, żeby faktycznie ją wypełnić, to bezpośrednie wywołanie API:
| |
Ustaw allowed_idps jawnie w body i to, co wyglądało na zepsute na wszystkie możliwe sposoby, po prostu zaczyna działać. Nic w panelu nie mówi, że to jest rozwiązanie. Jeśli serwer kiedyś znowu dostanie nowe app ID, to jest pierwsza rzecz, którą sprawdzę, nie ostatnia.
Dzień stracony przez coś, co nie było moją winą
W trakcie pierwszej próby wszystko przestało działać w sposób, który nie pasował do żadnej zmiany, jakiej dokonałem. Spaliłem większość dnia, zakładając, że gdzieś zepsułem politykę, zanim sprawdziłem stronę statusu Cloudflare i znalazłem “Workers AI experiencing degraded availability” dokładnie w tym oknie czasowym, w którym wszystko się posypało. Nic do naprawienia po mojej stronie, po prostu trzeba było poczekać. Rozsądne przypomnienie, że strona statusu to pięciosekundowe sprawdzenie i powinno przychodzić przed godziną ponownego czytania własnego YAML-a.
Potwierdzone działanie
Dowodem nie jest zielony ptaszek w panelu, tylko logi samego kontenera. Połączenie z claude.ai tworzy żywą sesję widoczną w czasie rzeczywistym: initialize, potem ListResourcesRequest, potem ListPromptsRequest, wszystko zwraca 200. To claude.ai po drugiej stronie internetu, przez portal, przez aplikację Access, przez tunel, trafiające w ten sam proces FastMCP, który odpowiada na zapytania Claude Code w mojej sieci lokalnej. Ten sam serwer, te same narzędzia, dwoje zupełnie różnych drzwi wejściowych.
Proces rag_save i memory compiler zapisują do tej bazy wiedzy, nie wiedząc i nie dbając o to, że cokolwiek z tego istnieje. O to właśnie chodzi w trzymaniu całego uwierzytelniania na brzegu. Serwer został dokładnie tak prosty, jak był przed rozpoczęciem tego wszystkiego. Wszystko, co się zmieniło, żyje w konfiguracji Cloudflare, nie w jednej linijce Pythona.
