Skip to content

refactor(mcp): stop importing the consumer, and drop what agent already owns - #19

Merged
Snider merged 1 commit into
mainfrom
fix/break-agent-cycle
Aug 8, 2026
Merged

refactor(mcp): stop importing the consumer, and drop what agent already owns#19
Snider merged 1 commit into
mainfrom
fix/break-agent-cycle

Conversation

@Snider

@Snider Snider commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

PR 2 of 2 in the dependency-cycle break. dAppCore/agent#20 landed the capability first; this removes the copy. 2786 deletions, 102 insertions.

This package imported Core\Mod\Agentic\Models\* in four files while dappcore/agent is meant to depend on it — a library importing the thing that imports it.

Three files go, because agent already owns them

Verified rather than assumed:

file finding
AgentToolRegistry agent's has all 13 shared methods plus 6 more, none only here. Unreferenced in this package — even McpApiController reaches for agent's FQCN, not this one.
AgentSessionService agent's has all 20 shared plus replay and getReplayContext, none only here. Zero references.
McpAgentServerCommand 2064 lines. Its 29 tool* methods each map to an extracted class in agent's Mcp\Tools\Agent, and agent's Boot::onMcpTools registers every one — I checked registration, not just that the files exist.

Why the capability moved first

The command's five resource* handlers were the one thing agent did not have — its server answered resources/list with a hardcoded empty array and had no resources/read at all. Deleting this first would have silently dropped:

plans://all
plans://{slug}
plans://{slug}/phases/{order}
plans://{slug}/state/{key}
sessions://{id}/context

They were ported to agent in #20, and only then is this removed.

The interface split

McpApiController carried a third copy of the same rendering, and that is what made the cycle compile-time. The plan and session renderers are replaced by AgentResourceProvider — an interface this package owns, implemented by the module that owns the data, resolved from the container and absent without complaint when agent isn't installed. Both model imports go with them.

Interim, stated plainly

Nothing is bound to AgentResourceProvider yet, so this package's HTTP plans:// and sessions:// endpoints answer "not found" until agent binds its registry to it. Agent implementing the interface requires agent to depend on dappcore/mcp, which is still blocked on the Core\Mcp\ PSR-4 root being claimed by both packages.

The data is not lost: agent's own MCP server serves all five URIs as of #20. It is the HTTP mirror here that is degraded until the namespace work lands.

Left by design

Two soft couplings remain — container lookups by string, no import, both guarded: AgentToolRegistry in executeTool and AgentApiKeyService in McpAuthenticate. Neither is a compile-time dependency and neither creates a composer cycle. Both would be better behind interfaces, and are left for when the namespace collision is resolved.

Verification

Suite unchanged: 21 failed, 299 passed — identical to main.

🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io

…dy owns

This package imported Core\Mod\Agentic\Models\* in four files while
dappcore/agent is meant to depend on it — a library importing the thing that
imports it. AX-8: the arrow points one way.

Three files go because agent already owns them, verified rather than assumed:

  AgentToolRegistry     agent's has all 13 shared methods plus 6 more, none
                        only here. Unreferenced in this package — even
                        McpApiController reaches for agent's FQCN, not this.
  AgentSessionService   agent's has all 20 shared plus replay and
                        getReplayContext, none only here. Zero references.
  McpAgentServerCommand 2064 lines. Its 29 tool methods each map to an
                        extracted class in agent's Mcp\Tools\Agent, and
                        agent's Boot::onMcpTools registers every one — checked
                        for registration, not just for the files existing.

Its five resource handlers were the one thing agent did NOT have: agent's
server answered resources/list with a hardcoded empty array and had no
resources/read. Deleting this first would have silently dropped
plans://all, plans://{slug}, plans://{slug}/phases/{order},
plans://{slug}/state/{key} and sessions://{id}/context. They were ported to
agent first, in dAppCore/agent#20, and only then is this removed.

McpApiController carried a third copy of the same rendering, and that is what
made the cycle a compile-time one. The plan and session renderers are replaced
by AgentResourceProvider, an interface this package owns and the module that
owns the data implements, resolved from the container and absent without
complaint when agent is not installed. Both model imports go with them.

Interim worth stating plainly: nothing is bound to AgentResourceProvider yet,
so this package's HTTP plans:// and sessions:// endpoints answer "not found"
until agent binds its registry to it. Agent implementing the interface needs
agent to depend on dappcore/mcp, which is still blocked on the Core\Mcp\ PSR-4
root being claimed by both packages. The data itself is not lost — agent's own
MCP server serves all five URIs as of #20; it is the HTTP mirror here that is
degraded until that lands.

Two soft couplings remain by design, both container lookups by string with no
import and a guard: AgentToolRegistry in executeTool and AgentApiKeyService in
McpAuthenticate. Neither is a compile-time dependency and neither creates a
composer cycle; both would be better behind interfaces, and are left for when
the namespace collision is resolved.

