[MCP] 2. 서버 : MCP 서버 만드는 법

MCP 직접 만들기 2편 - MCP 서버 만드는 법

작성자

카테고리:

* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

(MCP 시리즈 ②) 서버

개념은 개념이고, 직접 만들어 봐야 감이 온다. 파이썬 MCP SDK를 쓰면 도구 하나 노출하는 서버는 정말 몇 줄이면 된다. pip install mcp 하고 아래를 server.py로 저장해 보자.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather")

@mcp.tool()
def get_forecast(city: str) -> str:
    """도시의 오늘 날씨를 반환한다.
    사용자가 특정 도시의 날씨를 물을 때 호출한다."""
    # 실제로는 여기서 외부 날씨 API를 부른다
    return f"{city}: 맑음, 26도"

if __name__ == "__main__":
    mcp.run()   # 별다른 인자가 없으면 stdio 전송

핵심은 @mcp.tool() 데코레이터 하나다. 이걸 붙인 함수가 곧 모델이 호출할 수 있는 도구가 된다. 그런데 여기서 초보가 놓치는 게 있다. 함수의 독스트링(docstring)이 그냥 주석이 아니라 도구 설명서라는 점이다. 모델은 이 설명을 읽고 “언제 이 도구를 써야 할지”를 판단한다. 그래서 위 예시처럼 “~할 때 호출한다”까지 적어주는 게 좋다. 설명이 부실하면 모델이 엉뚱한 타이밍에 부르거나 아예 안 부른다.

타입 힌트(city: str)도 장식이 아니다. 이게 자동으로 입력 스키마가 돼서, 모델이 어떤 인자를 어떤 형식으로 넣어야 하는지 안다. 파이썬 타입만 제대로 달아주면 스키마를 손으로 쓸 일이 없다.

여기서 테스트 얘기를 좀 하자. MCP 서버 테스트는 두 층으로 나눠 보면 편하다. 하나는 도구가 하는 일 자체가 맞는지(로직), 다른 하나는 그게 MCP로 제대로 오가는지(연결).

로직은 굳이 MCP를 거칠 필요가 없다. 그래서 나는 실제 일하는 부분을 순수 함수로 빼두고, 도구는 그걸 감싸기만 하게 만든다. 그러면 pytest로 바로 찌를 수 있다.

# server.py — 로직을 순수 함수로 빼둔다
def forecast_logic(city: str) -> str:
    return f"{city}: 맑음, 26도"

@mcp.tool()
def get_forecast(city: str) -> str:
    """도시의 오늘 날씨를 반환한다."""
    return forecast_logic(city)

# test_server.py — MCP 없이 로직만 검증
def test_forecast():
    assert "26도" in forecast_logic("서울")

여러 도구와 느린 호출 다루기

도구는 @mcp.tool()을 여러 개 붙이면 된다. 외부 API 키는 환경변수로 읽고(5편), 느린 외부 호출은 async def로 만들면 그 사이에도 서버가 막히지 않는다. DB 커넥션 같은 자원은 서버 시작 때 한 번 열어 재사용한다.

import os, httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
KEY = os.environ["WEATHER_KEY"]           # 키는 환경변수로

@mcp.tool()
async def get_forecast(city: str) -> dict:
    """도시의 오늘 날씨를 반환한다. 날씨를 물을 때 호출."""
    async with httpx.AsyncClient() as c:
        r = await c.get(f"https://api.example.com/{city}",
                        headers={"Authorization": f"Bearer {KEY}"})
    return r.json()

오가는 것의 실체 — JSON-RPC 한 쌍

인스펙터에서 도구를 호출하면 이런 메시지가 오간다. MCP의 실체는 결국 이 JSON-RPC다 — 이걸 한 번 눈으로 보면 ‘프로토콜’이라는 말의 신비가 걷힌다.

→ 요청 (클라이언트 → 서버)
{"jsonrpc":"2.0","id":7,"method":"tools/call",
 "params":{"name":"get_forecast","arguments":{"city":"Seoul"}}}

← 응답 (서버 → 클라이언트)
{"jsonrpc":"2.0","id":7,
 "result":{"content":[{"type":"text","text":"맑음, 31도"}]}}

에러도 같은 규격으로 돌아온다(result.isError + 본문). 그래서 도구 정의 편에서 말한 “에러 메시지도 모델이 읽는 컨텍스트”가 프로토콜 수준에서 성립한다.

stdio vs 원격, 그리고 다른 언어

위 예제는 stdio(로컬 프로세스)다. 같은 서버를 원격(HTTP)으로도 띄워 팀이 공유하는 서비스로 만들 수 있다(3편). 그리고 MCP는 언어 중립이라 파이썬 말고 타입스크립트 등 다른 SDK도 있다 — 개념(도구·리소스·프롬프트)은 같고 문법만 다르다. 몇 줄로 시작해 점점 키워가면 된다.

연결 층은 인스펙터로 확인한다. mcp dev server.py를 띄우면(파이썬 외 언어는 npx @modelcontextprotocol/inspector) 브라우저에 UI가 뜬다. 도구 목록을 보고 입력을 채워 직접 호출하면서, 오가는 JSON-RPC 메시지를 눈으로 확인할 수 있다. 빈 입력이나 에러 같은 경계 상황도 여기서 미리 찔러본다. 클라이언트에 붙이기 전에 이 단계를 거치는 습관을 들이면 삽질이 확 준다. 다음 편에선 이 서버를 Claude에 실제로 물려본다.

자주 묻는 질문 (FAQ)

MCP 서버는 어떤 언어로 만드나요?

공식 SDK가 잘 갖춰진 Python·TypeScript가 흔합니다. 프로토콜만 지키면 언어는 자유이니 익숙한 걸 쓰세요.

로컬에서만 되나요, 원격도 되나요?

둘 다 됩니다. stdio(로컬)와 HTTP/SSE(원격) 전송을 모두 지원하므로 배포 상황에 맞게 고르면 됩니다.

코멘트

답글 남기기

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