作業者: hideyukiMORI バージョン: v1.8.170(セッション終了時点)
2026-05-30 を最後に約 2 ヶ月半コミットが無く、その間 CI も一度も走っていなかった。 コードは 1 行も変わっていないのに、上流の advisory データのほうが動いて CI が赤になっていた。
本日はその赤を閉じ、待たされていた PR をマージし、次に来る mcp 2.0 移行の実測調査を行った。
| PR | 内容 | 種別 |
|---|---|---|
| #769 | chore(deps): 依存の既知脆弱性 22 件を解消し CI を緑に戻す(v1.8.167) | security |
| #767 | chore(deps): bump nanoid to 3.3.18 in lockfile(GHSA-2v37-7h3g-55p8) | security |
#767 は 2ヶ月半前から存在した pip-audit の既存赤に阻まれて open のままだった。
#769 で赤を閉じてから rebase して通した。
pip-audit の出力は 27 行だが、pip / pyjwt / starlette に重複行があり
ユニークでは 22 件 / 8 パッケージ。
| パッケージ | 変更 | 解消した ID | 件数 |
|---|---|---|---|
| pyjwt | 2.12.1 → 2.13.0 | PYSEC-2026-175 / 176 / 177 / 178 / 179 | 5 |
| cryptography | 48.0.0 → 50.0.0 | PYSEC-2026-3552 / 3553 / 3554・GHSA-537c-gmf6-5ccf | 4 |
| starlette | 1.0.1 → 1.6.0 | PYSEC-2026-248 / 249 / 2280 / 2281 | 4 |
| mcp | 1.27.1 → 1.29.0 | PYSEC-2026-3481 / 3482 / 3483 | 3 |
| python-multipart | 0.0.29 → 0.0.32 | PYSEC-2026-3036 / 3037 / 3040 | 3 |
| msgpack | 1.1.2 → 1.2.1 | PYSEC-2026-3625 | 1 |
| pydantic-settings | 2.14.1 → 2.15.0 | GHSA-4xgf-cpjx-pc3j | 1 |
| pip | 26.1.1 → 26.2.1 | PYSEC-2026-196 | 1 |
uv.lockの更新のみで全件解消。依存宣言はすべて下限のみだったためpyproject.tomlの 依存宣言は無変更(変更はversionのみ)= PyPI 配布物の依存要件は変わっていない。- 新規の
--ignore-vulnはゼロ。 除外は既存のPYSEC-2025-1831 件のみ据え置き。 - 段階更新(①低リスク 5 点 → ②cryptography → ③starlette → ④mcp)で進め、
各段で
pytestを実行。④で実際に落ちたので、そこで止めて原因を切り分けた(§2)。
コードは 2026-05-30 以降変更されていない。変わったのは上流の advisory データのほう。
uv.lock を据え置いたまま CI が一度も走らなかったので、脆弱性だけが静かに積み上がった。
凍結中のプロジェクトほど静かに腐る。 これは「コードを変えない限り安全」という
直感が成り立たない領域で、定期再検査の装置が無いことの帰結である。
再発防止は docs/todo/current.md の優先タスクに起票した。
uv lock --upgrade-package mcp は 2.0.0(メジャー跨ぎ) まで上げるが、テストが落ちる。
advisory の要求は 1.28.1 以上なので 1.29.0 で停止し、移行は別タスクへ分離した。
🔴 見積もりの訂正 — 測らずに出した数字を記録として残す
当初この移行を 「
src/nene2/mcp/とsrc/example/mcp.pyの全面書き換えが必要」 と 見積もった。これは過大評価であり、誤りだった。そう書いた根拠は「
uv sync後に 4 本のテストが収集エラーになった」ことだけで、 依存の実体を測っていなかった。grep2 発で確かめられることを確かめずに重い見積もりを 出すと、その見積もり自体が着手を止める装置になる。実測すると
src/側の fastmcp 依存はserver.pyの 1 行のみ。 4 本が落ちたのはnene2/mcp/__init__.pyがserver.pyを import するカスケードであって、 4 本それぞれが fastmcp を使っているからではない。 実際の移行規模は概算 11 行。訂正後の数字(11 行)は着手判断に使う。訂正前の失敗(測らずに見積もった)は 次に見積もりを立てるときに使う。両方を残す。
$ grep -rn 'fastmcp' src/ tests/
src/nene2/mcp/server.py:10: from mcp.server.fastmcp import FastMCP
tests/example/test_http_mcp_parity.py:23: from mcp.server.fastmcp.exceptions import ToolError
| ファイル | 行数 | fastmcp 依存 |
|---|---|---|
src/nene2/mcp/__init__.py |
12 | なし(re-export のみ) |
src/nene2/mcp/server.py |
48 | 🔴 唯一の依存点 |
src/nene2/mcp/http_client.py |
119 | なし(httpx のみ) |
src/example/mcp.py |
147 | なし(LocalMcpServer 越し・15 ツール登録) |
LocalMcpServer(48 行)がアンチコラプションレイヤーとして機能している。
example/mcp.py の 15 ツールも、MCP 系テスト 22 本も、すべてこの 48 行の裏側に守られており、
メジャー更新の衝撃が 1 ファイルに閉じている。 元の設計が正しかったことの実証データである。
| 1.x | 2.0.0 |
|---|---|
mcp.server.fastmcp |
削除 → mcp.server.mcpserver(パッケージごと改名) |
FastMCP |
MCPServer |
mcp.server.fastmcp.exceptions.ToolError |
mcp.server.mcpserver.exceptions.ToolError(クラス名は同一) |
LocalMcpServer が使っている 4 機能の移行可否:
| 使用箇所 | 2.0.0 での状況 | 判定 |
|---|---|---|
FastMCP(name, instructions=, host=, port=) |
MCPServer(...)。host / port はコンストラクタから run() へ移動(消滅ではない) |
🟡 要追随 |
.tool(description=...) |
キーワード互換 | 🟢 |
._tool_manager.list_tools() |
同名・同挙動で残存 | 🟢(ただし私有 API) |
.run(transport=...) |
transport ごとの overload。sse / streamable-http が host= port= を受ける |
🟡 |
| 1.x | 2.0.0 | |
|---|---|---|
| 戻り値 | content block の列(tuple のことがある) | CallToolResult |
| フィールド | — | meta / content / structured_content / is_error / result_type |
| 本文の取り出し | raw[0] if isinstance(raw, tuple) else raw |
result.content |
- 「1 要素 1 content block」という性質は 2.0 でも成立(実測)。
- ツール内例外は 2.0 でも
ToolErrorで送出される(is_errorフラグに落ちるのではない)。 実測:ToolError: Error executing tool boom: bad input - = パリティテストの異常系アサーションは import パスの変更だけで生き残る。
| テスト | 件数 | 守っている保証 | 移行後 |
|---|---|---|---|
tests/nene2/mcp/test_server.py |
5 | LocalMcpServer の公開契約(登録したツールが list_tools() に名前で現れる・デコレータ付き関数が素の関数としても呼べる) |
変更不要。移行の合否判定器になる |
tests/example/test_mcp.py |
3 | Note の作成→取得→更新→削除ライフサイクル | 変更不要 |
tests/nene2/mcp/test_http_client.py |
14 | HttpxMcpClient の HTTP 挙動 |
変更不要(fastmcp と無関係・巻き添えで落ちただけ) |
tests/example/test_http_mcp_parity.py |
5 | 🔴 差別化そのもの(同一ストア上での両サーフェス等価性・巨大 title / 空 body が両方で拒否される) | 要修正 3 行 |
パリティテストが守る不変条件は UseCase の Input DTO 側にあって MCP ライブラリ側には無い (v1.8.166)。だから mcp のメジャー更新で保証が揺らがない。移行が小さく済む理由もここにある。
2.0.0 は httpx2 へ乗り換える。1.29.0 → 2.0.0 で:
- 追加:
httpx2/httpx2-jsfetch/mcp-types==2.0.0(完全一致ピン)/opentelemetry-api/truststore - 削除:
httpx-sse requires-pythonは>=3.10のまま = 3.12 / 3.14 サポートに影響なし
自艦は httpx>=0.27 を直接依存に持つため、2.0 を入れると httpx と httpx2 が同居する
(競合はしない)。「httpx2 へ寄せるか」は本移行とは別軸の判断として記録しておく。
| 案 | 内容 | 代償 |
|---|---|---|
| A | import パスと host/port だけ直す(約 11 行) | _tool_manager 私有 API 依存が残る |
| B-2(推し) | A + ツール名を自前で保持し私有 API 依存を外す | デコレータのラップ数行 |
| B-1 | list_tools() を 2.0 の async 公開 API に寄せる |
公開 API の破壊的変更。得られるのは体裁だけ |
| C | lowlevel に降りて fastmcp 相当を自前実装 |
🔴 スキーマ生成・引数検証・content 変換を全部抱える。「薄く包む」方針の変更 |
| D | 移行せず 1.x に留まる | 上流の 1.x サポート終了まで先送り |
推しは B-2。 _tool_manager は 2.0 でたまたま同名で生き残っただけで契約されていない。
パッケージ名ごと作り替える上流である以上、次も残る保証がない。
同じ移行を 2 回やらないために、今回のついでに外す。
見積もり: server.py 約 15 行 + パリティテスト 3 行 = 半日以内。
既存 27 本のテストがそのまま合否判定器になる。
🔴 pyproject.toml の mcp バージョン境界をどうするかの決定が先。
mcp>=2 に上げるのか、[tool.uv] constraint-dependencies で 1.x に留めるのかで、
この移行の着手是非そのものが決まる。
1.x に固定するなら、本移行は上流の 1.x サポート終了まで塩漬けが正しい (advisory は 1.29.0 で全て閉じているので急ぐ理由がない)。
なお本移行も mcp 下限の引き上げも、ADR-0011
(MCP をコア依存として含める)の決定とは矛盾しない — 動くのはバージョン境界であって、
コア依存かどうかではない。着手時の ADR は ADR-0011 を上書きするのではなく、その上に積む形になる。
mkdir /tmp/mcp20 && cd /tmp/mcp20
uv venv -p 3.14 && uv pip install "mcp==2.0.0"
ls .venv/lib/python3.14/site-packages/mcp/server/ # fastmcp が無いことを確認mcp.server.mcpserver から MCPServer を import し、.tool() で 2 本登録して
_tool_manager.list_tools() が名前を返すこと、call_tool() が CallToolResult を返すこと、
ツール内例外が ToolError になることを確認する。
mcp==2.0.0 の実測。 着手時には版が進んでいる
可能性があるので、mcp.server.mcpserver の存在と call_tool() の戻り値型だけは
測り直してから書き始めること。
- マージ済みリモートブランチ 4 本(
docs/translate-{de,fr,pt-br,zh})を削除。 - locked worktree 4 本(
.claude/worktrees/agent-*)を削除。未コミット成果物が無いことをgit status --porcelain --ignoredで確認してから外した。 - ローカルブランチ 20 本を削除。リモート・ローカルとも
mainのみになった。
測り方の教訓: 当初
git diff origin/main...<branch>でマージ済み判定を行い、 「10 本は差分が残っていて消せない」と誤判定した。三点 diff はマージベース以降に ブランチ側で入った変更を出すので、main が squash merge で同じ内容を持っていても 差分として見える。git cherry(patch-id 照合)と内容比較で測り直したところ、 未取り込みは実際にはゼロだった。 マージ済み判定に三点 diff を使わない。git branch --mergedも squash merge を 未マージと報告するので同様。 使うのはgit cherryか内容比較。
§1 の根本原因(CI が push に結合していて、コードが動かない限り外界の変化を検知できない)を
塞ぐため、.github/workflows/ci.yml に週次 schedule を入れた。
| 追加 | 値 | 理由 |
|---|---|---|
schedule |
cron: '30 0 * * 1'(月曜 09:30 JST) |
検知したいのは外界の変化。検知器は pip-audit |
workflow_dispatch |
— | schedule の初回発火を待たずに確認・再実行するため |
concurrency.group |
...-${{ github.event_name }} を含む |
含めないと schedule 実行が push に cancel される |
- 毎時 00 分を避けた理由: GitHub 公式に「
scheduleイベントは高負荷時に遅延しうる。 高負荷時間帯には毎時の開始が含まれる」「遅延の可能性を下げるには毎時の別の時刻に スケジュールせよ」と明記されているため、意図的に 30 分にずらした。 uv.lockをコミットしている効果:uv syncが lock を尊重するので、週次実行は 「いま固定している版に対する既知脆弱性」を正確に測る。検知器として精度が高い。- フル CI を回す設計にした理由: 監査専用の workflow を別に作ると、PR のマージ関門と
週次チェックが別々に育って乖離する。同じ
ci.ymlを回せば「週次で確認しているもの」= 「マージに必要なもの」が定義上一致する。
public リポジトリの scheduled workflow は、リポジトリの活動が 60 日間無いと GitHub により自動的に無効化される(公式ドキュメントに明記)。
本リポジトリの空白は 74 日(2026-05-30 → 2026-08-12)で、しきい値を 14 日超えている。
この装置は、それが検知しようとしている状態そのものによって停止させられうる。 「週次が動いていること」を外側から確かめる仕組みは、この workflow の中では解決できない。
対策はリポジトリ単体では完結しない(外部からの workflow_dispatch 発火や、
活動中のリポジトリからの横断ディスパッチなど)。フリート全体の装置設計に属するため、
本日時点では限界を明記するに留める。
週次で回しても、失敗が誰にも届かなければ「CI が走っていなかった」が 「走ったが誰も見なかった」に化けるだけなので、実測した。
| 測定項目 | 実測値 |
|---|---|
| 通知の配信 | 機能している(ci_activity 通知が実際に生成されている) |
| 未読通知の総数 | 1,828 件 |
うち ci_activity |
634 件 |
本リポジトリの未読 ci_activity |
128 件 |
| 最古の未読 | 2026-05-11(3 ヶ月前) |
配信は届いている。届いた先が飽和している。 週次の失敗通知を 1 件足しても、 1,828 件の山に 1,829 件目として積まれるだけで、検知装置としては機能しない。 通知経路の設計はフリート全体に効くため別途提案する。
uv.lock はこのリポジトリの CI と開発環境しか守らない。PyPI から入れた利用者が
古い版に固定していれば、脆弱な依存で動く。利用者へ伝播させるには宣言側を上げる必要がある。
| 依存 | 変更 | advisory |
|---|---|---|
mcp |
>=1.0 → >=1.28.1 |
PYSEC-2026-3481 / 3482 / 3483 |
python-multipart |
>=0.0.12 → >=0.0.31 |
PYSEC-2026-3036 / 3037 / 3040 |
pydantic-settings |
>=2.6 → >=2.14.2 |
GHSA-4xgf-cpjx-pc3j |
[tool.uv]
constraint-dependencies = ["mcp<2"]pyproject.toml の dependencies に mcp<2 と書くと、上限が全利用者へ伝播して
下流の解決衝突を生む(ライブラリに上限を書かない慣行の理由)。
constraint-dependencies は uv.lock の解決にのみ適用され、requires-dist には漏れないので、
その副作用だけを外せる。実測で確認した:
| 確認項目 | 結果 |
|---|---|
解決される mcp |
1.29.0(2.0.0 へ飛ばない) |
requires-dist の mcp |
>=1.28.1(<2 は含まれない) |
対照: uv lock --upgrade-package mcp を明示実行 |
1.29.0 のまま・lock に差分なし |
外す条件は (a) 上流の 1.x サポート終了、(b) 2.0 固有機能が必要になったとき。
pyproject.toml のコメントに①何を止めているか②理由③外す条件④参照先を併記した。
理由の無い制約は、半年後には理由不明の呪いになる。
今回の advisory 8 パッケージのうち、直接依存は上記 3 つだけ。残り 4 つの経路を uv tree で実測:
| パッケージ | 経路 | 親の宣言 | 必要版 | 到達可否 |
|---|---|---|---|---|
pyjwt |
← mcp[crypto] |
pyjwt[crypto]>=2.10.1 |
2.13.0 | 🔴 不能 |
cryptography |
← pyjwt[crypto] ← mcp[crypto] |
cryptography>=3.4.0 |
50.0.0 | 🔴 不能 |
starlette |
← fastapi + mcp + sse-starlette |
fastapi 0.136.1(最新)が starlette>=0.46.0 |
1.3.1 | 🔴 不能 |
msgpack |
← cachecontrol ← pip-audit[filecache] |
msgpack>=0.5.2,<2.0.0 |
1.2.1 | 🟢 dev 依存のみ・配布物に含まれない |
msgpack は利用者に無関係(dev 専用)。残り 3 つは、最新の親でさえ下限が足りないため、
親の版を上げるという手段では到達できない。塞ぐには nene2-python が
pyjwt / cryptography / starlette を直接依存として宣言するしかなく、
使っていない依存をバージョン床のためだけに抱えることになる。
ライブラリとしての是非を含めて要判断のため、本リリースの射程外とした。
| 優先度 | タスク |
|---|---|
| 判断待ち | 🔴 推移的依存(pyjwt / cryptography / starlette)を直接宣言して床を作るか(§5-3)。ライブラリが使っていない依存を抱える是非 |
| 判断待ち | CI 失敗の通知経路(配信は機能しているが受信箱が飽和・§4) |
| 中 | 🔴 週次 schedule 自体の生存監視(60 日自動無効化・§4) |
| 塩漬け | mcp 2.0 移行(§2 が設計ノート。constraint で 1.x 固定済み。解除条件は §5-2) |
| 低 | PyJWT 推移的 CVE(PYSEC-2025-183)— mcp 修正待ち |
オープン Issue / PR: ゼロ。main クリーン。CI グリーン(3.12 / 3.14・実 DB 統合テスト含む)。