이번 시간에는 MCP 도구를 200개까지 붙여 놓고, tool search를 켰을 때와 껐을 때 입력 토큰과 도구 선택 정확도가 어떻게 달라지는지 claude -p로 측정하며 배워가는 시간입니다. 도구 수에 따른 첫 요청 입력 토큰, 맞는 도구를 고르는지, 도구 이름이 뜻을 드러내지 않을 때 무엇이 달라지는지를 차례로 볼게요.
조건마다 같은 질문 20개를 세 번씩 돌렸어요. 모델 출력은 비결정적이라 같은 명령을 다시 돌리면 숫자가 달라질 수 있습니다. --model opus가 가리키는 모델도 시간이 지나면 달라질 수 있어요.
tool search는 무엇을 미루나
tool search는 Claude Code가 MCP 도구 정의를 처음부터 모두 싣지 않고 도구 이름만 알려 준 뒤, 모델이 ToolSearch라는 내장 도구로 필요한 정의를 찾아 불러오게 하는 기능이에요.
모델은 select:mcp__lab__get_like_count처럼 select: 뒤에 이름을 적어 그 도구를 바로 불러오거나, 키워드로 검색합니다. Tool search tool 문서에는 결과로 돌아온 tool_reference 블록(도구 이름만 담은 블록)을 API가 해당 도구의 전체 정의로 펼쳐 모델에게 보여 준다고 적혀 있어요.
켠 쪽은 도구 정의를 불러오는 검색 한 번만큼 요청이 하나 더 생깁니다.
실습 준비
이 글의 명령은 모두 실습 코드의 claude-mcp-tool-search/ 디렉터리에서 실행합니다.
mcpgen/: 설정 파일 하나로 가짜 도구 N개를 내는 MCP 서버(server.mjs)와mcp.json생성기(mkconfig.mjs)catalog200.json: 도구 200개 카탈로그tasks.jsonl: 질문 20개toolsets.jsonl: 도구 수별 도구 목록configs/: 조건별 생성기 설정과 번호 이름 대응표make_configs.mjs: 조건 26가지의 생성기 설정(configs/)과mcp.json을 만드는 스크립트run.mjs: 실습별 호출을 하나씩 돌려 stream-json을out/에 남기는 실행기show.mjs: stream-json 한 파일에서 필요한 필드만 골라 찍는 스크립트parse.mjs: 호출 하나에서 첫 요청 입력 토큰과 도구 호출을 뽑는 모듈grade.mjs: 네 단계 채점summary.mjs: 조건별 집계
컨테이너와 외부 패키지는 쓰지 않아요. MCP 서버는 claude가 직접 띄우는 stdio 자식 프로세스입니다.
도구 200개는 가상의 콘텐츠 사이트에서 글·코스·저자·회원을 조회하거나 메일·슬랙·결제·운영 작업을 하는 도구이고, 분류는 네 가지예요.
- 정답(
gold) 20개: 질문 20개의 정답 도구 - 같은 무리(
sibling) 20개: 같은 사이트 도구 가운데 정답이 아닌 20개 - 비슷한 도구(
near) 60개: 정답 도구마다 이름과 설명이 겹치지만 하는 일이 다른 도구 3개.get_like_count(글의 좋아요 수)에는get_like_history,get_comment_like_count,get_course_like_count를 붙였어요. - 무관한 도구(
unrelated) 100개: 이 사이트와 상관없는 운영 도구 10무리 × 10개
질문은 "mysql-gap-lock-02 글 좋아요 몇 개 받았어?"처럼 한국어 한 문장이고, 끝에 "필요한 도구를 써서 처리하고 결과만 짧게 답해."를 붙였어요. 질문마다 정답 도구가 모두 다릅니다.
도구 목록 N20은 정답 20개이고, N50, N100, N200은 그 뒤에 정답이 아닌 도구를 채운 목록의 앞부분입니다. 도구 5개짜리 목록은 정답이 반드시 들어가야 하니까, 과제마다 정답 + 비슷한 도구 3개 + 무관한 도구 1개로 따로 만듭니다.
make_configs.mjs는 조건마다 생성기 설정을 만들고, mkconfig.mjs를 불러 out/mcp/ 아래에 mcp.json을 씁니다. 호출마다 달라지는 것은 질문뿐이에요.
node make_configs.mjs
26 configs: main/N20, main/N50, main/N100, main/N200, opaque/N200-ko, opaque/N200-en, n5/<task> x 20
claude -p를 그냥 부르면 사용자 CLAUDE.md, 플러그인, 계정에 연결한 MCP 커넥터까지 함께 실려서 첫 요청이 크게 커져요. 그래서 run.mjs는 모든 호출을 아무 파일도 없는 빈 디렉터리($TMPDIR/ctsrch-run)에서 다음 옵션과 --model opus --no-session-persistence --output-format stream-json --verbose로 부릅니다.
--setting-sources project+ 빈 디렉터리: 사용자 설정과 CLAUDE.md를 읽지 않습니다.--strict-mcp-config:--mcp-config로 준 서버만 붙입니다. 이것이 없으면 계정의 claude.ai 커넥터가 따라 붙어요.--tools "ToolSearch": 내장 도구 가운데ToolSearch만 남깁니다.--tools ""로 비우면 tool search도 함께 꺼져요.--allowedTools "mcp__lab": 서버lab의 도구를 허가 요청 없이 부르게 합니다.--effort high: effort를 명시합니다.--max-turns 12: 한도에 걸리면 결과가error_max_turns입니다.
켬(ON)은 이 명령에 ENABLE_TOOL_SEARCH를 두지 않고, 끔(OFF)은 같은 명령에 ENABLE_TOOL_SEARCH=false만 더했습니다. run.mjs는 ANTHROPIC_API_KEY를 지우고 부르니까 구독 로그인이 전제입니다.
s1은 서버를 붙이지 않은 도구 0개 기준선을, main은 도구 수, 도구 5개, 번호 이름, 영어 설명 조건을 섞어 3회 돌리는 묶음이에요. main은 780호출이고, 이번 실행에서 추정 비용(API 단가 환산)은 약 30달러, 순수 실행에 2시간 가까이 걸렸습니다. 끝난 호출을 건너뛰기 때문에, 중간에 멈추면 같은 명령을 다시 실행해 이어서 돌리시면 됩니다.
node run.mjs s1
node run.mjs main
집계 출력은 모두 node summary.mjs <명령>이 찍은 그대로예요. 조건 이름은 도구 목록 이름(N20 등, 번호 이름은 op, 번호 이름에 영어 설명은 open) 뒤에 켬·끔(-on, -off)을 붙인 것입니다.
실습 1: 도구 수별 첫 요청 입력 토큰
첫 요청 입력 토큰은 첫 assistant 메시지의 usage에 있는 input_tokens, cache_creation_input_tokens, cache_read_input_tokens를 더한 값이에요. 캐시에서 읽었든 새로 썼든 모델이 읽은 양은 같으니까 셋을 더해서 봐요.
도구 5, 20, 50, 100, 200개 조건의 모든 호출에서 첫 요청 입력 토큰을 모았어요. 그리고 서버를 붙이지 않은 도구 0개 호출을 모드마다 세 번 더 돌려 기준선으로 삼았습니다. 도구 하나당 토큰은 질문 20개 중앙값으로 (N개 값 - 20개 값) / (N - 20)을 계산한 거예요.
node summary.mjs tokens
ON N=0 first request T13 2,711 tokens, all tasks 2,711 median 2,711 (3 calls)
ON N=5 first request T13 3,778 tokens, all tasks 3,771~3,804 median 3,781 (60 calls)
ON N=20 first request T13 3,976 tokens, all tasks 3,967~4,000 median 3,974 (60 calls)
ON N=50 first request T13 4,432 tokens, all tasks 4,423~4,456 median 4,430 (60 calls)
ON N=100 first request T13 5,154 tokens, all tasks 5,145~5,178 median 5,152 (60 calls)
ON N=200 first request T13 6,679 tokens, all tasks 6,670~6,703 median 6,677 (60 calls)
ON tokens per tool (median of all tasks): N20->N50 15.2, N20->N100 14.7, N20->N200 15.0
OFF N=0 first request T13 2,711 tokens, all tasks 2,711 median 2,711 (3 calls)
OFF N=5 first request T13 3,888 tokens, all tasks 3,823~4,090 median 3,935 (60 calls)
OFF N=20 first request T13 6,477 tokens, all tasks 6,468~6,501 median 6,475 (60 calls)
OFF N=50 first request T13 11,488 tokens, all tasks 11,479~11,512 median 11,486 (60 calls)
OFF N=100 first request T13 19,980 tokens, all tasks 19,971~20,004 median 19,978 (60 calls)
OFF N=200 first request T13 36,982 tokens, all tasks 36,973~37,006 median 36,980 (60 calls)
OFF tokens per tool (median of all tasks): N20->N50 167.0, N20->N100 168.8, N20->N200 169.5
끈 쪽은 도구 하나에 약 167~170토큰이 들었고, 도구 수에 거의 정확히 비례해 늘었어요. 도구 200개면 기준선 2,711토큰에 도구 정의 34,269토큰이 붙어 첫 요청의 93%가 도구 정의입니다.
켠 쪽은 도구 하나에 약 15토큰이 들었어요. 이름 한 줄의 몫입니다. 0이 아니니까 도구를 더 붙이면 켠 쪽도 첫 요청이 계속 커져요.
실습 2: 맞는 도구를 고르나
채점은 grade.mjs가 네 단계로 해요. MCP 도구를 불렀는지(call), 첫 MCP 호출이 정답 도구인지(pick), 인자가 정답 도구의 JSON Schema를 통과하는지(schema), 정답 인자 값이 맞는지(value)를 차례로 보고, 앞 단계를 통과한 호출만 다음 단계로 넘깁니다.
틀린 호출은 이렇게 나눠요.
near: 정답의 비슷한 도구를 먼저 부름same_family: 같은 무리의 다른 도구를 먼저 부름no_call: 도구를 부르지 않고 끝남not_finished:error_max_turns로 끝나거나 180초를 넘김
도구 수 비교 묶음(s2)의 여덟 조건을 집계해 볼게요.
node summary.mjs brief s2 N20-on N20-off N50-on N50-off N100-on N100-off N200-on N200-off
N20-on values ok per run 19, 19, 19 /20, misses same_family 3
turns median 3 (3~6), ToolSearch per call 1~2, duration median 6.6s (4.1s~13.5s), est. $0.96 for 60 calls
N20-off values ok per run 19, 19, 19 /20, misses same_family 3
turns median 2 (2~3), ToolSearch per call 0, duration median 4.5s (2.7s~15.3s), est. $0.76 for 60 calls
N50-on values ok per run 19, 19, 19 /20, misses same_family 3
turns median 3 (3~4), ToolSearch per call 1, duration median 6.2s (4.0s~9.3s), est. $1.07 for 60 calls
N50-off values ok per run 19, 19, 19 /20, misses same_family 3
turns median 2 (2~4), ToolSearch per call 0, duration median 4.9s (2.8s~11.8s), est. $1.01 for 60 calls
N100-on values ok per run 19, 19, 19 /20, misses same_family 3
turns median 3 (3~5), ToolSearch per call 1~2, duration median 6.5s (4.2s~42.7s), est. $1.42 for 60 calls
N100-off values ok per run 19, 19, 19 /20, misses same_family 3
turns median 2 (2~5), ToolSearch per call 0, duration median 5.0s (2.8s~13.2s), est. $1.40 for 60 calls
N200-on values ok per run 19, 19, 19 /20, misses same_family 3
turns median 3 (3~6), ToolSearch per call 1~2, duration median 6.4s (4.0s~12.2s), est. $1.90 for 60 calls
N200-off values ok per run 19, 19, 19 /20, misses same_family 3
turns median 2 (2~4), ToolSearch per call 0, duration median 4.8s (2.7s~11.6s), est. $1.84 for 60 calls
출력을 보면 여덟 조건 모두 세 번 다 19/20이었어요. 과제 단위로 비교해도 두 모드의 결과가 다른 과제는 하나도 없었습니다.
for n in 20 50 100 200; do node summary.mjs flips s2 N$n-on N$n-off; done
s2 N20-on vs N20-off: tasks with different value-pass counts 0
s2 N50-on vs N50-off: tasks with different value-pass counts 0
s2 N100-on vs N100-off: tasks with different value-pass counts 0
s2 N200-on vs N200-off: tasks with different value-pass counts 0
모든 조건에서 놓친 하나는 환불 질문(T33, "주문 ORD-20260917-0042 환불해 줘")이에요. 모델은 get_payment로 결제 상태를 먼저 확인한 뒤 결국 refund_payment를 불렀지만, 첫 호출만 보는 채점 기준에서는 오답입니다.
질문이 정답이 하나뿐인 단순 조회이고 도구 이름이 get_like_count처럼 뜻을 그대로 드러내는 조건이라, 이 결과만으로 도구가 많아도 된다고 보기는 어렵습니다.
정확도가 같으니, 두 모드의 차이는 비용 쪽에서 나요.
- 턴: 켠 쪽 중앙값 3, 끈 쪽 2. 검색 한 번만큼 늘었어요.
- 시간:
duration_ms중앙값이 켠 쪽 6.2~6.6초, 끈 쪽 4.5~5.0초로 1.3~2.1초 길었습니다. - 추정 비용: 60호출 합이 도구 200개에서 켠 쪽 1.90달러, 끈 쪽 1.84달러로 거의 같았습니다. 끈 쪽은 매 요청 3만 7천 토큰을 읽지만 대부분 캐시에서 읽었고, 캐시 읽기는 단가가 낮아요. 캐시가 비어 있는 첫 호출이 많은 환경이라면 끈 쪽이 더 비쌀 수 있어요.
실습 3: 이름이 뜻을 말해 주지 않을 때
tool search가 켜져 있으면 모델이 처음 보는 것은 도구 이름뿐이에요.
그럼 이름이 뜻을 말해 주지 않으면 어떻게 될까요? 도구 200개의 이름을 op_000~op_199로 바꾸고, 설명은 한국어 그대로 둔 채 같은 질문 20개를 세 번씩 보낸 묶음(s4)을 볼게요.
node summary.mjs brief s4 op-on op-off
op-on values ok per run 15, 16, 14 /20, misses not_finished 3, near 6, same_family 4, no_call 2
turns median 9 (4~14), ToolSearch per call 2~15, duration median 24.0s (7.9s~56.1s), est. $13.43 for 60 calls
op-off values ok per run 19, 19, 19 /20, misses same_family 3
turns median 2 (2~4), ToolSearch per call 0, duration median 5.0s (2.9s~11.7s), est. $2.07 for 60 calls
끈 쪽은 설명을 처음부터 다 보니까 이름이 바뀌어도 19/20 그대로였어요. 켠 쪽은 14~16/20으로 떨어졌고, 턴 중앙값은 9, 시간 중앙값은 24초, 60호출 추정 비용은 13.43달러로 끈 쪽의 6.5배였습니다.
검색이 어떻게 헛돌았는지 댓글 수 질문(T09)의 첫 회차로 볼게요(발췌).
node show.mjs out/s4/op-on/T09-r1.jsonl tools,result
{"type":"assistant","message":{"content":[{"type":"tool_use","name":"ToolSearch","input":{"query":"blog post comments","max_results":10}}]}}
{"type":"user","message":{"content":[{"type":"tool_result","content":"No matching deferred tools found"}]},"tool_use_result":{"matches":[],"query":"blog post comments","total_deferred_tools":200}}
{"type":"assistant","message":{"content":[{"type":"tool_use","name":"ToolSearch","input":{"query":"comment","max_results":10}}]}}
{"type":"user","message":{"content":[{"type":"tool_result","content":"No matching deferred tools found"}]},"tool_use_result":{"matches":[],"query":"comment","total_deferred_tools":200}}
{"type":"assistant","message":{"content":[{"type":"tool_use","name":"ToolSearch","input":{"query":"post","max_results":10}}]}}
{"type":"user","message":{"content":[{"type":"tool_result","content":"No matching deferred tools found"}]},"tool_use_result":{"matches":[],"query":"post","total_deferred_tools":200}}
{"type":"assistant","message":{"content":[{"type":"tool_use","name":"ToolSearch","input":{"query":"select:mcp__lab__op_000,mcp__lab__op_001,mcp__lab__op_002,mcp__lab__op_003,mcp__lab__op_004,mcp__lab__op_005,mcp__lab__op_006,mcp__lab__op_007,mcp__lab__op_008,mcp__lab__op_009","max_results":10}}]}}
{"type":"assistant","message":{"content":[{"type":"tool_use","name":"mcp__lab__op_008","input":{"slug":"nginx-502-notes","limit":20}}]}}
번호가 어떤 도구인지는 대응표에서 찾습니다.
grep -E '"op_(008|040|074)"' configs/opaque/N200-ko.map.json
"op_008": "list_comments",
"op_040": "get_timezone_offset",
"op_074": "get_comment_count",
영어 키워드 검색이 세 번 연달아 빈 결과를 돌려주자, 모델은 번호 이름 열 개를 select:로 한꺼번에 불러와 설명을 직접 읽었어요. 그 열 개 안에 정답 get_comment_count(op_074)는 없었고, 비슷한 도구 list_comments(op_008, 글에 달린 댓글 목록)를 불렀습니다.
node summary.mjs searches s4 op-on | head -4
node summary.mjs keywords s4 op-on
s4/op-on: 60 calls, 425 searches (english 170, select 160, korean 87, mixed 8), empty results 227
search hit 50/60; right tool 45; right tool among hits 45/50; right tool among misses 0/10
answered "no tool" (no call, finished) 2, ended by error_max_turns or timeout 8 (no MCP call 3)
gold first returned by select 31, by keyword 19; distinct tools returned per call median 120 (2~200); input tokens of all requests per call median 127,718
s4/op-on keyword searches: english 145 (empty 138), korean 87 (empty 87), mixed 4 (empty 2), required(+) 29 (empty 0)
"slug" -> get_article
"글 slug 발행일 제목" -> get_article
"slug 글" -> get_article
"post article slug" -> get_article
"cpu usage metrics" -> get_server_status, get_cpu_history
"UTC" -> get_timezone_offset
- 60호출에서
ToolSearch를 425번 불렀고, 그 가운데 227번이 빈 결과였어요. - 한국어 검색어 87번은 한 번도 결과를 돌려주지 않았습니다. 영어 검색어가 결과를 돌려준 것은 설명 안에
slug,CPU,UTC같은 영문이 섞여 있던 도구뿐이에요. - 정답 도구를 찾은 50번 가운데 31번은 키워드가 아니라 번호 묶음
select:로 찾았고, 검색에 정답이 걸리지 않은 10번은 하나도 맞히지 못했습니다.
정리
범위: Claude Code 2.1.295 claude -p, Opus 5.5(claude-opus-5-5), --effort high, 가짜 MCP 서버 도구 5~200개, 한국어 질문 20개를 조건마다 3번.
- 끈 쪽은 도구 하나에 약 169토큰씩 늘어 도구 200개에서 첫 요청이 36,980토큰이었고, 켠 쪽은 약 15토큰씩 늘어 6,677토큰이었습니다. 켜도 도구를 붙일수록 첫 요청은 커지니, 자기 서버의 값은 서버를 붙인 호출과 뺀 호출의 첫 요청 입력 토큰 차이를 도구 수로 나눠 재 보세요.
- 이름이 뜻을 드러내는 도구 20~200개에서는 두 모드 모두 세 번 다 19/20이었고, 결과가 다른 과제는 0개였습니다. 정답이 하나뿐인 단순 조회 질문에서 나온 결과이니, 이것만으로 도구를 많이 붙여도 된다고 보지는 마세요.
- 켠 쪽은 검색 한 번만큼 턴 중앙값이 3으로 하나 늘고 시간 중앙값이 1.3~2.1초 길었지만, 도구 200개 60호출 추정 비용은 켠 쪽 1.90달러, 끈 쪽 1.84달러로 거의 같았어요. 캐시가 비어 있는 첫 호출이 많은 환경의 비용은 재지 않았습니다.
- 이름을
op_000~op_199로 바꾸고 설명을 한국어로 둔 서버에서 켠 쪽은 14~16/20으로 떨어지고 턴 중앙값 9, 추정 비용 6.5배가 됐으며, 한국어 검색어 87번은 모두 빈 결과였습니다. 이 글에 싣지 않은 추가 측정에서는 같은 서버의 설명만 영어로 바꾸자 켠 쪽이 18/20, 턴 중앙값 3, 빈 검색 0번으로 돌아왔어요. tool search가 켜진 환경에 붙일 서버는 도구 이름이 뜻을 드러내게 짓고, 설명에 검색에 쓰일 영어 낱말(대상, 동작)을 함께 넣어 주세요.