Skip to content

feat(mcp): native MCP server for coding agents (ADR-021 Phase 5)#567

Merged
CybotTM merged 5 commits into
mainfrom
feat/adr-021-phase5-mcp
Jul 5, 2026
Merged

feat(mcp): native MCP server for coding agents (ADR-021 Phase 5)#567
CybotTM merged 5 commits into
mainfrom
feat/adr-021-phase5-mcp

Conversation

@CybotTM

@CybotTM CybotTM commented Jul 5, 2026

Copy link
Copy Markdown
Member

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.json is 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

  • Native MCP server (symfony/mcp-bundle + mcp/sdk) over Streamable HTTP at /mcp. The existing Bearer firewall claims the request by its tt_pat_ header — no security.yaml change.
  • Curated tools (src/Mcp/Tool), each gated by App\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 to SaveEntryAction (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 to DeleteEntryAction, keeping the IDOR ownership guard
  • Discovery: /.well-known/mcp/server.json; fixed the stale llms.txt ("no API token yet") and docs/agent-readiness.md.

Tests

ScopeGuard unit tests + integration tests exercising every tool through the container with a scoped ApiAccessToken — 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.

CybotTM added 4 commits July 5, 2026 22:19
…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>
Copilot AI review requested due to automatic review settings July 5, 2026 20:45

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/Mcp/Tool/LogTimeTool.php Outdated
Comment thread src/Mcp/Tool/LogTimeTool.php
Comment thread src/Mcp/Tool/LogTimeTool.php Outdated
@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0.78125% with 127 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.34%. Comparing base (9f955b6) to head (d678497).

Files with missing lines Patch % Lines
src/Mcp/Tool/LogTimeTool.php 0.00% 55 Missing ⚠️
src/Mcp/Tool/ListProjectsTool.php 0.00% 19 Missing ⚠️
src/Mcp/ScopeGuard.php 0.00% 11 Missing ⚠️
src/Mcp/Tool/ListActivitiesTool.php 0.00% 11 Missing ⚠️
src/Controller/WellKnown/WellKnownController.php 9.09% 10 Missing ⚠️
src/Mcp/Tool/DeleteEntryTool.php 0.00% 8 Missing ⚠️
src/Mcp/Tool/ListRecentEntriesTool.php 0.00% 7 Missing ⚠️
src/Mcp/DecodesActionResponse.php 0.00% 6 Missing ⚠️
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     
Flag Coverage Δ
integration 54.83% <0.78%> (-0.78%) ⬇️
unit 47.36% <0.00%> (-0.68%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 including log_time and delete_entry) wired via symfony/mcp-bundle.
  • Adds MCP discovery/docs updates (/.well-known/mcp/server.json, llms.txt text 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.

Comment thread src/Mcp/Tool/LogTimeTool.php
- 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>
@sonarqubecloud

sonarqubecloud Bot commented Jul 5, 2026

Copy link
Copy Markdown

@CybotTM CybotTM merged commit 5b9c72a into main Jul 5, 2026
26 checks passed
@CybotTM CybotTM deleted the feat/adr-021-phase5-mcp branch July 5, 2026 20:59
CybotTM added a commit that referenced this pull request Jul 5, 2026
Status-line bump now that the native MCP server (Phase 5) shipped in
#567. All ADR-021 phases are implemented.
CybotTM added a commit that referenced this pull request Jul 6, 2026
…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.
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.

2 participants