AI/Engineering

MCP 서버 만들기: Python SDK로 함수 세 개, 26줄 첫 MCP 서버 (직접 짜면 400줄인 이유까지)

agentnote 2026. 10. 20. 13:56

요약. MCP 서버는 요청 한 줄을 읽고, 형식이 맞는지 확인하고, 맞는 함수를 불러서, 답 한 줄을 쓰는 프로그램입니다. 규격을 손으로 다 짜면 약 400줄이 나옵니다. 공식 Python SDK를 쓰면 평범한 함수에 @mcp.tool() 표시 하나를 붙이는 것으로 끝나고, 같은 노트 서버가 26줄이 됩니다. 함수 이름과 설명, 인자 타입은 그대로 모델이 읽는 도구 설명이 됩니다.

5화에서 이어지는 질문: MCP 서버는 직접 만들 수 있을까

5화에서는 MCP를 쓰는 쪽에서 구조를 봤습니다. AI 앱(호스트) 안의 클라이언트가 서버 하나와 1대1로 붙고, 서버는 도구·리소스·프롬프트를 내놓습니다. 공식 파일 서버에 메시지를 보내 도구 14개를 받아 보기도 했습니다.

그럼 반대쪽은 어떨까요? 팀에서 매번 손으로 찾는 정보가 있다면, 그걸 MCP 서버로 만들어 AI 앱에 붙일 수 있을까요? 만든다면 코드가 얼마나 필요할까요?

이번 화는 같은 노트 서버를 두 번 만들어 보며 답을 찾습니다. 한 번은 원본 레슨처럼 표준 라이브러리만으로, 한 번은 공식 Python SDK로 만듭니다.

{경험: 사용자가 채움 — MCP 서버를 처음 만들어 보기 전에 짐작했던 것, 또는 회사에서 "이걸 서버로 만들면 좋겠다" 싶었던 일 한 가지}

MCP 서버가 하는 일: 읽고, 확인하고, 부르고, 쓴다

MCP 서버를 식당 주문 창구에 비유해 보겠습니다. 손님(앱)이 주문서(요청)를 창구에 넣으면, 직원(서버)이 양식이 맞는지 보고, 맞는 주방(함수)에 넘기고, 음식(결과)을 한 접시씩 내줍니다.

실제 순서도 같습니다. 원본 레슨 13-07 Building an MCP Server는 서버의 뼈대를 다섯 단계로 설명합니다.

그림 1. 요청 하나를 처리하는 순서. 확인 단계에서 떨어지는 갈래가 두 개 있습니다.

  1. 한 줄 읽기. 앱이 서버를 자기 컴퓨터에서 띄우면(stdio 방식), 요청은 표준 입력으로 JSON 한 줄씩 들어옵니다.
  2. 자기소개 확인. 5화에서 본 대로 MCP 최신 판(2026-07-28)은 처음 인사를 없앴습니다. 대신 요청마다 _meta에 "나는 이 판을 쓰고, 이런 기능이 있다"를 싣습니다. 서버는 이것부터 봅니다.
  3. 맞는 함수 부르기. 메서드 이름(tools/list, tools/call 등)을 보고 처리할 함수를 고릅니다.
  4. 결과 감싸기. 결과에 "완료" 표시와 서버 정보를 붙입니다.
  5. 한 줄 쓰기. 표준 출력에 JSON 한 줄로 답하고, 그 요청은 잊습니다.

2단계에서 자기소개가 빠지면 서버는 앞 요청의 정보로 채우지 않고 오류(-32602)로 돌려보냅니다. 모르는 판이면 -32022와 함께 자기가 아는 판 목록을 알려 줍니다. 서버는 앞 요청을 기억하지 않습니다. 그래서 같은 서버 프로세스가 여러 클라이언트를 번갈아 받아도, 앞사람의 설정이 뒷사람 요청에 새어 들어가지 않습니다.

직접 짜면 약 400줄: 원본 레슨의 노트 서버

