W poprzednich artykułach z naszej serii o MCP opisaliśmy budowę serwerów i klientów MCP w Rails. Skupiliśmy się na podstawach protokołu: sposobie udostępniania narzędzi przez serwery i korzystania z nich przez klientów. Nie poruszaliśmy jednak kwestii uwierzytelniania. W środowisku produkcyjnym sytuacja wygląda inaczej. Jeśli serwer MCP przechowuje wrażliwe dane biznesowe i wymaga kontroli dostępu opartej na rolach, potrzebujesz OAuth.
Dla programisty integracja OAuth z serwerem MCP nie jest szczególnie trudna. Generujesz client_id i client_secret, konfigurujesz klienta, a cały przepływ zaczyna działać. Serwery MCP nie są jednak przeznaczone wyłącznie dla programistów. Gdy użytkownicy biznesowi, na przykład zespoły sprzedaży, analitycy czy kierownicy projektów, chcą połączyć klienta AI takiego jak Claude z wewnętrznym narzędziem, wymaganie od nich obsługi danych uwierzytelniających OAuth tworzy niepotrzebną przeszkodę. Mają już login i hasło do aplikacji i korzystają z nich każdego dnia. Dlaczego podłączenie klienta AI miałoby wymagać czegoś więcej?
Specyfikacja MCP rozwiązuje ten problem przez automatyczne wykrywanie konfiguracji OAuth. W tym procesie klient AI sam odnajduje serwer autoryzacji, rejestruje się i w tle generuje własne dane uwierzytelniające. Użytkownik podaje tylko dwie rzeczy: adres URL serwera MCP oraz zwykłe dane logowania, których codziennie używa w aplikacji.
W tym artykule pokazuję, jak zaimplementować taki proces w aplikacji Rails i na co zwrócić uwagę podczas wdrażania go na produkcję.
Model WebsiteInit do oceny gotowości produkcyjnej
Zgodność z protokołem to dopiero pierwsza warstwa. Zanim udostępnię endpoint MCP rzeczywistym użytkownikom, oceniam integrację w pięciu obszarach: tożsamości, autoryzacji, rejestracji klientów, wykonywania narzędzi i operacji. Słabość w dowolnym z nich może zamienić poprawny proces OAuth w niebezpieczną funkcję produktu.
| Obszar | Decyzja do podjęcia | Dowody wymagane przed uruchomieniem |
|---|---|---|
| Tożsamość | Które istniejące konto służy do uwierzytelnienia użytkownika? | Testy logowania, wygasania sesji, odzyskiwania dostępu i wyłączonych kont |
| Autoryzacja | Które role mogą nawiązać połączenie i z jakich narzędzi mogą korzystać? | Macierz uprawnień przetestowana zarówno na poziomie kontrolera, jak i narzędzia |
| Rejestracja | O jakie URI przekierowań i zakresy może poprosić dynamicznie rejestrowany klient? | Testy list dozwolonych wartości i odrzucania złośliwych rejestracji |
| Wykonanie | Jak kontrolowane są narzędzia wykonujące działania destrukcyjne lub operujące na danych wrażliwych? | Walidacja danych wejściowych, zasady potwierdzania operacji, rejestry audytowe i limity żądań |
| Operacje | Jak zespół wykryje nadużycie albo awarię integracji z klientem? | Ustrukturyzowane logi, metryki błędów, mechanizm unieważniania dostępu i procedura reagowania na incydenty |
Najważniejsza zasada architektoniczna brzmi: OAuth nie zastępuje autoryzacji w aplikacji. Poprawny token Bearer potwierdza, że klient ukończył proces OAuth. Nie dowodzi jednak, że należy zezwolić na każde wywołanie narzędzia. Każde narzędzie MCP musi nadal egzekwować ograniczenia wynikające z przynależności do tenanta, roli użytkownika, dostępu do rekordów i reguł biznesowych, tak samo jak odpowiadająca mu operacja inicjowana przez użytkownika w standardowym interfejsie aplikacji.
Oddzielam też rejestrację od zaufania. Dynamic Client Registration umożliwia rozpoczęcie pracy bez wcześniejszego udostępniania danych uwierzytelniających, ale przyjęcie rejestracji nie może domyślnie przyznawać szerokich zakresów. Serwer powinien normalizować i sprawdzać żądane zakresy, ograniczać hosty przekierowań i liczbę rejestracji oraz zachowywać wystarczające metadane, aby później unieważnić dostęp problematycznego klienta.
Bezpieczniejsza kolejność wdrożenia
- Zacznij od jednego narzędzia tylko do odczytu i jednej roli wewnętrznej.
- Przetestuj pełny proces w rzeczywistym kliencie AI przez HTTPS, nie tylko za pomocą curl.
- Rejestruj rejestracje klientów, autoryzacje, użycie tokenów, wywołania narzędzi, odmowy dostępu i unieważnienia.
- Dodaj wyłącznik awaryjny, który dezaktywuje interfejs MCP bez wyłączania właściwego produktu.
- Rozszerzaj zakresy i role dopiero po przeanalizowaniu rzeczywistego użycia oraz przypadków awarii.
Takie podejście celowo traktuje integrację MCP jako odrębny obszar produktu, a nie tylko kolejny endpoint kontrolera. Ogranicza skutki ewentualnego błędu, gdy zespół dopiero poznaje sposób, w jaki klienci ponawiają żądania, buforują metadane, obsługują wygasłe tokeny i komunikują użytkownikom błędy autoryzacji.
Jak działa automatyczne wykrywanie OAuth w MCP
Specyfikacja MCP definiuje proces autoryzacji oparty na trzech dokumentach RFC. Razem eliminują one konieczność wcześniejszego udostępniania danych uwierzytelniających:
- RFC 9728 - Protected Resource Metadata: informuje klienta, gdzie znaleźć serwer autoryzacji
- RFC 7591 - Dynamic Client Registration: pozwala klientowi samodzielnie się zarejestrować i automatycznie uzyskać własne dane uwierzytelniające
- RFC 6750 - Bearer Token Usage: definiuje format nagłówka WWW-Authenticate, który uruchamia cały proces
Z perspektywy użytkownika wszystko wygląda prosto:
- Wprowadź adres URL serwera MCP w kliencie AI, na przykład
https://app.example.com/my_mcp - Poczekaj na otwarcie okna logowania
- Zaloguj się zwykłymi danymi do aplikacji
- Klient AI nawiązuje połączenie i zaczyna działać
W tle automatycznie wykonuje się dziewięć kroków:
- Klient AI wysyła żądanie do endpointu MCP
- Serwer odpowiada statusem
401 Unauthorizedi nagłówkiemWWW-Authenticatewskazującym metadane - Klient pobiera Protected Resource Metadata z
/.well-known/oauth-protected-resource - Klient pobiera Authorization Server Metadata z
/.well-known/oauth-authorization-server - Klient rejestruje się samodzielnie za pomocą Dynamic Client Registration pod adresem
registration_endpointodnalezionym w poprzednim kroku. Właśnie wtedy automatycznie otrzymuje własnyclient_id - Klient przekierowuje użytkownika do endpointu autoryzacji
- Użytkownik loguje się dotychczasowymi danymi, a serwer sprawdza jego rolę
- Serwer przekierowuje użytkownika z powrotem do klienta wraz z kodem autoryzacyjnym
- Klient wymienia kod na tokeny Bearer i rozpoczyna komunikację MCP
Kluczowy jest krok 5. Dzięki Dynamic Client Registration klient AI generuje własne dane uwierzytelniające na bieżąco. Nie wymaga to udziału administratora, udostępniania danych uwierzytelniających ani konfiguracji wykraczającej poza podanie adresu URL.

