fix(mcp): make /mcp reachable for remote clients + admin-status card#569
Conversation
End-to-end testing of the deployed endpoint surfaced two 403s the direct tool tests (which bypass the HTTP layer) missed: 1. RequireScopeSubscriber fail-closed denied /mcp because the bundle controller declares no #[RequireScope]. But MCP multiplexes many per-tool scopes (enforced by ScopeGuard), so a single controller scope can't express it. Add a SelfEnforcesScope marker the subscriber honours to skip the fail-closed gate for controllers that enforce scopes per call. 2. The SDK's StreamableHttpTransport hardcodes the DNS-rebinding guard to allowedHosts=[localhost], 403ing a real domain. The bundle's McpController is final and exposes no config, so override mcp.server.controller with McpEndpointController that rebuilds the secure middleware stack with the deployment's host(s) allowlisted (MCP_ALLOWED_HOSTS / app.mcp_allowed_hosts). Functional test drives the full HTTP stack (firewall + controller + transport): initialize succeeds for an allowed host, a disallowed host is 403, a bogus token is 401. Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
… count) Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
There was a problem hiding this comment.
Pull request overview
This PR fixes remote-client access to the Symfony MCP Streamable HTTP endpoint at /mcp by bypassing controller-level scope gating (while keeping per-tool scope enforcement) and by making the SDK’s DNS-rebinding Host protection configurable for real deployment hostnames. It also adds an MCP subsystem card to the admin status UI with full i18n coverage.
Changes:
- Introduce a
SelfEnforcesScopemarker and updateRequireScopeSubscriberto allow explicitly-marked controllers to enforce scopes per call (needed for MCP tool multiplexing). - Replace the bundle’s final MCP controller with
McpEndpointController, rebuilding the transport middleware stack with configurableMCP_ALLOWED_HOSTS. - Add an end-to-end functional test for
/mcp(allowed host OK, disallowed host 403, bogus token 401) and add an MCP subsystem card (+ translations) to/ui/admin/status.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/Mcp/McpHttpEndpointTest.php | New functional test exercising the full HTTP /mcp stack (auth + host guard + transport). |
| tests/Controller/GetStatusActionTest.php | Extends subsystem-card expectations to include the new mcp card. |
| src/Security/ApiToken/SelfEnforcesScope.php | Adds an explicit marker interface for controllers that enforce scopes internally/per call. |
| src/Security/ApiToken/RequireScopeSubscriber.php | Skips the fail-closed #[RequireScope] gate for SelfEnforcesScope controllers. |
| src/Mcp/McpEndpointController.php | New /mcp controller with configurable DNS-rebinding allowed hosts. |
| src/Controller/Admin/GetStatusAction.php | Adds MCP subsystem card (endpoint, transport, tool count, auth model). |
| frontend/src/pages/AdminStatus.tsx | Adds UI label/description entries for the MCP subsystem. |
| frontend/messages/{en,de,es,fr,ru}.json | Adds localized strings for the MCP subsystem label/description. |
| config/services.yaml | Adds app.mcp_allowed_hosts parameter and overrides mcp.server.controller with McpEndpointController. |
| .env | Adds default MCP_ALLOWED_HOSTS configuration and documentation. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #569 +/- ##
============================================
- Coverage 83.34% 83.22% -0.13%
- Complexity 3332 3337 +5
============================================
Files 236 237 +1
Lines 9061 9091 +30
============================================
+ Hits 7552 7566 +14
- Misses 1509 1525 +16
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
|



End-to-end testing of the deployed Phase 5 MCP server (#567) revealed the
/mcpendpoint 403s every remote client — the direct tool tests bypass the HTTP layer, so they missed it. Two distinct causes, plus the missing admin-status card./mcpwas 403 for valid tokensRequireScopeSubscriberdenies a token request to any controller without#[RequireScope]. MCP multiplexes many per-tool scopes (enforced in each tool byScopeGuard), so a single controller-level scope can't express it. Added aSelfEnforcesScopemarker the subscriber honours to skip the gate for controllers that enforce scopes per call.StreamableHttpTransporthardcodesallowedHosts=[localhost], 403ing a real domain; the bundle'sMcpControllerisfinalwith no config. Overridemcp.server.controllerwithMcpEndpointControllerthat rebuilds the secure middleware stack (CORS + DNS-rebinding + protocol-version) with the deployment's host(s) allowlisted viaMCP_ALLOWED_HOSTS.New functional test drives the full HTTP stack (firewall → controller → transport): initialize succeeds for an allowed host, disallowed host → 403, bogus token → 401.
Admin status
Added the missing MCP subsystem card to
/ui/admin/status(endpoint, transport, live tool count via a tagged locator, per-tool auth) + i18n across all five locales.Deploy note
Prod must set
MCP_ALLOWED_HOSTS=tt.netresearch.de(the mounted env shadows.env).All four PHP gates green; 27 backend + 418 frontend tests pass.