Skip to content

AgenticCMSに対応 - #4433

Open
ryuring wants to merge 56 commits into
baserproject:5.4.xfrom
ryuring:dev-agentic
Open

AgenticCMSに対応#4433
ryuring wants to merge 56 commits into
baserproject:5.4.xfrom
ryuring:dev-agentic

Conversation

@ryuring

@ryuring ryuring commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

概要

baserCMS を AI エージェントから操作できるようにする MCP(Model Context Protocol)サーバーを、コアプラグイン BcMcp として追加します。

Claude や ChatGPT などの MCP クライアントをコネクタとして接続し、会話から固定ページ・ブログ・カスタムコンテンツを作成・編集・削除できます。認証は OAuth 2.1、権限は Admin Web API に準じます。

MCP 仕様は 2026-07-28(ステートレスコア)と、それ以前の initialize 方式の両世代に対応します。

主な変更

1. BcMcp プラグインの追加

plugins/bc-mcp/ をコアプラグインとして追加します。あわせて composer.jsonphpunit.xml.distphpdoc.dist.xmlsplit_monorepo.yml へ登録し、baser-corecorePlugins に加えています。

提供するツール(51件)

分類 ツール
固定ページ getPages / getPage / addPage / editPage / deletePage
ブログ記事 getBlogPosts / getBlogPost / addBlogPost / editBlogPost / deleteBlogPost
ブログ getBlogContents ほか5件
ブログカテゴリ getBlogCategories ほか5件
ブログタグ getBlogTags ほか5件
カスタムコンテンツ getCustomContents ほか5件
カスタムエントリー getCustomEntries ほか5件
カスタムフィールド getCustomFields ほか5件
カスタムテーブル getCustomTables ほか5件
カスタムリンク getCustomLinks ほか5件
システム情報 serverInfo

2. MCP 仕様 2026-07-28 への対応

MCP SDK に logiscape/mcp-sdk-php v2 を採用し、Modern(2026-07-28)と Legacy(initialize 方式)を同時に提供する Dual-era サーバーとしています。プロトコルの世代判定・server/discover・必須ヘッダの検証・resultType やキャッシュヒントの付与は SDK が担います。

あわせて SDK では解決しない次の項目に対応しました。

  • Origin ヘッダの検証 — DNS リバインディング攻撃対策。Streamable HTTP の MUST 要件
  • iss パラメータの付与(RFC 9207)— 認可レスポンスに発行者を含める
  • ネゴシエーション内容のロギング — 接続ごとのプロトコル世代・クライアント名・メソッドを記録し、管理画面から確認できる

3. 常駐プロセスの廃止(プロセス内実行)

従来は MCP サーバーを常駐プロセスとして起動し、McpProxyController から内部 HTTP で転送していました。これを廃止し、CakePHP のリクエスト内で完結させます。

クライアント → HTTP → baserCMS(/bc-mcp)
  → McpProxyController(OAuth2 認証・権限チェック・Origin 検証)
  → McpRequestHandler(同一プロセス内で MCP サーバーを実行)
  → 各ツール → baserCMS 操作

管理画面の「MCPサーバー管理」は起動・停止の操作画面から、接続用 URL・提供ツール一覧・直近の接続状況を表示する情報画面に変わります。起動操作は不要になりました。

4. スコープの明文化

BcMcp を「運営者向けの認証付き MCP サーバー」と定義し、判断の軸として4つの原則を設計書に記録しました。

  1. 客層は運営者に限定する(一般ユーザー向け機能は扱わない)
  2. 権限は Admin Web API に委ねる(独自の権限体系を作らない)
  3. 特定クライアントの都合に合わせない(ただし標準化された拡張には追従する)
  4. 機能追加は要望ベース(Admin Web API の網羅を目的としない)

