도구·MCP 시스템 — 횡단분석

한 줄 요약

7개 하네스(Claude Code · Codex · OMC · gajae-code · ouroboros · fable-ish · 내 패턴)가 “도구 1개를 어떤 형식으로 정의하고, 언제 모델 앞에 올리고, 어떻게 컨텍스트에 주입하는가” 를 같은 잣대로 비교한 노트다.

왜 배우나 — 에이전트가 똑똑해지는 핵심은 “어떤 도구를 손에 쥐여 주느냐”인데, 도구가 수십~수백 개로 불어나면 전부 모델 앞에 늘어놓는 것이 토큰 낭비가 된다. 모든 하네스가 이 문제를 어떻게 푸는지 알면, 내 스택에 무엇을 차용할지 보인다.

그림

도구 1개가 정의되어 모델 컨텍스트에 등장하기까지의 공통 생명주기:

flowchart TD
    A["도구 정의<br/>name + description + input_schema"] --> B["서버 등록<br/>.mcp.json / config.toml"]
    B --> C{"도구가 몇 개인가?"}
    C -->|"적음"| D["전부 노출<br/>tools 배열에 직접 주입"]
    C -->|"많음 (수십~수백)"| E["지연 노출<br/>이름만 먼저, 스키마는 나중"]
    E --> F["모델이 검색<br/>BM25 / ToolSearch"]
    F --> G["필요한 도구만 스키마 로드"]
    D --> H["모델이 description 읽고 호출 판단"]
    G --> H
    H --> I{"권한 게이트"}
    I -->|"승인"| J["실행 → content + 메타 반환"]
    I -->|"거부"| K["isError로 차단"]

세 가지 핵심 질문을 축으로 본 구조:

flowchart LR
    Q1["① 형식/스키마<br/>도구 1개의 모양"] --> R["도구 1개"]
    Q2["② 생명주기·트리거<br/>등장·소멸 시점"] --> R
    Q3["③ AI 주입방식<br/>컨텍스트 진입 경로"] --> R
    R --> S["LLM provider API<br/>tools 배열"]

쉽게 풀기

도구·MCP를 “요리사(모델)에게 어떤 주방기구를 꺼내 주느냐” 로 비유하면 술술 이해된다.

  1. 도구 1개 = 라벨이 붙은 기구. 모든 기구에는 세 가지가 적혀 있다 — 이름(name, “거품기”), 설명(description, “달걀을 거품 낼 때 쓴다”), 사용법(input_schema, “그릇과 재료를 넣으세요”). 요리사는 설명을 읽고 언제 그 기구를 집을지 판단한다. 그래서 설명을 잘 쓰는 것이 곧 라우팅(어떤 도구를 부를지 결정)을 좌우한다.

  2. MCP 서버 = 기구를 담은 서랍. .mcp.json은 “어느 서랍을 주방에 들여놓을지” 적은 목록이다. 서랍마다 이름이 있고(mcp__<서버>__<도구>), 같은 이름 기구가 충돌하지 않도록 서랍 이름을 접두사로 붙인다.

  3. 기구가 너무 많아지면? 주방 조리대(컨텍스트)에 100개 기구를 다 늘어놓으면 요리사가 헷갈리고 자리(토큰)도 모자란다. 그래서 거의 모든 하네스가 같은 답에 도달했다 — 자주 쓰는 기구만 꺼내 두고, 나머지는 서랍에 넣어 두되 “거품기 어디 있지?” 하고 검색하면 그때 꺼내 준다(지연 노출 + BM25 검색).

  4. 누가 언제 서랍을 정리하나. 대부분은 주방 문 열 때(세션 시작) 한 번 정리한다. Codex만 유별나게 매 요리(턴)마다 다시 정리한다 — 메뉴가 바뀌면 필요한 기구도 바뀐다고 보기 때문이다.

  5. “도구”의 정체성도 제각각. 대부분은 “칼·도마” 같은 원자적 손발을 도구로 본다. ouroboros는 “코스 요리 한 단계 전체”를 기구 하나로 만든다. fable-ish는 아예 기구를 만들지 않고 옆에서 요리 결과를 검사하는 속기사 역할만 한다.

핵심 정리

각 하네스의 도구 정의 방식을 세 축으로 요약한다.

