From eb0844930f153182b9f4189fe0cd6cba569b5e3d Mon Sep 17 00:00:00 2001 From: Vitali Tatarintev Date: Wed, 15 Oct 2025 15:35:53 +0200 Subject: [PATCH] Default OAuth scope to `mcp` for dynamic client registration When dynamic OAuth applications request authorization without a scope parameter, default to `mcp` scope per RFC 7591. This simplifies MCP server configuration. Changelog: changed --- .../oauth/authorizations_controller.rb | 15 +++++-- .../oauth/dynamic_registrations_controller.rb | 2 +- .../model_context_protocol/mcp_server.md | 35 +++++++++++++++- .../oauth/authorizations_controller_spec.rb | 41 +++++++++++++++++++ 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index fdd18b313515a9..05e9977142b5e6 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -79,15 +79,24 @@ def pre_auth_params # Cannot be achieved with a before_action hook, due to the execution order. downgrade_scopes! if action_name == 'new' - # Force scope to `mcp` when resource is present, and the MCP server API - params[:scope] = Gitlab::Auth::MCP_SCOPE.to_s if params[:resource].present? && resource_is_mcp_server? + # Default scope to `mcp` for MCP server requests and dynamic MCP applications. + params[:scope] = Gitlab::Auth::MCP_SCOPE.to_s if resource_is_mcp_server? || should_default_to_mcp_scope? params[:organization_id] = ::Current.organization.id super end def resource_is_mcp_server? - params[:resource].end_with?('/api/v4/mcp') + params[:resource].present? && params[:resource].end_with?('/api/v4/mcp') + end + + def should_default_to_mcp_scope? + params[:scope].blank? && + doorkeeper_application&.dynamic? && + # Verify application only has mcp scope. Dynamic apps are always created with + # only mcp scope (see dynamic_registrations_controller.rb), but this check + # guards against future changes or manual modifications. + doorkeeper_application&.scopes == Doorkeeper::OAuth::Scopes.from_string(Gitlab::Auth::MCP_SCOPE.to_s) end # limit scopes when signing in with GitLab diff --git a/app/controllers/oauth/dynamic_registrations_controller.rb b/app/controllers/oauth/dynamic_registrations_controller.rb index aa2426b4b83491..4b737f2b4d2b15 100644 --- a/app/controllers/oauth/dynamic_registrations_controller.rb +++ b/app/controllers/oauth/dynamic_registrations_controller.rb @@ -77,7 +77,7 @@ def check_feature_flag! end def check_rate_limit - return if Rails.env.test? + return if Rails.env.test? || Rails.env.development? check_rate_limit!(:oauth_dynamic_registration, scope: request.ip) end diff --git a/doc/user/gitlab_duo/model_context_protocol/mcp_server.md b/doc/user/gitlab_duo/model_context_protocol/mcp_server.md index 0026a6d940e3ef..fff64c81c75997 100644 --- a/doc/user/gitlab_duo/model_context_protocol/mcp_server.md +++ b/doc/user/gitlab_duo/model_context_protocol/mcp_server.md @@ -73,7 +73,22 @@ To configure the GitLab MCP server in Cursor: - Replace `` with: - On GitLab Self-Managed, your GitLab instance URL. - On GitLab.com, `GitLab.com`. - - The `--static-oauth-client-metadata` parameter is mandatory for the `mcp-remote` module to set the OAuth scope to `mcp` as expected by the GitLab server. + + ```json + { + "mcpServers": { + "GitLab": { + "command": "npx", + "args": [ + "mcp-remote", + "https:///api/v4/mcp" + ] + } + } + } + ``` + + - The `--static-oauth-client-metadata` parameter is optional for the `mcp-remote` module to explicitly set the OAuth scope to `mcp`. If omitted, GitLab defaults to the `mcp` scope for dynamic applications. ```json { @@ -123,7 +138,23 @@ To configure the GitLab MCP server in Claude Desktop: - Replace `` with: - On GitLab Self-Managed, your GitLab instance URL. - On GitLab.com, `GitLab.com`. - - The `--static-oauth-client-metadata` parameter is mandatory for the `mcp-remote` module to set the OAuth scope to `mcp` as expected by the GitLab server. + + ```json + { + "mcpServers": { + "GitLab": { + "command": "npx", + "args": [ + "-y", + "mcp-remote", + "https:///api/v4/mcp" + ] + } + } + } + ``` + + - The `--static-oauth-client-metadata` parameter is optional for the `mcp-remote` module to explicitly set the OAuth scope to `mcp`. If omitted, GitLab defaults to the `mcp` scope for dynamic applications. ```json { diff --git a/spec/requests/oauth/authorizations_controller_spec.rb b/spec/requests/oauth/authorizations_controller_spec.rb index 4d1e1c1daaac4c..eed6eaca9ee57e 100644 --- a/spec/requests/oauth/authorizations_controller_spec.rb +++ b/spec/requests/oauth/authorizations_controller_spec.rb @@ -228,6 +228,18 @@ expect(response.body).to include('value="mcp"') expect(response.body).not_to include('value="read_user"') end + + context 'when scope param is not present' do + it 'defaults scope to mcp', :aggregate_failures do + get oauth_authorization_path, params: params.merge( + resource: 'https://gitlab.example.com/api/v4/mcp' + ).except(:scope) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('doorkeeper/authorizations/new') + expect(response.body).to include('value="mcp"') + end + end end context 'when resource param does not end with /api/v4/mcp' do @@ -267,5 +279,34 @@ end end end + + describe 'MCP scope defaulting for dynamic applications' do + context 'when dynamic application has only mcp scope and no scope provided' do + let(:application) { create(:oauth_application, :dynamic, scopes: 'mcp', redirect_uri: 'http://example.com') } + + it 'defaults scope to mcp', :aggregate_failures do + get oauth_authorization_path, params: params.except(:scope).merge( + code_challenge: 'valid_code_challenge', + code_challenge_method: 'S256' + ) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('doorkeeper/authorizations/new') + expect(response.body).to include('value="mcp"') + end + end + + context 'when non-dynamic application has multiple scopes and no scope provided' do + let(:application) { create(:oauth_application, scopes: 'api read_user', redirect_uri: 'http://example.com') } + + it 'does not default to mcp scope', :aggregate_failures do + get oauth_authorization_path, params: params.except(:scope) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('doorkeeper/authorizations/new') + expect(response.body).to include('value="api read_user"') + end + end + end end end -- GitLab