InfoGrab Docs

MCP GraphQL 통합

요약

이 설계는 GitLab GraphQL API를 활용하는 Model Context Protocol(MCP) 도구를 만들기 위한 재사용 가능한 패턴을 소개합니다. 이 패턴을 통해 개발자는 일관된 보안 및 오류 처리 방식을 유지하면서 최소한의 보일러플레이트로 새로운 GraphQL 기반 MCP 도구를 만들 수 있습니다.

이 설계는 GitLab GraphQL API를 활용하는 Model Context Protocol(MCP) 도구를 만들기 위한 재사용 가능한 패턴을 소개합니다. 이 솔루션은 두 계층 아키텍처를 제공합니다: GraphQL 실행 및 오류 처리를 담당하는 재사용 가능한 GraphqlTool 클래스와 유효성 검사 및 응답 포맷팅을 처리하는 GraphqlService를 확장하는 서비스 래퍼입니다.

이 패턴을 통해 개발자는 일관된 보안 및 오류 처리 방식을 유지하면서 최소한의 보일러플레이트로 새로운 GraphQL 기반 MCP 도구를 만들 수 있습니다.

이 구현을 통해 AI 클라이언트(Duo 에이전트 플랫폼, Claude, Cursor 등)는 GraphQL 뮤테이션과 쿼리를 통해 GitLab 리소스에 대한 복잡한 작업을 수행할 수 있으며, 기존 GraphQL 스키마 정의와 인가 로직을 재사용합니다.

동기#

현재 상태#

GitLab MCP 구현은 REST API 엔드포인트를 MCP 도구로 노출하는 route_setting :mcp를 사용하는 경로 기반 아키텍처를 사용합니다. REST 작업에 효과적이지만 이 접근법은 GraphQL 통합에 한계가 있습니다:

  • GraphQL 기반 MCP 도구를 위한 내장 패턴 없음
  • 개발자가 GraphQL 실행을 수동으로 연결해야 함
  • REST와 GraphQL 도구 간의 일관성 없는 오류 처리
  • 일반적인 GraphQL 작업을 위한 재사용 가능한 추상화 없음
  • 원자적 다중 작업 요청 지원 없음

목표#

  1. 재사용성: GraphQL 기반 MCP 도구의 보일러플레이트를 제거하는 기본 클래스 생성
  2. 일관성: 모든 GraphQL 도구에 걸쳐 균일한 오류 처리 및 응답 포맷팅 유지
  3. 개발자 경험: 50줄 미만의 코드로 새 GraphQL MCP 도구 생성 가능
  4. 보안: 기존 GraphQL 인가 활용
  5. 타입 안전성: 런타임 오류 방지를 위해 정적 GraphQL 스키마 정의 사용
  6. 유지보수성: 더 쉬운 테스트를 위해 비즈니스 로직과 GraphQL 작업을 분리
  7. 원자성: 단일 요청에서 여러 뮤테이션 지원

비목표#

  • 기존 경로 기반 MCP 아키텍처 대체
  • GraphQL 프록시 또는 쿼리 빌더 생성
  • 도구 생성을 위한 자동 스키마 내성 구현
  • GraphQL 구독 지원(실시간 업데이트)
  • 범용 GraphQL 클라이언트 라이브러리 생성
  • 병렬 뮤테이션 실행(GraphQL은 순차적으로 뮤테이션을 실행함)

솔루션#

GraphQL 기반 MCP 도구를 위한 두 계층 아키텍처 도입:

계층 1: GraphQL 도구 클래스 (Mcp::Tools::GraphqlTool)

  • GraphQL 작업(쿼리/뮤테이션)을 정적 상수로 정의
  • 단일 뮤테이션 또는 하나의 요청에 여러 뮤테이션 지원
  • 입력 파라미터를 GraphQL 입력 형식으로 변환
  • GitlabSchema에 대해 작업 실행
  • 결과 처리 및 오류 처리 표준화

계층 2: 서비스 래퍼 (Mcp::Tools::*Service < GraphqlService)

  • 사용자 유효성 검사와 GraphQL 도구 실행을 제공하는 GraphqlService 확장
  • Versionable 관심사를 사용하여 버전별 로직 구현
  • 버전당 도구 메타데이터(설명, 입력 스키마) 정의
  • execute_graphql_tool을 통해 계층 1 GraphQL 도구 호출
  • MCP 프로토콜을 위한 응답 포맷팅
  • 서비스 수준 오류 및 로깅 처리

아키텍처 다이어그램#

Mermaid 다이어그램 (11줄)
소스 코드 보기
graph TB
    A[AI Client<br/>Claude/Cursor] -->|MCP Request| B[GraphqlCreateIssueService<br/>GraphqlService]
    B -->|params, version| E[CreateIssueTool<br/>GraphqlTool]
    E -->|GraphQL Mutation| F[GitlabSchema.execute]
    F -->|Response| E
    E -->|Structured Result| B
    B -->|MCP Response| A
style B fill:#e1f5fe
style E fill:#fff9c4
style F fill:#f3e5f5</code></pre></details></div>

데이터 흐름#

1. MCP Client Request
   ↓
2. Service.execute(params)
   ├─ Validate current_user exists
   ├─ Call super (BaseService.execute)
   │  ├─ Validate arguments against input_schema
   │  └─ Call perform(arguments)
   ↓
3. perform method calls execute_graphql_tool(arguments)
   ├─ Build GraphQL query/mutation with static schema
   ├─ Transform params → GraphQL input variables
   ├─ Execute GitlabSchema.execute(graphql_operation, variables)
   └─ Process result (success/errors)
   ↓
4. Format response
   ├─ Success: Response.success(message, payload)
   └─ Error: Response.error(message)
   ↓
5. Return to MCP Client

설계 및 구현 세부 사항#

계층 1: GraphQL 도구 기본 클래스#

파일: app/services/mcp/tools/graphql_tool.rb

목적: GraphQL 실행, 오류 처리 및 버저닝을 처리하는 GraphQL 기반 MCP 도구의 기본 클래스입니다.

