{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# MCP + LangGraph 핸즈온 튜토리얼\n", "\n", "- 작성자: [테디노트](https://youtube.com/c/teddynote)\n", "- 강의: [패스트캠퍼스 RAG 비법노트](https://fastcampus.co.kr/data_online_teddy)\n", "\n", "**참고자료**\n", "- https://modelcontextprotocol.io/introduction\n", "- https://github.com/langchain-ai/langchain-mcp-adapters" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 환경설정\n", "\n", "아래 설치 방법을 참고하여 `uv` 를 설치합니다.\n", "\n", "**uv 설치 방법**\n", "\n", "```bash\n", "# macOS/Linux\n", "curl -LsSf https://astral.sh/uv/install.sh | sh\n", "\n", "# Windows (PowerShell)\n", "irm https://astral.sh/uv/install.ps1 | iex\n", "```\n", "\n", "**의존성 설치**\n", "\n", "```bash\n", "uv pip install -r requirements.txt\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "환경변수를 가져옵니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from dotenv import load_dotenv\n", "\n", "load_dotenv(override=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## MultiServerMCPClient" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "사전에 `mcp_server_remote.py` 를 실행해둡니다. 터미널을 열고 가상환경이 활성화 되어 있는 상태에서 서버를 실행해 주세요.\n", "\n", "> 명령어\n", "```bash\n", "source .venv/bin/activate\n", "python mcp_server_remote.py\n", "```\n", "\n", "`async with` 로 일시적인 Session 연결을 생성 후 해제" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from langchain_mcp_adapters.client import MultiServerMCPClient\n", "from langgraph.prebuilt import create_react_agent\n", "from utils import ainvoke_graph, astream_graph\n", "from langchain_anthropic import ChatAnthropic\n", "\n", "model = ChatAnthropic(\n", " model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n", ")\n", "\n", "async with MultiServerMCPClient(\n", " {\n", " \"weather\": {\n", " # 서버의 포트와 일치해야 합니다.(8005번 포트)\n", " \"url\": \"http://localhost:8005/sse\",\n", " \"transport\": \"sse\",\n", " }\n", " }\n", ") as client:\n", " print(client.get_tools())\n", " agent = create_react_agent(model, client.get_tools())\n", " answer = await astream_graph(agent, {\"messages\": \"서울의 날씨는 어떠니?\"})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "다음의 경우에는 session 이 닫혔기 때문에 도구에 접근할 수 없는 것을 확인할 수 있습니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "await astream_graph(agent, {\"messages\": \"서울의 날씨는 어떠니?\"})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "이제 그럼 Async Session 을 유지하며 도구에 접근하는 방식으로 변경해 보겠습니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 1. 클라이언트 생성\n", "client = MultiServerMCPClient(\n", " {\n", " \"weather\": {\n", " \"url\": \"http://localhost:8005/sse\",\n", " \"transport\": \"sse\",\n", " }\n", " }\n", ")\n", "\n", "\n", "# 2. 명시적으로 연결 초기화 (이 부분이 필요함)\n", "# 초기화\n", "await client.__aenter__()\n", "\n", "# 이제 도구가 로드됨\n", "print(client.get_tools()) # 도구가 표시됨" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "langgraph 의 에이전트를 생성합니다." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# 에이전트 생성\n", "agent = create_react_agent(model, client.get_tools())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "그래프를 실행하여 결과를 확인합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "await astream_graph(agent, {\"messages\": \"서울의 날씨는 어떠니?\"})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Stdio 통신 방식\n", "\n", "Stdio 통신 방식은 로컬 환경에서 사용하기 위해 사용합니다.\n", "\n", "- 통신을 위해 표준 입력/출력 사용\n", "\n", "참고: 아래의 python 경로는 수정하세요!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from mcp import ClientSession, StdioServerParameters\n", "from mcp.client.stdio import stdio_client\n", "from langgraph.prebuilt import create_react_agent\n", "from langchain_mcp_adapters.tools import load_mcp_tools\n", "from langchain_anthropic import ChatAnthropic\n", "\n", "# Anthropic의 Claude 모델 초기화\n", "model = ChatAnthropic(\n", " model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n", ")\n", "\n", "# StdIO 서버 파라미터 설정\n", "# - command: Python 인터프리터 경로\n", "# - args: 실행할 MCP 서버 스크립트\n", "server_params = StdioServerParameters(\n", " command=\"./.venv/bin/python\",\n", " args=[\"mcp_server_local.py\"],\n", ")\n", "\n", "# StdIO 클라이언트를 사용하여 서버와 통신\n", "async with stdio_client(server_params) as (read, write):\n", " # 클라이언트 세션 생성\n", " async with ClientSession(read, write) as session:\n", " # 연결 초기화\n", " await session.initialize()\n", "\n", " # MCP 도구 로드\n", " tools = await load_mcp_tools(session)\n", " print(tools)\n", "\n", " # 에이전트 생성\n", " agent = create_react_agent(model, tools)\n", "\n", " # 에이전트 응답 스트리밍\n", " await astream_graph(agent, {\"messages\": \"서울의 날씨는 어떠니?\"})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RAG 를 구축한 MCP 서버 사용\n", "\n", "- 파일: `mcp_server_rag.py`\n", "\n", "사전에 langchain 으로 구축한 `mcp_server_rag.py` 파일을 사용합니다.\n", "\n", "stdio 통신 방식으로 도구에 대한 정보를 가져옵니다. 여기서 도구는 `retriever` 도구를 가져오게 되며, 이 도구는 `mcp_server_rag.py` 에서 정의된 도구입니다. 이 파일은 사전에 서버에서 실행되지 **않아도** 됩니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from mcp import ClientSession, StdioServerParameters\n", "from mcp.client.stdio import stdio_client\n", "from langchain_mcp_adapters.tools import load_mcp_tools\n", "from langgraph.prebuilt import create_react_agent\n", "from langchain_anthropic import ChatAnthropic\n", "from utils import astream_graph\n", "\n", "# Anthropic의 Claude 모델 초기화\n", "model = ChatAnthropic(\n", " model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n", ")\n", "\n", "# RAG 서버를 위한 StdIO 서버 파라미터 설정\n", "server_params = StdioServerParameters(\n", " command=\"./.venv/bin/python\",\n", " args=[\"./mcp_server_rag.py\"],\n", ")\n", "\n", "# StdIO 클라이언트를 사용하여 RAG 서버와 통신\n", "async with stdio_client(server_params) as (read, write):\n", " # 클라이언트 세션 생성\n", " async with ClientSession(read, write) as session:\n", " # 연결 초기화\n", " await session.initialize()\n", "\n", " # MCP 도구 로드 (여기서는 retriever 도구)\n", " tools = await load_mcp_tools(session)\n", "\n", " # 에이전트 생성 및 실행\n", " agent = create_react_agent(model, tools)\n", "\n", " # 에이전트 응답 스트리밍\n", " await astream_graph(\n", " agent, {\"messages\": \"삼성전자가 개발한 생성형 AI의 이름을 검색해줘\"}\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## SSE 방식과 StdIO 방식 혼합 사용\n", "\n", "- 파일: `mcp_server_rag.py` 는 StdIO 방식으로 통신\n", "- `langchain-dev-docs` 는 SSE 방식으로 통신\n", "\n", "SSE 방식과 StdIO 방식을 혼합하여 사용합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from langchain_mcp_adapters.client import MultiServerMCPClient\n", "from langgraph.prebuilt import create_react_agent\n", "from langchain_anthropic import ChatAnthropic\n", "\n", "# Anthropic의 Claude 모델 초기화\n", "model = ChatAnthropic(\n", " model_name=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000\n", ")\n", "\n", "# 1. 다중 서버 MCP 클라이언트 생성\n", "client = MultiServerMCPClient(\n", " {\n", " \"document-retriever\": {\n", " \"command\": \"./.venv/bin/python\",\n", " # mcp_server_rag.py 파일의 절대 경로로 업데이트해야 합니다\n", " \"args\": [\"./mcp_server_rag.py\"],\n", " # stdio 방식으로 통신 (표준 입출력 사용)\n", " \"transport\": \"stdio\",\n", " },\n", " \"langchain-dev-docs\": {\n", " # SSE 서버가 실행 중인지 확인하세요\n", " \"url\": \"https://teddynote.io/mcp/langchain/sse\",\n", " # SSE(Server-Sent Events) 방식으로 통신\n", " \"transport\": \"sse\",\n", " },\n", " }\n", ")\n", "\n", "\n", "# 2. 비동기 컨텍스트 매니저를 통한 명시적 연결 초기화\n", "await client.__aenter__()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "langgraph 의 `create_react_agent` 를 사용하여 에이전트를 생성합니다." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "from langgraph.checkpoint.memory import MemorySaver\n", "from langchain_core.runnables import RunnableConfig\n", "\n", "prompt = (\n", " \"You are a smart agent. \"\n", " \"Use `retriever` tool to search on AI related documents and answer questions.\"\n", " \"Use `langchain-dev-docs` tool to search on langchain / langgraph related documents and answer questions.\"\n", " \"Answer in Korean.\"\n", ")\n", "agent = create_react_agent(\n", " model, client.get_tools(), prompt=prompt, checkpointer=MemorySaver()\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "구축해 놓은 `mcp_server_rag.py` 에서 정의한 `retriever` 도구를 사용하여 검색을 수행합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "config = RunnableConfig(recursion_limit=30, thread_id=1)\n", "await astream_graph(\n", " agent,\n", " {\n", " \"messages\": \"`retriever` 도구를 사용해서 삼성전자가 개발한 생성형 AI 이름을 검색해줘\"\n", " },\n", " config=config,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "이번에는 `langchain-dev-docs` 도구를 사용하여 검색을 수행합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "config = RunnableConfig(recursion_limit=30, thread_id=1)\n", "await astream_graph(\n", " agent,\n", " {\"messages\": \"langgraph-dev-docs 참고해서 self-rag 의 정의에 대해서 알려줘\"},\n", " config=config,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`MemorySaver` 를 사용하여 단기 기억을 유지합니다. 따라서, multi-turn 대화도 가능합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "await astream_graph(\n", " agent, {\"messages\": \"이전의 내용을 bullet point 로 요약해줘\"}, config=config\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## LangChain 에 통합된 도구 + MCP 도구\n", "\n", "여기서는 LangChain 에 통합된 도구를 기존의 MCP 로만 이루어진 도구와 함께 사용이 가능한지 테스트 합니다." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "from langchain_community.tools.tavily_search import TavilySearchResults\n", "\n", "# Tavily 검색 도구를 초기화 합니다. (news 타입, 최근 3일 내 뉴스)\n", "tavily = TavilySearchResults(max_results=3, topic=\"news\", days=3)\n", "\n", "# 기존의 MCP 도구와 함께 사용합니다.\n", "tools = client.get_tools() + [tavily]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "langgraph 의 `create_react_agent` 를 사용하여 에이전트를 생성합니다." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "from langgraph.checkpoint.memory import MemorySaver\n", "from langchain_core.runnables import RunnableConfig\n", "\n", "# 재귀 제한 및 스레드 아이디 설정\n", "config = RunnableConfig(recursion_limit=30, thread_id=2)\n", "\n", "# 프롬프트 설정\n", "prompt = \"You are a smart agent with various tools. Answer questions in Korean.\"\n", "\n", "# 에이전트 생성\n", "agent = create_react_agent(model, tools, prompt=prompt, checkpointer=MemorySaver())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "새롭게 추가한 `tavily` 도구를 사용하여 검색을 수행합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "await astream_graph(agent, {\"messages\": \"오늘 뉴스 찾아줘\"}, config=config)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`retriever` 도구가 원활하게 작동하는 것을 확인할 수 있습니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "await astream_graph(\n", " agent,\n", " {\n", " \"messages\": \"`retriever` 도구를 사용해서 삼성전자가 개발한 생성형 AI 이름을 검색해줘\"\n", " },\n", " config=config,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Smithery 에서 제공하는 MCP 서버\n", "\n", "- 링크: https://smithery.ai/" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "사용한 도구 목록은 아래와 같습니다.\n", "\n", "- Sequential Thinking: https://smithery.ai/server/@smithery-ai/server-sequential-thinking\n", " - 구조화된 사고 프로세스를 통해 역동적이고 성찰적인 문제 해결을 위한 도구를 제공하는 MCP 서버\n", "- Desktop Commander: https://smithery.ai/server/@wonderwhy-er/desktop-commander\n", " - 다양한 편집 기능으로 터미널 명령을 실행하고 파일을 관리하세요. 코딩, 셸 및 터미널, 작업 자동화\n", "\n", "**참고**\n", "\n", "- smithery 에서 제공하는 도구를 JSON 형식으로 가져올때, 아래의 예시처럼 `\"transport\": \"stdio\"` 로 꼭 설정해야 합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from langchain_mcp_adapters.client import MultiServerMCPClient\n", "from langgraph.prebuilt import create_react_agent\n", "from langchain_anthropic import ChatAnthropic\n", "\n", "# LLM 모델 초기화\n", "model = ChatAnthropic(model=\"claude-3-7-sonnet-latest\", temperature=0, max_tokens=20000)\n", "\n", "# 1. 클라이언트 생성\n", "client = MultiServerMCPClient(\n", " {\n", " \"server-sequential-thinking\": {\n", " \"command\": \"npx\",\n", " \"args\": [\n", " \"-y\",\n", " \"@smithery/cli@latest\",\n", " \"run\",\n", " \"@smithery-ai/server-sequential-thinking\",\n", " \"--key\",\n", " \"89a4780a-53b7-4b7b-92e9-a29815f2669b\",\n", " ],\n", " \"transport\": \"stdio\", # stdio 방식으로 통신을 추가합니다.\n", " },\n", " \"desktop-commander\": {\n", " \"command\": \"npx\",\n", " \"args\": [\n", " \"-y\",\n", " \"@smithery/cli@latest\",\n", " \"run\",\n", " \"@wonderwhy-er/desktop-commander\",\n", " \"--key\",\n", " \"89a4780a-53b7-4b7b-92e9-a29815f2669b\",\n", " ],\n", " \"transport\": \"stdio\", # stdio 방식으로 통신을 추가합니다.\n", " },\n", " \"document-retriever\": {\n", " \"command\": \"./.venv/bin/python\",\n", " # mcp_server_rag.py 파일의 절대 경로로 업데이트해야 합니다\n", " \"args\": [\"./mcp_server_rag.py\"],\n", " # stdio 방식으로 통신 (표준 입출력 사용)\n", " \"transport\": \"stdio\",\n", " },\n", " }\n", ")\n", "\n", "\n", "# 2. 명시적으로 연결 초기화\n", "await client.__aenter__()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "langgraph 의 `create_react_agent` 를 사용하여 에이전트를 생성합니다." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "from langgraph.checkpoint.memory import MemorySaver\n", "from langchain_core.runnables import RunnableConfig\n", "\n", "config = RunnableConfig(recursion_limit=30, thread_id=3)\n", "agent = create_react_agent(model, client.get_tools(), checkpointer=MemorySaver())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "`Desktop Commander` 도구를 사용하여 터미널 명령을 실행합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "await astream_graph(\n", " agent,\n", " {\n", " \"messages\": \"현재 경로를 포함한 하위 폴더 구조를 tree 로 그려줘. 단, .venv 폴더는 제외하고 출력해줘.\"\n", " },\n", " config=config,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "이번에는 `Sequential Thinking` 도구를 사용하여 비교적 복잡한 작업을 수행할 수 있는지 확인합니다." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "await astream_graph(\n", " agent,\n", " {\n", " \"messages\": (\n", " \"`retriever` 도구를 사용해서 삼성전자가 개발한 생성형 AI 관련 내용을 검색하고 \"\n", " \"`Sequential Thinking` 도구를 사용해서 보고서를 작성해줘.\"\n", " )\n", " },\n", " config=config,\n", ")" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.8" } }, "nbformat": 4, "nbformat_minor": 2 }