본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 6장: Python FastMCP로 서버 구축하기
2026년 1월 31일·AI / ML·

6장: Python FastMCP로 서버 구축하기

Python의 FastMCP 프레임워크를 사용하여 데코레이터 기반의 간결하고 직관적인 MCP 서버를 구축하는 방법을 다룹니다.

17분1,071자10개 섹션
mcptypescriptpython
공유
mcp-guide6 / 10
12345678910
이전5장: TypeScript로 MCP 서버 구축하기다음7장: MCP 클라이언트 구현하기

FastMCP 소개

FastMCP는 Python으로 MCP 서버를 구축하기 위한 고수준 프레임워크입니다. 데코레이터 기반의 직관적인 API를 제공하여, 최소한의 보일러플레이트 코드로 MCP 서버를 작성할 수 있습니다.

FastMCP의 설계 철학은 FastAPI에서 영감을 받았습니다. 함수에 데코레이터를 붙이면 자동으로 MCP 도구, 리소스, 프롬프트로 등록되며, 타입 힌트에서 JSON Schema가 자동 생성됩니다. Python 개발자에게 이미 친숙한 패턴입니다.

FastMCP 설치
bash
pip install fastmcp

첫 번째 FastMCP 서버

가장 기본적인 FastMCP 서버를 작성합니다.

server.py
python
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)을 분석하여 도구 스키마를 자동 생성합니다.

서버 실행
bash
python server.py
Warning

@mcp.tool() 데코레이터는 반드시 괄호와 함께 호출해야 합니다. @mcp.tool처럼 괄호 없이 사용하면 TypeError가 발생합니다. 이는 데코레이터가 설정 매개변수를 받을 수 있도록 팩토리 패턴으로 설계되었기 때문입니다.

도구 구현 상세

타입 힌트 기반 스키마 생성

FastMCP는 함수의 타입 힌트를 분석하여 JSON Schema를 자동 생성합니다. Pydantic 모델도 지원합니다.

타입 힌트 기반 도구
python
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 작업이 포함된 도구는 비동기로 구현하는 것이 효율적입니다.

비동기 도구
python
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

도구 이름과 설명 커스터마이징

기본적으로 도구 이름은 함수 이름, 설명은 독스트링에서 가져옵니다. 이를 명시적으로 지정할 수도 있습니다.

이름과 설명 지정
python
@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가 자동으로 에러 응답을 구성합니다. 사용자 친화적인 에러 메시지를 제공하려면 명시적으로 예외를 처리합니다.

에러 처리 패턴
python
@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() 데코레이터로 등록합니다.

정적 리소스

정적 리소스
python
@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에 중괄호를 사용하여 동적 리소스를 정의합니다.

동적 리소스
python
@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)

프롬프트 구현

프롬프트
python
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 서버

지금까지 학습한 내용을 종합하여, 실용적인 파일 관리 MCP 서버를 구축합니다.

file_server.py
python
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() 메서드에 매개변수를 전달합니다.

Streamable HTTP로 실행
python
if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
환경 변수로 설정
python
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 SDK와의 비교

동일한 도구를 TypeScript와 Python으로 구현한 코드를 비교합니다.

TypeScript SDK
typescript
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) }],
  })
);
Python FastMCP
python
from fastmcp import FastMCP
 
mcp = FastMCP("calc")
 
@mcp.tool()
def add(a: float, b: float) -> float:
    """두 숫자를 더합니다."""
    return a + b

FastMCP는 함수의 반환값을 자동으로 MCP 응답 형식으로 변환합니다. 문자열, 숫자, 딕셔너리 등 Python의 기본 자료형을 반환하면 됩니다. TypeScript SDK에서는 content 배열을 명시적으로 구성해야 합니다.

비교 항목TypeScript SDKPython FastMCP
스키마 정의Zod 스키마타입 힌트 + Pydantic
반환값 형식content 배열 직접 구성자동 변환
비동기 지원기본 (async/await)동기/비동기 모두 지원
코드량상대적으로 많음매우 간결
타입 안전성컴파일 타임 검사런타임 검사
Info

어떤 SDK를 선택할지는 팀의 기술 스택에 따라 결정합니다. Node.js 기반 프로젝트라면 TypeScript SDK가 자연스럽고, Python 기반이라면 FastMCP가 적합합니다. 두 SDK 모두 동일한 MCP 프로토콜을 구현하므로, 어떤 것을 사용하든 동일한 클라이언트와 호환됩니다.

정리

이 장에서는 Python FastMCP를 사용하여 MCP 서버를 구축했습니다.

  • 데코레이터 기반 API: @mcp.tool(), @mcp.resource(), @mcp.prompt()로 간결하게 프리미티브를 등록합니다.
  • 자동 스키마 생성: 타입 힌트와 Pydantic 모델에서 JSON Schema가 자동 생성됩니다.
  • 동기/비동기 지원: 동기 함수와 비동기 함수를 모두 도구로 등록할 수 있습니다.
  • 실전 서버 구축: 파일 관리 서버를 통해 도구, 리소스, 프롬프트의 조합을 실습했습니다.

다음 장 미리보기

7장에서는 MCP 클라이언트 구현을 다룹니다. 클라이언트 측에서 MCP 서버에 연결하고, 도구를 호출하며, LLM과 통합하는 방법을 상세히 살펴보겠습니다. 기존 애플리케이션에 MCP 클라이언트 기능을 추가하여 AI 기반 도구 통합을 구현하는 실전 패턴을 다룹니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#mcp#typescript#python

관련 글

AI / ML

7장: MCP 클라이언트 구현하기

MCP 클라이언트를 직접 구현하여 서버에 연결하고, LLM과 통합하여 도구 호출 파이프라인을 완성하는 방법을 다룹니다.

2026년 2월 2일·15분
AI / ML

5장: TypeScript로 MCP 서버 구축하기

TypeScript SDK를 사용하여 프로젝트 설정부터 도구, 리소스, 프롬프트 구현, 테스트까지 MCP 서버를 구축하는 전 과정을 다룹니다.

2026년 1월 29일·18분
AI / ML

8장: 기존 시스템과 MCP 연동하기

데이터베이스, REST API, 레거시 시스템을 MCP 서버로 래핑하여 AI 모델이 접근할 수 있도록 만드는 실전 패턴을 다룹니다.

2026년 2월 4일·18분
이전 글5장: TypeScript로 MCP 서버 구축하기
다음 글7장: MCP 클라이언트 구현하기

댓글

목차

약 17분 남음
  • FastMCP 소개
  • 첫 번째 FastMCP 서버
  • 도구 구현 상세
    • 타입 힌트 기반 스키마 생성
    • 비동기 도구
    • 도구 이름과 설명 커스터마이징
    • 에러 처리
  • 리소스 구현
    • 정적 리소스
    • 동적 리소스 (리소스 템플릿)
  • 프롬프트 구현
  • 실전 서버: 파일 관리 MCP 서버
  • 전송 방식 설정
  • TypeScript SDK와의 비교
  • 정리
  • 다음 장 미리보기