프레임워크형식/스키마지연 노출 방식
Claude Code내장 카탈로그 + MCP(.mcp.json). 정의 = name+description+input_schema기본 연기(deferred), ToolSearch로 끌어옴
CodexToolSpec enum(Function/Namespace/ToolSearch 등), ResponsesApiTooltool_search(BM25), MCP 100개 이상 시 자동 deferred
OMC단일 서버 t, ToolDef{name,description,annotations,schema(zod),handler}호스트(Claude Code)의 연기 메커니즘에 얹힘
gajae-codeTool/AgentTool(loadMode/summary 등), parameters=zodsearch_tool_bm25, 도구 속성 loadMode
ouroborosMCPToolDefinition, 워크플로 단계를 도구로 외부화(해당 없음, 워크플로 엔진형)
fable-ish도구를 정의하지 않음 — 남의 도구 결과를 관찰(소비자 관점, 노출 개념 없음)
내 패턴OMC 차용: 단일 t 서버, zod 도구 + team 서버(DEPRECATED)도구 60개 초과 시 ToolSearch select:<name>

생명·트리거·주입 세부 (펼쳐 보기)

  • Claude Code — 세션 시작 시 .mcp.json 파싱→서버 연결. 설명·서버지침 각 2KB 컷, MCP 출력 25k토큰 상한(MAX_MCP_OUTPUT_TOKENS), 초과분은 디스크 저장+파일참조. @server:protocol://path 리소스, /mcp__server__prompt 프롬프트. 프로젝트 범위 .mcp.json은 사용 전 승인 게이팅().
  • Codex — 매 턴 build_tool_router()가 model/features/config 재평가해 도구목록 재계산. 핸들러 exposure()=Direct/DirectModelOnly/Deferred/Hidden. 외부 스키마는 sanitize→prune→compact(~1k토큰 손실압축). merge_into_namespaces()로 function들을 네임스페이스로 묶고, read-only만 병렬. PreToolUse/PostToolUse 훅 내장.
  • OMC — esbuild가 src/tools/*+registry를 단일 CJS 번들로. ListTools 시 allTools 순회+zodToJsonSchema. 새 도구 추가해도 .mcp.json은 불변. allTools 배열 = single source of truth.
  • gajae-codecreateTools(session)가 매 세션 빌드: 자동동반→isToolAllowed 게이팅→factory→resolve. essential(read/bash/edit)만 초기 노출. MCP는 250ms 초과 시 config-해시 캐시로 DeferredMCPTool 선노출. 이름순 정렬로 프롬프트 캐시 안정성, OAuth PKCE+Smithery.
  • ouroborosuvx ... ouroboros mcp serve로 기동→get_ouroboros_tools()가 핸들러 일괄 등록→tools/list 광고. tools/call 시 SecurityLayer(인증→레이트→인가→입력검증). long-run은 JobManager로 job_id 즉시반환+폴링.
  • fable-ish — 매 도구 호출 직후 PostToolUse 훅(10s 타임아웃). 검증명령 식별(VERIFY_RE)→exit_code 우선 판정→커버리지 4단계→원장 원자적 갱신. Stop 훅에서 검증 미달이면 종료 차단, 실패 시 additionalContext로 “완료 보고 금지” 주입.
  • 내 패턴mcp__plugin_oh-my-claudecode_t__<name> 네임스페이스로 등록. 내장도구는 접두사 없음=2계층 구분. stdout=프로토콜 채널이라 로그는 stderr만.

공통 패턴 (모두가 수렴한 설계)

  • 도구 1개 = name+description+input_schema 삼위일체 — 언어 무관. provider API의 tool-calling 와이어 포맷이 요구하기 때문 (fable-ish만 예외: 관찰자라서)
  • description이 곧 라우팅 신호 — 모델이 설명을 읽고 호출 판단. 그래서 설명에 프롬프트 엔지니어링이 들어온다
  • mcp__<server>__<tool> 네임스페이스 표준 — 서버 간 이름 충돌 방지 + 소속 식별
  • 도구 폭증 → 지연 노출로 수렴 — Codex tool_search(BM25), gajae search_tool_bm25가 명확히 BM25. Claude Code/OMC는 host의 ToolSearch/deferred에 의존
  • 결과 = content 블록 + 부가 메타 분리 — 모델용 텍스트와 감사/추적용 구조 데이터 분리, isError로 throw 없이 실패 전달
  • 호출 시점 권한/승인 게이팅 — “정의는 노출하되 실행은 게이트”

분기점 — 누가 왜 다르게 했나

  • 서버 개수: 다중 vs 단일 t — 정통 MCP는 서비스마다 서버, OMC·내 패턴은 서버 하나(t)에 수십 도구. 단일은 배포 단순(+)·권한 분리 불가(−)
  • 노출 계산 시점: 세션 1회 vs 매 턴 — Codex만 매 턴 재계산(모델/feature 상태가 턴마다 변할 수 있다고 봄)
  • progressive disclosure 위치 — Claude Code/Codex/내 패턴은 호스트·프로토콜 레벨, gajae는 도구 자체 속성(loadMode)
  • 도구의 정체성 — 손발(대부분) vs 워크플로 외부화(ouroboros) vs 관찰·검증(fable-ish)
  • MCP 시작 지연 — gajae만 250ms 데드라인+config-해시 캐시, 이름순 정렬로 캐시 breakpoint 보존
  • ToolChoice 강제·강등 — gajae만 provider별 빌드, Google처럼 지명 불가하면 "required"로 capability degradation
  • 스키마 압축 — Codex만 sanitize→prune→compact로 ~1k토큰 예산 강제
  • 승인 모델 결합도 — Claude Code는 정의/권한 분리, Codex/ouroboros는 디스패치에 내장

실제 예시

도구 정의의 삼위일체가 각 언어에서 어떻게 나타나는지 비교한다.

// OMC / 내 패턴 — src/tools/*.ts (단일 t 서버에 등록되는 ToolDef)
const myTool: ToolDef = {
  name: "wiki_query",
  description: "위키에서 BM25로 검색한다. 예시·메타변수 사용법까지 설명",
  annotations: { /* 읽기전용 등 힌트 */ },
  schema: z.object({ query: z.string().describe("검색어") }), // zod → zodToJsonSchema
  handler: async (args) => ({
    content: [{ type: "text", text: result }],   // 모델용
    isError: false,                               // throw 없이 실패 전달
  }),
};
// Codex — codex-rs/tools/src/tool_spec.rs (enum으로 도구 종류 분기)
// #[serde(tag = "type")]
pub enum ToolSpec {
    Function(ResponsesApiTool),   // name + description + strict + defer_loading + parameters(JsonSchema)
    Namespace(..),                // function들을 네임스페이스로 묶음
    ToolSearch,                   // BM25 검색 도구
    ImageGeneration, WebSearch, Freeform,  // hosted/custom은 삼위일체와 다른 형식
}
// 내 패턴 실사용 — ~/.claude/plugins/marketplaces/omc/.mcp.json
// 서버는 단 하나 "t". 새 도구를 추가해도 이 파일은 불변
{
  "mcpServers": {
    "t": { "command": "node", "args": ["bridge/mcp-server.cjs"] }
  }
}
# fable-ish — hooks/post_tool_use.py (도구를 정의하지 않고 결과를 관찰)
# tool_input / tool_response.exit_code 를 파싱해 검증 원장에 기록
# coverage_relation: none < uncertain < generic < direct (단조상승)
# exit_code 0 이 곧 "테스트 통과"의 물증 — 모델의 말이 아니라 레코드로 판정

