Claude Code로 MCP 서버 만들기 (‘Skill으로 충분하지 않을까?’에 대한 답변과, SDK 유무별 최소 구현)
요약
본 글은 Claude Code에서 절차를 추가하는 '스킬(Skill)'과 실제 기능을 수행하는 'MCP 서버'의 차이점을 비교 설명합니다. 스킬은 매뉴얼 역할로, 간단한 실행이나 정보 제공에 적합하며, MCP는 새로운 도구 정의와 기능을 대화 시스템에 영구적으로 추가할 때 사용됩니다.
핵심 포인트
- 스킬(Skill)은 절차를 담은 매뉴얼 역할을 하며, 필요할 때만 내용을 보여줍니다.
- MCP 서버는 Claude Code에 실제 '도구'를 추가하고 그 정의를 전달하는 방식입니다.
- SDK 없이 Node.js만으로도 MCP 서버를 구현하여 Claude Code에 등록할 수 있습니다.
- MCP 사용 시 도구의 이름과 스키마가 시스템 메시지에 명확히 노출됩니다.
Claude Code를 자신에게 확장하는 방법으로서 스킬(SKILL.md)은 완전히 정착했습니다. 자연어로 절차만 작성하면 되기 때문에 간편합니다. 반면 MCP 서버는 직접 만드는 사람이 적다는 인상을 받습니다. 이름에 '서버'가 붙어 있어 어딘가에 상주시키거나 계속 실행해야 하는 것처럼 보이고, 자연어가 아니라 프로그램을 작성할 필요도 있습니다.
- '서버'라는 말을 들으면 어딘가에 상시 구동하거나 배포하는 것이라고 생각한다.
- 스킬로 충분한데 MCP를 만들 의미를 모르겠다.
- 프로그램 작성이 스킬보다 훨씬 어려워 보인다.
실제 MCP 서버는 Claude Code가 실행되는 작은 스크립트로, SDK를 사용하지 않아도 Node.js만으로 작성할 수 있습니다. 이 글에서는 먼저 스킬과 MCP의 차이점을 확인하고, MCP로 만들 의미가 있는 경우를 정리합니다. 그 후, MCP 서버를 SDK 없음과 SDK 있음 두 가지 방식으로 만들어 Claude Code에 등록하여 사용해 보겠습니다.
저는 'Claude Code의 내부를 볼 수 있다. 할 수 있는 것이 늘어난다.'라는 슬로건을 가진 macOS 앱 'tanacode'를 개인적으로 만들고 있습니다. 읽은 파일이나 컨텍스트 내용을 채팅 옆에 보여주는 기능 외에도, 제가 구동하는 Claude Code에 4개의 MCP 서버(브라우저 확인/체크리스트/하위 세션/워크스루. 총 49개의 도구)를 추가했습니다. 이 글에서는 그 제작 과정도 예시로 사용하고, 후반부에서 만들어보면서 알게 된 점들을 소개하겠습니다.
스킬과 MCP는 Claude에게 어떻게 전달되는가
Claude Code에는 절차를 추가하는 스킬(.claude/skills/<이름>/SKILL.md)과 도구를 추가하는 MCP가 있습니다. 각각이 Claude에게 어떻게 전달되는지, 요청 내용을 통해 확인했습니다.
스킬: 처음에는 설명만, 사용할 때 본문
설명에 '표식: SKILL-DESC', 본문에 '표식: SKILL-BODY'라고 적은 스킬을 두자, 첫 번째 요청에는 설명만 들어 있었습니다.
The following skills are available for use with the Skill tool:
- release-note: 리스 노트 작성 절차 (표식: SKILL-DESC)
Claude가 Skill 도구로 스킬을 사용하면, 그때서야 본문이 대화에 들어옵니다. 스킬은 필요할 때 읽는 매뉴얼입니다. 새로운 도구가 늘어나지 않기 때문에, 절차에 적힌 내용은 Claude가 현재 가지고 있는 도구(Bash, Read, Edit 등)로 실행합니다.
스킬에 스크립트를 넣어두면 API를 호출하는 같은 처리도 스킬로 작성할 수 있습니다. 본문에 'node scripts/search.mjs <키워드>로 검색한다'라고 적으면, Claude는 Bash로 그것을 실행합니다. 한 번 호출해서 결과를 볼 것만이라면, 스킬로 충분합니다.
MCP: 도구가 늘어나고, 그 정의가 전달된다
이후에 만들 메모의 MCP 서버를 등록하고 Claude에게 전달된 요청을 살펴보니, 도구 이름은 mcp__<서버명>__<도구명> 형태였습니다.
{"name": "mcp__memo__save_memo", "description": "Save a short memo that the user wants to keep across sessions.", "input_schema": {"type": "object", "properties": {"text": {"type": "string", "description": "The memo text"}}, "required": ["text"]}}
initialize로 반환한 서버 설명(instructions)은 대화의 초반부 system 발언에 다음과 같이 들어 있었습니다.
# MCP Server Instructions
The following MCP servers have provided instructions for how to use their tools and resources:
## memo
...
도구의 설명은 '이 도구가 무엇을 하는지', 서버의 설명은 '어떤 때에 이 서버의 도구를 사용할 것인지'를 작성하는 공간입니다.
비교하자면
| 스킬 | MCP | |
|---|---|---|
| 내용 | 절차서(Markdown)와 첨부된 스크립트 등 | 작동하는 프로그램 |
| ... |
차이점은 '내용을 Claude에게 맡기느냐'에 달려있다
스킬로도 API를 호출할 수 있다면, MCP를 사용할 의미는 어디에 있을까요? 가장 큰 차이점은 툴의 내용을 Claude가 임의로 변경할 수 없다는 점입니다.
| 스킬 (및 스크립트) | MCP 툴 | |
|---|---|---|
| 사용 여부 | Claude가 결정 | Claude가 결정 |
| ... | 프로그램이 결정한다. 대화 속에서 Claude는 변경할 수 없다 |
스킬은 Claude가 읽고 판단하여 실행하기 위한 절차서입니다. MCP의 툴은 사용하는지 여부만 Claude에게 맡기고, 사용한 후의 처리는 코드로 정의된 부품입니다.
tanacode의 체크리스트로 예를 들어보겠습니다. tanacode에는 사람과 Claude가 함께 보면서 수정하는 체크리스트가 있는데, Claude는 MCP 툴을 통해 읽고 쓰기 합니다. 그중 '결정 사항'은 대화에서 결정된 내용(예: '이 PR에서는 결제 코드에 손대지 않기')을 남겨두는 목록이며, 확인해도 사라지지 않습니다. 이 목록에서는 Claude가 할 수 있는 것은 카드를 추가하는 것과 이유를 붙여 취소하는 것뿐입니다. 수정하려고 하면 앱의 코드가 막히면서 다음과 같이 응답합니다.
"결정 사항"은 ... 여기에 카드를 추가하고, 이유와 함께 취소(card_revoke, 이는 쓰레기통으로 이동함)할 수 있지만, 카드 자체를 재작성할 수는 없습니다. 오직 사용자만 가능합니다.
스킬에 '결정 사항은 수정하지 마세요'라고 적어도, 지켜줄지 여부는 Claude에게 달려있습니다. MCP 툴이라면, 지키게 할 수 있습니다.
AI에게 맡기지 않고 코드로 결정하는 메커니즘은 다른 곳에도 있습니다. 무엇을 코드로 결정하느냐에 따라 나열하면 다음과 같습니다.
| 언제 작동하는가 | 무엇을 하는가 | |
|---|---|---|
| 스킬 | Claude가 결정 | Claude가 결정 (절차서를 읽고 실행한다) |
| ... | ||
| MCP는 사용하는 판단은 AI에게 맡기고, 사용한 결과는 확실하게 만드는, 딱 중간 지점에 있습니다. |
어떤 것을 선택할까
스킬로 충분한 경우는 절차나 규칙을 전달하고 싶을 때입니다. 릴리스 노트 작성 방식이나 리뷰 관점처럼, Claude가 현재 가지고 있는 툴로 할 수 있는 것이라면 스킬에 적는 것만으로 충분합니다. gh
이나 aws
처럼 이미 CLI가 존재하는 것도 사용법을 스킬에 쓰면 충분한 경우가 많습니다. 한 번 API를 호출하는 스크립트도, 스킬에 넣어두면 충분합니다.
MCP가 적합한 경우는 다음과 같습니다.
- 코드로 허용할 수 있는 것을 결정하고 싶을 때 (결정 사항은 추가만 하고, 읽는 툴과 쓰는 툴을 분리하는 경우 등)
- 작동 중인 앱과 상호작용할 때. tanacode의 체크리스트는 앱이 저장하여 화면에 보여주는 것이고, Claude는 MCP 툴을 통해 읽고 쓰기 합니다.
- 사람의 조작을 기다려야 할 때 / 로그인이나 연결 상태를 유지해야 할 때
- 같은 서버를 Claude Desktop 등 MCP를 지원하는 다른 앱에서도 사용할 때
둘 다 조합할 수도 있습니다. MCP로 할 수 있는 것을 추가하고, 언제/어떤 순서로 사용할지에 대한 긴 절차는 스킬에 적으면 평소의 컨텍스트를 늘리지 않아도 됩니다.
MCP는 어렵지 않다: 서버를 띄울 필요가 없다
이름 때문에 오해하기 쉽지만, 로컬에서 사용하는 MCP 서버는 포트를 열고 상시 구동시키는 것이 아닙니다. Claude Code가 자식 프로세스로 시작하여 표준 입출력(stdio)으로 주고받는 단순한 스크립트입니다. HTTP로 연결하는 형태도 있지만, 이것은 원격으로 제공되는 서버를 위한 것이며, 스스로 사용하는 도구라면 stdio로 충분합니다.
이후에 만들 메모 서버에 대해, 시작/수신 메시지/종료를 기록하도록 하여 툴을 두 번 호출해 보았습니다.
13:32:29.154 start
13:32:29.162 recv server/discover
13:32:29.174 recv initialize
...
Claude Code 실행과 함께 스크립트가 하나 실행됩니다. 도구 목록을 반환한 후에는 호출될 때까지 입력을 기다리기만 합니다.
두 번의 호출은 같은 프로세스가 받습니다.
Claude Code가 끝나면 시그널(SIGINT)로 중지할 수 있습니다 (마지막 줄은 받은 시그널을 기록한 것입니다).
서버를 준비하거나 배포하거나, 잊어버리고 종료하지 않을 걱정을 할 필요는 없습니다. Claude Code를 사용하는 동안만 작동하는 작은 스크립트를 작성하면 충분합니다.
MCP 서버가 하는 일
표준 입출력(stdio)으로 JSON-RPC 메시지를 한 줄씩 주고받습니다. 도구를 추가하기만 한다면, 다음 4가지로 응답할 수 있습니다.
| 메서드 | 역할 |
|---|---|
initialize | 대응하는 버전, 서버 이름, 서버 설명(instructions)을 반환합니다. |
tools/list | 도구 목록(이름, 설명, 인자의 JSON Schema)을 반환합니다. |
tools/call | 도구를 실행하고 결과를 반환합니다. |
ping | 빈 결과를 반환합니다. |
notifications/initialized와 같은 알림(ID가 없는 메시지)에는 응답하지 않습니다. 모르는 메서드에는 오류(Method not found)를 반환합니다. 위 기록의 server/discover에도 오류를 반환했지만, 문제없이 진행되었습니다.
SDK 없이 작성하기
메모를 저장하는 save_memo와 목록을 반환하는 list_memos 두 개의 도구를 가진 서버입니다. Node.js만으로 작성할 수 있습니다.
// 메모를 저장하고 읽는 MCP 서버(stdio). SDK를 사용하지 않고, JSON-RPC를 한 줄씩 주고받음
import { createInterface } from 'node:readline';
import { readFileSync, writeFileSync } from 'node:fs';
...
protocolVersion은 간단하게 하기 위해 Claude Code가 요구한 버전을 그대로 반환하고 있습니다. 제대로 만들려면 대응하는 버전 목록을 가지고, 요청된 버전이 없으면 자신의 버전으로 응답해야 합니다.
SDK로 작성하기
'왜 SDK를 사용하지 않는 거야?'라고 생각했을 분도 계실 겁니다. 공식 SDK(@modelcontextprotocol/sdk 1.32.1 및 인자 형태를 작성하는 zod)를 사용하면, 같은 서버를 다음과 같이 작성할 수 있습니다.
// memo-mcp.mjs와 동일한 도구를 공식 SDK로 작성한 것
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
...
npm install @modelcontextprotocol/sdk zod
Claude Code에서 호출했을 때, SDK가 없는 버전과 동일하게 작동했습니다. SDK는 버전 규약, ping, 알림, 취소 등의 규칙을 처리해주고 인자 검사도 해줍니다. text를 전달하지 않고 save_memo를 호출하면, 도구 내부 로직이 실행되기 전에 SDK가 다음과 같이 응답했습니다.
MCP error -32602: Input validation error: Invalid arguments for tool save_memo: Invalid input: expected string, received undefined at text
SDK를 사용하는 장점과 단점
| SDK 사용 | SDK 미사용 |
|---|---|
| 작성 분량 | 36 줄 |
| 인자 검사 | zod 정의로부터 자동 |
| 명세의 세부 사항 (버전 규칙, 취소, 리소스나 프롬프트, HTTP를 통한 상호작용 등) | SDK가 담당 |
| 명세 변경 | SDK 업데이트로 따라잡음 |
| 의존성 | 91 패키지・29 MB (node_modules) |
실행 및 initialize 응답까지 걸리는 시간 (리눅스 기준 중앙값) | 216 밀리초 |
| 배포 방법 | npm install 필요 (패키지화하거나 모아서 배포해야 함) |
SDK 작성 방식도 버전에 따라 바뀝니다. 1.32.1 버전에서는 도구를 등록하는 server.tool()이 비권장(deprecated)되어 registerTool()을 사용하게 되었습니다.
처음 만들 때, 도구가 많을 때, HTTP로 공개할 때는 SDK를 사용하는 것이 안전합니다. 의존성을 늘리고 싶지 않거나 앱에 포함시킬 때는 SDK 미사용도 선택할 수 있습니다. tanacode는 MCP 서버를 앱에 포함시키고, 앱 본체(Electron)를 Node.js로 구동하여 시작합니다. 사용할 메서드와 취소 알림만 사용하기 때문에, SDK를 사용하지 않고 작성했습니다.
Claude Code에 등록하기
claude mcp add로 등록합니다. -- 뒤가 서버를 실행하는 명령어입니다.
$ claude mcp add memo -- node /path/to/memo-mcp.mjs
Added stdio MCP server memo with command: node /path/to/memo-mcp.mjs to local config
$ claude mcp list
...
등록 위치는 --scope로 선택할 수 있습니다.
--scope | 등록 위치 | 사용 가능 범위 |
|---|---|---|
local (기본값) | ~/.claude.json의 해당 폴더 | 자신만・해당 폴더만 |
project | 리포지토리의 .mcp.json | 커밋하면, 리포지리를 사용하는 모든 사람 |
user | ~/.claude.json | 자신만・어느 폴더에서든 |
--scope project로 .mcp.json에 작성된 서버는 claude mcp list에서
의 permissions.allow
에 같은 이름을 작성합니다.
오류는, 다음에 할 일과 함께 반환
툴 안에서 실패했을 때는 결과에 isError: true를 붙입니다. 빈 메모를 저장하게 하면, Claude에게는 오류 표시와 함께 다음 문장이 전달되었습니다.
text is empty. Ask the user what to save.
Claude는 이 문장을 읽고 다음 행동을 결정합니다. '실패했습니다'뿐만 아니라, 무엇이 부족하고 다음에 무엇을 해야 하는지까지 적어두면, 같은 호출을 반복할 필요가 없습니다.
너무 긴 결과는 파일로 저장됨
결과가 길면, Claude Code는 그대로 전달하지 않았습니다.
| 결과 길이 | Claude에 전달된 것 |
|---|---|
| 3만 문자 | 그대로 |
| ... | |
| 파일로 저장할지 여부는 글자 수로, 오류 처리 여부는 토큰 수로 결정되는 것 같습니다. 같은 10만 문자라도, 한 글자의 반복처럼 토큰이 적은 것은 파일의 위치와 시작 2KB였습니다. |
어느 경우든 Claude는 파일을 다시 읽으면 내용에 도달할 수 있습니다. 다만, 그만큼 호출이 늘어납니다. 페이지 전체나 로그처럼 길어지는 내용은, 툴의 인자(argument)로 범위나 건수를 제한할 수 있도록 해두면 다루기 쉽습니다.
응용: 공식 MCP 서버가 없는 SaaS도 MCP로 만들 수 있다
MCP 서버의 내용은 tools/call에서 받은 인자를 사용해 원하는 처리를 할 수만 있으면 됩니다. 따라서, 공식 MCP 서버가 없는 SaaS나 사내 툴도, 그것을 조작하는 스크립트를 MCP 형태로 작성하면 Claude Code에서 사용할 수 있게 됩니다.
- Web API가 있다면,
fetch로 호출하고 -
CLI가 있다면,execFile로 실행하여 출력을 반환하고 - 화면만 있다면, 브라우저를 조작하는 스크립트(Playwright 등)를 호출합니다.
한 번 호출해서 결과를 볼 것만이라면, 앞서 작성한 대로 스크립트를 스킬에 넣어도 충분합니다. MCP 툴로 만들어 두면, Claude가 할 수 있는 작업을 툴이 준비한 것에 한정할 수 있습니다. 예를 들어, 과제 관리 SaaS의 검색 API를 호출하는 툴은 위 메모의 서버의 call을 교체하기만 하면 작성할 수 있습니다. 이 툴에서는 Claude가 검색만 할 수 있습니다. 토큰에 업데이트나 삭제 권한이 있어도, 툴을 통하는 한 사용되지 않습니다.
const API = process.env.TICKETS_API ?? 'https://api.example.com';
const tools = [{
name: 'search_tickets',
...
call이 비동기(asynchronous)가 되므로, tools/call에서는 결과가 도착한 후에 답변을 작성합니다.
case 'tools/call':
// API를 기다리는 동안에도 다른 메시지를 받을 수 있도록, 결과가 도착한 후에 반환
return void call(req.params.name, req.params.arguments ?? {}).then(
...
API의 토큰은 등록할 때 -e로 환경변수로 전달합니다.
claude mcp add tickets -e TICKETS_TOKEN=xxxx -- node /path/to/tickets-mcp.mjs
API인 것처럼 하는 로컬 서버에서 테스트해 본 결과, Claude Code에서 툴을 호출하면 검색 결과가 반환되었고, 토큰을 잘못 입력했을 때는 'The API returned 401. Ask the user to check TICKETS_TOKEN.'이 오류로 반환되었습니다.
tanacode로 49개의 툴을 만들면서 알게 된 것
tanacode가 추가한 4개의 MCP 서버는 다음과 같습니다.
| 서버 | 툴의 수 | 할 수 있는 것 |
|---|---|---|
| 앱 내 브라우저 | 18 | 스크린샷・클릭・입력・콘솔 로그・사람에게 조작을 부탁하기 등 |
| ... | ||
| 모두, 위 최소 서버와 마찬가지로 SDK를 사용하지 않고, | ||
initialize | ||
・tools/list | ||
・tools/call | ||
・ping | ||
과 취소(notifications/cancelled) |
)만 작성하고 있습니다. 만들면서 알게 된 점을 소개합니다.
서버 설명에는 '언제 사용하는지'를 작성하세요
사람이 도구의 존재를 모른다면, '체크리스트를 사용해서'라고 부탁하지 않습니다. 그래서 서버 설명에 Claude가 스스로 사용할 상황을 작성하고 있습니다. 체크리스트 서버 설명은 이렇게 시작합니다.
The user may not know about these checklists, so do not wait to be asked; use them as described below.
이어서 '절차가 여러 개 있는 작업을 받았다면, 시작하기 전에 할 일 목록을 만든다'처럼 사용하는 상황을 구체적으로 나열하고 있습니다.
Claude만 읽는 문장은 영어로 작성하세요
도구와 서버 설명/도구의 결과는 Claude만 읽기 때문에, Claude Code의 시스템 프롬프트에 맞춰 영어로 작성합니다. 사람이 일본어로 부탁하면, Claude는 일본어로 답변합니다. 체크리스트 카드처럼 사람이 읽는 것은 '사람이 사용하는 언어로 작성한다'고 설명에서 전달하고 있습니다.
호출 횟수를 줄이는 방식으로 만드세요
도구를 한 번 호출할 때마다, Claude Code는 결과와 그전까지의 대화를 모두 포함한 요청을 API에 다시 보냅니다. 대화가 길수록, 한 번의 호출 비용이 높아집니다. tanacode에서는 긴 대화(컨텍스트 1020만 토큰)에서 체크리스트 도구를 36회 호출하는 턴이, API 요금으로 환산하여 약 1.2달러 상당이었습니다.
그래서 카드를 추가하는 card_add는 한 번에 여러 장을 추가할 수 있도록 했습니다(인수 cards가 배열).
체크하거나 삭제하는 것도 번호를 모아서 전달할 수 있습니다.
사람을 기다리는 도구는 120초로 이동됩니다
로그인처럼 사람의 조작을 기다리는 도구는 Claude Code가 120초 후에 백그라운드 작업으로 옮깁니다. 또한, 도구를 기다릴 최대 시간(claude -p에서는 60초)을 초과하면 중단됩니다. 자세한 내용은 MCP 도구에서 '사람의 조작을 기다리기' 기사에 정리했습니다.
취소되면 기다리는 것을 멈춥니다
사람이 Esc로 중단하거나, 기다릴 최대 시간을 초과하면 Claude Code는 notifications/cancelled로 취소를 알려줍니다(params.requestId가 취소 호출의 id). tanacode 서버는 이것을 받으면 처리 대기를 멈추고, 해당 호출에는 답변을 작성하지 않습니다.
앱과 MCP 서버를 분리하세요
MCP 서버는 Claude Code 세션마다 Claude Code가 시작됩니다. tanacode의 MCP 서버는 도구 호출을 앱으로 전달하는 중계 역할만 하며, 앱과는 Unix 소켓으로 주고받습니다. HTTP를 사용하지 않은 이유는, 앱을 다시 시작할 때마다 포트가 바뀌어 움직이고 있는 Claude Code에서 연결이 끊기기 때문입니다.
앱이 닫혀 있을 때는, 중계가 Claude에게 이렇게 반환합니다. Claude는 사람에게 앱의 시작을 부탁할 수 있습니다.
tanacode is not running. To use checklists, ask the user to start tanacode.
도구 이름과 종류를 테스트로 고정하세요
도구의 이름이나 '읽기 전용인지'를 변경하면, 허가 설정(--allowedTools)이나, 허가 확인이 나오는지 여부가 달라집니다. tanacode에서는 도구의 이름·종류·인수 형태를 스냅샷 테스트로 보관하고, 변경했을 때는 차이점(diff)으로 알 수 있게 했습니다.
덧붙임: tanacode의 도구는 채팅에서 한 줄로 요약됩니다
tanacode의 채팅에서는 MCP 도구 호출을 '체크리스트 · 체크하기 · 완료 전 체크 #2'와 같은 짧은 한 줄로 표시하고, 누르면 해당 대상(카드 등)을 엽니다. 도구마다 사람이 읽는 짧은 이름을 정해두고, 같은 이름은 tools/list의 title에도 넣었습니다.

맺음말
절차나 규칙은 스킬에, 확실히 하고 싶은 조작은 MCP에. MCP 서버도 JSON을 한 줄씩 읽고 쓰는 작은 스크립트부터 시작할 수 있습니다. 스킬로 부족하다고 느껴지면, 시도해 보세요.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기