데이터 분석/LLM

MCP Python SDK로 로컬 MCP 서버 구축하기

woojc 2025. 5. 10. 10:10
반응형

MCP란?

Model Context Protocol로, 애플리케이션이 LLM에 컨텍스트를 제공하는 방법을 표준화하는 개방형 프로토콜이다. ( 모델 컨텍스트 프로토콜 (MCP) - Anthropic, Claude 모델로 유명한 Anthropic에서 표준을 제시하였다.) 이렇게만 설명하면 어려울 수 있는데, 쉽게 말하면 "툴"을 만들어서 "서버"에 등록해놓고, LLM이 서버에 접속하여 필요한 툴을 선택하고 사용하는 것을 "표준화"한 것이다. LangGraph를 써 본 사람이라면 `create_react_agent`와 유사하다는 것을 파악할 수 있을 것이다.

https://modelcontextprotocol.io/introduction

 

 

MCP 서버를 쓰는 이유는?

앞서 말했던 `create_react_agent`와 비슷하다면 왜 MCP 서버를 구축해서 쓰는 걸까? 그 이유는 MCP가 "표준화"되어있다는 데 있다. LangGraph에서 툴을 등록하기 위해서는 프롬프트 엔지니어링과 함수에 docstring을 넣어주어야 하는데, 사람마다 쓰는 방식이 다르고 docstring의 포맷도 달라질 수 있다. 하지만 MCP에는 JSON 기반 스키마로 설명을 작성하여 "표준"처럼 쓸 수 있다. 또, 새로운 툴을 추가하는 확장성에 있어서도 JSON 형태이기 때문에 쉽게 등록할 수 있다는 장점이 있다. 비슷한 이유로 코드 기반이 아닌 JSON 형태의 구조이기 때문에 툴 관리가 편하다는 장점도 있다.

 

 

MCP Python SDK

그럼 MCP 서버는 어떻게 구축할 수 있을까? 서버에는 대표적으로 Github MCP, Slack MCP, PostgreSQL MCP 등이 있으며, 구축하기 위한 툴에는 매우 다양한 것들이 있다. 

https://github.com/punkpeye/awesome-mcp-servers/blob/main/README-ko.md

MCP SDK를 이용하기로 했다.

https://github.com/modelcontextprotocol/python-sdk

FastMCP가 유명하긴 하지만 Claude Desktop App이 있어야 작동하는 것으로 보인다. 어떻게든 써먹어보려 했지만 실패했다.

 

 

설치 및 서버 구축

먼저 `uv`라는 것을 먼저 설치해야 한다.

uv

uv라는 것을 MCP 프레임워크를 설치하면서 처음 알게 되었는데, `pip`보다 패키지를 더 빠르게 설치할 수 있는 거라고 한다.

 

난 윈도우를 쓰고 있기 때문에 아래 코드를 cmd에서 실행했다.

`powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`

 

`uv` 설치 이후, 서버를 구축할 폴더에서 `uv init mcp-server`를 입력한다. 그러면 `mcp-server`라는 폴더가 만들어지면서 그 안에 기본적인 파일 구조가 만들어진다.

 

[project]
name = "mcp-server"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []

 

다음으로 `cd mcp-server`로 폴더 이동 후 `uv add "mcp[cli]"`로 mcp를 설치한다.

 

이후 `server_stdio.py` 파일을 생성하고 예시처럼 작성한다.

from mcp.server.fastmcp import FastMCP

# Create an MCP server
mcp = FastMCP("Demo")


# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b


# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
    """Get a personalized greeting"""
    return f"Hello, {name}!"

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

 

또, `server_sse.py` 파일도 생성하자. 마지막 부분만 `transport="sse"`로 변경해준 코드이다.

`transport`에는 `stdio`와 `sse` 두 가지 방식이 있다. `stdio`는 클라이언트가 서버를 실행하는 것으로 보통 Cursor IDE나 Claude Desktop APP에 등록해서 사용하는 것으로 알고 있고, `sse`가 우리가 하고자 하는 네트워크로 서버를 띄우는 것이다.

 

 

`client.py` 파일을 생성하자.

import asyncio
import sys
from urllib.parse import urlparse

from mcp import ClientSession
from mcp.client.stdio import stdio_client
from mcp.client.sse import sse_client
from mcp import StdioServerParameters

