MCPツールを使う汎用的なStreamlitチャットアプリを作って、いろんなMCPツールを使ってみた。

MCPがあれば何でもできる。

MCPとStreamlitを使って汎用的なチャットアプリを作ってみました。

今回は「**LangChain MCP Adapters**」を使用します。

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

「LangChain MCP Adapters」のREADMEをChatGPTにREADMEを要約してもらいました。

- LangChain MCP Adapters は、Anthropic Model Context Protocol (MCP) のツールを LangChain や LangGraph で使えるようにする軽量なラッパーです
- MCP ツールを LangChain のツールとして変換し、複数の MCP サーバーと接続できるクライアント実装を提供します
- 簡単なインストールとセットアップで、MCP ツールを LangGraph エージェントに統合できます

やってみよう！

## まずはただのチャットを作る

ライブラリーをインストールします。

```shell
pip install langchain langchain-aws langchain-mcp-adapters streamlit
```

Pythonスクリプトを書きます。

```python
import asyncio

import streamlit as st
from langchain_aws import ChatBedrockConverse
from langchain_core.messages import AIMessage, HumanMessage


async def main():
    st.title("Bedrock chat with MCP tools")

    if "messages" not in st.session_state:
        st.session_state.messages = []
    messages = st.session_state.messages

    for message in messages:
        with st.chat_message(message.type):
            st.write(message.content)

    if prompt := st.chat_input():
        with st.chat_message("human"):
            st.write(prompt)

        messages.append(HumanMessage(prompt))

        chat_model = ChatBedrockConverse(
            model="us.anthropic.claude-3-haiku-20240307-v1:0"
        )
        ai_response = await chat_model.ainvoke(messages)

        messages.append(ai_response)

        with st.chat_message("ai"):
            st.write(ai_response.content)


asyncio.run(main())
```

起動します。

```shell
streamlit run app.py 
```

ブラウザで`http://localhost:8501/`を表示します。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/82b0b9ae-2329-4884-a861-6edb07ca285b.png)

この状態から始めます。

## MCPツールを使用するよう修正していきます

`mcp_config.json`を作成します。今回は[Time MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/time)を使用します。

```json:mcp_config.json
{
    "mcpServers": {
        "time": {
            "command": "docker",
            "args": [
                "run",
                "-i",
                "--rm",
                "mcp/time"
            ],
            "transport": "stdio"
        }
    }
}
```

:::note
Claude DesktopでMCPを使用する際の設定ファイルとほぼ同じですが、`"transport": "stdio"`だけ追加してください。
:::

インポートを追加します。

```python
import json

from langchain_mcp_adapters.client import MultiServerMCPClient
```

`mcp_config.json`を読み込みます。

```python
        with open("mcp_config.json", "r") as f:
            config = json.load(f)
```

ここで「LangChain MCP Adapters」を使用します。

`MultiServerMCPClient`を使ってMCPツールを取得します。

```python
        async with MultiServerMCPClient(config["mcpServers"]) as mcp_client:
            tools = mcp_client.get_tools()
```

toolsの型は、`list[BaseTool]`です。なんと、MCPのツールがLangChainのツールになりました。

:::note
ここから先はもう、MCPの世界ではなくLangChainの世界の話になります。
:::

ツールをバインドしてBedrockを呼び出します。

```python
            ai_response = await chat_model.bind_tools(tools).ainvoke(messages)
```

ツールを使用する場合、Bedrockからの返答に含まれる`content`が配列になる場合があります。そのため出力処理を修正します。

```python
                if isinstance(ai_response.content, str): # 文字列の場合
                    with st.chat_message("ai"):
                        st.write(ai_response.content)
                elif isinstance(ai_response.content, list): # 配列の場合
                    for content in ai_response.content:
                        if content["type"] == "text":
                            with st.chat_message("ai"):
                                st.write(content["text"])
```

応答にツール実行要求が含まれている場合はツールを実行します。

```python
                if ai_response.tool_calls:
                    for tool_call in ai_response.tool_calls:
                        # LangChainツールの配列から、呼び出し対象のツールを取得
                        selected_tool = {tool.name.lower(): tool for tool in tools}[
                            tool_call["name"].lower()
                        ]
                        # ツールを実行
                        tool_msg = await selected_tool.ainvoke(tool_call)
```

ツール実行結果をメッセージに追加します。

```python
                        messages.append(tool_msg)
```

ツール呼び出し要求がなくなるまでBedrockを呼び続けます。Whileループで囲います。

```python
            while True:

                ...
                
                if ai_response.tool_calls:

                ...
                
                else:
                    break
```

これでツール呼び出し処理は完成です。


最後に、チャット履歴を表示する処理も一部変更します。（ツール呼び出しメッセージを表示しないように修正）

```python
    printable_messages = [
        message for message in messages if message.type in ["ai", "human"]
    ]

    for message in printable_messages:
        if isinstance(message.content, str):
            with st.chat_message(message.type):
                st.write(message.content)
        elif isinstance(message.content, list):
            for content in message.content:
                if content["type"] == "text":
                    with st.chat_message(message.type):
                        st.write(content["text"])
```

