Skip to content
Published on

Model Context Protocol(MCP) 완전 가이드: AI 도구 표준화와 Claude 통합까지

Authors

들어가며

AI가 단순한 텍스트 생성기를 넘어 실제 도구를 사용하는 에이전트가 되려면, 모델과 외부 시스템 간의 표준화된 통신 방법이 필요합니다. Anthropic이 2024년 11월 발표한 Model Context Protocol(MCP) 은 바로 이 문제를 해결하는 오픈 표준입니다.

MCP는 USB-C가 다양한 기기를 하나의 표준으로 연결하듯, LLM이 어떤 데이터 소스·도구와도 표준 방식으로 통신할 수 있게 합니다. 이 가이드에서는 MCP의 아키텍처부터 실전 서버 구현, Claude Desktop 통합, 그리고 에이전트 시스템 구축까지 완전히 다룹니다.

MCP 아키텍처 개요

JSON-RPC 2.0 기반 통신

MCP는 JSON-RPC 2.0 프로토콜 위에서 동작합니다. 클라이언트가 요청을 보내면 서버가 응답하는 단순한 구조지만, 이 위에 강력한 추상화 계층이 올라갑니다.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "read_file",
    "arguments": {
      "path": "/tmp/example.txt"
    }
  }
}

Hosts / Clients / Servers 삼각 구조

MCP는 세 가지 역할로 구성됩니다:

  • Host: Claude Desktop, VS Code, 커스텀 AI 앱 등 LLM을 구동하는 애플리케이션
  • Client: Host 내부에서 MCP 서버와 1:1 세션을 관리하는 컴포넌트
  • Server: 실제 도구·리소스·프롬프트를 외부에 제공하는 경량 프로세스
[Host: Claude Desktop]
    └── [Client] ──── stdio/SSE ──── [MCP Server: filesystem]
    └── [Client] ──── stdio/SSE ──── [MCP Server: github]
    └── [Client] ──── stdio/SSE ──── [MCP Server: database]

Host는 여러 MCP 서버에 동시에 연결하고, 각 서버에서 제공하는 도구를 LLM에게 노출합니다.

MCP 핵심 원시 타입 (Primitives)

Resources — 읽기 전용 데이터

Resources는 서버가 클라이언트에게 노출하는 읽기 전용 데이터입니다. 파일, DB 레코드, API 응답 등을 표준화된 URI로 접근합니다.

  • URI 형식: file:///home/user/docs/report.pdf, db://mydb/users/42
  • 부수 효과(side effect) 없음 — 순수 데이터 읽기
  • 텍스트 또는 바이너리 콘텐츠 지원

Tools — 함수 실행

Tools는 LLM이 실행할 수 있는 함수입니다. Resources와 달리 부수 효과를 가질 수 있습니다.

  • 파일 생성/수정, API 호출, 데이터베이스 쓰기 등
  • JSON Schema로 입력 파라미터 정의
  • 실행 결과를 텍스트/이미지로 반환

Prompts — 재사용 가능한 템플릿

Prompts는 서버가 제공하는 프롬프트 템플릿입니다. 사용자가 반복적으로 쓰는 패턴을 서버 측에서 표준화합니다.

@mcp.prompt()
def code_review_prompt(language: str, code: str) -> str:
    return f"{language} 코드를 검토하고 개선점을 알려주세요:\n\n{code}"

Sampling — 서버가 LLM 호출

Sampling은 MCP의 독특한 기능으로, 서버가 클라이언트를 통해 LLM을 역방향 호출할 수 있습니다. 서버가 복잡한 처리 중 AI의 판단이 필요할 때 활용합니다.

MCP 서버 구현: Python FastMCP

설치 및 기본 구조

pip install fastmcp

FastMCP는 데코레이터 기반의 간결한 API로 MCP 서버를 만들 수 있습니다.

from fastmcp import FastMCP

mcp = FastMCP("filesystem-server")

@mcp.tool()
def read_file(path: str) -> str:
    """파일 내용을 읽어 반환합니다."""
    with open(path, "r", encoding="utf-8") as f:
        return f.read()

@mcp.tool()
def write_file(path: str, content: str) -> str:
    """파일에 내용을 씁니다."""
    with open(path, "w", encoding="utf-8") as f:
        f.write(content)
    return f"{path} 파일 저장 완료"

@mcp.resource("file://{path}")
def get_file_resource(path: str) -> str:
    """URI를 통해 파일 리소스를 제공합니다."""
    with open(path, "r", encoding="utf-8") as f:
        return f.read()

