From 86bb6d089a2db642ff24686af264524c77fb2efd Mon Sep 17 00:00:00 2001 From: Szymon Kurcab Date: Fri, 20 Mar 2026 18:34:02 +0100 Subject: [PATCH 1/2] feat: add supported_scopes method and scope: :all shorthand (closes #107) Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/mcp_client/auth/oauth_provider.rb | 20 +++- .../mcp_client/auth/oauth_provider_spec.rb | 96 +++++++++++++++++++ 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/lib/mcp_client/auth/oauth_provider.rb b/lib/mcp_client/auth/oauth_provider.rb index 388e78a..2dd29ad 100644 --- a/lib/mcp_client/auth/oauth_provider.rb +++ b/lib/mcp_client/auth/oauth_provider.rb @@ -14,7 +14,7 @@ class OAuthProvider # @!attribute [rw] redirect_uri # @return [String] OAuth redirect URI # @!attribute [rw] scope - # @return [String, nil] OAuth scope + # @return [String, Symbol, nil] OAuth scope (use :all for all server-supported scopes) # @!attribute [rw] logger # @return [Logger] Logger instance # @!attribute [rw] storage @@ -27,7 +27,7 @@ class OAuthProvider # Initialize OAuth provider # @param server_url [String] The MCP server URL (used as OAuth resource parameter) # @param redirect_uri [String] OAuth redirect URI (default: http://localhost:8080/callback) - # @param scope [String, nil] OAuth scope + # @param scope [String, Symbol, nil] OAuth scope (use :all for all server-supported scopes) # @param logger [Logger, nil] Optional logger # @param storage [Object, nil] Storage backend for tokens and client info def initialize(server_url:, redirect_uri: 'http://localhost:8080/callback', scope: nil, logger: nil, storage: nil) @@ -58,6 +58,14 @@ def access_token refresh_token(token) if token.refresh_token end + # Return the scopes supported by the authorization server + # Discovers server metadata and returns the scopes_supported list. + # @return [Array] supported scopes, or empty array if not advertised + # @raise [MCPClient::Errors::ConnectionError] if server discovery fails + def supported_scopes + @supported_scopes ||= (discover_authorization_server.scopes_supported || []) + end + # Start OAuth authorization flow # @return [String] Authorization URL to redirect user to # @raise [MCPClient::Errors::ConnectionError] if server discovery fails @@ -328,12 +336,14 @@ def get_or_register_client(server_metadata) def register_client(server_metadata) logger.debug("Registering OAuth client at: #{server_metadata.registration_endpoint}") + resolved_scope = scope == :all ? supported_scopes.join(' ') : scope + metadata = ClientMetadata.new( redirect_uris: [redirect_uri], token_endpoint_auth_method: 'none', # Public client grant_types: %w[authorization_code refresh_token], response_types: ['code'], - scope: scope + scope: resolved_scope ) response = @http_client.post(server_metadata.registration_endpoint) do |req| @@ -396,11 +406,13 @@ def build_authorization_url(server_metadata, client_info, pkce, state) # Use the redirect_uri that was actually registered registered_redirect_uri = client_info.metadata.redirect_uris.first + resolved_scope = scope == :all ? supported_scopes.join(' ') : scope + params = { response_type: 'code', client_id: client_info.client_id, redirect_uri: registered_redirect_uri, - scope: scope, + scope: resolved_scope, state: state, code_challenge: pkce.code_challenge, code_challenge_method: pkce.code_challenge_method, diff --git a/spec/lib/mcp_client/auth/oauth_provider_spec.rb b/spec/lib/mcp_client/auth/oauth_provider_spec.rb index c1bdcfc..cf5e60f 100644 --- a/spec/lib/mcp_client/auth/oauth_provider_spec.rb +++ b/spec/lib/mcp_client/auth/oauth_provider_spec.rb @@ -190,6 +190,102 @@ end end + describe '#supported_scopes' do + let(:server_metadata) do + MCPClient::Auth::ServerMetadata.new( + issuer: 'https://mcp.example.com', + authorization_endpoint: 'https://mcp.example.com/authorize', + token_endpoint: 'https://mcp.example.com/token', + scopes_supported: %w[read write admin] + ) + end + + before do + allow(oauth_provider).to receive(:discover_authorization_server).and_return(server_metadata) + end + + it 'returns the scopes from server metadata' do + expect(oauth_provider.supported_scopes).to eq(%w[read write admin]) + end + + it 'memoizes the result' do + oauth_provider.supported_scopes + oauth_provider.supported_scopes + expect(oauth_provider).to have_received(:discover_authorization_server).once + end + + context 'when scopes_supported is nil' do + let(:server_metadata) do + MCPClient::Auth::ServerMetadata.new( + issuer: 'https://mcp.example.com', + authorization_endpoint: 'https://mcp.example.com/authorize', + token_endpoint: 'https://mcp.example.com/token', + scopes_supported: nil + ) + end + + it 'returns an empty array' do + expect(oauth_provider.supported_scopes).to eq([]) + end + end + end + + describe 'scope: :all' do + let(:server_metadata) do + MCPClient::Auth::ServerMetadata.new( + issuer: 'https://mcp.example.com', + authorization_endpoint: 'https://mcp.example.com/authorize', + token_endpoint: 'https://mcp.example.com/token', + registration_endpoint: 'https://mcp.example.com/register', + scopes_supported: %w[read write admin] + ) + end + + let(:client_metadata) do + MCPClient::Auth::ClientMetadata.new( + redirect_uris: [redirect_uri], + token_endpoint_auth_method: 'none' + ) + end + + let(:client_info) do + MCPClient::Auth::ClientInfo.new( + client_id: 'client123', + metadata: client_metadata + ) + end + + let(:provider) do + described_class.new( + server_url: server_url, + redirect_uri: redirect_uri, + scope: :all, + logger: logger, + storage: storage + ) + end + + before do + allow(storage).to receive(:get_server_metadata).and_return(server_metadata) + allow(storage).to receive(:set_server_metadata) + allow(storage).to receive(:get_client_info).and_return(client_info) + allow(storage).to receive(:set_pkce) + allow(storage).to receive(:set_state) + end + + it 'resolves :all to all supported scopes in the authorization URL' do + auth_url = provider.start_authorization_flow + uri = URI.parse(auth_url) + params = URI.decode_www_form(uri.query).to_h + expect(params['scope']).to eq('read write admin') + end + + it 'does not mutate @scope' do + provider.start_authorization_flow + expect(provider.scope).to eq(:all) + end + end + describe '#exchange_authorization_code' do let(:storage_instance) { MCPClient::Auth::OAuthProvider::MemoryStorage.new } let(:logger) { instance_double('Logger') } From f7602537bcbfa04f99394de96367b1ae6cc16a75 Mon Sep 17 00:00:00 2001 From: Szymon Kurcab Date: Fri, 20 Mar 2026 19:14:13 +0100 Subject: [PATCH 2/2] style: fix rubocop offense (redundant parentheses) --- lib/mcp_client/auth/oauth_provider.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mcp_client/auth/oauth_provider.rb b/lib/mcp_client/auth/oauth_provider.rb index 2dd29ad..f53fd32 100644 --- a/lib/mcp_client/auth/oauth_provider.rb +++ b/lib/mcp_client/auth/oauth_provider.rb @@ -63,7 +63,7 @@ def access_token # @return [Array] supported scopes, or empty array if not advertised # @raise [MCPClient::Errors::ConnectionError] if server discovery fails def supported_scopes - @supported_scopes ||= (discover_authorization_server.scopes_supported || []) + @supported_scopes ||= discover_authorization_server.scopes_supported || [] end # Start OAuth authorization flow