完成したソースがこちらです。

```python
import asyncio
import json

import streamlit as st
from langchain_aws import ChatBedrockConverse
from langchain_core.messages import HumanMessage
from langchain_mcp_adapters.client import MultiServerMCPClient


async def main():
    st.title("Bedrock chat with MCP tools")

    if "messages" not in st.session_state:
        st.session_state.messages = []
    messages = st.session_state.messages

    printable_messages = [
        message for message in messages if message.type in ["ai", "human"]
    ]

    for message in printable_messages:
        if isinstance(message.content, str):
            with st.chat_message(message.type):
                st.write(message.content)
        elif isinstance(message.content, list):
            for content in message.content:
                if content["type"] == "text":
                    with st.chat_message(message.type):
                        st.write(content["text"])

    if prompt := st.chat_input():
        with st.chat_message("human"):
            st.write(prompt)

        messages.append(HumanMessage(prompt))

        chat_model = ChatBedrockConverse(
            model="us.anthropic.claude-3-haiku-20240307-v1:0"
        )

        with open("mcp_config.json", "r") as f:
            config = json.load(f)

        async with MultiServerMCPClient(config["mcpServers"]) as mcp_client:
            tools = mcp_client.get_tools()

            while True:
                ai_response = await chat_model.bind_tools(tools).ainvoke(messages)

                messages.append(ai_response)

                if isinstance(ai_response.content, str):
                    with st.chat_message("ai"):
                        st.write(ai_response.content)
                elif isinstance(ai_response.content, list):
                    for content in ai_response.content:
                        if content["type"] == "text":
                            with st.chat_message("ai"):
                                st.write(content["text"])

                if ai_response.tool_calls:
                    for tool_call in ai_response.tool_calls:
                        selected_tool = {tool.name.lower(): tool for tool in tools}[
                            tool_call["name"].lower()
                        ]
                        tool_msg = await selected_tool.ainvoke(tool_call)
                        messages.append(tool_msg)
                else:
                    break


asyncio.run(main())
```


:::note
コンパクトに纏まってますね！
:::

## 色々やってみた

では、色々MCPツールを登録して動作させてみました。

:::note
※ツールの実行結果を表示するように少しだけ修正しました。

```diff_python
                if ai_response.tool_calls:
                    for tool_call in ai_response.tool_calls:
+                       status = st.status(
+                           f"Tool Call: {tool_call['name']}", expanded=True
+                       )
+                       status.write(tool_call['args'])
                        selected_tool = {tool.name: tool for tool in tools}[
                            tool_call["name"].lower()
                        ]
                        tool_msg = await selected_tool.ainvoke(tool_call)
+                       status.update(state="complete")
                        messages.append(tool_msg)
                else:
                    break
```

:::

### Time MCP Server

https://github.com/modelcontextprotocol/servers/tree/main/src/time

> 時間とタイムゾーンの変換機能を提供するモデル コンテキスト プロトコル サーバー。このサーバーにより、LLM は現在の時間情報を取得し、IANA タイムゾーン名を使用してタイムゾーン変換を実行し、システムのタイムゾーンを自動検出できるようになります。

上で解説した設定ファイルを使用します。

```json:mcp_config.json
{
    "mcpServers": {
        "time": {
            "command": "docker",
            "args": [
                "run",
                "-i",
                "--rm",
                "mcp/time"
            ],
            "transport": "stdio"
        }
    }
}
```

![localhost_8501_ (10).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/924d499e-cfce-4cd0-ac7a-9f2ba7faff54.png)


### Brave Search MCP Server

https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search

> Brave Search API を統合し、Web 検索機能とローカル検索機能の両方を提供する MCP サーバー実装

`env`の`YOUR_API_KEY_HERE`は、APIキーに置き換えてください

```json:mcp_config.json
{
    "mcpServers": {
        "brave-search": {
            "command": "docker",
            "args": [
                "run",
                "-i",
                "--rm",
                "-e",
                "BRAVE_API_KEY",
                "mcp/brave-search"
            ],
            "env": {
                "BRAVE_API_KEY": "YOUR_API_KEY_HERE"
            },
            "transport": "stdio"
        }
    }
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/fdb1807c-f21c-427a-a4ad-a56de8298131.png)

### Fetch MCP Server

https://github.com/modelcontextprotocol/servers/tree/main/src/fetch

> Web コンテンツの取得機能を提供するモデル コンテキスト プロトコル サーバー。このサーバーにより、LLM は Web ページからコンテンツを取得して処理し、HTML をマークダウンに変換して簡単に利用できるようになります。

MCPツールは複数指定できますので、Brave検索とFetchを指定します。

```json:mcp_config.json
{
    "mcpServers": {
        "brave-search": {
            "command": "docker",
            "args": [
                "run",
                "-i",
                "--rm",
                "-e",
                "BRAVE_API_KEY",
                "mcp/brave-search"
            ],
            "env": {
                "BRAVE_API_KEY": "YOUR_API_KEY_HERE"
            },
            "transport": "stdio"
        },
        "fetch": {
            "command": "docker",
            "args": [
                "run",
                "-i",
                "--rm",
                "mcp/fetch"
            ],
            "transport": "stdio"
        }
    }
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/2b237032-4162-4998-95ce-ececcea41632.png)

