5장. LLM 외부 도구 연동
FastMCP 로 서버 만들고 LangChain 에 붙이기
가게를 열려면 간판을 달고, 메뉴판을 걸고, 문을 열면 됩니다. FastMCP 로 MCP 서버를 만드는 것도 세 걸음입니다. FastMCP("이름") 으로 간판을 달고, @mcp.tool() 로 메뉴를 걸고, mcp.run() 으로 문을 엽니다. 손님 쪽에서는 MultiServerMCPClient 가 가게를 찾아가 메뉴를 받아 오고, 어댑터가 그 메뉴를 LangChain 도구로 바꿉니다.
세 상자는 서버 파일 하나가 우리 챗봇의 도구가 되기까지입니다. FastMCP 로 만든 서버 파일(1)을 클라이언트가 띄워 이어지고(2), get_tools() 로 받은 도구는 @tool 로 만든 도구와 같은 모양이라 bind_tools 와 에이전트에 그대로 넣습니다(3). 여기서 간판이 서버 이름, 메뉴판이 @mcp.tool() 로 건 함수들, 가게를 찾아가는 손님이 클라이언트입니다. 비유와 다른 점은, 이 가게는 손님(클라이언트)이 올 때 문을 열고 손님이 가면 닫는다는 것입니다. stdio 서버는 클라이언트가 켜고 끕니다.
FastMCP 서버의 세 걸음
| 걸음 | 코드 | 뜻 |
|---|---|---|
| 간판 달기 | mcp = FastMCP("trip-data") | 서버 이름을 정한다 |
| 메뉴 걸기 | @mcp.tool() 을 함수 위에 | docstring 이 설명, 타입 힌트가 인자가 된다 |
| 문 열기 | if __name__ == "__main__": mcp.run() | 파일을 실행하면 stdio 로 요청을 기다린다 |
MultiServerMCPClient 의 설정
| 열쇠 | 뜻 | 이 장의 값 |
|---|---|---|
| 바깥 열쇠 | 서버에 붙인 이름. 여럿 적으면 서버도 여럿 | "trip" |
command | 서버를 켤 프로그램 | sys.executable(지금 쓰는 파이썬) |
args | 그 프로그램에 넘길 인자 | ["out_ch05_server.py"] |
transport | 전송 방식 | "stdio" |
sys.executable 은 지금 이 코드를 돌리는 파이썬의 경로입니다. 그냥 "python" 이라고 적으면 가상 환경(.venv)이 아닌 다른 파이썬이 켜져 mcp 패키지를 못 찾을 수 있습니다.
짧은 예시
서버 객체를 만들고 도구를 걸어, 도구 목록과 인자 스키마를 봅니다. @mcp.tool() 은 함수를 그대로 돌려주므로, 서버를 띄우지 않고 함수로 바로 시험할 수도 있습니다. 끝으로 클라이언트 설정을 만들어 봅니다. 클라이언트를 만드는 것만으로는 서버가 켜지지 않습니다.
import asyncio
import sys
from langchain_mcp_adapters.client import MultiServerMCPClient
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("trip-demo") # 간판 달기
@mcp.tool() # 메뉴 걸기
def count_people(adults: int, children: int = 0) -> str:
"""어른과 아동 수를 받아 일행의 인원을 알려 준다."""
return f"일행은 모두 {adults + children}명입니다."
for t in asyncio.run(mcp.list_tools()):
print(t.name, "|", t.description)
print(" 인자:", t.inputSchema["properties"], "| 꼭 넣을 것:", t.inputSchema["required"])
# 서버를 띄우지 않고 함수로 바로 시험한다
print("함수로 시험:", count_people(2, 1))
# 클라이언트 설정만 만든다. 서버는 get_tools() 를 부를 때 켜진다
client = MultiServerMCPClient({
"trip": {"command": sys.executable, "args": ["out_ch05_server.py"], "transport": "stdio"},
})
print("붙을 서버:", list(client.connections))count_people | 어른과 아동 수를 받아 일행의 인원을 알려 준다.
인자: {'adults': {'title': 'Adults', 'type': 'integer'}, 'children': {'default': 0, 'title': 'Children', 'type': 'integer'}} | 꼭 넣을 것: ['adults']
함수로 시험: 일행은 모두 3명입니다.
붙을 서버: ['trip']도구 목록에는 이름, 설명, 인자 스키마가 있고, 기본값을 준 children 은 꼭 넣을 것에서 빠졌습니다. @tool 에서 본 것과 같습니다. 함수로 바로 불러 보면 서버 없이도 도구의 계산이 맞는지 확인할 수 있습니다. 클라이언트에는 trip 이라는 서버 하나가 적혔지만, 아직 켜지지는 않았습니다. 실제로 켜고 도구를 부르는 것은 실습 4에서 합니다.
코드가 의미하는 것
이 코드는 가게를 여는 세 걸음 가운데 간판 달기와 메뉴 걸기까지만 하고, 손님(클라이언트)이 찾아갈 곳을 적어 두는 것입니다. 문 열기(mcp.run())는 하지 않습니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
mcp = FastMCP("trip-demo") | 서버 이름을 정한다 | 간판 달기입니다. 서버 하나에 이름 하나를 붙입니다 |
@mcp.tool() | 함수를 서버의 도구로 건다 | 메뉴 걸기입니다. docstring 이 설명, 타입 힌트가 인자 스키마가 됩니다 |
count_people(2, 1) | 데코레이터를 얹은 함수를 그냥 부른다 | @mcp.tool() 이 함수를 그대로 돌려주므로 서버 없이 계산만 시험할 수 있습니다. 서버를 띄우기 전에 도구 속이 맞는지 보는 방법입니다 |
MultiServerMCPClient({"trip": {...}}) | 서버를 켤 명령과 전송 방식을 적은 클라이언트를 만든다 | 손님이 들고 갈 약도입니다. 적어 두기만 하고, 서버는 get_tools() 를 부를 때 켭니다 |
client.connections | 적어 둔 서버 설정의 이름을 본다 | 클라이언트가 어느 서버에 붙을지 확인합니다 |
결과에서 읽을 것은 두 가지입니다.
- 꼭 넣을 것은 `['adults']` 하나입니다. 기본값을 준
children은 빠졌습니다.@tool의 꼭 넣을 인자와 같은 규칙이 MCP 도구에도 적용됩니다. - 서버는 아직 켜지지 않았습니다.
붙을 서버: ['trip']은 설정에 적은 이름을 보여 줄 뿐입니다. 서버 프로그램을 실제로 켜는 것은 실습 4의get_tools()입니다.
주의할 점
- 클라이언트의
get_tools()와 MCP 도구의 실행은 비동기 함수입니다.await를 쓰려면async def main():안에 코드를 넣고asyncio.run(main())으로 시작합니다. 에이전트도invoke대신await agent.ainvoke(...)(a 는 asynchronous)로 부릅니다. MultiServerMCPClient는 기본으로 도구를 부를 때마다 서버와 새로 이어졌다 끊습니다. 그래서 서버가 기억해 둔 값(변수)은 다음 호출에서 사라집니다. 기억이 필요한 서버는 파일이나 데이터베이스에 적어 둡니다.- VS Code 의 노트북(Jupyter)에서는 이미 이벤트 루프가 돌고 있어
asyncio.run()이 "이미 도는 루프가 있다"는 오류를 냅니다. 이때는asyncio.run(main())대신await main()한 줄로 부릅니다. 이 교안의 실습 코드는python 파일.py로 실행하는 것을 기준으로 했습니다.