레슨은 이 뼈대를 Python 표준 라이브러리만으로 구현합니다. 노트를 다루는 서버이고, 도구 3개(노트 만들기, 목록, 검색), 리소스(노트 내용), 프롬프트 1개(노트 검토 요청)를 내놓습니다. code/main.py는 409줄입니다. TypeScript 판(main.ts)도 같은 일을 합니다.

그림 2. 왼쪽은 서버가 직접 해야 하는 일, 오른쪽은 그 일을 SDK에 맡긴 코드입니다.

400줄 대부분은 "노트"와 상관없는 일입니다. 요청마다 자기소개를 검증하고, 메서드별로 나누고, 결과를 같은 모양으로 감싸고, 도구 목록을 늘 같은 순서로 내놓고, 상황에 맞는 오류 코드를 고릅니다. 레슨은 모든 성공 결과가 감싸기 함수 하나를 거치게 해서, 처리기 하나가 필드를 빠뜨리는 실수를 막습니다.

레슨 코드는 함께 들어 있는 테스트 10개를 모두 통과하고, 데모도 설명대로 돌아갑니다. 검색어를 비워서 부르면 "검색어가 비어 있다"는 결과가, JSON이 아닌 줄을 보내면 파싱 오류가 돌아옵니다.

직접 짜 보면 좋은 점이 있습니다. 규격이 서버에 무엇을 요구하는지 하나도 빠짐없이 보입니다. 하지만 노트 검색 하나를 붙이자고 매번 400줄을 쓸 수는 없습니다.

Python SDK로 MCP 서버 만들기: 함수에 표시 하나

같은 서버를 공식 Python SDK(PyPI mcp 2.3.0)로 다시 만들면 이렇게 됩니다. 도구를 1개로 줄였지만 리소스와 프롬프트는 그대로 있습니다.

from mcp.server import MCPServer

mcp = MCPServer("notes-sdk")
NOTES = {"note-1": "Stateless requests and JSON-RPC.", "note-2": "Provider envelopes differ."}


@mcp.tool()
def notes_search(query: str, limit: int = 10) -> list[str]:
    """Search note bodies by keyword."""
    return [k for k, v in sorted(NOTES.items()) if query.lower() in v.lower()][:limit]


@mcp.resource("notes://{note_id}")
def note(note_id: str) -> str:
    """Read one note."""
    return NOTES[note_id]


@mcp.prompt()
def review_note(note_id: str) -> str:
    """Ask for a review of one note."""
    return f"Review this note and propose improvements:\n\n{NOTES[note_id]}"


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

26줄입니다. 평범한 Python 함수 세 개에 각각 @mcp.tool(), @mcp.resource(), @mcp.prompt()를 붙였을 뿐입니다. 검증, 결과 감싸기, 오류 코드, 표준 입출력은 SDK가 맡습니다.

더 흥미로운 것은 도구 설명이 어디서 오느냐입니다. 이 서버에 "무엇을 할 수 있나요?"(tools/list)를 물으면, 함수를 그대로 옮긴 도구 설명이 돌아옵니다.

그림 3. 실제 `tools/list` 응답을 줄였습니다. 기본값이 없는 `query`만 필수가 됩니다.

함수 이름은 도구 이름이 되고, docstring은 도구 설명이 됩니다. 인자 타입(query: str, limit: int = 10)은 입력 스키마가 되는데, 기본값이 없는 query만 필수로 표시됩니다. 반환 타입 list[str]은 출력 스키마가 됩니다.

2화에서 본 것처럼 모델은 이 도구 설명만 보고 언제 무엇을 부를지 정합니다. 그러니 MCP 서버를 만들 때 가장 공들일 곳은 코드 줄 수가 아닙니다. 함수 이름과 docstring을 모델이 알아듣게 쓰는 일입니다.

직접 만든 서버, 어떤 클라이언트와 통할까

여기서 5화의 "바뀌는 중"이 다시 나옵니다. MCP에는 지금 두 판이 함께 쓰입니다. 처음에 인사(initialize)를 나누는 2025-11-25 판과, 인사 없이 요청마다 자기소개를 싣는 2026-07-28 판입니다.

