본문으로 바로가기
Lab

MCP 서버 직접 만들기, 클로드 코드에 붙여 토큰과 호출 확인

의존성 없는 40줄짜리 MCP 서버를 Claude Code에 붙여 새 프로젝트의 한 턴 요청에서 늘어난 입력 토큰(도구 1개 +11, 도구 10개 +167)과 도구를 실제로 부르는지 쟀습니다. 내 서비스용 서버를 만들고 붙이는 순서도 함께 정리했습니다.

·

작은 도구 상자 하나가 큰 코딩 에이전트에 파이프로 연결된 모습을 그린 개념 그림
MCP 서버 직접 만들기, 클로드 코드에 붙여 토큰과 호출 확인 표지 삽화

직접 만든 40줄짜리 MCP 서버를 Claude Code에 붙였더니 요청 한 번의 입력 토큰은 30,714개에서 30,725개가 됐습니다. 11개가 늘어난 셈입니다. 같은 서버에 도구를 10개 두면 +167, 공식 참조 서버 memory는 +150이었죠. 도구를 써 달라고 한 요청 3번에서는 모두 cart_total을 불러 맞는 답 8600을 냈습니다.

도구 하나를 붙이면 토큰이 얼마나 늘까요?

결론부터 보면, 이번 새 프로젝트의 한 턴 측정에서는 도구 하나를 연결했을 때 입력 토큰 중앙값이 11개 늘었습니다. 조건마다 새 임시 프로젝트에서 claude -p "Reply with only: OK"를 한 턴 실행하는 측정을 3번 하고, 중앙값을 비교했습니다.

조건입력 토큰(중앙값)빈 프로젝트 대비3번 잰 값
MCP 없음30,714+030,714 · 30,715 · 30,713
직접 만든 서버(도구 1개)30,725+1130,726 · 30,725 · 30,724
직접 만든 서버(도구 10개)30,881+16730,881 · 30,877 · 30,882
공식 참조 서버 memory30,864+15030,863 · 30,864 · 30,865

이번 세 번의 관측에서는 한 조건 안에서 값이 흔들린 폭보다 조건 사이의 중앙값 차이가 더 컸습니다. 다만 도구 10개짜리가 왜 1개짜리보다 이만큼 늘었는지는 이 측정만으로 가를 수 없습니다. 도구 수, 이름, 설명문이 함께 달라졌기 때문이죠.

MCP 없음, 직접 만든 서버(도구 1개·10개), 공식 memory 서버의
조건별로 직접 잰 입력 토큰 증가량(3회 중앙값). 새 임시 프로젝트의 첫 한 턴 요청 기준이며, 후속 요청마다 같은 증가량이 발생한다는 뜻은 아닙니다
도식출처: Claude Code 입력 토큰 측정

세션 시작 정보도 조건마다 조금씩 달랐습니다. MCP 없음과 직접 만든 서버에서는 내장 도구가 28개였고, MCP 도구는 각각 0개, 1개, 10개였습니다. 그런데 memory 조건에서는 내장 도구 31개, MCP 도구 9개로 나왔습니다. 스킬은 모든 조건에서 22개였어요. 내장 도구 수가 왜 달라졌는지는 측정 기록이 말해 주지 않으니, 비슷한 비교를 할 때 세션 시작 정보를 함께 기록해 두면 좋습니다.

그래서 1인 개발자에게 남는 판단은 이렇습니다. 꼭 필요한 도구 몇 개만 둔 서버라면 붙여 두는 비용은 크지 않았습니다. 반대로 쓰지 않는 도구를 잔뜩 실어 둔 서버라면, 도구를 빼고 같은 방식으로 다시 재 보는 쪽부터 확인하는 편이 좋습니다.

함께 읽을 글

붙여 두면 모델이 정말 도구를 부를까요?

토큰이 적게 든다 해도 모델이 도구를 안 부르면 소용없겠죠. 같은 서버(도구 1개)에 "cart_total 도구로 장바구니 합계를 계산해 숫자만 답해 주세요. 항목: 1,200원짜리 3개, 2,500원짜리 2개"라고 3번 요청했습니다.

회차부른 도구답에 8600 포함걸린 시간입력 토큰
1mcp__tiny-1__cart_total예9.9초93,065
2mcp__tiny-1__cart_total예12.5초93,056
3mcp__tiny-1__cart_total예6.8초93,065