module Mcp
  module Tools
    class GraphqlTool
      include Mcp::Tools::Concerns::Versionable

      attr_reader :current_user, :params

      def initialize(current_user:, params:, version: nil)
        @current_user = current_user
        @params = params
        initialize_version(version)
      end

      # Override in subclasses or use version metadata
      def graphql_operation
        raise NotImplementedError unless self.class.version_metadata(version)[:graphql_operation]

        self.class.version_metadata(version)[:graphql_operation]
      end

      def operation_name
        self.class.version_metadata(version)[:operation_name] ||
        raise(NotImplementedError, "operation_name must be defined")
      end

      # Can be overridden with version-specific methods
      def build_variables
        raise NotImplementedError, "build_variables must be implemented"
      end

      def execute
        result = GitlabSchema.execute(
          graphql_operation_for_version,
          variables: build_variables_for_version,
          context: execution_context
        )

        process_result(result)
      end

      private

      def execution_context
        {
          current_user: current_user,
          is_sessionless_user: false
        }
      end

      def process_result(result)
        # Handle GraphQL-level errors (syntax, validation, etc.)
        if result['errors']
          error_messages = extract_error_messages(result['errors'])
          return ::Mcp::Tools::Response.error(error_messages.join(', '))
        end

        operation_data = result.dig('data', operation_name)

        return ::Mcp::Tools::Response.error("Operation returned no data") if operation_data.nil?

        # Check for operation-specific errors
        operation_errors = operation_data['errors']
        if operation_errors&.any?
          error_messages = extract_error_messages(operation_errors)
          return ::Mcp::Tools::Response.error(error_messages.join(', '))
        end

        formatted_content = [{ type: 'text', text: Gitlab::Json.dump(operation_data) }]
        ::Mcp::Tools::Response.success(formatted_content, operation_data)
      end

      def extract_error_messages(errors)
        errors.map do |error|
          if error.is_a?(String)
            error
          elsif error.is_a?(Hash)
            error['message'] || error.to_s
          else
            error.to_s
          end
        end
      end
    end
  end
end

핵심 설계 결정:

  • 불변 작업: 상수로 정의된 GraphQL 문자열은 런타임 수정을 방지합니다.
  • MCP 응답 형식: Mcp::Tools::Response 객체(성공 또는 오류)를 반환합니다.
  • 유연한 작업 지원: 뮤테이션과 쿼리 모두 지원합니다.
  • 컨텍스트 격리: 각 도구 인스턴스는 독립적입니다(공유 상태 없음).
  • 프레임워크 비의존: 모든 GraphQL 작업 유형과 함께 작동합니다.
  • 오류 추출: GraphQL 수준과 작업 수준 오류를 모두 처리합니다.

계층 2: GraphqlService 기본 클래스#

파일: app/services/mcp/tools/graphql_service.rb

목적: 사용자 유효성 검사, 버저닝 지원 및 GraphQL 도구 실행을 갖춘 GraphQL 기반 MCP 도구를 위한 전문 기본 서비스를 제공합니다.

module Mcp
  module Tools
    class GraphqlService < BaseService
      include Mcp::Tools::Concerns::Versionable
      extend Gitlab::Utils::Override

      def initialize(name:, version: nil)
        super(name: name)
        initialize_version(version)
      end

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

      override :execute
      def execute(request: nil, params: nil)
        return Response.error("#{self.class.name}: current_user is not set") unless current_user.present?

        super
      end

      protected

      # Subclasses should override this to return their GraphQL tool class
      def graphql_tool_class
        raise NotImplementedError, "#{self.class.name}#graphql_tool_class must be implemented"
      end

      # Default implementation - can be overridden in subclasses
      def perform_default(_arguments = {})
          raise NoMethodError, "No implementation found for version #{version}"
      end

      private

      def execute_graphql_tool(arguments)
        tool = graphql_tool_class.new(
          current_user: current_user,
          params: arguments,
          version: version
        )

        tool.execute
      end
    end
  end
end

핵심 기능:

  • BaseService 확장: MCP 프로토콜 지원 및 유효성 검사 상속
  • Versionable: register_version으로 여러 버전 지원
  • 사용자 유효성 검사: 실행 전 current_user 설정 보장
  • GraphQL 도구 통합: GraphQL 도구를 인스턴스화하고 실행하기 위한 execute_graphql_tool 헬퍼 제공
  • 템플릿 패턴: 서브클래스는 graphql_tool_class와 버전별 perform_X_Y_Z 메서드를 정의합니다.

Versionable 관심사 향상#

파일: app/services/mcp/tools/concerns/versionable.rb

GraphQL 관련 메서드:

# Retrieve GraphQL operation from version metadata
def graphql_operation
  version_metadata.fetch(:graphql_operation) do
    raise NotImplementedError, "GraphQL operation not defined for version #{version}"
  end
end

# Retrieve operation name from version metadata
def operation_name
  version_metadata.fetch(:operation_name) do
    raise NotImplementedError, "operation_name must be defined"
  end
end

protected

# Get operation with fallback to method override
def graphql_operation_for_version
  version_metadata[:graphql_operation] || graphql_operation
end

# Call version-specific build_variables method or fallback
def build_variables_for_version
  method_name = "build_variables_#{version_method_suffix}"
  respond_to?(method_name, true) ? send(method_name) : build_variables
end

버전별 변수 빌딩:

도구는 버전별 변수 빌딩 메서드를 정의할 수 있습니다:

# Default implementation
def build_variables
  { input: { projectPath: params[:project_path] } }
end

# Version 2.0.0 specific implementation
def build_variables_2_0_0
  {
    input: {
      projectPath: params[:project_path],
      includeArchived: params[:include_archived]
    }.compact
  }
end

서비스 래퍼 패턴#

파일: app/services/mcp/tools/graphql_create_issue_service.rb

목적: 입력 유효성 검사, MCP 프로토콜 준수 및 버전 관리를 제공합니다. 인가는 GraphQL 계층에 위임됩니다.

