Flama를 사용하여 MCP 서버 구축하기
요약
Flama를 사용하여 Model Context Protocol(MCP) 서버를 구축하는 방법을 설명합니다. Python 데코레이터를 활용해 AI 에이전트가 사용할 수 있는 도구, 리소스, 프롬프트를 손쉽게 노출하는 과정을 다룹니다.
핵심 포인트
- MCP는 AI 애플리케이션과 외부 기능을 연결하는 개방형 표준입니다.
- Flama를 사용하면 Python 함수에 데코레이터를 추가하여 MCP 서버를 빠르게 구축할 수 있습니다.
- MCP 서버는 도구(Tools), 리소스(Resources), 프롬프트(Prompts)를 제공합니다.
- Flama는 stateless 방식을 지원하여 MCP 서버의 수평적 확장이 용이합니다.
ML 모델을 서빙하는 것은 이야기의 절반에 불과합니다. 나머지 절반은 AI 에이전트에게 당신의 세계에 대한 접근 권한을 부여하는 것입니다. 즉, 에이전트가 호출할 수 있는 함수, 읽을 수 있는 데이터, 그리고 재사용할 수 있는 프롬프트 템플릿(prompt templates)을 제공하는 것입니다. Model Context Protocol (MCP)은 바로 이를 위한 개방형 표준이며, Flama는 일반 Python 함수에 몇 가지 데코레이터(decorators)를 추가하는 것만으로도 MCP 서버를 구축할 수 있도록 네이티브하고 일류(first-class)의 지원을 제공합니다.
이 포스트에서는 Flama를 사용하여 완전한 MCP 서버를 구축하는 과정을 살펴봅니다. 우리는 MCP가 가능한 모든 클라이언트에 도구(tools), 리소스(resources), 프롬프트(prompts)를 노출할 것이며, 백그라운드 작업(background tasks), 대화형 입력(interactive input), 임베디드 사용자 인터페이스(embedded user interfaces)를 위한 고급 확장 기능도 탐구할 것입니다. 마지막에는 어떤 AI 어시스턴트라도 발견하고 호출할 수 있는 실행 중인 서버를 갖게 될 것입니다.
세부 사항에 들어가기에 앞서, 다음 리소스들을 준비해 두는 것을 권장합니다:
- 공식 Flama 문서: Flama documentation
- Model Context Protocol 페이지: MCP docs
- Flama GitHub 저장소: Flama on GitHub
목차
- MCP란 무엇인가?
- 프로젝트 설정
- MCP 서버 등록
- 도구(tools) 노출
- 리소스(resources) 노출
- 프롬프트(prompts) 노출
- 고급 확장 기능
- 백그라운드 작업 (Background tasks)
- 유도 (Elicitation)
- MCP Apps
- 하나의 애플리케이션 내 여러 서버
- 전체 애플리케이션
- curl을 이용한 테스트
- 결론
- 참고 문헌
- 저희의 작업을 지원해 주세요
- 저자에 대하여
MCP란 무엇인가?
Model Context Protocol은 AI 애플리케이션이 통일된 인터페이스를 통해 외부 기능에 연결할 수 있도록 하는 개방형 표준입니다. MCP 서버는 세 가지 종류의 기능을 광고합니다:
- Tools (도구): 모델이 호출할 수 있는 함수.
- Resources (리소스): URI로 주소가 지정되며 모델이 읽을 수 있는 데이터.
- Prompts (프롬프트): 인자(arguments)를 포함한 재사용 가능한 프롬프트 템플릿.
클라이언트(AI 어시스턴트, 에이전트 프레임워크, IDE)는 이러한 기능들을 발견하고, JSON 메시지를 교환하는 경량 원격 프로시저 호출 (Remote-Procedure-Call, RPC) 프로토콜인 JSON-RPC를 통해 이를 호출합니다.
Flama는 해당 프로토콜의 stateless 2026-07-28 리비전을 구현합니다. initialize 핸드셰이크(handshake)를 통해 세션을 협상하는 대신, 모든 요청은 자체적으로 완결성을 갖습니다. 즉, _meta 객체에 프로토콜 버전과 기능(capabilities)을 담고, Mcp-Method / Mcp-Name 헤더에 라우팅 데이터를 담아 전달합니다. 호출 사이에 클라이언트별 상태(state)를 유지하지 않으므로, MCP 서버를 수평적으로 확장(scale horizontally)하는 것이 매우 용이합니다.
이것이 왜 중요한가요?
- 상호 운용성 (Interoperability): MCP 기능을 지원하는 어떤 클라이언트라도 별도의 맞춤형 통합 코드 없이 귀하의 도구(tools)를 사용할 수 있습니다.
- 재사용성 (Reuse): API를 구동하는 동일한 Python 함수를 단 하나의 데코레이터(decorator)만으로 AI 에이전트에 노출할 수 있습니다.
- 타입 안전성 (Type safety): Flama는 핸들러의 타입 힌트(type hints)로부터 각 도구의 입력 및 출력 JSON 스키마 (JSON Schema)를 도출하므로, 클라이언트는 정확하고 독립적인 계약(contracts)을 전달받게 됩니다.
프로젝트 설정하기
이 포스트의 모든 예제는 uv를 통해 pydantic extras가 포함된 Flama가 설치되어 있다고 가정합니다.
uv pip install "flama[pydantic]"
또는, 사전 설치 없이 uvx --from "flama[pydantic]" flama ...를 사용하여 모든 명령을 실행할 수도 있지만, 간결함을 위해 Flama가 이미 설치되어 있다고 가정합니다.
MCP 서버 등록하기
Flama에서 MCP 서버는 특정 URL 경로에 애플리케이션에 마운트(mount)하는 이름이 지정된 레지스트리(registry)입니다. add_server 메서드는 서버를 생성하는 동시에 마운트하므로, 단일 애플리케이션에서 여러 개의 독립적인 서버를 호스팅할 수 있습니다.
import flama
from flama import Flama
...
이는 /mcp/tools/에서 접근 가능한 tools라는 이름의 서버를 등록합니다. version 파라미터는 서버의 시맨틱 버전 (semantic version)을 선언하며, instructions는 클라이언트가 표시할 수 있는 사람이 읽을 수 있는 설명을 제공합니다. 서버가 준비되면, 이름을 통해 서버를 채워 넣습니다. 모든 도구 (tool), 리소스 (resource), 프롬프트 데코레이터 (prompt decorator)는 해당 기능이 어느 서버에 속하는지를 식별하는 mcp 인자를 가집니다.
도구 (tools) 노출하기
**도구 (tool)**는 모델이 호출할 수 있는 함수입니다. tool 데코레이터를 사용하여 도구를 선언하고, mcp 인자를 통해 대상 서버를 지정합니다. Flama는 핸들러의 타입 힌트 (type hints)로부터 도구의 입력 및 출력 스키마 (schema)를 추론합니다:
@app.mcp.tool("add", description="두 정수를 더하기", mcp="tools")
def add(a: int, b: int) -> int:
return a + b
도구는 동기 (synchronous) 방식이거나 비동기 (asynchronous) 방식일 수 있습니다. 이름을 생략하면 함수의 이름이 사용되며, 설명을 생략하면 대신 독스트링 (docstring)이 사용됩니다. 파라미터와 반환 어노테이션 (return annotation)은 도구의 inputSchema 및 outputSchema가 되어 클라이언트에게 있는 그대로 전달됩니다.
다음은 문자열을 반환하는 비동기 도구의 예시입니다:
@app.mcp.tool("greet", description="이름으로 인사하기", mcp="tools")
async def greet(name: str) -> str:
return f"Hello, {name}!"
도구가 작동하는지 확인해 보겠습니다. 애플리케이션을 실행합니다:
flama run app:app
그리고 curl로 호출합니다:
curl -s -X POST http://127.0.0.1:8000/mcp/tools/ \
-H 'Content-Type: application/json' \
-H 'Mcp-Method: tools/call' \
...
서버는 JSON-RPC 결과로 응답합니다:
{
"jsonrpc": "2.0",
"id": 1,
...
structuredContent 필드는 타입이 지정된 반환 값을 담고 있으며, content는 비정형 출력을 선호하는 클라이언트를 위해 텍스트 표현을 제공합니다.
리소스 (resources) 노출하기
**리소스 (resource)**는 URI로 주소 지정이 가능한 읽을 수 있는 데이터입니다. resource 데코레이터는 지정된 서버에 리소스를 등록합니다:
import json
@app.mcp.resource("config://app", name="config", description="애플리케이션 설정",
...
리소스는 URI를 통해 목록화되고 읽히므로, 클라이언트는 config://app을 요청함으로써 위의 설정을 가져옵니다.
MIME 타입은 클라이언트에게 콘텐츠를 어떻게 해석할지 알려줍니다.
리소스 읽기:
curl -s -X POST http://127.0.0.1:8000/mcp/tools/ \
-H 'Content-Type: application/json' \
-H 'Mcp-Method: resources/read' \
...
{
"jsonrpc": "2.0",
"id": 1,
...
프롬프트 (Prompts) 노출하기
**프롬프트 (prompt)**는 이름이 지정된 재사용 가능한 프롬프트 템플릿입니다. prompt 데코레이터는 핸들러의 파라미터로부터 인자를 유도하여 지정된 서버에 프롬프트를 등록합니다:
@app.mcp.prompt("summarise", description="Summarise the given text", mcp="tools")
def summarise(text: str):
return f"Summarise the following:\n\n{text}"
프롬프트는 이름으로 목록화되며 클라이언트가 제공하는 인자와 함께 렌더링됩니다. 여기에서 text는 유일한 필수 인자가 됩니다. 렌더링된 프롬프트를 가져오려면:
curl -s -X POST http://127.0.0.1:8000/mcp/tools/ \
-H 'Content-Type: application/json' \
-H 'Mcp-Method: prompts/get' \
...
{
"jsonrpc": "2.0",
"id": 1,
...
고급 확장 기능 (Advanced extensions)
2026-07-28 프로토콜은 선택적 확장 기능들을 정의하며, Flama는 이 모든 기능을 네이티브로 지원합니다. 서버는 자신의 탐색 기능 (discovery capabilities) 내에서 사용하는 확장 기능을 광고하므로, 클라이언트는 요청마다 이를 협상합니다.
백그라운드 작업 (Background tasks)
오래 걸리는 도구(tools)는 호출을 차단하는 대신 백그라운드 **작업 (Tasks)**으로 실행될 수 있습니다. task=True를 전달하면 서버는 클라이언트가 폴링 (poll) 할 수 있는 작업 핸들을 반환합니다:
@app.mcp.tool("square", task=True, description="Square a number as a background task", mcp="tools")
async def square(x: int) -> int:
return x * x
클라이언트가 square를 호출할 때, 서버는 빠른 연산의 경우 결과를 직접 반환하거나, 클라이언트가 완료될 때까지 폴링할 수 있는 진정으로 오래 걸리는 계산을 위해 작업 토큰 (task token)을 발행할 수 있습니다.
유도 (Elicitation)
도구는 호출 중간에 일시 중지하여 사용자로부터 추가 입력을 유도 (Elicitation) 할 수 있습니다. 핸들러는 지금까지 수집된 답변을 읽기 위해 Elicitation이 주석 처리된 파라미터를 선언하고, 더 많은 정보를 요청하기 위해 Elicit.require(...)를 반환합니다.
from flama.mcp.data_structures import Elicit, Elicitation
@app.mcp.tool("confirm", description="Confirm an action through an elicitation round-trip", mcp="tools")
...
elicitation 파라미터는 서버에 의해 제공되며 도구의 입력 스키마(input schema)에서 제외되므로, 클라이언트가 채워야 하는 도구 인자(argument)로 나타나지 않습니다. 프로토콜은 상태가 없는 (stateless) 방식이기 때문에, 지금까지 수집된 답변은 클라이언트가 재시도 시 다시 에코(echo)하는 불투명한 지속 토큰 (opaque continuation token)을 통해 왕복(round-trip)됩니다.
클라이언트가 사전 답변 없이 confirm을 호출하면, resultType: "inputRequired"와 서버가 필요로 하는 내용을 설명하는 스키마가 포함된 응답을 받습니다. 클라이언트는 사용자로부터 해당 입력을 수집한 후, 이번에는 수집된 답변을 지참하여 재시도합니다.
MCP 앱 (MCP Apps)
도구는 결과와 함께 렌더링될 수 있는 프리페치 가능한 (prefetchable) 사용자 인터페이스 템플릿 (MCP 앱 (MCP App))을 선언할 수 있습니다. app_template으로 템플릿을 등록하고 ui_template을 통해 도구가 해당 템플릿을 가리키도록 설정합니다:
@app.mcp.app_template("ui://widget", name="widget", description="A small UI widget", mcp="tools")
def widget():
return "<html><body><h1>Flama widget</h1></body></html>"
...
MCP 앱을 지원하는 클라이언트는 템플릿을 프리페치(prefetch)하여 도구의 결과와 함께 렌더링함으로써 더 풍부한 상호작용 경험을 제공할 수 있습니다.
하나의 애플리케이션 내 여러 서버
단일 Flama 애플리케이션은 필요한 만큼 많은 MCP 서버를 각각 고유한 경로 아래에 호스팅할 수 있습니다. 이는 관심사 분리 (separation of concerns)나 서로 다른 기능 세트의 버전을 관리할 때 유용합니다:
app.mcp.add_server("/mcp/tools/", "tools", version="2.0.0", instructions="Flama demo MCP tools server")
app.mcp.add_server("/mcp/math/", "math", version="2.0.0")
각 서버는 독립적입니다. 도구 (Tools), 리소스 (Resources), 프롬프트 (Prompts)는 mcp 인자를 통해 해당 서버에 바인딩됩니다:
@app.mcp.tool("multiply", description="Multiply two integers", mcp="math")
def multiply(a: int, b: int) -> int:
return a * b
/mcp/tools/에 대한 tools/list 요청은 tools 서버에 등록된 도구들만 반환하는 반면, /mcp/math/에 대한 요청은 multiply만 반환합니다. 클라이언트는 각 서버를 독립적으로 탐색합니다.
전체 애플리케이션
모든 것을 종합하면, 다음과 같은 전체 애플리케이션이 완성됩니다. 이 애플리케이션은 단일 Flama 앱에 두 개의 MCP 서버를 등록하고, 도구들 (동기 (sync), 비동기 (async), 백그라운드 작업 (background task), 유도 (elicitation), UI 템플릿 (UI template)), 리소스 (resource), 그리고 프롬프트 (prompt)를 채워 넣습니다:
import json
import flama
...
이를 app.py로 저장하고 실행하세요:
python app.py
서버가 8000번 포트에서 시작되며, 두 MCP 엔드포인트 (endpoints)가 준비됩니다.
curl을 이용한 테스트
애플리케이션이 실행되면, 명령줄(command line)에서 모든 기능을 테스트할 수 있습니다.
tools 서버에서 사용 가능한 도구 목록 나열 (List available tools):
curl -s -X POST http://127.0.0.1:8000/mcp/tools/ \
-H 'Content-Type: application/json' \
-H 'Mcp-Method: tools/list' \
...
응답에는 다섯 가지 도구(add, confirm, greet, square, with_ui)가 나열되며, 각 도구는 Python 타입 힌트 (type hints)로부터 유도된 전체 입력 및 출력 스키마 (schema)를 가집니다.
math 서버의 도구 호출 (Call a tool):
curl -s -X POST http://127.0.0.1:8000/mcp/math/ \
-H 'Content-Type: application/json' \
-H 'Mcp-Method: tools/call' \
...
{
"jsonrpc": "2.0",
"id": 1,
...
리소스 읽기 (Read a resource):
curl -s -X POST http://127.0.0.1:8000/mcp/tools/ \
-H 'Content-Type: application/json' \
-H 'Mcp-Method: resources/read' \
...
렌더링된 프롬프트 가져오기 (Get a rendered prompt):
curl -s -X POST http://127.0.0.1:8000/mcp/tools/ \
-H 'Content-Type: application/json' \
-H 'Mcp-Method: prompts/get' \
...
모든 요청은 동일한 패턴을 따릅니다: 서버 경로로 POST 요청을 보내며, Mcp-Method는 작업을 식별하고, Mcp-Name은 대상을 식별하며, MCP-Protocol-Version은 프로토콜 버전을 선언합니다.
결론 (Conclusions)
Flama는 "Python 함수를 가지고 있다"에서 "AI 에이전트가 이를 발견하고 호출할 수 있다"로 가는 여정을 최대한 단축해 줍니다. MCP 지원을 위해 설정 파일, 코드 생성, 또는 외부 도구가 전혀 필요하지 않습니다. 사용자는 일반적인 Python 함수를 작성하고 데코레이터(decorator)를 붙이기만 하면 되며, 나머지는 프레임워크가 처리합니다:
add_server: 지정된 경로에 이름이 있는 MCP 서버를 마운트(mount)합니다.@tool: 완전한 스키마 추론(schema inference)을 통해 함수를 호출 가능한 도구(tool)로 노출합니다.@resource: 클라이언트가 읽을 수 있도록 URI를 통해 데이터를 노출합니다.@prompt: 타입이 지정된 인자(typed arguments)를 가진 재사용 가능한 프롬프트 템플릿(prompt template)을 노출합니다.- 확장 기능 (Extensions): 더 풍부한 상호작용을 위한 백그라운드 작업(background tasks), 유도(elicitation), 그리고 MCP 앱(MCP Apps)을 지원합니다.
프로토콜이 상태를 유지하지 않는(stateless) 방식이기 때문에, 서버는 스티키 세션(sticky sessions) 없이도 수평적으로 확장(scale horizontally)할 수 있습니다. 스키마가 타입 힌트(type hints)로부터 유도되기 때문에, 클라이언트는 수동 명세 없이도 정확한 계약(contracts)을 전달받습니다. 또한 여러 서버가 단일 애플리케이션 내에 존재할 수 있으므로, 도메인, 버전 또는 액세스 수준별로 기능을 조직화할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기