세 번 모두 도구를 불렀고 답도 맞았습니다. 걸린 시간은 6.8초에서 12.5초까지 차이가 났죠. 이 요청의 입력 토큰은 앞의 한 턴 측정보다 훨씬 큽니다. 하지만 요청 문장과 도구 호출 과정이 함께 달라졌으므로, 어느 부분이 얼마를 차지하는지는 이 측정만으로 알 수 없습니다. 직접 확인하려면 결과 JSON의 입력 토큰을 새 입력, 캐시 쓰기, 캐시 읽기로 나눠서 보세요.

한 가지 짚어 둘 점은 요청에서 도구 이름을 직접 지목했다는 것입니다. 도구 이름 없이 일반적인 말로 물었을 때도 잘 불리는지는 이번 측정의 범위 밖입니다. 그렇다면 이 서버는 안에서 어떻게 생겼을까요?

MCP 서버가 내놓는 세 가지

MCP 서버 개념 문서에 따르면 MCP 서버는 표준 프로토콜로 AI 애플리케이션에 기능을 내놓는 프로그램입니다. 기능은 세 가지 단위로 나뉘고, 누가 쓰기를 결정하는지가 다릅니다.

단위하는 일쓰기를 정하는 쪽
도구(Tools)모델이 요청을 보고 직접 부르는 함수입니다. DB 쓰기, 외부 API 호출 등을 합니다모델
리소스(Resources)파일 내용이나 DB 스키마처럼 읽기 전용 맥락을 줍니다애플리케이션
프롬프트(Prompts)특정 도구·리소스로 일하도록 짜 둔 지시 템플릿입니다사용자

이번에 만든 서버는 도구만 내놓습니다. 도구는 하나의 작업만 맡고, 입력은 JSON Schema로 정의합니다. 모델은 tools/list로 도구 목록을 받고, tools/call로 실행하죠.

40줄 서버는 어떻게 주고받나요?

아래 코드는 의존성 없이 Node.js만으로 동작합니다. stdio로 JSON-RPC 2.0 메시지를 한 줄에 하나씩 주고받고, node server.mjs <tools>처럼 실행하면 그 개수만큼 도구를 내놓습니다. 첫 도구 cart_total은 가격 × 수량의 합을 돌려주죠.

js
#!/usr/bin/env node
// A minimal MCP server over stdio (JSON-RPC 2.0, one message per line), no dependencies.
// `node server.mjs <tools>` offers that many tools; the first adds up a cart: sum of price × qty.
import { createInterface } from 'node:readline';

