diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index fdd18b313515a936b360a2d0a635972ae2c78057..05e9977142b5e6a250a8474da9f0faa979f9b823 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 aa2426b4b8349112b26432f6892d9c8782e171e6..4b737f2b4d2b1521d412cf68a0e6ab557adbfceb 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 0026a6d940e3ef69cecdf1b48b4415765f0b359b..fff64c81c75997d619450800884a33b9868ebf86 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 4d1e1c1daaac4c7e8949500447be744301a767bf..eed6eaca9ee57ea9ab80b5a35657fe4a435f9582 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