🦀

MCPサーバをLangChain/LangGraphで使う2種類の方法

2025/02/23に公開

1. はじめに

1.1 目的

Anthropic社が2024年12月末に公開したMCP(Model Context Protocol)の仕様を元に、
世に沢山のMCPサーバのプログラムが公開されています。

最近は、Cursorで代表されるAIエディタ(WindsurfやCline)でも利用できるようになり、
エコシステムが拡大していると感じます。
そんな中、LangChain/LangGraphもMCPサーバに対応するためのAdaputerが公開されたので、
2種類の実装方法を試してみました。

1.2. 読者対象と前提知識

  • MCPサーバを利用されている方
  • LangChain/LangGraphの知識がある方

2. langchain-mcp-adapters

2.1 langchain-mcp-adaptersとは

langchain-mcp-adaptersのgithubを読むと、2つのことができると記述されています。
特徴:
🛠️ MCP ツールをLangGraphエージェントで使用できるLangChain ツールに変換する
📦 複数の MCP サーバーに接続し、そこからツールをロードできるクライアント実装

https://github.com/langchain-ai/langchain-mcp-adapters

Github上のREADMEにサンプルコードが記述されているので、簡単に試すことが出来ます。

2.2 利用するMCPサーバ

複数のMCPサーバが動作するか試すのに、以下の2種類のMCPサーバを利用します。

  1. 現在時刻を取得することができるMCPサーバ
    https://github.com/SecretiveShell/MCP-timeserver

  2. Tavilyを使ってWeb検索できるMCPサーバ
    https://github.com/Tomatio13/mcp-server-tavily

2は、自分で作ったMCPサーバなので使い方を理解しているのもありますが。

2.3 動作確認

REAMEを参考に以下のソースファイルを作成し、実行させて見ました。
※実行する前に、MCPサーバへのパスは修正してください。

# Create server parameters for stdio connection
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import AIMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_openai import ChatOpenAI
import os
import asyncio

async def main():
    x_api_key = os.environ["GROK_API_KEY"]
    base_url = 'https://api.x.ai/v1'
    model = ChatOpenAI(model="grok-2-latest", 
            base_url=base_url, api_key=x_api_key)

    async with MultiServerMCPClient() as client:
        # Tavily検索サーバーに接続
        await client.connect_to_server(
            "tavily",
            command="uv",
            args=[
                "--directory",
                "/path/to/mcp-server-tavily",
                "run",
                "tavily-search"
            ],
            env={
                "TAVILY_API_KEY": os.getenv("TAVILY_API_KEY"),
                "PYTHONIOENCODING": "utf-8"
            }
        )
        await client.connect_to_server(
            "timeserver",
            command="uvx",
            args=["--directory","/path/to/mcp-timeserver","mcp-timeserver"],
        )

        # エージェントの作成と実行
        agent = create_react_agent(model, client.get_tools())
        
        # メッセージの履歴を表示
        messages = []
        agent_response = await agent.ainvoke(
           {"messages": 
           "今日の日付を調べてから、来週末の鎌倉のイベントを調べて詳細を教えてください"})
        for message in agent_response["messages"]:
            if hasattr(message, 'content') and isinstance(message, AIMessage):
                messages.append(message.content)
        
        # すべての回答を表示
        print("\n".join(messages))

if __name__ == "__main__":
    asyncio.run(main())

実行すると、以下のように検索して調べてくれています。

2025-02-23 11:09:24,065 - tavily-search-server - INFO - Starting Tavily search server
2025-02-23 11:09:24,067 - tavily-search-server - INFO - Server initialized, starting main loop
2025-02-23 11:09:24,071 - mcp.server - INFO - Processing request of type ListToolsRequest
2025-02-23 11:09:24,071 - tavily-search-server - INFO - Listing available tools
2025-02-23 11:09:27,540 - mcp.server - INFO - Processing request of type CallToolRequest
2025-02-23 11:09:27,540 - tavily-search-server - INFO - TOOL_CALL_DEBUG: Tool called - name: search, arguments: {'query': '来週末 鎌倉 イベント', 'search_depth': 'advanced'}
2025-02-23 11:09:27,540 - tavily-search-server - INFO - Executing search with query: '来週末 鎌倉 イベント'
2025-02-23 11:09:31,015 - httpx - INFO - HTTP Request: POST https://api.tavily.com/search "HTTP/1.1 200 OK"
2025-02-23 11:09:31,015 - tavily-search-server - INFO - Search successful - Answer generated
2025-02-23 11:09:31,015 - tavily-search-server - INFO - Search successful - Results available
I am calling the `get-current-time` function to find out today's date.
今日は2025年2月23日です。来週末の鎌倉のイベントについて調べますね。
今日は2025年2月23日です。来週末の鎌倉のイベントについて調べた結果、以下のイベントがあります:

