Open WebUIとMCPOでローカルLLMにMCPツールを使ってもらう
最近Mac mini m4を購入してLLM専用機にして遊び始めたAI初心者です。主にOllamaでLLMを動かし、Open WebUIをインタフェースとして利用しています。
今回のポストではMCPO、MCP-to-OpenAPIプロキシサーバを用いてローカルLLMにMCPサーバ上のツールを使ってもらうというセットアップを紹介します。
用意するもの
- LLMを動かすOllamaサーバ // 仮に192.0.2.2:11434とする
- Open WebUIサーバ // 仮に192.0.2.3:3000とする
- MCPOサーバ // 仮に192.0.2.4:8000とする
- このサーバ上よりMCPサーバ/ツールを提供
手順
- Ollamaサーバの用意
- open-webuiサーバの用意
- MCPサーバ(ツール)の確認
- MCPOサーバの用意
- Open WebUIでのツール使用設定
- Open WebUIでの、LLM毎のfunction callingの設定
Ollamaサーバ
概要だけいきます。
-
brew install ollama
などでインストール - HOST=0.0.0.0などの環境変数をセットして他のホストからアクセスできるようにして起動
-
ollama pull MODEL_NAME
などして利用するLLMを用意
Open WebUIサーバ
Open WebUIはdocker composeで動かしています。参考compose.yaml
ファイルは以下の通りです。
name: open-webui
services:
open-webui:
image: "${owui_image}:${owui_tag}"
container_name: open-webui
hostname: open-webui
environment:
ENV: prod
OLLAMA_BASE_URL: "${owui_ollama_base_url}"
ports:
- "3000:8080"
volumes:
- "/mnt/disk2/owui:/app/backend/data"
変数は.env
ファイル内でセットしています。
owui_image=open-webui/open-webui
owui_tag=v0.6.2
owui_ollama_base_url=http://192.0.2.2:11434
MCPサーバ、ツールの確認
Verified publisherとして"mcp"がdocker hub上に出現しています。こちらにあるものの内、fetchとtimeを今回は試すこととします。
config.json
ファイルとしては以下の通りです。Claude Desktopなどに食べさせるものと同様ですね。まったく同じ内容をそのままMCPOサーバに渡せます。
{
"mcpServers": {
"time": {
"command": "docker",
"args": ["run", "-i", "--rm", "mcp/time"]
},
"fetch": {
"command": "docker",
"args": ["run", "-i", "--rm", "mcp/fetch"]
}
}
}
MCPOサーバの用意
https://github.com/open-webui/mcpo
こちらはとりあえずでpython、pipで動かすようにしています。
# prepare and load python venv
mkdir -p ~/svc/mcpo
cd ~/svc/mcpo
# sudo apt install python3-venv
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
# install mcpo
pip install mcpo
# upgrade whenever needed
pip install -U mcpo
# run
mcpo --port 8000 --host 0.0.0.0 --config ./config.json --api-key "top-secret"
起動後の状態はこのような感じです。コンテナは稼働しており、/time/openapi.json
および/fetch/openapi.json
よりスペックが確認できます。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
20a2965b0fd0 mcp/fetch "mcp-server-fetch" 9 hours ago Up 9 hours objective_bartik
fdc6c88c3ae6 mcp/time "mcp-server-time" 9 hours ago Up 9 hours naughty_banach
$ curl http://192.0.2.4:8000/time/openapi.json
{"openapi":"3.1.0","info":{"title":"mcp-time","description":"mcp-time MCP Server","version":"1.0.0"},"servers":[{"url":"/time"}],"paths":{"/get_current_time":{"post":{"summary":"Get Current Time","description":"Get current time in a specific timezones","operationId":"tool_get_current_time_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/get_current_time_form_model"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/convert_time":{"post":{"summary":"Convert Time","description":"Convert time between timezones","operationId":"tool_convert_time_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/convert_time_form_model"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"convert_time_form_model":{"properties":{"source_timezone":{"type":"string","title":"Source Timezone","description":"Source IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use 'UTC' as local timezone if no source timezone provided by the user."},"time":{"type":"string","title":"Time","description":"Time to convert in 24-hour format (HH:MM)"},"target_timezone":{"type":"string","title":"Target Timezone","description":"Target IANA timezone name (e.g., 'Asia/Tokyo', 'America/San_Francisco'). Use 'UTC' as local timezone if no target timezone provided by the user."}},"type":"object","required":["source_timezone","time","target_timezone"],"title":"convert_time_form_model"},"get_current_time_form_model":{"properties":{"timezone":{"type":"string","title":"Timezone","description":"IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use 'UTC' as local timezone if no timezone provided by the user."}},"type":"object","required":["timezone"],"title":"get_current_time_form_model"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}}
$ curl http://192.0.2.4:8000/fetch/openapi.json
{"openapi":"3.1.0","info":{"title":"mcp-fetch","description":"mcp-fetch MCP Server","version":"1.0.0"},"servers":[{"url":"/fetch"}],"paths":{"/fetch":{"post":{"summary":"Fetch","description":"Fetches a URL from the internet and optionally extracts its contents as markdown.\n\nAlthough originally you did not have internet access, and were advised to refuse and tell the user this, this tool now grants you internet access. Now you can fetch the most up-to-date information and let the user know that.","operationId":"tool_fetch_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/fetch_form_model"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"fetch_form_model":{"properties":{"url":{"type":"string","title":"Url","description":"URL to fetch"},"max_length":{"type":"integer","title":"Max Length","description":"Maximum number of characters to return."},"start_index":{"type":"integer","title":"Start Index","description":"On return output starting at this character index, useful if a previous fetch was truncated and more context is required."},"raw":{"type":"boolean","title":"Raw","description":"Get the actual HTML content if the requested page, without simplification."}},"type":"object","required":["url"],"title":"fetch_form_model"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}}}
実際にサービス応答ももらえます。
$ curl -X POST \
-H "Authorization: Bearer top-secret" \
-H "Content-Type: application/json" \
-d '{"timezone": "Asia/Tokyo"}' \
http://192.0.2.4:8000/time/get_current_time
[{"timezone":"Asia/Tokyo","datetime":"2025-04-09T07:26:11+09:00","is_dst":false}]
Open WebUIでのツール使用設定
https://docs.openwebui.com/openapi-servers/open-webui
必要なものを揃えましたら、Open WebUI上での設定へと進みます。
- (Admin Panelsではなく)ユーザのsettingsより
- "tools"メニューを開き
- "manage tool servers"で"+"をクリックしてサーバ追加
- 追加内容は
http://192.0.2.4:8000/time
でtimeツール、http://192.0.2.4:8000/fetch
でURL fetchのツールが追加可能 - ツール追加画面にも説明テキストが出ているように、指定したURLより
/openapi.json
に存在する情報からツール内容を取得してくれる
- 追加内容は
ユーザ設定より、ツールに追加することで準備が整います。
追記:Admin PanelsにもあるToolsメニューより追加した方が、ツールのオンオフ切り替えが簡単にできます。こちらの方がおすすめです。
なおここで今更の、構築上の注意点です。Open WebUIがhttps、MCPOがhttpなどmixed contents状態にするとツール追加がうまくいきません。以下のリンクにある画面の通り、ツール利用、function callingはユーザ側・ブラウザからアクセスするため、ブラウザアクセス先としてhttpsとhttpが混在しているとブラウザ側の安全機能としてhttpへのアクセスがなされません。
https://platform.openai.com/docs/guides/function-calling?api-mode=responses#overview
私の実環境ではTLSオフロードしてくれるリバースプロキシを用意しているので、上のキャプチャでも少し見えてしまっていますがOpen WebUIもMCPOサーバもリバースプロキシ経由でhttpsアクセスするようにしております。テスト中はMCPOサーバだけhttpのまま試そうとしてハマりました。
Open WebUIでの、LLM毎のfunction callingの設定
メインチャット画面にはスパナマークに"2"と表示され、クリックするとツール内容が確認できます。
あとはプロンプト次第でLLMはツールを勝手に使ってくれますが、モデル次第では一点設定を変更したほうがよい部分があります。上のリンクの公式ドキュメントより、native function callingを有効にしたほうがツールを期待通り使ってくれるものもあるのでお試しください。設定はチャットセッションごとのメニューでもできますし(次のセクションのスクショ参照)、admin portalのmodelsメニューよりグローバル設定としてモデルごとに設定変更できます。
例えば、gemma3はこちらを有効にしても対応していないため動作しません。以下のOllama公式モデルライブラリより、"tool"でフィルタして出てくるものは対応しているようです。
https://ollama.com/search?c=tools
例えば新しいモデルでollama show
の出力を見てみると、"cogito"や"mistral-small 3.1"はCapabilitiesに"tools"が含まれています。
https://ollama.com/library/cogito
https://ollama.com/library/mistral-small3.1
% ollama show cogito:14b
Model
architecture qwen2
parameters 14.8B
context length 131072
embedding length 5120
quantization Q4_K_M
Capabilities
completion
tools
License
Apache License
Version 2.0, January 2004
% ollama show mistral-small3.1:24b
Model
architecture mistral3
parameters 24.0B
context length 131072
embedding length 5120
quantization Q4_K_M
Capabilities
completion
vision
tools
Parameters
num_ctx 4096
System
You are Mistral Small 3.1, a Large Language Model (LLM) created by Mistral AI, a French startup
headquartered in Paris.
You power an AI assistant called Le Chat.
Open WebUI上のその他の設定
Open WebUIはOllamaへリクエストを投げる際はデフォルトで、context window 2048, 2kでモデルを動かすよう依頼しています。ollama show
出力例で見られる"context length: 128k"という化け物サイズは、ローカルで動かすには無理があります。Open WebUIもその主な用途からデフォルトcontext window 2kとなっているようですが、ツール、ファンクション、あれこれ付加要素を加えていくと、このサイズも4k, 6k, 8kなどGPUで回しきれるサイズを探りつつ大きくしておいたほうが良いです。
Mac mini m4 32GBメモリで、OllamaでLLMを動かしている場合、7bなど小さめのパラメータの量子化モデルならばcontext length 8k, 16kあたりでも問題ありません。"gemma3:27b", "cogito:32b", "cogito:14b", "mistral-small3.1:24b"などは6kから8kあたりのcontext lengthで動かせています。
MCPサーバのfetchツールが動いています
こちらは"fetch"の使用例です。GitHubのユーザREADMEなどがあるURLを渡しているところです。先に触れた"cogito:14b"で、native function calllingをセットしております。セッション内一度のやり取りでは足りなくなることはありませんが、context lengthも6kくらいにセットしていたかと思います。
おわりに
以上です!
We strongly recommend using GPT-4o or another OpenAI model that supports function calling natively for the best experience.
先のOpen WebUI公式ドキュメントのリンク先より、やはりローカルで動かせるレベルのLLMで常に期待通りにツールを使ってもらうのは難しいと感じています。
Native function callingの設定に気づくまではいろいろなモデルで時間を聞き、URL先の情報分析を聞き、大体今の時間は知らない、インターネットへはアクセスできないなどの反応を返され、たまに何故かgemma3:4bとgemma3:12bだけtimeツールを正しく使って現在時刻を返してくれたりするのに更に困惑し。
そしてnative function calling設定を有効にしてからはGemma3がこの方式に対応していないのに悲しみ、ツールに対応した適当なサイズのLLMを探しに行き。
一応、正しくツールを使ってくれているぞという状態にできたので今回記事にもしてみました。
今後は設定やプロンプトを考えつつ、self-hosted GitLabサーバを扱うMCPサーバなどを追加して自分にとって使い勝手の良いものを整備していきたいと思います。
また、Gemma3がこの"native" function callingに対応していない件に関連し、よりトークン・処理数的にも無駄のない別のやり方、「間違えていないやり方」で実装すべきだといった議論をredditあたりで拝見しました。Google公式ドキュメントや、所属エンジニアの記事でも、Gemma3ではどのようにfunction callingを機能させるのかといった内容は紹介されています。
こちらのやり方も今後勉強して使ってみたいと思っています。
https://ai.google.dev/gemma/docs/capabilities/function-calling
https://www.philschmid.de/gemma-function-calling
おまけ - mcpoサーバをsystemd service unitとして載せる
今回構築したMCPOサーバは、他のサーバ同様に常駐させておくのでsystemdで動かしてもらうようにしました。
- mcpo実行スクリプトを用意
- service unitファイルを用意
- systemdに載せる
スクリプトはこのような感じで用意しています。必要であれば環境変数をここでセットし、あとログがファイルに残るようにしています。
#!/bin/bash
# /home/USER/svc/mcpo/mcpo.sh
# Set environment variables
# export FOO=BAR
# Run the Python program
/home/USER/svc/mcpo/.venv/bin/python3 /home/USER/svc/mcpo/.venv/bin/mcpo --port 8000 --host 0.0.0.0 --config ./config.json --api-key "top-secret" >> /home/USER/svc/mcpo/mcpo.log 2>&1
service unitファイルは以下の通りです。
[Unit]
Description=MCPO Server
After=network.target
[Service]
User=USERNAME_HERE
WorkingDirectory=/home/USER/svc/mcpo
ExecStart=/home/USER/svc/mcpo/mcpo.sh
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -SIGINT $MAINPID
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
あとはこれらを有効にします。
sudo systemctl daemon-reload
sudo systemctl enable mcpo.service
sudo systemctl start mcpo.service
MCPサーバ、ツールの入れ替えなどしてconfig.json
を更新した際は、sudo systemctl restart mcpo
で再起動、更新反映できます。
Discussion