두 서버에 클라이언트 다섯 가지로 접속해 본 결과입니다(2026-10-19 기준).

그림 4. ○는 도구 목록과 호출까지 성공, ×는 연결 실패입니다.

레슨 서버는 일부러 최신 판만 받도록 만들었습니다. 레슨도 이 점을 밝힙니다. 그래서 최신 판으로 말하는 Python SDK 클라이언트와는 잘 통합니다. 하지만 이전 판으로 말하는 TypeScript SDK 1.32.1 클라이언트와, 기본 설정으로 띄운 MCP Inspector(서버 점검 도구)는 "_meta가 필요하다"는 오류를 받고 연결하지 못했습니다.

SDK 서버는 다섯 가지 모두와 통했습니다. 최신 판으로 물으면 최신 판으로, 이전 판으로 인사하면 이전 판으로 답합니다. 레슨이 연습문제로 남겨 둔 "두 판 모두 받기"를 SDK가 기본으로 해 줍니다.

그래서 이번 화의 결론은 이렇습니다. 규격을 이해하려면 레슨처럼 직접 짜 보는 것이 좋습니다. 실제로 앱에 붙일 서버는 SDK로 만드는 편이 안전합니다. 명세는 앞서가고 도구들은 아직 따라가는 중이기 때문입니다.

MCP 서버 오류는 두 종류: 모델이 고칠 수 있게

서버를 만들면 오류를 어떻게 돌려줄지도 정해야 합니다. MCP 명세 2026-07-28 Tools 절은 오류를 두 종류로 나눕니다.

그림 5. 왼쪽은 요청 자체가 틀린 경우, 오른쪽은 요청은 맞는데 실행이 실패한 경우입니다.

  • 프로토콜 오류: 요청 자체가 잘못된 경우입니다. 자기소개가 없거나, 모르는 판이거나, JSON이 아닌 경우입니다. JSON-RPC의 error로 돌려줍니다.
  • 도구 실행 오류: 요청 모양은 맞는데 도구가 돌다 실패한 경우입니다. 검색어가 비었거나, 바깥 API가 실패한 경우입니다. 정상 결과에 isError: true를 달아 돌려줍니다.

차이는 누가 그 오류를 보느냐입니다. 명세는 도구 실행 오류를 앱이 모델에게 보여 줘야 한다고(SHOULD) 적습니다. 모델이 "검색어가 비어 있다"는 문장을 읽고 고쳐서 다시 부를 수 있게 하려는 것입니다. 그러니 도구 안에서 실패했을 때는 "잘못된 요청"보다 "검색어를 한 글자 이상 넣어 주세요"처럼 모델이 읽고 고칠 수 있는 문장으로 돌려주는 것이 좋습니다.

SDK 서버에 필수 인자 query를 빼고 부르면, SDK가 알아서 검증 오류 문구를 isError: true로 돌려줍니다. 타입 힌트가 스키마이자 검증 규칙이 되는 셈입니다.

실행하다 막힌 곳 두 가지

printf로 한꺼번에 보내면 답이 일부만 온다. 5화처럼 printf로 요청 세 줄을 파이프로 보내면, SDK 서버는 세 번 중 응답을 1개, 2개, 1개만 돌려줬습니다. 입력이 끝나면(stdin EOF) 처리 중인 요청을 기다리지 않고 종료하기 때문입니다. 명세 stdio 절이 "stdin이 닫히면 곧 종료해야 한다"고 하니 잘못된 동작은 아닙니다. 한 줄씩 동기로 처리하는 레슨 서버는 같은 방법으로 보내도 전부 답했습니다. SDK 서버를 확인할 때는 아래 "따라 해 보기"의 Inspector를 쓰면 됩니다.

