kaigo-gap
kaigo_mcp
日本の介護の需給データを、AIエージェントが使える道具として公開するMCPサーバー。
Claude Code や Claude Desktop から接続すると、「この市は特養が足りているのか」を 公開データに基づいて答えられるようになる。
ステータス: サーバー・エージェント・eval まで動作。 列A(ローカル7B)・列B(ローカル9B)・列D(Claude Haiku 4.5)を計測済み。
何をするものか
MCP(Model Context Protocol)は、LLMに道具を持たせるための規格。このリポジトリが作るのは 道具の側で、LLMそのものは含まない。サーバー単体ではAPIを一切呼ばず、課金も発生しない。
[考える側] [このリポジトリ]
Claude Code / 自作エージェント ←stdio→ kaigo-gap サーバー
「どの道具を使うか」を判断 呼ばれたらデータを返すRelated MCP server: Japan Data MCP
道具
道具 | 用途 |
| 全国の基準値。個別の数字を評価する前提になる |
| 市区町村名・保険者名で需給を引く |
| 特養の不足順(充足順)に並べる |
| 介護保険法と特養の運営基準の条文を、出典つきで検索する |
主指標は 要介護3以上の認定者100人あたりの特養定員数(全国24.6人・中央値26.7人)。 定義と出典は kaigo_gap_analysis 側にある。
制度文書の検索(search_regulations)
対象は介護保険法と「指定介護老人福祉施設の人員、設備及び運営に関する基準」の本則(e-Gov法令API、 465条を項単位の1,508塊に分割)。通知・Q&A・他サービス(ショート・老健・小多機)の基準は含まない。
python scripts/fetch_laws.py # 法令の取得(data/laws.json。git管理外)
ollama pull bge-m3 # 埋め込みモデル(約1.2GB)
python scripts/eval_retrieval.py # 初回のみ埋め込みを作る(CPUで約45分)→ 検索のeval埋め込みが無い、または Ollama に繋がらないときは BM25 に退避し、応答の 検索方式 に出す。
検索の評価(retrieval-cases.json 56問、LLMを呼ばず0円。単位は条):
方式 | r@1 | r@3 | r@5 | MRR |
BM25(文字2-gram) | 0.38 | 0.46 | 0.57 | 0.438 |
埋め込み(bge-m3) | 0.75 | 0.86 | 0.89 | 0.804 |
ハイブリッド(RRF) | 0.55 | 0.73 | 0.82 | 0.643 |
質問を「条文の語そのまま/言い換え/現場の言い方」の3段階に分けると、差は現場の言い方で最も開く (r@1: BM25 0.17 → 埋め込み 0.66)。質問は自作で56問のため、1問で約1.8ポイント動く。 ハイブリッドが単体に負けたのは、弱いBM25が同じ重みで混ざるため。重みの調整は、調整用と本番用に 質問を分けるまでやらない。
集計をこちらで書いていない
データは案4(kaigo_gap_analysis)が export_web.py で書き出し、
公開ダッシュボード が配信しているのと
同じ insurers.json をコピーして使う。ここで割り算を書き直さない。
指標の定義を2か所に持つと、エージェントの答えとダッシュボードの数字が食い違ったとき、
どちらが正しいのか判定できなくなる。案4の dataset.py が「定義は1か所」という方針で
書かれているので、それを跨いで守っている。更新は python scripts/sync_data.py。
道具の設計で気をつけたこと
数字だけ返しても判断できない。 「12.4」を渡されてもLLMは高いか低いか分からないので、 全国順位と、道具の説明文に全国値を含めている。
同名自治体を1件に決め打ちしない。 「府中市」は東京都と広島県にある。 候補を両方返し、絞り込みは呼び出し側に委ねる。
特養定員0を「全国最下位」と読ませない。 該当が90保険者あり、全部が同率1位になる。
同率件数を必ず添え、さらに「小規模自治体では珍しくなく、住民は近隣自治体の施設を
利用していることが多い」という注記を付ける。これが無いと、
rank_insurers の結果から「新郷村は全国最悪の地域」という誤った結論が出る。
ドメイン知識を道具の応答に埋めておかないと、呼ぶ側のモデルを変えるたびに 同じ誤読が再発する。プロンプトではなく道具側に置くのはそのため。
使い方
pip install -r requirements.txt
python scripts/sync_data.py # 案4からデータを取り込む
python scripts/smoke_test.py # 通信の疎通確認Claude Code から使うには、このディレクトリで起動すれば .mcp.json が読まれる。
他のクライアントに登録する場合の設定:
{
"mcpServers": {
"kaigo-gap": {
"command": "python",
"args": ["-m", "kaigo_mcp"],
"cwd": "/path/to/kaigo_mcp"
}
}
}Windows のパスは、区切りをバックスラッシュ2つで書くこと。
"cwd": "C:\\projects\\kaigo_mcp"1つだと JSON の不正なエスケープ(\p)になり、設定ファイルごと読めなくなる。
このリポジトリの .mcp.json は実際にそれで壊れていて、
Claude Code から一度も読み込めていなかった。
エージェント
python -m kaigo_mcp.agent --list
python -m kaigo_mcp.agent "尼崎市は特養が足りてる?" --verbose道具はMCPサーバー越しに呼ぶ(関数を直接importしない)。importで済ませると MCPを通していないことになり、「MCPサーバーを作った」という主張が検証されないため。
比較する列
列 | モデル | 場所 | 設定 | 1問コスト | 状態 |
A | qwen2.5:7b | メイン機(CPU) | — | 0円 | 計測済み |
B | qwen3.5:9b | RTX 5050 8GB |
| 0円 | 計測済み |
B' | qwen3.5:9b | RTX 5050 8GB |
| 0円 | 計測済み |
C | Qwen3.5-397B-A17B | DeepInfra | — | 約0.8円 | 保留(登録にカードが要る可能性) |
D | Claude Haiku 4.5 | Anthropic | — | 約1.4円(実測) | 計測済み |
各列の差が1つの要因だけになるように組んでいる。
比べる | 分かること |
A → B | ハードとモデル世代の効果( |
B → B' | 設定だけの効果。モデルも重みも同じ |
B' → C | モデルサイズだけの効果(同じ Qwen3.5 系で揃えてある) |
C → D | モデル系統の差 |
B と B' を両方残しているのは、片方だけだと消える発見があるため。
同じモデル・同じ重みで、num_ctx を変えただけで正答率が 83% と 100% に割れる。
8k だけを載せれば「ローカル9Bは優秀」に見え、4k だけを載せれば
「ローカルは実用にならない」に見える。どちらも本当ではない。
列Cは保留中。当初 Qwen3-235B-A22B を指していたが、世代が3.0で列Bと揃わないうえ、
提供も終了していた。無料枠のカタログは入れ替わるので、使う前に
/models で実在を確かめること。NVIDIA無料枠(C-alt-nvidia)も試したが、
1往復に168秒かかる回があり eval には使えなかった。
ループは1本しか書かない。 片方だけSDKのツールランナー、片方だけ手書きにすると、 列間の差がモデルの差なのかループ実装の差なのか分離できなくなる。 バックエンドは履歴の変換だけを担当する。
実測(列A・列B)
各6回・python scripts/probe_tool_calling.py で計測。
列A→列B ではハードとモデル世代が同時に変わるので、 切り分け用に 7B を GPU 機でも走らせた(真ん中の列)。
列A qwen2.5:7b (CPU) | qwen2.5:7b (GPU) | 列B qwen3.5:9b (GPU) | |
道具呼び出し成功率(既定温度) | 4/6 = 67% | 5/6 = 83% | 6/6 = 100% |
道具呼び出し成功率(温度0) | 6/6 = 100% | 6/6 = 100% | 6/6 = 100% |
指示追従(温度0) | 0/6 = 0% | 0/6 = 0% | 6/6 = 100% |
1回あたり(温まった後) | 7.6秒 | 0.4秒 | 3.2秒 |
初回(モデルロード込み) | 102.6秒 | 36.1秒 | 19.2秒 |
温度0が要るのは 7B 固有だった
qwen2.5:7b は既定温度だと <tool_call> の開始タグが壊れ
(olith pering のような数文字が先頭に付く)、ollama のパーサが認識できず
呼び出しが本文テキストに漏れる。閉じタグ </tool_call> だけが残るのが目印。温度0で解消。
qwen3.5:9b では既定温度でも 6/6。 この壊れ方はローカル実行一般の問題ではなく、
このモデルの世代・サイズに固有のものだった。
同じ 7B を GPU 機で走らせても既定温度で漏れ(5/6)、漏れ方の形まで同じだった
(brtc {"name": "lookup_insurer", ...} </tool_call> — 開始タグの位置に数文字が居座り、
閉じタグだけが残る)。ハードを替えても直らない。 モデル側の癖である。
もっと大きい差は「指示に従うか」のほう
システムプロンプトは「答える前に get_national_baseline で全国の基準値を確認すること」
と指示している。9B は 6/6 で従い、7B は 0/6 で一度も従わなかった。
しかも 9B は2つの道具を1ステップで並列に呼ぶ。
7B が正しい答えを出せたのは、道具の説明文に埋めておいた全国値(24.6)を 拾ったからで、指示された手順は踏んでいない。安定性ではなく指示追従の差であり、 道具が増えるほど効いてくる。
効いたのはモデル世代で、GPUは速度だけだった
7B を GPU に載せても、品質の指標はどちらも動かなかった。 既定温度でのタグ崩れは残り(4/6 → 5/6、n=6 なのでゆらぎの範囲)、 温度0での指示追従は 0/6 のまま。GPU 機の 7B は既定温度のとき6回中2回だけ 基準値を引いたが、温度0では一度も引かない。
動いたのは秒数だけで、7.6秒 → 0.4秒(約19倍)。 つまり A→B の差のうち、成功率と指示追従はモデル世代の効果、 速さはハードの効果と読める。同じ表の中で分離できた。
ついでに、9B は 7B より8倍遅い(3.2秒 vs 0.4秒)。 同じ GPU でも、道具を2つ並列に呼び、指示に従うぶんだけ出力が長い。 速さと指示追従はここで真正面からトレードオフになっている。
秒数についての前回の訂正の、さらに裏取り
列Bの「既定 18.9秒 → 温度0 4.4秒」は温度の効果ではなく初回ロードだった。 改良版スクリプトで採り直すと、初回 19.2秒・以降 3.2秒(温度0では以降 3.9秒)で、 温まった後は温度で差がない。列Aの初回 102.6秒・以降 7.6秒と同じ構図で、 初回を混ぜた平均は温度差にもハード差にも見えてしまう。
通しで走らせると、賢さより先にコンテキストが落ちた
道具呼び出し単体ではなく、エージェントを1問最後まで回した結果。
「尼崎市は特養が足りてる?」 | 列A 7B (CPU) | 列B 9B (4k) | 列B' 9B (8k) |
ステップ | 2 | 2 | 3 |
秒 | 72.31 | 75〜89 | 21.6〜37.2 |
入力 / 出力トークン | 1,648 / 171 | 1,985 / 3,121 | 3,176 / 601 |
答え | 正しい | 空(0/4回) | 正しい(3/3回) |
先に頭打ちになったのは VRAM ではなく、ollama の既定 num_ctx = 4096 だった。
9B が 6.6GB を占めるので KVキャッシュが苦しいと踏んでいたが、
そこへ届く前に既定のコンテキスト長で止まる。
止まり方が分かりにくい。2ステップ目が stop_reason='length' で、
プロンプト約1,150 + 出力2,946 = ちょうど 4,096。出力は3,121トークンあるのに本文は空。
引き金はモデル自身の誤った引数だった。尼崎市は兵庫県なのに
lookup_insurer({'name': '尼崎市', 'pref': '大阪'}) を投げて該当0が返り、
そこから2,946トークン考え込んで、答えを書き始める前に使い切る。
自己回復に要るトークンがコンテキストを食い潰す。
num_ctx を 8192 にすると解消するが、今度は ollama ps が 12%/88% CPU/GPU を出す。
8GB には載りきらない。4k では答えが出ず、8k では GPU に載らない、
というのが RTX 5050 8GB で 9B を回すということだった。
難しい質問(青森県で3市町村を挙げて全国と比較・--max-steps 8)は
4k でも 3ステップ・41.06秒で完走した。
コンテキストを決めるのは質問の難しさではなく、回復の要否。
計測そのものの欠陥だった(修正済み)
直す前の loop.py は、道具呼び出しが無ければ stopped_by = "end" にしていた。
length で切られて本文が空でも "end" なので、
上の4回は記録上「2ステップで完走」と、列Aの成功と同じ形で残っていた。
空答えが完走と同じ顔をしていたら、列を並べても意味がない。
length または本文が空なら truncated を立てるようにした。
集計は RunRecord.answered を見る。CLI は理由を出して終了コード1を返す。
この手の欠陥は、失敗する列を実際に踏むまで見つからない。 列Aだけで回していたら気づけなかった。
そしてこの修正自体に穴が2つ残っていた。どちらも列C・Dを回した瞬間に効く。
切れたことを表す値が各社で違う。 OpenAI互換は
length、Anthropic はmax_tokens。lengthしか見ていなかったので、列D(Claude)では切れても検出できない。 列Bで空答えを完走と数えていたのと同じ壊れ方が、そのまま列Dで再発する。道具呼び出しの途中で切れる場合を見ていなかった。 判定が 「道具呼び出しが無いとき」の中にあったため。引数のJSONが欠けたまま次へ進むと、 道具が失敗した記録だけが残り、原因が「モデルが下手」に見えてしまう。
TRUNCATED_REASONS = {"length", "max_tokens"} を道具呼び出しの有無より先に判定する。
max_tokens を 8(道具の手前で切れる)と 40(道具呼び出しの途中で切れる)に絞って
実際に踏み、正常系が truncated に化けないことも併せて確認した。
eval の結果(列A・列B・列D)
6ケース・python scripts/eval_agent.py で計測。判定はすべてプログラムで書ける
条件のみ(LLM-as-judge は使わない)。列Bは3周(18件)。
列A 7B (CPU) | 列B 9B (4k) | 列B' 9B (8k) | 列D Haiku 4.5 | |
正答 | 5/6 = 83% | 15/18 = 83% | 18/18 = 100% | 15/18 = 83% |
完走 | 6/6 = 100% | 15/18 = 83% | 18/18 = 100% | 18/18 = 100% |
指示追従 | 2/6 = 33% | 18/18 = 100% | 18/18 = 100% | 17/18 = 94% |
1問の秒数(中央値) | 72〜85秒 | 16.9秒 | 24.0秒 | 6.5〜8.5秒 |
コスト | 0円 | 0円 | 0円 | 25円 |
num_ctx は速度の設定ではなく、正答率の設定だった
4k の失点3件はすべて amagasaki-basic の truncated/length。
仕組みは前節(通しで走らせると、賢さより先にコンテキストが落ちた)
で見たものがそのまま eval にも出ただけで、新しい壊れ方ではない。
evalまで通して分かったのは、同じモデル・同じ重みで、 設定だけで正答率が 83% と 100% に割れること。 8k は回復ぶんの余地を与えているだけで、モデルは何も変わっていない。
「ローカルが Haiku を超えた」とは書けない
この6ケースでは列B'(8k)が上回っている(正答 18/18 対 15/18、 指示追従 18/18 対 17/18)。ただしそう読むには弱すぎる。
6ケース×3周しかない。 母数が小さい
Haiku の失点は性能由来ではない。 下の「効きすぎた注記」の通り、 道具側に埋めた注記を読んで定員0の4件を除外した回が含まれる。 こちらの設計に起因する失点を、モデルの弱さとして数えるのは誤り
秒数が3倍(24.0秒 対 6.5〜8.5秒)。同じ土俵ではない
言えるのは「この6問では、8kのローカル9Bで足りた」まで。 0円で手元から出ないという条件を考えれば、それ自体が十分な結果ではある。
正答率が同じでも中身は違う(列A)
列A・列B・列D はどれも正答 83% だが、内訳が違う。 列Aは道具の説明文に埋めた全国値を拾って偶然当てていて、 指示された基準値の確認をしていない(指示追従 2/6)。 列B・列Dは毎回引く。道具が増えるほどこの差が効く。
列B の失点は上記の truncated で、当てられなかったのではなく
答えを書き終えられなかった。同じ83%でも、届いていない理由が三者三様。
正答率という1つの数字だけを並べると、この違いが消える。
道具側の注記は効いたが、効きすぎた
rank_insurers の応答に「定員0は小規模自治体では珍しくない、単独で断じるな」と
埋めてある。列Dはこれを読んで、定員0の4件を答えから除外した(3周中1回)。
数字は実データどおりででっち上げではない。
誤読は防げたが、代わりに情報が隠れた。「断じるな」が「触れるな」と解釈された。 ドメイン知識を道具の応答に埋める判断は効くが、効きすぎることがある。
これから
列Bは 4k / 8k の両方を載せると決めた(B と B')。設定だけで割れることが本題なので
列C は保留。DeepInfra の登録にカードが要る可能性があるため
NVIDIA無料枠は eval には使えない(速度が保証されない。1往復168秒の回があった)
関連
この計測を書いた記事
num_ctx が正答率の設定でもあったこと、空答えを「完走」と数えていた計測の欠陥を、
このリポジトリの実測をもとに書いている。
同じ作者の関連プロジェクト
kaigo_gap_analysis — 保険者別の需給ギャップ分析(このデータの出所)
kaigo_matching — 介護記録から8項目を構造化して取り出すパイプライン
This server cannot be deployed
Maintenance
Related MCP Connectors
Search Japanese company credit, financials, subsidies, and procurement via AI agents.
Local government intelligence for AI agents.
Japan data tools for AI agents: calendar (rokuyo), address, name splitting, corporate number lookup
Raw Japanese regulatory data for AI agents: pension, gazette, gBizINFO. x402-metered (USDC).
Related MCP Servers
- AlicenseNot gradedqualityFmaintenanceEnables LLMs to search and retrieve Japanese real estate data through unified access to the MLIT Real Estate Information Library API, supporting over 25 geospatial datasets for interactive queries.189MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to query Japanese public data (laws, corporations, statistics) from official government APIs, returning normalized English metadata with source attribution.1MIT
- FlicenseNot gradedqualityDmaintenanceProvides AI agents with comprehensive Japanese market intelligence through 27 MCP tools, covering corporate data, macroeconomics, financials, and environmental data from 14 integrated sources.-
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to search Japanese government subsidies and grants, including archived closed calls and recurring application periods based on historical data.10 npm1MIT