요약 & 셀프체크

  • 3줄 요약

    1. 도구 1개는 name+description+input_schema 삼위일체로 수렴하며, description이 곧 호출 라우팅 신호다.
    2. 도구가 폭증하면 거의 모두 지연 노출 + 검색(Codex/gajae는 BM25)으로 같은 해법에 도달했다.
    3. “도구”의 정의 자체는 손발(대부분)·워크플로 외부화(ouroboros)·결과 검증(fable-ish)으로 갈린다.
  • 셀프체크

    1. 도구가 100개로 늘어났을 때, 전부 컨텍스트에 싣지 않으려면 어떤 메커니즘을 쓰는가? 그 메커니즘은 호스트 레벨인가 도구 속성 레벨인가?
    2. Codex가 매 턴 build_tool_router()로 도구목록을 다시 계산하는 이유는? 다른 하네스는 왜 세션 1회로 충분하다고 보는가?
    3. fable-ish가 “도구를 정의하지 않는다”는 말의 의미는? 그럼 무엇을 하는가?

베스트 — 내 스택에 차용할 구체안

내 환경(Claude Code 호스트 + OMC 단일 t 서버 + deferred/ToolSearch)에 다른 프레임워크의 검증된 디테일을 이식한다.

  1. [gajae→OMC] 도구 목록 위치 안정화allTools는 등록순=노출순이므로, 새 도구는 항상 배열 끝에 추가해 기존 도구 위치(=캐시 breakpoint)를 흔들지 않는다. (등록순에 의미가 없다면 buildListToolsResponse에서 이름순 정렬이 더 직접적)
  2. [gajae→OMC] loadMode 선언ToolDefloadMode:"essential"|"discoverable"+summary 추가. 단, 호스트가 소비하지 않으면 문서용 메타에 그치므로, core/rare 서버 분리나 description/schema 축소가 더 현실적일 수 있다.
  3. [gajae→OMC] LSP 지연 시작 대응 — jdtls 같은 느린 LSP 자식은 준비 안 됐을 때 “준비 중, 잠시 후 재호출” 응답 + 백그라운드 워밍업. (lsp_status + lazy warmup이 250ms config-hash 캐시보다 적합)
  4. [fable-ish→전체] PostToolUse 증거 원장coverage_relation 단조상승 + Stop 게이트로 “물증을 남겨라”. 단, 모델이 수정 가능한 state_write와 분리된 hook 소유 append-only 원장이어야 한다. exit_code 우선→텍스트 폴백 판정 순서 차용.
  5. [Codex→내패턴] 스키마 토큰 예산화compact(description 제거→definitions drop→깊은 객체 접기) 순서. 단 외부 MCP(Gmail/Notion/Slack) 스키마는 프록시 없이는 직접 다이어트 불가, OMC 자체/proxy 도구에 적용.
  6. [ouroboros→내패턴] long-run 도구 job_id 비동기화 — 긴 작업을 백그라운드 잡으로. 단 team MCP는 레거시라, 새 서버를 되살리기보다 omc team CLI job/artifact와 연결.
  7. [gajae→전체] content / details 영수증 분리 — 모델용 content와 감사용 details를 분리해 trace_timeline 디버깅을 풍부하게.