도구 안에서 print()를 쓰면. stdio 서버는 표준 출력에 MCP 메시지만 써야 합니다. 로그는 표준 오류(stderr)로 보냅니다. 공식 튜토리얼 "Build an MCP server"는 stdout에 쓰면 "JSON-RPC 메시지가 망가지고 서버가 깨진다"고 경고합니다. 검색 도구 안에 print() 한 줄을 넣고 Python SDK 클라이언트로 불러 보면, 클라이언트 쪽에 파싱 오류가 찍혔습니다. 이번에는 그 줄만 버려지고 호출 결과는 정상으로 왔지만, 클라이언트마다 처리가 다를 수 있습니다. 로그는 Python 표준 logging(기본 출력이 stderr)으로 남기는 습관을 들이는 게 좋습니다.

따라 해 보기

준비물은 uv(Python 패키지 실행 도구)와 Node.js입니다. API 키는 필요 없습니다.

1. 서버 파일 만들기. 위의 26줄 코드를 notes_server.py로 저장합니다.

2. Inspector로 도구 목록 보기. MCP Inspector는 공식 서버 점검 도구입니다. --cli를 주면 화면 없이 명령줄에서 요청을 보내고 결과를 보여 줍니다.

# 최신 판(2026-07-28)으로 도구 목록 묻기
npx -y @modelcontextprotocol/inspector --cli uv run --with mcp python notes_server.py --protocol-era modern --method tools/list

# 기본값(이전 판)으로 도구 부르기
npx -y @modelcontextprotocol/inspector --cli uv run --with mcp python notes_server.py --method tools/call --tool-name notes_search --tool-arg query=json

첫 명령에서 그림 3의 도구 설명이, 둘째 명령에서 note-1이 나오면 성공입니다. 두 판 모두 답한다는 것도 함께 확인됩니다.

3. (선택) Claude Code에 붙이기. Claude Code 공식 문서 "Connect Claude Code to tools via MCP"에 따르면 아래처럼 등록합니다. -- 앞은 Claude Code 옵션, 뒤는 서버를 띄우는 명령입니다. 기본 범위는 "이 프로젝트, 나만"입니다. 이 글의 실행 기록에는 이 단계가 없으니, 해 보신 분은 결과를 알려 주세요.

claude mcp add notes -- uv run --with mcp python /절대/경로/notes_server.py
claude mcp list

이런 데 쓸 수 있다

  • 자주 묻는 조회 하나를 읽기 전용 도구로. 팀이 매번 손으로 찾는 정보(문서 검색, 상태 조회)를 함수 하나로 만들고 @mcp.tool()을 붙입니다. 그러면 채팅 도구든 코드 편집기든 MCP를 아는 앱 어디서나 부를 수 있습니다. 처음에는 도구 하나로 시작합니다.
  • 쓰기 도구의 권한은 서버 안에서 막기. 지우기·바꾸기 도구를 만든다면, 도구에 붙이는 "위험한 도구" 표시(destructiveHint)에 기대지 않습니다. 명세는 이 표시를 클라이언트가 그대로 믿지 말라고 합니다. 권한 확인은 서버 함수 안에서 직접 합니다.
  • 오류 문구를 모델이 읽을 문장으로. 입력이 틀렸을 때 "날짜는 YYYY-MM-DD 형식으로, 오늘 이후만"처럼 돌려주면 모델이 스스로 고쳐 다시 부릅니다.

자주 묻는 질문

Q. MCP 서버를 만들려면 꼭 SDK를 써야 하나요? 아닙니다. 원본 레슨처럼 표준 라이브러리만으로도 만들 수 있고, 규격을 이해하는 데는 그 편이 좋습니다. 다만 2026년 10월 기준으로 앱과 도구가 쓰는 판이 섞여 있어서, 두 판을 모두 받는 공식 SDK로 만드는 편이 연결 문제가 적습니다.

Q. 도구, 리소스, 프롬프트를 다 만들어야 하나요? 아닙니다. Anthropic의 Mahesh Murag는 MCP 워크숍에서 "처음에는 도구로 시작해 서버를 이해하고, 그다음 프롬프트와 리소스로 가라"고 조언합니다. 레슨의 서버 설계 도우미도 "쓸 데 없는 리소스·프롬프트를 지어내지 말라"는 규칙을 둡니다.

