update auth solution for custom tools

What does this MR do and why?

After some experiment, It seems it's better to extract the auth solution to a dedicated MR -- this MR here.

!205614 (diffs)

This is the first custom tool where we need to auth a user, previously we don't have this need. The existing infra does not support this.

It looks like this is big MR, but it actually just did the following three things:

1. for mcp_server_new_implementation

Feature.enabled?(:mcp_server_new_implementation, current_user) #=>true

before execute, we need to set the current_user, if it's a ::Mcp::Tools::CustomService,

Mcp::Tools::ApiTool does not need us insert the current_user

# only custom_service needs the current_user injected
tool.set_cred(current_user: current_user) if tool.is_a? ::Mcp::Tools::CustomService
tool.execute(request: request, params: params)

2. for non mcp_server_new_implementation

Feature.enabled?(:mcp_server_new_implementation, current_user) #=>false

starting point is /api/v4/mcp:

here we have access_token and current_user, which is calculated in APIGuard and handy to use, which will be passed to upstream services.

the first-level upstream service is handlers:

for example: Handlers::CallToolRequest, Handlers::ListToolsRequest, etc. All these handlers inherits this Base:

module API
  module Mcp
    module Handlers
      class Base
        attr_reader :access_token, :current_user, :params

        def initialize(params, access_token, current_user)
          @params = params
          @access_token = access_token
          @current_user = current_user
        end

so that these handlers have access to access_token and current_user, and they can pass them to the next-level(see next section) upstream services:

# API::Mcp::Handlers::CallToolRequest
tool = tool_klass.new(name: params[:name])
tool.set_cred(current_user: current_user, access_token: access_token)

the second-level upstream service is tools

for example:

'get_code_context' => ::Mcp::Tools::SearchCodebaseService
'get_issue' => ::Mcp::Tools::GetIssueService,

(tool_klass above in first-level upstream service is from this hash)

SearchCodebaseService a custom service. Now it has what it needs, which is the current_user.

GetIssueService is a api service. Now it also has what it needs, which is the access_token.

# API::Mcp::Handlers::CallToolRequest
tool = tool_klass.new(name: params[:name])
tool.set_cred(current_user: current_user, access_token: access_token)

in custom_service:

override :set_cred
def set_cred(current_user: nil, access_token: nil)
  @current_user = current_user
  _ = access_token # access_token is not used in CustomService
end

in api_service:

override :set_cred
def set_cred(current_user: nil, access_token: nil)
  @access_token = access_token
  _ = current_user # current_user is not used in ApiService
end

3. Update the exec function arguments list to kwargs

# frozen_string_literal: true

module Mcp
  module Tools
    class BaseService
      def execute(request: nil, params: nil) 
....
....
....

this is to make Mcp::Tools::BaseService (old implementation) a duck type of Mcp::Tools::ApiTool(new implementation):

module Mcp
  module Tools
    class ApiTool
....
....
      def execute(request: nil, params: nil)
....
....

this way we can call both executes like: tool.execute(request: request, params: params) (even request is not used in old implementation. Maybe we can sunset the old implementation later, but for now the execute function signature works for both implementations)

Edited by Tian Gao

Merge request reports

Loading