module Mcp
  module Tools
    class GraphqlCreateIssueService < GraphqlService
      # Register version 0.1.0 with metadata
      register_version '0.1.0', {
        description: 'Create a new issue in a GitLab project using GraphQL mutation',
        input_schema: {
          type: 'object',
          properties: {
            project_path: { type: 'string', description: 'Full project path or ID' },
            title: { type: 'string', description: 'Issue title' },
            description: { type: 'string', description: 'Issue description' }
          },
          required: ['project_path', 'title']
        }
      }

      protected

      # Specify which GraphQL tool class to use
      def graphql_tool_class
        Mcp::Tools::CreateIssueTool
      end

      # Version 0.1.0 implementation
      def perform_0_1_0(arguments = {})
        execute_graphql_tool(arguments)
      end

      # Fallback to 0.1.0 behavior for any unimplemented versions
      override :perform_default
      def perform_default(arguments = {})
        perform_0_1_0(arguments)
      end
    end
  end
end

핵심 설계 결정:

  • GraphqlService 상속: 사용자 유효성 검사와 GraphQL 도구 실행 인프라 제공
  • 버전 등록: register_version을 사용하여 버전당 도구 메타데이터 정의
  • 버전별 메서드: 다른 버전을 위해 perform_0_1_0, perform_0_2_0 등을 구현
  • GraphQL 도구 클래스: 사용할 도구를 지정하기 위해 graphql_tool_class 오버라이드
  • 간소화된 perform 메서드: 도구 인스턴스화와 실행을 처리하는 execute_graphql_tool(arguments)를 호출
  • 응답 처리: 도구 실행은 MCP 응답을 직접 반환

뮤테이션 도구 예시#

파일: app/services/mcp/tools/create_issue_tool.rb

사용 사례: 기본 필드로 이슈 생성.

module Mcp
  module Tools
    class CreateIssueTool < GraphqlTool
      # Register version with GraphQL operation in metadata
      register_version '0.1.0', {
        operation_name: 'createIssue',
        graphql_operation: <<~GRAPHQL
          mutation($input: CreateIssueInput!) {
            createIssue(input: $input) {
              issue {
                id
                iid
                title
                description
                webUrl
                state
              }
              errors
            }
          }
          GRAPHQL
        }

      # Can register additional versions with different operations
      register_version '0.2.0', {
        operation_name: 'createIssue',
        graphql_operation: <<~GRAPHQL
          mutation($input: CreateIssueInput!) {
            createIssue(input: $input) {
              issue {
                id
                iid
                title
                description
                webUrl
                state
                createdAt
                updatedAt
              }
              errors
            }
          }
        GRAPHQL
      }

      # Default variable building (used by v0.1.0)
      def build_variables
        {
          input: {
            projectPath: params[:project_path],
            title: params[:title],
            description: params[:description],
            confidential: params[:confidential]
          }.compact
        }
      end

      private

      # Version-specific variable building for v0.2.0
      def build_variables_0_2_0
        {
          input: {
            projectPath: params[:project_path],
            title: params[:title],
            description: params[:description],
            confidential: params[:confidential],
            labelIds: params[:label_ids]
          }.compact
        }
      end
    end
  end
end

복합 도구 예시#

복합 도구는 여러 관련 작업을 단일하고 통합된 MCP 도구로 결합합니다. 각 다른 리소스에 대해 별도의 도구를 만드는 대신 복합 도구는 작업별 파라미터를 가진 통합 인터페이스를 제공합니다.

중요한 제한: 도구 호출당 하나의 뮤테이션 작업만 수행할 수 있습니다.

복합 도구의 장점:

  • 도구 확산 감소: AI 클라이언트가 발견하고 관리할 도구 수 감소
  • 일관된 인터페이스: 관련 작업들이 공통 파라미터를 공유
  • 더 나은 발견성: 논리적 그룹화로 사용자가 관련 기능을 더 쉽게 찾을 수 있음
  • 간소화된 유지보수: 공유된 유효성 검사 및 오류 처리 로직

예시 사용 사례:

  • 작업 항목 노트 생성(뮤테이션) - 유연한 식별 방법(URL, 프로젝트/그룹 ID + IID)으로 작업 항목에 노트(댓글) 생성을 처리하는 단일 도구
  • 작업 항목 노트 가져오기(쿼리) - 유연한 식별 방법(URL, 프로젝트/그룹 ID + IID)으로 작업 항목에서 노트(댓글) 가져오기를 처리하는 단일 도구

구현된 이슈:

  • #581890 - 작업 항목에 노트(댓글) 생성을 위한 GraphQL 기반 MCP 뮤테이션 도구
  • #581892 - 작업 항목에서 노트(댓글) 가져오기를 위한 GraphQL 기반 MCP 쿼리 도구

보안 모델#

인가 흐름:

  1. MCP 클라이언트가 service.execute(request:, params:) 호출
  2. GraphqlService.execute가 current_user 존재 여부 유효성 검사
  3. BaseService.executeperform(arguments) 호출
  4. perform 메서드가 execute_graphql_tool(arguments) 호출
  5. GraphQL 도구가 뮤테이션/쿼리 실행
  6. GraphQL이 뮤테이션/리졸버의 authorize 지시어를 통해 인가 강제

GraphQL 컨텍스트:

  • current_user: 모든 GraphQL 실행에 설정됨
  • is_sessionless_user: false: API/MCP 요청으로 표시
  • 컨텍스트는 다중 뮤테이션 요청의 모든 뮤테이션에 걸쳐 일관됨

오류 처리 전략#

세 가지 오류 수준:

  1. 서비스 수준 오류 (GraphqlService):
    • current_user 없음
    • 반환: Response.error(message)
  2. GraphQL 수준 오류 (구문, 유효성 검사):
    • 잘못된 GraphQL 구문
    • 타입 불일치
    • 반환: Response.error(joined_messages)
  3. 뮤테이션 수준 오류 (비즈니스 로직):
    • 유효성 검사 실패 (예: 빈 제목)
    • 상태 충돌
    • 반환: Response.error(joined_messages)

오류 전파:

GitlabSchema.execute → GraphqlTool.process_result →
GraphqlService.execute_graphql_tool → Response.error → MCP Client

다중 뮤테이션 오류 처리:

  • 실패: 어떤 뮤테이션이 실패하면 전체 작업이 실패로 표시됩니다.
  • 오류 집계: 모든 뮤테이션의 오류가 수집되어 함께 반환됩니다.
  • 예시: createIssue가 성공하지만 updateIssue가 실패하는 경우:
    • 전체 결과: Response.error(...)
    • 오류 포함: "updateIssue: Title can't be blank"