Implementacja procesu w Rails
Implementacja wymaga czterech elementów: endpointu MCP uruchamiającego proces, endpointów metadanych służących do wykrywania konfiguracji, endpointu rejestracji dynamicznych klientów oraz standardowego procesu autoryzacji OAuth. Pozostałe elementy OAuth, czyli endpoint tokenów, tokeny odświeżające i schemat bazy danych, wykorzystują standardowe wzorce OAuth 2.0 i nie są tutaj omawiane. Kompletne, działające rozwiązanie ze wszystkimi elementami znajdziesz w repozytorium mcp-oauth-demo. Jeśli Twoja aplikacja Rails używa już Doorkeeper, pamiętaj, że biblioteka nie obsługuje RFC 7591 (Dynamic Client Registration) od razu po instalacji. Niezależnie od tego komponent rejestracji będzie więc wymagał własnej implementacji.
W istniejącym produkcie takie prace rzadko ograniczają się do jednego kontrolera. Granice autoryzacji, znaczenie ról, przechowywanie tokenów, obserwowalność i strategia wdrożenia muszą pasować do reszty aplikacji. Konsultacje dotyczące architektury i bezpieczeństwa Rails pomagają przełożyć wymagania protokołu na plan wdrożenia produkcyjnego, zanim implementacja obejmie dużą część kodu.
Poniższe przykłady kodu zostały uproszczone dla czytelności. Pokazują najważniejsze koncepcje, a nie wszystkie przypadki brzegowe. Pełną, działającą implementację znajdziesz w demonstracyjnej aplikacji, którą możesz sklonować, uruchomić lokalnie z użyciem ngrok i przetestować w prawdziwym kliencie AI.
1. Endpoint MCP: uruchomienie automatycznego wykrywania
Gdy do endpointu MCP trafia nieuwierzytelnione żądanie, serwer musi zwrócić odpowiedź 401 z nagłówkiem WWW-Authenticate, który wskazuje klientowi lokalizację metadanych autoryzacji:
class McpController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :authenticate_bearer_token
def handle
# MCP request handling logic
end
private
def authenticate_bearer_token
token = request.headers["Authorization"]&.delete_prefix("Bearer ")
return if token.present? && valid_token?(token)
response.headers["WWW-Authenticate"] = %(Bearer resource_metadata="#{resource_metadata_url}")
head :unauthorized
end
def resource_metadata_url
"#{request.base_url}/.well-known/oauth-protected-resource?scope=my_mcp"
end
end
Parametr scope w adresie URL metadanych informuje klienta, o jaki zakres powinien poprosić podczas rejestracji. Nabiera to znaczenia, gdy aplikacja udostępnia kilka serwerów MCP o różnych poziomach dostępu.
2. Protected Resource Metadata (RFC 9728)
Ten endpoint informuje klienta, który serwer autoryzacji chroni dany zasób:
# config/routes.rb
get "/.well-known/oauth-protected-resource", to: "oauth/metadata#protected_resource"
get "/.well-known/oauth-authorization-server", to: "oauth/metadata#authorization_server"
post "/register", to: "oauth/registration#create"
# app/controllers/oauth/metadata_controller.rb
class Oauth::MetadataController < ApplicationController
def protected_resource
scope = params[:scope] || "default"
render json: {
resource: request.base_url,
authorization_servers: [request.base_url],
scopes_supported: [scope]
}
end
def authorization_server
render json: {
issuer: request.base_url,
authorization_endpoint: "#{request.base_url}/authorize",
token_endpoint: "#{request.base_url}/token",
registration_endpoint: "#{request.base_url}/register",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code"],
code_challenge_methods_supported: ["S256"],
scopes_supported: OauthClient::ALLOWED_SCOPES
}
end
end
Wartość code_challenge_methods_supported: ["S256"] jest obowiązkowa. Specyfikacja MCP wymaga PKCE z metodą S256, a zgodni z nią klienci odmówią kontynuowania procesu, jeśli tej wartości zabraknie.
3. Dynamic Client Registration (RFC 7591)
Ten element eliminuje ręczne zarządzanie danymi uwierzytelniającymi. Gdy klient AI wywołuje /register, serwer od razu tworzy dla niego nowego klienta OAuth:
# app/controllers/oauth/registration_controller.rb
class Oauth::RegistrationController < ApplicationController
skip_before_action :verify_authenticity_token
def create
scope = validated_scope
client = OauthClient.create!(
client_name: registration_params[:client_name] || "MCP Client",
redirect_uris: registration_params[:redirect_uris],
scope: scope,
token_endpoint_auth_method: "none"
)
render json: {
client_id: client.client_id,
client_name: client.client_name,
redirect_uris: client.redirect_uris,
scope: client.scope,
token_endpoint_auth_method: "none"
}, status: :created
end
private
def validated_scope
scope = registration_params[:scope].presence || "default"
return scope if OauthClient::ALLOWED_SCOPES.include?(scope)
raise ActiveRecord::RecordInvalid.new(
OauthClient.new.tap { |c| c.errors.add(:scope, "must be one of: #{OauthClient::ALLOWED_SCOPES.join(', ')}") }
)
end
def registration_params
params.permit(:client_name, :scope, :token_endpoint_auth_method, redirect_uris: [])
end
end
Zwróć uwagę na token_endpoint_auth_method: "none". Klienci MCP są klientami publicznymi: działają w przeglądarkach lub aplikacjach desktopowych i nie mogą bezpiecznie przechowywać sekretu klienta. Endpoint rejestracji musi akceptować tę metodę. Rozwiązanie jest bezpieczne, ponieważ proces opiera się na mechanizmie PKCE z metodą S256, którego wyzwanie kryptograficzne jest weryfikowane podczas wymiany kodu na token, a nie na statycznym sekrecie klienta.
4. Autoryzacja ze sprawdzaniem roli
Endpoint autoryzacji służy do weryfikacji tożsamości i uprawnień użytkownika. Jest to standardowy proces OAuth uzupełniony o jeden ważny element: kontrolę dostępu opartą na rolach.
# app/controllers/oauth/authorization_controller.rb
class Oauth::AuthorizationController < ApplicationController
before_action :authenticate_user!
def authorize
client = OauthClient.find_by!(client_id: params[:client_id])
unless current_user.has_role?(client.scope)
render plain: "Access denied: insufficient permissions", status: :forbidden
return
end
code = AuthorizationCode.create!(
user: current_user,
oauth_client: client,
redirect_uri: params[:redirect_uri],
code_challenge: params[:code_challenge],
code_challenge_method: params[:code_challenge_method],
scope: client.scope
)
redirect_to "#{params[:redirect_uri]}?code=#{code.value}",
allow_other_host: true
end
end
Wywołanie has_role? kontroluje dostęp. Członek zespołu z odpowiednią rolą loguje się i przechodzi dalej. Osoba bez wymaganej roli widzi komunikat o odmowie dostępu. Nie jest potrzebny udział administratora ani udostępnianie danych uwierzytelniających OAuth.
Na co zwrócić uwagę
Powyższa implementacja jest prosta. Problemy pojawiają się dopiero przy współpracy z rzeczywistymi klientami AI w środowisku produkcyjnym. Żadnego z poniższych problemów nie było widać podczas testów lokalnych. Ujawniły się dopiero wtedy, gdy klient działający w przeglądarce realizował cały proces na wdrożonym serwerze.
Udostępnij WWW-Authenticate przez CORS
Proces automatycznego wykrywania rozpoczyna się, gdy klient odczytuje nagłówek WWW-Authenticate z odpowiedzi 401. Domyślnie CORS nie udostępnia tego nagłówka kodowi JavaScript. Przeglądarka go otrzymuje i pokazuje w narzędziach deweloperskich, ale kod klienta nie ma do niego dostępu. Cały proces wykrywania zatrzymuje się już na pierwszym kroku.
Dodaj expose: ["WWW-Authenticate"] do konfiguracji CORS każdego endpointu MCP:
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "*"
resource "/.well-known/*", headers: :any, methods: [:get, :options]
resource "/token", headers: :any, methods: [:post, :options]
resource "/register", headers: :any, methods: [:post, :options]
resource "/my_mcp",
headers: :any,
methods: [:get, :post, :delete, :options],
expose: ["WWW-Authenticate"]
end
end
Specyfikacja MCP wspomina, że klienci POWINNI korzystać ze ścieżki rezerwowej, czyli bezpośrednio sprawdzać URI .well-known, gdy nagłówek nie jest dostępny. Nie każdy klient obsługuje taką ścieżkę, dlatego nie należy na niej polegać.
Sanityzuj wartości parametru scope w żądaniach rejestracji
Nagłówek WWW-Authenticate zawiera ujęty w cudzysłów adres URL z parametrem zapytania scope:
Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource?scope=my_scope"
Niektóre parsery po stronie klienta wyodrębniają zakres razem z końcowym znakiem cudzysłowu: my_scope" zamiast my_scope. Ten jeden dodatkowy znak wystarcza, aby endpoint rejestracji odrzucił żądanie.
Uwzględnienie takiego przypadku w parserze po stronie serwera pozwala obsłużyć go bez przerywania procesu:
def validated_scope
scope = registration_params[:scope].presence || "default"
scope = scope.delete_suffix('"')
return scope if ALLOWED_SCOPES.include?(scope)
raise InvalidScopeError, "must be one of: #{ALLOWED_SCOPES.join(', ')}"
end
Ogólna zasada brzmi: zawsze testuj pełny proces z rzeczywistym klientem AI. Curl i Postman parsują nagłówki inaczej niż klienci działający w przeglądarce, więc nie ujawnią tych problemów.
Dodaj hosty przekierowań OAuth do listy dozwolonej w CSP
Jeśli aplikacja ma Content Security Policy z ustawieniem form_action :self, często spotykanym na produkcji, przekierowania OAuth do zewnętrznych klientów AI zostaną po cichu zablokowane. Chrome stosuje ograniczenia form-action nie tylko do adresu docelowego formularza, ale również do przekierowań następujących po jego wysłaniu. Formularz logowania wysyła dane do Twojej aplikacji, jednak adres zwrotny klienta AI ma inny origin, czyli inną kombinację schematu, hosta i portu.
Firefox nie zachowuje się w ten sposób, przez co problem zależy od przeglądarki i jest trudny do zdiagnozowania.
Skonfiguruj dozwolone hosty przekierowań za pomocą zmiennych środowiskowych:
# config/initializers/content_security_policy.rb
Rails.application.configure do
config.content_security_policy do |policy|
oauth_hosts = ENV.fetch("OAUTH_ALLOWED_REDIRECT_HOSTS", "")
.split(",").map(&:strip).compact_blank
oauth_hosts.each do |host|
uri = URI.parse(host)
raise "OAUTH_ALLOWED_REDIRECT_HOSTS: #{host} must be HTTPS" unless uri.scheme == "https"
end
policy.form_action :self, *oauth_hosts
end
end
OAUTH_ALLOWED_REDIRECT_HOSTS: "https://claude.ai"
Dodanie obsługi nowego klienta AI wymaga więc zmiany konfiguracji, a nie kodu.
Testuj przez tunel, nie przez localhost
Wszystkie trzy opisane problemy mają wspólną cechę: są niewidoczne podczas pracy lokalnej. Curl omija CORS. Przy serwerze lokalnym nie ujawniają się ograniczenia cross-origin, czyli ograniczenia dotyczące żądań między różnymi originami. Środowiska deweloperskie rzadko korzystają z restrykcyjnej CSP.
Użyj ngrok lub podobnej usługi tunelującej, aby udostępnić serwer deweloperski pod rzeczywistym adresem HTTPS. Skonfiguruj rzeczywistego klienta AI tak, aby łączył się przez tunel. Sprawdź konsolę przeglądarki pod kątem naruszeń CORS i CSP, ponieważ logi serwera mogą rejestrować prawidłowe odpowiedzi, mimo że klient po cichu przestaje działać.
Doświadczenie użytkownika
Po zaimplementowaniu procesu i rozwiązaniu trzech opisanych problemów z perspektywy członka zespołu wszystko wygląda tak:
- Otwiera klienta AI
- Dodaje nowy serwer MCP:
https://app.example.com/my_mcp - Automatycznie otwiera się okno logowania
- Loguje się zwykłymi danymi do aplikacji
- Klient AI nawiązuje połączenie, a narzędzia stają się dostępne
Bez client_id, client_secret, udziału administratora ani konfiguracji technicznej. Istniejąca rola użytkownika określa, do czego ma on dostęp. Dodanie nowego członka zespołu sprowadza się do przypisania mu odpowiedniej roli w aplikacji, czyli przebiega tak samo jak w przypadku każdej innej funkcji.
Dodawanie niestandardowego połączenia w Claude: wystarczy adres URL serwera, bez client_id ani client_secret.
Po automatycznym wykryciu OAuth narzędzia MCP są dostępne bez ręcznej konfiguracji.
Claude używa połączonego serwera MCP do wyświetlenia listy użytkowników. Cały proces działa od początku do końca.
Specyfikacja MCP precyzyjnie opisuje ten proces. Implementacja w Rails jest prosta. Błędy produkcyjne nie tkwią w samej logice, lecz na styku specyfikacji, gdzie CORS, CSP i różne implementacje parserów tworzą problemy widoczne dopiero w rzeczywistych warunkach.
Protokół jest solidny. Najwięcej pracy wymagają szczegóły implementacji.
Wcześniejsza wersja tego artykułu ukazała się na Blog Visuality.pl.
