MCP 제작하기 기초

MCP(Model Context Protocol)는 한 가지 일을 아주 잘 해내는 오픈 표준입니다. 바로 LLM이 외부 툴을 발견하고 사용하는 방식을 ‘깔끔하고 일관되게’ 통일해 주는 것입니다. 더 이상 제멋대로인 파싱 로직도, 깨지기 쉬운 연동 코드도 필요 없습니다. 그냥 작동하는 프로토콜만 있으면 됩니다.

개인적으로 복잡하게 설명하는 거 좋아하지 않습니다. 대부분의 MCP 설명글을 보면 JSON-RPC 명세서니 전송 계층(Transport layer)이니 하는 이야기부터 시작합니다. 순서가 완전히 거꾸로 됐다고 볼 수 있습니다.

웹 앱을 만들 때 HTTP 프로토콜 내부를 낱낱이 알 필요가 없듯이, MCP를 쓰기 위해 프로토콜 내부 구조를 다 알 필요는 없습니다.

당장 필요한 건 딱 세 가지 개념30분의 시간뿐입니다.

세 가지 핵심 블럭

MCP는 딱 세 가지 블록으로 구성됩니다. 진짜, 그게 다입니다.

  1. 서버 (Server) — 여러분의 툴(도구)을 진열해 놓는 곳입니다. 파이썬 스크립트로 “자, 여기 LLM이 가져다 쓸 수 있는 함수들이 있어요~”라고 선언하는 역할이라고 볼 수 있습니다. 실행해 두면 요청이 들어오길 기다립니다.
  2. 툴 (Tool)LLM이 사용했으면 하는 실제 함수입니다. 날씨 조회, 데이터베이스 쿼리, 이메일 전송 등 뭐든 가능합니다. 평범한 파이썬 함수처럼 작성하고, 위에 ‘데코레이터’ 하나만 딱 붙여주면 나머지는 MCP가 알아서 처리합니다.
  3. 클라이언트 (Client) — 서버에 접속해서 툴을 호출하는 녀석입니다. 실제 서비스(Production) 환경에서는 여러분이 만든 LLM 애플리케이션이 이 역할을 합니다. 테스트할 때는? FastMCP가 별도 설정 없이 바로 쓸 수 있는 클라이언트를 제공해 줍니다.

“서버는 툴을 내놓고, 클라이언트는 툴을 쓴다.” 이게 머릿속에 넣어야 할 개념의 전부입니다.

나머지 잡다한 것들—전송 계층(Transports), JSON-RPC, 기능 협상(Capability negotiation)—은 전부 ‘구현 디테일‘ 일 뿐입니다. 나중에 서비스 규모를 키워서 배포할 때나 고민하면 됩니다.

1 단계 : FastMCP 설치하기

FastMCP는 MCP를 아주 심플하게 만들어주는 파이썬 프레임워크입니다. 설치 한 번이면 끝입니다. 골치 아픈 설정? 그런 거 없습니다.

pip install fastmcp

2 단계: 서버 생성하기

my_server.py 를 하나 생성하기:

from fastmcp import FastMCP

# Initialize the server with a name
mcp = FastMCP("my-first-server")

# Define a tool using the @mcp.tool decorator
@mcp.tool
def get_weather(city: str) -> dict:
"""Get the current weather for a city."""
# In production, you'd call a real weather API
# For now, we'll return mock data
weather_data = {
"new york": {"temp": 72, "condition": "sunny"},
"london": {"temp": 59, "condition": "cloudy"},
"seoul": {"temp": 68, "condition": "rainy"},
}

city_lower = city.lower()
if city_lower in weather_data:
return {"city": city, **weather_data[city_lower]}
else:
return {"city": city, "temp": 70, "condition": "unknown"}

# Run the server
if __name__ == "__main__":
mcp.run(transport="stdio")

자, 방금 무슨 일이 일어난 건지 하나씩 뜯어볼까요?

  • FastMCP("my-first-server"): 이 한 줄로 이름을 가진 서버가 뚝딱 만들어집니다.
  • @mcp.tool: 평범한 함수를 MCP 툴로 변신시켜 주는 데코레이터입니다.
  • Docstring (함수 설명): 이게 곧 툴의 ‘설명서’가 됩니다. (LLM은 이 설명을 읽고 “아, 이럴 때 호출하는 거구나!” 하고 판단하죠.)
  • Type hints (city: str, -> dict): MCP에게 어떤 입력값이 들어가고, 어떤 결과값이 나오는지 명확히 알려줍니다.
  • transport="stdio": 서버가 표준 입출력(콘솔)을 통해 소통한다는 뜻입니다. 로컬에서 테스트할 때 아주 완벽한 방식이죠.

이게 서버의 전부입니다. 실제 코드는 고작 15줄밖에 안 됩니다.

3단계: 클라이언트 생성하고 테스트하기

test_client.py 파일 생성하기:

import asyncio
from fastmcp import Client

async def main():
# Point the client at your server file
client = Client("my_server.py")

# Connect to the server
async with client:
# List available tools
tools = await client.list_tools()
print("Available tools:")
for tool in tools:
print(f" - {tool.name}: {tool.description}")

print("\n" + "="*50 + "\n")

# Call the weather tool
result = await client.call_tool(
"get_weather",
{"city": "seoul"}
)
print(f"Weather result: {result}")

