Python의 FastMCP 프레임워크를 사용하여 데코레이터 기반의 간결하고 직관적인 MCP 서버를 구축하는 방법을 다룹니다.
FastMCP는 Python으로 MCP 서버를 구축하기 위한 고수준 프레임워크입니다. 데코레이터 기반의 직관적인 API를 제공하여, 최소한의 보일러플레이트 코드로 MCP 서버를 작성할 수 있습니다.
FastMCP의 설계 철학은 FastAPI에서 영감을 받았습니다. 함수에 데코레이터를 붙이면 자동으로 MCP 도구, 리소스, 프롬프트로 등록되며, 타입 힌트에서 JSON Schema가 자동 생성됩니다. Python 개발자에게 이미 친숙한 패턴입니다.
pip install fastmcp가장 기본적인 FastMCP 서버를 작성합니다.
from fastmcp import FastMCP
mcp = FastMCP("calculator-server")
@mcp.tool()
def add(a: float, b: float) -> float:
"""두 숫자를 더합니다."""
return a + b
@mcp.tool()
def multiply(a: float, b: float) -> float:
"""두 숫자를 곱합니다."""
return a * b
if __name__ == "__main__":
mcp.run()이것이 완전한 MCP 서버입니다. 7줄의 핵심 코드로 두 개의 도구를 가진 서버가 완성됩니다. @mcp.tool() 데코레이터가 함수의 시그니처와 독스트링(docstring)을 분석하여 도구 스키마를 자동 생성합니다.
python server.py@mcp.tool() 데코레이터는 반드시 괄호와 함께 호출해야 합니다. @mcp.tool처럼 괄호 없이 사용하면 TypeError가 발생합니다. 이는 데코레이터가 설정 매개변수를 받을 수 있도록 팩토리 패턴으로 설계되었기 때문입니다.
FastMCP는 함수의 타입 힌트를 분석하여 JSON Schema를 자동 생성합니다. Pydantic 모델도 지원합니다.
from typing import Optional
from pydantic import BaseModel, Field
class SearchParams(BaseModel):
query: str = Field(description="검색어")
max_results: int = Field(default=10, ge=1, le=100, description="최대 결과 수")
language: Optional[str] = Field(default="ko", description="결과 언어 코드")
@mcp.tool()
def search_documents(params: SearchParams) -> str:
"""문서를 검색합니다. 키워드 기반 전문 검색을 지원합니다."""
results = perform_search(params.query, params.max_results, params.language)
return format_results(results)Pydantic의 Field를 사용하면 매개변수에 대한 상세한 제약 조건(최솟값, 최댓값, 정규식 패턴 등)과 설명을 정의할 수 있습니다.
FastMCP는 동기 함수와 비동기 함수를 모두 지원합니다. I/O 작업이 포함된 도구는 비동기로 구현하는 것이 효율적입니다.
import httpx
@mcp.tool()
async def fetch_webpage(url: str) -> str:
"""웹 페이지의 내용을 가져옵니다."""
async with httpx.AsyncClient() as client:
response = await client.get(url, follow_redirects=True, timeout=30.0)
response.raise_for_status()
return response.text기본적으로 도구 이름은 함수 이름, 설명은 독스트링에서 가져옵니다. 이를 명시적으로 지정할 수도 있습니다.
@mcp.tool(name="query_db", description="SQL 쿼리를 실행하여 데이터베이스에서 데이터를 조회합니다")
async def execute_database_query(query: str, database: str = "main") -> str:
"""내부적으로는 긴 이름을 사용하지만, MCP에 노출되는 이름은 query_db입니다."""
result = await db.execute(query, database)
return str(result)도구에서 예외가 발생하면 FastMCP가 자동으로 에러 응답을 구성합니다. 사용자 친화적인 에러 메시지를 제공하려면 명시적으로 예외를 처리합니다.
@mcp.tool()
async def read_file(path: str) -> str:
"""지정된 경로의 파일을 읽습니다."""
import os
if not os.path.exists(path):
raise FileNotFoundError("파일을 찾을 수 없습니다: " + path)
if not os.path.isfile(path):
raise ValueError("지정된 경로는 파일이 아닙니다: " + path)
with open(path, "r", encoding="utf-8") as f:
return f.read()FastMCP에서 리소스는 @mcp.resource() 데코레이터로 등록합니다.
@mcp.resource("config://app")
def get_app_config() -> str:
"""애플리케이션 설정 정보를 제공합니다."""
import json
config = {
"app_name": "Weather Service",
"version": "1.0.0",
"supported_cities": ["서울", "부산", "제주", "인천", "대구"],
"api_version": "v2",
"rate_limit": {"requests_per_minute": 60},
}
return json.dumps(config, ensure_ascii=False, indent=2)@mcp.resource("config://app")에서 문자열이 리소스의 URI입니다.
URI에 중괄호를 사용하여 동적 리소스를 정의합니다.
@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
"""특정 사용자의 프로필 정보를 제공합니다."""
import json
profiles = {
"user001": {"name": "김개발", "role": "backend", "team": "platform"},
"user002": {"name": "이디자인", "role": "designer", "team": "product"},
}
profile = profiles.get(user_id)
if not profile:
return json.dumps({"error": "사용자를 찾을 수 없습니다"}, ensure_ascii=False)
return json.dumps(profile, ensure_ascii=False, indent=2)from fastmcp.prompts import Message
@mcp.prompt()
def code_review(code: str, language: str = "python") -> list[Message]:
"""코드 리뷰를 수행합니다."""
return [
Message(
role="user",
content=(
"다음 " + language + " 코드를 리뷰해 주세요.\n\n"
"리뷰 관점:\n"
"1. 버그 가능성\n"
"2. 보안 취약점\n"
"3. 성능 개선 여지\n"
"4. 코드 가독성\n"
"5. 모범 사례 준수 여부\n\n"
"코드:\n```" + language + "\n" + code + "\n```"
),
)
]
@mcp.prompt()
def debug_error(error_message: str, stack_trace: str = "") -> list[Message]:
"""에러를 분석하고 해결 방안을 제시합니다."""
content = "다음 에러를 분석하고 해결 방안을 제시해 주세요.\n\n"
content += "에러 메시지:\n" + error_message + "\n"
if stack_trace:
content += "\n스택 트레이스:\n" + stack_trace + "\n"
content += (
"\n분석 형식:\n"
"1. 원인 분석\n"
"2. 해결 방안 (우선순위 순)\n"
"3. 재발 방지 대책"
)
return [Message(role="user", content=content)]지금까지 학습한 내용을 종합하여, 실용적인 파일 관리 MCP 서버를 구축합니다.
import os
import json
from datetime import datetime
from pathlib import Path
from typing import Optional
from fastmcp import FastMCP
from fastmcp.prompts import Message
from pydantic import BaseModel, Field
mcp = FastMCP("file-manager", description="파일 시스템 관리 MCP 서버")
# 작업 디렉토리 설정
WORKSPACE = os.environ.get("WORKSPACE_DIR", os.path.expanduser("~/workspace"))
# --- 도구 ---
@mcp.tool()
def list_files(
directory: str = ".",
pattern: str = "*",
recursive: bool = False,
) -> str:
"""디렉토리의 파일 목록을 반환합니다."""
base = Path(WORKSPACE) / directory
if not base.exists():
raise FileNotFoundError("디렉토리가 존재하지 않습니다: " + str(base))
if recursive:
files = list(base.rglob(pattern))
else:
files = list(base.glob(pattern))
result = []
for f in sorted(files)[:100]:
stat = f.stat()
result.append({
"name": f.name,
"path": str(f.relative_to(WORKSPACE)),
"size": stat.st_size,
"is_directory": f.is_dir(),
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
})
return json.dumps(result, ensure_ascii=False, indent=2)
@mcp.tool()
def read_file(path: str, encoding: str = "utf-8") -> str:
"""파일의 내용을 읽습니다."""
file_path = Path(WORKSPACE) / path
if not file_path.exists():
raise FileNotFoundError("파일을 찾을 수 없습니다: " + path)
if not file_path.is_file():
raise ValueError("경로가 파일이 아닙니다: " + path)
return file_path.read_text(encoding=encoding)
@mcp.tool()
def write_file(path: str, content: str, encoding: str = "utf-8") -> str:
"""파일에 내용을 씁니다. 파일이 없으면 생성합니다."""
file_path = Path(WORKSPACE) / path
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content, encoding=encoding)
return "파일이 저장되었습니다: " + path
@mcp.tool()
def search_in_files(
query: str,
directory: str = ".",
file_pattern: str = "*.py",
) -> str:
"""파일 내용에서 텍스트를 검색합니다."""
base = Path(WORKSPACE) / directory
matches = []
for file_path in base.rglob(file_pattern):
if not file_path.is_file():
continue
try:
content = file_path.read_text(encoding="utf-8")
for i, line in enumerate(content.splitlines(), 1):
if query.lower() in line.lower():
matches.append({
"file": str(file_path.relative_to(WORKSPACE)),
"line": i,
"content": line.strip(),
})
except (UnicodeDecodeError, PermissionError):
continue
return json.dumps(matches[:50], ensure_ascii=False, indent=2)
# --- 리소스 ---
@mcp.resource("workspace://structure")
def get_workspace_structure() -> str:
"""작업 디렉토리의 구조를 트리 형태로 제공합니다."""
def build_tree(path: Path, prefix: str = "", max_depth: int = 3, depth: int = 0) -> str:
if depth >= max_depth:
return ""
lines = []
entries = sorted(path.iterdir(), key=lambda e: (not e.is_dir(), e.name))
for i, entry in enumerate(entries):
if entry.name.startswith("."):
continue
is_last = i == len(entries) - 1
connector = "--- " if is_last else "|-- "
lines.append(prefix + connector + entry.name)
if entry.is_dir():
extension = " " if is_last else "| "
subtree = build_tree(entry, prefix + extension, max_depth, depth + 1)
if subtree:
lines.append(subtree)
return "\n".join(lines)
tree = build_tree(Path(WORKSPACE))
return WORKSPACE + "\n" + tree
@mcp.resource("workspace://stats")
def get_workspace_stats() -> str:
"""작업 디렉토리의 통계 정보를 제공합니다."""
base = Path(WORKSPACE)
file_count = 0
dir_count = 0
total_size = 0
extensions: dict[str, int] = {}
for item in base.rglob("*"):
if item.name.startswith("."):
continue
if item.is_file():
file_count += 1
total_size += item.stat().st_size
ext = item.suffix or "(없음)"
extensions[ext] = extensions.get(ext, 0) + 1
elif item.is_dir():
dir_count += 1
stats = {
"workspace": WORKSPACE,
"files": file_count,
"directories": dir_count,
"total_size_mb": round(total_size / (1024 * 1024), 2),
"top_extensions": dict(sorted(extensions.items(), key=lambda x: -x[1])[:10]),
}
return json.dumps(stats, ensure_ascii=False, indent=2)
# --- 프롬프트 ---
@mcp.prompt()
def analyze_project() -> list[Message]:
"""프로젝트 구조를 분석하고 개선 사항을 제안합니다."""
return [
Message(
role="user",
content=(
"현재 작업 디렉토리의 프로젝트를 분석해 주세요.\n\n"
"1. workspace://structure 리소스에서 디렉토리 구조를 확인하세요.\n"
"2. workspace://stats 리소스에서 통계 정보를 확인하세요.\n"
"3. 주요 설정 파일(package.json, pyproject.toml 등)을 read_file로 확인하세요.\n\n"
"분석 항목:\n"
"- 프로젝트 유형과 기술 스택\n"
"- 디렉토리 구조의 적절성\n"
"- 발견된 문제점이나 개선 사항\n"
"- 권장 사항"
),
)
]
if __name__ == "__main__":
mcp.run()FastMCP는 기본적으로 stdio 전송을 사용합니다. Streamable HTTP로 전환하려면 run() 메서드에 매개변수를 전달합니다.
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)import os
if __name__ == "__main__":
transport = os.environ.get("MCP_TRANSPORT", "stdio")
if transport == "http":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
else:
mcp.run()동일한 도구를 TypeScript와 Python으로 구현한 코드를 비교합니다.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({ name: "calc", version: "1.0.0" });
server.tool(
"add",
"두 숫자를 더합니다",
{ a: z.number(), b: z.number() },
async (params) => ({
content: [{ type: "text", text: String(params.a + params.b) }],
})
);from fastmcp import FastMCP
mcp = FastMCP("calc")
@mcp.tool()
def add(a: float, b: float) -> float:
"""두 숫자를 더합니다."""
return a + bFastMCP는 함수의 반환값을 자동으로 MCP 응답 형식으로 변환합니다. 문자열, 숫자, 딕셔너리 등 Python의 기본 자료형을 반환하면 됩니다. TypeScript SDK에서는 content 배열을 명시적으로 구성해야 합니다.
| 비교 항목 | TypeScript SDK | Python FastMCP |
|---|---|---|
| 스키마 정의 | Zod 스키마 | 타입 힌트 + Pydantic |
| 반환값 형식 | content 배열 직접 구성 | 자동 변환 |
| 비동기 지원 | 기본 (async/await) | 동기/비동기 모두 지원 |
| 코드량 | 상대적으로 많음 | 매우 간결 |
| 타입 안전성 | 컴파일 타임 검사 | 런타임 검사 |
어떤 SDK를 선택할지는 팀의 기술 스택에 따라 결정합니다. Node.js 기반 프로젝트라면 TypeScript SDK가 자연스럽고, Python 기반이라면 FastMCP가 적합합니다. 두 SDK 모두 동일한 MCP 프로토콜을 구현하므로, 어떤 것을 사용하든 동일한 클라이언트와 호환됩니다.
이 장에서는 Python FastMCP를 사용하여 MCP 서버를 구축했습니다.
@mcp.tool(), @mcp.resource(), @mcp.prompt()로 간결하게 프리미티브를 등록합니다.7장에서는 MCP 클라이언트 구현을 다룹니다. 클라이언트 측에서 MCP 서버에 연결하고, 도구를 호출하며, LLM과 통합하는 방법을 상세히 살펴보겠습니다. 기존 애플리케이션에 MCP 클라이언트 기능을 추가하여 AI 기반 도구 통합을 구현하는 실전 패턴을 다룹니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
MCP 클라이언트를 직접 구현하여 서버에 연결하고, LLM과 통합하여 도구 호출 파이프라인을 완성하는 방법을 다룹니다.
TypeScript SDK를 사용하여 프로젝트 설정부터 도구, 리소스, 프롬프트 구현, 테스트까지 MCP 서버를 구축하는 전 과정을 다룹니다.
데이터베이스, REST API, 레거시 시스템을 MCP 서버로 래핑하여 AI 모델이 접근할 수 있도록 만드는 실전 패턴을 다룹니다.