OllamaとローカルLLMでMCPサーバーを使うならブリッジが要る
目次
DEV Communityの記事で、OllamaとMCPサーバーをつなぐ話が出ていた。
手元のM1 Max 64GBではOllamaの qwen3.6:35b を普通に使えているが、MCPサーバーを足すと話は変わる。
Ollamaができるのはtool callingであって、MCPクライアントとしてサーバーのライフサイクルや能力交渉まで面倒を見るわけではない。
原典はMCPFind発の記事で、ai-mlカテゴリに832件のMCPサーバーが索引化されている、と書いている。
数だけ見るとローカルLLMでも何でも刺せそうに見えるが、実際の分かれ目は「MCPを誰がしゃべるか」だ。
Ollamaは道具を呼べるがMCPを話さない
Ollama公式ドキュメントでは、/api/chat に tools を渡すtool callingが説明されている。
モデルは関数名と引数を含むtool callを返し、アプリ側がその関数を実行して、結果をtoolロールのメッセージとして戻す。
OpenAI互換APIも http://localhost:11434/v1/ で使える。
既存のOpenAIクライアントをOllamaへ向けるだけなら、ここが境界になる。
MCPはもう一段上の話だ。
仕様上はhost、client、serverに分かれ、hostの中に複数のclientがいて、それぞれがMCP serverとJSON-RPCベースの状態付きセッションを張る。
serverはtoolsだけでなくresourcesやpromptsも持てるし、初期化時にcapability negotiationも走る。
flowchart LR
User[人間] --> Host[MCP host]
Host --> Client[MCP client]
Client --> Server[MCP server]
Host --> Ollama[Ollama<br/>local model]
Ollama --> Host
Server --> Local[filesystem / DB / CLI / API]
なので、Ollamaに直接MCPサーバーを設定する、という発想だとずれる。
Ollamaは推論エンジンで、MCP hostではない。
MCPサーバーの起動、接続、ツール一覧の取得、tool callの実行、結果の返却を担当するブリッジが要る。
MCPHostをそのまま採用する前にリポジトリを見る
原典ではMCPHostを使う例が出ている。
go install github.com/mark3labs/mcphost@latest
設定ファイルにMCPサーバーを書き、Ollamaモデルを指定して起動する。
mcphost --config mcp-config.json \
--model ollama:qwen2.5:14b \
--ollama-url http://localhost:11434
仕組みとしては分かりやすい。
MCPHostがMCP hostになり、filesystemなどのMCPサーバーと接続し、Ollamaにはtool calling相当の形でツールを見せる。
ただし、MCPHostのGitHub READMEには「もうアクティブにメンテされておらず、後継はKit」と明記されている。
原典の導入例を写経するだけだと、ここを見落とす。
ローカル実験ならMCPHostで感触を見るのはありだが、長く使う設定にするなら、現在メンテされているhostを選ぶほうがいい。
Continue.devや公式SDKで自前hostを書く選択肢もあるが、自前実装ではtool callのループ、ストリーミング、エラー時の再試行、ツール結果の切り詰めまで自分で持つことになる。
ローカルで相性がいいのはファイルとDB
MCPサーバーをローカルLLMに刺すなら、まずfilesystem、Git、SQLiteのようなローカル完結のサーバーから試すのがいい。
モデルもツールも同じマシン上で動き、外部APIキーを要求しないので、ローカルLLMを使う理由と噛み合う。
open-notebookをDockerもクラウドAPIも使わずM1 Maxで動かしたときも、気持ちよかったのはOllama、SurrealDB、Worker、フロントが全部手元で閉じていた点だった。
MCPでも同じで、filesystemやDBのようにデータが手元にある対象ほど、ローカルLLMの利点が残る。
逆に、GitHub、AWS、SaaS系のMCPサーバーは、LLMだけローカルにしてもAPIトークンは外部サービス用に必要になる。
検索やブラウザ系も外部HTTPリクエストは発生する。
「クラウドLLMに文書を送らない」と「ネットワーク通信が一切ない」は別物だ。
このへんはCLIからAIへ、人間がソフトウェアと話す入口が変わるで書いたMCPとCLIの使い分けにもつながる。
git status や docker ps はCLIで十分だが、社内APIやデバイス操作のようにパラメータの形をモデルに推測させたくないものはMCPのスキーマが効く。
tool callingの精度はモデルと量子化で崩れる
原典は、Qwen2.5、Llama 3.3、Mistral Nemo、Gemma 3をtool calling向きの候補として挙げている。
Ollama公式のtool calling例も qwen3 を使っているので、Qwen系は入り口として選びやすい。
ただ、tool callingは「モデルがJSONをそれっぽく返す」だけでは足りない。
引数の型、必須項目、ネストしたオブジェクト、複数ツール呼び出し、前回のtool結果の反映まで安定して初めて使える。
手元でQwen3.6をOllamaに載せたときも、生成速度そのものはQwen3.6-35B-A3BをM1 Max 64GBで動かした記事の通り十分だった。
でも思考トークンが膨らむモデルでは、MCPのツール定義とtool結果を足したときに、会話全体の待ち時間が一気に伸びる。
小さい量子化モデルでは、JSONの閉じ括弧が壊れたり、必須引数を落としたりする。
これはOllama固有というより、ローカルLLMでfunction callingを使うときの古典的な弱点だ。
本番寄りに使うなら、実際に使うモデルタグ、量子化、コンテキスト長、MCPサーバーのツール数を固定してテストする。
サーバーを増やすほどコンテキストを食う
MCPをローカルLLMで使うと、クラウドモデルより先にコンテキストの重さが目立つ。
MCPサーバーを追加すると、ツール名、説明、JSON Schemaがモデルに渡る。
さらにtool結果も会話履歴に戻る。
ClaudeやGPTの大きいコンテキストなら雑に持てる量でも、ローカルの7Bから14Bでは効き方が変わる。
filesystemサーバーが巨大なディレクトリ一覧を返すと、それだけで後続の会話が鈍る。
検索系サーバーが長い本文を返す場合も同じだ。
オープンソースMCPサーバー50件をスキャンして見つかった脆弱性では、MCPサーバーを増やすこと自体が攻撃面を増やす話を書いた。
ローカルLLMではセキュリティに加えて、コンテキストとレイテンシも増える。
便利そうなサーバーをまとめて有効化するより、作業単位で使うサーバーを絞るほうが動きは読みやすい。
「ローカルだから安全」という感覚もずれる。
MCPサーバーはNode.jsやPythonのプロセスとしてローカルで走るが、そのコード自体に脆弱性があれば、モデルからの入力がそのまま攻撃ベクタになる。
FlowiseのCustomMCPノードにCVSS 10.0のRCE脆弱性が見つかった件が典型で、MCPサーバーへの入力値がバリデーションなしでコード実行に到達した。
ローカルLLMがtool callで渡す引数も同じパスを通るので、モデルのハルシネーションや悪意あるプロンプトが刺さる余地は変わらない。
サーバーを足すなら、ツール定義のJSON Schemaを見て入力バリデーションが入っているか、stdioのサブプロセスとして隔離されているか、くらいは先に確認しておいたほうがいい。
MCPサーバーのセキュリティについては50件スキャンの記事でもう少し踏み込んで書いている。
MCPサーバーを自分で書く
このブログでMCPサーバーの使い方は何度か扱ったが、そもそもどう作るかは一度も書いていなかった。
外部に公開するつもりのない自分用サーバーなら、公式SDKで数十行から動く。
TypeScript SDKの高レベルAPI McpServer を使うと、ツール定義とハンドラを書くだけでサーバーになる。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "local-note-search",
version: "0.1.0",
});
server.tool(
"search_notes",
{ query: z.string() },
async ({ query }) => {
const { execFileSync } = await import("child_process");
const out = execFileSync("grep", ["-rl", query, `${process.env.HOME}/notes`], {
encoding: "utf-8",
timeout: 5000,
});
return { content: [{ type: "text", text: out.trim() }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
server.tool() にツール名、Zodスキーマ、ハンドラを渡す。
StdioServerTransport はstdin/stdoutでJSON-RPCをやりとりするので、MCPホストからサブプロセスとして起動される前提だ。
自分でHTTPサーバーを立てる必要はない。
Python SDKのFastMCPならさらに短い。
from mcp.server.fastmcp import FastMCP
import subprocess
mcp = FastMCP("local-note-search")
@mcp.tool()
def search_notes(query: str) -> str:
"""ローカルのノートをgrepで検索"""
r = subprocess.run(["grep", "-rl", query, "/Users/me/notes"],
capture_output=True, text=True, timeout=5)
return r.stdout.strip() or "該当なし"
mcp.run()
デコレータでツールを登録し、型ヒントからJSON Schemaが自動生成される。
どちらのSDKも npm init や pip install のあと、MCPホストの設定に command と args を書けばそのまま繋がる。
ここまでは拍子抜けするほど簡単だが、使い続けると別の話になる。
入力バリデーション、タイムアウト、ツール結果のサイズ制限あたりは自分で入れないといけない。
Ollamaのモデル経由だとハルシネーションで想定外の引数が来ることもあり、上の grep の例でもパスの制限くらいは足したほうがいい。
前のセクションで触れた脆弱性パターンは、自作サーバーにもそのまま当てはまる。
外部公開しないならテストも雑でいいし、動かしながら直していける。
ローカルLLMと自作サーバーの組み合わせならクラウドAPIを一切通さずにツール連携が完結するので、オフラインやプライバシーを気にする構成には向いている。
自分で書くと多機能サーバーの重さが見える
自作サーバーの server.tool() を1個書いてみると、ツール名、引数のJSON Schema、descriptionがセットでモデルに渡ることが体感で分かる。
1ツールならスキーマも小さいが、公開サーバーによくある「filesystem + git + shell + search + browser」みたいな多機能構成を想像すると、ツール定義だけで数百トークンになる。
TypeScript SDKの server.tool() に渡すZodスキーマは、MCPホストがJSON Schema形式に変換してモデルのsystemメッセージかtool定義に詰める。
ツールが10個あれば10個分のスキーマと説明文が毎ターン送られる。
クラウドの128kや200kコンテキストなら誤差の範囲でも、Ollamaで8kや32kのモデルを使っていると、ツール定義が会話本体を圧迫し始める。
多機能サーバーを1個つなぐか、単機能サーバーを必要な時だけつなぐか。
作り方を知ると、後者の軽さが具体的に見えてくる。
自分用に1ツール1サーバーで切り出しておけば、使わないツールのスキーマがモデルに渡らない。
ローカルLLMのコンテキスト制約では、この差が応答品質に直結する。