모델이 쓰는 “도구”는 하네스 내장 도구 + MCP 서버가 주입하는 도구 두 층이고, MCP 서버는 {이름·설명·입력형식} 세트로 도구를 모델 앞에 차려준다.
왜 배우나: 내 도구 수십 개가 어디서 어떻게 모델에게 보이는지 알아야 새 도구를 붙이거나 도구가 안 보일 때 원인을 짚을 수 있다.
그림
flowchart TD
A[세션 시작] --> B[.mcp.json 읽기]
B --> C[node mcp-server.cjs 를 별도 프로세스로 띄움]
C --> D["모델: 도구 목록 줘 ListTools 요청"]
D --> E["서버: 메뉴판 생성 buildListToolsResponse<br/>이름·설명·입력형식"]
E --> F[모델 화면에 mcp__...t__이름 으로 등록]
F --> G{"설명서를 지금 다 줄까?"}
G -->|상시 도구| H[모델이 바로 인자 채워 호출]
G -->|이름만 먼저 deferred| I[ToolSearch 로 설명서 끌어오기]
I --> H
H --> J["CallTool: 도구의 handler 실제 실행"]
J --> K["결과 글자 content text 를 모델에 반환"]
쉽게 풀기
식당 비유로 풀어 봅니다.
1) 메뉴판이 두 종류로 깔린다 — 모델은 손님이고, 앞에 메뉴판 두 장이 놓입니다.
기본 메뉴: 식당이 원래 가진 내장 도구(Bash·Read·Edit). 항상 있고 설치 불필요.
종업원이 가져다주는 메뉴: MCP 서버가 주입하는 도구. MCP(Model Context Protocol)는 “도구를 외부 프로그램으로 떼어내 표준 약속으로 연결하는” 방식.
2) 종업원은 한 명, 메뉴는 수십 개 — .mcp.json은 t라는 종업원(MCP 서버) 딱 한 명만 부르는데, 이 한 명이 가져오는 메뉴판엔 LSP·AST·상태·메모·위키 버튼이 60개쯤 박혀 있습니다. 종업원 수 ≠ 메뉴 수.
3) 메뉴 한 줄 = 3종 세트{이름, 설명, 입력형식}.
이름: 무슨 버튼인지 (예: lsp_hover)
설명: “언제·왜 누르는지” 안내문 — 모델은 이 글만 보고 판단
입력형식: “어떤 값을 넣는지”(파일 경로·줄 번호 등)와 제약
4) 모델은 설명만 보고 손을 뻗는다 — 코드 내부는 못 봅니다. 설명·입력형식만 읽고 호출을 정하면, 값 검사·파일 읽기·에러 포장은 전부 종업원 쪽 handler가 하고 모델엔 결과 글자만 돌아옵니다.
flowchart LR
M[모델] -->|"설명·입력형식만 읽음"| D[도구 정의]
M -->|값 채워 호출| H[handler 실행함수]
H -->|"값검사·파일읽기·에러포장"| W[실제 작업]
W -->|결과 글자만| M
5) 메뉴 많으면 이름만 먼저(deferred) — 버튼 60개 설명서를 항상 펼치면 컨텍스트를 너무 먹습니다. 그래서 일부는 이름만 먼저 깔고, 모델이 ToolSearch로 요청해야 입력형식이 펼쳐집니다. “메뉴판엔 이름만, 누르려면 그 버튼 설명서를 먼저 끌어오기.”
핵심 정리
개념
한마디로
어디에
2계층 도구
내장 + MCP 주입
내장은 접두사 없음
MCP 서버 t
종업원 한 명
.mcp.json
도구 3종 세트
이름·설명·입력형식
서버 내부 정의
deferred 노출
이름만 먼저, 설명은 나중
ToolSearch로 로드
펼쳐보기: 모델에게 도구가 보이는 방식 + 정의 객체 전체 필드
모델 화면에서:
mcp__plugin_oh-my-claudecode_t__<이름> 처럼 긴 접두사가 붙음 → “어느 MCP 서버 소속인지”를 박아 이름 충돌 방지.
내장 도구(Bash/Read/Edit)는 접두사 없음 — 이 유무가 곧 2계층을 가르는 표식.
모델이 읽는 것은 description(언제 쓸지)과 inputSchema(인자 필수·타입) 둘뿐.
도구 정의 객체 필드 (서버 내부, zod 스키마):
name (필수): 식별자(소문자_스네이크 권장)
description (필수): 모델이 언제 쓸지 판단하는 자연어 설명. 예시까지 길게 적기도 함
// bridge/mcp-server.cjsvar lspHoverTool = { name: "lsp_hover", description: "Get type information, documentation, and signature at a specific position in a file. ...", schema: { file: external_exports.string().describe("Path to the source file"), line: external_exports.number().int().min(1).describe("Line number (1-indexed)"), character: external_exports.number().int().min(0).describe("Character position (0-indexed)") }, handler: async ({ file, line, character }) => withLspClient(file, "hover", async (client) => formatHover(await client.hover(file, line - 1, character)))};
부작용(상태 쓰기)이 있는 도구는 annotations로 위험도를 표기합니다.
펼쳐보기: annotations 붙은 도구 전문 ( state_write)
// bridge/mcp-server.cjsvar ... = { name: "state_write", description: "Write/update state for a specific mode. ...", annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, schema: { mode: external_exports.enum(STATE_TOOL_MODES).describe("The mode to write state for"), active: external_exports.boolean().optional().describe("Whether the mode is currently active"), iteration: external_exports.number().optional().describe("Current iteration number"), state: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("Additional custom state fields ..."), session_id: external_exports.string().optional().describe("Session ID for session-scoped state isolation. ...") }, handler: async (args) => { /* ... */ }};
(3) 메뉴판 만들기 + 호출 받기 (zod → JSONSchema)
서버는 zod schema를 MCP 표준 JSONSchema(inputSchema)로 바꿔 노출합니다. 변환기는 ZodOptional을 벗기고, ZodDefault는 default를 채우며, int 체크가 있으면 integer로, 옵셔널 아닌 키는 required에 넣습니다. 두 흐름(목록 요청 / 호출 요청)이 한 서버에서 갈립니다.
flowchart TD
R1[ListTools 요청] --> B1[buildListToolsResponse]
B1 --> Z[zodToJsonSchema 변환]
Z --> O1["name·description·inputSchema 목록 반환"]
R2["CallTool 요청 name+args"] --> F2[allTools 에서 name 으로 찾기]
F2 -->|없음| E2[Unknown tool isError]
F2 -->|있음| H2[tool.handler 실행]
H2 --> O2["content·isError 반환"]
(4) 두 번째 서버 team — JSONSchema 손으로 적기 (현재 DEPRECATED)
bridge/team-mcp.cjs는 두 번째 MCP 서버(name:"team")로, zod 대신 JSONSchema를 직접 적습니다. codex/gemini 외부 CLI 워커를 tmux로 돌리던 도구들이며, 지금은 [DEPRECATED]로 CLI(omc team ...) 이관 안내만 합니다. 실제 워커 본체는 bridge/team-bridge.cjs의 spawnCliProcess(provider, ...)가 provider별로 codex exec ... 또는 gemini ...를 자식 프로세스로 띄웁니다(기본 gpt-5.3-codex / gemini-3.1-pro-preview).