MCP 서버 만들기 — Python + TypeScript SDK

대부분의 MCP 튜토리얼(tutorial)은 stdio 기반의 hello-world 예제만 보여줍니다. 하지만 실제 서버(server)는 도구(Tools)뿐 아니라 리소스(Resources)와 프롬프트(Prompts)를 함께 노출하고, 능력 협상(capability negotiation)을 처리하며, 구조화된 오류(structured error)를 방출하고, SDK가 달라도 같은 방식으로 동작해야 합니다. 이 강의(lesson)에서는 메모 서버(notes server)를 처음부터 끝까지 구현합니다. 표준 라이브러리(stdlib) 기반 stdio 전송, JSON-RPC 디스패치(dispatch), 세 가지 서버 프리미티브(primitive), 그리고 이후 Python SDK의 FastMCP나 TypeScript SDK로 옮겨도 그대로 들어가는 순수 함수(pure-function) 스타일을 다룹니다.

유형: Build 언어: Python (표준 라이브러리, stdio MCP 서버) 선수 지식: Phase 13 · 06 (MCP 기초) 예상 시간: 약 75분

학습 목표

  • initialize, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get 메서드(method)를 구현합니다.
  • 표준 입력(stdin)에서 JSON-RPC 메시지를 읽고 표준 출력(stdout)으로 응답을 쓰는 디스패치 루프(dispatch loop)를 작성합니다.
  • JSON-RPC 2.0 명세(spec)와 MCP의 추가 오류 코드(code)에 맞춰 구조화된 오류 응답을 방출합니다.
  • 도구 로직을 다시 작성하지 않고도 표준 라이브러리 구현을 FastMCP(Python SDK) 또는 TypeScript SDK로 옮길 수 있는 방법을 이해합니다.

문제

원격 전송(remote transport, Phase 13 · 09)이나 인증 계층(auth layer, Phase 13 · 16)을 적용하기 전에 먼저 깔끔한 로컬 서버(local server)가 필요합니다. 여기서 "로컬"이란 stdio를 의미합니다. 클라이언트(client)가 서버를 자식 프로세스(child process)로 실행하고, 메시지는 표준 입력과 표준 출력 위에서 줄바꿈으로 구분된(newline-delimited) 형태로 흐릅니다.

2025-11-25 명세는 stdio 메시지가 명시적인 \n 구분자(separator)를 가진 JSON 객체로 인코딩되도록 규정합니다. 여기에는 SSE가 없습니다. SSE는 과거의 원격 모드였고 2026년 중반에 제거되는 중입니다(Atlassian의 Rovo MCP 서버는 2026년 6월 30일에, Keboola는 2026년 4월 1일에 SSE를 폐기 예고(deprecate)했습니다). stdio에서는 한 줄에 JSON 객체 하나가 전송 형식(wire format)의 전부입니다.