- **デジタルスタンプラリーイベント「#秘密のえのかま」**:2025年2月22日から3月23日まで開催。
- **湘南美術学院「基礎科祭2025」-10代が創る、未来のアート-**:2025年3月1日から2日まで開催。
- **三浦海岸桜まつり**:2025年2月5日から3月2日まで開催。桜の開花状況により変更の可能性あり。
- **桜ウェルカムドーム**:2025年3月1日から4月14日まで開催。
- **横浜ストロベリーフェスティバル**:2025年2月6日から3月2日まで開催。
- **珍奇の世界展**:2025年2月15日から3月30日まで開催。

これらのイベントについて、さらに詳しい情報が必要ですか?

現在時刻を調べた上で、TaivilyでWeb検索を行っています。
langchain-mcp-adaptersのソースを参照した感じ、LangGraphは使わずに実現しているようです。

3. langgraph-supervisorとの融合

3.1 langgraph-supervisorとは

langgraph-supervisorは以下の図のように、中央にスーパーバイザーがおり、複数のAgentを使って、目的を達成するためのアーキテクチャーを実現することができるライブラリです。

https://github.com/langchain-ai/langgraph-supervisor

Agentは、Toolを利用することができるので、このToolをMCPサーバを利用できれば、
世に公開されているMCPサーバが利用できるのでは?と思いませんか?

3.2 動作確認

以下、ソースコードですが、langchain-mcp-adaptersのREADMEに記述されている
単一のMCPサーバの実装方法で、AgentのToolに設定しています。

# Create server parameters for stdio connection
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

from langchain_mcp_adapters.tools import load_mcp_tools
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import AIMessage
from langchain_openai import ChatOpenAI
from langgraph_supervisor import create_supervisor
import os
import asyncio
import asyncio.subprocess

async def main():
    x_api_key = os.environ["GROK_API_KEY"]
    base_url = 'https://api.x.ai/v1'
    model = ChatOpenAI(model="grok-2-latest",
            base_url=base_url, api_key=x_api_key)

    # Tavily検索サーバーのパラメータ
    tavily_params = StdioServerParameters(
        command="uv",
        args=[
            "--directory",
            "/path/to/mcp-server-tavily",
            "run",
            "tavily-search"
        ],
        env={
            "TAVILY_API_KEY": os.getenv("TAVILY_API_KEY"),
            "PYTHONIOENCODING": "utf-8"
        }
    )

    # タイムサーバーのパラメータ
    time_params = StdioServerParameters(
        command="uvx",
        args=["--directory", "/path/to/mcp-timeserver", "mcp-timeserver"],
    )

    # 両方のサーバーに接続
    async with (
        stdio_client(tavily_params) as (tavily_read, tavily_write),
        stdio_client(time_params) as (time_read, time_write),
    ):
        async with (
            ClientSession(tavily_read, tavily_write) as tavily_session,
            ClientSession(time_read, time_write) as time_session,
        ):
            # 各セッションを初期化
            await tavily_session.initialize()
            await time_session.initialize()

            # 各エージェントのツールを取得
            tavily_tools = await load_mcp_tools(tavily_session)
            time_tools = await load_mcp_tools(time_session)

            # 専門エージェントを作成
            time_agent = create_react_agent(
                model=model,
                tools=time_tools,
                name="time_expert",
                prompt="あなたは現在時間を調べる専門家です。"
            )
            search_agent = create_react_agent(
                model=model,
                tools=tavily_tools,
                name="search_expert",
                prompt="あなたはWebサイトを検索する専門家です。"
            )

            # スーパーバイザーを作成
            supervisor = create_supervisor(
                agents=[time_agent, search_agent],
                model=model,
                prompt=(
                    "あなたはチームスーパーバイザーです。\n"
                    "- time_expertは現在時間の確認を担当します\n"
                    "- search_expertはWebサイトを検索する担当します\n"
                    "質問の内容に応じて適切な専門家に振り分けてください。\n"
                    "必要に応じて複数の専門家に質問を振り分けることもできます。"
                )
            ).compile()
            
            # メッセージの履歴を表示
            response = await supervisor.ainvoke({
                "messages": [
                    {
                        "role": "user",
                        "content": "今日の日付を調べてから、
                        来週末の鎌倉のイベントを調べて詳細を教えてください"
                    }
                ]
            })
            
            # 応答を表示
            for message in response["messages"]:
                if isinstance(message, AIMessage):
                    print(message.content)

