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.
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)