この方針に基づき、次を整理しました。

  • stdio トランスポートの削除bin/cake bc_mcp.server は OAuth 認証・権限チェック・Origin 検証をいずれも通らない経路だったため廃止し、提供するトランスポートを HTTP のみとしました(原則2)
  • search / fetch の削除 — 公開済みコンテンツのみを返す一般ユーザー向けの検索であり、客層が異なります。単一ベンダー固有のレスポンス形式を要求される点も原則3に反します
  • チャンク分割アップロードの削除 — MCP の File Uploads WG でも仕様から外された方式です。アイキャッチ等の画像は URL と data: URI の2方式に絞りました

5. ツール注釈の宣言

全51ツールに MCP のツール注釈(readOnlyHint / destructiveHint / idempotentHint / openWorldHint)を宣言し、クライアントが読み取り専用ツールと破壊的操作を区別できるようにしました。

接頭辞 readOnly destructive idempotent
get* / serverInfo true
add* false false false
edit* false true true
delete* false true true

Claude の Research はツール呼び出しに都度承認を挟まないため、この区別を提供する意味があります。

baser-core 側の変更

ファイル 変更
config/setting.php corePluginsBcMcp を追加
tests/TestCase/BaserCorePluginTest.php 認証を免除するパスに /bc-mcp/bc-mcp/oauth2/*/baser/admin/bc-mcp/oauth2/* が加わるため、期待値を更新
tests/TestCase/Service/BcDatabaseServiceTest.php コアプラグイン追加に伴う期待値の更新
plugins/bc-installer/.../InstallationsAdminServiceTest.php 同上

/bc-mcp は MCP クライアントからのエンドポイント、/bc-mcp/oauth2/* は OAuth の認可フローで、いずれも管理画面のログインセッションとは別に認証されるため、baserCMS 側の認証を免除しています。実際の認可は McpProxyController がアクセストークンを検証して行います。

依存パッケージ

パッケージ 用途
logiscape/mcp-sdk-php ^2.0 MCP SDK(Dual-era 対応)
league/oauth2-server ^8.5 OAuth 2.1 認可サーバー
nyholm/psr7 ~1.8.2 PSR-7 実装
symfony/psr-http-message-bridge ~2.3.1 CakePHP と PSR-7 の橋渡し
ext-openssl JWT の署名

テスト

  • BcMcp: 215 tests / 1358 assertions
  • 全体: 4820 tests / 10953 assertions、失敗0・エラー0

Modern と Legacy 両世代の疎通、ツール実行、OAuth の各フロー、ツール注釈の全数検証(ツール追加時の付け忘れを検出)を含みます。

動作確認

自動テストに加えて、実際のクライアントで次を確認しました。

  • MCP Inspector — 固定ページの作成 → 公開 → 公開 URL が 200 → 削除 → 404 まで、CRUD を通しで確認
  • 正式な SSL 環境 — Cloudflare Tunnel で正式な証明書を持つ HTTPS URL を用意し、issuer が公開 URL で生成されること、および実クライアントから接続できることを確認

多くの MCP クライアントは自己署名証明書のサーバーに接続できないため、ローカル環境を一時的に HTTPS 公開して検証する手順を plugins/bc-mcp/README.md に記載しています。

設計ドキュメント

判断の経緯を docs/superpowers/ に残しています。

ファイル 内容
specs/2026-08-12-mcp-2026-07-28-bc-mcp-impact.md MCP 仕様 2026-07-28 の影響調査
specs/2026-08-12-bc-mcp-sdk-migration-design.md SDK 移植と Dual-era 対応の設計
specs/2026-08-17-bc-mcp-scope-design.md スコープと方針(4原則、将来構想の着手条件)

既知の制約とスコープ外

ファイルのアップロードは、ローカルのファイルを直接送ることができません。これは BcMcp の制約ではなく、ホスト(Claude や ChatGPT)がファイルの中身を MCP サーバーへ渡す手段をまだ持っていないためです。MCP に File Uploads Working Group が設置され、SEP-2631 で検討が進んでいます。現状は URL と data: URI の2方式で対応します。

次はスコープ外としています。それぞれ着手条件を設計書に記録しました。

  • 一般ユーザー向けの公開エンドポイント — サイトの MCP エンドポイントをエージェントが自動発見する規約(SEP-2127 ほか)が未確定のため
  • 横断検索 — 各一覧ツールの keyword で代替します
  • Admin Web API の網羅Contents / ContentFolders(サイト構造)などは要望に応じて追加します

@ryuring

ryuring commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

@ryuring ryuring closed this Jun 21, 2026
@ryuring ryuring reopened this Jun 21, 2026
@ryuring

ryuring commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

ryuring and others added 11 commits June 21, 2026 13:03
DB を全 drop して再マイグレーションするテスト/インストール処理の再作成対象に
BcMcp が含まれず、oauth2 系テーブルが復活しないため OAuth2 系テストが 500 になっていた。

- bc-installer InstallationsAdminServiceTest::test_deleteAllTables の再作成リストに BcMcp を追加
- BcInstaller の testStep3(createDb) は defaultInstall のみ作り直すため、BcMcp の testsuite を
  BcInstaller より前(BcSeo の後)へ移動(phpunit.xml.dist)
- コアプラグインコンバートスキルに知見を追記(複数のマイグレーション再作成リスト/testsuite 順序)
- 外部依存取り込みに伴う composer.lock 更新

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
testIntegration / testIntegrationWithPKCE は実 MCP サーバー(SSE)が必要で、
CI(GitHub Actions)ではバックグラウンドサーバーを起動・到達できず 500 になる。
サーバー起動を該当2テストに限定し、起動できなければ markTestSkipped する
(サーバーのある環境では実行、CI では安全にスキップ)。スキルにも知見を追記。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isServerRunning() は pidファイル+プロセス生存のみで、ポート bind 完了を保証しない。
CI ではプロセス起動後にポート bind が間に合わず、プロキシが 127.0.0.1:{port} へ
接続できず 500 になっていた。requireMcpServer() を fsockopen による実接続待ち
(最大15秒ポーリング)に変更し、到達できればテスト実行、到達不可なら markTestSkipped。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
markTestSkipped はサーバー起動不具合を隠して CI を緑にしてしまうため、
実ポート到達待ち(最大15秒)後に到達できなければ assertTrue で明示的に失敗させる。
サーバーが立てばテストは実行・検証され、立たなければ赤で検知できる。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
外部プロセス(MCPサーバー等)を要するテストは、該当テスト側で起動し、
プロセス存在でなく実接続到達まで待ち、到達不可なら skip せず明示的に失敗させる方針を
.github/instructions/basercms.instructions.md に記載。コアプラグイン変換スキルも同方針へ更新。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI で MCP サーバーが起動しない原因を切り分けるため、127.0.0.1:{port} へ到達できない
場合は bc_mcp_server.log の末尾を失敗メッセージに添える。コマンド未登録・DB未接続・
ポート競合などの起動失敗理由が CI 出力に現れるようにする。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
サーバー子プロセス bin/cake bc_mcp.server は default 接続の plugins テーブル
(status=true)から有効プラグインをロードする。BcMcp は defaultInstall 外のため
bin/cake install では有効化されず、CI で bc_mcp.server コマンドが未登録=起動失敗
→プロキシ500 になっていた。requireMcpServer() の冒頭で BcMcp を plugins へ
有効化(既存ならスキップ)してから起動する。test.yml は変更しないテスト完結の対応。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- oauth2-server 8.x の RedirectUriValidator が呼ぶ league/uri の
  非推奨 Uri::createFromString() を回避するため、自前の
  RedirectUriValidator(Uri::new() 使用) と AuthCodeGrant override を追加し
  OAuth2Service で差し替え。依存ツリーは無変更(psr/http-message ^1.0 /
  php-mcp/server 据え置き、oauth2-server 9.x は psr ^2 必須で導入不可のため)。
- CustomContentsTool の暗黙nullable引数3箇所を明示化(?Type $x = null)。
- BaseMcpTool の $http_response_header を PHP8.5 互換の取得方法に修正。

フルスイート 4583 tests OK / createFromString・deprecated 出現 0 を確認。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- php-migration: vendor 由来の非推奨は継承 override で呼び出し自体を回避する
  手法、自前コードとの切り分けを追記。
- basercms-unittest: 関連メモを更新。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ryuring and others added 2 commits June 23, 2026 16:32
bin/cake bc_mcp.server は PHPUnit とは別プロセスで起動し、bootstrap 時の
getEnablePlugins() が参照する DB(既定 default)に BcMcp が無いと
コマンドが未登録となり「Unknown command cake bc_mcp.server」で起動失敗する
(ローカルは常駐プロセスが残っていたため偽陽性で通っていた)。

- baser-core/config/bootstrap.php: CLI かつ env BC_CONNECTION 指定時に
  その接続を default にエイリアスする汎用機構を追加(プラグインロード前)。
  BcMcp 非依存。install.php が test 接続も定義済みのため子プロセスでも利用可能。
- McpServerManager::startMcpServer: connection 指定時、子プロセスへ
  BC_CONNECTION 環境変数と --connection オプションを付与。
- McpServerCommand: --connection オプション追加(実行時エイリアス+ログ)。
- OAuth2ControllerTest::requireMcpServer: connection=test を渡す。

クリーン状態(default/test とも BcMcp レコード無し)でフルスイート
4583 tests OK / Unknown command 出現 0 を確認。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
統合テストが起動した SSE 常駐サーバーを停止しておらず、孤児プロセスが
CI ステップの継承 fd を掴んだまま残り、出力パイプが EOF にならずジョブが
次ステップへ進めず無限待機していた(GHA の PHP8.1 で5時間ハング→キャンセル)。

- OAuth2ControllerTest に tearDown を追加し、起動したサーバーを確実に停止。
- McpServerManager::startMcpServer: 子プロセスの stdin を /dev/null へ切り、
  stdin/stdout/stderr すべてを親から切り離して継承 fd を掴ませない。

クリーン状態で統合テスト OK / テスト後にサーバープロセス・PIDファイルが
残らないことを確認。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@momofff momofff added this to the 5.4.0 milestone Jul 9, 2026
ryuring and others added 30 commits August 12, 2026 19:16
McpProxyController が常駐サーバーへ HTTP 転送するのをやめ、McpRequestHandler
経由で同一プロセス内の SDK を実行するようにした。責務は認証・認可と、CakePHP の
リクエスト/レスポンスと SDK の HttpMessage の相互変換に絞られる。

- initialize 応答の偽装(protocolVersion の書き換えと capabilities の虚偽申告)を削除
- GET / DELETE は認証より前に 405 を返す(2026-07-28 で GET ストリームが廃止)
- 応答のステータスコードとヘッダは SDK が返した内容をそのまま反映する
- 認証済みの操作者は McpContext 経由で渡し、リクエストボディは改変しない

設計の抜けとして、Legacy 世代のクライアントはセッションを必要とし、ヘッダを
透過しないと「Session ID required」で 400 になる事が判明したため、
Mcp-Session-Id と Last-Event-ID を透過対象に追加した。これにより
initialize → セッションID発行 → tools/list という Legacy の正規フローが
動作する事を確認済み。

OAuth2ControllerTest は常駐サーバーを起動していた requireMcpServer()(63行)と
その停止処理を削除し、Modern 形式でリクエストする postMcp() ヘルパに統一した。
CI で孤児プロセスがジョブをハングさせる問題も、常駐プロセスの廃止により
根本的に解消する。

bc-mcp のテストは 187 tests / 791 assertions で全て通過。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
クライアントがどの世代(Modern / Legacy)でどのプロトコルバージョンを要求して
きたかを logs/mcp.log に記録する。常駐プロセスの死活監視が不要になった代わりに、
これがクライアント側の移行を検知する主要な手段になる。引数やトークンの中身は
記録しない。

実装中に判明した点を2つ解消した。
- Log::write() の第3引数は $context['scope'] でスコープを判定するため、
  配列を直接渡すと scope が空になり mcp.log へ書かれない
- プラグインの setting.php は baser-core が Configure::consume('Log') を実行した
  後に読み込まれるため、mcp スコープのロガーが Log へ登録されない。
  BcMcpPlugin::bootstrap() で登録するようにした

記録される形式:
MCP negotiation: era=modern protocolVersion=2026-07-28 client=名前/版 method=tools/call

bc-mcp のテストは 195 tests / 818 assertions で全て通過。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
McpServerManger(PIDファイル管理・ps による死活監視・起動/停止/再起動)を
削除し、コマンドは標準入出力のみを提供する形にした。HTTP 経由の利用は
/bc-mcp エンドポイントが同一プロセス内で処理する。

管理画面は死活監視・起動停止ではなく情報表示画面として再構成した。
- 接続情報(エンドポイントURL・.well-known の各URL・対応プロトコルバージョン)
- 直近の接続状況(世代・プロトコルバージョン・クライアント)。常駐プロセスの
  死活監視を失う代わりに、クライアント側の移行をここで確認できる
- 利用可能なツール(tools/list から取得)。移植前は3行の手書きで実態と大きく
  ずれていたが、ツールを追加すれば表示にも反映されるようになった

SDK はツール一覧の取得 API を持たないため、本番と同じ経路で tools/list を
実行して取得している。プラグイン単位のグループ化は取得手段が無いため行わない。

bc-mcp のテストは 197 tests / 918 assertions で全て通過。描画時の
Undefined variable も発生していない事を確認済み。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
本移植の受け入れテストとして、同一サーバーが両世代に応じる事を検証する。
- server/discover が supportedVersions と capabilities を返す(Modern の MUST)
- Modern の tools/call が resultType: complete で応答する
- Legacy の initialize がセッションIDを払い出し、それを使って tools/list と
  tools/call まで通る
- Legacy でセッションID無しのリクエストは 400 になる
- 未対応バージョンは -32022(UnsupportedProtocolVersion)
- ヘッダとボディの不一致は -32020(HeaderMismatch)

あわせて serverInfo ツールの outputSchema 宣言漏れを修正した。ツールクラスへの
一括適用時に McpServer.php が対象から漏れており、serverInfo の呼び出しだけが
「expected string or CallToolResult, got array」で失敗していた。このテストが
検出した。戻り値の構造が固定であるため、型のみの共通宣言ではなく詳細な
スキーマを宣言している。

server/discover の応答は supportedVersions で返り、サーバーの識別情報は
仕様どおり _meta の io.modelcontextprotocol/serverInfo に入る事を確認した。

bc-mcp のテストは 203 tests / 941 assertions で全て通過。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Origin 検証(Streamable HTTP の MUST 要件):
DNS リバインディング攻撃対策として、ブラウザから送信された Origin を検証し、
許可リストに無い場合は 403 を返す。トランスポートレベルの要件であるため認証より
前に効かせる。Origin を持たないリクエスト(サーバー間通信)は検証対象外。
許可オリジンは setting.php の BcMcp.allowedOrigins で設定でき、空の場合は
SITE_URL のオリジンのみを許可する。あわせて CORS の
Access-Control-Allow-Origin をワイルドカードから許可オリジンのみに変更した。

iss パラメータ(RFC 9207):
認可レスポンスの approve / deny 双方に iss を付与する。2026-07-28 の
クライアントは iss があれば検証が MUST。メタデータには
authorization_response_iss_parameter_supported を追加した。issuer は
認可レスポンスの iss と同一でなければならないため、OAuth2Util::getIssuer() に
導出処理を集約し、メタデータ側もこれを使うようにした。

テストが環境依存で不安定になる点に対処した。baserCMS は TRUST_PROXY が有効な
場合 https 検出器を静的に差し替え、HTTPS ではなく X-Forwarded 系を参照する。
単体では通るが全体実行では失敗するため、どちらの検出器でも成立するよう
テストのリクエストに両方の環境変数を渡している。

bc-mcp のテストは 212 tests / 959 assertions で全て通過。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
フルスイートで回帰が無い事を確認した。
Tests: 4810, Assertions: 10544, 失敗・エラー 0 件
(Skipped 2 / Incomplete 440 はいずれも移植前から存在する既存の状態)

固定ページツールは削除の挙動とテスト間のDBクリーンアップが未解決のため、
SDK 移植とは分けて仕上げる。実装で判明した baserCMS の作法(検索インデックス
生成にログイン状態が必要、ContentFactory のノードは recover() が必要、
getIndex が draft に LIKE 条件を無条件付与する等)を第12.4節に記録した。

あわせて BcTestCase::setFixtureTruncate() が宣言のみで実際に FixtureStrategy を
切り替えるコードが存在せず機能しない事も記録した(bc-mcp とは無関係な
baser-core 側の発見)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bc-mcp には固定ページを操作するツールが無かったため、getPages / getPage /
addPage / editPage / deletePage の5つを追加した。

固定ページは pages テーブルと contents テーブルの複合構造で、pages.contents が
ページ本文、content キー(Contents アソシエーション)がタイトルや URL という
紛らわしい構成のため、対応関係をコメントで明示している。plugin(BaserCore)と
type(Page)は固定値のためツール側で補い、AI クライアントには指定させない。

実装で判明した baserCMS の作法(設計書 第12.4節に記録)。
- 保存の afterSave で検索インデックス生成が走り、現在のサイト情報を参照するため
  テストではログイン状態が必要
- ContentFactory で作ったノードは lft / rght が整合せず Contents->recover() が必要
- PagesService::getIndex() が draft に LIKE 条件を無条件付与するため、draft が
  NULL のレコードは一覧に出てこない。保存時に空文字を入れる
- PagesService::delete() は完全削除であり、pages と contents のレコードが
  いずれも消える(ゴミ箱には残らない)

bc-mcp のテストは 218 tests / 1011 assertions で全て通過。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
固定ページは Content が必須で、Content にはサイトの指定が必須である。つまり
「どのサイトに作るのか」という情報が必ず必要になるため、サイトIDを 1 で決め打ち
していた実装を改め、省略された場合は SitesTable::getRootMain() でメインサイトを
解決するようにした。解決できない場合はエラーを返す。

親フォルダについても同様に、省略時は指定されたサイトのルートを解決し、
特定できない場合はエラーを返す。

あわせて次を整理した。
- draft を空文字で保存する回避策を削除(baser-core 側の
  PagesService::createIndexConditions() を修正したため不要になった)
- テストの loginAdmin() を削除。検索インデックス生成にログインが必要という
  当初の見立ては誤りで、真因はテストDBに残っていた前回実行のデータにより
  sites テーブルが空になっていた事だった(実験で確認)

bc-mcp のテストは 220 tests / 1016 assertions で全て通過。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 検索インデックス生成にログインが必要という記述を削除。実験(ログインなし・
  Router のリクエストなしでも成功)により誤りと確認した。真因はテストDBに
  残っていた前回実行のデータで sites テーブルが空になっていた事だった
- 固定ページとブログ記事の差(固定ページは保存中の Content から Sites->get() を
  呼ぶが、ブログ記事は既存 Content を引くだけで Sites->get() を呼ばない)を追記
- draft の LIKE 条件は baser-core 側のバグとして 5.4.x で修正済みである事を反映
- サイトの指定を必須情報として扱う方針を追記

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MCP クライアント(公式 Inspector)で検証したところ、tools/list の時点で全ツールが
拒否された。outputSchema.type に複数型(['object','array'])を指定していたためで、
JSON Schema としては有効だが MCP のクライアント実装は単一値を期待する。
SDK 側は検証しないためユニットテストでは緑のまま通っていた。

  "path": ["tools", 0, "outputSchema", "type"]
  "message": "Invalid input: expected \"object\""

BaseMcpTool::OUTPUT_SCHEMA を ['type' => 'object'] に修正した。既存の一覧系ツールは
いずれも data / pagination を持つオブジェクトを返すため object で正しい。素の配列を
返していた getPages のみ不整合だったので、他の一覧系ツールと同じ形式に統一した。

MCP Inspector で OAuth 認可を通し、次を実機で確認済み。
- tools/list が全ツールを受理
- serverInfo / getBlogTags / getPages が structuredContent 付きで応答
- addPage で固定ページを作成(サイトIDと親フォルダIDが自動解決される)
- editPage で公開に切り替え、公開URLが 200 で表示される
- deletePage で削除、URLが 404 になる

bc-mcp のテストは 220 tests / 1019 assertions で全て通過。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- ツール一覧を実際に登録されている 51 ツールに更新
  無効化されている search / fetch / sendFileChunk は記載しない
- 誤ったツール名を修正(getCustomContentEntries → getCustomEntries など)
- 常駐プロセス前提の記述を、プロセス内実行の実態に合わせて修正
- 対応プロトコルバージョンの説明を追加
- 存在しないログパスを実際の logs/mcp.log・logs/bc_mcp_error.log に修正
- Cloudflare Tunnel でローカル環境を HTTPS 公開して実クライアントで
  動作確認する手順を追加

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bc-mcp を「運営者向けの認証付き MCP サーバー」と位置づけ、機能追加の
可否を判断する4つの原則を定める。スコープ外のコードの削除、ツール注釈
の宣言、将来構想の着手条件を設計書に記録する。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PermissionManager を呼ぶのは McpProxyController のみで、bin/cake
bc_mcp.server は OAuth 認証・権限チェック・Origin 検証・ログイン
ユーザーの設定を全て素通りしていた。

管理画面のツール一覧は McpRequestHandler をプロセス内で呼ぶため
影響を受けない。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
search / fetch は一般ユーザー向けの検索インデックスを露出しており、
運営者向けという位置づけと客層が異なる。加えて単一ベンダー固有の
レスポンス形式を要求される。

チャンクアップロードは MCP の File Uploads WG でも仕様から外された
方式で、根本のボトルネックはホストがファイルの生バイトをサーバーへ
渡せない点にある。受け取り側の processChunkFile() も対で削除し、
アイキャッチは URL と data: URI の2方式に絞る。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
チャンクアップロード経路を廃止した後もisFileUploadable()に
拡張子付き文字列をアップロード可能とみなす分岐が残っていた。
CustomEntriesTool::isFileUploadable利用箇所で、拡張子付きの
通常のテキスト値がファイルと誤判定され、processFileUpload()が
falseを返して静かに失敗する不整合があったため、対象をdata: URI
とhttp(s) URLの2方式に揃える。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
接頭辞から自動判定せず明示指定する方針のため、付け忘れを検出する
仕組みとして tools/list を全数走査するテストを既存の McpServerTest
に追加する。

この時点では各ツールへの注釈付与が未了のため testAllToolsDeclareAnnotations
は失敗する(意図的な赤)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
全ツールの tool() 呼び出しに annotations を追加し、クライアントが
読み取り専用ツール(get系)と破壊的操作(add/edit/delete系)を
区別できるようにする。接頭辞ごとに BaseMcpTool の
ANNOTATION_READ/CREATE/UPDATE/DELETE 定数を割り当て、
serverInfo のみ BaseMcpTool を継承していないため同等の配列を直値指定した。
ツールの追加・削除・改名は行っていない(公開ツール数51のまま)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
getCustomEntries はタイトル・スラッグの絞り込みを実装として持ちながら
inputSchema に宣言しておらず、クライアントから指定できなかった。
横断検索を持たない方針のため、各一覧ツールの絞り込みは揃っている
必要がある。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ファイルアップロードは URL と data: URI の2方式であることを明記し、
廃止したチャンク方式の記述を削除する。Cloudflare Quick Tunnel の
制約と、固定ホスト名が必要な場合の Named Tunnel を追記する。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ファイルアップロード節の冒頭が、直後の「現状の対応方法」(URLとdata: URIの
2方式)と矛盾していたため書き直す。bc-mcpの制約ではなくホスト側の制約である
ことを明記する。

Named Tunnel の手順が create/route dnsのみでingress設定とトンネル起動が
欠けており、このままでは動作しないため、config.ymlの作成とtunnel runを追記する。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
チャンクアップロード廃止でisFileUploadable()から拡張子付き文字列を許容する分岐を
削除した際、呼び出し側3箇所のfalse側分岐を棚卸ししていなかったため、以下が起きていた。

- editBlogPostで不正なeyeCatch指定(例: 'photo.jpg')が「空文字列=削除」の分岐に
  落ち、既存のアイキャッチが黙って削除される回帰があった(最優先で修正)
- addBlogPost/editBlogPostともに、processFileUpload()が失敗(到達不能URL等)しても
  エラーを返さず無言で成功扱いになっていた
- カスタムエントリーのBcCcFile型フィールドに、アップロード不可な素のファイル名を
  渡すと「通常の値」として素通りし、存在しないファイル名がDBに書き込まれていた

あわせて、processImageUpload()のdocblockに残っていたチャンクアップロード経路の
古い記述をprocessFileUpload()に揃えて修正した。

重複していたtestAddBlogPostWithInlineEyeCatch()をeditBlogPostの回帰テストに
書き換え、上記の不具合それぞれに対する赤→緑のテストを追加した。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
testAddBlogPostWithUnreachableEyeCatchUrlReturnsError() がloginUserIdを渡して
いなかったため、アイキャッチ処理に到達する前にgetAuthorId(null, null)が例外に
なり、I-2の実装をrevertしても緑のまま(空振り)になっていた。

loginUserId: 1 を渡して投稿者解決を通過させ、エラーメッセージの中身まで検証する
ようアサーションを強化。実装を一時的にコメントアウトして赤くなること、戻して
緑になることを確認した(結果はfinal-fix-report.mdに追記)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLI の別プロセスへ任意の DB 接続を使わせる仕組みで、ユニットテストが
起動する常駐 MCP サーバープロセスのために追加したもの。その常駐プロセス
(bin/cake bc_mcp.server)を本ブランチ内で削除したため、この環境変数を
設定する箇所はどこにも残っていない。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
期待する項目を個別に assert していたため、読み取り専用ツールに
destructiveHint が付いているような「余分な注釈」を素通りしていた。
配列全体を比較する形に変え、付け忘れ・値の取り違え・余分な項目の
いずれも検出できるようにする。

title は MCP の任意項目のため比較から除く。

get 系ツールに destructiveHint を一時的に足して赤くなることを確認した。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
eyeCatch の分岐は「未指定=変更しない」「空文字列=削除」「不正値=エラー」
「アップロード失敗=エラー」と成功の5経路がある。異常系にテストを足した際、
主経路である成功だけが未検証で、条件を書き換えて正当な値まで弾いても
気づけない状態だった。

成功経路を一時的に潰して赤くなることを確認した。

あわせて、テスト用の PNG の data: URI を eyeCatchBase64() へ切り出し、
addBlogPost 側のテストと共用する。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
processCustomFields() がフィールドごとに CustomLinks を検索しており、
カスタムエントリーの登録・更新でフィールド数に比例したクエリが出ていた。
BcCcFile 型の判定を追加した際にその検索が2度走るようになり顕在化した。

CustomEntriesService::setup() が CustomEntriesTable::setLinks() を通じて
CustomLinks を CustomFields 付きで読み込み済みであり、processCustomFields()
が呼ばれる時点でフィールド定義はメモリ上にある。それを参照する
buildFieldTypeMap() を用意し、フィールド型のマップを1回だけ組み立てて
渡す形に変える。本体の CustomEntriesTable が links を使い回すのと同じ扱い。

これに伴い、独自にDBを引いていた getCustomFieldType() /
isFileUploadField() / isFileUpload() を削除する。対応するテストは
buildFieldTypeMap() の検証に置き換えた。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BlogHelperTest::testPosts の乱数依存による不安定な失敗の修正を取り込む。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants