Python으로 MCP 서버 구축하기 — 아키텍처, FastMCP, 그리고 프로덕션 코드
요약
Anthropic이 도입한 Model Context Protocol(MCP)을 활용하여 Python 기반의 프로덕션급 MCP 서버를 구축하는 방법을 다룹니다. MCP의 아키텍처, JSON-RPC 2.0 기반의 작동 원리, 그리고 도구·리소스·프롬프트라는 세 가지 핵심 요소를 상세히 설명합니다.
핵심 포인트
- MCP는 M×N의 통합 문제를 M+N으로 줄여주는 개방형 표준 프로토콜임
- 단순한 함수 호출(Function Calling)이 아닌 전송 프로토콜이자 협상 계층임
- JSON-RPC 2.0을 기반으로 호스트, 클라이언트, 서버 역할을 정의함
- 도구(Tools), 리소스(Resources), 프롬프트(Prompts) 세 가지 기본 요소를 통해 기능을 노출함
저는 제 블로그인 devmindset.dev를 커스텀 MCP 서버를 통해 운영합니다. 포스트 게시, SEO 메타데이터 업데이트, 카테고리 할당 — 이 모든 것이 1년 전에는 프로덕션 형태로 존재하지 않았던 프로토콜을 통해 이루어집니다. 따라서 저는 문서를 바탕으로 MCP에 대해 쓰는 것이 아니라, 실제로 작동하는 서버를 구축하고 매일 운영하는 사람의 관점에서 글을 쓰고 있습니다. 이것은 흔한 "Hello World" 수준의 글이 아닙니다. 프로토콜 아키텍처 (protocol architecture), 의도적인 설계 결정, 그리고 Python으로 작성된 프로덕션 코드 (production code)에 관한 이야기입니다.
Model Context Protocol (MCP)는 2024년 11월 Anthropic에 의해 도입된 개방형 표준이며, 현재 Agentic AI Foundation (Linux Foundation) 산하에서 개발되고 있습니다. 안정적인 사양(stable spec)은 2025-11-25 기준이며, 출시 이후 가장 큰 개정은 2026년 7월 28일에 이루어졌습니다 — 이에 대해서는 곧 자세히 다루겠습니다. 왜냐하면 이 개정이 전송(transport) 설계 방식을 바꾸기 때문입니다. 하지만 대부분의 튜토리얼이 건너뛰는 질문부터 시작해 봅시다: 이 프로토콜이 실제로 해결하는 문제는 무엇인가?
MCP가 실제로 해결하는 문제
MCP가 해결하는 문제는 조합적(combinatorial)입니다. 여러분에게는 _M_개의 LLM 애플리케이션 (Claude Desktop, Cursor, VS Code, ChatGPT)과 _N_개의 외부 시스템 (데이터베이스, GitHub, 내부 API, WordPress)이 있습니다. 공유된 표준이 없다면, 모든 쌍마다 맞춤형 통합이 필요합니다. 즉, 각각 고유한 형식, 고유한 인증, 고유한 유지보수 부담을 가진 M×N개의 구현이 필요하다는 뜻입니다. MCP는 이를 M+N으로 축소합니다. 서버를 한 번만 작성하면, 규격을 준수하는 모든 클라이언트는 클라이언트 측에서 코드 한 줄 작성하지 않고도 서버를 발견하고 사용할 수 있습니다.
기계적으로 MCP는 JSON-RPC 2.0을 기반으로 하며 세 가지 역할을 정의합니다. **호스트 (host)**는 모든 것을 조정하는 LLM 애플리케이션입니다. **클라이언트 (client)**는 호스트에 의해 인스턴스화되며, 서버당 하나의 클라이언트가 생성됩니다. **서버 (server)**는 컨텍스트 (context)와 기능 (capabilities)을 제공합니다. 이는 의도적으로 Language Server Protocol (LSP)을 모델로 삼았습니다. LSP가 에디터 전반에 걸쳐 언어 지원을 표준화했듯이, MCP는 도구와 데이터를 AI 생태계로 연결하는 방식을 표준화합니다.
그리고 여기서 바로잡아야 할 첫 번째 오해는 다음과 같습니다: MCP는 "함수 호출 (function calling)"이 아닙니다. 함수 호출은 단일 벤더(single-vendor) 메커니즘입니다. 즉, 코드 내에 함수를 정의하면 특정 모델 하나가 이를 호출하는 방식입니다. 반면 MCP는 전송 프로토콜 (transport protocol)이자 협상 계층 (negotiation layer)입니다. 서버는 자신의 기능을 광고하고, 클라이언트는 런타임 (runtime)에 이를 발견하며, 초기화 단계에서 버전 협상이 이루어집니다. 함수 호출은 하나의 애플리케이션 내부에 머물지만, MCP 서버는 어떤 호스트 (host)에서도 재사용이 가능합니다.
세 가지 기본 요소: 도구 (tools), 리소스 (resources), 그리고 프롬프트 (prompts)
MCP 서버는 세 가지 기본 요소 (primitives)를 통해 기능을 노출합니다. 이들을 혼동하는 것이 가장 흔한 설계 실수입니다. 각 요소는 서로 다른 계약 (contract)과 용도를 가집니다.
| 기본 요소 | 정의 | 제어 주체 | 용도 |
|---|---|---|---|
| 도구 (Tool) | 검증 및 로직을 포함한 실행 가능한 동작 | 모델 (필요할 때 호출) | 부수 효과 (side-effecting)가 있는 작업, 복잡한 로직 |
| ... |
경험 법칙: 입력 검증과 비즈니스 로직이 필요한 경우(예: "제목 X와 상태 Y로 게시물 생성")에는 **도구 (Tool)**를 사용하세요. 단순한 파라미터 하에 데이터를 노출하는 경우(예: "문서 Z의 내용")에는 **리소스 (Resource)**를 사용하세요. 사용자에게 준비된 매개변수화된 시나리오를 전달할 때는 **프롬프트 (Prompt)**를 사용하세요. 실제로 대부분의 서버는 도구 (tools)로 시작해서 도구로 끝납니다. 나머지는 컨텍스트 최적화 (context optimization)의 영역입니다.
전송 (Transport): stdio vs Streamable HTTP
MCP는 두 가지 전송 (transport) 방식을 정의하며, 이 둘 중 하나를 선택하는 것이 MCP 서버를 구축할 때 내리는 첫 번째 아키텍처 결정입니다.
| 차원 (Dimension) | stdio | Streamable HTTP |
|---|---|---|
| 위치 (Location) | 로컬 (Local), 동일 머신 | 원격 (Remote), HTTPS를 통해 |
| ... |
그리고 아직 대부분의 자료가 따라잡지 못한 중요한 변화가 있습니다. 2026-07-28 개정판(현재 출시 후보 버전)에서는 프로토콜 수준의 세션(session)이 제거되었습니다. 즉, Mcp-Session-Id 헤더가 사라졌습니다 (SEP-2567). 이제 프로토콜 버전, 클라이언트 정보 및 기능(capabilities)은 모든 요청의 _meta 필드를 통해 전달되며, 새로운 server/discover 메서드를 통해 클라이언트가 필요할 때 서버의 기능(capabilities)을 가져올 수 있습니다. 실질적인 결과는 다음과 같습니다: 어떠한 요청이라도 어떤 서버 인스턴스로든 도달할 수 있습니다. 기존의 수평적 배포(horizontal deployments)에서 필요했던 스티키 라우팅(sticky routing)과 공유 세션 저장소(shared session stores)가 더 이상 프로토콜 계층에서 요구되지 않습니다.
이것이 애플리케이션이 반드시 상태가 없는(stateless) 구조여야 한다는 의미는 아닙니다. 호출 간에 상태(state)가 필요한 서버는 HTTP API가 항상 해왔던 방식을 사용하면 됩니다. 즉, 하나의 도구(tool)에서 명시적인 핸들(예: basket_id)을 생성하고, 모델이 이후의 호출에서 이를 일반 인자(argument)로 다시 전달하도록 하는 것입니다. 따라서 처음부터 상태가 없는 전송(stateless transport)을 고려하여 설계하십시오. 이것이 프로토콜이 나아가는 방향이며, 확장(scale)을 위한 더 저렴한 경로입니다.
최소한의 프로덕션 MCP 서버 — FastMCP
공식 Python SDK에는 FastMCP가 포함되어 있습니다. 이는 함수 시그니처(signatures)와 독스트링(docstrings)으로부터 입력 스키마(input schema)를 생성하고, Pydantic 검증(validation)을 통합하며, 데코레이터(decorator)를 통해 도구(tools)를 등록하는 고수준 프레임워크(high-level framework)입니다. 아래 코드는 단순한
모델을 돕는 에러 핸들링 (Error handling)
위의 _handle_error 함수를 살펴보세요. 이것은 단순히 보기 좋게 만들기 위한 것이 아닙니다. MCP 서버의 에러 메시지는 로그를 응시하는 사람이 아니라 모델이 읽으며, 모델이 호출을 합리적으로 복구할지 아니면 막혀버릴지를 결정합니다. "Error 404"는 아무런 정보도 주지 않지만, "도시를 찾을 수 없습니다. 철자를 확인하세요"는 모델에게 다음에 무엇을 해야 할지 알려줍니다. 모든 메시지를 로그 라인이 아닌, 복구를 위한 지침(recovery instruction)으로 취급하세요.
이는 추측하기보다는 연역의 과정으로서의 디버깅 (debugging as a process of deduction rather than guessing)과 동일한 원칙입니다. 노이즈 대신 정밀한 신호를 전달하는 것이 원인으로 가는 경로를 단축합니다. 차이점은 여기서 신호의 수신자가 다음 단계를 계획하는 모델이라는 점입니다.
보안: 도구 설명(tool descriptions)을 신뢰할 수 없는 이유
MCP 명세(spec)는 이를 명확하게 명시하고 있습니다: 도구는 임의의 코드 실행(arbitrary code execution)을 나타내며 적절한 주의를 기울여 다뤄야 합니다. 더욱이 — 어노테이션(annotations)을 포함한 도구 동작에 대한 설명은 신뢰할 수 있는 서버에서 온 것이 아니라면 신뢰할 수 없습니다. 이는 형식적인 절차가 아닙니다. 악의적인 서버는 도구 설명이나 도구의 결과값에 모델이 명령으로 취급할 수 있는 지침을 몰래 숨길 수 있으며, 이것이 바로 도구 출력(tool output)을 통한 프롬프트 인젝션(prompt injection)입니다.
서버 작성자로서 당신이 직면할 결과는 구체적입니다. 비밀 정보(secrets)는 환경 변수(environment variables)에 보관하고, 코드나 설명에 절대 포함하지 마세요 (위의 예시에서 os.environ을 통해 WEATHER_API_KEY를 가져오는 것을 볼 수 있습니다). 원격 전송(remote transport)에는 OAuth 2.1 / OIDC를 사용하세요 — 2026-07-28 개정판은 권한 부여(authorization)를 OAuth 및 OpenID Connect와 더 밀접하게 일치시키며, Enterprise-Managed Authorization 확장이 이제 안정화되었습니다. 모델은 무엇이든 전달할 수 있으므로, Pydantic을 사용하여 모든 입력을 검증(validate)하세요. 그리고 어노테이션을 정직하게 설정하세요:
| 어노테이션 (Annotation) | 의미 | 예시 |
|---|---|---|
readOnlyHint | 도구가 상태를 변경하지 않음 | 예보 가져오기, 게시물 읽기 |
| ... | ... | ... |
호스트(Host)는 이러한 신호들을 기반으로 사용자 동의(user-consent) 흐름을 구축합니다. 잘못된 어노테이션(예: 데이터를 삭제하는 도구에 readOnlyHint를 설정하는 경우)은 단순히 나쁜 코드에 그치지 않습니다. 이는 전체 MCP 신뢰 모델(trust model)이 기반하고 있는 보안 계약(security contract)을 깨뜨리는 행위입니다.
상태(State), 동시성(Concurrency), 그리고 확장성(Scaling)
프로덕션 서버는 동시에 많은 클라이언트를 처리하며, 모든 도구는 API 호출, 데이터베이스, 디스크 작업과 같은 I/O를 수행합니다. 이것이 모든 코드가 비동기(async def, httpx.AsyncClient)로 작성되는 이유입니다. 네트워크 응답을 기다리는 동안 이벤트 루프(event loop)가 다른 태스크로 전환되기 때문에, 하나의 프로세스가 차단(blocking) 없이 많은 동시 호출을 처리할 수 있습니다.
이는 epoll과 io_uring이 이벤트 루프만으로 충분하지 않을 때 해결하는 방식과 정확히 동일한 I/O 확장성 문제입니다. "연결당 하나의 스레드(one thread per connection)" 모델은 무한히 확장될 수 없습니다. Streamable HTTP 기반의 MCP 서버도 동일한 계층에 있습니다. 비동기(async)는 장식 요소가 아니라, 하나의 인스턴스에서 많은 클라이언트를 서비스하기 위한 필수 조건입니다. 그리고 2026-07-28 stateless core 덕분에, 수평적 확장(horizontal scaling)은 로드 밸런서(load balancer) 뒤에 더 많은 인스턴스를 세우는 것으로 단순화되며, 스티키 세션(sticky sessions)도 필요하지 않습니다.
# 로컬 — stdio (기본값)
mcp.run()
...
결론
단순한 연습용이 아닌 MCP 서버를 구축하는 것은 몇 가지 의도적인 결정으로 귀결됩니다: 전송 방식의 선택(로컬에서는 stdio, 프로덕션에서는 Streamable HTTP), 적절한 프리미티브(primitive) 선택(tool vs resource vs prompt), Pydantic 검증, 실행 가능한 에러 처리, 환경 변수를 통한 비밀 정보(secrets) 관리, 그리고 정직한 어노테이션입니다. FastMCP가 상용구(boilerplate) 코드를 대신 처리해 주지만, 아키텍처와 보안에 대한 책임은 여전히 개발자에게 있습니다.
한 가지 더, 최신 정보입니다. 무상태성 (statelessness)을 고려하여 설계하십시오. 2026-07-28 수정 버전은 전송 (transport)을 기본적으로 세션리스 (sessionless)로 만드는데, 이는 프로토콜이 제공해 온 확장 (scale)을 위한 가장 저렴한 경로입니다. 세션 (session) 대신 명시적인 상태 핸들 (state handles)을 중심으로 오늘 작성된 MCP 서버는 재작성 없이도 해당 변경 사항에 대응할 수 있을 것입니다. 이것은 MCP에 관한 시리즈의 첫 번째 포스트이며, 다음 포스트에서는 보안과 고급 패턴 (advanced patterns)에 대해 더 깊이 다룰 예정입니다.
원문 게시처: devmindset.dev — Linux 내부 구조, 시스템 프로그래밍, 그리고 독학하는 개발자의 마인드셋.
관련 심층 분석:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기