if __name__ == "__main__":
    asyncio.run(main())

実行すると、以下のように検索して調べてくれています。

2025-02-23 11:31:33,880 - tavily-search-server - INFO - Starting Tavily search server
2025-02-23 11:31:33,882 - tavily-search-server - INFO - Server initialized, starting main loop
2025-02-23 11:31:34,189 - mcp.server - INFO - Processing request of type ListToolsRequest
2025-02-23 11:31:34,189 - tavily-search-server - INFO - Listing available tools
2025-02-23 11:31:39,388 - mcp.server - INFO - Processing request of type CallToolRequest
2025-02-23 11:31:39,388 - tavily-search-server - INFO - TOOL_CALL_DEBUG: Tool called - name: search, arguments: {'query': '鎌倉 次週末 イベント', 'search_depth': 'advanced'}
2025-02-23 11:31:39,388 - tavily-search-server - INFO - Executing search with query: '鎌倉 次週末 イベント'
2025-02-23 11:31:42,004 - httpx - INFO - HTTP Request: POST https://api.tavily.com/search "HTTP/1.1 200 OK"
2025-02-23 11:31:42,004 - tavily-search-server - INFO - Search successful - Answer generated
2025-02-23 11:31:42,004 - tavily-search-server - INFO - Search successful - Results available
I am transferring your request to check today's date to the time expert.
今日の日付は2025年2月23日です。次に、来週末の鎌倉のイベントについて調べます。
Transferring back to supervisor
I am transferring your request to search for events in Kamakura next weekend to the search expert.
鎌倉の来週末のイベントについて調べました。以下のイベントが見つかりました:

- **クリスマスマーケット in 横浜**
- **YOKOHAMA MINATOMIRAI WINTER HOLIDAY**
- **YOKOHAMA STRAWBERRY PARK**
- **長谷寺の桜見物**
- **東慶寺、円覚寺、鎌倉宮などでの紅葉**

他に知りたいことがあれば教えてください。
Transferring back to supervisor
他に知りたいことがあれば教えてください。

動きは、langchain-mcp-adaptersとほぼ同じですね。

4. 考察

4.1 使い分け

今回の例では、langchain-mcp-adaptersとlanggraph-supervisorの実行結果はほぼ同じでした。
使い分けるとすると・・

  • langchain-mcp-adapters :
    MCPサーバのみを利用することが前提で、素早くAgentを作成する
  • langgraph-supervisor :
    ToolがMCPサーバだけではなく、他の既存Toolと組み合わせて、Agentを作成する

とかでしょうか。
もう少しMCPサーバを利用数を多くしていくと、性能面など差がでてくるかもしれません。

4.2 実行性

両方共そうですが、タイムサーバーを使ったり、使わなかったりと、実行する毎に変わります。
Agentが人間っぽい感じになりますが、なかなか制御難しいです。
Workflowの方が良いのでは、と思ってしまいます。

5. 謝辞

LangChainチームの皆様、公開ありがとうございました。
MCPサーバをAIエディタだけでなく、プログラムからも利用しやすいのは非常助かります、

Discussion