티스토리 뷰

== 검증 필요==

MCP 서버 만들기: 처음부터 끝까지

이 문서는 MCP(Model Context Protocol)를 처음 접하는 분을 위한 교육용 가이드입니다. "할일 관리" MCP 서버 하나를 처음부터 만들고, 테스트하고, Claude에 연결하는 과정을 단계별로 설명합니다.


스킬과 MCP의 차이

MCP를 시작하기 전에, 앞서 만든 스킬과 무엇이 다른지 이해하는 것이 중요합니다.

 

  스킬 (SKILL.md)  MCP 서버
작동 방식 Claude가 지침 문서를 읽고 스스로 판단해서 작업 Claude가 실제 코드를 실행해서 결과를 받아옴
할 수 있는 것 텍스트 작성, 형식 변환, 분석 파일 읽기/쓰기, 외부 API 호출, 데이터베이스 조회
실제 데이터 없음 (Claude의 언어 능력만 사용) 있음 (진짜 데이터를 가져오거나 저장)

예를 들어 "내 할일 목록 보여줘"라는 요청에서, 스킬은 할일 목록을 지어낼 수밖에 없지만 MCP는 실제 저장된 데이터를 가져올 수 있습니다.


준비사항

  • Python 3.10 이상이 설치되어 있어야 합니다
  • Claude Code가 설치되어 있어야 합니다
  • 터미널 사용법을 기본적으로 알고 있어야 합니다

1. 예제 시나리오

Claude에게 "할일 추가해줘", "할일 목록 보여줘", "완료 표시해줘"라고 말하면 실제로 파일에 저장되고 불러와지는 할일 관리 MCP 서버를 만들겠습니다.

이 서버는 세 가지 도구(tool)를 제공합니다.

도구 이름 하는 일
todo_add 새 할일을 추가한다
todo_list 저장된 할일 목록을 보여준다
todo_complete 특정 할일을 완료 처리한다

2. 폴더 만들기

mkdir todo-mcp
cd todo-mcp

3. 의존성 설치

MCP 서버를 Python으로 만들 때는 FastMCP 라이브러리를 사용합니다.

pip install "mcp[cli]"

설치 확인:

python -c "from mcp.server.fastmcp import FastMCP; print('설치 완료')"

[설명]
python -c: 파이썬 파일 없이 한 줄짜리 코드를 바로 실행
from mcp.server.fastmcp import FastMCP: mcp 패키지 안의 FastMCP를 가져옴
import가 성공하면 print('설치 완료') 실행

 


4. 서버 코드 작성

todo-mcp/ 폴더 안에 server.py 파일을 만듭니다.

#!/usr/bin/env python3
"""
할일 관리 MCP 서버.
할일을 추가, 조회, 완료 처리하는 도구를 제공합니다.
"""

import json
from pathlib import Path
from pydantic import BaseModel, Field, ConfigDict
from mcp.server.fastmcp import FastMCP

# 서버 초기화 (이름은 {서비스명}_mcp 형식으로)
mcp = FastMCP("todo_mcp")

# 할일 데이터를 저장할 파일 경로
TODO_FILE = Path(__file__).parent / "todos.json"


# ─── 내부 유틸리티 함수 ────────────────────────────────────────────

def _load_todos() -> list[dict]:
    """저장된 할일 목록을 불러온다. 파일이 없으면 빈 목록 반환."""
    if not TODO_FILE.exists():
        return []
    return json.loads(TODO_FILE.read_text(encoding="utf-8"))


