"""
LangGraph × MCP 클라이언트 학습 예제
──────────────────────────────────
- math : 로컬 stdio 서브프로세스로 실행되는 MCP 서버
- weather : 원격 streamable_http MCP 서버 (미리 기동해두어야 함)
그래프는 노드가 단 하나뿐인 '미니 에이전트'.
실제 에이전트라면 LLM 이 도구를 고르겠지만, 여기서는 규칙 기반으로
라우팅해 MCP 호출 흐름 자체에 집중한다.
"""
import asyncio
import re
from typing import Any, Sequence, TypedDict
from langchain_core.messages import HumanMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.graph import StateGraph
class AgentState(TypedDict):
messages: Sequence[Any]
# ── 1) 두 MCP 서버를 한 클라이언트에 등록 ────────────────────────
mcp_client = MultiServerMCPClient({
"math": {
"command": "python3",
"args": ["ch04/mcp_servers/MCP_math_server.py"],
"transport": "stdio", # 서브프로세스 + stdin/stdout 파이프
},
"weather": {
# 먼저 `python ch04/mcp_servers/MCP_weather_server.py` 로 기동
"url": "http://0.0.0.0:8000/mcp",
"transport": "streamable_http", # 원격 HTTP 스트리밍
},
})
# get_tools() 는 서버에 질의하는 비용이 있으므로 최초 1회만 호출해 캐시
_tools: list | None = None
async def get_tools() -> list:
global _tools
if _tools is None:
_tools = await mcp_client.get_tools()
return _tools
# ── 2) 규칙 기반 라우팅: 마지막 사용자 메시지 → (도구명, 입력) ──
_HAS_OP = re.compile(r"[+\-*/()]") # 연산자 하나라도 있으면 수식 취급
_WEATHER_IN = re.compile(r"weather in\s+(.+?)\??$", re.I)
_UI_NALSSI = re.compile(r"(.+?)\s*의\s*날씨")
def route(text: str) -> tuple[str, dict] | None:
if _HAS_OP.search(text):
return "math", {"expression": text}
if "weather" in text.lower() or "날씨" in text:
m = _WEATHER_IN.search(text) or _UI_NALSSI.search(text)
return "weather", {"location": m.group(1).strip() if m else "NYC"}
return None
# ── 3) 단일 그래프 노드: 라우팅 결과대로 MCP 도구를 호출 ─────────
async def call_mcp_tools(state: AgentState) -> dict[str, Any]:
user_text = state["messages"][-1].content
picked = route(user_text)
if picked is None:
reply = "수학 또는 날씨 질문만 답변할 수 있습니다."
else:
name, tool_input = picked
tool = next((t for t in await get_tools() if t.name == name), None)
reply = await tool.ainvoke(tool_input) if tool else f"{name} 도구를 사용할 수 없습니다."
return {"messages": [{"role": "assistant", "content": reply}]}
# ── 4) 노드 1개짜리 StateGraph 컴파일 ───────────────────────────
def build_graph():
g = StateGraph(AgentState)
g.add_node("assistant", call_mcp_tools)
g.set_entry_point("assistant")
return g.compile()
GRAPH = build_graph()
# ── 5) 실행 헬퍼 ────────────────────────────────────────────────
async def ask(question: str) -> str:
"""질문 한 건을 그래프에 넣고 assistant 의 최종 응답 텍스트만 뽑는다."""
result = await GRAPH.ainvoke({"messages": [HumanMessage(content=question)]})
msg = result["messages"][-1]
return msg["content"] if isinstance(msg, dict) else msg.content
async def main():
print("Math ▶", await ask("(3 + 5) * 12은 얼마인가요?"))
print("Weather ▶", await ask("NYC의 날씨는 어때요?"))
if __name__ == "__main__":
asyncio.run(main())