if __name__ == "__main__":
asyncio.run(main())

이어서 핵심 포인트를 짚어보겠습니다.

  • Client("my_server.py"): 클라이언트에게 “이 파일에 있는 서버랑 연결해!”라고 알려줍니다.
  • async with client:: 연결을 열고 닫는 복잡한 과정을 자동으로 처리해 줍니다.
  • list_tools(): 현재 사용할 수 있는 툴이 무엇인지 찾아냅니다. (이게 바로 MCP의 핵심 기능인 **’동적 탐색(Dynamic Discovery)’**입니다.)
  • call_tool("get_weather", {"city": "Tokyo"}): 툴 이름과 필요한 파라미터를 넘겨서 실제로 실행합니다.

4단계: 실행하기

터미널 열고 실행:

python test_client.py

아래와 같은 결과…:

Available tools:
- get_weather: Get the current weather for a city.
==================================================Weather result: {'city': 'seoul', 'temp': 68, 'condition': 'rainy'}

끝입니다. 방금 여러분은 MCP 서버를 하나 만들고, 클라이언트에서 호출까지 해냈습니다.

5단계 : 툴 을 더 추가하기

MCP의 진짜 위력은 기능을 확장하는 게 너무나 쉽다는 데 있습니다. 자, 우리 서버에 툴 두 개를 더 추가해 봅시다.

from fastmcp import FastMCP
from datetime import datetime

mcp = FastMCP("my-first-server")

@mcp.tool
def get_weather(city: str) -> dict:
"""Get the current weather for a city."""
weather_data = {
"new york": {"temp": 72, "condition": "sunny"},
"london": {"temp": 59, "condition": "cloudy"},
"seoul": {"temp": 68, "condition": "rainy"},
}
city_lower = city.lower()
if city_lower in weather_data:
return {"city": city, **weather_data[city_lower]}
return {"city": city, "temp": 70, "condition": "unknown"}

@mcp.tool
def get_time(timezone: str = "UTC") -> str:
"""Get the current time in a specified timezone."""
# Simplified - in production use pytz or zoneinfo
return f"Current time ({timezone}): {datetime.now().strftime('%H:%M:%S')}"

@mcp.tool
def calculate(expression: str) -> dict:
"""Safely evaluate a mathematical expression."""
try:
# Only allow safe math operations
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return {"error": "Invalid characters in expression"}

result = eval(expression) # Safe because we validated input
return {"expression": expression, "result": result}
except Exception as e:
return {"error": str(e)}
if __name__ == "__main__":
mcp.run(transport="stdio")

테스트 클라이언트를 다시 실행해 보세요. 알아서 3개의 툴을 모두 찾아낼 겁니다.

Available tools:
- get_weather: Get the current weather for a city.
- get_time: Get the current time in a specified timezone.
- calculate: Safely evaluate a mathematical expression.

설정을 바꿀 필요도, 복잡한 라우팅 로직을 짤 필요도 없습니다. 그저 툴만 추가했을 뿐인데, MCP가 알아서 바로 쓸 수 있게 대령해 준 겁니다.

다음 단계: LLM과 연결하기

방금 우리가 만든 클라이언트는 테스트용이었습니다. 실제 서비스 환경(Production)에서는 여러분의 LLM 프레임워크가 직접 클라이언트가 되어 서버에 붙게 됩니다.

개념적으로는 이런 모습입니다:

MCP가 LLM과 외부 툴을 연결하는 원리는 이렇습니다. 프레임워크가 클라이언트를 호출하면, 그 클라이언트가 서버에 있는 툴들을 찾아내고(Discover) 실행(Invoke)하는 구조

우리가 짠 서버 코드요? 하나도 안 바꿔도 됩니다. 그게 바로 MCP의 존재 이유니까요. 툴을 딱 한 번만 만들어 두면, MCP와 호환되는 그 어떤 클라이언트든 마음대로 가져다 쓸 수 있습니다.

실제 서비스(Production)에 배포할 때는 전송 방식(Transport)만 stdio에서 **http**로 살짝 바꿔주면 됩니다.

if __name__ == "__main__":
mcp.run(transport="http", host="0.0.0.0", port=8000)

이렇게 하면 여러분의 MCP 서버가 HTTP 엔드포인트로 공개됩니다. 이제 멀리 떨어져 있는 원격 클라이언트들도 얼마든지 접속할 수 있게 되는 거죠.

위대한 추상화의 힘

MCP는 마법이 아닙니다. 이것은 ‘아주 잘 설계된 배관(Plumbing)’입니다. 탐색, 라우팅, 데이터 직렬화 같은 지루하고 반복적인 하수도 공사는 MCP가 바닥 아래에서 모두 처리합니다. 덕분에 우리는 바닥 위에서 진짜 중요한 것, 즉 ‘어떤 기능을 제공할 것인가‘ 에만 집중할 수 있게 됩니다.

오늘의 교훈을 한 문장으로 요약하며 마치겠습니다.

훌륭한 추상화는 어려운 일을 소리 없이 지워버린다.

이제 30분을 투자해 보세요. 이전에 “아, 내 AI가 이것도 할 줄 알면 좋겠는데“라고 생각했던 바로 그 기능을 지금 만들어 불 수 있습니다.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다