DifyとOutlineをMCP(SSE)でつないでチャットボットにドキュメントを読んでもらう

# はじめに

[技術チャレンジ部](https://challenge-club.connpass.com/)のとも(Tomo)です。
以前作った、[DifyとDiscordやSlackをつなぐチャットボット](https://qiita.com/tomo-v/items/6aeec8c27cc5586d4fa8)に、ナレッジを持たせたい！と思い、DifyでRAGを色々試していましたが、チャンク分割と検索というアプローチがうまくマッチしないケースもあると感じていました。
技術チャレンジ部では、ブラウザのみでwikiのように使えるOutlineに、VR活動で訪問したVRChatワールドの情報などを蓄えていました。
OutlineにはAPIもあり、世の中のコンテキストウィンドウの拡大も著しく、これを丸ごと読ませられれば、またRAGとは異なる結果になるのでは…?
そこで今回は、Outlineをナレッジとして活用できるか、チャレンジします。

# ご注意

:::note warn
いずれも執筆時点で「試してみた」レベルの記事となっています。
日々進化している領域で、自分の理解が追いついていない部分があります。
特にセキュリティにはご注意ください。
:::

# 元の環境

- Ubuntu 22.04 Server
    - Docker Engine上で動作する、セルフホストのDify
    - Docker Engine上で動作する、セルフホストのOutline
    - DiscordやSlackとDifyをつなぐ、自作Pythonスクリプトのチャットボット

# 全体設計

世の中MCPが話題なので、チャットボット <-> Dify <-> MCP <-> Outline という構成を検討します。
OutlineでMCPを使えないかと調べたところ、いくつか実装があるようです(未比較)。

- https://github.com/Vortiago/mcp-outline
- https://github.com/mmmeff/outline-mcp-server

他にも、MCPを集約して利用するようなサービスがいくつかあるかと思います(未調査)。
元環境がセルフホストということもあり、試しに[mcp-outline](https://github.com/Vortiago/mcp-outline)を使ってみます。

# mcp-outline
## インストール
### uv
[READMEのInstallation](https://github.com/Vortiago/mcp-outline?tab=readme-ov-file#installation)を読むと、[uv](https://docs.astral.sh/uv/)を使うようです。
今回は、[uvのInstallation](https://docs.astral.sh/uv/#installation)の最初にある、curlでインストールしてみます。

```shell-session
curl -LsSf https://astral.sh/uv/install.sh | sh
```

uvでvenvを作れるということで、作ってみます。

```shell-session
uv venv mcp-outline-venv
source mcp-outline-venv/bin/activate
cd mcp-outline-venv
```

### mcp-outline
[READMEのInstallation](https://github.com/Vortiago/mcp-outline?tab=readme-ov-file#installation)の通りに行います。

```shell-session
git clone https://github.com/Vortiago/mcp-outline.git
cd mcp-outline
uv pip install -e ".[dev]"
```

## 設定
[READMEのConfiguration](https://github.com/Vortiago/mcp-outline?tab=readme-ov-file#configuration)の通り、`.env`にOutlineのAPIキーとURLを設定します。
- `OUTLINE_API_KEY`: 設定 -> アカウント -> APIキー -> (画面右上の)新しいAPIキー… で生成
- `OUTLINE_API_URL`: `http://localhost:3000`など

## 起動
早速動かそうとしましたが、MCPに無知すぎて、何を行えばよいか、全くわかりません。
[READMEのInstallation](https://github.com/Vortiago/mcp-outline?tab=readme-ov-file#installation)には、`mcp dev src/mcp_outline/server.py`と、`./start_server.sh`があり、どういう関係かわかりません。Dify側も、どうすればいいか、わかりません。
MCPのことを理解するのも目的の1つなので、`tcpdump`なども活用しながら、理解を進めました。

# MCPの調査
試行錯誤で理解したのは、以下のような点です。
(無知すぎて、間違っているかも&明日には世の中が変わっているかも、すいません)
- MCPには、現状、[stdio](https://modelcontextprotocol.io/docs/concepts/transports#standard-input%2Foutput-stdio)と[SSE](https://modelcontextprotocol.io/docs/concepts/transports#standard-input%2Foutput-stdio)がある
    - stdioは、標準入出力を使う
    - SSEは、Server-Sent Eventsを使う (Dify APIでSSEに少し触れていてよかった…)
        -  今後、[Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)に変わっていく模様
    - 認証は、[MCPの2025-03-26の仕様で規定された](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization)が、まだサポートされていない実装もありそう
- [MCP Inspector](https://github.com/modelcontextprotocol/inspector)という、ブラウザからMCPの動作を確認できるツールがある
    - mcp-outlineでは、`mcp dev src/mcp_outline/server.py`で起動できる
    - 起動すると、[MCP Inspector client UI(MCPI)とMCP Proxy(MCPP)がそれぞれ起動する](https://github.com/modelcontextprotocol/inspector?tab=readme-ov-file#from-an-mcp-server-repository)
        - MCPIは、起動時のコンソールログより、`http://127.0.0.1:6274`でアクセスできる (アドレスの変え方はわからない)
        - MCPPは、6277でアクセスできるが、MCPプロセスと通信できてしまうので、セキュリティ的に公開しないほうがよい (と[書かれている](https://github.com/modelcontextprotocol/inspector?tab=readme-ov-file#security-considerations)が、`ss`で調べると、6277はデフォルトでは*でlistenしている)
- mcp-outlineの実装は、[ClaudeとWSL2環境](https://github.com/Vortiago/mcp-outline/blob/main/start_server.sh)でのstdioが想定されている
    - [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk)を[用いて](https://github.com/Vortiago/mcp-outline/blob/main/uv.lock)いる

# MCP Inspectorの使用

色々わからなすぎるので、まずMCP Inspectorを使って雰囲気を手動で確認します。

## 起動

venvを有効化し、MCP Inspectorを起動します。

```shell-session
source .venv/bin/activate
mcp dev src/mcp_outline/server.py
```

なお、起動しても、コンソールには何も表示されません。

## sshポートフォワーディング

MCP Inspector client UIは`http://127.0.0.1:6274`でアクセスできますが、リモートでホストするため、ブラウザの動く手元のPCからsshでポートフォワーディングします。

```
ssh -L 6274:localhost:6274 remotehost -N
```

## ブラウザからの接続

ブラウザで`http://localhost:6274`に接続します。

## stdioでの使用 (あまり試せていません)

(画面左下の)Connectボタンを押せばすぐ使える…と思いましたが、当方ポンコツのため色々ひっかかりました。
結果として、以下の設定が必要でした。

- Arguments -> Configuration -> Inspector Proxy Address に、httpで始まるMCPPのURL(`http://localhost:6277`など)を入力する
    - MCPIからアクセスするURLのはずなので、`localhost`などでも大丈夫
- (必要に応じて)環境変数を追加する

### Connecting to MCP Inspector Proxyエラー
Connectボタンを押すと、画面左下に`Error Connecting to MCP Inspector Proxy - Check Console logs`と出たので、試行錯誤しました。
結果として、Arguments -> Configuration -> Inspector Proxy Address には、`http://remotehost:6277`など、MCPPのアドレスを`http://`から入力する必要があるようです。

### uvのVIRTUAL_ENVエラー
これでConnectできるかと思いきや、画面左下に
```
warning: `VIRTUAL_ENV=/home/tomo/mcp-outline-venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
```

といったエラーが出続けました。軽く調べた範囲では、`UV_PROJECT_ENVIRONMENT`と、`VIRTUAL_ENV`が一致していれば、回避できるようです。

Arguments -> Environment Variables -> Add Environment Variable で環境変数を追加し、`UV_PROJECT_ENVIRONMENT`に`VIRTUAL_ENV`の内容(フルパス)を書いてみます。(最適解でない可能性あり)

### 接続後
接続すると、

- Command: `uv`
- Arguments: `run --with mcp mcp run src/mcp_outline/server.py`

が、それぞれ入力されていました。これで色々実験できそうです。

## SSEでの使用
### SSEサーバーの起動
mcp-outlineをSSEに対応させるために、[`src/mcp_outline/server.py`](https://github.com/Vortiago/mcp-outline/blob/main/src/mcp_outline/server.py)の`mcp.run()`を、`mcp.run(transport="sse")`に書き換えた後、SSEサーバーを起動します。

```
source .venv/bin/activate
bash ./start_server.sh
```

### 設定
- stdioと同じように、Arguments -> Configuration -> Inspector Proxy Address に、httpで始まるMCPPのURL(`http://localhost:6277`など)を入力
- (画面左上の)URLに、SSEサーバーのアドレス(デフォルトでは`http://localhost:8000/sse`でアクセスできるはず)を入力

これでConnectできるようになりました。

<img width="150" alt="settings.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764521/e32d12fe-f9bb-4bbc-be04-065e33af5b23.png">

# MCP InspectorによるOutlineの挙動確認
LLMの気持ちになって(?)MCP InspectorでOutlineの挙動の確認することで、LLMからOutlineがどう見えるのか、よくわかりました。
この理解が、後々のLLMのプロンプト改善に役立ちました。

## MCP Inspectorの操作

詳細は[本家](https://modelcontextprotocol.io/docs/tools/inspector#inspector)に譲りますが、下記を試してみました。

- (画面上の)Tools -> List Tools で、Toolの一覧が取得できる
    - 取得後、一覧にある各Toolをクリックすることで、Toolを呼び出せる
    - 呼び出した結果は(画面右の)Tool Resultに表示され、JSONは(画面下の)Historyにtool/callとして格納されるので、展開して見られる
        - HistoryのResponse内の長い文字列は折りたたまれているので、クリックで展開して見られる
- Historyは、新しい操作が上になる模様

## MCPで呼び出せるOutlineの挙動

気になった挙動をいくつかピックアップします。

- 呼び出せるToolの説明が(英語で)取得できる
- コレクション(Outlineの大分類)の一覧が取得できる
    - コレクションのタイトルだけでなく、コレクションに直接書かれた内容も取得できる
    - コレクション配下のドキュメント一覧は、コレクション一覧には入ってこない
        - コレクションのIDを指定すれば、ドキュメントの一覧を取得できる
- 検索機能がある

基本はOutlineのAPIそのものかと思います。LLMは、自然言語の説明を読んでToolを活用するというわけですね。

MCP Inspectorではこんな感じで見られます。

- Tools
    - ![tools.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764521/4d735950-81a6-464f-8597-eefcc023c630.png)
- History
    - <img width="350" alt="history.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764521/7636b777-3c23-424b-9eb0-499e5a6dad2a.png">

# Difyからの呼び出し

ここまででmcp-outlineが動かせるようになりました。MCP Inspectorを停止し、DifyからMCPで接続します。
Difyでは、マーケットプレースにて[MCP SSE](https://marketplace.dify.ai/plugins/junjiem/mcp_sse)と[MCP Agent Strategy](https://marketplace.dify.ai/plugins/hjlarry/agent)がプラグインとしてインストールできるようです。
いずれもlanggeniusによるものではなさそうですが、[dify.aiのblogで紹介されていました](https://dify.ai/blog/dify-mcp-plugin-hands-on-guide-integrating-zapier-for-effortless-agent-tool-calls)。また、SSEに対応しているようです。
今回はチャットフローで使いたいので、MCP Agent Strategyを使ってみます。
mcp-outlineは、SSEで使用するので、`start_server.sh`でSSEサーバーを起動しておきます。

## MCP Agent Strategyのインストール

(画面右上の)プラグイン -> (画面左上の)マーケットプレイスを探索する -> (画面中の)エージェント戦略 で、MCP Agent Strategyをインストールします。

## DockerのDifyからホスト上のサービスへの接続

元々、DifyのDockerコンテナからホスト上のサービスに接続できるように、Difyの`docker-compose.override.yaml`の`extra_hosts:`で`- "host.docker.internal:host-gateway"`を設定していました。これをこのまま利用します。
`docker-compose.override.yaml`では、`api`と`plugin_daemon`の`service`に対して、`host.docker.internal`を設定していました。(どちらが必須かなど、詳細未調査)

## チャットフロー

エージェントノードを追加し、以下のように設定してみました。

- エージェンティック戦略: MCP Agent -> MCP FunctionCall を設定
- MCP Server URL: `{  "outline": {    "url": "{{#env.mcp_url#}}",    "headers": {}, "timeout": 60,    "sse_read_timeout": 300  }}`
    - (画面右上の)ENVで、環境変数`mcp_url`に、エンドポイントも含む`http://host.docker.internal:8000/sse`を設定
- Maximum Iterations: デフォルトの`3`では少なそうなので、とりあえず`5`を設定

# 実行と解析

これでついに質問ができる! と思い、DifyのプレビューでOutlineに含まれていそうな情報の質問をしてみます。…が、微妙な回答…。

## Difyでの解析

Difyの会話ログ -> 実行追跡 -> エージェント(ノードの名前) -> function calling -> 詳細情報 で、エージェントの各Roundの挙動をJSONレベルで確認します。
すると、LLMがまずOutlineの検索機能でキーワード検索し、検索にかからなかったのであきらめたりしている様子が見えました。

## Outline

LLMの気持ちになって、Outlineの画面で単語を入れて色々検索してみます。
うーん、確かにこれは厳しいかも…。

# 改善
## Outlineのドキュメント構造

MCP Inspectorでの確認で、Outlineのコレクション一覧を取得する際に、コレクションに直接書かれた内容も取得できることがわかっています。
そこで、コレクションのトップ階層に、目次として、各ドキュメントにどんなことが書いてあるか、ドキュメントへのリンクとセットで記述しておきます。リンクには、ドキュメントのIDが入っているので、該当ドキュメントを取得してくれると期待します。

<img width="450" alt="collection.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764521/79f25034-890a-4fb9-ba95-d7e0f233c831.png">

## DifyのLLMプロンプト

Maximum Iterationsが5回だと、検索などを試しているうちに、最大回数に近づいてしまいそうです。
そこで、Maximum Iterationsを8回にしつつ、エージェントノードのInstructionのプロンプトで、

> ドキュメントの調査は、Outlineの検索機能は使わず、コレクションリストの内容から関連しそうなドキュメントIDを探し出す方法で行い、そのIDのドキュメント内容を取得・解釈した上で回答して下さい

などと指定してみます。

## SSEサーバーのlistenアドレス変更

SSEサーバーのlistenアドレスを`ss`で確認すると、`0.0.0.0:8000`になっていました。
気分レベルの対処ですが、Docker内部からhost.docker.internalで解決されるデフォルトブリッジのアドレスに変えてみます。
`ip addr show dev docker0`で見ると、この環境でのアドレスはデフォルトの`172.17.0.1`のようです。
変え方がわからなかったのですが、MCP Python SDKの[fastmcpのserver.py](https://github.com/modelcontextprotocol/python-sdk/blob/babb477dffa33f46cdc886bc885eb1d521151430/src/mcp/server/fastmcp/server.py)のコードを読むと、`FASTMCP_HOST`という環境変数で設定できそうだったので、`.env`に`FASTMCP_HOST="172.17.0.1"`を追記したところ、効果がありました。

## SSEサーバーのsystemd --user化

SSEサーバーをsystemd --userで管理するために、.serviceファイルを設置しました。(内容は適当です)
```~/.config/systemd/user/mcp-outline.service
[Unit]
Description=mcp sse server for outline
After=network.target

[Service]
WorkingDirectory=/home/tomo/mcp-outline-venv/mcp-outline
ExecStart=bash start_server.sh
Restart=always
Environment="PYTHONUNBUFFERED=1"

[Install]
WantedBy=default.target
```

その後、起動・確認しました。

```
systemd --user daemon-reload
systemd --user enable --now mcp-outline.service
systemd --user status mcp-outline.service
```

ユーザーがログアウトしても動き続けるように、だいぶ前に、lingeringを有効にしていた…と思われる…。

```
sudo loginctl enable-linger $(USER)
```

# 改めて実行

<img width="400" alt="discord.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3764521/9d8ef082-d496-4bbd-9ce9-1e31cdd0b1ff.png">

やったね!

# 感想
未知の世界で、AI自身のMCP関連知識もまだ少なめの模様、自分の中で総力戦でした。
組織でのナレッジ活用に向けて、情報の単位の切れ目をどう表現するか、コンテキストウィンドウとコストにどう合わせるか、それをどうLLMに伝えるか、その結果をどうキャッシュするかなど、今後進化があるかもと思いました。
LLM側の進化で、LLMが既存の情報構造に歩み寄ってくれるかもですが…。

# 課題と今後
- mcp-outline
    - 再起動や停止に時間がかかる: グレースフルな終了には対処が必要そう
    - セキュリティ: MCP Python SDKの認証の実装進展に期待(自分でやりましょう＞私)
- Outline
    - APIキーへのパーミッション: 詳細を調べて、適切なパーミッションにしたい
        - 今は自然言語で「変更しないでね」とLLMにお願い…指示追従性は高まっているそうですが、AIなので…
- Dify
    - (チャットフローではなく)エージェントで作る場合: プラグインと挙動の調査
- 技術チャレンジ部でのユースケース
    - VR活動: VRChatの訪問先を自動的にデータベース化したい!(お気に入りがもういっぱいで…)
    - [自動運転AIチャレンジ](https://www.jsae.or.jp/jaaic/2024ver/): GitHubのソースコードもAIチャットボットに見て欲しい!

# 謝辞
- VR活動の記録を残し続けていただいている[たけ](https://qiita.com/hide_take)さん、いつも本当にありがとうございます!
- 多数の記事に助けられました! ありがとうございます!

https://dify.ai/blog/dify-mcp-plugin-hands-on-guide-integrating-zapier-for-effortless-agent-tool-calls

https://note.com/kazu_t/n/na0065addc7fb

https://nikkie-ftnext.hatenablog.com/entry/model-context-protocol-quickstart-python-sse-transport

https://times.serizawa.me/p/mcp-changelog-2025-03-26

https://zenn.dev/atamaplus/articles/5bf2bc1acef5b1

https://qiita.com/baku2san/items/adeeb82dd6a9b18f1887

https://qiita.com/mechamogera/items/b1bb9130273deb9426f5