근거

기능노트

소스/문서 경로

  • /mnt/d/6study/10_프레임워크분석/_원문아카이브/claude-code/tools-reference.md, .../claude-code/mcp.md (공식문서 135p 아카이브)
  • /mnt/d/6study/10_프레임워크분석/_원문아카이브/codex/48_running-codex-as-an-mcp-server.md (공식문서 88p 아카이브)
  • /home/seunghyeong/harness-work/codex/codex-rs/tools/src/tool_spec.rs · tool_definition.rs · json_schema.rs; core/src/tools/{router,spec_plan,parallel}.rs; core/src/{mcp,mcp_tool_exposure,mcp_tool_call}.rs
  • /home/seunghyeong/harness-work/oh-my-claudecode/src/mcp/tool-registry.ts · standalone-server.ts · src/tools/state-tools.ts
  • /home/seunghyeong/harness-work/gajae-code/packages/coding-agent/src/tools/index.ts · runtime-mcp/{manager,tool-bridge,tool-cache}.ts · utils/tool-choice.ts
  • /home/seunghyeong/harness-work/ouroboros/src/ouroboros/mcp/{types.py,tools/registry.py,tools/subagent.py,job_manager.py}
  • /home/seunghyeong/harness-work/fable-ish/scripts/parse_tool_result.py · hooks/post_tool_use.py · scripts/verify_state.py
  • /home/seunghyeong/.claude/plugins/marketplaces/omc/{.mcp.json,bridge/mcp-server.cjs,bridge/team-mcp.cjs} (내 패턴 실사용본)

연결

_분석축_루브릭 · HOME

Codex 교차검증 — 경계 정정과 과일반화 경고

큰 축은 맞지만 MCP 서버 구현·호스트의 노출 정책·provider의 tool_choice 제약이 한 표에 섞였다. 핵심 정정:

  • 사실 정정 — Codex deferred 기준은 “100개 초과”가 아니라 “100개 이상”(>=, mcp_tool_exposure.rs:35). gajae wire name은 mcp__<server>__<tool>이 아니라 mcp__${server}_${tool}(중복 prefix 제거, tool-bridge.ts:174). OMC “팀 도구 별도 서버 team”은 기본 설치 기준 과함 — 기본 .mcp.jsont 하나뿐, team은 레거시(team-server.ts:40). gajae “초기 노출 read/bash/edit”는 discoveryMode==="all"일 때로 좁혀야 함(sdk.ts:1652). Claude Code ws는 CLI --transport가 아닌 JSON 경로 지원(mcp.md:128). MCP startup은 현재 기본 nonblocking, 대기는 ToolSearch 안에서 처리(env-vars.md:320).
  • 빠진 차이 — “name+description+input_schema”는 MCP/function tool에만 맞고 hosted/freeform/image/web search엔 안 맞음(tool_spec.rs:18). resources/prompts/list_changed 차이 미다룸. prompt cache 이름순 정렬은 gajae만의 것이 아님 — Codex도 namespace 내 정렬(spec_plan.rs:519).
  • 분기점 해석 — “Codex만 매 턴 재계산”은 과장(Claude Code list_changed, gajae search_tool_bm25로도 active set 변함). “description이 곧 라우팅”도 단정적 — 실제 라우팅은 노출·deferred·provider capability·tool_choice·permission이 함께 결정. “거의 모두 BM25”는 Codex/gajae로 한정해야 함(OMC는 자체 BM25 아님, host 의존).
  • 차용안 현실성loadMode는 host가 소비 안 하면 메타뿐. 250ms 캐시를 LSP에 적용하는 건 빗나감(캐시는 list-tools용, LSP는 실행 시 child 준비 문제). PostToolUse 원장은 hook 소유 append-only여야 함. Codex schema compaction은 외부 MCP엔 프록시 없이 적용 불가.
  • 수정 방향 — “공통 패턴”을 줄이고 4개 층(서버 제공 목록 / 호스트 노출 / provider tool_choice / 실행 전후 permission·hook)을 분리해서 볼 것.