새 도구 만들기 - 단계별 가이드#

1단계: GraphQL 도구 클래스 정의

# app/services/mcp/tools/your_graphql_tool.rb
module Mcp
  module Tools
    class YourGraphqlTool < GraphqlTool
      # Register version with operation in metadata
      register_version '0.1.0', {
        operation_name: 'yourMutation',
        graphql_operation: <<~GRAPHQL
          mutation($input: YourInput!) {
            yourMutation(input: $input) {
              result { id title }
              errors
            }
          }
        GRAPHQL
      }

      # Implement variable building
      def build_variables
        {
          input: {
            projectPath: params[:project_path],
            title: params[:title]
          }.compact
        }
      end

      # Optional: Version-specific variable building
      private

      def build_variables_0_2_0
        {
          input: {
            projectPath: params[:project_path],
            title: params[:title],
            extraField: params[:extra_field]
          }.compact
        }
      end
    end
  end
end

2단계: 서비스 래퍼 생성

# app/services/mcp/tools/your_service.rb
module Mcp
  module Tools
    class YourService < GraphqlService
      # Register version with metadata
      register_version '0.1.0', {
        description: 'Description of what this tool does',
        input_schema: {
          type: 'object',
          properties: {
            project_path: { type: 'string', description: '...' },
            title: { type: 'string', description: '...' }
          },
          required: ['project_path', 'title']
        }
      }

      protected

      # Specify the GraphQL tool class to use
      def graphql_tool_class
        Mcp::Tools::YourGraphqlTool
      end

      # Version 0.1.0 implementation
      def perform_0_1_0(arguments = {})
        execute_graphql_tool(arguments)
      end

      # Fallback to 0.1.0 behavior for any unimplemented versions
      override :perform_default
      def perform_default(arguments = {})
        perform_0_1_0(arguments)
      end
    end
  end
end

3단계: Manager에 도구 등록

GraphQL 도구는 Mcp::Tools::Manager에서 커스텀 도구와 별도로 등록됩니다:

GRAPHQL_TOOLS = {
  'your_tool_name' => ::Mcp::Tools::YourService
}.freeze

3단계: 테스트 추가

  • GraphQL 도구에 대한 단위 테스트
  • 서비스에 대한 통합 테스트
  • ee/spec/services/ee/mcp/tools/manager_spec.rb의 테스트 업데이트
  • spec/requests/api/mcp/handlers/list_tools_spec.rbee/spec/requests/api/mcp/handlers/list_tools_spec.rb의 테스트 업데이트

대안 솔루션#

대안 1: 서비스에서 직접 GraphQL 실행#

접근법: 추상화 계층 없이 서비스 클래스에서 직접 GraphQL 실행.

class GraphqlCreateIssueService < GraphqlService
  def perform_0_1_0(params)
    result = GitlabSchema.execute(MUTATION, variables: params, context: {...})
    # Handle result inline
  end
end

장점:

  • 더 적은 추상화 계층
  • 모든 로직이 한 파일에

단점:

  • 코드 중복: 모든 서비스에서 오류 처리 반복
  • 재사용성 없음: GraphQL 작업을 다른 곳에서 사용할 수 없음
  • 테스트 복잡성: 모든 서비스 테스트에서 GraphQL 실행을 테스트해야 함
  • 일관성 없는 패턴: 각 개발자가 다르게 구현

결정: 유지보수성 문제로 거부.

대안 2: GraphQL 프록시 서비스#

접근법: MCP 클라이언트의 임의 GraphQL 쿼리를 허용하는 범용 서비스.

class GraphqlProxyService < GraphqlService
  def perform_0_1_0(params)
    query = params[:query]
    GitlabSchema.execute(query, variables: params[:variables], context: {...})
  end
end

장점:

  • 클라이언트에 최대한의 유연성
  • 새 도구를 만들 필요 없음

단점:

  • 보안 위험: 클라이언트가 인가되지 않은 쿼리를 실행할 수 있음
  • 스키마 유효성 검사 없음: 필수 필드를 강제할 수 없음
  • 성능 우려: 쿼리 복잡성 제한 없음
  • 발견성 없음: MCP 클라이언트가 어떤 작업이 가능한지 알 수 없음

결정: 보안 및 인가 우려로 거부.

대안 3: GraphQL 스키마에서 자동 생성된 도구#

접근법: GraphQL 스키마를 내성하여 MCP 도구를 자동으로 생성.

장점:

  • 수동 도구 생성 불필요
  • 스키마와 항상 동기화됨

단점:

  • 일반적인 설명: 자동 생성된 문서가 직접 작성한 것보다 덜 유용함
  • 과도한 노출: 승인된 것만이 아닌 모든 뮤테이션을 노출함
  • 파라미터 복잡성: 복잡한 중첩 입력을 단순화하기 어려움
  • 유지보수 부담: 스키마 변경이 모든 도구를 동시에 중단시킴

결정: 다음 반복에서 고려할 예정입니다. 수동 도구 생성이 더 나은 제어와 문서화를 제공합니다.

성능 고려사항#

쿼리 복잡성: GraphQL 도구는 GitLab 쿼리 복잡성 제한(기본 200)을 상속합니다.

캐싱: GraphQL 리졸버 수준 캐싱이 자동으로 적용됩니다.

배치 로딩: GraphQL의 내장 배치 로딩은 중첩 필드에 대한 N+1 쿼리를 방지합니다.

모니터링: 모든 GraphQL 실행은 기존 GraphqlLogger를 통해 로그됩니다.

성공 지표#

  • 채택: 생성된 GraphQL 기반 MCP 도구 수
  • 개발자 속도: 새 도구 생성 시간
  • 코드 재사용: 대안 접근법 대비 절약된 코드 줄 수

향후 개선 사항#

  • 다중 뮤테이션: 여러 뮤테이션 GraphQL 호출 지원. 서비스 클래스에서 다른 도구를 호출하고 응답을 집계하여 구현할 수 있습니다.
  • 종속 뮤테이션: 종속 뮤테이션을 지원하기 위한 여러 GraphQL 호출 실행
  • 자동 생성 도구: GraphQL 스키마 내성으로 MCP 도구 자동 생성