def _save_todos(todos: list[dict]) -> None:
    """할일 목록을 파일에 저장한다."""
    TODO_FILE.write_text(
        json.dumps(todos, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )


# ─── 입력 모델 정의 ────────────────────────────────────────────────
# Claude가 도구를 호출할 때 어떤 값을 넘겨야 하는지 정의합니다.

class TodoAddInput(BaseModel):
    """할일 추가 입력 모델."""
    model_config = ConfigDict(str_strip_whitespace=True)

    title: str = Field(
        ...,
        description="추가할 할일 내용 (예: '보고서 작성', '팀 미팅 준비')",
        min_length=1,
        max_length=200
    )


class TodoCompleteInput(BaseModel):
    """할일 완료 처리 입력 모델."""
    model_config = ConfigDict(str_strip_whitespace=True)

    todo_id: int = Field(
        ...,
        description="완료 처리할 할일의 번호 (todo_list로 확인 가능)",
        ge=1
    )


# ─── 도구(Tool) 정의 ───────────────────────────────────────────────

@mcp.tool(
    name="todo_add",
    annotations={
        "readOnlyHint": False,      # 데이터를 변경함
        "destructiveHint": False,   # 기존 데이터를 삭제하지 않음
        "idempotentHint": False,    # 같은 내용을 두 번 추가하면 두 개가 생김
    }
)
async def todo_add(params: TodoAddInput) -> str:
    """새 할일을 목록에 추가한다.

    Args:
        params: 할일 내용 (title)

    Returns:
        추가된 할일의 번호와 내용을 포함한 확인 메시지
    """
    todos = _load_todos()

    # 새 할일 생성 (번호는 현재 목록 개수 + 1)
    new_todo = {
        "id": len(todos) + 1,
        "title": params.title,
        "done": False
    }
    todos.append(new_todo)
    _save_todos(todos)

    return f"✅ 할일 추가됨: [{new_todo['id']}] {new_todo['title']}"


@mcp.tool(
    name="todo_list",
    annotations={
        "readOnlyHint": True,       # 데이터를 읽기만 함
        "destructiveHint": False,
        "idempotentHint": True,     # 같은 결과를 반복해서 반환
    }
)
async def todo_list() -> str:
    """저장된 할일 목록 전체를 보여준다.

    Returns:
        번호, 내용, 완료 여부가 포함된 할일 목록.
        할일이 없으면 빈 목록 메시지 반환.
    """
    todos = _load_todos()

    if not todos:
        return "등록된 할일이 없습니다."

    lines = ["# 할일 목록", ""]
    for todo in todos:
        status = "✅" if todo["done"] else "⬜"
        lines.append(f"{status} [{todo['id']}] {todo['title']}")

    total = len(todos)
    done = sum(1 for t in todos if t["done"])
    lines.append("")
    lines.append(f"전체 {total}개 | 완료 {done}개 | 남은 것 {total - done}개")

    return "\n".join(lines)


@mcp.tool(
    name="todo_complete",
    annotations={
        "readOnlyHint": False,      # 데이터를 변경함
        "destructiveHint": False,
        "idempotentHint": True,     # 이미 완료된 항목을 다시 완료해도 같은 결과
    }
)
async def todo_complete(params: TodoCompleteInput) -> str:
    """특정 번호의 할일을 완료 처리한다.

    Args:
        params: 완료 처리할 할일 번호 (todo_id)

    Returns:
        완료 처리된 할일 내용 또는 오류 메시지
    """
    todos = _load_todos()

    # 해당 번호의 할일 찾기
    target = next((t for t in todos if t["id"] == params.todo_id), None)

    if target is None:
        return f"Error: 번호 {params.todo_id}인 할일을 찾을 수 없습니다. todo_list로 번호를 확인하세요."

    if target["done"]:
        return f"이미 완료된 할일입니다: [{target['id']}] {target['title']}"

    target["done"] = True
    _save_todos(todos)

    return f"✅ 완료 처리됨: [{target['id']}] {target['title']}"


if __name__ == "__main__":
    mcp.run()

5. 코드 구조 이해하기

서버 코드는 세 부분으로 나뉩니다.

① 유틸리티 함수 (_load_todos, _save_todos)

_로 시작하는 함수는 내부적으로만 쓰이는 함수입니다. Claude에게 노출되지 않고, 도구들이 내부적으로 호출합니다.

② 입력 모델 (TodoAddInput, TodoCompleteInput)

Claude가 도구를 호출할 때 어떤 값을 넘겨야 하는지 정의합니다. Field의 description은 Claude가 읽는 설명입니다. Claude는 이 설명을 보고 무엇을 채워야 하는지 판단합니다.

title: str = Field(
    ...,
    description="추가할 할일 내용 (예: '보고서 작성', '팀 미팅 준비')",
)
# "..." 은 "이 값은 반드시 있어야 한다"는 의미입니다.

③ 도구 함수 (todo_add, todo_list, todo_complete)

@mcp.tool() 데코레이터가 붙은 함수가 Claude에게 노출되는 실제 도구입니다. 함수의 docstring 첫 줄이 도구 설명으로 사용됩니다.


6. 로컬에서 테스트하기

코드를 작성했으면 Claude에 연결하기 전에 먼저 혼자 돌아가는지 확인합니다.

MCP Inspector 실행:

# todo-mcp/ 폴더 안에서 실행
npx @modelcontextprotocol/inspector python server.py

브라우저에서 http://localhost:5173이 자동으로 열립니다.

Inspector에서 확인할 것:

  1. 왼쪽 패널에서 Tools 탭 클릭
  2. todo_add, todo_list, todo_complete 세 도구가 목록에 보이는지 확인
  3. todo_add 클릭 → title에 "보고서 작성" 입력 → Run 클릭
  4. todo_list 클릭 → Run 클릭 → 방금 추가한 항목이 보이는지 확인

Inspector는 Claude 없이 도구를 직접 실행해볼 수 있는 테스트 환경입니다. Claude에 연결하기 전에 여기서 먼저 동작을 검증합니다.


7. Claude Code에 연결하기

테스트가 끝나면 Claude Code에 연결합니다. 프로젝트 루트에 CLAUDE.md를 만들고 MCP 서버 정보를 등록합니다.

# 프로젝트 루트에서 실행 (todo-mcp/ 의 상위 폴더)
touch CLAUDE.md
# CLAUDE.md

## MCP 서버

이 프로젝트에는 todo_mcp 서버가 연결되어 있습니다.
할일 추가, 조회, 완료 처리는 반드시 todo_mcp 도구를 사용하세요.
직접 todos.json 파일을 편집하지 마세요.

그리고 Claude Code의 MCP 설정 파일에 서버를 등록합니다. Claude Code를 실행한 뒤 아래 명령어를 입력하세요.

claude mcp add todo-mcp python /절대경로/todo-mcp/server.py

/절대경로/todo-mcp/server.py 부분은 실제 경로로 바꾸세요. 현재 위치를 모르면 pwd 명령어로 확인할 수 있습니다.

등록 확인:

claude mcp list
# todo-mcp 가 목록에 보이면 성공

8. 실제로 사용해보기

Claude Code를 실행합니다.

claude

아래 문장들을 순서대로 입력해보세요.

할일 추가:

보고서 작성 할일에 추가해줘

목록 확인:

할일 목록 보여줘

예상 결과:

# 할일 목록

⬜ [1] 보고서 작성

전체 1개 | 완료 0개 | 남은 것 1개

완료 처리:

1번 할일 완료 표시해줘

다시 목록 확인:

할일 목록 보여줘

예상 결과:

# 할일 목록

✅ [1] 보고서 작성

전체 1개 | 완료 1개 | 남은 것 0개

Claude Code 화면에서 Claude가 todo_add, todo_list 같은 도구를 호출했다는 표시가 나타납니다. 그리고 todo-mcp/todos.json 파일을 열어보면 실제 데이터가 저장된 것을 확인할 수 있습니다.


9. 완성된 파일 구조

프로젝트 루트/
├── CLAUDE.md                 ← MCP 사용 지침
└── todo-mcp/
    ├── server.py             ← MCP 서버 코드
    └── todos.json            ← 실제 데이터 저장 파일 (자동 생성됨)

10. 스킬과 MCP를 함께 쓸 때

스킬과 MCP는 경쟁 관계가 아니라 서로 다른 역할을 합니다.

사용자: "완료 못 한 할일들로 오늘 회의 안건 만들어줘"
         ↓
todo_mcp 도구 호출 → 미완료 할일 목록 가져옴 (실제 데이터)
         ↓
meeting-notes 스킬 활용 → 회의 안건 형식으로 정리 (텍스트 작업)

실제 데이터가 필요한 부분은 MCP, 그 데이터를 가공하고 형식을 만드는 부분은 스킬이 담당합니다.


11. 자주 하는 실수

도구가 Claude에게 안 보일 때 claude mcp list로 서버가 등록됐는지 확인하세요. 등록은 됐는데 안 보인다면 Claude Code를 재시작해보세요.

module not found: mcp 오류 pip install "mcp[cli]"를 실행한 Python 환경과 server.py를 실행하는 환경이 다른 경우입니다. which python으로 어떤 Python이 사용되는지 확인하세요.

todos.json이 엉뚱한 곳에 생길 때 TODO_FILE 경로를 Path(__file__).parent / "todos.json"으로 지정했기 때문에 server.py와 같은 폴더에 생깁니다. 다른 위치에 저장하고 싶다면 이 경로를 바꾸면 됩니다.

Error: 번호 X인 할일을 찾을 수 없습니다 가 계속 나올 때 todo_list로 현재 번호를 먼저 확인하세요. 번호는 삭제해도 재정렬되지 않습니다.

Inspector에서는 되는데 Claude에서는 안 될 때 CLAUDE.md에 해당 MCP 서버를 사용하라는 지침이 있는지 확인하세요. 지침이 없으면 Claude가 어떤 도구를 써야 할지 판단하지 못할 수 있습니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함