Skip to content

feat(mcp): import a catalogue from the CLI, so a deploy needs no click - #863

Merged
CybotTM merged 1 commit into
mainfrom
feat/mcp-import-command
Aug 21, 2026
Merged

feat(mcp): import a catalogue from the CLI, so a deploy needs no click#863
CybotTM merged 1 commit into
mainfrom
feat/mcp-import-command

Conversation

@CybotTM

@CybotTM CybotTM commented Aug 21, 2026

Copy link
Copy Markdown
Member

Closes #836. A seeded MCP server has a record and no tools until somebody opens the module and presses Import catalogue. On an instance rebuilt by a deploy that is a manual step after every fresh install, and the server supplies nothing until it happens.

vendor/bin/typo3 nrllm:mcp:import deepwiki
vendor/bin/typo3 nrllm:mcp:import --all

It is the module's button, not a second path

Checked before writing anything: McpServerController::importAction() resolves the record and formats the report — that is all it does. Every refusal reason, the SSRF gate, the ADR-170 operation budget and the catalogue reconciliation live in McpImportService::import(). So the command holds no policy of its own, and the issue's "not a second path" is a property of the code rather than an intention.

Three decisions the issue did not settle

Identifier, not uid — a uid is not knowable to whoever writes the deploy script. This needs no new repository method: findEnabled() is filtered in the command. That matters, because identifiers are deliberately not unique in the table — soft-deleted rows keep theirs, which is exactly why the service refuses on a twin instead of relying on an index. The ambiguity is therefore reachable here too, and is reported once naming both rather than resolved by picking a row.

--all walks findEnabled(), not findUsable() — the latter filters out servers without a data class. Silently skipping one is worse than importing it and printing the service's reason, which names the missing setting.

An empty installation exits zero. A deploy runs --all unconditionally; "no enabled server yet" is a valid state, not a red pipeline. A refusing server does fail the run, and does not stop the others.

Registered explicitly, because schedulable is a decision