MCP GraphQL 통합

원문 보기
요약

이 설계는 GitLab GraphQL API를 활용하는 Model Context Protocol(MCP) 도구를 만들기 위한 재사용 가능한 패턴을 소개합니다. 이 패턴을 통해 개발자는 일관된 보안 및 오류 처리 방식을 유지하면서 최소한의 보일러플레이트로 새로운 GraphQL 기반 MCP 도구를 만들 수 있습니다.

이 설계는 GitLab GraphQL API를 활용하는 Model Context Protocol(MCP) 도구를 만들기 위한 재사용 가능한 패턴을 소개합니다. 이 솔루션은 두 계층 아키텍처를 제공합니다: GraphQL 실행 및 오류 처리를 담당하는 재사용 가능한 GraphqlTool 클래스와 유효성 검사 및 응답 포맷팅을 처리하는 GraphqlService를 확장하는 서비스 래퍼입니다.

이 패턴을 통해 개발자는 일관된 보안 및 오류 처리 방식을 유지하면서 최소한의 보일러플레이트로 새로운 GraphQL 기반 MCP 도구를 만들 수 있습니다.

이 구현을 통해 AI 클라이언트(Duo 에이전트 플랫폼, Claude, Cursor 등)는 GraphQL 뮤테이션과 쿼리를 통해 GitLab 리소스에 대한 복잡한 작업을 수행할 수 있으며, 기존 GraphQL 스키마 정의와 인가 로직을 재사용합니다.

동기#

현재 상태#

GitLab MCP 구현은 REST API 엔드포인트를 MCP 도구로 노출하는 route_setting :mcp를 사용하는 경로 기반 아키텍처를 사용합니다. REST 작업에 효과적이지만 이 접근법은 GraphQL 통합에 한계가 있습니다:

  • GraphQL 기반 MCP 도구를 위한 내장 패턴 없음
  • 개발자가 GraphQL 실행을 수동으로 연결해야 함
  • REST와 GraphQL 도구 간의 일관성 없는 오류 처리
  • 일반적인 GraphQL 작업을 위한 재사용 가능한 추상화 없음
  • 원자적 다중 작업 요청 지원 없음

목표#

  1. 재사용성: GraphQL 기반 MCP 도구의 보일러플레이트를 제거하는 기본 클래스 생성
  2. 일관성: 모든 GraphQL 도구에 걸쳐 균일한 오류 처리 및 응답 포맷팅 유지
  3. 개발자 경험: 50줄 미만의 코드로 새 GraphQL MCP 도구 생성 가능
  4. 보안: 기존 GraphQL 인가 활용
  5. 타입 안전성: 런타임 오류 방지를 위해 정적 GraphQL 스키마 정의 사용
  6. 유지보수성: 더 쉬운 테스트를 위해 비즈니스 로직과 GraphQL 작업을 분리
  7. 원자성: 단일 요청에서 여러 뮤테이션 지원

비목표#

  • 기존 경로 기반 MCP 아키텍처 대체
  • GraphQL 프록시 또는 쿼리 빌더 생성
  • 도구 생성을 위한 자동 스키마 내성 구현
  • GraphQL 구독 지원(실시간 업데이트)
  • 범용 GraphQL 클라이언트 라이브러리 생성
  • 병렬 뮤테이션 실행(GraphQL은 순차적으로 뮤테이션을 실행함)

솔루션#

GraphQL 기반 MCP 도구를 위한 두 계층 아키텍처 도입:

계층 1: GraphQL 도구 클래스 (Mcp::Tools::GraphqlTool)

  • GraphQL 작업(쿼리/뮤테이션)을 정적 상수로 정의
  • 단일 뮤테이션 또는 하나의 요청에 여러 뮤테이션 지원
  • 입력 파라미터를 GraphQL 입력 형식으로 변환
  • GitlabSchema에 대해 작업 실행
  • 결과 처리 및 오류 처리 표준화

계층 2: 서비스 래퍼 (Mcp::Tools::*Service < GraphqlService)

  • 사용자 유효성 검사와 GraphQL 도구 실행을 제공하는 GraphqlService 확장
  • Versionable 관심사를 사용하여 버전별 로직 구현
  • 버전당 도구 메타데이터(설명, 입력 스키마) 정의
  • execute_graphql_tool을 통해 계층 1 GraphQL 도구 호출
  • MCP 프로토콜을 위한 응답 포맷팅
  • 서비스 수준 오류 및 로깅 처리

아키텍처 다이어그램#

Mermaid 다이어그램 (11줄)
소스 코드 보기
graph TB
    A[AI Client<br/>Claude/Cursor] -->|MCP Request| B[GraphqlCreateIssueService<br/>GraphqlService]
    B -->|params, version| E[CreateIssueTool<br/>GraphqlTool]
    E -->|GraphQL Mutation| F[GitlabSchema.execute]
    F -->|Response| E
    E -->|Structured Result| B
    B -->|MCP Response| A
style B fill:#e1f5fe
style E fill:#fff9c4
style F fill:#f3e5f5</code></pre></details></div>

데이터 흐름#

1. MCP Client Request
   ↓
2. Service.execute(params)
   ├─ Validate current_user exists
   ├─ Call super (BaseService.execute)
   │  ├─ Validate arguments against input_schema
   │  └─ Call perform(arguments)
   ↓
3. perform method calls execute_graphql_tool(arguments)
   ├─ Build GraphQL query/mutation with static schema
   ├─ Transform params → GraphQL input variables
   ├─ Execute GitlabSchema.execute(graphql_operation, variables)
   └─ Process result (success/errors)
   ↓
4. Format response
   ├─ Success: Response.success(message, payload)
   └─ Error: Response.error(message)
   ↓
5. Return to MCP Client

설계 및 구현 세부 사항#

계층 1: GraphQL 도구 기본 클래스#

파일: app/services/mcp/tools/graphql_tool.rb

목적: GraphQL 실행, 오류 처리 및 버저닝을 처리하는 GraphQL 기반 MCP 도구의 기본 클래스입니다.