const count = Math.max(1, Number(process.argv[2] ?? 1));
const items = { type: 'array', items: { type: 'object', properties: { price: { type: 'number' }, qty: { type: 'number' } }, required: ['price', 'qty'] } };
const tools = Array.from({ length: count }, (_, i) => ({
  name: i === 0 ? 'cart_total' : `cart_total_${i + 1}`,
  description: i === 0 ? '장바구니 항목(price, qty)을 받아 합계(price × qty의 합)를 돌려준다.' : `측정용 도구 ${i + 1}: 장바구니 항목을 받아 합계를 돌려준다.`,
  inputSchema: { type: 'object', properties: { items }, required: ['items'] },
}));
const send = (msg) => process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...msg })}\n`);

createInterface({ input: process.stdin }).on('line', (line) => {
  let req;
  try { req = JSON.parse(line); } catch { return; }
  if (req.id === undefined) return; // notifications (notifications/initialized) need no answer
  if (req.method === 'initialize') {
    return send({ id: req.id, result: { protocolVersion: req.params?.protocolVersion ?? '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'tiny-cart', version: '1.0.0' } } });
  }
  if (req.method === 'tools/list') return send({ id: req.id, result: { tools } });
  if (req.method === 'tools/call') {
    const total = (req.params?.arguments?.items ?? []).reduce((sum, i) => sum + i.price * i.qty, 0);
    return send({ id: req.id, result: { content: [{ type: 'text', text: String(total) }] } });
  }
  if (req.method === 'ping') return send({ id: req.id, result: {} });
  send({ id: req.id, error: { code: -32601, message: `unknown method ${req.method}` } });
});

코드가 메시지를 처리하는 순서는 다음과 같습니다.

  • initialize: 프로토콜 버전과 서버 정보, 도구 기능이 있다는 사실을 답합니다.
  • notifications/initialized처럼 id가 없는 알림: 답하지 않습니다.
  • tools/list: 도구 이름, 설명, 입력 스키마 목록을 돌려줍니다.
  • tools/call: 합계를 계산해 텍스트로 돌려줍니다.
  • ping은 빈 결과로 답하고, 그 밖의 메서드는 -32601 오류로 답합니다.
initialize 응답, 알림 무시, 도구 목록 응답, 도구 실행 응답, 그 밖의 메서드 처리로 이어지는 순서도
stdio로 한 줄에 메시지 하나씩 주고받을 때 서버 코드가 처리하는 순서
도식출처: 근거를 바탕으로 코드가 그린 도식

여기서 가장 흔한 실수가 하나 있습니다. stdio 서버는 표준 출력이 곧 통신 통로라서, console.log()로 로그를 찍으면 JSON-RPC 메시지가 깨지고 서버가 망가집니다. 서버 만들기 문서는 console.error()처럼 표준 오류(stderr)로 쓰라고 안내합니다. 위 코드도 응답 말고는 표준 출력에 아무것도 쓰지 않죠.

클라이언트와 서버 프로세스 사이의 메시지 통로와, 따로 빠져나가는 로그 통로를 그린 개념 그림
stdio 서버에서 표준 출력은 메시지 전용 통로이고 로그는 표준 오류로 보낸다는 개념을 그린 삽화

클로드 코드에 MCP 서버를 붙이는 방법

이제 서버를 Claude Code에 연결할 차례입니다. Claude Code MCP 문서에서 로컬 stdio 서버의 기본 문법은 이렇습니다.

bash
# Basic syntax
claude mcp add [options] <name> -- <command> [args...]

-- 앞은 --transport, --env, --scope 같은 Claude Code의 옵션이고, 뒤는 서버를 실행하는 명령입니다. -- 뒤의 내용은 그대로 서버에 넘어가죠. 그래서 서버가 받는 인자(이 글의 서버라면 도구 개수)는 -- 뒤에 둡니다.

bash
claude mcp add --transport stdio myserver -- npx server

원격 서버라면 claude mcp add --transport http <name> <url>을 씁니다. 범위는 따로 정하지 않으면 로컬 범위(local scope)에 기록되고, --scope project나 --scope user로 바꿀 수 있습니다. 팀과 공유하려면 프로젝트 루트의 .mcp.json에 넣고 커밋하면 됩니다.

뼈대를 손으로 쓰기 번거롭다면 공식 mcp-server-dev 플러그인에 맡길 수도 있습니다. claude를 실행한 뒤 아래 두 명령을 차례로 입력하면, 사용 목적을 묻고 원격 HTTP나 로컬 stdio 서버를 만들어 줍니다.

/plugin install mcp-server-dev@claude-plugins-official
/mcp-server-dev:build-mcp-server

남이 만든 서버를 붙일 때는 믿을 수 있는지 먼저 확인하세요. 외부 콘텐츠를 가져오는 서버는 프롬프트 인젝션 위험에 노출될 수 있다고 문서가 경고합니다.

내 서비스용 서버를 만드는 순서

위 내용을 1인 개발자의 작업 순서로 묶으면 이렇습니다.

  1. 채팅에 자꾸 복사해 붙이던 데이터를 찾습니다. 문서도 이슈 트래커나 모니터링 대시보드처럼 그런 도구가 있을 때 서버를 연결하라고 권합니다.
  2. 작업 하나에 도구 하나를 두고, 입력을 JSON Schema로 적습니다. 이름과 설명은 모델이 읽는 글이니 짧고 분명하게 씁니다.
  3. 구현 방식을 고릅니다. 위처럼 의존성 없이 써도 되고, TypeScript라면 Node.js 20 이상에서 npm install @modelcontextprotocol/server zod로 SDK를 씁니다.
  4. 로그는 표준 오류로만 보냅니다.
  5. claude mcp add ... -- <command>로 붙이고 범위를 정합니다.
  6. 도구 이름을 지목한 요청을 몇 번 돌려 호출 여부와 답을 확인하고, claude -p "Reply with only: OK" 한 턴으로 붙이기 전후의 입력 토큰을 비교합니다.

측정 조건과 한계

측정 환경은 Claude Code 2.1.309, 모델 claude-opus-5-5, darwin arm64, Node.js v22.23.1입니다. 사용자 설정·플러그인·훅은 --setting-sources project로 빼고, MCP는 --strict-mcp-config로 조건에 적은 서버만 붙였습니다. memory는 @modelcontextprotocol/server-memory 1.0.0을 npm으로 설치해 node로 실행했어요. 조건마다 3번씩 번갈아 쟀고, 뺀 회차는 없었습니다.

한계도 분명합니다. 토큰은 한 턴짜리 요청의 입력 토큰만 쟀고, 긴 대화에서 쌓이는 양은 보지 않았습니다. 도구도 장바구니 합계라는 작은 것 하나였죠. 그러니 설명이 길거나 스키마가 복잡한 도구, 도구 이름을 말하지 않는 요청이라면 같은 방법으로 직접 재 보는 것이 가장 확실합니다.

참고 자료