if __name__ == "__main__":
    mcp.run()

디렉토리 탐색 도구 추가

import os
from pathlib import Path
from fastmcp import FastMCP

mcp = FastMCP("filesystem-server")

@mcp.tool()
def list_directory(path: str = ".") -> list[str]:
    """디렉토리의 파일 목록을 반환합니다."""
    p = Path(path)
    if not p.is_dir():
        raise ValueError(f"{path}는 디렉토리가 아닙니다")
    return [str(item) for item in p.iterdir()]

@mcp.tool()
def search_files(directory: str, pattern: str) -> list[str]:
    """디렉토리에서 패턴에 맞는 파일을 검색합니다."""
    p = Path(directory)
    return [str(f) for f in p.rglob(pattern)]

if __name__ == "__main__":
    mcp.run(transport="stdio")

MCP 서버 구현: TypeScript SDK

TypeScript로 데이터베이스 리소스를 제공하는 MCP 서버를 구현합니다.

import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'

const server = new McpServer({
  name: 'database-server',
  version: '1.0.0',
})

// 리소스: DB 레코드를 URI로 노출
server.resource(
  'user',
  new ResourceTemplate('db://users/{id}', { list: undefined }),
  async (uri, { id }) => {
    const user = await fetchUserById(id)
    return {
      contents: [
        {
          uri: uri.href,
          text: JSON.stringify(user, null, 2),
          mimeType: 'application/json',
        },
      ],
    }
  }
)

// 도구: SQL 쿼리 실행
server.tool('query_db', { sql: z.string().describe('실행할 SQL 쿼리') }, async ({ sql }) => {
  const results = await executeQuery(sql)
  return {
    content: [
      {
        type: 'text',
        text: JSON.stringify(results, null, 2),
      },
    ],
  }
})

async function main() {
  const transport = new StdioServerTransport()
  await server.connect(transport)
}

main()

stdio vs SSE 전송 방식

stdio 전송

로컬 프로세스 간 통신에 적합합니다. Claude Desktop처럼 서버를 자식 프로세스로 실행할 때 사용합니다.

  • 장점: 간단한 설정, 추가 네트워크 불필요, 보안(로컬 전용)
  • 단점: 원격 서버 불가, 단일 클라이언트만 지원
  • 사용 케이스: Claude Desktop, 로컬 개발 환경, CI 파이프라인

SSE (Server-Sent Events) 전송

HTTP 기반 원격 통신에 적합합니다. 여러 클라이언트가 하나의 서버에 연결할 때 사용합니다.

  • 장점: 원격 접근, 다중 클라이언트, 웹 기반 통합
  • 단점: HTTP 서버 설정 필요, 인증 구현 필요
  • 사용 케이스: 팀 공유 MCP 서버, 클라우드 배포, 다중 사용자 환경
# SSE 서버 실행 예시
if __name__ == "__main__":
    mcp.run(transport="sse", host="0.0.0.0", port=8000)

Claude Desktop 통합

claude_desktop_config.json 설정

macOS 기준 ~/Library/Application Support/Claude/claude_desktop_config.json에 설정합니다.

{
  "mcpServers": {
    "filesystem": {
      "command": "python",
      "args": ["/path/to/filesystem_server.py"],
      "env": {
        "ALLOWED_DIRS": "/home/user/documents,/tmp"
      }
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here"
      }
    },
    "sqlite": {
      "command": "uvx",
      "args": ["mcp-server-sqlite", "--db-path", "/path/to/database.db"]
    }
  }
}

로컬 서버 실행 확인

Claude Desktop을 재시작하면 연결된 서버의 도구가 자동으로 사용 가능해집니다. 채팅창 하단의 도구 아이콘에서 사용 가능한 MCP 도구 목록을 확인할 수 있습니다.

MCP 클라이언트 구현