module Mcp
  module Tools
    class GraphqlTool
      include Mcp::Tools::Concerns::Versionable

      attr_reader :current_user, :params

      def initialize(current_user:, params:, version: nil)
        @current_user = current_user
        @params = params
        initialize_version(version)
      end

      # Override in subclasses or use version metadata
      def graphql_operation
        raise NotImplementedError unless self.class.version_metadata(version)[:graphql_operation]

        self.class.version_metadata(version)[:graphql_operation]
      end

      def operation_name
        self.class.version_metadata(version)[:operation_name] ||
        raise(NotImplementedError, "operation_name must be defined")
      end

      # Can be overridden with version-specific methods
      def build_variables
        raise NotImplementedError, "build_variables must be implemented"
      end

      def execute
        result = GitlabSchema.execute(
          graphql_operation_for_version,
          variables: build_variables_for_version,
          context: execution_context
        )

        process_result(result)
      end

      private

      def execution_context
        {
          current_user: current_user,
          is_sessionless_user: false
        }
      end

      def process_result(result)
        # Handle GraphQL-level errors (syntax, validation, etc.)
        if result['errors']
          error_messages = extract_error_messages(result['errors'])
          return ::Mcp::Tools::Response.error(error_messages.join(', '))
        end

        operation_data = result.dig('data', operation_name)

        return ::Mcp::Tools::Response.error("Operation returned no data") if operation_data.nil?

        # Check for operation-specific errors
        operation_errors = operation_data['errors']
        if operation_errors&.any?
          error_messages = extract_error_messages(operation_errors)
          return ::Mcp::Tools::Response.error(error_messages.join(', '))
        end

        formatted_content = [{ type: 'text', text: Gitlab::Json.dump(operation_data) }]
        ::Mcp::Tools::Response.success(formatted_content, operation_data)
      end

      def extract_error_messages(errors)
        errors.map do |error|
          if error.is_a?(String)
            error
          elsif error.is_a?(Hash)
            error['message'] || error.to_s
          else
            error.to_s
          end
        end
      end
    end
  end
end

핵심 설계 결정:

  • 불변 작업: 상수로 정의된 GraphQL 문자열은 런타임 수정을 방지합니다.
  • MCP 응답 형식: Mcp::Tools::Response 객체(성공 또는 오류)를 반환합니다.
  • 유연한 작업 지원: 뮤테이션과 쿼리 모두 지원합니다.
  • 컨텍스트 격리: 각 도구 인스턴스는 독립적입니다(공유 상태 없음).
  • 프레임워크 비의존: 모든 GraphQL 작업 유형과 함께 작동합니다.
  • 오류 추출: GraphQL 수준과 작업 수준 오류를 모두 처리합니다.

계층 2: GraphqlService 기본 클래스#

파일: app/services/mcp/tools/graphql_service.rb

목적: 사용자 유효성 검사, 버저닝 지원 및 GraphQL 도구 실행을 갖춘 GraphQL 기반 MCP 도구를 위한 전문 기본 서비스를 제공합니다.

module Mcp
  module Tools
    class GraphqlService < BaseService
      include Mcp::Tools::Concerns::Versionable
      extend Gitlab::Utils::Override

      def initialize(name:, version: nil)
        super(name: name)
        initialize_version(version)
      end

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

      override :execute
      def execute(request: nil, params: nil)
        return Response.error("#{self.class.name}: current_user is not set") unless current_user.present?

        super
      end

      protected

      # Subclasses should override this to return their GraphQL tool class
      def graphql_tool_class
        raise NotImplementedError, "#{self.class.name}#graphql_tool_class must be implemented"
      end

      # Default implementation - can be overridden in subclasses
      def perform_default(_arguments = {})
          raise NoMethodError, "No implementation found for version #{version}"
      end

      private

      def execute_graphql_tool(arguments)
        tool = graphql_tool_class.new(
          current_user: current_user,
          params: arguments,
          version: version
        )

        tool.execute
      end
    end
  end
end

핵심 기능:

  • BaseService 확장: MCP 프로토콜 지원 및 유효성 검사 상속
  • Versionable: register_version으로 여러 버전 지원
  • 사용자 유효성 검사: 실행 전 current_user 설정 보장
  • GraphQL 도구 통합: GraphQL 도구를 인스턴스화하고 실행하기 위한 execute_graphql_tool 헬퍼 제공
  • 템플릿 패턴: 서브클래스는 graphql_tool_class와 버전별 perform_X_Y_Z 메서드를 정의합니다.

Versionable 관심사 향상#

파일: app/services/mcp/tools/concerns/versionable.rb

GraphQL 관련 메서드:

# Retrieve GraphQL operation from version metadata
def graphql_operation
  version_metadata.fetch(:graphql_operation) do
    raise NotImplementedError, "GraphQL operation not defined for version #{version}"
  end
end

# Retrieve operation name from version metadata
def operation_name
  version_metadata.fetch(:operation_name) do
    raise NotImplementedError, "operation_name must be defined"
  end
end

protected

# Get operation with fallback to method override
def graphql_operation_for_version
  version_metadata[:graphql_operation] || graphql_operation
end

# Call version-specific build_variables method or fallback
def build_variables_for_version
  method_name = "build_variables_#{version_method_suffix}"
  respond_to?(method_name, true) ? send(method_name) : build_variables
end

버전별 변수 빌딩:

도구는 버전별 변수 빌딩 메서드를 정의할 수 있습니다:

# Default implementation
def build_variables
  { input: { projectPath: params[:project_path] } }
end

# Version 2.0.0 specific implementation
def build_variables_2_0_0
  {
    input: {
      projectPath: params[:project_path],
      includeArchived: params[:include_archived]
    }.compact
  }
end

서비스 래퍼 패턴#

파일: app/services/mcp/tools/graphql_create_issue_service.rb

목적: 입력 유효성 검사, MCP 프로토콜 준수 및 버전 관리를 제공합니다. 인가는 GraphQL 계층에 위임됩니다.