autoconfigure: true and the ../Classes/* resource import would have picked up #[AsCommand] on its own. The entry exists so the flag is chosen rather than defaulted: this is the only one of the four registered commands that is schedulable, because the import is idempotent and a catalogue changes on the server's schedule rather than ours.

Functional, and not by preference

The importer and both repositories are final readonly, so a faked import service is not constructible. More to the point: the acceptance criterion that matters most — a second run writes nothing — cannot be shown by a fake that never writes a row. Only the HTTP client is scripted, which is what the neighbouring McpImportServiceTest does and says why.

One trap worth naming: McpTestServer answers a queue, one entry per contact. A test importing two servers must script two, and getting that wrong reads as a refusal from the second server rather than as an exhausted fixture — which is how the --all test failed first. The queue depth is now an explicit argument at every call site.

Three controls, each observed

defect injected result
break out of the loop on the first refusal fails — the healthy server was no longer imported
return SUCCESS regardless of refusals fails — exit-code assertion
silently take the first of two twins fails — ambiguity assertion

Restored tree: 10 tests green.

An architecture rule said no, and it was right

Classes/Command/ is "core"; the MCP importer is the tool module. ModuleSeamTest rejected the dependency (ADR-090), and the fix is the established one — three commands are already named in its exception list because they are a module's operational surface living in a shared directory. This is the fourth, and where it moves in a split (nr_llm_tools) is recorded both in the test and in the command's own docblock, as that docblock demands.

Found while doing it: the docblock said "Seven classes are excluded BY NAME" and listed six — a class left the list without the number following. The count is gone rather than corrected, for the reason #793 is about.

Verification

gate result
-s functional -d sqlite (this class) 10 tests, 52 assertions, exit 0
-s unit (full) 7288 tests, 24766 assertions, exit 0
-s phpstan (carries phpat) No errors
-s cgl -n SUCCESS
composer ci:test:changelog exit 0

Assisted by claude-code:claude-opus-5 — Session

Copilot AI lite review requested due to automatic review settings August 21, 2026 06:27
@CybotTM
CybotTM requested a review from a team as a code owner August 21, 2026 06:27
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests Test-related changes configuration Configuration changes labels Aug 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

github-actions[bot]
github-actions Bot previously approved these changes Aug 21, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated approval for maintainer PR

All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 74 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.50%. Comparing base (425860f) to head (4774c87).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
Classes/Command/ImportMcpCatalogueCommand.php 0.00% 74 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #863      +/-   ##
============================================
- Coverage     86.70%   86.50%   -0.20%     
- Complexity     9926     9947      +21     
============================================
  Files           574      575       +1     
  Lines         32080    32154      +74     
============================================
  Hits          27814    27814              
- Misses         4266     4340      +74     
Flag Coverage Δ
unit 64.97% <0.00%> (-0.16%) ⬇️

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

Files with missing lines Coverage Δ
Classes/Command/ImportMcpCatalogueCommand.php 0.00% <0.00%> (ø)
🚀 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.

github-actions[bot]
github-actions Bot previously approved these changes Aug 21, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated approval for maintainer PR

All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.

@CybotTM

CybotTM commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

Review record — no Copilot review exists (monthly, account-wide quota), so the green gate is not a read of this change.

The premise I checked before writing a line. The issue says the command must be "the module's button, not a second path". That is only true if the controller adds nothing, so I read McpServerController::importAction() first: it resolves the record and formats the report, and every refusal, the SSRF gate, the ADR-170 budget and the reconciliation live in McpImportService::import(). The command therefore holds no policy — a property of the code, not an intention I wrote down.

Three decisions the issue left open, each with its reason in the body: identifier rather than uid and no new repository method; findEnabled() rather than findUsable() so a server without a data class gets the service's reason instead of silence; and exit 0 on an installation with no enabled server, because a deploy runs --all unconditionally.

Functional rather than unit, and not by preference. The importer and both repositories are final readonly, so a faked import service is not constructible — and the acceptance criterion that matters most, that a second run writes nothing, cannot be shown by a fake that never writes a row.

Three controls, each observed: break on the first refusal fails the test that the healthy server was still imported; returning SUCCESS regardless of refusals fails the exit-code test; silently taking the first of two twins fails the ambiguity test.

One trap I hit and left documented in the test. McpTestServer answers a queue, one entry per contact. A test importing two servers must script two, and getting it wrong reads as a refusal from the second server rather than as an exhausted fixture — which is how the --all test failed first. The queue depth is now an explicit argument at every call site.

Rector: this is the PR where I paid for skipping my own note. I ran cgl/phpstan/unit/functional at 8.4 and reported Rector as CI's to judge; CI failed on one NewlineBeforeNewAssignSetRector line. A composerUpdate -p 8.2 afterwards reproduced it locally in a minute and confirmed the fix, and the gate is green there now.

ModuleSeamTest names the command by class, because Classes/Command/ is core and the MCP importer is the tool module — the fourth such exception, with its split destination recorded as that test demands. Its docblock said "Seven classes are excluded BY NAME" while listing six; the count is gone rather than corrected.

Not merging on this record — I hold no merge authorisation for this PR.

Assisted by claude-code:claude-opus-5 — Session

@CybotTM

CybotTM commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

Self-review: 04ec820

The review this pull request demands is unsatisfiable (Copilot quota wall or repeated bot failures on this head). Per the documented fallback, the diff on this head was reviewed by the PR author; this comment is the on-the-record attestation the merge gate reads back. It stops matching on the next push.

@CybotTM
CybotTM added this pull request to the merge queue Aug 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Aug 21, 2026
A seeded MCP server had a record and no tools until somebody opened the
module and pressed Import catalogue. On an instance rebuilt by a deploy
that is a manual step after every fresh install, and the server supplies
nothing until it happens.

The command is the module's button, not a second path. It calls the same
McpImportService::import() and adds no policy: every refusal reason, the
SSRF gate, the ADR-170 operation budget and the catalogue reconciliation
stay where they were. Verified before writing it — the controller does
nothing but resolve the record and format the report.

Servers are named by identifier, not uid: a uid is not knowable to
whoever writes the deploy script. That needs no repository method.
Identifiers are deliberately NOT unique in the table — soft-deleted rows
keep theirs, which is why the service refuses on a twin rather than
relying on an index — so the ambiguity is reachable here too and is
reported once, naming both, instead of resolved by picking a row.

--all walks findEnabled() rather than findUsable(): a server without a
data class should get the service's reason, not be skipped in silence. A
refusing server does not stop the others and does fail the run, and an
installation with no enabled server exits zero, because a deploy runs
--all unconditionally and a valid empty state must not go red.

Registered explicitly although autoconfigure would have picked up the
attribute, because schedulable is a decision: this is the only one of the
four that is schedulable. The import is idempotent, and a catalogue
changes on the server's schedule rather than ours.

Functional rather than unit, and not by preference: the importer and both
repositories are `final readonly`, so a faked import service is not
constructible — and the acceptance criterion that matters most, that a
second run writes nothing, cannot be shown by a fake that never writes a
row. Only the HTTP client is scripted, which is what the neighbouring
McpImportServiceTest does and says why.

Three controls, each observed: breaking out of the loop on the first
refusal fails the test that the healthy server was still imported;
returning SUCCESS regardless of refusals fails the exit-code test; and
silently taking the first of two twins fails the ambiguity test.

ModuleSeamTest names the command, because Classes/Command/ is core and
the MCP importer is the tool module: in a package split it moves to
nr_llm_tools with the code it drives (ADR-090). Its docblock said "Seven
classes are excluded BY NAME" while listing six — one left without the
number following — so the count is gone rather than corrected.

Closes #836

Assisted-by: claude-code:claude-opus-5
Agent-Session: https://claude.ai/code/session_01MNg1MysJVugv1xo2husknU
Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
@sonarqubecloud

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated approval for maintainer PR

All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.

@CybotTM

CybotTM commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

Self-review: 4774c87

The review this pull request demands is unsatisfiable (Copilot quota wall or repeated bot failures on this head). Per the documented fallback, the diff on this head was reviewed by the PR author; this comment is the on-the-record attestation the merge gate reads back. It stops matching on the next push.

@CybotTM
CybotTM added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit 3ec0592 Aug 21, 2026
93 checks passed
@CybotTM
CybotTM deleted the feat/mcp-import-command branch August 21, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configuration Configuration changes documentation Improvements or additions to documentation tests Test-related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nrllm:mcp:import — a CLI entry point for McpImportService, so a seeded server gets its catalogue on deploy

2 participants