서버뿐 아니라 직접 MCP 클라이언트를 만들어 서버에 연결할 수 있습니다.

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    server_params = StdioServerParameters(
        command="python",
        args=["filesystem_server.py"],
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # 초기화
            await session.initialize()

            # 사용 가능한 도구 목록
            tools = await session.list_tools()
            print("도구 목록:", [t.name for t in tools.tools])

            # 도구 호출
            result = await session.call_tool(
                "read_file",
                arguments={"path": "/tmp/test.txt"}
            )
            print("결과:", result.content[0].text)

asyncio.run(main())

에이전트 시스템: LangGraph + MCP

LangGraph와 MCP를 연동하면 MCP 서버의 도구를 LangGraph 에이전트가 직접 사용할 수 있습니다.

from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import asyncio

async def create_mcp_agent():
    model = ChatAnthropic(model="claude-3-5-sonnet-20241022")

    server_params = StdioServerParameters(
        command="python",
        args=["filesystem_server.py"],
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # MCP 도구를 LangChain 도구로 변환
            tools = await load_mcp_tools(session)

            # ReAct 에이전트 생성
            agent = create_react_agent(model, tools)

            result = await agent.ainvoke({
                "messages": [{"role": "user", "content": "/tmp 디렉토리의 파일 목록을 보여주세요"}]
            })
            return result

asyncio.run(create_mcp_agent())

사용자 승인이 필요한 위험한 도구

민감한 작업은 사용자 확인을 요청하는 패턴을 구현합니다.

from fastmcp import FastMCP
from fastmcp.exceptions import ToolError

mcp = FastMCP("safe-server")

DANGEROUS_PATHS = ["/etc", "/sys", "/proc", "/root"]

@mcp.tool()
def delete_file(path: str, confirmed: bool = False) -> str:
    """
    파일을 삭제합니다. 삭제 전 반드시 confirmed=True를 명시해야 합니다.

    Args:
        path: 삭제할 파일 경로
        confirmed: 삭제 의사를 명시적으로 확인 (기본값: False)
    """
    import os
    from pathlib import Path

    abs_path = str(Path(path).resolve())

    # 위험 경로 차단
    for danger in DANGEROUS_PATHS:
        if abs_path.startswith(danger):
            raise ToolError(f"{danger} 하위 경로는 삭제할 수 없습니다")

    # 확인 없이 호출된 경우 사용자에게 확인 요청
    if not confirmed:
        return (
            f"경고: {abs_path} 파일을 영구 삭제하려고 합니다. "
            f"confirmed=True를 설정하여 다시 호출하면 삭제됩니다."
        )

    os.remove(abs_path)
    return f"{abs_path} 삭제 완료"

보안 고려사항

권한 모델

MCP 서버는 최소 권한 원칙을 따라야 합니다.

  • 필요한 디렉토리·DB에만 접근 허용
  • 환경 변수로 허용 경로 목록 관리
  • 파일시스템 트래버설 공격 방어 (Path traversal)
import os
from pathlib import Path

ALLOWED_DIRS = os.getenv("ALLOWED_DIRS", "/tmp").split(",")

def is_path_allowed(path: str) -> bool:
    abs_path = Path(path).resolve()
    return any(
        str(abs_path).startswith(allowed.strip())
        for allowed in ALLOWED_DIRS
    )

민감 데이터 처리

  • API 키·비밀번호는 환경 변수로 주입, 코드에 하드코딩 금지
  • 로그에 민감한 응답 내용 기록 금지
  • TLS 없이 원격 MCP 서버 운영 금지

Sandboxing

프로덕션 환경에서는 MCP 서버를 컨테이너나 가상 환경으로 격리합니다.

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
# 루트가 아닌 전용 사용자로 실행
RUN useradd -m mcpuser
USER mcpuser
CMD ["python", "server.py"]

Tool Call 검증

LLM이 생성한 tool call 파라미터를 서버 측에서 반드시 재검증해야 합니다. LLM 프롬프트 인젝션 공격으로 악의적인 파라미터가 주입될 수 있습니다.

from pydantic import BaseModel, validator

class FileReadParams(BaseModel):
    path: str

    @validator("path")
    def validate_path(cls, v):
        p = Path(v).resolve()
        if ".." in str(p):
            raise ValueError("경로 트래버설 시도 감지")
        if not is_path_allowed(str(p)):
            raise ValueError("허용되지 않은 경로입니다")
        return str(p)

MCP vs 기존 function calling

항목OpenAI Function CallingMCP
표준화벤더 종속오픈 표준
재사용성모델별 재구현 필요서버 한 번 구현으로 모든 호환 모델 지원
데이터 접근없음 (도구만)Resources로 데이터 직접 노출
역방향 LLM 호출없음Sampling으로 서버가 LLM 호출 가능
전송 방식HTTP만stdio, SSE 지원

퀴즈

Q1. MCP에서 Resources와 Tools의 핵심 차이는 무엇인가요?

정답: Resources는 읽기 전용 데이터 접근이고, Tools는 부수 효과를 포함하는 함수 실행입니다.

설명: Resources는 file://, db:// 같은 URI를 통해 데이터를 읽어오는 역할로, 시스템 상태를 변경하지 않습니다. Tools는 파일 쓰기, API 호출, DB 쓰기처럼 시스템 상태를 변경할 수 있는 작업을 수행합니다. LLM이 데이터를 단순히 읽을 때는 Resource를, 무언가를 실행하거나 변경할 때는 Tool을 사용합니다.

Q2. MCP가 OpenAI function calling보다 도구 표준화에 유리한 이유는?

정답: MCP는 벤더 중립 오픈 표준으로, 한 번 구현한 MCP 서버를 Claude, GPT, 오픈소스 모델 등 모든 호환 모델에서 재사용할 수 있습니다.

설명: OpenAI function calling은 OpenAI API 형식에 종속되어, 다른 모델로 전환하면 도구를 전부 재구현해야 합니다. MCP 서버는 프로토콜 수준에서 표준화되어 있어, 모델과 완전히 분리된 독립 서비스로 운영됩니다. 또한 Resources로 데이터 노출, Prompts로 템플릿 공유 등 function calling에 없는 기능도 제공합니다.

Q3. stdio와 SSE 전송 방식은 각각 어떤 상황에 적합한가요?

정답: stdio는 로컬 단일 클라이언트 환경(Claude Desktop, 개인 개발)에, SSE는 원격·다중 클라이언트 환경(팀 공유 서버, 클라우드 배포)에 적합합니다.

설명: stdio는 부모 프로세스가 자식 프로세스를 직접 실행하므로 네트워크 설정이 불필요하고 보안이 단순합니다. SSE는 HTTP 서버를 띄워 여러 클라이언트가 동시 접속할 수 있으며, 클라우드에 배포하거나 여러 팀원이 공유할 때 유리합니다. 단, SSE를 사용할 경우 인증·TLS 설정이 추가로 필요합니다.

Q4. MCP Sampling primitive의 동작 방식과 보안 고려사항은?

정답: Sampling은 MCP 서버가 클라이언트(Host)를 통해 LLM을 역방향으로 호출할 수 있는 기능으로, 사용자 승인 및 프롬프트 검토 후에만 실행되어야 합니다.

설명: 일반적인 흐름은 LLM이 서버 도구를 호출하지만, Sampling은 반대로 서버가 LLM에게 "이 텍스트를 분석해 줘"라고 요청하는 구조입니다. Host는 이 요청을 중간에서 검토하고 사용자 승인 후에만 LLM으로 전달해야 합니다. 악의적인 서버가 Sampling을 통해 무제한 LLM을 호출하거나 민감한 내용을 추출하는 것을 방지해야 합니다.

Q5. MCP 서버에서 위험한 도구에 대한 사용자 승인 워크플로우는 어떻게 구현하나요?

정답: confirmed 파라미터 패턴을 사용하여 첫 호출 시 경고를 반환하고, 사용자가 명시적으로 confirmed=True를 설정한 두 번째 호출에서만 실제 작업을 수행합니다.

설명: LLM은 Tool의 설명(description)을 읽고 어떻게 호출할지 결정합니다. confirmed=False인 첫 호출에서 "이 작업을 수행하려 합니다, confirmed=True로 재호출하세요"라는 명확한 경고를 반환하면, LLM은 사용자에게 확인을 요청하게 됩니다. 사용자가 승인하면 LLM이 confirmed=True로 재호출하여 실제 작업이 실행됩니다. 이 패턴은 위험 경로 차단, 권한 검사와 함께 사용해야 합니다.

마무리

MCP는 AI 에이전트 생태계의 USB-C입니다. 한 번 표준을 정의하면 어떤 모델, 어떤 클라이언트와도 연결할 수 있습니다. Anthropic이 오픈 표준으로 공개한 덕분에 이미 수백 개의 MCP 서버가 커뮤니티에서 개발되고 있으며, VS Code, Cursor, JetBrains 등 주요 개발 도구들도 MCP를 지원하기 시작했습니다.

Python FastMCP로 5분 만에 첫 서버를 만들어 Claude Desktop에 연결해 보세요. AI가 여러분의 파일시스템, 데이터베이스, API를 직접 다루는 경험이 얼마나 강력한지 바로 느낄 수 있습니다.