ツールを連続で呼び出してくれています。

### Chroma MCP Server

https://github.com/chroma-core/chroma-mcp

> このサーバーは Chroma を利用したデータ取得機能を提供し、AI モデルが生成されたデータとユーザー入力に基づいてコレクションを作成し、ベクトル検索、全文検索、メタデータ フィルタリングなどを使用してそのデータを取得できるようにします。

Dockerイメージが公開されていないので、Dockerfileからビルドします。

```shell
git clone https://github.com/chroma-core/chroma-mcp.git
cd chroma-mcp
docker build -t chroma-mcp .
```

```json:mcp_config.json
{
    "mcpServers": {
        "chroma": {
            "command": "docker",
            "args": [
                "run",
                "-i",
                "--rm",
                "-v",
                "/tmp/chroma-mcp:/tmp/chroma-mcp",
                "chroma-mcp",
                "chroma-mcp",
                "--client-type",
                "persistent",
                "--data-dir",
                "/tmp/chroma-mcp"
            ],
            "transport": "stdio"
        }
    }
}
```

コレクションを作成します。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/a2963639-6e5e-46fd-b0d5-282201ca7c57.png)

ドキュメントを登録します。
※埋め込みモデルをダウンロードしてインストールします。Chromaサーバーを別途立ち上げて接続する形が良さそうです。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/76cdf3d5-7282-4de0-ada1-54a383d6e04b.png)

登録ドキュメントを取得します。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/7ed1870c-d962-4b1b-b73d-a38be642dcec.png)

エスケープされてますが、一応取得はできました。

（Streamlitがずっと処理中なのが気になるところ）

### Playwright MCP

https://github.com/microsoft/playwright-mcp

> Playwright を使用してブラウザ自動化機能を提供するモデル コンテキスト プロトコル (MCP) サーバー。このサーバーにより、LLM は構造化されたアクセシビリティ スナップショットを通じて Web ページと対話できるようになり、スクリーンショットや視覚的に調整されたモデルが不要になります。

話題のPlaywright MCPです。

Dockerfileが提供されていないので自作しました。

```dockerfile
FROM node:lts-bookworm

RUN npm install -g playwright @playwright/mcp@latest && \
    npx playwright install --with-deps chrome

ENTRYPOINT [ "mcp-server-playwright" ]
```

ビルドします。

```shell
docker build -t playwright-mcp .
```

```json:mcp_config.json
{
    "mcpServers": {
        "playwright": {
            "command": "docker",
            "args": [
                "run",
                "-i",
                "--rm",
                "-v",
                "/tmp:/tmp",
                "playwright-mcp",
                "--headless"
            ],
            "transport": "stdio"
        }
    }
}
```

GoogleにアクセスしてPDFにしてみます。

![localhost_8501_ (14).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/e4e42d06-1476-4d35-9fbe-75afe5d482e9.png)

うまくPDFを作成してくれてます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/d849e31a-0dd0-4dcc-80f9-a91ea0a4a20a.png)

:::note
今回の実装では、一回の会話のターンでMCPツールとの接続を閉じる実装になっています。（多分）
なので、会話をまたいでブラウザ操作を指せることはできないと思います。（多分）
:::

### Claude Code

https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/tutorials#use-claude-code-as-an-mcp-server

Claude CodeをMCPサーバーとして利用します。

Claude Codeの[GitHubリポジトリ](https://github.com/anthropics/claude-code)から、[`Dockerfile`](https://github.com/anthropics/claude-code/blob/main/.devcontainer/Dockerfile)と[`init-firewall.sh`](https://github.com/anthropics/claude-code/blob/main/.devcontainer/init-firewall.sh)をコピーしてきます。

ビルドします。

```shell
docker build -t claude-code .
```

```json:mcp_config.json
{
    "mcpServers": {
        "claude-code": {
            "command": "docker",
            "args": [
                "run",
                "-i",
                "--rm",
                "claude-code",
                "claude",
                "mcp",
                "serve"
            ],
            "transport": "stdio"
        }
    }
}
```

カレントディレクトリを取得してみます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/de3f3201-5072-4806-8424-22a24304ff15.png)

ファイルの作成はうまくいくようですが、`command`がうまく動作しないようでした。（理由不明）

↓のBashツールの応答取得ができず、ずっと待ち状態です。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/f92efde3-cd95-4720-8074-775a3b09a919.png)



## まとめ

`mcp_config.json`に必要なMCPツールを追記するだけで、そのツールを使ったチャットアプリが作れます！

おすすめです。

（MCPを使ったClaude Desktopっぽいものを、Claude以外の生成AIモデルで構築できますよ！）

## おまけ

こちらのGitHubで公開しております。

今回紹介した内容以外にいくつか機能を追加しています。

- OpenAIのモデルやGeminiなどにも対応
- チャットのスレッドに対応
- ツール利用を促すシステムプロンプト作成機能

https://github.com/moritalous/bedrock-mcp-streamlit