module Mcp
  module Tools
    class GraphqlCreateIssueService < GraphqlService
      # Register version 0.1.0 with metadata
      register_version '0.1.0', {
        description: 'Create a new issue in a GitLab project using GraphQL mutation',
        input_schema: {
          type: 'object',
          properties: {
            project_path: { type: 'string', description: 'Full project path or ID' },
            title: { type: 'string', description: 'Issue title' },
            description: { type: 'string', description: 'Issue description' }
          },
          required: ['project_path', 'title']
        }
      }

      protected

      # Specify which GraphQL tool class to use
      def graphql_tool_class
        Mcp::Tools::CreateIssueTool
      end

      # Version 0.1.0 implementation
      def perform_0_1_0(arguments = {})
        execute_graphql_tool(arguments)
      end

      # Fallback to 0.1.0 behavior for any unimplemented versions
      override :perform_default
      def perform_default(arguments = {})
        perform_0_1_0(arguments)
      end
    end
  end
end

핵심 설계 결정:

  • GraphqlService 상속: 사용자 유효성 검사와 GraphQL 도구 실행 인프라 제공
  • 버전 등록: register_version을 사용하여 버전당 도구 메타데이터 정의
  • 버전별 메서드: 다른 버전을 위해 perform_0_1_0, perform_0_2_0 등을 구현
  • GraphQL 도구 클래스: 사용할 도구를 지정하기 위해 graphql_tool_class 오버라이드
  • 간소화된 perform 메서드: 도구 인스턴스화와 실행을 처리하는 execute_graphql_tool(arguments)를 호출
  • 응답 처리: 도구 실행은 MCP 응답을 직접 반환

뮤테이션 도구 예시#

파일: app/services/mcp/tools/create_issue_tool.rb

사용 사례: 기본 필드로 이슈 생성.

module Mcp
  module Tools
    class CreateIssueTool < GraphqlTool
      # Register version with GraphQL operation in metadata
      register_version '0.1.0', {
        operation_name: 'createIssue',
        graphql_operation: <<~GRAPHQL
          mutation($input: CreateIssueInput!) {
            createIssue(input: $input) {
              issue {
                id
                iid
                title
                description
                webUrl
                state
              }
              errors
            }
          }
          GRAPHQL
        }

      # Can register additional versions with different operations
      register_version '0.2.0', {
        operation_name: 'createIssue',
        graphql_operation: <<~GRAPHQL
          mutation($input: CreateIssueInput!) {
            createIssue(input: $input) {
              issue {
                id
                iid
                title
                description
                webUrl
                state
                createdAt
                updatedAt
              }
              errors
            }
          }
        GRAPHQL
      }

      # Default variable building (used by v0.1.0)
      def build_variables
        {
          input: {
            projectPath: params[:project_path],
            title: params[:title],
            description: params[:description],
            confidential: params[:confidential]
          }.compact
        }
      end

      private

      # Version-specific variable building for v0.2.0
      def build_variables_0_2_0
        {
          input: {
            projectPath: params[:project_path],
            title: params[:title],
            description: params[:description],
            confidential: params[:confidential],
            labelIds: params[:label_ids]
          }.compact
        }
      end
    end
  end
end

복합 도구 예시#

복합 도구는 여러 관련 작업을 단일하고 통합된 MCP 도구로 결합합니다. 각 다른 리소스에 대해 별도의 도구를 만드는 대신 복합 도구는 작업별 파라미터를 가진 통합 인터페이스를 제공합니다.

중요한 제한: 도구 호출당 하나의 뮤테이션 작업만 수행할 수 있습니다.

복합 도구의 장점:

  • 도구 확산 감소: AI 클라이언트가 발견하고 관리할 도구 수 감소
  • 일관된 인터페이스: 관련 작업들이 공통 파라미터를 공유
  • 더 나은 발견성: 논리적 그룹화로 사용자가 관련 기능을 더 쉽게 찾을 수 있음
  • 간소화된 유지보수: 공유된 유효성 검사 및 오류 처리 로직

예시 사용 사례:

  • 작업 항목 노트 생성(뮤테이션) - 유연한 식별 방법(URL, 프로젝트/그룹 ID + IID)으로 작업 항목에 노트(댓글) 생성을 처리하는 단일 도구
  • 작업 항목 노트 가져오기(쿼리) - 유연한 식별 방법(URL, 프로젝트/그룹 ID + IID)으로 작업 항목에서 노트(댓글) 가져오기를 처리하는 단일 도구

구현된 이슈:

  • #581890 - 작업 항목에 노트(댓글) 생성을 위한 GraphQL 기반 MCP 뮤테이션 도구
  • #581892 - 작업 항목에서 노트(댓글) 가져오기를 위한 GraphQL 기반 MCP 쿼리 도구

보안 모델#

인가 흐름:

  1. MCP 클라이언트가 service.execute(request:, params:) 호출
  2. GraphqlService.execute가 current_user 존재 여부 유효성 검사
  3. BaseService.executeperform(arguments) 호출
  4. perform 메서드가 execute_graphql_tool(arguments) 호출
  5. GraphQL 도구가 뮤테이션/쿼리 실행
  6. GraphQL이 뮤테이션/리졸버의 authorize 지시어를 통해 인가 강제

GraphQL 컨텍스트:

  • current_user: 모든 GraphQL 실행에 설정됨
  • is_sessionless_user: false: API/MCP 요청으로 표시
  • 컨텍스트는 다중 뮤테이션 요청의 모든 뮤테이션에 걸쳐 일관됨

오류 처리 전략#

세 가지 오류 수준:

  1. 서비스 수준 오류 (GraphqlService):
    • current_user 없음
    • 반환: Response.error(message)
  2. GraphQL 수준 오류 (구문, 유효성 검사):
    • 잘못된 GraphQL 구문
    • 타입 불일치
    • 반환: Response.error(joined_messages)
  3. 뮤테이션 수준 오류 (비즈니스 로직):
    • 유효성 검사 실패 (예: 빈 제목)
    • 상태 충돌
    • 반환: Response.error(joined_messages)

오류 전파:

GitlabSchema.execute → GraphqlTool.process_result →
GraphqlService.execute_graphql_tool → Response.error → MCP Client

다중 뮤테이션 오류 처리:

  • 실패: 어떤 뮤테이션이 실패하면 전체 작업이 실패로 표시됩니다.
  • 오류 집계: 모든 뮤테이션의 오류가 수집되어 함께 반환됩니다.
  • 예시: createIssue가 성공하지만 updateIssue가 실패하는 경우:
    • 전체 결과: Response.error(...)
    • 오류 포함: "updateIssue: Title can't be blank"