Q. 만든 서버가 연결되는지 어떻게 확인하나요? MCP Inspector를 씁니다. 위 "따라 해 보기"처럼 --cli로 도구 목록과 호출을 확인합니다. 공식 튜토리얼에 따르면 Inspector는 기본값이 이전 판이므로, --protocol-era modern으로 최신 판도 한 번 더 확인하는 것이 좋습니다.

더 깊이: MCP 명세, 레슨, 공식 튜토리얼, 워크숍, 코드

MCP 명세 2026-07-28, Tools. Tools 절에서 서버를 만들 때 직접 걸리는 규칙만 추렸습니다. 도구는 모델이 고르는(model-controlled) 기능이고, 안전을 위해 호출을 거부할 수 있는 사람이 늘 있어야 합니다(SHOULD). tools/list는 정해진 순서로 돌려줘야 합니다(SHOULD). 목록이 흔들리면 캐시와 프롬프트 캐시 적중률이 떨어지기 때문입니다. 도구 이름은 1~128자, 영문·숫자·_·-·.만 씁니다(SHOULD). readOnlyHint, destructiveHint 같은 annotations는 신뢰할 수 있는 서버가 아니면 클라이언트가 믿지 않아야 합니다(MUST). outputSchema를 주면 structuredContent가 그 스키마를 따라야 하고(MUST), 하위 호환을 위해 같은 JSON을 text로도 넣는 것이 좋습니다(SHOULD). SDK 서버의 실제 응답도 content에 "note-1"을, structuredContent에 {"result": ["note-1"]}를 함께 담았습니다. 비규범 절 "Stateful Tools"는 프로토콜 세션이 없으니, 상태가 필요하면 만들기 도구가 핸들(예: basket_id)을 돌려주고 다음 호출의 인자로 받으라고 안내합니다.

명세와 구현이 다른 곳: 없는 도구 이름. 명세의 "Error Handling"은 알 수 없는 도구를 프로토콜 오류(-32602)의 예시로 듭니다. 그런데 레슨 서버와 SDK 2.3.0 서버는 둘 다 없는 도구 notes_delete를 부르면 도구 실행 오류(isError: true, "Unknown tool: notes_delete")로 돌려줍니다. 레슨은 테스트 이름도 test_unknown_tool_is_tool_level_error입니다. 실행 오류는 모델에게 보여 주는 쪽(SHOULD)이라, 모델이 "그런 도구가 없다"를 읽고 고칠 기회가 생기기는 합니다. 어느 쪽이 맞는지는 이 글에서 판단하지 않습니다.

명세 2026-07-28, stdio와 하위 호환. stdio 절에 따르면 클라이언트가 서버를 하위 프로세스로 띄우고, 메시지는 줄바꿈으로 구분하며 메시지 안에 줄바꿈을 넣으면 안 됩니다(MUST NOT). 서버는 stdout에 MCP 메시지가 아닌 것을 쓰면 안 되고(MUST NOT), 로그는 stderr에 써도 됩니다(MAY). 종료는 클라이언트가 stdin을 닫고, 기다리고, 안 끝나면 SIGTERM, SIGKILL 순서입니다. 두 판을 다 아는 클라이언트는 먼저 server/discover로 탐침합니다. 최신 판 응답이 오면 최신 판으로, 그 밖의 오류나 무응답이면 이전 판의 initialize로 내려갑니다. Python SDK Client의 mode="auto"가 이 방식이고, 그래서 그림 4에서 레슨 서버와 SDK 서버 모두와 통했습니다.

