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 작업을 위한 재사용 가능한 추상화 없음
- 원자적 다중 작업 요청 지원 없음
목표#
- 재사용성: GraphQL 기반 MCP 도구의 보일러플레이트를 제거하는 기본 클래스 생성
- 일관성: 모든 GraphQL 도구에 걸쳐 균일한 오류 처리 및 응답 포맷팅 유지
- 개발자 경험: 50줄 미만의 코드로 새 GraphQL MCP 도구 생성 가능
- 보안: 기존 GraphQL 인가 활용
- 타입 안전성: 런타임 오류 방지를 위해 정적 GraphQL 스키마 정의 사용
- 유지보수성: 더 쉬운 테스트를 위해 비즈니스 로직과 GraphQL 작업을 분리
- 원자성: 단일 요청에서 여러 뮤테이션 지원
비목표#
- 기존 경로 기반 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 프로토콜을 위한 응답 포맷팅
- 서비스 수준 오류 및 로깅 처리
아키텍처 다이어그램#
소스 코드 보기
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 쿼리 도구
보안 모델#
인가 흐름:
- MCP 클라이언트가
service.execute(request:, params:) 호출
GraphqlService.execute가 current_user 존재 여부 유효성 검사
BaseService.execute가 perform(arguments) 호출
perform 메서드가 execute_graphql_tool(arguments) 호출
- GraphQL 도구가 뮤테이션/쿼리 실행
- GraphQL이 뮤테이션/리졸버의
authorize 지시어를 통해 인가 강제
GraphQL 컨텍스트:
current_user: 모든 GraphQL 실행에 설정됨
is_sessionless_user: false: API/MCP 요청으로 표시
- 컨텍스트는 다중 뮤테이션 요청의 모든 뮤테이션에 걸쳐 일관됨
오류 처리 전략#
세 가지 오류 수준:
- 서비스 수준 오류 (GraphqlService):
current_user 없음
- 반환:
Response.error(message)
- GraphQL 수준 오류 (구문, 유효성 검사):
- 잘못된 GraphQL 구문
- 타입 불일치
- 반환:
Response.error(joined_messages)
- 뮤테이션 수준 오류 (비즈니스 로직):
- 유효성 검사 실패 (예: 빈 제목)
- 상태 충돌
- 반환:
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.rb 및 ee/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 도구 자동 생성
