From 3d54c4fa5104f8a9c08f8fe68f8c6d65203574f5 Mon Sep 17 00:00:00 2001 From: Tan Le Date: Wed, 22 Oct 2025 20:59:30 +1000 Subject: [PATCH] Control access to MCP with beta features setting This allows users to control access to MCP server using instance and namespace beta features setting. Changelog: changed EE: true --- ee/lib/ee/api/mcp/base.rb | 38 +++++++++++ ee/spec/requests/api/mcp/base_spec.rb | 63 +++++++++++++++++++ .../api/mcp/handlers/list_tools_spec.rb | 4 ++ lib/api/mcp/base.rb | 8 +++ spec/requests/api/mcp/base_spec.rb | 4 ++ .../api/mcp/handlers/call_tool_spec.rb | 4 ++ .../mcp/handlers/initialize_request_spec.rb | 4 ++ .../initialized_notification_request_spec.rb | 4 ++ .../api/mcp/handlers/list_tools_spec.rb | 4 ++ 9 files changed, 133 insertions(+) create mode 100644 ee/lib/ee/api/mcp/base.rb create mode 100644 ee/spec/requests/api/mcp/base_spec.rb diff --git a/ee/lib/ee/api/mcp/base.rb b/ee/lib/ee/api/mcp/base.rb new file mode 100644 index 00000000000000..ba073e4333f818 --- /dev/null +++ b/ee/lib/ee/api/mcp/base.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module EE + module API + module Mcp + module Base + extend ActiveSupport::Concern + + prepended do + helpers do + extend ::Gitlab::Utils::Override + + override :feature_available? + def feature_available? + return false unless instance_allows_experiment_and_beta_features + return false unless gitlab_com_namespace_enables_experiment_and_beta_features + + true + end + + def instance_allows_experiment_and_beta_features + return true if ::Gitlab::Saas.feature_available?(:gitlab_duo_saas_only) + + ::Gitlab::CurrentSettings.instance_level_ai_beta_features_enabled? + end + + def gitlab_com_namespace_enables_experiment_and_beta_features + # namespace-level settings check is only relevant for .com + return true unless ::Gitlab::Saas.feature_available?(:gitlab_duo_saas_only) + + current_user.any_group_with_ai_available? + end + end + end + end + end + end +end diff --git a/ee/spec/requests/api/mcp/base_spec.rb b/ee/spec/requests/api/mcp/base_spec.rb new file mode 100644 index 00000000000000..95546ce8474ef7 --- /dev/null +++ b/ee/spec/requests/api/mcp/base_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Mcp::Base, feature_category: :mcp_server do + using RSpec::Parameterized::TableSyntax + + let_it_be_with_refind(:user) { create(:user) } + let_it_be(:access_token) { create(:oauth_access_token, user: user, scopes: [:mcp]) } + + describe 'POST /mcp' do + context 'when gitlab.com', :saas do + let_it_be(:group) { create(:group_with_plan, plan: :ultimate_plan, developers: user) } + + where(:group_experiment_features, :instance_ai_enabled, :expected_status) do + true | true | :ok + false | true | :not_found + true | false | :ok # instance setting ignored + false | false | :not_found + end + + with_them do + before do + stub_saas_features(gitlab_duo_saas_only: true) + stub_application_setting(instance_level_ai_beta_features_enabled: instance_ai_enabled) + group.namespace_settings.reload.update!(experiment_features_enabled: group_experiment_features) + end + + it 'behaves according to access control rules' do + post api('/mcp', user, oauth_access_token: access_token), + params: { jsonrpc: '2.0', method: 'initialize', id: '1' } + + expect(response).to have_gitlab_http_status(expected_status) + end + end + end + + context 'when not gitlab.com' do + let_it_be(:group) { create(:group, developers: user) } + + where(:group_experiment_features, :instance_ai_enabled, :expected_status) do + true | true | :ok + false | true | :ok # group features don't matter + true | false | :not_found + false | false | :not_found + end + + with_them do + before do + stub_application_setting(instance_level_ai_beta_features_enabled: instance_ai_enabled) + group.namespace_settings.reload.update!(experiment_features_enabled: group_experiment_features) + end + + it 'behaves according to access control rules' do + post api('/mcp', user, oauth_access_token: access_token), + params: { jsonrpc: '2.0', method: 'initialize', id: '1' } + + expect(response).to have_gitlab_http_status(expected_status) + end + end + end + end +end diff --git a/ee/spec/requests/api/mcp/handlers/list_tools_spec.rb b/ee/spec/requests/api/mcp/handlers/list_tools_spec.rb index 79ce4c3b5181b1..4da0512f8d323d 100644 --- a/ee/spec/requests/api/mcp/handlers/list_tools_spec.rb +++ b/ee/spec/requests/api/mcp/handlers/list_tools_spec.rb @@ -7,6 +7,10 @@ let_it_be(:user) { create(:user) } let_it_be(:access_token) { create(:oauth_access_token, user: user, scopes: [:mcp]) } + before do + stub_application_setting(instance_level_ai_beta_features_enabled: true) + end + describe 'POST /mcp with tools/list method' do let(:params) do { diff --git a/lib/api/mcp/base.rb b/lib/api/mcp/base.rb index d92be7eb9a6ad7..655055ed4a10e1 100644 --- a/lib/api/mcp/base.rb +++ b/lib/api/mcp/base.rb @@ -42,10 +42,16 @@ class Base < ::API::Base before do authenticate! not_found! unless Feature.enabled?(:mcp_server, current_user) + not_found! unless feature_available? forbidden! unless AccessTokenValidationService.new(access_token).include_any_scope?([Gitlab::Auth::MCP_SCOPE]) end helpers do + def feature_available? + # This method will be redefined in EE. + true + end + def invoke_basic_handler method_name = params[:method] handler_class = JSONRPC_METHOD_HANDLERS[method_name] || method_not_found!(method_name) @@ -144,3 +150,5 @@ def format_jsonrpc_response(result) end end end + +API::Mcp::Base.prepend_mod diff --git a/spec/requests/api/mcp/base_spec.rb b/spec/requests/api/mcp/base_spec.rb index 36b85bd2516846..d3445d262178b2 100644 --- a/spec/requests/api/mcp/base_spec.rb +++ b/spec/requests/api/mcp/base_spec.rb @@ -6,6 +6,10 @@ let_it_be(:user) { create(:user) } let_it_be(:access_token) { create(:oauth_access_token, user: user, scopes: [:mcp]) } + before do + stub_application_setting(instance_level_ai_beta_features_enabled: true) + end + describe 'POST /mcp' do context 'when unauthenticated' do it 'returns authentication error' do diff --git a/spec/requests/api/mcp/handlers/call_tool_spec.rb b/spec/requests/api/mcp/handlers/call_tool_spec.rb index 581f7d1d40891f..f1c4f634303f3b 100644 --- a/spec/requests/api/mcp/handlers/call_tool_spec.rb +++ b/spec/requests/api/mcp/handlers/call_tool_spec.rb @@ -19,6 +19,10 @@ } end + before do + stub_application_setting(instance_level_ai_beta_features_enabled: true) + end + describe 'POST /mcp with tools/call method' do let(:tool_params) do { name: 'get_issue', arguments: { id: project.full_path, issue_iid: issue.iid } } diff --git a/spec/requests/api/mcp/handlers/initialize_request_spec.rb b/spec/requests/api/mcp/handlers/initialize_request_spec.rb index d175312ce482a4..354e9dfd01d1ed 100644 --- a/spec/requests/api/mcp/handlers/initialize_request_spec.rb +++ b/spec/requests/api/mcp/handlers/initialize_request_spec.rb @@ -7,6 +7,10 @@ let_it_be(:user) { create(:user) } let_it_be(:access_token) { create(:oauth_access_token, user: user, scopes: [:mcp]) } + before do + stub_application_setting(instance_level_ai_beta_features_enabled: true) + end + describe 'POST /mcp with initialize method' do let(:params) do { diff --git a/spec/requests/api/mcp/handlers/initialized_notification_request_spec.rb b/spec/requests/api/mcp/handlers/initialized_notification_request_spec.rb index ba701bf5e556dd..f9fc700d608c4c 100644 --- a/spec/requests/api/mcp/handlers/initialized_notification_request_spec.rb +++ b/spec/requests/api/mcp/handlers/initialized_notification_request_spec.rb @@ -7,6 +7,10 @@ let_it_be(:user) { create(:user) } let_it_be(:access_token) { create(:oauth_access_token, user: user, scopes: [:mcp]) } + before do + stub_application_setting(instance_level_ai_beta_features_enabled: true) + end + describe 'POST /mcp with notifications/initialized method' do let(:params) do { diff --git a/spec/requests/api/mcp/handlers/list_tools_spec.rb b/spec/requests/api/mcp/handlers/list_tools_spec.rb index ae55fdb4ac98c3..a43f6ca4442b7f 100644 --- a/spec/requests/api/mcp/handlers/list_tools_spec.rb +++ b/spec/requests/api/mcp/handlers/list_tools_spec.rb @@ -7,6 +7,10 @@ let_it_be(:user) { create(:user) } let_it_be(:access_token) { create(:oauth_access_token, user: user, scopes: [:mcp]) } + before do + stub_application_setting(instance_level_ai_beta_features_enabled: true) + end + describe 'POST /mcp with tools/list method' do let(:params) do { -- GitLab