MCP 서버 만들기 2026 — Python 20줄 실습과 Claude 등록 3가지, FastMCP는 MCPServer가 됐다
MCP 서버를 Python 20줄로 만들어 Claude에 붙이는 전 과정. FastMCP가 MCPServer로 바뀐 v2 변경점, claude mcp add·.mcpb·설정 파일 세 경로 비교, 도구가 안 보일 때 보는 순서, TypeScript 버전까지 실행해 확인했다.
MCP 서버는 Python 20줄이면 돌아간다. 막히는 곳은 서버가 아니라 Claude에 붙이는 쪽이고, 2026년에는 여기서 한 번 더 막힌다. 검색해서 나오는 예제 대부분이 from mcp.server.fastmcp import FastMCP로 시작하는데, 그 경로는 지금 존재하지 않는다. 공식 Python SDK가 2.0에서 FastMCP를 MCPServer로 바꿨고, 별칭을 남기지 않고 지웠다.
이 글은 가장 작은 서버 하나를 만들어 실제로 호출되는 것까지 확인한다. 아래 코드와 오류 메시지는 전부 2026-11-14에 mcp 2.3.0 · @modelcontextprotocol/server 2.4.0으로 직접 실행해 확인한 것이다.
가장 작은 서버 — 도구 하나, 20줄
공식 문서가 쓰는 설치 명령은 uv를 쓴다. pip도 된다.
uv add "mcp[cli]" # 또는: pip install "mcp[cli]"cli 추가분이 mcp 명령(mcp dev · mcp run · mcp install)을 같이 깔아 준다. 서버만 돌릴 거면 mcp 하나로도 충분하다.
server.py:
import logging
from mcp.server import MCPServer
logging.basicConfig(level=logging.INFO) # 기본 출력이 stderr 다
log = logging.getLogger("demo")
mcp = MCPServer("demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""두 정수를 더한다."""
log.info("add(%s, %s) 호출", a, b)
return a + b
if __name__ == "__main__":
mcp.run(transport="stdio")끝이다. 스키마를 따로 쓰지 않는다. 타입 힌트에서 입력 스키마가, 독스트링에서 도구 설명이 만들어진다. 실제로 받아 보면 이렇게 나온다.
tool: add | 두 정수를 더한다. | {"properties": {"a": {"title": "A", "type": "integer"},
"b": {"title": "B", "type": "integer"}}, "required": ["a", "b"], "type": "object"}그래서 독스트링을 성의 없이 쓰면 모델이 도구를 잘못 고른다. 설명은 사람이 읽을 주석이 아니라 모델이 읽는 입력이다.
mcp.run()의 transport 기본값은 stdio라서 인자를 빼도 같다. 선택지는 stdio · sse · streamable-http 세 개다. 로컬에서 Claude에 붙일 때는 stdio를 쓴다.
FastMCP로 시작하는 예제는 전부 구버전이다
이게 지금 가장 많이 걸리는 자리다. v1 경로를 그대로 쓰면 이렇게 끝난다.
ModuleNotFoundError: No module named 'mcp.server.fastmcp'.
This is mcp 2.x, where FastMCP was renamed to MCPServer
(from mcp.server.mcpserver import MCPServer) and other APIs changed;
see the migration guide ... or pin 'mcp<2' to keep running v1 code.친절한 메시지지만, 검색 결과의 절반이 이 오류로 끝난다는 뜻이기도 하다. pip install mcp는 이제 2.x를 깐다. 공식 마이그레이션 문서가 「옛 임포트 경로는 폐기 예고가 아니라 삭제됐다」고 적는다.
바뀐 이름을 모아 두면 이렇다.
| v1 (FastMCP 시절) | v2 (현재) |
|---|---|
from mcp.server.fastmcp import FastMCP | from mcp.server import MCPServer |
FastMCP("Demo") | MCPServer("Demo") |
mcp.server.fastmcp.* 하위 전부 | mcp.server.mcpserver.* |
ctx.fastmcp | ctx.mcp_server |
get_context() | 삭제 |
FastMCPError | MCPServerError |
FastMCP("x", port=9000) | TypeError — 전송 옵션은 run()으로 |
Tool.inputSchema | Tool.input_schema |
마지막 두 줄은 문서 표에 없어서 직접 실행하다 걸린 것이다. 생성자에 port를 넣으면 MCPServer.__init__() got an unexpected keyword argument 'port'가 뜬다. 그리고 서버 쪽 Tool 객체의 필드가 camelCase에서 snake_case로 바뀌어서, v1 코드에서 t.inputSchema를 읽던 자리가 AttributeError로 죽는다. 와이어 포맷은 여전히 inputSchema라서 더 헷갈린다.
v1에 의존하는 패키지를 쓰고 있다면 마이그레이션 전까지 mcp<2 상한을 걸어 두라는 것이 공식 권고다. 프로토콜 자체의 구조와 A2A와의 차이는 MCP 서버 완전 가이드에 정리해 뒀다.
Claude에 붙이는 길이 셋이고, 문서가 서로 다르다
여기서 한 번 더 막힌다. 2026년 기준으로 경로가 세 개인데 공식 문서끼리 안내가 갈린다.
| 경로 | 쓰는 곳 | 공식 문서 상태 |
|---|---|---|
claude mcp add (Claude Code) | 터미널·프로젝트 단위 | 가장 자세히 문서화돼 있다 |
Desktop 확장(.mcpb) | Claude Desktop, 배포용 | 지원 문서가 이쪽만 설명한다 |
claude_desktop_config.json 직접 편집 | Claude Desktop, 내 서버 | 지원 문서에는 없고 프로토콜 문서에만 있다 |
Claude Code가 지금 가장 확실하다.
claude mcp add --transport stdio demo -- uv --directory /절대/경로/demo run server.py
claude mcp list # ✔ Connected / ! Needs authentication / ✘ Failed to connect
claude mcp get demo--가 중요하다. 그 앞은 Claude의 옵션이고 뒤는 서버를 띄우는 명령어로 그대로 넘어간다. 환경변수는 --env KEY=value로 주는데, 서버 이름이 --env 바로 뒤에 오면 CLI가 거부하므로 사이에 다른 옵션을 하나 끼워야 한다. 기본 범위는 local이고 --scope project나 --scope user로 바꾼다.
팀과 공유하려면 프로젝트 루트의 .mcp.json에 적어 커밋한다.
{
"mcpServers": {
"demo": {
"command": "uv",
"args": ["--directory", "/절대/경로/demo", "run", "server.py"]
}
}
}type이 없으면 stdio로 읽는다. url은 있는데 type이 없으면 설정 오류다. ${VAR:-기본값} 꼴의 치환이 되고, 커밋한 프로젝트 서버는 받는 사람 쪽에서 한 번 승인해야 붙는다.
Claude Desktop은 지원 문서가 .mcpb 확장 쪽만 설명한다. Settings → Desktop app → Extensions의 Manage, 내가 만든 걸 넣으려면 Advanced settings → Extension Developer → Install Extension…으로 .mcpb 파일을 고른다. Desktop에 Node.js가 내장돼 있어서 받는 사람이 Node를 따로 깔 필요가 없다. 포장은 manifest.json을 만들고 npx @anthropic-ai/mcpb pack을 돌린다.
설정 파일을 직접 고치는 길은 프로토콜 문서에는 아직 살아 있다. macOS 경로는 ~/Library/Application Support/Claude/claude_desktop_config.json이고 모양은 위 .mcp.json과 같다. 다만 지원 문서가 「JSON을 손으로 고칠 필요가 없게 하려고 확장을 만들었다」고 적고 있으니, 남에게 배포할 거면 .mcpb로 가는 게 맞다. 혼자 쓰는 서버는 설정 파일이 여전히 가장 빠르다.
붙었는지 확인하는 법 — 핸드셰이크를 눈으로 본다
「도구가 안 보인다」의 원인은 거의 항상 서버가 아니라 등록이다. 그래서 Claude를 거치지 않고 먼저 서버만 때려 본다.
uv run mcp dev server.py # MCP Inspector 로 띄운다TypeScript 쪽 공식 명령은 이렇게 생겼는데, 옵션 하나를 놓치면 오해한다.
npx @modelcontextprotocol/inspector --cli node build/index.js \
--protocol-era modern --method tools/listInspector의 기본 era가 legacy라서, 같은 명령을 --protocol-era legacy로 한 번 더 돌려 봐야 한다. 둘 중 하나만 통과하는 경우가 실제로 있다.
왜 era가 둘인지는 SDK 안을 보면 드러난다. Python SDK 2.3.0의 상수를 그대로 찍어 보면:
HANDSHAKE_PROTOCOL_VERSIONS = ('2024-11-05', '2025-03-26', '2025-06-18', '2025-11-25')
MODERN_PROTOCOL_VERSIONS = ('2026-07-28',)
LATEST_PROTOCOL_VERSION = 2026-07-28최신 리비전 2026-07-28은 옛 initialize 핸드셰이크로는 도달하지 않는 별도 계열이다. 실제로 위 서버에 공식 Python 클라이언트를 붙여 보면 협상 결과가 2025-11-25로 나온다. 그리고 TypeScript SDK 2.4.0의 LATEST_PROTOCOL_VERSION은 아직 2025-11-25다 — README는 2026-07-28 스펙 구현이라고 적는데 상수는 그렇지 않다. 상수 하나로 「이 SDK는 몇 년 스펙이다」를 판단하면 안 된다.
붙은 뒤의 확인은 단순하다. Claude Code에서는 /mcp로 상태를 보고 켜고 끌 수 있다. 끝까지 통하면 이런 줄이 나온다.
call_tool add(2,40) -> ['42']도구·리소스·프롬프트 — 첫 서버에서는 도구만 만든다
MCP가 노출하는 것은 세 가지다.
- 도구(tool) — 모델이 호출하는 함수. 부수효과가 있어도 된다.
- 리소스(resource) — 모델이 읽는 데이터.
@mcp.resource("greeting://{name}")처럼 URI 템플릿을 받는다. - 프롬프트(prompt) — 재사용할 프롬프트 템플릿.
첫 서버에서 셋을 다 만들면 안 되는 이유는 취향이 아니다. 어디가 안 되는지가 가려진다. 도구만 있는 서버가 tools/list에 뜨면 등록·전송·스키마 생성이 전부 통과한 것이고, 그 상태에서 리소스를 더하면 실패 원인이 리소스 쪽으로 좁혀진다. 셋을 같이 올리면 「아무것도 안 보인다」만 남는다.
MCP와 비슷해 보이지만 다른 층인 Claude 스킬과의 차이는 클로드 스킬 만들기에 비교해 뒀다. 요약하면 스킬은 지시문이고 MCP는 연결이다.
TypeScript로 같은 서버
패키지 이름이 바뀐 것부터 봐야 한다. @modelcontextprotocol/sdk는 v1 계열(현재 1.32.2)이고, 새로 쓰는 코드는 @modelcontextprotocol/server(현재 2.4.0)를 쓴다. 공식 README가 v1을 새 코드에 쓰지 말라고 적는다.
npm install @modelcontextprotocol/server zod
npm install -D @types/node typescriptimport { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
function createServer() {
const server = new McpServer({ name: "demo", version: "1.0.0" });
server.registerTool(
"add",
{ description: "두 정수를 더한다.", inputSchema: z.object({ a: z.number(), b: z.number() }) },
async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] }),
);
return server;
}
await serveStdio(createServer, { onerror: (e) => console.error(e) });두 가지가 함정이다. 첫째, 도구 등록은 server.registerTool(...)이다. server.tool(...)은 v2에 존재하지 않는다 — 객체를 찍어 보면 undefined로 나온다. 그 형태를 쓰는 글은 v1 기준이다. 둘째, new StdioServerTransport()에 server.connect()하는 고전적인 형태도 여전히 export돼 있지만, 공식 문서는 serveStdio(createServer, ...)를 권한다. 전자는 2025년대 클라이언트만 받는다고 적혀 있다.
그리고 Node 20 이상이어야 하고, npm run build를 돌려야 한다. 빌드하지 않아 연결이 안 되는 경우가 문서에 경고로 따로 적혀 있을 만큼 흔하다.
처음 만드는 사람이 걸리는 자리 다섯
print()한 줄이 서버를 죽인다. stdio 전송은 stdout으로 JSON-RPC를 주고받는다. 거기에 아무 글자나 끼면 프레임이 깨진다. 공식 문서 표현 그대로 「절대 stdout에 쓰지 말라」다. Python은logging을 쓰면 stderr로 나가고, TypeScript는console.log가 아니라console.error를 쓴다.- 상대경로로는 안 뜬다. 클라이언트 설정의
command·args는 절대경로여야 한다.uv자체도 못 찾는 경우가 있어서which uv결과를 그대로 박아 넣어야 할 때가 있다. - 창을 닫는 건 재시작이 아니다. Claude Desktop은 설정을 읽는 시점이 실행 시점이라, 완전히 종료(macOS는 Cmd+Q)하고 다시 열어야 한다.
- 로그를 먼저 본다. macOS에서
~/Library/Logs/Claude/mcp*.log를 보면 된다.mcp.log에 연결 과정이,mcp-server-<이름>.log에 그 서버의 stderr가 들어간다. 「도구가 안 보인다」의 답이 거의 여기 있다. - 버전 고정. 서버가 돌기 시작하면 SDK 버전을 정확히 박아 둔다. 위에서 본 것처럼 이 생태계는 마이너 릴리스에서 임포트 경로가 바뀐다.
실제로 쓸 만한 서버가 되면 비용 쪽이 다음 문제다. 도구 응답이 그대로 컨텍스트로 들어가므로 응답 크기가 토큰 청구서가 된다. Playwright MCP 토큰 최적화에 스냅샷을 96% 줄인 사례를 적어 뒀다.
그래서 MCP 서버를 언제 직접 만드나
만들 이유는 하나다. 모델이 우리 쪽 데이터나 동작에 닿아야 할 때. 공개된 서버로 되는 일은 만들지 않는 게 맞다.
직접 만든 값이 나오는 자리는 대체로 이렇다. 사내 시스템에 API가 있지만 사람이 매번 조회해서 옮겨 적고 있는 경우, 업무가 특정 파일 형식에 묶여 있는 경우가 그렇다. 한국에서는 두 번째가 많다 — 한글 문서를 모델이 직접 읽고 쓰게 만든 사례를 HWP-MCP에 적어 뒀고, PDF·DOCX를 RAG에 넣는 쪽은 Docling 사용법에서 다뤘다. 서버 하나를 만들어 실제 업무 흐름에 붙인 전체 사례는 MCP로 만든 뉴스 수집·배포 시스템에 있다.
트리숲이 AX 과제에서 실제로 겪은 순서도 같다. 도구를 먼저 많이 만드는 팀은 대체로 실패한다. 사람이 반복해서 하고 있는 조회·전사·옮겨적기를 먼저 찾고, 그중 하나만 MCP 도구로 만들어 붙인 다음 그 도구가 실제로 호출되는지를 2주쯤 본다. 호출되지 않는 도구는 설명이 나쁜 것이고, 호출되는데 결과가 틀리면 그건 모델 문제가 아니라 데이터 문제다.
이 판단과 구축을 한 번에 묶어 진행하는 쪽이 업무 자동화 구축이고, 사내 업무에 바로 붙여 쓸 수 있는 형태로 만들어 둔 것이 ax.trina.app이다.
Q: pip install mcp를 했는데 예제가 안 돌아간다. 왜인가?
예제가 v1 기준일 가능성이 가장 높다. pip install mcp는 2.x를 깔고, mcp.server.fastmcp 경로는 2.x에 없다. 오류 메시지에 마이그레이션 문서 주소와 mcp<2 고정 방법이 같이 나온다. 새로 쓰는 코드라면 from mcp.server import MCPServer로 고치는 쪽이 맞다.
Q: Python 몇 버전이 필요한가?
문서가 엇갈린다. PyPI의 요구 사항과 공식 퀵스타트는 3.10 이상, GitHub README는 3.11 이상이라고 적는다. 3.9에서 설치하면 「조건을 만족하는 버전이 없다」로 끊기는 것까지는 확인했다. 안전하게는 3.11 이상을 쓴다.
Q: Claude Desktop에 붙일 때 설정 파일을 고쳐야 하나, 확장으로 만들어야 하나?
혼자 쓰는 서버면 설정 파일이 빠르다. 남에게 주려면 .mcpb 확장으로 포장하는 게 맞다. 받는 쪽이 의존성을 깔지 않아도 되고, Desktop에 Node.js가 내장돼 있어서 설치가 한 번에 끝난다. 공식 지원 문서는 확장 쪽만 설명한다는 점도 참고할 만하다.
Q: 도구가 Claude에서 안 보인다. 무엇부터 보나?
순서가 있다. ① Inspector로 서버만 띄워 tools/list가 나오는지 본다 — 나오면 서버는 정상이고 등록 문제다. ② Claude Code면 claude mcp list의 상태 표시를, Desktop이면 ~/Library/Logs/Claude/mcp*.log를 본다. ③ 설정의 경로가 절대경로인지, stdout에 출력이 섞이지 않았는지 본다. 이 셋에서 거의 끝난다.
Q: 프로토콜 리비전이 2026-07-28이라는데 내 SDK는 2025-11-25로 붙는다. 문제인가?
아니다. 2026-07-28은 옛 핸드셰이크와 별도 계열로 취급된다. Python SDK 2.3.0은 핸드셰이크 계열 최신을 2025-11-25로, 별도 계열을 2026-07-28로 따로 들고 있다. Inspector에 --protocol-era 옵션이 있는 이유도 이것이다. 두 era로 각각 한 번씩 확인해 보는 편이 좋다.
참고한 1차 출처 (2026-11-14 확인)