원본 레슨 13-07. 레슨의 모토는 "요즘 MCP 서버는 핸드셰이크를 기억하지 않는다"입니다. 첫 메시지에서 받은 클라이언트 능력을 저장해 두는 서버는 만들기는 쉽지만, 같은 프로세스가 여러 클라이언트를 받거나 원격 요청이 다른 워커로 가면 남은 상태가 엉뚱한 요청에 새어 들어갑니다. 노트 같은 앱 데이터는 남겨도 되지만, 다음 요청의 해석을 바꾸는 숨은 프로토콜 상태는 두지 말라고 합니다. 리소스 목록과 읽기 결과에는 캐시 힌트(ttlMs, cacheScope)를 붙이고, 사용자별 데이터는 private로 둡니다. 레슨 서버의 실제 값은 tools/list 60000/public, resources/read 5000/private였습니다. 서버가 사용자 입력이 필요하면 스스로 요청을 보내지 않고 "입력 필요" 결과를 돌려줍니다. 연습문제는 다섯 개입니다. 그중 "파괴적 도구 notes_delete에 실제 권한 검사 넣기"와 "2025-11-25 이전 판 어댑터를 따로 만들기"는 이번 화 결론과 바로 이어집니다. outputs/skill-mcp-server-scaffolder.md는 도메인을 주면 최신 판 서버 설계를 한 쪽짜리로 내 주는 스킬입니다.

공식 튜토리얼 Build an MCP server. 도구 2개짜리 날씨 서버를 만들어 Claude 데스크톱 앱에 붙입니다. Python은 uv add "mcp[cli]" 뒤 MCPServer를 쓰고, "타입 힌트와 docstring으로 도구 정의를 자동으로 만든다"고 설명합니다. Claude 데스크톱 앱 연결은 설정 파일(claude_desktop_config.json)의 mcpServers에 uv 실행 명령과 절대 경로를 넣고 앱을 다시 시작하는 방식입니다. TypeScript 탭은 새 패키지 @modelcontextprotocol/server의 serveStdio를 쓰는데, 튜토리얼에 따르면 클라이언트마다 판을 협상해서 두 판 모두에 답합니다. 이 TypeScript 경로는 이번에 직접 돌려 보지 않았습니다.

Mahesh Murag(Anthropic), "Building Agents with Model Context Protocol" 워크숍. 5화에서 본 영상의 뒷부분입니다. 2025년 초 녹화라 연결 방식은 이전 판 기준입니다. 22:48에서 Claude 데스크톱 앱에 GitHub 서버와 Asana 서버를 붙여, 이슈를 가져와 Asana 작업으로 넣는 데모를 합니다. 24:56에서 발표자는 두 서버 모두 커뮤니티가 만들었고 각각 수백 줄 정도이며, 주로 도구를 드러내는 일이라 로직이 많지 않다고 말합니다. 48:13에서는 출시 때 서버 15개 남짓을 각각 45분쯤에 Claude와 함께 썼다고 말하고, 도구부터 시작하라고 조언합니다. 측정값이 아니라 발표자의 말입니다. 69:51에서는 디버깅에 Inspector를 쓰라고 권합니다.

코드. modelcontextprotocol/python-sdk(MIT, PyPI mcp 2.3.0)는 MCPServer와 데코레이터 세 개, mcp dev server.py로 Inspector를 여는 명령을 제공합니다. 같은 패키지의 Client는 mode를 'auto'와 'legacy' 중에서 고릅니다. modelcontextprotocol/inspector(npm 2.11.0)는 --cli 모드와 --protocol-era modern|legacy를 지원하고, 라이선스는 MIT에서 Apache-2.0으로 옮겨 가는 중입니다. TypeScript SDK @modelcontextprotocol/sdk 1.32.1은 2025-11-25를 최신 판으로 알고 있어서, 그림 4에서 레슨 서버와 통하지 않았습니다.

다음 화 예고. 5화와 6화에서 MCP 서버를 쓰고 만들어 봤습니다. 7화에서는 에이전트 스킬을 다룹니다. 도구를 붙이는 것과 일하는 방법을 가르치는 것은 어떻게 다를까요?


원본: rohitg00/ai-engineering-from-scratch (MIT, © 2026 Rohit Ghumare). 이 글은 원문을 바탕으로 직접 정리하고 재구성했습니다.

출처