USE_STDIO = True  # True: stdio 모드 / False: SSE 모드

async def run_stdio():
    server_params = StdioServerParameters(
        command="mcp",
        args=["run", "server_stdio.py"]
    )
    async with stdio_client(server_params) as (reader, writer):
        async with ClientSession(reader, writer) as session:
            await session.initialize()
            await interact(session)

async def run_sse(server_url: str):
    if urlparse(server_url).scheme not in ("http", "https"):
        print("Error: Server URL must start with http:// or https://")
        sys.exit(1)

    try:
        async with sse_client(server_url) as (reader, writer):
            async with ClientSession(reader, writer) as session:
                await session.initialize()
                await interact(session)
    except Exception as e:
        print(f"Error connecting to server: {e}")
        sys.exit(1)

async def interact(session: ClientSession):
    # 도구 호출
    add_result = await session.call_tool("add", arguments={"a": 10, "b": 15})
    print("Add result:", add_result.content[0].text)

    # 리소스 호출
    greeting_result = await session.read_resource("greeting://Bob")
    print("Greeting:", greeting_result.contents[0].text)

if __name__ == "__main__":
    if USE_STDIO:
        asyncio.run(run_stdio())
    else:
        if len(sys.argv) != 2:
            sys.exit(1)
        asyncio.run(run_sse(sys.argv[1]))

참고: https://github.com/slavashvets/mcp-http-client-example/blob/main/main.py

 

 

핵심은 `interact` 함수인데, 툴은 `call_tool`을 하고 리소스는 `read_resource`를 해야 한다.

 

먼저 `stdio`를 테스트 하기 위해 `python client.py`를 하면 다음과 같은 결과를 얻을 수 있다.

 

 

그리고 `sse`를 테스트 하기 전에 먼저 `python server_sse.py` 또는 `uv run server_sse.py`로 서버를 띄워준다.

이렇게 로그가 나오면 잘 작동된 것이다.

`client.py`에서 `USE_STDIO = False`로 수정한 후 `python client.py http://localhost:8000/sse`를 하면 아까 `stdio`의 결과와 동일하게 얻을 수 있는 것을 확인할 수 있다.

 

 

MCP 활용하기

요즘 뜨고 있는 CURSOR IDE에서 MCP를 등록해주자.

먼저 Cursor Settings - MCP에서 'Add new gloabl MCP server' 버튼을 클릭해준다.

그럼 mcp.json 파일이 생성되는데, 여기에 다음과 같이 작성한다.

{
  "mcpServers": {
    "StdioServer": {
      "command": "uv",
      "args": [
        "--directory",
        "D:/mcp-server",
        "run",
        "server_stdio.py"
      ]
    },
    "SSEServer": {
      "url": "http://localhost:8000/sse"
    }
  }
}

 

`StdioServer`는 파일의 경로만 잘 작성해주면 되고, `SSEServer`는 미리 서버를 띄워놓아야만 작동한다.

그럼 위와 같이 초록색 표시가 뜨면서 사용 가능한 툴이 표시된다.

 

만약 StdioServer 사용 시 아나콘다 가상환경을 지정이 필요하면 아래와 같이 작성해도 된다.

{
  "mcpServers": {
    "StdioServer":{
      "command": "D:/conda/mcp/python.exe",
      "args": [
        "D:/mcp-server/server_stdio.py"
      ]
    }
  }
}

 

 

LLM을 통해 MCP 서버 활용하기

Ctrl+Shift+P로 'Open Chat in Agent Mode'를 클릭하여 대화하면 LLM이 툴을 선택하는 것을 볼 수 있다.

이전 Agent tool 활용에서 `duckduckgo_search`를 이용하여 검색하는 걸 만들어놨었는데 이를 MCP 서버에 심어주었다.

2025.04.11 - [데이터 분석/LLM] - LangGraph로 만든 Agent의 응답을 Streaming으로 받기

Run tool 버튼을 클릭해주면 MCP 툴을 이용하여 기사를 검색하고 그 결과를 정리해준다.

다만 docstring을 통해 queries가 list라고 설명을 줬는데도 불구하고 string으로 던진다. LangGraph와 달리 Cursor AI에서는 docstring을 안 읽는지도 모르겠다.

 

참고

For Server Developers - Model Context Protocol

MCP Server 개발 - Python - [루닥스 블로그] 연습만이 살길이다

728x90
반응형