Suite unchanged: 21 failed, 299 passed. 2786 deletions, 102 insertions.

Co-Authored-By: Virgil <virgil@lethean.io>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Snider, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: be056c3f-a98e-42b6-88b6-7327752fa166

📥 Commits

Reviewing files that changed from the base of the PR and between f9fb390 and eed8488.

📒 Files selected for processing (6)
  • php/src/Mcp/Boot.php
  • php/src/Mcp/Console/Commands/McpAgentServerCommand.php
  • php/src/Mcp/Controllers/McpApiController.php
  • php/src/Mcp/Resources/Contracts/AgentResourceProvider.php
  • php/src/Mcp/Services/AgentSessionService.php
  • php/src/Mcp/Services/AgentToolRegistry.php

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Snider
Snider merged commit ad44a48 into main Aug 8, 2026
1 of 3 checks passed
@Snider
Snider deleted the fix/break-agent-cycle branch August 8, 2026 10:51
Snider added a commit that referenced this pull request Aug 8, 2026
…ally (#21)

#19 moved the plans:// and sessions:// rendering behind AgentResourceProvider
and I described the dead endpoints as an interim, waiting on dappcore/agent to
depend on this package and implement the interface. That day does not come:
agent maintains its own copy of Core\Mcp rather than depending on this package,
so it can never name the interface in an implements clause. An interim with no
end is a decision that has not been made, so it is made here.

The controller now accepts any object bound under the interface name that
provides read(). A provider able to name the interface still implements it and
is accepted nominally; one that cannot binds and satisfies the method. Both
work, neither package imports the other, and two endpoints that would otherwise
have stayed dead permanently come back on as soon as agent binds.

Half a contract is rejected rather than accepted: an object with neither read()
nor the interface returns null, so the controller answers a clean not-found
instead of fatalling mid-request.

The contract also loses entries(). Nothing in this package ever called it —
GET servers/{id}/resources lists a server's own configured resources, which is
a different concept — so it was a method an implementer had to satisfy for no
consumer. Providers are still free to offer listing for their own transports;
agent's registry does, for its stdio server. This package asks only for what it
uses.

Four tests cover the resolution: a duck-typed binding with no implements
clause, a nominal implementation, an object missing read(), and nothing bound
at all.

Suite: 21 failed, 303 passed, from 21 failed, 299 passed — the four new tests,
no change to the existing failures.
Snider added a commit that referenced this pull request Aug 8, 2026
…lasses (#20)

Step 1 of letting dappcore/agent consume this package instead of forking it.

Eleven classes exist in both repos under the same Core\Mcp namespace, because
agent maps Core\Mcp\ onto its own php/Mcp/ as well. The public API is identical
on all eleven — same methods, same constants — and every difference is in
protected helpers: this package prefixes them with get (getStateKey,
getCacheKey, getRegisteredServers) where agent does not, and agent extracts a
few more.

Provenance settles which way to merge. These files date from this package's
2026-03-09 initial commit; agent's landed 2026-04-25 as "implement extended RFC
services + transport" and were then hardened on 2026-04-27 by "address
CodeRabbit + SonarCloud findings". Agent's are the downstream, newer, reviewed
copies, so they come upstream rather than being overwritten by ours — SPOR
with best-version-wins, not first-writer-wins.

Chased the one candidate for the other direction: OpenApiGenerator::
buildSchemas exists only here. It is not lost — agent emits the identical
schemas block inline inside buildComponents rather than through an extracted
helper. No behaviour only this side had.

AX-8 verified before copying, not assumed: none of the eleven imports
Core\Mod\Agentic. Their only non-framework imports are Core\Tenant,
Core\Mod\Content and Laravel\Mcp, all of which this package already uses.
php-content is now declared for the first time — ContentResource has always
imported Core\Mod\Content\Models\ContentItem without it being in composer.json.

CircuitBreaker arrives with its namespace normalised from the braced
`namespace X { ... }` form, which was the only file in either repo using it.

One correction to #19, which I got wrong. That PR was titled "stop importing
the consumer" and it introduced one: running pint over the changed files let
fully_qualified_strict_types rewrite the inline
\Core\Mod\Agentic\Services\AgentToolRegistry::class in executeTool into a
top-of-file use statement. The coupling never changed — it is still a guarded,
late-bound app()->bound() lookup that autoloads nothing — but the source did,
and the claim in that PR body was wrong as written. It is now a string literal,
which expresses the late binding honestly and which no formatter will promote
into an import.

SPDX headers come across with the files: 13 of 132 files now carry one, from 2.

Suite unchanged: 21 failed, 299 passed.
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.

1 participant