MCPサーバ実装 - #6832
Conversation
StringClassNameToClassConstantRector を Bundle-1.0.0 / Bundle-1.0.1 の BundleCompilerPass に適用。 husky pre-commit の Rector dry-run が origin/4.4 時点で fail していたものを解消する (修正内容自体は同等動作)。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- symfony/mcp-bundle 依存追加と /admin/mcp ルート配線
- AllowListResolver: Api44 の core.api.allow_list を再利用するヘルパ
- EntityArraySerializer: allow_list 駆動の Reflection ベース変換 (深さ 2、循環検知)
- McpAuditLogger: mcp チャネルへの単一エントリポイント
- OriginContentTypeListener: ^/admin/mcp 配下の Content-Type / Origin 検証
- SearchProductsTool / GetProductTool / GetProductStockTool: 商品/在庫 3 ツール
(#[IsGranted('ROLE_OAUTH2_MCP:PRODUCT:READ')] + allow_list ベース出力)
認証認可と scope は Api44 (別 PR) に依存。 注文/顧客会員/プラグイン管理の
8 ツール / Rate Limiter / 管理 UI / 受入基準テストは後続。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- SearchProductsToolTest: scope check + 検索 / limit / offset clamp + allow_list 出力 - GetProductToolTest: id / code 取得 + 不在時の空配列 + allow_list 出力 - GetProductStockToolTest: 規格あり / 規格なし / 在庫無制限 / 不在 + allow_list 出力 UsernamePasswordToken に scope role を渡して TokenStorage に直接セットすることで Tool の AuthorizationChecker を満たす形で結合テストを実現。 Api44 が install + enable されている前提 (core.api.allow_list 経由で出力フィールドが allow_list に従う)。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
実機検証で Master entity が Status: [] と空で返るバグを発見。 原因は Doctrine の Lazy Proxy が返ってきたとき、 自動生成された proxy class 名 (Proxies\__CG__\...) で allow_list を引いて未登録扱いになる経路。 Doctrine\Persistence\Proxy 実装 オブジェクトを get_parent_class で実 entity 名に unwrap してから allow_list を 引くように修正。 同時に DEFAULT_MAX_DEPTH を 2 → 1 に変更。 get_product_stock など root が ProductClass の場合、 Product.ProductClasses[] 経由で兄弟 ProductClass の中身が 大量に重複出力されるノイズを抑止。 必要な Tool は明示的に 2 以上を指定する。 - EntityArraySerializer::resolveEntityClass() で Proxy unwrap - DEFAULT_MAX_DEPTH 2 → 1 - 新規テスト 2 件: Proxy 経路 / 新デフォルト深さ - 既存テスト 16 件は深さ変更後も結果不変 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- SearchOrdersTool: キーワード / 注文番号 / ステータス / 期間 / 金額レンジ / 顧客 ID で検索 OrderRepository::getQueryBuilderBySearchDataForAdmin を流用 - GetOrderTool: 注文 ID または注文番号で詳細取得 - GetShippingTool: 注文に紐づく Shipping 一覧 (出荷ステータス / 配送日 / 追跡番号 / 配送先) 必要 scope: mcp:order:read。 出力は Api44 の allow_list (Order / Shipping) の項目のみ。 氏名・住所等の PII が含まれ得る (設計どおり、 scope 付与で運用統制)。 各 Tool に結合テスト追加 (14 件、 全 121 assertions)。 createOrder ヘルパのデフォルト status (PROCESSING) は admin 検索のデフォルト除外条件と衝突するため、 Generator 経由で OrderStatus::NEW の Order を作るヘルパ createOrderInDefaultSearchable を用意。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tomer_orders) - SearchCustomersTool: キーワード / 電話 / ステータス / 登録期間 / 購入合計 / 購入回数で検索 CustomerRepository::getQueryBuilderBySearchData を流用 (email 専用キーは admin 側にないため keyword/multi で兼ねる) - GetCustomerTool: 会員 ID から詳細取得 - GetCustomerOrdersTool: 指定会員の購入履歴を OrderRepository::getQueryBuilderByCustomer 経由で取得 必要 scope: mcp:customer:read。 出力は Api44 の allow_list (Customer / Order) の項目のみ。 氏名・メール・電話・住所等の PII を含み得る (設計どおり、 scope 付与で運用統制)。 各 Tool に結合テスト追加 (12 件、 全 71 assertions)。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ListPluginsTool: インストール済みプラグイン一覧 (enabledOnly フィルタ可)。 出力は Plugin entity の allow_list (id / name / code / version / enabled / initialized 等) のみ - GetPluginTool: id または code から Plugin entity 詳細 + app/Plugin/<code>/composer.json の description / require をマージ (依存関係を AI が見る用途)。 個別プラグインの設定値 (API キー等の機微データ) には踏み込まない 必要 scope: mcp:plugin:read。 設計 §5 のプラグイン管理境界 (メタ情報まで、 設定値は別 scope の get_plugin_settings として将来検討) に従う。 各 Tool に結合テスト追加 (9 件、 全 42 assertions)。 Api44 自身が install 済みである前提で Api44 を題材に詳細取得・composer.json マージを検証。 これにより設計の全 11 ツール (商品/在庫 3 + 注文 3 + 顧客会員 3 + プラグイン管理 2) が 出揃った。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ScopeChecker は scope 不足時に ToolCallException を投げる - mcp-bundle が catch し result.isError=true + content text に整形 - role 名を OAuth2 scope 名 (mcp:order:read) に変換して返す - mcp-bundle が Tool 内例外を catch するため HTTP 403 化は不可 (docs/mcp/scope-denied-response.md) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- mcp-bundle は空配列を text では []、 structuredContent では {} と出し分ける
- Get 系 4 Tool の不在分岐を 'data' => ['found' => false] に変更
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- mcp_ip: IP 単位 60/分。 kernel.request priority 14 で admin firewall より先に消費 (認証エラー連発も抑制) - mcp_client: client_id 単位 300/分。 firewall 通過後 OAuth2Token から取得し kernel.controller で消費 - 超過時 429 + retry_after_seconds、 Retry-After / X-RateLimit-* ヘッダ付与 - 監査ログに rate_limited を記録 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ToolsListContractTest: 11 Tool の DI 登録と #[McpTool] name 一致を検証 - AllowListContractTest: Product/Customer/Order の出力 keys が allow_list の subset、 かつ空でないことを検証 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- NoDirectMcpLoggerInjectionRule: __construct で LoggerInterface $mcpLogger を持つクラスを検出 - McpAuditLogger 以外は error (eccube.mcp.directLoggerInjection) - 監査ログを McpAuditLogger に一本化する規律を core で静的に強制 - カスタマイズ側は phpstan の paths 拡張で同規律を適用できる旨を docblock に明記 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Bearer なし → 401 (oauth2 entry point) - 不正な opaque Bearer → 401 (league validator) - 署名不正な JWT → 401 (signature 検証) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- league の AccessToken + CryptKey で JWT を自前発行 - revoke() 後の JWT → 401 - Member.Work=NON_ACTIVE → MemberProvider 解決失敗で 401 - McpFirewallContractTest の HTTP method / status を定数化 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Api44 が install + enabled + initialized であることを検証 - /admin/mcp の FirewallMap 解決が mcp (stateless oauth2) - /admin/ は admin firewall (cookie based) のまま - McpScope::ROLE_* 定数が §4.1 の scope 文字列と一致 - 「無効化で消える」 は kernel reboot が必要なため手動確認で代替 (docblock 明記) - rector.php: ContainerGetNameToTypeInTestsRector を本テストのみ skip (private service ID は test container で FQCN 解決不可) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ScopeEnforcingReferenceHandler: Builder::setReferenceHandler に差し込み全 Tool 呼び出しが通過。 McpToolScopeMap で必要 scope を引き、 未登録は fail-closed deny / 不足は ToolCallException / 非 Tool は素通し - McpToolScopeMap: tool 名 → 必要 role の唯一の中央定義 (未登録は全 deny) - McpScopeEnforcementPass: McpPass の Tool ServiceLocator を再利用して inner ReferenceHandler を構築し setReferenceHandler に配線 (優先度 -100) - ToolInvoker: requiredScope と ScopeChecker 呼び出しを撤去 (audit + 計時のみ) - 11 Tool: invoke() から requiredScope を削除 - テスト: 各 Tool の scope テストを ScopeEnforcingReferenceHandlerTest に集約 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- McpAuditLoggerChannelLockPass: $mcpLogger の autowire alias のみ削除し、 mcp チャンネルへの到達を @monolog.logger.mcp 名指しのみに限定 - 他クラスが $mcpLogger を注入しても default チャンネルに解決され mcp は汚れない - LoggerChannelPass の後・AutowirePass の前に走らせるため before-optimization 負優先度で登録 - McpAuditLogger は @monolog.logger.mcp を名指しバインド - NoDirectMcpLoggerInjectionRule と neon 登録を撤去 (DI 方式に一本化し PHPStan を回さなくても常時有効) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- consume() 例外時は 503 rate_limiter_unavailable を返す - 監査ログに InternalError (reason: rate_limiter_unavailable) を記録 - IP / client_id の consume を共通 check() に集約 - 黙ってカウンタを失う劣化 (Redis ダウン時の miss 等) は本層で検知不可 (docblock 明記) - storage 例外で 503 になることをテスト Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 「401 でない」 から 200 + result 検証に強化し偽陽性を防ぐ - ensureClient の戻り型を ClientInterface に修正 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
scope 未登録の Tool は ScopeEnforcingReferenceHandler が実行時に fail-closed で deny するため、 登録漏れが本番呼び出しまで気付かれない。 これを CI で拾う。 McpToolScopeMapContractTest: - Tool ディレクトリを実走査して #[McpTool] を全件発見し、 各 tool 名が McpToolScopeMap に登録されていることを検証 (登録漏れゼロを保証) - 逆に McpToolScopeMap の各エントリが実在 Tool に対応することも検証 (typo / 削除済み Tool の残骸を検出) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
mcp チャンネルが存在するのに想定 id の autowire alias が見つからない場合 (monolog のバージョン/命名規約変更で id がズレた等)、 削除が空振りして 監査チャンネルが誰でも書ける状態に黙って戻る。 これを LogicException で build を止めて検出する (fail loud)。 mcp チャンネル自体が無い構成では 保護対象も無いのでスキップする。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- McpScopeEnforcementPassTest: builder への setReferenceHandler 差し込みと inner 構築を検証 - McpScopeEnforcementIntegrationTest: 実カーネルに tools/call を流し、 充足は result / 不足は isError:true + Insufficient scope を確認 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- safeAudit() で監査ログの例外を握り潰し 503 / 429 の返却を保証 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- safeAudit は監査例外を握り潰しつつ、 default チャンネルに失敗を 1 行記録 - 拒否 (429/503) は維持しつつ mcp 監査チャンネル障害を可観測にする - フォールバックは $logger (default チャンネル) 経由で mcp チャンネルは汚さない - 監査失敗でも 429 が返り fallback に記録されることをテスト Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- revoked / Member 無効化の両 401 で WWW-Authenticate: Bearer を確認 (oauth2 の bearer 拒否であることを担保) - Member 無効化は body "Bad credentials" で user 解決失敗の経路を識別 (token 拒否と区別) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- mcp チャネル専用ハンドラ (rotating_file, info から常時出力) を追加 - 出力先 var/log/<env>/mcp.log、 権限 0640、 保管 90 日 (ECCUBE_MCP_LOG_RETENTION_DAYS) - 1 レコード 1 JSON (eccube.mcp.log.formatter.json) - prod / dev の main(site.log) から mcp チャネルを除外し PII の site.log 流入を防ぐ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- mcp チャネルに書いたログが mcp.log に出て site.log に漏れないことを検証 (実書き込みで確認) - prod / dev の main ハンドラが mcp チャネルを除外する設定であることを検証 - rector.php: monolog.logger.mcp を ID 取得するため ContainerGetNameToTypeInTestsRector を本テストで skip Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AuthFailureAuditListener: mcp パスの 401 レスポンスを kernel.response で拾い logAuthEvent(TokenInvalid) を記録 (無トークン・無効トークン両方を捕捉) - mcp パスで 401 を返すのは認証失敗のみ (scope 拒否=200 / rate=429,503 / origin=403,415) なので誤検知しない - 監査書き込み失敗は default チャネルに記録して握り潰し、 401 応答を壊さない - TokenInvalid のログレベルを warning に (error はサーバ障害 InternalError 専用) - 未参照の AuditResult::ValidationError を削除 - AuditResultUsageTest: 全 case が src から参照されること (孤児 case 防止) を検証 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- WWW-Authenticate あり → reason にヘッダ値が入る - WWW-Authenticate なし → reason は fallback 'unauthorized' - 匿名 logger の log() に context の型注釈を付与 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughEC-CUBE に MCP サーバを統合し、OAuth2 認証、scope 強制、レート制限、監査ログ、11 個の Tool、CLI 実行、allow_list 準拠のシリアライズ、ユニット・統合・E2E テストを追加した。 ChangesMCP サーバ統合
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Api44/DB 非依存の 2 クラスを素の TestCase で単体テストし、 これまで Api44 ゲートの結合テスト経由でしか触れられなかった round2 のロジックを直接縛る
- 正規化・nullable 除去・非配列プロパティ防御・列=全行キー和集合・{min,max} レンジとスカラーガード・パイプ/改行エスケープを回帰から守る
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- admin 検索は空 status を「絞り込みなし」と解釈するため、 明示指定した statusIds が 1 つも解決しないと status フィルタが落ち全状態 (非公開・廃止含む) へ広がっていた - 絞り込み意図を尊重し、 その場合は 0 件 (該当なし) を返す Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 表の途中に紛れたスカラー要素を空行で消さず先頭列に出す
- {min,max} の unlimited を filter_var で厳密判定し、 文字列 "false" 等を無制限と誤読しない
- 複数型 union の基底型は一意に決められないため string 扱いにし、 有効な他型入力の誤 reject を防ぐ
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # composer.lock
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/Eccube/Command/EccubeCliToolCommand.php (1)
1-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCLI コマンドの型変換・必須検証ロジックに対する単体テストが見当たりません。
cast()の整数/真偽値変換やアレイオプションの必須検証など、分岐が多く誤りが混入しやすい箇所です。今回のレビュー対象テスト一覧 (tests/Eccube/Tests/...) にEccubeCliToolCommandやMcpCliCommandPass向けのテストが含まれていないため、McpCliToolInvoker/McpMarkdownFormatterをモックした単体テストの追加を推奨します。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Eccube/Command/EccubeCliToolCommand.php` around lines 1 - 142, 追加された EccubeCliToolCommand の分岐をカバーする単体テストを作成してください。McpCliToolInvoker と McpMarkdownFormatter をモックし、cast() の整数・浮動小数点・真偽値の成功および不正入力、配列オプションの変換、必須オプション不足時の Command::INVALID、正常実行時のフォーマット結果出力を検証してください。ツール定義と inputSchema は各テストで必要な型・必須条件を設定し、McpCliCommandPass 向けテストが未実装なら対象シンボルのコマンド登録も検証してください。src/Eccube/DependencyInjection/Compiler/McpCliCommandPass.php (1)
34-36: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
list/help/引数なし起動時は全ツール分のコマンドがインスタンス化されてしまいます。doc コメント (34-36行目) は「実際に呼ばれたコマンドだけがインスタンス化される」としていますが、
console.commandタグにdescription属性を渡していないため、bin/console list・bin/console help・引数なし起動時に Symfony が各コマンドの説明を取得するために全コマンドを実体化してしまいます (Symfony Console のドキュメント上、listはレイジーコマンドであっても説明取得のために全コマンドを instantiate すると明記されています)。ツール数分、configure()→$this->invoker->tool($toolName)が毎回呼ばれることになり、コメントの意図と食い違います。
#[McpTool]属性インスタンスからnameと同様にdescriptionも compile 時に取得し、タグに渡すことでlistを本当にレイジーにできる可能性があります (属性がその情報を持っているか要確認)。♻️ 提案する修正の方向性
- foreach ($reflection->getAttributes(McpTool::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { - /** `@var` McpTool $instance */ - $instance = $attribute->newInstance(); - $names[] = $instance->name ?? $reflection->getShortName(); - } + foreach ($reflection->getAttributes(McpTool::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + /** `@var` McpTool $instance */ + $instance = $attribute->newInstance(); + $name = $instance->name ?? $reflection->getShortName(); + $descriptions[$name] = $instance->description ?? ''; + }- $definition = (new Definition(EccubeCliToolCommand::class)) - ->setArguments([...]) - ->addTag('console.command', ['command' => 'eccube:cli:'.$toolName]); + $definition = (new Definition(EccubeCliToolCommand::class)) + ->setArguments([...]) + ->addTag('console.command', [ + 'command' => 'eccube:cli:'.$toolName, + 'description' => $descriptions[$toolName] ?? '', + ]);Also applies to: 48-58, 82-98
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Eccube/DependencyInjection/Compiler/McpCliCommandPass.php` around lines 34 - 36, Update McpCliCommandPass so each console.command tag receives the tool description from the corresponding McpTool attribute, retrieving it at compile time alongside the tool name. Ensure the tag metadata is sufficient for Symfony to display list/help descriptions without instantiating every command, preserving lazy command creation and updating the affected documentation to match the behavior.app/config/eccube/packages/lock.yaml (1)
1-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
framework.lockの重複設定に注意。
framework.yamlにも既にlock: flock(Agent Commerce/UCP Catalog のキャッシュスタンピード対策用)が設定されており、本ファイルは同じキーを MCP 向けの別理由で再度設定しています。現在は同値のため実害はありませんが、将来どちらか一方だけを変更すると設定ドリフトが起きやすくなります。理由コメントを1ファイルに集約するか、既存のframework.yamlにMCP向けの理由コメントを追記する形に統合することを推奨します。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/config/eccube/packages/lock.yaml` around lines 1 - 6, Remove the duplicate framework.lock setting from lock.yaml and consolidate the flock rationale in the existing framework.yaml configuration. Extend that file’s comments to cover the MCP Rate Limiter and login_throttling requirements while preserving the single shared lock: flock setting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/e2e-test.yml:
- Around line 230-240: Update both actions/checkout steps in the mcp job—the
self-repository Checkout and Checkout Api44 plugin—to set persist-credentials to
false, including the external EC-CUBE/eccube-api4 checkout.
---
Nitpick comments:
In `@app/config/eccube/packages/lock.yaml`:
- Around line 1-6: Remove the duplicate framework.lock setting from lock.yaml
and consolidate the flock rationale in the existing framework.yaml
configuration. Extend that file’s comments to cover the MCP Rate Limiter and
login_throttling requirements while preserving the single shared lock: flock
setting.
In `@src/Eccube/Command/EccubeCliToolCommand.php`:
- Around line 1-142: 追加された EccubeCliToolCommand
の分岐をカバーする単体テストを作成してください。McpCliToolInvoker と McpMarkdownFormatter をモックし、cast()
の整数・浮動小数点・真偽値の成功および不正入力、配列オプションの変換、必須オプション不足時の
Command::INVALID、正常実行時のフォーマット結果出力を検証してください。ツール定義と inputSchema
は各テストで必要な型・必須条件を設定し、McpCliCommandPass 向けテストが未実装なら対象シンボルのコマンド登録も検証してください。
In `@src/Eccube/DependencyInjection/Compiler/McpCliCommandPass.php`:
- Around line 34-36: Update McpCliCommandPass so each console.command tag
receives the tool description from the corresponding McpTool attribute,
retrieving it at compile time alongside the tool name. Ensure the tag metadata
is sufficient for Symfony to display list/help descriptions without
instantiating every command, preserving lazy command creation and updating the
affected documentation to match the behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9895f4a9-f053-497d-80aa-5c68955c1d14
⛔ Files ignored due to path filters (1)
composer.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
.github/workflows/e2e-test.yml.github/workflows/unit-test.ymlapp/config/eccube/packages/dev/monolog.ymlapp/config/eccube/packages/e2e/monolog.ymlapp/config/eccube/packages/lock.yamlapp/config/eccube/packages/monolog.ymlapp/config/eccube/packages/prod/monolog.ymlapp/config/eccube/services.yamlcomposer.jsone2e/playwright.config.tse2e/tests/mcp.spec.tsrector.phpsrc/Eccube/Command/EccubeCliToolCommand.phpsrc/Eccube/DependencyInjection/Compiler/McpCliCommandPass.phpsrc/Eccube/DependencyInjection/Compiler/McpScopeEnforcementPass.phpsrc/Eccube/Kernel.php
🚧 Files skipped from review as they are similar to previous changes (6)
- app/config/eccube/packages/dev/monolog.yml
- app/config/eccube/packages/prod/monolog.yml
- app/config/eccube/packages/monolog.yml
- .github/workflows/unit-test.yml
- src/Eccube/DependencyInjection/Compiler/McpScopeEnforcementPass.php
- app/config/eccube/services.yaml
- framework.yaml が既に lock: flock を設定しており、 lock.yaml は同値の再設定で重複していた - 削除後も framework.yaml 側が残るため実効設定は不変 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- console.command タグに name だけを渡していたため、 bin/console list/help が説明取得のため全 eccube:cli:* を実体化していた - #[McpTool] の description を compile 時に取得してタグへ渡し、 実際に呼ばれたコマンドだけを実体化する Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- integer/number/boolean の成功・不正入力 (null=失敗)・falsy 成功値・素通しを DataProvider で網羅する - McpCliToolInvoker/Builder が final でモック不可のため、 $this に触れない純粋 private ヘルパを newInstanceWithoutConstructor + リフレクションで直接検証する Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
CodeRabbit のレビュー指摘に対応しました(feature/poc-mcp 反映済み)。
|
MCPサーバ本体のレビュー(実機検証ベース)
|
- pc.stock の andWhere は fetch-join した ProductClasses を部分ハイドレートし、 ProductPriceStockSummarizer の集計が条件外の規格を落としてレンジが縮んでいた - 商品単位の EXISTS 部分クエリ (別エイリアス) で絞り込み、 pc の完全ハイドレート (=正しいレンジ) を保つ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 会員・注文検索で明示 statusIds が 1 つも解決しないと status フィルタが落ち、 全件 (PII 込み) を返していた。 SearchProducts と同じ 0 件ガードを入れる - 注文番号検索は OrderRepository で完全一致のため前後空白を trim して渡し、 doc の「部分一致」を「完全一致」に訂正する Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 会員・注文検索の解決不能 statusIds が該当なしを返すことを縛る - 在庫絞り込みの EXISTS 部分クエリが DQL エラーにならず実行されることを縛る Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ECCUBE_MCP_ALLOWED_ORIGINS 未設定でも prod (kernel.debug=false) では Origin ヘッダを持つ (=ブラウザ発) リクエストを 403 で拒否し、 未設定のまま DNS リバインディング等に晒さない - dev/test は従来どおり skip、 Origin 無し (curl/サーバ間) は prod でも通す - skipUnvalidatedOrigin は kernel.debug をバインド。 未バインドはビルド失敗で検知 (fail-closed) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- product scope のみの token が customer / plugin 領域のツールを呼べないことを実カーネル経由で縛る (従来は product↔order の 1 組のみ) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 認可は firewall(ROLE_ADMIN) とツール層の領域 scope の二層で、 領域/read はツール層でのみ効くことを明記 - /admin/mcp 配下に scope 非検査の管理コントローラを足さない (足すなら領域 scope 検査必須) を規約化し、 盗難/低 scope トークンの到達を防ぐ意図を記す Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Member 認証で ROLE_ADMIN は付くが mcp read scope を 1 つも持たない token は、 ツール到達前に access_control 段で 403 になることを実カーネル経由で検証する Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /admin/mcp が access_control で最低 1 つの mcp read scope を要求するようになったため、 認証経路(正常/失効/無効化)を検証する有効フロー用の JWT に mcp:product:read を付与する - 失効/無効化は認証段で 401 になり access_control 前で弾かれるため挙動は不変 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- composer.lock は MCP 依存ツリーを保持したまま upstream 更新を取り込み再生成する - OrderMemoFlowTest は本体規約に合わせ rector (ContainerGetNameToTypeInTestsRector) を適用する Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ContainerGetNameToTypeInTestsRector が別サービス ('eccube.purchase.flow.shopping' / '.order') を同一 PurchaseFlow::class へ潰すと、 shopping flow の processor が消え OrderMemoFlowTest が落ちる
- 同ルールの skip が短縮名と FQCN の2キーに分かれ、 配列キー衝突で OrderMemoFlowTest 分が無効化されていたため1エントリに統合する
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ScopeFilteringRegistry が mcp.registry を装飾し、 呼べない Tool を一覧に出さない (最小権限) - ページング前に絞る。 内側で先にページングすると、 可視 Tool が pageSize を跨いだとき空ページ + 非 null カーソルになり一覧が欠けるため - トークンが無い経路 (CLI) は素通し。 call 時の拒否は ScopeEnforcingReferenceHandler が担う二重防御 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ttokoro20240902
left a comment
There was a problem hiding this comment.
再確認レビューです。07-21 に出した 6 件はいずれも指摘した範囲を超えて丁寧に対応いただいており(Origin は環境別 fail-closed、tools/list はページング順序の落とし穴まで踏まえた実装、認可境界は規約文書化+実カーネルでのテスト固定)、対応漏れはありませんでした。CI も現 HEAD fad62d943e で 115 チェック全緑です。
残る指摘は 3 点で、すべて 在庫絞り込みを EXISTS 化した 9b0b4bc94 が持ち込んだ意味変更とそのテストです。base 4.4 に当該ファイルは無いので 4.4 に対する regression ではなく、PR 内での意味変更という位置づけです。
| # | 重大度 | 指摘 | 該当 | 内容 | 提案 |
|---|---|---|---|---|---|
| A | 🟠 仕様確認 | stockMin / stockMax が別々の EXISTS なので、min と max を別の規格が満たしてもヒットする |
src/Eccube/Service/Mcp/Tool/SearchProductsTool.php の在庫絞り込み |
独立した 2 本の EXISTS は式として max(stock) >= stockMin AND min(stock) <= stockMax、つまり**「商品の在庫レンジが [stockMin, stockMax] と交差する」と等価です。EXISTS 化前の andWhere('pc.stock >= :min') / andWhere('pc.stock <= :max') は「[stockMin, stockMax] に入る規格が 1 つ以上ある」**でした。どちらも成立する解釈ですが、現状の挙動は docblock(=AI に渡すツール契約)に書かれていません。実データでの確認: 商品 id 6041 の規格在庫は 372, 822, 832, 931。stockMin=400 stockMax=800 で現状はヒットしますが [400,800] に入る規格は 1 つもありません。同条件の件数は 分離 26 件 / 同一規格 25 件(差は 6041 の 1 件)で、eccube:cli:search_products --stockMin=400 --stockMax=800 の total も 26 でした。 |
レンジ交差が意図なら docblock に明記。「範囲内の規格を持つ商品」が意図なら min/max を 1 本の EXISTS にまとめる(pc を制約しない=レンジ縮み対策は維持されます)。C と同じ 1 箇所で両方直ります |
| C | 🟠 要修正 | EXISTS に visible 条件が無く、非表示規格だけが在庫条件を満たす商品がヒットする(出力レンジに現れない在庫で絞り込まれる) |
同上 | 置き換え前の pc は ProductRepository::getQueryBuilderBySearchDataForAdmin が andWhere('pc.visible = :visible')(true)を掛けていたため、在庫絞り込みも表示規格だけが対象でした。新しい EXISTS は別エイリアスなのでこの制約が外れています。一方 ProductPriceStockSummarizer は isVisible() で非表示規格を除外して集計するので、絞り込みの母集団と出力レンジの母集団が食い違います。非表示規格は特殊データではなく、規格なし商品に規格を登録すると ProductClassController の「デフォルト規格を非表示にする」処理が stock / stock_unlimited をそのまま残して visible=false にするだけなので、通常運用で発生します。実データでの確認: stockMin=900 の件数は 現状 11 件 / visible=true 条件付き 8 件(CLI の total も 11)。差分 3 件(id 6026・6029・6041)は非表示規格だけが 900 以上で、表示規格の最大在庫は 705 / 426 / 832。「在庫 900 以上」で検索して stock.max が 832 の商品が返ります。 |
EXISTS に pcStock.visible = true を追加 |
| B | 🔵 テスト | 在庫絞り込みのテストが、②の元の不具合も A・C も縛れていない | tests/Eccube/Tests/Command/McpCliCommandTest.php の testSearchProductsStockFilterExecutes |
--stockMin=1 --stockMax=1000 で Command::SUCCESS のみ assert しており、コミットメッセージどおり「DQL エラーにならない」ことしか保証していません。②の価格/在庫レンジが縮まないことは未検証で、SearchProductsToolTest にも在庫絞り込みのケースがありません。 |
置き場所は SearchProductsToolTest(Tool を直接呼べて items[].stock.min/max を見られる)が適切。①絞り込みあり/なしでレンジが一致(②の回帰ガード)②[400,800] に入る規格が無い商品がヒットしない(A)③非表示規格だけが満たす場合にヒットしない(C)の 3 本。Generator::createProduct は全規格を visible=true / 在庫 100〜999 で作るので、テスト内で stock を上書きし、C 用に visible=false の規格を 1 つ足す形になります |
A・C をまとめて直す差分案
// 在庫絞り込みは 1 本の EXISTS にまとめる (同一規格が両条件を満たすことを要求)。
// visible = true は元の pc (getQueryBuilderBySearchDataForAdmin) と揃え、
// 出力レンジ (ProductPriceStockSummarizer は非表示規格を除外) と母集団を一致させる。
if (null !== $stockMin || null !== $stockMax) {
$dql = 'SELECT pcStock.id FROM '.ProductClass::class.' pcStock'
.' WHERE pcStock.Product = p AND pcStock.visible = true AND pcStock.stock_unlimited = false';
if (null !== $stockMin) {
$dql .= ' AND pcStock.stock >= :mcpStockMin';
}
if (null !== $stockMax) {
$dql .= ' AND pcStock.stock <= :mcpStockMax';
}
$qb->andWhere($qb->expr()->exists($dql));
if (null !== $stockMin) {
$qb->setParameter('mcpStockMin', $stockMin);
}
if (null !== $stockMax) {
$qb->setParameter('mcpStockMax', $stockMax);
}
}A をレンジ交差のまま維持する判断であれば、visible の追加(=C の修正)だけ入れて docblock に交差セマンティクスを明記する形でも構いません。A はどちらの解釈を採るかの仕様判断なので、実装前に方針だけ合わせさせてください。
検証方法: コード読解 + 実 DB への直 SQL + eccube:cli:search_products の実機実行(件数は SQL と実機で一致)。ブラウザ経路は手元環境がメンテナンスモードのままで未確認です。
- RegistryInterface 変更に ScopeFilteringRegistry を追随(register 系の戻り値を各 Reference へ・$isManual 削除、 Resource→ResourceDefinition、 unregister*/has* 追加、 clear/DiscoveryState 撤去) - bundle 0.12 が file-based discovery を compile-time 登録へ置換したため mcp.yaml の discovery.scan_dirs を撤去(#[McpTool] の autoconfiguration で登録される) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- e2e-test.yml: 我々の mcp job と 4.4 の playwright-installer job を両方保持する(git が共通行で 2 job を交錯させたため、各 job を完全な形で再構成) - e2e/playwright.config.ts: mcp-tests / install-tests の両 project を保持する - composer.lock: 4.4 の依存を base に mcp-bundle 0.12 / mcp/sdk 0.7 を再解決する Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- min/max を別々の EXISTS にすると min と max を別規格が満たしてもヒットする(レンジ交差)。 1 本の EXISTS にまとめ、 同一規格が [stockMin, stockMax] に入ることを要求する - EXISTS に visible = true を追加。 出力レンジ(ProductPriceStockSummarizer は非表示規格を除外)と絞り込みの母集団を揃え、 非表示規格だけが条件を満たす商品を除外する - docblock(ツール契約)に単一規格・表示規格のセマンティクスを明記する - SearchProductsToolTest に在庫絞り込みの回帰テストを追加(レンジ非縮小・範囲内規格の要求・非表示規格の除外) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ProductClass::setStock は ?string。 テストは declare(strict_types=1) のため int を渡すと TypeError になる (Generator は非 strict で暗黙変換されていた) - stock の集計値は文字列で返るため、 レンジ比較は int にキャストして数値で比較する Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
再確認レビューありがとうございます。 A(仕様判断)は単一 EXISTS を採用しました。 C は EXISTS に B は
|
- services.yaml: Eccube\ glob の exclude に MCP 用の Rector / Command/EccubeCliToolCommand.php 除外を残す (McpCliCommandPass がツールごとに定義を生成するため、 これらを glob から外さないとコンテナ生成が壊れる) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
概要
EC-CUBE の管理データ(商品・在庫/注文/顧客会員/プラグイン)を、AI クライアント(Claude Desktop / Cursor / VS Code など)から自然言語で参照できる読み取り専用の MCP サーバを追加します。
/admin/mcpで Streamable HTTP を提供し、4 領域・11 ツール(すべて read-only)を公開します。MCP サーバ本体は LLM を持たない薄いアダプタで、推論はクライアント側で行います。認証・認可は独自実装せず、API プラグイン api44 の OAuth2 に任せます。
Refs #6796
構成(2 層)
.well-known/ 動的登録)。認証・認可を api44 の 1 箇所に寄せ、分散によるリスクを避けます。
方針
mcp:product:read/mcp:order:read/mcp:customer:read/mcp:plugin:read)。api44 のread/writeとはmcp:接頭辞で分け、GraphQL 用トークンで MCP を叩けないようにします。ScopeEnforcingReferenceHandlerでデコレートし、中央レジストリMcpToolScopeMapをIsGrantedで照合します。未登録ツールは fail-closed で全 deny。ツール本体は scope を意識しません。mcp→mcp.log(DB テーブルは増やしません)。MCP 境界のイベント(ツール呼び出し/scope 拒否/rate limit/Origin 違反/認証失敗)を client・IP 単位で記録します。実装メモ
^/admin/mcp用 firewall(stateless / oauth2)を prepend します。本体の security.yaml は変更しません(^/apiと同じ手法)。result.isError = true。mcp-bundle がツール実行中の例外を JSON-RPC に変換するため、kernel.exception に届かず HTTP は常に 200 です(詳細docs/mcp/scope-denied-response.md)。McpAuditLoggerを唯一の書き手とし、compiler pass で mcp チャネルへの注入を 1 点に限定。401 はAuthFailureAuditListenerが kernel.response で拾います。/admin/mcpと api44 の scope 追加が中心で、MCP 機能は管理画面で有効/無効を切り替えられます。テスト
{"found": false})。McpToolScopeMapに scope を持つ契約テスト(未登録=fail-closed)。mcp.logへ分離(site.log に漏れない)契約テスト、401 の記録、AuditResult 全 case の参照確認。CI は unit-test.yml で実行します:
phpunit(16 マトリクス・--exclude-group mcp)と、mcp(--group mcp・api44 を mock-package-api で導入)。相談したいこと
mcp:product:readがそのままROLE_OAUTH2_MCP:PRODUCT:READになり role 名にコロンが残ります(動作はします)。定数で集中管理し後から一括変更可能ですが、命名の最終形に意見が欲しいです。Summary by CodeRabbit