feat(mcp): native MCP server for coding agents (ADR-021 Phase 5)#567
Conversation
…json Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
… not agent-skills.json Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Expose a curated set of MCP tools to coding agents over Streamable HTTP at /mcp, authenticated by the existing scoped personal access token (the Bearer firewall claims the request by its tt_pat_ header; no security.yaml change). Tools (src/Mcp/Tool), each gated by App\Mcp\ScopeGuard (mirrors the #[RequireScope] rule — scopes narrow, roles still apply): - log_time (entries:write) — flagship; resolves project/activity by name or id and synthesises start/end from a duration, then delegates to SaveEntryAction so persistence, Jira sync and day-class recalc are the web-UI code path - list_recent_entries (entries:read), list_projects (projects:read), list_activities (activities:read) - delete_entry (entries:write) — delegates to DeleteEntryAction, keeping the IDOR ownership guard Discovery: /.well-known/mcp/server.json points MCP clients at /mcp. Tests: ScopeGuard unit tests + integration tests per tool (happy path and a scope-denied path for all five, fail-closed). Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
There was a problem hiding this comment.
Code Review
This pull request implements Phase 5 of ADR-021, introducing a native Model Context Protocol (MCP) server over Streamable HTTP at /mcp using symfony/mcp-bundle. It exposes a curated set of tools (log_time, delete_entry, list_activities, list_projects, and list_recent_entries) secured by scoped personal access tokens via a custom ScopeGuard. Feedback on the implementation focuses on LogTimeTool.php, recommending a try-catch block to gracefully handle malformed date strings during time calculations, and suggesting the replacement of magic Doctrine finders with standard find and findOneBy methods to avoid magic method overhead and improve static analysis.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #567 +/- ##
============================================
- Coverage 84.53% 83.34% -1.19%
- Complexity 3286 3332 +46
============================================
Files 229 236 +7
Lines 8934 9061 +127
============================================
Hits 7552 7552
- Misses 1382 1509 +127
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:
|
There was a problem hiding this comment.
Pull request overview
Adds a native Symfony MCP server endpoint to expose a curated, scope-gated toolset for coding agents (ADR-021 Phase 5), reusing the existing PAT bearer authentication and scope model.
Changes:
- Introduces MCP tooling layer (
ScopeGuard, response decoding helper, and 5 MCP tools includinglog_timeanddelete_entry) wired viasymfony/mcp-bundle. - Adds MCP discovery/docs updates (
/.well-known/mcp/server.json,llms.txttext update, ADR + agent readiness docs). - Adds unit + integration tests covering scope enforcement and tool happy/denied paths; updates composer dependencies and Symfony config.
Reviewed changes
Copilot reviewed 17 out of 19 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/Mcp/ScopeGuardTest.php | Unit coverage for per-tool scope enforcement behavior. |
| tests/Mcp/McpToolsTest.php | Container-level integration tests for each MCP tool under PAT scopes. |
| symfony.lock | Registers Flex recipe additions for http discovery; records MCP bundle. |
| src/Mcp/Tool/LogTimeTool.php | Implements log_time tool delegating to SaveEntryAction with scope gate. |
| src/Mcp/Tool/ListRecentEntriesTool.php | Implements list_recent_entries tool with scope gate and day bounds. |
| src/Mcp/Tool/ListProjectsTool.php | Implements list_projects tool for project discovery under scope. |
| src/Mcp/Tool/ListActivitiesTool.php | Implements list_activities tool for activity discovery under scope. |
| src/Mcp/Tool/DeleteEntryTool.php | Implements delete_entry tool delegating to DeleteEntryAction under scope. |
| src/Mcp/ScopeGuard.php | Central PAT-only + scope enforcement for MCP tools. |
| src/Mcp/DecodesActionResponse.php | Shared JSON response decoding/error extraction for delegated controllers. |
| src/Controller/WellKnown/WellKnownController.php | Adds MCP server card endpoint + updates llms.txt API auth wording. |
| docs/agent-readiness.md | Updates agent readiness guidance to reflect PAT + MCP availability. |
| docs/adr/ADR-021-api-token-authentication.md | Updates ADR Phase 5 decision and implementation notes for MCP. |
| config/routes/mcp.yaml | Imports MCP route loader to expose /mcp endpoint. |
| config/packages/mcp.yaml | Configures MCP bundle (app metadata + Streamable HTTP transport/path). |
| config/packages/http_discovery.yaml | Wires PSR-17 factory services via php-http/discovery. |
| config/bundles.php | Registers Symfony\\AI\\McpBundle\\McpBundle. |
| composer.lock | Locks new dependencies (symfony/mcp-bundle, mcp/sdk, php-http/discovery, etc.). |
| composer.json | Adds MCP-related dependencies to project requirements. |
- resolveTimes uses plain minute arithmetic: a malformed start is a clear ToolCallException, not a PHP 8.3 DateMalformedStringException, and a duration that would run past midnight is rejected up front instead of wrapping the clock into an end < start. - use find() / findOneBy(['name' => …]) instead of the magic findOneById / findOneByName finders (identity-map lookup, clearer static analysis). - tests for the past-midnight and malformed-start cases. Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
|
Status-line bump now that the native MCP server (Phase 5) shipped in #567. All ADR-021 phases are implemented.
…569) End-to-end testing of the deployed Phase 5 MCP server (#567) revealed the `/mcp` endpoint 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. ## `/mcp` was 403 for valid tokens 1. **Fail-closed subscriber.** `RequireScopeSubscriber` denies a token request to any controller without `#[RequireScope]`. MCP multiplexes many per-tool scopes (enforced in each tool by `ScopeGuard`), so a single controller-level scope can't express it. Added a `SelfEnforcesScope` marker the subscriber honours to skip the gate for controllers that enforce scopes per call. 2. **DNS-rebinding host guard.** The SDK's `StreamableHttpTransport` hardcodes `allowedHosts=[localhost]`, 403ing a real domain; the bundle's `McpController` is `final` with no config. Override `mcp.server.controller` with `McpEndpointController` that rebuilds the secure middleware stack (CORS + DNS-rebinding + protocol-version) with the deployment's host(s) allowlisted via `MCP_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.



Completes ADR-021 with a native MCP server so coding agents (Claude Code / Cursor) can log and query time, authenticated by the scoped personal access tokens shipped in Phases 1–4.
Decision
agent-skills.jsonis dropped — 2026 has no client-consumed standard for a callable-skill manifest (ai-plugin.json is dead, llms.txt is a docs pointer, Anthropic "Agent Skills" is a local packaging format, the Cloudflare RFC never converged). MCP is the convergent standard, and its auth spec sanctions static scoped Bearer tokens — our PATs drop straight in. See the revised ADR-021 Phase 5.What ships
symfony/mcp-bundle+mcp/sdk) over Streamable HTTP at/mcp. The existing Bearer firewall claims the request by itstt_pat_header — nosecurity.yamlchange.src/Mcp/Tool), each gated byApp\Mcp\ScopeGuard(mirrors#[RequireScope]; scopes narrow, roles still apply):log_time(entries:write) — flagship; resolves project/activity by name or id, synthesises start/end from a duration, delegates toSaveEntryAction(same persistence + Jira sync + recalc as the web UI)list_recent_entries(entries:read),list_projects(projects:read),list_activities(activities:read)delete_entry(entries:write) — delegates toDeleteEntryAction, keeping the IDOR ownership guard/.well-known/mcp/server.json; fixed the stalellms.txt("no API token yet") anddocs/agent-readiness.md.Tests
ScopeGuardunit tests + integration tests exercising every tool through the container with a scopedApiAccessToken— happy path and a scope-denied path for all five tools (fail-closed). 18 tests. PHPStan L10, cs-fixer, rector, phpat all clean locally.Note
symfony/mcp-bundle(v0.10) /mcp/sdk(v0.6) are pre-1.0 and marked experimental; the thin tool layer keeps protocol churn confined to the bundle.