MCP 서버 개발 가이드라인
GitLab v19.5요약
이 페이지는 GitLab MCP 서버를 개발하고 사용하는 방법에 대한 정보를 포함합니다. 개발 환경을 설정하려면 다음을 수행합니다. 더 자세한 로깅을 위해 mcp-remote 명령에 --debug를 추가합니다. Claude Desktop은 지원되지 않는 버전의 Node.js를 사용합니다.
이 페이지는 GitLab MCP 서버를 개발하고 사용하는 방법에 대한 정보를 포함합니다.
개발 환경 설정#
개발 환경을 설정하려면 다음을 수행합니다.
- GDK에서 HTTPS 활성화 및 구성.
node를 설치하고mcp-remote를 전역으로 설치합니다. GDK에는 Node.js가 포함되어 있지만, 설치된 AI 어시스턴트는 GDK 버전을 사용할 수 없습니다.- AI 어시스턴트를 MCP 서버에 연결합니다.
디버깅 및 문제 해결#
Cursor 디버그#
더 자세한 로깅을 위해 mcp-remote 명령에 --debug를 추가합니다. Output을 열고
MCP:SERVERNAME을 선택해 MCP 서버 로그를 확인합니다. 아래 예시의 경우 MCP:user-GitLab-GDK입니다.
{
"mcpServers": {
"GitLab-GDK": {
"command": "npx",
"args": [
"mcp-remote",
"https://gdk.test:3443/api/v4/mcp",
"--debug"
],
"env": {
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
}
}
}
Claude Desktop 디버그#
Node.js 버전 확인#
Claude Desktop은 지원되지 않는 버전의 Node.js를 사용합니다. 특정 버전을 사용하는 커스텀 래퍼 스크립트를 생성합니다.
#!/bin/bash
# Force use of your Node.js version
NODE_BIN="/PATH_TO_NODE_INSTALL/node/22.17.0/bin/node"
MCP_REMOTE_BIN="/PATH_TO_NODE_INSTALL/node/22.17.0/bin/mcp-remote"
# Run mcp-remote with your Node.js
exec "$NODE_BIN" "$MCP_REMOTE_BIN" "$@"
Claude Desktop 구성에서 이 래퍼 스크립트를 사용합니다.
{
"mcpServers": {
"GitLab-GDK": {
"command": "/PATH_TO_REMOTE_WRAPPER_SCRIPT/mcp-remote-wrapper",
"args": [
"https://gdk.test:3443/api/v4/mcp",
"--debug"
],
"env": {
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
}
}
}
mcp-remote 디버그#
AI 어시스턴트 외부에서 mcp-remote에서 GDK로의 인증을 테스트합니다.
NODE_TLS_REJECT_UNAUTHORIZED=0 npx mcp-remote https://gdk.test:3443/api/v4/mcp --debug
브랜치를 전환하면 인증 문제가 발생할 수 있으며, 로그에 UNABLE_TO_VERIFY_LEAF_SIGNATURE
오류가 포함될 수 있습니다. 체인의 신뢰할 수 없는 인증서 오류는 TLS를 사용하는 GDK 인스턴스에만 해당합니다. 이 오류는
npx를 통해 사용되는 Node.js의 https 클라이언트와 mcp-remote 라이브러리에서 비롯됩니다. https 클라이언트는
번들된 인증 기관 목록 밖에서 서명된 인증서를 신뢰하지 않습니다.
인증 문제가 발생하는 경우, 최후의 수단으로 ~/.mcp-auth 디렉터리를 삭제하면 mcp-remote에 저장된
자격 증명이 초기화됩니다. AI 어시스턴트가 MCP 서버에 다시 연결할 때 브라우저 창이 열려 인가를
요청합니다.
rm -rf ~/.mcp-auth
MCP Inspector로 디버그#
MCP Inspector는 MCP 서버를 테스트하고 디버깅하기 위한 대화형 개발자 도구입니다.
다음 명령은 서버에 연결하고 MCP 도구를 나열하고 실행할 수 있는 직관적인 Web UI를 엽니다.
npx -y @modelcontextprotocol/inspector npx
개발 워크플로#
유용한 링크#
새 도구 추가#
도구 제안 프로세스#
현재 개발 가이드라인은 아직 초기 단계입니다. 특히 커스텀 도구와 집계 도구에 대한 도구 개발 표준을 계속 확립해 나가는 과정에서,
구현 전에 제안된 도구를 평가하고 새 MCP 도구를 계획하는 팀을 안내하기 위해 임시 mcp-tool-review-board 위원회를 만들었습니다.
새 도구를 추가하려면 MCP 도구 제안 이슈를 생성하고 템플릿 지침을 따릅니다.
도구 구현 위치는 GitLab 리소스와의 상호작용 여부에 따라 달라집니다.
- GitLab 리소스와 상호작용하는 도구는 최종적으로 MCP 서버에 있어야 하지만, 단기적이거나 긴급한 필요가 있으면 에이전트 플랫폼에 구현할 수 있습니다.
- GitLab 리소스와 상호작용하지 않는 도구는 에이전트 플랫폼에 구현합니다.
MCP 서버 기능을 에이전트 플랫폼에 통합하는 작업을 진행 중입니다. 이 이슈에서 진행 상황을 확인할 수 있습니다.
모든 엔지니어가 도구 제안 프로세스를 따르고 사용 사례를 명확히 설명하기를 강력히 권장합니다.
도구 이름 지정 및 통합 규칙#
도구를 추가하거나 통합할 때는 도구 표면을 작고 예측 가능하게 유지하기 위해 다음 규칙을 따릅니다. 이 규칙은 기능을 잃지 않으면서 도구 수와 토큰 오버헤드를 줄입니다.
동사 클래스: 모든 도구 이름은 verb_object 형태를 사용하며, 동사는 작업 클래스를
나타냅니다.
- 단일 객체에는
get_, 컬렉션에는list_를 사용합니다. - 생성 및 업데이트 필드 변경에는
save_를 사용합니다.id의 존재 여부가 해당 작업이 생성인지 업데이트인지를 결정합니다. 생성 시 필수인 파라미터는 도구 정의에 그렇게 표시합니다. 의도적인 예외:save_도구는 필드 변경이 아닌 수명 주기 액션(예:retry,cancel)을action파라미터 뒤에 묶을 수도 있습니다. 단, 그 액션이 도구가 생성하는 것과 동일한 리소스에서 동작하고 전용 도구를 따로 둘 만하지 않은 경우에 한합니다. 상위 식별자가 아니라 리소스 자체의 ID 존재 여부로 라우팅합니다. 예를 들어save_pipeline은pipeline_id가 없으면 생성으로,pipeline_id가 있고action이 함께 오면 해당 파이프라인에 대한 수명 주기 전환으로 처리합니다. 이를 도구 설명에 문서화하고 의도적인 예외로 기록합니다(아래 참조). - 실제 삭제 작업에는
delete_를 사용합니다. 거버넌스 처리를 더 잘하기 위해 이를save_에 묶어서는 안 됩니다. add_또는 이 패턴에서 벗어난 다른 형태는 커밋, 브랜치, 세션처럼 일반적인 CRUD 형태를 갖지 않는 객체에 예약되어 있습니다(예:add_commit,add_branch).
리소스 식별: 모든 프로젝트 범위 도구는 동일한 방식으로 대상을 식별합니다.
각 도구는 자체 입력 스키마에 url, project_id, 리소스의 내부 ID를 선언합니다.
이 파라미터들을 해석하려면 Mcp::Tools::Concerns::UrlParser와
Mcp::Tools::Concerns::ResourceFinder concern을 포함합니다.
호출자는 다음 중 하나를 제공합니다.
url- 전체 경로를 인코딩한 전체 GitLab URL(예:https://gitlab.com/group/project/-/merge_requests/1), 또는- ID 그룹 -
project_id(숫자 ID 또는gitlab-org/gitlab과 같은 전체 경로)와 리소스의 내부 ID(merge_request_iid,work_item_iid,commit_sha등).
일반 전체 경로(gitlab-org/gitlab)와 URL 인코딩된 형식(gitlab-org%2Fgitlab) 모두
ResourceFinder 또는 ApiTool로 프로젝트나 그룹을 해석하는 모든 도구에서 프로젝트 또는 그룹 인수로
동작합니다. MCP 경계는 값이 조회 로직에 도달하기 전에 퍼센트 디코딩합니다. GraphQL 기반 도구와 커스텀 도구는
ResourceFinder#find_project!와 #find_group!에서, REST 기반 도구는 ApiTool#execute에서 디코딩하며,
이는 Rack이 REST 요청의 경로 세그먼트에 적용하는 디코딩과 같습니다. 경로를 GraphQL에 그대로 전달하는 도구
(예: search_labels 또는 list_vulnerabilities)는 두 디코딩 지점을 모두 건너뛰며 일반 형식만 받습니다.
예시와 도구 자체의 input_schema 설명에는 일반 전체 경로를 사용합니다. 디코딩은 URL 인코딩된 형식을
안내하는 REST 경로에서 복사한 Grape 파생 설명이 틀린 내용이 되지 않도록 하기 위해서만 존재합니다.
REST API도 사용하는 Gitlab::ResourceLookup은 디코딩하지 않습니다. 호출자가 이미 디코딩된 값을 받으며,
여기서 디코딩하면 REST API가 이중 인코딩된 경로를 받아들이게 되기 때문입니다.
프로젝트 식별자와 내부 ID는 별도의 파라미터로 유지합니다.
이들을 하나의
id로 묶지 않습니다. 서로 다른 값이며(iid/sha는 프로젝트 범위여서
project_id 없이는 의미가 없습니다), url은 이미 이들을 하나로 합쳐 주는 단일 값 편의 수단입니다. url과
ID가 모두 제공되면 서로 교차 검증되며, 불일치하면 오류가 발생합니다. 작업 항목 도구는 같은 그룹에서
group_id 또는 project_id를 허용합니다.
ResourceFinder#find_project!와 #find_group!는 인가를 데이터베이스(DB) 조회에 통합합니다.
레코드가 없거나 호출자에게 필요한 ability가 없으면 둘 모두 동일한 오류
"'<id>' not found or inaccessible"를 발생시킵니다. 이는 인증된 호출자가 오류 문자열을 비교해
비공개 프로젝트나 그룹을 열거하는 것을 방지합니다. 이 finder를 호출한 뒤에 별도의 인가 확인을
추가하지 않습니다. 기본값이 아닌 ability가 필요하면 ability: 키워드로 전달합니다.
find_project!(project_id, ability: :read_merge_request).
선택적 파라미터: Base::BaseService는 선택적 파라미터에 명시적으로 전달된 null 또는 "" 값을
키가 생략된 것과 동일하게 처리하므로, 모든 스키마 속성을 채워 보내는 호출자도
선택적 enum 파라미터에 대한 검증을 통과합니다.
읽기(단일 대 컬렉션):
- 하나의 상위 객체로 범위가 지정된 패싯은 별도의
list_*도구가 아니라include파라미터를 통해 해당 객체의get_도구에 통합됩니다(예:include: ["diffs"]를 사용하는get_merge_request). 유효한 값은diffs,commits,notes,pipelines,discussions입니다. 호출당 패싯 하나만 지원하더라도include는 열거형 값의 배열로 선언하고maxItems로 제한합니다. 나중에 그 상한을 올리는 것은 추가적인 변경이지만, 파라미터를 문자열에서 배열로 바꾸는 것은 기존 호출자를 중단시킵니다. 단일 패싯으로 범위가 지정된 필터(job_status)를 포함해 이 패턴을 구현한 예시는get_pipeline(include: ["jobs"]/["downstream_pipelines"]/["bridge_jobs"])을 참고합니다. - 자체적으로 조회할 수 있는 독립적인 컬렉션은 자체
list_도구를 갖습니다(예:list_merge_requests,list_pipelines). - 패싯 범위 페이지네이션은
get_리더에 위치하며 관련된include값에만 적용됩니다. 이를 파라미터 설명에 문서화합니다. 파라미터 이름은<facet>_first와<facet>_after로 지정하고, 끝에서부터 읽는 것이 중요한 경우에는<facet>_last/<facet>_before를 추가합니다(예: 가장 최근 노트). 이 파라미터들은 공통 상한을 강제하는Mcp::Tools::Concerns::CursorPagination.input_schema_params로 만듭니다. 노드와 함께 연결의pageInfo를 반환합니다. 구현 예시는get_merge_request(정방향 전용)의commits패싯과get_merge_request및get_work_item(양방향)의notes패싯을 참고합니다. - diff가 페이로드의 대부분을 차지하는 diff 포함 읽기에는
detail열거형(none/stats/full_patch)을 추가합니다(예:get_commit의diff패싯과get_merge_request의diffs패싯). 더 적합한 조절 수단이 이미 있는 곳에는detail을 소급 적용하지 않습니다. 파일 내용은 줄 페이지네이션(offset/limit)을, job 로그는 바이트 페이지네이션 (byte_offset/byte_limit)을 사용하며, 이러한 API에는 diff 방식의 상세 수준이 없기 때문입니다. 단일 목적 리더가 패싯으로 승격된 예시는get_job(include: log)을 참고합니다. - 한 패싯이 다른 패싯의 부분집합인 경우에는 새 패싯이나 새 도구보다 필터 파라미터를 우선합니다(예:
별도의
failing_jobs패싯 대신job_status: failed필터).
페이지네이션: 도구의 페이지네이션 방식은 서버 전역의 단일 규칙을 강요하지 않고, 그 도구가 감싸는 엔드포인트의 방식을 그대로 따릅니다. 방식 간 변환을 하지 않습니다(예: 페이지 번호를 불투명한 커서로 감싸지 않습니다). 오프셋 페이지네이션 위에 합성한 커서는 커서 안정성을 얻지 못한 채 메커니즘만 숨기고, 엔드포인트가 제공하는 보장에 대해 호출자를 오해하게 만듭니다.
- REST 기반 도구는
page(1부터 시작, 기본값1)와per_page(기본값20, 최대100)를 사용하는 오프셋 페이지네이션을 사용하며,page,per_page,has_more를 포함하는metadata객체를 반환합니다.list_branches,list_pipelines,search같은 도구에 적용됩니다. - GraphQL 기반 도구는
first(기본값20, 최대100)와after(불투명한 커서)를 사용하는 네이티브 커서 페이지네이션을 사용하며,endCursor와hasNextPage를 포함하는pageInfo객체를 반환합니다.list_work_items,list_merge_requests,list_commits,list_repository_tree같은 도구에 적용됩니다.list_repository_tree는 기반 커넥션이 페이지 크기를 100으로 고정하므로first파라미터를 받지 않습니다. - 콘텐츠 리더는 항목 페이지네이션 대신 페이로드에 맞는 범위 윈도우를 사용합니다. 파일
내용은 줄 페이지네이션(
offset/limit)을, job 로그는 바이트 페이지네이션 (byte_offset/byte_limit)을 사용합니다. 다음 윈도우를 가져오는 방법을 호출자에게 알려 주는system_instruction을 반환합니다. get_리더의 패싯 범위 페이지네이션에는 패싯 이름을 접두사로 붙이며(예:get_merge_request의notes_first/notes_after/notes_last/notes_before,get_commit의notes_first/notes_after)와 같이 붙이며, 해당 패싯을 지원하는 엔드포인트의 방식을 따릅니다.
출력 ID(GraphQL 기반 도구): 도구가 전역 숫자 ID로 지정하는 리소스, 즉 그룹·프로젝트·파이프라인에
대해서는 process_result에서 GID를 숫자 정수로 풀어냅니다. 그래야 출력이 project_id나 group_id
같은 입력으로 바로 이어집니다
(GlobalID.parse(node['id']).model_id.to_i. get_project_tool, list_groups_tool,
list_projects_tool 참고). iid로 지정되는 리소스(작업 항목, 머지 리퀘스트, 이슈)의 ID는 풀어내지
않습니다. 이 경우 GlobalID#model_id는 iid가 아니라 전역 데이터베이스 키를 반환하며,
이를 *_iid 파라미터로 되돌려 넣는 에이전트는 조용히 잘못된 레코드를 대상으로 동작합니다.
대신 GID와 함께 iid를 노출합니다.
확산보다 통합:
- 리소스별 검색 도구를 추가하는 대신, 범위가 지정된 검색 변형들을
scope파라미터가 있는 통합search도구로 병합합니다.
의도적인 예외를 문서화합니다. 도구가 의도적으로 규칙을 벗어나는 경우(일회성 액션
동사, 하나의 리소스에 대한 두 번째 write_ 도구)에는 실수로 오인되지 않도록
제안서에 의도적임을 기록합니다.
REST API 경로에서 도구 구현#
이 머지 리퀘스트는 API 경로에서 MCP 도구를 만드는 프로세스를 정의합니다.
API 경로 정의에 다음 route_setting을 추가합니다.
route_setting :mcp, tool_name: :get_issue, params: [:id, :issue_iid], resource_name: "issue"
- 도구 목록에
get_issue도구를 추가하고 그 실행을 활성화합니다 - 도구와 파라미터 설명은 OpenAPI 경로 정의에서 가져옵니다
- 허용되는 파라미터는
params인수로 필터링됩니다. 예를 들어id와issue_iid만 공개되고 허용됩니다 - 도구가 호출되면 전달된 파라미터로 경로 코드가 직접 실행됩니다
- 선택적
resource_name필드는 리소스별 404 오류 메시지를 제공합니다(예: 일반적인"404 Not Found"대신"404 Issue Not Found")."issue"나"merge request"같은 소문자 문자열을 사용합니다. 렌더링된 메시지에서는 첫 글자가 대문자로 표시됩니다.
이 머지 리퀘스트는 더 많은 예시를 제공합니다.
경로의 HTTP 메서드에 대한 MCP 토큰 액세스 선언#
MCP 범위 토큰은 MCP 경로가 사용하는 각 HTTP 메서드에 대해 API 클래스에
명시적인 액세스 선언을 요구합니다. 이 선언이 없으면
route_setting :mcp가 있어도 MCP 토큰 요청이 거부됩니다.
경로가 포함된 API 클래스에 해당하는 allow_mcp_access_* 호출을 추가합니다.
| HTTP 메서드 | 필요한 호출 |
|---|---|
GET / HEAD |
allow_mcp_access_read |
POST |
allow_mcp_access_create |
PUT / PATCH |
allow_mcp_access_update |
DELETE |
allow_mcp_access_delete |
예를 들어 GET 경로와 PUT 경로를 모두 MCP 도구로 노출하는 API 클래스는
두 선언을 모두 포함해야 합니다.
class MergeRequests < ::API::Base
include ::API::Concerns::McpAccess
allow_mcp_access_read
allow_mcp_access_create
allow_mcp_access_update
# ...
end
집계된 REST API 도구 구현#
집계된 API 도구는 여러 관련 API 도구를 하나의 통합 인터페이스로 결합해 도구 수를 줄이고 사용자 경험을 개선합니다. 검색 도구는 전역·그룹·프로젝트 검색을 하나의 도구로 통합해 이 패턴을 보여 줍니다.
집계 도구를 사용하는 경우:
비슷한 목적을 수행하면서 서로 다른 범위(전역, 그룹, 프로젝트)에서 동작하는 API 엔드포인트가 여럿 있을 때 집계 도구를 사용합니다. 이렇게 하면 도구 세 개 대신 하나를 제시해 LLM의 인지 부하를 줄입니다.
구현 단계:
Mcp::Tools::Base::AggregatedService를 상속하는 집계 서비스 클래스를 생성합니다.
module Mcp
module Tools
class ExampleAggregatedService < Base::AggregatedService
include Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override
register_version '0.1.0', {
description: 'My example aggregated tool',
input_schema: {
type: 'object',
properties: {},
required: []
}
}
override :tool_name
def self.tool_name
'new_tool'
end
override :select_tool
def select_tool(args)
tool_name = if args[:group_id]
:example_tool_for_group
elsif args[:project_id]
:example_tool_for_project
end
tools.find { |tool| tool.name.to_sym == tool_name }
end
override :transform_arguments
def transform_arguments(args)
if args[:group_id]
args.merge(id: args[:group_id])
elsif args[:project_id]
args.merge(id: args[:project_id])
else
args
end
end
end
end
end
- 경로 정의에서 기반 API 도구를 집계기에 등록합니다.
route_setting :mcp, tool_name: :example_tool_for_group, params: [:id], aggregators: [::Mcp::Tools::ExampleAggregatedService]
route_setting :mcp, tool_name: :example_tool_for_project, params: [:id], aggregators: [::Mcp::Tools::ExampleAggregatedService]
Mcp::Tools::Manager는aggregators가 지정된 경로를 스캔해 집계된 도구를 자동으로 발견하고, 수집된 도구로 집계기 클래스를 인스턴스화합니다.
GraphQL 기반 도구 구현#
GitLab GraphQL API를 사용하는 MCP 도구는 GraphQL 통합 가이드라인을 참고합니다.
AI 코딩 어시스턴트로 GraphQL 기반 도구를 스캐폴딩하려면
gitlab-mcp-tool-builder 스킬을 사용합니다.
이 스킬은 이 페이지와 GraphQL 통합 가이드라인의 지침을 요약하고,
흔히 겪는 함정을 함께 담고 있으며, 도구 클래스, 서비스 클래스,
.graphql 작업 파일, 매니저 등록, 스펙 작성 과정을 안내합니다.
이 리포지터리는 .claude/skills/gitlab-mcp-tool-builder/에 이 스킬을 포함하고 있으므로,
따로 설치할 것이 없습니다.
- Claude Code와 GitLab Duo CLI는 세션 시작 시 이 스킬을 발견합니다. 어시스턴트에게 GraphQL MCP 도구를 빌드하거나 스캐폴딩하도록 요청하거나, 스킬을 이름으로 호출합니다. GitLab Duo CLI가 스킬을 발견하고 실행하는 방법은 에이전트 스킬을 참고합니다.
AGENTS.md규칙을 따르는 어시스턴트는.agents/skills심볼릭 링크를 통해 같은 스킬을 로드합니다.
가이드라인이 신뢰할 수 있는 원본입니다. 스킬과 가이드라인이 서로 다르면 가이드라인을 따르고 스킬을 업데이트합니다.
도구를 관할하는 네임스페이스 선언#
관리자는 프로젝트나 그룹에 도구 규칙(Allow, Ask, Deny)을 설정할 수 있습니다. 호출에 규칙을
적용하려면 서버가 그 호출이 어느 프로젝트나 그룹에 작용하는지 알아야 하고, 서버는 그것을
도구 자체의 선언에서 알아냅니다. 핸들러는 list_vulnerabilities가 프로젝트를
project_full_path로 지정하고 search_labels는 full_path로 지정한다는 것을 짐작할 수 없습니다.
쓸 수 있는 선언이 없는 도구는 네임스페이스를 해석하지 못합니다. 이 도구는 거버넌스 없이 제공되며,
그 호출에는 어떤 도구 규칙도 적용되지 않습니다.
계약은 Mcp::Tools::Concerns::GovernanceContainerResolver에 있으며 모든 도구가 이를 포함합니다.
클래스 수준의 container_arguments 매크로와 이를 읽는
governed_containers 메서드를 제공합니다.
기본값. 모든 도구는 다음을 상속합니다.
{ project: :project_id, group: :group_id, project_or_group: :url }
도구의 입력 스키마가 이미 인수 이름을 project_id 또는 group_id로 쓰고 있다면 아무것도
바꿀 필요가 없습니다. 리졸버는 두 인수 모두에 대해 숫자 ID, 전체 경로, Global ID를
허용합니다.
재정의. 도구가 인수 이름을 다르게 지정한다면 서비스 클래스에서 container_arguments 매크로를
호출합니다. 전달한 종류는 일치하는 기본 키를 대체하고 나머지 기본값은 유지됩니다. 예를 들어
list_vulnerabilities는 프로젝트를 project_id 대신 project_full_path로 지정합니다.
module Mcp
module Tools
module Security
class ListVulnerabilitiesService < Base::GraphqlService
container_arguments project: :project_full_path
# ...
end
end
end
end
도구가 프로젝트와 그룹을 따로 지정해야 하는 것은 아닙니다. search_labels는 둘 중 어느 쪽이든
가리킬 수 있는 full_path 인수 하나만 받으므로, 대신 :project_or_group 항목 하나를
선언합니다.
class SearchService < Base::GraphqlService
container_arguments project_or_group: :full_path
end
인식되는 컨테이너 종류. resolve_governance_containers는 다음 키만 읽습니다.
다른 키는 무시되므로, 도구는 아무것도 해석하지 못하고 거버넌스 대상이 되지 않습니다.
| 종류 | 해석 결과 |
|---|---|
:project |
프로젝트입니다. |
:group |
그룹입니다. |
:project_or_group |
프로젝트나 그룹 중 어느 쪽이든 지정할 수 있는 인수 하나로, 먼저 프로젝트로 해석하고 그다음 그룹으로 해석합니다. |
Global ID 자체의 클래스는 선언된 종류와 일치해야 합니다. :project 인수에 그룹의 Global ID를
전달하면 숫자 ID로 프로젝트를 조회하는 대신 아무것도 해석되지
않습니다. :project_or_group 인수에서는 전체 경로가 둘 사이에서 충돌할 수 없으므로,
프로젝트 조회가 가져가지 않은 식별자는 그룹 조회로 넘어갑니다.
경로 기반 도구. route_setting :mcp로 REST 경로에서 만든 도구는 보통 선언이 필요하지 않습니다.
ApiTool은 경로의 route_setting :authorization에서 boundary_type을 읽고
:project와 :group을 경로의 :id 파라미터에 매핑합니다. 경로가 boundary type을 설정하지 않으면
ApiTool은 기본 선언으로 대체합니다.
경로가 컨테이너를 지정하는 다른 인수를 받는다면 경로에 container_arguments:로 선언합니다.
ApiTool은 이를 경계에서 파생된 종류와 병합합니다. fork_repository는 경계에 따른 원본 프로젝트와
포크할 대상 네임스페이스로 거버넌스가 적용됩니다.
route_setting :mcp, tool_name: :fork_repository, toolset: :repository,
params: [:id, :namespace_id, :namespace_path, :name, :path, :description, :visibility],
container_arguments: {
record: {
namespace_id: ->(id) { ::Namespace.without_project_namespaces.find_by_id(id) },
namespace_path: ->(path) { ::Namespace.without_project_namespaces.find_by_full_path(path) }
}
}
EE에서만 인수를 추가한다면, 경로가 파라미터를 전달하는 방식과 같이 EE가 오버라이드하는 CE 헬퍼를 통해
선언을 전달합니다. create_issue는 EE가 epic_id 리졸버로 확장하는
Helpers::IssuesHelpers.create_issue_mcp_container_arguments를
사용합니다.
URL 인수. 도구마다 url을 읽는 파서가 다릅니다. 머지 리퀘스트 도구는
MergeRequest.link_reference_pattern과, 커밋 도구는 Commit.link_reference_pattern과 일치시키고,
그 밖의 도구는 /-/ 앞의 경로를 사용합니다. 하나의 URL에 각 파서가 서로 다른 프로젝트로 읽는 텍스트가
들어 있을 수 있습니다. 리졸버는 세 가지를 모두 적용하고 반환된 모든 프로젝트 또는 그룹으로 호출에
거버넌스를 적용하므로, 가장 제한적인 규칙이 우선합니다.
리스트 인수. 인수는 식별자 하나가 아니라 식별자 목록을 담을 수 있습니다.
리졸버는 첫 항목만이 아니라 목록의 모든 항목을 조회하며, 실제 프로젝트나 그룹으로 해석되지 않는
항목은 오류로 처리하지 않고 건너뜁니다.
attach_scan_profile은 리스트 인수를 사용해 한 번의 호출에서 여러 프로젝트와 여러 그룹을
받습니다.
class AttachScanProfileService < Base::GraphqlService
container_arguments project: :project_ids,
group: :group_ids,
record: { security_scan_profile_id: ->(id) { container_from_scan_profile(id) } }
end
거버넌스할 수 없는 도구. 도구의 어떤 인수도 프로젝트나 그룹을 지정하지 않으면,
container_arguments를 선언하는 대신 클래스를 ungovernable!로 표시합니다. 인수를 전혀 받지 않는
GetServerVersionService가 그 예입니다.
class GetServerVersionService < Base::CustomService
ungovernable!
# ...
end
레코드를 통해 접근하는 도구. 인수가 프로젝트나 그룹이 아니라 레코드를 지정한다면 record: 종류로
선언하고 조회 로직을 연결합니다. 리졸버가 이를 실행하고 인수를 대신 집계하므로 재정의가
필요하지 않습니다.
class GetVulnerabilityService < Base::GraphqlService
container_arguments record: { vulnerability_id: ->(id) { ::Vulnerability.find_by_id(id)&.project } }
# ...
end
조회는 프로젝트, 그룹, 네임스페이스를 반환할 수 있습니다. 리졸버는 프로젝트 네임스페이스를 해당
프로젝트로 변환합니다. 개인 네임스페이스에는 적용되는 도구 규칙이 없으므로 집계하지 않습니다. 조회가
nil을 반환해도 인수는 집계되므로, 존재하지 않는 레코드를 지정한 호출은 거버넌스 없이 제공되지 않고
거부됩니다.
가드레일. ee/spec/lib/ai/tool_rules/governable_tools_container_spec.rb는 거버넌스 대상
도구마다 다음을 검증합니다.
- 도구가 컨테이너 인수를 선언하거나
ungovernable!로 표시되어 있습니다. - 선언된 인수가 도구가 실제로 받는 인수입니다.
- 선언이 인식되는 종류를 키로 사용합니다.
- 모든 입력 속성이 선언되어 있거나, 프로젝트나 그룹을 지정하지 않는 속성으로 해당 도구에 대해 목록에 기재되어 있습니다. 이 목록은 도구별로 관리되므로, 한 도구에서 안전한 인수가 다른 도구에서 면제되지는 않습니다.
이 스펙이 도구에서 실패하면 실제로 프로젝트나 그룹을 담는 인수를 선언하고, 컨테이너를 지정하지 않는 인수라면
도구의 목록 항목에 해당 인수를 추가하며, 도구의 어떤 항목도 컨테이너를 지정하지 않는다면 도구를 ungovernable!로 표시합니다.
커스텀 도구 구현#
API 노출과 분리해서 유지해야 하는 고유 기능을 가진 도구는 독립적인 클래스를 정의할 수 있습니다(이 예시 참고).
AI 에이전트 아키텍처에서의 도구 확산 위험#
너무 많은 도구를 추가할 때의 핵심 문제#
- 성능 저하
- 컨텍스트 팽창: 모든 도구 정의(이름, 설명, JSON 스키마)가 모델의 프롬프트 컨텍스트에 추가되어 입력 토큰 수가 늘어납니다
- 지연 시간 증가: 프롬프트가 커지면 응답 시간이 느려집니다
- 비용 상승: 토큰이 많아질수록 요청당 API 비용이 높아집니다
- 정확도 저하: 연구에 따르면 컨텍스트가 늘어나는 것과 도구가 늘어나는 것 모두 에이전트 성능을 떨어뜨립니다
- 궤적 저하: 더 긴 추론 경로가 필요한 에이전트는 도구 확산에 따라 더 빠르게 저하됩니다
- 도구 선택 혼란
- 결정 복잡성: 도구가 많아지면 모델이 올바른 도구를 선택하기 어려워집니다
- 파라미터 설계 문제: LLM이 학습한 공개 형식(프로젝트 경로, IID) 대신 내부 ID(GitLab 전역 ID 등)를 사용하면 도구 사용의 효과가 떨어집니다
- 유사한 도구 간의 모호성: 모델은 목적이 비슷한 도구(예: 로컬 파일 작업과 원격 파일 작업)를 구분하는 데 어려움을 겪습니다
- 사용자 경험 영향
- 도구 체이닝 소음: 도구가 많아지면 한 요청에서 도구 호출이 늘어나 소음과 산만함이 생깁니다
- 느린 응답: 처리 시간이 늘어나면 사용자 만족도에 영향을 줍니다
- 잘못된 도구 선택: 잘못된 도구 선택은 부정확하거나 관련 없는 응답으로 이어집니다
도구 추가 전에 개발자가 답해야 할 질문#
- 필요성: 이것이 정말 새로운 기능인지, 아니면 파라미터 조정으로 기존 도구가 처리할 수 있는지 확인합니다.
- 통합 가능성: 이 기능을 열거형이나 파라미터를 사용해 기존 도구와 병합할 수 있는지 확인합니다.
- 파라미터 설계: 모델의 학습 데이터와 일치하는, 공개적으로 인식 가능한 식별자를 사용하고 있는지 확인합니다.
- 평가 전략: 이 도구가 에이전트 전체 성능을 향상시키는지 저하시키는지 측정하는 방법을 정합니다.
- 컨텍스트 관리: 이 도구의 정의가 실제 추론에 쓸 수 있는 컨텍스트 창에 어떤 영향을 주는지 확인합니다.
- 도구 라우팅: 이 도구를 메인 에이전트의 도구 세트가 아니라 특수 서브 에이전트에 두어야 하는지 검토합니다.
- 권한 모델: 이 도구가 권한·가격 모델과 어떻게 상호작용하는지, 복잡성을 만들지 않는지 확인합니다.
- 의미론적 구별: 이 도구가 도구 세트의 다른 도구와 명확하게 구별되는지 확인합니다.
도구가 많은 것이 항상 더 좋은 것은 아닙니다. 연구에 따르면 컨텍스트 크기와 도구 수 모두 수익이 체감하며 결국 성능 저하로 이어집니다. 도구 세트를 계속 확장하는 대신 도구 통합, 특수 서브 에이전트, 동적 도구 라우팅을 고려합니다.
기존 도구 수정#
MCP 도구는 소비자에게 호환성이 깨지는 변경이 발생하지 않도록 시맨틱 버저닝을 사용합니다. 도구를 수정할 때는 이 머지 리퀘스트에서 도입된 버저닝 시스템을 사용합니다.
버저닝이 중요한 이유:
LLM과 AI 에이전트는 도구 스키마를 캐시하고 특정 도구 동작을 중심으로 워크플로를 구성합니다. 도구 파라미터, 설명, 출력 형식이 바뀌면 기존 통합이 중단될 수 있습니다. 버저닝은 하위 호환성을 유지하면서 안전하게 발전시킬 수 있게 합니다.
버전 등록 패턴:
집계된 API 도구, 커스텀 도구, GraphQL 도구는 register_version을 사용해 버전을 등록합니다.
module Mcp
module Tools
class GetServerVersionService < Base::CustomService
register_version '0.1.0', {
description: 'Get the current version of MCP server.',
input_schema: {
type: 'object',
properties: {},
required: []
}
}
def perform_v0_1_0(_arguments = {})
data = { version: Gitlab::VERSION, revision: Gitlab.revision }
formatted_content = [{ type: 'text', text: data[:version] }]
::Mcp::Tools::Base::Response.success(formatted_content, data)
end
override :perform_default
def perform_default(arguments = {})
perform_v0_1_0(arguments)
end
end
end
end
새 버전 추가:
도구의 동작을 수정해야 할 때는 다음과 같이 진행합니다.
- 업데이트된 메타데이터로 새 버전을 등록합니다.
register_version '0.2.0', {
description: 'Get version with additional metadata.',
input_schema: {
type: 'object',
properties: {
include_metadata: {
type: 'boolean',
description: 'Include additional metadata'
}
},
required: []
}
}
- 버전별 메서드를 구현합니다.
def perform_v0_2_0(arguments = {})
data = {
version: Gitlab::VERSION,
revision: Gitlab.revision
}
if arguments[:include_metadata]
data[:metadata] = { build_date: Time.current }
end
formatted_content = [{ type: 'text', text: data[:version] }]
::Mcp::Tools::Base::Response.success(formatted_content, data)
end
- 최신 버전을 사용하도록
perform_default를 업데이트합니다.
override :perform_default
def perform_default(arguments = {})
perform_v0_2_0(arguments)
end
API 도구의 경우:
API 도구는 자동으로 버전 0.1.0을 기본값으로 사용합니다. 필요하면 경로 설정에서 버전을
지정할 수 있습니다.
route_setting :mcp, tool_name: :get_issue,
params: [:id, :issue_iid],
version: '1.0.0'
경로에서 만들어진 API 도구는 도구당 단일 버전을 사용합니다. 여러 버전이 필요한 도구는 커스텀 도구로 구현하는 것을 고려합니다.
버전 지원 정책:
버전이 지정되지 않으면 프레임워크가 자동으로 최신 버전을 사용합니다. 소비자는 도구 호출 시 특정 버전을 요청할 수 있습니다. 버전을 폐기할 때는 다중 버전 호환성 가이드라인을 따릅니다.
도구 이름 변경#
도구 이름을 변경할 때는 하위 호환성을 유지하기 위해 도구 별칭을 사용해야 합니다. 연결된 클라이언트는 도구 이름을 캐시하며 도구 이름이 바뀌어도 자동으로 새로 고치지 않습니다. 이 머지 리퀘스트에서 도입된 별칭 시스템은 기존 통합을 중단하지 않고 매끄럽게 이름을 변경할 수 있게 합니다.
별칭이 필요한 이유:
MCP 클라이언트는 tools/list에서 받은 도구 목록을 캐시하며 도구가 바뀌어도 자동으로 다시
가져오지 않습니다. 도구 이름을 변경하면 클라이언트가 존재하지 않는 도구 이름을 호출해 오류가 발생하거나
무한히 멈춥니다. MCP 사양은 변경 사항을 클라이언트에 알리기 위해 notifications/tools/list_changed를
지원하지만, GitLab MCP 서버는 이를 구현하지 않습니다(이 이슈에서
추적 중).
구현 단계:
- 도구 클래스에서
tool_aliases를 오버라이드해 이전 이름을 포함합니다.
module Mcp
module Tools
class RenamedService < Base::AggregatedService
override :tool_name
def self.tool_name
'new_name'
end
override :tool_aliases
def self.tool_aliases
['old_name']
end
end
end
end
route_setting :mcp로 정의된 API 도구는 클래스 메서드를 오버라이드하는 대신 tool_aliases:
설정으로 별칭을 선언합니다. ApiTool은 모든 경로가 공유하는 단일 클래스이므로,
별칭은 경로별로 지정해야 합니다.
route_setting :mcp, tool_name: :new_name,
params: [:id],
tool_aliases: [:old_name]
-
새 도구 이름을 사용하도록 모든 참조를 업데이트합니다.
tool_name:이 있는 경로 설정- 테스트 파일
- 문서
- 하드코딩된 도구 이름 참조
-
Mcp::Tools::Manager는get_tool호출 중에 별칭을 자동으로 해석하므로, 이전 이름을 사용하는 클라이언트는 계속 동작합니다.tools/list도X-Gitlab-Enabled-Mcp-Server-Tools헤더로 응답을 필터링할 때 별칭을 해석하므로, 이전 이름으로 도구를 지정한 헤더도 해당 도구를 계속 반환합니다.
중요 사항:
list_tools는 별칭이 아니라 표준 도구 이름만 반환합니다- 별칭은 모든 도구 유형에서 동작합니다. 커스텀 도구, GraphQL 도구, 집계된 도구는 도구 클래스에서
self.tool_aliases를 오버라이드합니다. API 도구는tool_aliases:경로 설정으로 별칭을 선언합니다 aggregators:도 함께 설정하는 경로에서는tool_aliases:가 효과가 없습니다. 집계된 도구의 별칭은 집계기 클래스의self.tool_aliases에서 가져옵니다- 별칭 해석은 모든 도구 레지스트리를 확인하는
Manager#resolve_alias에서 일어납니다
별칭 제거 시점
지금은 제거하지 않습니다. 별칭은 현재 두 가지 목적을 가집니다.
- 이전 도구 이름을 캐시한 MCP 클라이언트에 대한 하위 호환성.
tool_aliases를 통해 MCP 도구 이름을 해석하는 기능의 ID 매핑. 예를 들어 AI Catalog의 도구 거버넌스가 그렇습니다.
거버넌스가 의존하는 별칭을 제거하면 그 도구의 deny, ask, allow 규칙이 조용히 깨집니다. 전용 ID 메커니즘이 이름 변경 별칭에서 거버넌스를 분리할 때까지 (작업 항목 609451), 모든 별칭을 그대로 유지합니다.
도구 오류 분류#
Response.error는 선택 사항인 reason: 심볼을 받으며, 이 값은 mcp.log에 tool_status로 기록됩니다.
도구가 실패한 이유를 알고 있다면 항상 전달합니다. 그러면 확장 로깅을 켜지 않아도
구조화된 로그에서 실패 유형을 확인할 수 있습니다.
::Mcp::Tools::Base::Response.error(
'Work item not found or inaccessible.', reason: ::Mcp::Tools::Base::Response::Reason::NOT_FOUND
)
Reason 상수 |
사용 대상 |
|---|---|
BAD_REQUEST |
에이전트가 누락되었거나 형식이 잘못되었거나 유효하지 않은 인수를 보냈습니다. |
UNAUTHORIZED |
사용자에게 접근 권한이 없습니다. 리소스의 존재 여부가 드러나지 않도록 메시지는 모호하게 유지하고, 실제 원인은 reason에 기록합니다. |
NOT_FOUND |
리소스가 실제로 존재하지 않거나, GraphQL 쿼리가 null 데이터로 해석되어 부재와 비가시성을 구분할 수 없습니다. |
ERROR |
백엔드 또는 다운스트림 실패입니다. reason을 지정하지 않으면 기본값입니다. |
이 네 가지 값만 기록됩니다. Response::Reason::ALL이 허용 목록이며, 그 밖의 값은 ERROR로
대체됩니다. reason은 응답이 에이전트에 도달하기 전에 제거되므로, reason을 추가해도 에이전트가 보는
내용은 바뀌지 않습니다.
ResourceFinder#find_project! 또는 #find_group!로 프로젝트나 그룹을 해석하는 도구는 이 분류를
자동으로 얻습니다. 이 메서드는 리소스가 존재하지 않으면 ResourceNotFoundError를, 리소스가 존재하지만
사용자가 볼 수 없으면 ResourceForbiddenError를 발생시키며, 기본 서비스가 각각을 분류합니다.
두 오류는 같은 메시지를 사용하므로 에이전트는 둘을 구분할 수 없습니다.
그 밖에는 성공한 페이로드에서 예상한 키를 찾지 못한 응답에 reason을 전달하지 않습니다.
이는 형식이 잘못된 응답이며, ERROR가 의미하는 바입니다.
도구의 오류가 다른 도구의 결과를 감싸는 경우 Response.error_reason으로 해당 결과의 reason을 전달합니다.
이 메서드는 결과의 reason을 반환하며, 결과에 reason이 없으면 ERROR를 반환합니다. reason 없이 자체 오류를 만드는
래퍼는 내부 도구가 이미 분류한 상태에 대해서도 ERROR를 기록합니다. reason이 없는 내부 실패에 자체 기본값을
유지하려면 대신 result[:reason]을 읽습니다.
response = inner_tool.execute
return response unless ::Mcp::Tools::Base::Response.error?(response)
::Mcp::Tools::Base::Response.error(
'Actionable guidance for the agent',
reason: ::Mcp::Tools::Base::Response.error_reason(response)
)
경로 기반 내부 도구는 HTTP 상태에서 reason을 가져오지만, 이것이 항상 실제 원인은 아닙니다. 예를 들어
워크플로 API는 AI Catalog 플로를 실행할 수 없는 호출자를 400 Bad request로 거부하며, 이는 BAD_REQUEST로
전달됩니다. 특정 거부를 인식하고 메시지를 교체하는 래퍼는 상태보다 더 많은 정보를 알고 있습니다.
거부 메시지 표의 각 항목에 고유한 reason을 지정하고 fetch로 읽습니다. reason 없이 추가한 항목은
상태를 조용히 기록하는 대신 스펙에서 오류를 발생시킵니다.
KNOWN_REFUSALS = {
::Ai::Catalog::BaseService::INSUFFICIENT_PERMISSIONS_MESSAGE => {
guidance: 'You need at least the Developer role on the project to run this flow.',
reason: ::Mcp::Tools::Base::Response::Reason::UNAUTHORIZED
}
}.freeze
def error_response(response)
message = ::Mcp::Tools::Base::Response.error_message(response).to_s
_, entry = KNOWN_REFUSALS.find { |fragment, _| message.include?(fragment) }
return response unless entry
::Mcp::Tools::Base::Response.error(entry.fetch(:guidance), reason: entry.fetch(:reason))
end
MCP 출처로 Duo 세션 필터링#
/api/v4/mcp로 생성한 GitLab Duo 세션은 source_type이 mcp로 설정됩니다.
다음 위치에서 이러한 세션을 찾을 수 있습니다.
| 위치 | 필터링 방법 |
|---|---|
| GitLab 데이터베이스 | duo_workflows_workflows 테이블에서 mcp의 값인 source_type = 2를 사용합니다. Rails에서는 Ai::DuoWorkflows::Workflow.source_mcp를 사용합니다. |
| Snowflake | source = mcp인 agent_platform_session_created 이벤트를 사용합니다. 이벤트 value는 세션 ID입니다. |
| Kibana | mcp.log에서 event_name = duo_session_created를 검색합니다. 예: json.subcomponent: "mcp" and json.event_name: "duo_session_created". |
correlation_id는 duo_session_created 줄을 세션을 시작한 도구의 tool_call 줄과 연결합니다.
duo_session_created 줄에는 세션이 실행하는 플로 또는 에이전트도 기록됩니다.
agent_platform_session_created 이벤트에는 같은 필드가 추가 속성으로 있습니다.
필드 이름은 trigger_ai_catalog_item 이벤트와 일치합니다.
세션에 해당하지 않는 필드는 생략됩니다.
| 필드 | 의미 |
|---|---|
item_type |
항목의 종류입니다. foundational_flow, foundational_agent, foundational_external_agent, custom_flow, custom_agent, custom_external_agent 중 하나입니다. |
flow_name |
GitLab 플로 이름입니다(예: developer). 기반 에이전트의 값은 chat입니다. |
component_name |
기반 에이전트 참조입니다(예: analytics_agent). |
custom_item_id |
AI Catalog 항목 ID입니다. GitLab 플로와 기반 에이전트에는 설정되지 않습니다. |
item_version |
항목의 버전입니다. GitLab 플로와 에이전트의 경우 ^3.0.0과 같은 버전 제약일 수 있습니다. |
item_schema_version |
플로 스키마 버전입니다(예: v1). |
예를 들어 developer 플로를 실행하는 세션을 찾으려면 Kibana에서 다음 쿼리를 사용합니다.
json.event_name: "duo_session_created" and json.flow_name: "developer".
MCP 클라이언트 식별#
initialize_mcp_connection, start_mcp_tool_call, finish_mcp_tool_call 이벤트는
클라이언트를 mcp_client_name과 mcp_client_version 추가 속성에 기록합니다.
mcp.log의 initialize 및 tool_call 줄에도 같은 이름의 필드가 있습니다.
이 필드는 표준 컨텍스트의 client_name 및 client_type 필드가 아닙니다.
표준 컨텍스트의 필드는 클라이언트 ID에서 가져오며,
허용 목록에 있는 값만 받고 모든 MCP 요청에 integration과 mcp를 설정합니다.
MCP 클라이언트는 자유 형식의 이름을 보고하므로 별도의 속성에 보관합니다.
Gitlab::Mcp::ClientInfo는 이름이 있는 첫 번째 소스에서 클라이언트를 해석합니다.
params._meta["io.modelcontextprotocol/clientInfo"].2026-07-28개정판이 모든 요청에서 보냅니다.User-Agent헤더의 첫 번째 제품 토큰입니다(예:Cursor/1.0.0또는node).
두 소스는 모든 요청에 있을 수 있으므로, 같은 클라이언트의 initialize와 tools/call은
같은 값을 가집니다. 서버는 상태를 저장하지 않으며 두 요청을 연결할 세션이 없습니다.
핸드셰이크 개정판 클라이언트가 initialize에서 보내는 clientInfo는
declared_client_name과 declared_client_version으로 별도 기록됩니다. 이 값은
initialize_mcp_connection 이벤트의 추가 속성이자 mcp.log의 initialize 줄의 필드입니다.
이후 tools/call 요청은 이 값을 보내지 않으므로 mcp_client_name에는 사용하지 않습니다.
예를 들어 GitLab Duo Agent Platform 세션은 mcp-client를 선언하지만
GitLab-Workhorse-Mcp-Client User-Agent를 보냅니다.
모든 값은 클라이언트가 보고합니다. 이름과 버전만 보관합니다. 제어 문자는 제거되며, 각 값은 64자로 잘립니다.
도구 가용성 제어#
특정 사용자에게 도구를 제공할지 제어하려면 available?를 오버라이드합니다. 기본값은 true입니다.
이 메서드는 현재 사용자를 인수로 받으므로, 사용자, 라이선스, 그 밖의 요청 상태에 따라 확인할 수 있습니다.
module Mcp
module Tools
class ExampleService < Base::CustomService
def available?(current_user)
Feature.enabled?(:example_tool, current_user)
end
end
end
end
도구 인스턴스는 동시 요청 사이에서 공유되므로, 인스턴스 상태 대신 current_user 인수를 사용합니다.
툴셋은 정적이므로 사용자별 필터링은 available?(current_user)에서 처리합니다.
available?는 tools/list 응답만 필터링합니다.
tools/call은 이를 확인하지 않으므로, 도구 이름을 이미 알고 있는 클라이언트는 도구가 사용할 수 없다고 보고해도 여전히 호출할 수 있습니다.- AI Catalog 도구 선택기는 이를 참조하지 않습니다. 이 선택기는 사용자 컨텍스트가 없는 요청별 캐시 스냅샷에서 도구를 가져옵니다.
- 커스텀 도구, GraphQL 도구, 집계된 도구는
available?를 지원합니다. 경로 설정으로 정의된 API 도구는 항상 사용 가능한 것으로 간주됩니다.
디스커버리에서 도구 숨기기#
도구를 완전히 호출할 수 있는 상태로 두면서 디스커버리에서 숨기려면 도구를 unlisted로 표시합니다. unlisted
도구는 tools/list 응답과 AI Catalog 도구 선택기에서 제외되지만, get_tool로는 계속
해석할 수 있고 Duo Workflow 허용 목록에도 남아 있으며 tools/call로 호출할 수도
있습니다. 도구를 공개할 준비가 되기 전에 미리 배치해 두려면 이 방법을 사용합니다.
Workhorse는 커스텀 에이전트가 선택한 도구와 함께 X-Gitlab-Enabled-Mcp-Server-Tools 헤더를 보내므로,
unlisted 처리되기 전에 도구를 선택한 에이전트는 그 도구를 계속 사용합니다. ai_workflows 스코프가 있는
토큰만 해당 헤더에 이름을 지정해 tools/list에서 unlisted 도구를 받을 수 있습니다. 이름을 지정한
다른 클라이언트는 여전히 도구를 보지 못합니다.
선택기는 unlisted 도구를 제공하지 않지만, API로 만든 에이전트는 여전히 이름을 지정할 수 있으며 그러면 해당 도구를 받습니다. 따라서 사용자가 도구를 호출하지 못하게 하려고 도구를 unlisted로 표시하지 않습니다. 도구를 숨길 뿐 차단하지는 않습니다.
unlisted?는 available?와 다릅니다. available?는 사용자별
확인으로 tools/list에서 사용자에게 도구를 제공할지 결정하는 반면, unlisted?는 모든 사용자의
tools/list에서 그리고 AI Catalog 선택기에서 도구를 숨기는 정적 속성입니다.
선택기 구분이 중요한 이유는, 선택기가 사용자 정보 없는 캐시 스냅샷을 읽기 때문에 unlisted? 같은 정적
플래그는 반영할 수 있지만 available? 같은 사용자별 확인은 반영할 수 없다는 점입니다. 둘 다 tools/call을 막지는 않습니다.
커스텀 도구, GraphQL 도구, 집계된 도구는 unlisted?를 오버라이드합니다.
module Mcp
module Tools
class ExampleService < Base::CustomService
def unlisted?
true
end
end
end
end
API 도구는 경로 설정에서 unlisted를 설정합니다.
route_setting :mcp, tool_name: :example_tool, params: [:id], unlisted: true
기본값은 false이므로, 기존 도구는 명시적으로 설정할 때까지 계속 목록에 표시됩니다.
unlisted?는 정적으로 유지합니다. AI Catalog 선택기가 평가할 수 없으므로, 사용자별 확인이나
자격 증명에 의존하는 확인으로 이를 결정하지 않습니다.
툴셋 선언#
모든 도구는
Mcp::Tools::Toolsets::ALL에서
정확히 하나의 툴셋을 선언해야 합니다. 툴셋을 선언하지 않은 도구는 어떤 툴셋으로도 접근할 수 없습니다.
spec/services/mcp/tools/toolset_coverage_spec.rb는 등록된 모든 도구가 유효한 툴셋을 선언하는지 검증하므로,
누락되었거나 유효하지 않은 선언은 런타임 오류로 나타나는 대신 CI에서 실패합니다.
커스텀 도구, GraphQL 도구, 집계된 도구에는 register_version
메타데이터에 toolset: :symbol을 추가합니다.
register_version '0.1.0', {
toolset: :work_items,
description: 'Example description',
# ...
}
API 도구에는 route_setting :mcp에 toolset: :symbol을 추가합니다.
route_setting :mcp, tool_name: :example_tool, toolset: :work_items, params: [:id]
X-Gitlab-Enabled-Mcp-Server-Toolsets 헤더의 알 수 없는 툴셋 이름은 툴셋이 닫힌 열거형이므로 400 오류를
반환합니다. X-Gitlab-Enabled-Mcp-Server-Tools의 알 수 없는 도구 이름은 도구 이름이 GitLab 에디션과 버전에 따라
달라지는 열린 집합이므로 경고로 기록되고 무시됩니다.
집계된 도구에서 액션 분리#
여러 작업을 하나의 파라미터(예: action 또는 불리언 플래그) 뒤에 묶은 집계된 도구는
list/save 분리 규칙에 따라 컬렉션 읽기 액션을 위한 전용
list_ 도구를 가질 수 있습니다. 이름 변경과 달리 호출 형태가 바뀌므로(호출자가 더 이상 선택자
파라미터를 전달하지 않음) 도구 별칭으로는 동작을 보존할 수
없습니다. 이전 액션을 사용하던 호출자는 새 도구로 명시적으로 마이그레이션해야 합니다.
MCP 서버 도구에서 이전 도구의 블록에
[Removed] 항목을 추가해 어떤 액션이 어느 도구로 이동했는지 기록하고, 새 도구에 대한 일반적인
[Introduced] 항목도 함께 문서화합니다. 예시는 해당 페이지의
list_pipelines 및 manage_pipeline 항목을 참고합니다.