👻

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上での設定へと進みます。

OWUI Adding Tools Servers

  • (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メニューより追加した方が、ツールのオンオフ切り替えが簡単にできます。こちらの方がおすすめです。

OWUI Adding Tools Servers

なおここで今更の、構築上の注意点です。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の設定

OWUI tools count shown

OWUI tools description

メインチャット画面にはスパナマークに"2"と表示され、クリックするとツール内容が確認できます。

https://docs.openwebui.com/openapi-servers/open-webui/#optional-step-4-use-native-function-calling-react-style-tool-use-

あとはプロンプト次第で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ツールが動いています

OWUI fetch and analyze pkkudo

こちらは"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