새 도구 만들기 - 단계별 가이드#

1단계: GraphQL 도구 클래스 정의

# app/services/mcp/tools/your_graphql_tool.rb
module Mcp
  module Tools
    class YourGraphqlTool < GraphqlTool
      # Register version with operation in metadata
      register_version '0.1.0', {
        operation_name: 'yourMutation',
        graphql_operation: <<~GRAPHQL
          mutation($input: YourInput!) {
            yourMutation(input: $input) {
              result { id title }
              errors
            }
          }
        GRAPHQL
      }

      # Implement variable building
      def build_variables
        {
          input: {
            projectPath: params[:project_path],
            title: params[:title]
          }.compact
        }
      end

      # Optional: Version-specific variable building
      private

      def build_variables_0_2_0
        {
          input: {
            projectPath: params[:project_path],
            title: params[:title],
            extraField: params[:extra_field]
          }.compact
        }
      end
    end
  end
end

2단계: 서비스 래퍼 생성

# app/services/mcp/tools/your_service.rb
module Mcp
  module Tools
    class YourService < GraphqlService
      # Register version with metadata
      register_version '0.1.0', {
        description: 'Description of what this tool does',
        input_schema: {
          type: 'object',
          properties: {
            project_path: { type: 'string', description: '...' },
            title: { type: 'string', description: '...' }
          },
          required: ['project_path', 'title']
        }
      }

      protected

      # Specify the GraphQL tool class to use
      def graphql_tool_class
        Mcp::Tools::YourGraphqlTool
      end

      # Version 0.1.0 implementation
      def perform_0_1_0(arguments = {})
        execute_graphql_tool(arguments)
      end

      # Fallback to 0.1.0 behavior for any unimplemented versions
      override :perform_default
      def perform_default(arguments = {})
        perform_0_1_0(arguments)
      end
    end
  end
end

3단계: Manager에 도구 등록

GraphQL 도구는 Mcp::Tools::Manager에서 커스텀 도구와 별도로 등록됩니다:

GRAPHQL_TOOLS = {
  'your_tool_name' => ::Mcp::Tools::YourService
}.freeze

3단계: 테스트 추가

  • GraphQL 도구에 대한 단위 테스트
  • 서비스에 대한 통합 테스트
  • ee/spec/services/ee/mcp/tools/manager_spec.rb의 테스트 업데이트
  • spec/requests/api/mcp/handlers/list_tools_spec.rbee/spec/requests/api/mcp/handlers/list_tools_spec.rb의 테스트 업데이트

대안 솔루션#

대안 1: 서비스에서 직접 GraphQL 실행#

접근법: 추상화 계층 없이 서비스 클래스에서 직접 GraphQL 실행.

class GraphqlCreateIssueService < GraphqlService
  def perform_0_1_0(params)
    result = GitlabSchema.execute(MUTATION, variables: params, context: {...})
    # Handle result inline
  end
end

장점:

  • 더 적은 추상화 계층
  • 모든 로직이 한 파일에

단점:

  • 코드 중복: 모든 서비스에서 오류 처리 반복
  • 재사용성 없음: GraphQL 작업을 다른 곳에서 사용할 수 없음
  • 테스트 복잡성: 모든 서비스 테스트에서 GraphQL 실행을 테스트해야 함
  • 일관성 없는 패턴: 각 개발자가 다르게 구현

결정: 유지보수성 문제로 거부.

대안 2: GraphQL 프록시 서비스#

접근법: MCP 클라이언트의 임의 GraphQL 쿼리를 허용하는 범용 서비스.

class GraphqlProxyService < GraphqlService
  def perform_0_1_0(params)
    query = params[:query]
    GitlabSchema.execute(query, variables: params[:variables], context: {...})
  end
end

장점:

  • 클라이언트에 최대한의 유연성
  • 새 도구를 만들 필요 없음

단점:

  • 보안 위험: 클라이언트가 인가되지 않은 쿼리를 실행할 수 있음
  • 스키마 유효성 검사 없음: 필수 필드를 강제할 수 없음
  • 성능 우려: 쿼리 복잡성 제한 없음
  • 발견성 없음: MCP 클라이언트가 어떤 작업이 가능한지 알 수 없음

결정: 보안 및 인가 우려로 거부.

대안 3: GraphQL 스키마에서 자동 생성된 도구#

접근법: GraphQL 스키마를 내성하여 MCP 도구를 자동으로 생성.

장점:

  • 수동 도구 생성 불필요
  • 스키마와 항상 동기화됨

단점:

  • 일반적인 설명: 자동 생성된 문서가 직접 작성한 것보다 덜 유용함
  • 과도한 노출: 승인된 것만이 아닌 모든 뮤테이션을 노출함
  • 파라미터 복잡성: 복잡한 중첩 입력을 단순화하기 어려움
  • 유지보수 부담: 스키마 변경이 모든 도구를 동시에 중단시킴

결정: 다음 반복에서 고려할 예정입니다. 수동 도구 생성이 더 나은 제어와 문서화를 제공합니다.

성능 고려사항#

쿼리 복잡성: GraphQL 도구는 GitLab 쿼리 복잡성 제한(기본 200)을 상속합니다.

캐싱: GraphQL 리졸버 수준 캐싱이 자동으로 적용됩니다.

배치 로딩: GraphQL의 내장 배치 로딩은 중첩 필드에 대한 N+1 쿼리를 방지합니다.

모니터링: 모든 GraphQL 실행은 기존 GraphqlLogger를 통해 로그됩니다.

성공 지표#

  • 채택: 생성된 GraphQL 기반 MCP 도구 수
  • 개발자 속도: 새 도구 생성 시간
  • 코드 재사용: 대안 접근법 대비 절약된 코드 줄 수

향후 개선 사항#

  • 다중 뮤테이션: 여러 뮤테이션 GraphQL 호출 지원. 서비스 클래스에서 다른 도구를 호출하고 응답을 집계하여 구현할 수 있습니다.
  • 종속 뮤테이션: 종속 뮤테이션을 지원하기 위한 여러 GraphQL 호출 실행
  • 자동 생성 도구: GraphQL 스키마 내성으로 MCP 도구 자동 생성