5장. LLM 외부 도구 연동
호스트·클라이언트·서버의 짜임
큰 건물의 관리실을 떠올려 보세요. 관리실(호스트)에는 직원이 여럿 있고, 직원마다 맡은 업체가 하나씩 있습니다. 한 직원은 청소 업체만, 다른 직원은 승강기 업체만 상대합니다. 업체(서버)는 자기 일만 하고, 관리실 안의 사정은 모릅니다. MCP 도 이렇게 나뉩니다. 우리 챗봇(호스트) 안에 클라이언트가 서버마다 하나씩 붙어서 도구를 빌려 옵니다.
보라 점선 틀이 호스트, 곧 프로그램 하나입니다. 그 안의 에이전트가 도구가 필요하면 클라이언트에게 부탁하고, 클라이언트는 파랑 선으로 이어진 자기 서버 하나에만 요청을 보냅니다. 위쪽의 자료 서버는 같은 컴퓨터에서 stdio 로, 아래쪽의 다른 서버는 네트워크 너머에서 HTTP 로 이어진 예입니다. 회청 점선 뒤의 자료(data 폴더)는 서버만 읽습니다. 여기서 관리실이 호스트, 직원이 클라이언트, 업체가 서버입니다. 비유와 다른 점은, 관리실 직원은 사람이라 업체를 알아서 고르지만 MCP 클라이언트는 우리가 설정에 적어 준 서버에만 붙는다는 것입니다.
세 역할
| 역할 | 하는 일 | 이 장에서 |
|---|---|---|
| 호스트(host) | 사용자와 만나고 모델을 부른다. 클라이언트를 품는다 | 우리 챗봇 프로그램(실습 코드) |
| 클라이언트(client) | 서버 하나와 이어져 도구 목록을 받고 도구를 부른다 | MultiServerMCPClient 가 서버마다 만든다 |
| 서버(server) | 도구와 자료를 내놓고, 요청이 오면 실행한다 | out_ch05_server.py(FastMCP) |
두 가지 전송 방식
| 전송 방식 | 원래 말 | 어떻게 잇나 | 어울리는 때 |
|---|---|---|---|
stdio | standard input/output | 클라이언트가 서버 프로그램을 직접 켜고, 표준 입력과 표준 출력으로 글을 주고받는다 | 같은 컴퓨터의 서버. 이 장의 실습 |
streamable_http | 흘려보낼 수 있는 HTTP | 이미 떠 있는 서버의 주소(URL)로 HTTP 요청을 보낸다 | 다른 컴퓨터나 회사 서버 |
stdio 의 네 걸음입니다. 클라이언트가 python out_ch05_server.py 처럼 서버 프로그램을 켜고(1), 서버의 표준 입력에 요청을 한 줄 쓰면(2), 서버가 그 요청대로 함수를 실행하고(3), 결과를 표준 출력에 써서 클라이언트가 읽습니다(4). 두 프로그램이 같은 컴퓨터 안에서 글을 주고받을 뿐이라 네트워크 설정이 필요 없습니다.
짧은 예시
stdio 로 오가는 글은 JSON-RPC 형식입니다. MCP 규격에 적힌 요청과 답의 생김새를 사전으로 만들어 찍어 봅니다. 서버는 띄우지 않습니다.
import json
# 1) 도구 목록을 달라는 요청. id 는 요청 번호로, 답에도 같은 번호가 붙는다
list_request = {"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
# 2) 도구 하나를 불러 달라는 요청. params 에 도구 이름과 인자를 담는다
call_request = {
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {"name": "find_products", "arguments": {"region": "부산"}},
}
# 3) 서버의 답. 같은 id 와 함께 결과(content)를 글 조각의 목록으로 돌려준다
call_answer = {
"jsonrpc": "2.0",
"id": 2,
"result": {"content": [{"type": "text", "text": "P03 부산 야경과 시장 1박 2일 | 18만 원"}], "isError": False},
}
for name, msg in [("목록 요청", list_request), ("호출 요청", call_request), ("호출 답", call_answer)]:
# ensure_ascii=False: 한글을 \uXXXX 로 바꾸지 않고 그대로 적는다
print(name, json.dumps(msg, ensure_ascii=False))목록 요청 {"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
호출 요청 {"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "find_products", "arguments": {"region": "부산"}}}
호출 답 {"jsonrpc": "2.0", "id": 2, "result": {"content": [{"type": "text", "text": "P03 부산 야경과 시장 1박 2일 | 18만 원"}], "isError": false}}요청에는 method(무엇을 할지)가, 답에는 result(결과)가 있고, 둘은 같은 id 로 짝을 맞춥니다. ToolMessage 의 tool_call_id 와 같은 생각입니다. 결과는 글 조각(type: "text")의 목록으로 오므로, 실습 4에서 LangChain 이 받아 온 결과도 목록 모양입니다. 이 글을 우리가 직접 쓸 일은 없습니다. 클라이언트와 서버의 SDK 가 대신 쓰고 읽습니다.
코드가 의미하는 것
이 코드는 관리실 직원과 업체가 주고받는 쪽지 세 장을 펼쳐 보는 것입니다. "할 수 있는 일 목록을 주세요", "이 일을 해 주세요", "했습니다"의 세 장입니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
{"jsonrpc": "2.0", "id": 1, "method": "tools/list"} | 도구 목록을 달라는 요청을 만든다 | 클라이언트가 도구 목록을 받아 올 때 서버에 보내는 쪽지의 생김새입니다 |
"method": "tools/call" 과 "params": {...} | 도구 이름과 인자를 담은 호출 요청을 만든다 | 모델의 tool_calls 한 줄이 MCP 서버로 갈 때의 꼴입니다. 인자 칸의 이름이 args 대신 arguments 입니다 |
"result": {"content": [...], "isError": False} | 같은 id 를 단 답을 만든다 | 서버가 도구를 실행한 결과입니다. 결과가 글 조각의 목록이라는 점이 실습 4의 text_of() 로 이어집니다 |
json.dumps(msg, ensure_ascii=False) | 사전을 한 줄 JSON 글로 찍는다 | stdio 로 오가는 것이 이런 한 줄 글입니다 |
결과에서 읽을 것은 두 가지입니다.
- id 2 가 요청과 답을 잇습니다. 호출 요청과 호출 답이 모두
"id": 2입니다.tool_call_id와 같은 번호표가 MCP 에도 있습니다. - 파이썬의 `False` 가 JSON 의 `false` 가 됐습니다. 출력의 마지막 줄을 보면 사전의
False가 소문자false로 적혔습니다. 오가는 것이 파이썬 객체가 아니라 JSON 글이므로, 다른 언어로 짠 클라이언트와 서버도 같은 규격으로 이어집니다.
주의할 점
- stdio 서버는 표준 출력을 MCP 의 글을 주고받는 데 씁니다. 서버 코드에서
print()로 아무 글이나 찍으면 클라이언트가 그 글을 요청의 답으로 잘못 읽어 연결이 깨질 수 있습니다. 서버에서 무엇을 찍어 보고 싶으면 표준 오류(stderr)로 찍습니다. - 클라이언트 하나는 서버 하나만 맡습니다. 서버가 둘이면 클라이언트도 둘입니다.
MultiServerMCPClient는 설정에 적힌 서버마다 클라이언트를 만들어 줍니다.