diff --git a/ee/lib/ee/api/mcp/base.rb b/ee/lib/ee/api/mcp/base.rb new file mode 100644 index 0000000000000000000000000000000000000000..ba073e4333f8186d980518b55a4e84b00d8ef426 --- /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 0000000000000000000000000000000000000000..95546ce8474ef725cb2911edba55436540dd12fe --- /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 79ce4c3b5181b13c90e988bfb73a2b648395cc39..4da0512f8d323df9caa804b63eb901a25e97530b 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 d92be7eb9a6ad78a8afce1ff1109fe1c1ca58ce1..655055ed4a10e1e11b1644f70c7b38dd24cd33fc 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 36b85bd2516846de7b1439aec7e458e0aa4a4306..d3445d262178b2667d448de51702a30de442a399 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 581f7d1d40891f40d4ec7360c6f58052201b0310..f1c4f634303f3b4e6d0d938dcddb8cc6d1d16c17 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 d175312ce482a4e6eb30999abf8a6ead1fbd8431..354e9dfd01d1ed5671fa0b4847eef73fff489054 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 ba701bf5e556ddecd3d27527e4b471ef5814f03f..f9fc700d608c4c44e03340d31697654fe785f652 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 ae55fdb4ac98c39a48b4e1c398ba9332f7c1625c..a43f6ca4442b7f9714de0f28980656c175c3fd7c 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 {