메모 서버는 세 가지 서버 프리미티브를 모두 연습하기에 적합한 형태입니다. 도구(Tools)는 변경(mutation)을 수행합니다(notes_create). 리소스(Resources)는 데이터를 노출합니다(notes://{id}). 프롬프트(Prompts)는 템플릿(template)을 제공합니다(review_note). 이 강의의 구조는 어떤 도메인(domain)에도 일반화할 수 있습니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.MCP 서버 구축가 해결하는 핵심 과제는?

2.MCP 서버 구축 이전의 주요 한계는?

0/2 답변 완료

개념

디스패치 루프(Dispatch loop)

loop:
  line = stdin.readline()
  msg = json.loads(line)
  if has id:
    handle request -> write response
  else:
    handle notification -> no response

세 가지 규칙이 있습니다.

  • JSON-RPC 봉투(envelope)가 아닌 것은 표준 출력에 출력하지 않습니다. 디버그 로그(debug log)는 표준 오류(stderr)로 보냅니다.
  • 모든 요청(request)은 같은 id를 가진 응답(response)과 반드시 짝지어져야 합니다.
  • 알림(notification)에는 절대로 응답하지 않습니다.

initialize 구현

def initialize(params):
    return {
        "protocolVersion": "2025-11-25",
        "capabilities": {
            "tools": {"listChanged": True},
            "resources": {"listChanged": True, "subscribe": False},
            "prompts": {"listChanged": False},
        },
        "serverInfo": {"name": "notes", "version": "1.0.0"},
    }

지원하는 기능만 선언합니다. 클라이언트는 능력(capability) 집합에 의존해 기능을 분기 처리(gate)합니다.

tools/listtools/call 구현

tools/list{tools: [...]}를 반환하며, 각 항목에는 name, description, inputSchema가 있습니다. tools/call{name, arguments}를 받아 {content: [blocks], isError: bool}을 반환합니다.

콘텐츠 블록(content block)은 타입이 지정된(typed) 형태입니다. 가장 흔한 형태는 다음과 같습니다.

{"type": "text", "text": "Found 2 notes"}
{"type": "resource", "resource": {"uri": "notes://14", "text": "..."}}
{"type": "image", "data": "<base64>", "mimeType": "image/png"}

도구 오류(tool error)는 두 가지 형태로 나타납니다. 프로토콜 수준 오류(protocol-level error), 예컨대 알 수 없는 메서드(unknown method)나 잘못된 파라미터(bad params)는 JSON-RPC 오류로 처리합니다. 도구 수준 오류(tool-level error), 즉 호출 자체는 유효하지만 도구 실행이 실패한 경우에는 {content: [...], isError: true} 형태로 반환합니다. 이렇게 하면 모델이 자신의 문맥(context) 안에서 실패 사실을 볼 수 있습니다.

리소스(Resources) 구현

리소스는 설계상 읽기 전용(read-only)입니다. resources/list는 매니페스트(manifest)를 반환하고, resources/read는 실제 내용을 반환합니다. URI는 file://..., http://..., 또는 notes://와 같은 사용자 정의 스킴(custom scheme)이 될 수 있습니다.

데이터를 도구가 아니라 리소스로 노출하면 다음과 같은 점이 달라집니다.

  • 모델이 직접 "호출(call)"하지 않습니다. 클라이언트가 사용자 요청에 따라 문맥에 주입할 수 있습니다.
  • 구독(subscription)을 사용하면 리소스가 바뀔 때 서버가 변경 사항을 푸시(push)할 수 있습니다(Phase 13 · 10).
  • Phase 13 · 14에서는 상호작용 가능한 리소스(interactive resource)를 위한 ui:// 스킴으로 이 개념을 확장합니다.

프롬프트(Prompts) 구현

프롬프트는 이름 있는 인자(named argument)를 가진 템플릿입니다. 호스트(host)는 이를 슬래시 명령(slash-command)으로 표면화합니다. review_note 프롬프트는 note_id 인자를 받아 클라이언트가 자신의 모델에 전달할 수 있는 여러 메시지(multi-message) 형태의 프롬프트 템플릿을 생성할 수 있습니다.

stdio 전송의 세부 사항

  • 줄바꿈으로 구분된 JSON입니다. 길이 접두사 프레이밍(length-prefixed framing)이 아닙니다.
  • 버퍼링하지 않습니다. 매 출력(write) 뒤에 sys.stdout.flush()를 호출합니다.
  • 수명(lifetime)은 클라이언트가 제어합니다. 표준 입력이 닫혀 EOF가 되면 깔끔하게 종료합니다.
  • SIGPIPE를 조용히 무시하지 않습니다. 로그를 남기고 종료합니다.

어노테이션(Annotations)

각 도구는 안전 속성을 설명하는 annotations를 가질 수 있습니다.

  • readOnlyHint: true: 순수 읽기 동작이며 재시도(retry)해도 안전합니다.
  • destructiveHint: true: 되돌릴 수 없는 부수 효과(side effect)가 있으므로 클라이언트가 확인 절차를 거쳐야 합니다.
  • idempotentHint: true: 동일한 입력에 대해 동일한 출력을 만듭니다(멱등성, idempotent).
  • openWorldHint: true: 외부 시스템(external system)과 상호작용합니다.

클라이언트는 이 정보를 사용해 사용자 경험(UX), 예를 들어 확인 다이얼로그(confirmation dialog)나 상태 표시자(status indicator)를 결정하고 라우팅(routing)을 판단합니다(Phase 13 · 17).

졸업 경로(Graduation path)

code/main.py의 표준 라이브러리 기반 서버는 약 180줄입니다. FastMCP(Python)는 동일한 로직을 데코레이터(decorator) 스타일로 압축합니다.

from fastmcp import FastMCP
app = FastMCP("notes")

@app.tool()
def notes_search(query: str, limit: int = 10) -> list[dict]:
    ...

TypeScript SDK도 동등한 형태를 가집니다. 준비가 되면 코드를 거의 그대로 옮겨 넣을(drop-in) 수 있습니다. 개념, 즉 능력(capabilities), 디스패치, 콘텐츠 블록은 동일합니다.

사용해보기

code/main.py는 stdio 위에서 동작하는 완전한 메모(notes) MCP 서버이며, 표준 라이브러리만 사용합니다. initialize, 세 가지 도구(notes_list, notes_search, notes_create)에 대한 tools/listtools/call, 각 메모별 resources/listresources/read, 그리고 review_note 프롬프트를 처리합니다. JSON-RPC 메시지를 파이프(pipe)로 넣어 직접 구동할 수 있습니다.

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python main.py

살펴볼 지점은 다음과 같습니다.

  • 디스패처(dispatcher)는 메서드 이름을 키로 하는 dict[str, Callable]입니다.
  • 모든 도구 실행자(executor)는 단순 문자열이 아닌 콘텐츠 블록 목록(list)을 반환합니다.
  • 실행자가 예외를 발생시키면 isError: true가 설정됩니다.

산출물 만들기

이 강의는 outputs/skill-mcp-server-scaffolder.md를 만듭니다. 도메인(메모, 티켓, 파일, 데이터베이스 등)이 주어지면 이 스킬(skill)은 적절한 도구/리소스/프롬프트 분리와 SDK 졸업 경로를 갖춘 MCP 서버를 스캐폴딩(scaffold)합니다.

연습문제

  1. (쉬움) code/main.py를 실행하고 손으로 만든 JSON-RPC 메시지로 구동해 봅니다. notes_create로 새 메모를 만든 뒤 resources/read로 새 메모를 읽어 옵니다.

  2. (중간) annotations: {destructiveHint: true}가 설정된 notes_delete 도구를 추가합니다. 클라이언트가 확인 다이얼로그를 표시할 수 있는지 검증합니다(이 검증에는 실제 호스트가 필요하며, Claude Desktop을 사용할 수 있습니다).

  3. (중간) 메모가 수정될 때마다 서버가 notifications/resources/updated를 푸시하도록 resources/subscribe를 구현합니다. 킵얼라이브(keepalive) 작업도 함께 추가합니다.

  4. (어려움) 서버를 FastMCP로 이식(port)합니다. Python 파일은 80줄 미만으로 줄어야 합니다. 전송 동작(wire behavior)은 동일해야 하며, 같은 JSON-RPC 테스트 하니스(test harness)로 검증합니다.

  5. (어려움) 명세의 server/tools 섹션을 읽고, 이 강의의 서버가 구현하지 않은 도구 정의(tool definition) 필드를 하나 찾습니다(힌트: 여러 개가 있습니다). 그중 하나를 골라 추가합니다.

핵심 용어

용어흔한 설명실제 의미
MCP 서버(MCP server)"도구를 노출하는 것"stdio 또는 HTTP로 MCP JSON-RPC를 말하는 프로세스
stdio 전송(stdio transport)"자식 프로세스 모델"클라이언트가 서버를 실행하고 stdin/stdout으로 통신하는 방식
디스패처(Dispatcher)"메서드 라우터"JSON-RPC 메서드 이름을 핸들러 함수로 매핑한 구조
콘텐츠 블록(Content block)"도구 결과 조각"도구 응답의 content 배열 안에 들어가는 타입 지정 요소
isError"도구 수준 실패"도구가 실패했음을 알리며 JSON-RPC 오류와 구분된다
어노테이션(Annotations)"안전 힌트"readOnly / destructive / idempotent / openWorld 플래그
FastMCP"Python SDK"MCP 프로토콜 위에 얹은 데코레이터 기반 고수준 프레임워크
리소스 URI(Resource URI)"주소 지정 가능한 데이터"리소스를 식별하는 file://, db://, 사용자 정의 스킴
프롬프트 템플릿(Prompt template)"슬래시 명령 브리프"호스트 UI를 위한 인자 슬롯을 가진 서버 제공 템플릿
능력 선언(Capability declaration)"기능 토글"initialize에서 선언하는 프리미티브별 플래그

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

mcp-server-scaffolder

Scaffold a domain-specific MCP server with the right tools/resources/prompts split and SDK graduation path.

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.프로덕션에서 MCP 서버 구축의 가장 중요한 설계 원칙은?

2.MCP 서버 구축가 올바른 선택이 아닌 경우는?

3.MCP 서버 구축는 AI 생태계에 어떻게 들어맞나요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다