Skip to content

refactor(mcp): remove the MCP surface and clean up after it on upgrade (#1422) - #1425

Merged
github-actions[bot] merged 17 commits into
mainfrom
refactor/issue-1422-remove-mcp-surface
Aug 9, 2026
Merged

refactor(mcp): remove the MCP surface and clean up after it on upgrade (#1422)#1425
github-actions[bot] merged 17 commits into
mainfrom
refactor/issue-1422-remove-mcp-surface

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Closes #1422.

Removes the MCP surface and cleans up after it on upgrade. Operator-ratified
2026-08-06, twice: the removal itself, then the amended shape of the cleanup once
discovery showed the auto-edit half rested on paths this repo has never named.

Why remove rather than repair

serve() failed on every version across its declared range
fastmcp>=0.2.0, each differently:

fastmcp result
1.0 TypeError: FastMCP.tool() got an unexpected keyword argument 'annotations'
2.0.0 same
2.10.0 TypeError: cannot specify both default and default_factory
3.2.4 NameError: name 'Field' is not defined

Not an upstream regression — there is no version on which it starts. The 3.x
failure has the clean mechanism: from pydantic import Field is a local of
serve(), while from __future__ import annotations makes fastmcp resolve tool
annotations against module globals, which never contained it.

Broken at d6bafcb9 (2026-05-08), shipped in v2.0.1 and every release since.
Five commits touched the module afterwards, one adding a feature by mirroring a
lock window onto it. ci.yml installs --extra archive only and no workflow ever
installed mcp, so the two tests reaching the code assert the fastmcp-absent
branch and read the ambient environment — green in CI precisely because the
feature was not installed. test_mcp_wrapper_layer.py did exercise registration,
but through a hand-written shim that never resolves annotations, which is the step
that fails.

Beyond the breakage: tool invocation is at the model's discretion, so MCP cannot
put the right beliefs in the prompt before the model reads the message. That is
the guarantee aelfrice sells. Same reasoning as #605, and borne out by the Codex
host — built on hooks plus a skills bundle, no MCP reference at all.

What this is not

Not a store migration. mcp_remember stays a first-class source_kind, with
its frozenset membership intact. Existing stores hold rows carrying it, and a
reader that rejects a value it wrote last release makes the store unopenable —
#1161's class, unrecoverable in the field. Production stops writing it; readers
keep accepting it. tests/test_mcp_remember_reader_contract_1422.py pins that
(mutation-checked: dropping it from either frozenset, or renaming the wire string,
each turn it red), and the replay-soak corpus covers it end to end.

Not a aelf confirm change. It reached apply_feedback through the MCP
module; it now calls it directly. respect_lock=False — the #1168 lock-floor
exemption — is carried across and commented as load-bearing. It had no test:
every existing confirm test used unlocked beliefs, where the exemption is
invisible. The first commit adds one, before the call site moves.

The seven ported test files

Not a mechanical import swap — mcp_server held real logic beside the plumbing,
so each file was triaged assertion by assertion.

Behaviour ported rather than dropped:

  • test_ingest_log — the post-[v2.x] Derivation worker — beliefs become materialized state #264 re-lock contract (a second lock appends a
    log row and corroborates rather than duplicating) was asserted only through
    tool_lock; the neighbouring CLI test stops at a single lock. Now CLI-driven.
  • test_lock_management — the [v2.0 / Track E] unlock / promote / demote lock-management surface (CLI + MCP) #391 demote lock-drop audit row, and the
    lock → unlock → re-lock round trip. Both drive aelf lock / aelf demote
    rather than seeding a locked row, because what they exercise is the ingest path
    that makes "re-lock" mean corroborate.
  • test_session_id_population_rate — the four tool_lock calls are
    replaced, not deleted: deleting them drops the measured rate to 75% against
    its own >= 0.80 gate. Re-measured at 10/12 = 83.33%, composition still 12.

Wire-format assertions removed only after confirming an equivalent exists
(tool_stats phantom payloads → core + CLI tests; to_scope shapes →
test_to_scope's CLI block, not-found → test_cli.py:191; tool_confirm
return dicts → test_cli_confirm).

Part 2 — the upgrade cleans up after itself

A one-shot pass behind ~/.aelfrice/mcp-surface-removed, fired from
_cmd_setup_locked beside the #733 and #1064 migrations.

It reports; it does not edit. git grep mcpServers on main returns two hits,
both lines inside the doc that told users to paste one — no path constant for
those files exists in the tree, and the file usually holds the user's other
servers. lifecycle.py:791-797 already states the rule: anything not named in the
dotdir contract is reported and never deleted. aelf migrate --remove-mcp-config
is the opt-in verb, behind a timestamped backup.

It advises the reinstall. maybe_migrate_to_uv may shell out precisely
because it never runs on a uv-tool install; this targets the opposite population,
so the extra is detected by reading uv's own receipt with stdlib tomllib and the
command is printed rather than run.

The predicate covers all four shapes ever publishedaelf mcp,
uv run --project <abs> aelf mcp, aelf serve (docs/INSTALL.md @99160871) and
python -m aelfrice.mcp_server — plus the documented-but-never-shipped
aelf-mcp. Never matches on the map key (users rename it) or on aelf alone.

Sentinel registered in both conftest lists. _PRECREATED_SENTINELS is the one
that matters: an exists()-guarded sentinel pinned at a fresh tmp path arms the
guarded side effect instead of disarming it, and CI cannot catch that because a
runner's HOME is empty. Also added to _DOTDIR_INSTALL_STATE and the uninstall
agreement test.

Known reach limit

Both existing one-shot migrations fire only from _cmd_setup_locked. A user who
runs uv tool upgrade aelfrice and never runs aelf setup will not see this.
Firing from auto_install_at_cli_entry is the #1332 re-arm channel and is out of
bounds, so the CHANGELOG says when the cleanup runs rather than claiming the
upgrade cleans up unconditionally.

Deliberately untouched

docs/audits/* and docs/design/* are dated records; ROADMAP's shipped-version
rows and PHILOSOPHY's "15 MCP tools at v3.3" are release history; store.py's
#1161 narrative describes what used to break. Rewriting any would falsify the
record. slash_commands/onboard.md is left byte-identical — its MCP mention is
not a claim that we ship one, and rewriting the line would re-add a token the
discretion gate blocks on additions.

Verification

  • 10 commits, all signed, FF on github/main
  • uv run --extra archive pytest -q -p no:randomly: 7316 passed, 70 skipped,
    71 xfailed, 0 failed
  • uv sync --frozen --group dev --extra archive (CI's exact invocation) succeeds
    against the regenerated lockfile
  • discretion grep on added lines: clean
  • Mutation-checked at every gate: the lock-floor exemption (1), the
    mcp_remember reader contract (3), and the cleanup's safety properties (5 —
    widened predicate, key-only match, skipped backup, auto-edit, swallowed parse
    errors). All killed.
  • 66 files, +1210 / −5032

Summary by Sourcery

Remove the broken MCP server surface and associated CLI entrypoint while preserving backwards compatibility of stored data, and add an opt-in, one-shot cleanup path for leftover MCP configuration and extras.

Enhancements:

  • Route aelf confirm directly through apply_feedback while keeping the lock-floor exemption and tightening tests around confirm behavior, including for locked beliefs.
  • Rebase lock-management, ingest-log, and session-id tests off the MCP tool layer onto the CLI and core library so behavior is exercised through supported entry points.
  • Introduce a host-scoped MCP cleanup module and sentinel that detects lingering MCP tool registrations and [mcp] uv extras, reporting them on aelf setup and allowing optional removal via aelf migrate --remove-mcp-config.
  • Extend lifecycle and uninstall metadata to account for the new MCP cleanup sentinel and keep dotdir/install state accounting aligned.
  • Clarify documentation, templates, and comments across the repo to remove MCP as a supported interface and to describe hooks, slash commands, and dependency extras as the remaining integration surfaces.

Build:

  • Drop the [mcp] optional dependency and its fastmcp/pydantic requirements from pyproject.toml, and update the lockfile accordingly.

Documentation:

  • Update user and conceptual docs (README, INSTALL, COMMANDS, ARCHITECTURE, PHILOSOPHY, PRIVACY, LIMITATIONS, ROADMAP, HARNESS_INTEGRATION, SLASH_COMMANDS, CONTRIBUTING, SECURITY, docs README, workflows, issue templates, v4 changelog) to reflect the removal of the MCP server, its extra, and related tooling, while clarifying remaining supported surfaces and guarantees.

Tests:

  • Replace MCP-based tests with CLI- and library-level tests for lock management, ingest logging, session-id propagation, confirm posterior behavior (including for locked beliefs), phantom lifecycle counts, and mcp_remember reader compatibility, plus new coverage for the MCP cleanup logic and its safety properties.
  • Update conftest and uninstall tests to register and assert the new MCP cleanup sentinel and ensure dotdir contents are consistent.

Summary by CodeRabbit

  • Removed

    • Removed the MCP server interface, installation option, and dedicated documentation.
    • MCP is no longer listed as a supported product surface.
  • New Features

    • Added setup checks for leftover MCP registrations.
    • Added an opt-in migration command that removes detected registrations after creating backups.
    • aelf confirm now updates locked beliefs while preserving their locks and supports optional notes.
  • Documentation

    • Updated guides and issue templates to focus on CLI and slash-command workflows.
    • Documented MCP removal while preserving compatibility with existing stored mcp_remember data.

@robotrocketscience robotrocketscience added the author-Kulili PR coordination mutex label Aug 6, 2026

@sourcery-ai sourcery-ai 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.

Sorry @robotrocketscience, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 55 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a983c617-3865-47d3-8d0b-79e359b067b2

📥 Commits

Reviewing files that changed from the base of the PR and between 1325adf and b962eeb.

📒 Files selected for processing (10)
  • .github/ISSUE_TEMPLATE/question.yml
  • CHANGELOG/v4.md
  • src/aelfrice/cli.py
  • src/aelfrice/federation.py
  • src/aelfrice/mcp_cleanup.py
  • src/aelfrice/retrieval.py
  • src/aelfrice/store.py
  • src/aelfrice/wonder/dispatch.py
  • tests/test_mcp_cleanup_1422.py
  • tests/test_mcp_remember_reader_contract_1422.py
📝 Walkthrough

Walkthrough

The PR removes the MCP server, dependency extra, CLI command, documentation, and MCP-specific tests. It adds report-only stale-registration detection and explicit configuration removal with backups. CLI behavior remains available, and historical mcp_remember records remain readable.

Changes

MCP removal and migration

Layer / File(s) Summary
Remove MCP runtime and CLI surface
src/aelfrice/mcp_server.py, src/aelfrice/cli.py, pyproject.toml
The MCP server, CLI command, parser entry, and optional dependency group were removed.
Detect and remove stale MCP configuration
src/aelfrice/mcp_cleanup.py, src/aelfrice/cli.py, tests/test_mcp_cleanup_1422.py
Setup reports stale installations and registrations. aelf migrate --remove-mcp-config removes matching entries after creating backups.
Retain CLI behavior and migrate tests
src/aelfrice/cli.py, tests/test_cli_confirm.py, tests/test_lock_management.py, tests/test_ingest_log.py, tests/test_session_id_*.py
Confirmation applies feedback directly. Lock, demote, session, and scope coverage now uses CLI or library paths.
Preserve historical mcp_remember reads
tests/test_mcp_remember_reader_contract_1422.py
Tests verify that persisted mcp_remember values remain recognized, derivable, storable, and readable.
Update current documentation and support contracts
.github/*, README.md, docs/*, CONTRIBUTING.md, SECURITY.md, CHANGELOG/v4.md, CITATION.cff
Current MCP support references, installation instructions, issue guidance, and documentation links were removed or replaced with CLI and slash-command references.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SetupCLI
  participant McpCleanup
  participant HostConfig
  User->>SetupCLI: run aelf setup
  SetupCLI->>McpCleanup: detect stale MCP state
  McpCleanup->>HostConfig: scan known configuration files
  HostConfig-->>McpCleanup: matching registrations and notes
  McpCleanup-->>SetupCLI: report cleanup state
  User->>SetupCLI: run aelf migrate --remove-mcp-config
  SetupCLI->>McpCleanup: remove matching registrations
  McpCleanup->>HostConfig: create backup and update JSON
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Most requirements are covered, but AC9 is unmet because the migration reports the stale MCP extra and prints a reinstall command instead of refreshing the owned installation. Make the setup migration refresh the owned aelfrice installation without the [mcp] extra, then verify the excluded uv.lock and frozen-lockfile result.
Out of Scope Changes check ⚠️ Warning The BM25 cache comment change is unrelated to MCP removal, confirm routing, compatibility, cleanup, tests, or related documentation. Remove or justify the unrelated comment change in src/aelfrice/bm25.py and keep the PR limited to its linked objectives.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the primary change: removing the MCP surface and cleaning up remnants during upgrade.
Description check ✅ Passed The description sufficiently covers the change, linked issue, verification, test coverage, migration limits, and reviewer notes.
Docstring Coverage ✅ Passed Docstring coverage is 85.23% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/issue-1422-remove-mcp-surface

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.

@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label Aug 6, 2026
@sourcery-ai

sourcery-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Reviewer's Guide

Removes the broken FastMCP-based MCP server surface (code, CLI verb, tests, docs, optional extra) and replaces it with a conservative, one-shot upgrade-time cleanup plus an opt-in migration command, while porting all meaningful MCP-only behaviour and invariants to CLI/library paths (notably confirm, lock/demote/remember semantics and mcp_remember store compatibility) and wiring a host-scoped sentinel so the cleanup runs once per machine from aelf setup.

Sequence diagram for the updated confirm CLI path using apply_feedback

sequenceDiagram
    actor User
    participant CLI as cli._cmd_confirm
    participant Store as MemoryStore
    participant Feedback as feedback.apply_feedback

    User->>CLI: aelf confirm <belief_id> [--source S] [--note TEXT]
    CLI->>Store: _open_store()
    activate Store
    CLI->>Feedback: apply_feedback(store, belief_id, valence=1.0, source, respect_lock=False)
    activate Feedback
    Feedback->>Store: assert_local_ownership(belief_id)
    Store-->>Feedback: (may raise ForeignBeliefError)
    Feedback-->>CLI: FeedbackResult(prior_alpha, new_alpha, new_beta)
    deactivate Feedback

    alt ValueError or ForeignBeliefError
        CLI-->>User: stderr "confirm error: <message>" (exit 1)
    else Success
        CLI-->>User: stdout "confirmed <id>: alpha a->b, mean m [note]"
        CLI->>CLI: _feed_log_event("feedback.applied", ...)
    end

    CLI->>Store: close()
    deactivate Store
Loading

File-Level Changes

Change Details Files
Remove the MCP server surface and its dependency while keeping stores and core APIs compatible.
  • Delete aelfrice.mcp_server and all MCP-specific tests (test_cli_mcp.py, test_mcp_* files) and wiring that referenced it, including the aelf mcp CLI subcommand implementation and parser registration.
  • Drop the [mcp] optional dependency (fastmcp + pydantic) from pyproject.toml and remove all runtime mentions of the MCP server from code comments and docs, keeping only historical references where they document shipped behaviour.
  • Update modules that mentioned MCP surfaces (e.g. retrieval.py, store.py, bm25.py, derivation.py, detector_thresholds.py, classification.py, db_paths.py, federation.py, hook.py, lock_expiry.py, models.py, scoring.py, setup.py, wonder/*) so references are either removed or restated in terms of CLI and slash-command usage without changing functional behaviour.
src/aelfrice/mcp_server.py
src/aelfrice/cli.py
pyproject.toml
src/aelfrice/retrieval.py
src/aelfrice/store.py
src/aelfrice/bm25.py
src/aelfrice/derivation.py
src/aelfrice/detector_thresholds.py
src/aelfrice/classification.py
src/aelfrice/db_paths.py
src/aelfrice/federation.py
src/aelfrice/hook.py
src/aelfrice/lock_expiry.py
src/aelfrice/models.py
src/aelfrice/scoring.py
src/aelfrice/setup.py
src/aelfrice/wonder/__init__.py
src/aelfrice/wonder/dispatch.py
src/aelfrice/wonder/result.py
tests/test_cli_mcp.py
tests/test_mcp_lock_via_worker.py
tests/test_mcp_server.py
tests/test_mcp_wrapper_layer.py
Introduce a safe, one-shot MCP cleanup mechanism invoked from setup and an opt-in CLI migration to remove stale MCP host config entries.
  • Add aelfrice.mcp_cleanup with functions to detect whether the [mcp] extra is installed via uv receipts, scan host MCP config files (mcpServers maps) for aelfrice entries, and report registrations without editing by default, guarded by a host-scoped sentinel ~/.aelfrice/mcp-surface-removed.
  • Expose an opt-in CLI path aelf migrate --remove-mcp-config that calls mcp_cleanup.remove_registration to delete aelfrice mcpServers entries, writing timestamped backups and normalising JSON formatting, and return non-zero if any removal failed.
  • Invoke maybe_clean_up_mcp() from _cmd_setup_locked in cli.py, printing any notes when the cleanup runs and swallowing all exceptions so setup cannot fail due to cleanup; register the sentinel in lifecycle dotdir metadata and uninstall tests.
  • Add focused tests for the cleanup logic (test_mcp_cleanup_1422.py), covering recognition predicates across all documented command shapes, behaviour on malformed/odd configs, backup semantics, idempotency, sentinel behaviour, and uv receipt parsing without invoking uv.
src/aelfrice/mcp_cleanup.py
src/aelfrice/cli.py
src/aelfrice/lifecycle.py
tests/conftest.py
tests/test_uninstall_dotdir.py
tests/test_mcp_cleanup_1422.py
Port meaningful MCP tool behaviour and invariants to CLI and library-level tests, especially for confirm, lock/demote workflows, session IDs, ingest log contracts, and scope promotion/demotion.
  • Refactor confirm so the CLI _cmd_confirm calls feedback.apply_feedback directly with respect_lock=False, handles ValueError/ForeignBeliefError as exit-1, and prints note text from the original args instead of the old dict payload; add CLI tests to pin that confirm still moves locked beliefs' posterior while feedback honours the lock floor.
  • Replace MCP-based confirm tests in test_confirm_posterior_shift.py with a local _confirm helper that mirrors _cmd_confirm's apply_feedback call (including respect_lock=False) and assert posterior math, source propagation and audit row behaviour using the typed result instead of dict payloads.
  • Rewrite MCP-centric lock/demote/unlock tests in test_lock_management.py, test_ingest_log.py, and the session ID tests to drive behaviour through the CLI (aelf lock, aelf demote, _cmd_lock) on file-backed stores, verifying audit rows, lock level transitions, log rows, and session id stamping without relying on MCP tools.
  • Remove MCP-specific scope tests in test_to_scope.py but keep and rely on the CLI promote/demote scope-to-scope coverage, ensuring scope flipping and audit behaviour remain tested.
  • Adjust other tests that asserted MCP payload shapes (phantom lifecycle stats, session-id propagation, slash-command coverage, bug-report templates, llm classifier import guarantees) to either drop MCP-specific assertions or restate them in terms of CLI/library APIs or metadata only.
src/aelfrice/cli.py
src/aelfrice/feedback.py
tests/test_cli_confirm.py
tests/test_confirm_posterior_shift.py
tests/test_lock_management.py
tests/test_ingest_log.py
tests/test_session_id_population_rate.py
tests/test_session_id_propagation.py
tests/test_to_scope.py
tests/test_phantom_lifecycle_counts.py
tests/test_slash_commands.py
tests/test_llm_classifier.py
.github/ISSUE_TEMPLATE/bug_report.yml
.github/ISSUE_TEMPLATE/question.yml
Ensure legacy mcp_remember data remains fully readable and behaves consistently even though no new rows are written with that source kind.
  • Keep INGEST_SOURCE_MCP_REMEMBER, CORROBORATION_SOURCE_MCP_REMEMBER, and their membership in INGEST_SOURCE_KINDS, CORROBORATION_SOURCE_TYPES, and CORROBORATION_SOURCES_USER_EXPLICIT, so existing stores with mcp_remember rows remain openable and their semantics unchanged.
  • Verify via derive(DerivationInput(..., source_kind=INGEST_SOURCE_MCP_REMEMBER)) that legacy mcp_remember rows still derive to user-locked, user-stated beliefs with the lock prior, and that such beliefs can be inserted and read from MemoryStore without error.
  • Add explicit regression tests in test_mcp_remember_reader_contract_1422.py to guard the above behaviours and to ensure the on-disk wire string 'mcp_remember' is not renamed, preventing future refactors from treating these constants as dead code and breaking compatibility.
  • Reference the replay-soak corpus path that exercises mcp_remember end-to-end so forward-compat test coverage is clear.
src/aelfrice/models.py
src/aelfrice/derivation.py
tests/test_mcp_remember_reader_contract_1422.py
tests/test_derivation.py
tests/corpus/replay_soak/v0.1/mcp_remember_v0_1.jsonl
Clean up documentation and ancillary metadata to remove current-tense MCP claims while preserving historical record and updating changelog.
  • Update README, user docs (COMMANDS, INSTALL, CONFIG, PRIVACY, LIMITATIONS, SLASH_COMMANDS, SECURITY, ARCHITECTURE, ROADMAP, HARNESS_INTEGRATION, PHILOSOPHY, user README) and docs/README to remove or rephrase MCP references so the only supported surfaces are CLI and slash commands, while keeping historical mentions in design/audit docs where they describe past behaviour.
  • Delete docs/user/MCP.md and adjust references in other docs and GitHub issue templates (bug_report, question, post-release-docs-issue) to no longer point to MCP or count MCP surfaces.
  • Extend CHANGELOG/v4.md with a detailed entry under "Removed" explaining why the MCP server was removed, what was affected (module, CLI verb, extra, docs), the non-impact on confirm/store data, and the upgrade-time cleanup behaviour.
  • Make minor comment changes in various modules to align with the new surface (e.g. hooks instead of MCP, CLI-only routes) without changing code behaviour.
README.md
docs/README.md
docs/user/README.md
docs/user/INSTALL.md
docs/user/COMMANDS.md
docs/user/CONFIG.md
docs/user/PRIVACY.md
docs/user/LIMITATIONS.md
docs/user/SLASH_COMMANDS.md
docs/user/SECURITY.md
docs/user/INSTALL.md
docs/concepts/ARCHITECTURE.md
docs/concepts/HARNESS_INTEGRATION.md
docs/concepts/PHILOSOPHY.md
docs/concepts/ROADMAP.md
docs/user/MCP.md
CHANGELOG/v4.md
.github/ISSUE_TEMPLATE/bug_report.yml
.github/ISSUE_TEMPLATE/question.yml
.github/workflows/post-release-docs-issue.yml

Assessment against linked issues

Issue Objective Addressed Explanation
#1422 Remove the MCP surface (code, CLI, tests, docs) while preserving store compatibility for mcp_remember and relocating any non-MCP logic such as tool_confirm so functionality like aelf confirm continues to work.
#1422 Add an upgrade-time MCP cleanup mechanism that runs once per host, detects old MCP registrations and the [mcp] extra, reports them from aelf setup, and provides an opt-in CLI flow to safely remove only aelfrice’s own MCP entries from host config files with backups, without re-arming or mutating unrelated config.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Setr:2026-08-06T22:15:33Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Review: the removal is sound; the new cleanup path had six machine-reachable defects

The deletion half holds up — I ran the full suite and traced the deleted surface for surviving references, and the mcp_remember reader contract is genuinely pinned rather than merely appearing to be. What needed work is mcp_cleanup.py, which is the one file here that runs on users' machines and edits their config. All six are fixed and pushed (1325adf1), each mutated red-then-green.

1. Locally-scoped registrations were invisible — and the sentinel latched anyway (major)

_scan_file read only the top-level mcpServers map. A local-scope server is stored under projects.<dir>.mcpServers in the same file, so the pass reported "nothing to clean up" on that shape, printed no notes, and still wrote ~/.aelfrice/mcp-surface-removed — so it never looks again. The user keeps a server that fails to start and is never told.

This is not hypothetical. Checked structurally against a real host config: the top-level mcpServers map was empty while two projects.* entries carried their own non-empty ones. On that machine the pass finds nothing, reports nothing, and latches — permanently.

Registration now carries the project it lives under, both scopes are scanned in one pass, and remove_registration resolves the right container. Both halves matter: finding a nested entry without carrying its container would find it and then be unable to remove it. There is a test asserting both scopes are scanned, so a fix that merely switched which map is read fails too.

2. The backup named as the undo path was clobbered (major)

The stamp is second-resolution. Two registrations removed from one file in one aelf migrate --remove-mcp-config run resolve to the same backup filename, and the second write puts already-edited content there. The pre-edit original is then gone from disk while both printed messages still cite that path as the undo — AC11 is false for every removal but the last. Now takes the first free name.

Verified on a three-entry config: two backups are written and one holds the untouched original.

3–6 (minor, all fixed)

  • candidate_config_paths() had no dedup, returning ~/.mcp.json and cwd/.mcp.json. With cwd == HOME that is one file scanned twice — duplicate notes, and --remove-mcp-config exits 1 on a removal that actually succeeded (second pass sees "already gone").
  • The sentinel latched on an incomplete scan. A pass that could not read one of its inputs has not established there is nothing to clean up, so latching suppresses the one-shot report on exactly the machines that still need it. The module docstring already promised the opposite ("written only when the pass ran to completion"). Now re-arms.
  • _basename did not strip .exe/.cmd/.bat, so a Windows aelf.exe registration was unrecognised — and with the map key named aelfrice, the routine printed that aelfrice did not publish that command, about a command aelfrice did publish. The verb is still required, so the strip does not widen the match. (Same family as windows/codex: .exe hook launchers bypass ownership matching and duplicate every hook group #1412.)
  • mcp_extra_is_installed() read only the extras list, missing uv tool install --with fastmcp aelfrice, which uv records as a sibling requirement. The CHANGELOG promises that population gets told; it did not.
mutation result
scan only the top-level map 2 RED
single-name backup 1 RED
no path dedup 1 RED
latch the sentinel unconditionally 1 RED
no .exe strip 1 RED
drop the fastmcp requirement check 1 RED

tests/test_mcp_cleanup_1422.py: 40 passed. Full suite: 7314 passed, 70 skipped, 71 xfailed.

A note on the shape of these

Five of the six are failures of reporting rather than of deletion — the routine is appropriately conservative about editing, and nothing here destroyed data it was not asked to. But four of them end in the same place: the user is told nothing, or told something false, and the sentinel makes it permanent. For a one-shot migration the latch is the amplifier, which is why I gated it on a complete scan rather than only fixing the individual blind spots.

CI

Actions is still recovering from the outage, so the required checks have not run on this head. The suite figure above is local, run exactly as ci.yml invokes it (--extra archive). Branch is FF on main, all commits signed. Not labelling ready-to-merge.

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/post-release-docs-issue.yml (1)

18-18: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pin actions/checkout to a commit SHA.

actions/checkout@v4 is a mutable reference. Replace it with a reviewed commit SHA and retain the version in a trailing comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/post-release-docs-issue.yml at line 18, Update the
actions/checkout step to reference a reviewed immutable commit SHA instead of
the mutable v4 tag, and retain the checkout version in a trailing comment.

Source: Path instructions

docs/user/COMMANDS.md (1)

72-72: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the new MCP cleanup option.

The current migrate entry still lists only --from, --apply, and --all. Add --remove-mcp-config and document its stale-registration report, timestamped backups, and opt-in removal behavior.

Proposed documentation update
-| `migrate [--from P] [--apply] [--all]` | Port beliefs from the legacy global DB into the active project's per-project DB. Dry-run by default. Read-only on the source. |
+| `migrate [--from P] [--apply] [--all] [--remove-mcp-config]` | Port beliefs from the legacy global DB into the active project's per-project DB. `--remove-mcp-config` reports stale MCP registrations and extras, then removes them only after timestamped backups. No configuration is edited without this flag. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/user/COMMANDS.md` at line 72, Update the migrate command documentation
to include the new --remove-mcp-config option alongside --from, --apply, and
--all. Describe that it reports stale MCP registrations, creates timestamped
backups, and removes them only when explicitly enabled.
🧹 Nitpick comments (5)
tests/test_lock_management.py (2)

424-424: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the single-element slice with next(iter(...)).

Ruff reports RUF015 on both lines. list(...)[0] materialises the whole result to read one element.

♻️ Proposed refactor
-        bid = list(s.list_locked_beliefs())[0].id
+        bid = next(iter(s.list_locked_beliefs())).id

Also applies to: 440-440

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_lock_management.py` at line 424, In the affected lock-management
tests, replace each list_locked_beliefs() result materialized with list(...)[0]
by retrieving the first element via next(iter(...)). Update both occurrences
while preserving the existing .id access and test behavior.

Source: Linters/SAST tools


217-230: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Three new CLI helpers hand-roll the same AELFRICE_DB save/set/restore block. pytest's monkeypatch.setenv performs the same swap and restores it during teardown, including on an unexpected exception path. The shared root cause is one duplicated env-swap idiom across the migrated CLI tests.

  • tests/test_lock_management.py#L217-L230: accept a monkeypatch argument in _demote_via_cli and call monkeypatch.setenv("AELFRICE_DB", str(db)), then drop the manual save/restore.
  • tests/test_lock_management.py#L368-L382: apply the same change to _lock_via_cli.
  • tests/test_ingest_log.py#L438-L471: replace the inline os.environ block in test_relock_appends_a_log_row_and_corroborates with monkeypatch.setenv, and extract the shared helper if the same pattern is needed again.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_lock_management.py` around lines 217 - 230, Replace the duplicated
manual AELFRICE_DB environment save/set/restore logic with pytest monkeypatch
handling: update _demote_via_cli and _lock_via_cli to accept monkeypatch and
call setenv, update tests/test_lock_management.py:217-230 and
tests/test_lock_management.py:368-382 accordingly, and replace the inline
environment block in tests/test_ingest_log.py:438-471 within
test_relock_appends_a_log_row_and_corroborates with monkeypatch.setenv; extract
a shared helper there only if the pattern is reused.
src/aelfrice/mcp_cleanup.py (2)

411-417: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider an atomic write for the edited config.

path.write_text truncates before it writes. If the process stops mid-write, the host config is left truncated. The backup makes the data recoverable, but the user must restore it by hand. A write to a temporary file in the same directory followed by os.replace removes that window.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aelfrice/mcp_cleanup.py` around lines 411 - 417, Replace the direct
path.write_text call in the configuration write flow with an atomic
same-directory temporary-file write, then commit it using os.replace only after
the complete JSON content is written successfully. Preserve UTF-8 encoding,
formatting, the existing backup behavior, and the current OSError return
message.

162-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use itertools.pairwise for the adjacent-pair scan.

Ruff reports B905 (missing strict=) and RUF007 on this zip call. itertools.pairwise states the intent directly and removes both warnings.

♻️ Proposed refactor
-    for first, second in zip(arg_list, arg_list[1:]):
+    for first, second in pairwise(arg_list):
         if first in _AELF_COMMANDS and second in _AELF_SUBCOMMANDS:
             return True

Add the import near the other stdlib imports:

 from dataclasses import dataclass, field, replace
 from datetime import datetime, timezone
+from itertools import pairwise
 from pathlib import Path
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aelfrice/mcp_cleanup.py` around lines 162 - 167, Replace the zip-based
adjacent-pair scan in the command-detection function with itertools.pairwise,
adding the required stdlib import alongside the existing imports. Preserve the
current checks against _AELF_COMMANDS and _AELF_SUBCOMMANDS and the existing
boolean return behavior.

Source: Linters/SAST tools

src/aelfrice/cli.py (1)

6164-6165: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

--remove-mcp-config silently ignores the other migrate flags.

The early return runs before any validation. aelf migrate --remove-mcp-config --apply --from X performs only the MCP removal and never reports that --apply and --from were ignored. Consider rejecting the combination, or state the exclusivity in the help text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aelfrice/cli.py` around lines 6164 - 6165, Update the migrate argument
handling around remove_mcp_config so --remove-mcp-config cannot silently ignore
other migration flags such as --apply or --from. Validate the combination before
the early return in the _remove_mcp_config path and report an invalid
combination, or explicitly document and enforce that this option is exclusive.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/ISSUE_TEMPLATE/question.yml:
- Line 13: Update the COMMANDS link in the question template to target
docs/user/COMMANDS.md instead of docs/COMMANDS.md, preserving the existing
repository URL and link text.

In `@src/aelfrice/cli.py`:
- Around line 6141-6152: Update _remove_mcp_config around find_registrations to
detect the scan-incomplete marker and return a non-zero status when any
configuration was unreadable or invalid, rather than reporting success for no
registrations. Expose the existing _SCAN_INCOMPLETE marker from mcp_cleanup as
SCAN_INCOMPLETE (or provide a public alias), and use that symbol to distinguish
incomplete scans from a clean empty result.

In `@src/aelfrice/federation.py`:
- Line 55: Update the documentation text near the federation exception handling
to say that CLI error messages surface belief_id and owning_scope, replacing the
inaccurate reference to CLI exit codes; leave the surrounding exception behavior
unchanged.

In `@src/aelfrice/mcp_cleanup.py`:
- Around line 418-421: Update the success message returned by the removal flow
to use Registration.location() instead of hardcoding
mcpServers.<registration.key>, while preserving the existing path, backup, and
formatting details.
- Around line 319-326: Update maybe_clean_up_mcp when extracting requirements
from receipt so a non-table tool value is treated as an invalid receipt and
returns False instead of calling .get on it. Validate the tool value before
accessing requirements, while preserving the existing list-type validation and
valid receipt behavior.

In `@src/aelfrice/wonder/dispatch.py`:
- Line 13: Update the docstrings in src/aelfrice/wonder/dispatch.py at lines
13-13, 112-112, and 432-432: document both supported forms, “aelf wonder QUERY”
and “aelf wonder --axes QUERY”; state at line 112 that both forms return the
payload; and at line 432 correct the invocation order and explicitly state that
the CLI serializes and returns the payload.

In `@tests/test_mcp_remember_reader_contract_1422.py`:
- Around line 78-100: Revise
test_a_store_holding_an_mcp_remember_row_still_opens_and_reads to exercise a
legacy persisted record rather than inserting a current row into
MemoryStore(":memory:"). Seed a file-backed SQLite database or use the existing
legacy fixture with the historical source_kind value, close it, reopen it
through MemoryStore, and assert the record remains readable. Move the current
derive and insertion assertions into a separate test.

---

Outside diff comments:
In @.github/workflows/post-release-docs-issue.yml:
- Line 18: Update the actions/checkout step to reference a reviewed immutable
commit SHA instead of the mutable v4 tag, and retain the checkout version in a
trailing comment.

In `@docs/user/COMMANDS.md`:
- Line 72: Update the migrate command documentation to include the new
--remove-mcp-config option alongside --from, --apply, and --all. Describe that
it reports stale MCP registrations, creates timestamped backups, and removes
them only when explicitly enabled.

---

Nitpick comments:
In `@src/aelfrice/cli.py`:
- Around line 6164-6165: Update the migrate argument handling around
remove_mcp_config so --remove-mcp-config cannot silently ignore other migration
flags such as --apply or --from. Validate the combination before the early
return in the _remove_mcp_config path and report an invalid combination, or
explicitly document and enforce that this option is exclusive.

In `@src/aelfrice/mcp_cleanup.py`:
- Around line 411-417: Replace the direct path.write_text call in the
configuration write flow with an atomic same-directory temporary-file write,
then commit it using os.replace only after the complete JSON content is written
successfully. Preserve UTF-8 encoding, formatting, the existing backup behavior,
and the current OSError return message.
- Around line 162-167: Replace the zip-based adjacent-pair scan in the
command-detection function with itertools.pairwise, adding the required stdlib
import alongside the existing imports. Preserve the current checks against
_AELF_COMMANDS and _AELF_SUBCOMMANDS and the existing boolean return behavior.

In `@tests/test_lock_management.py`:
- Line 424: In the affected lock-management tests, replace each
list_locked_beliefs() result materialized with list(...)[0] by retrieving the
first element via next(iter(...)). Update both occurrences while preserving the
existing .id access and test behavior.
- Around line 217-230: Replace the duplicated manual AELFRICE_DB environment
save/set/restore logic with pytest monkeypatch handling: update _demote_via_cli
and _lock_via_cli to accept monkeypatch and call setenv, update
tests/test_lock_management.py:217-230 and tests/test_lock_management.py:368-382
accordingly, and replace the inline environment block in
tests/test_ingest_log.py:438-471 within
test_relock_appends_a_log_row_and_corroborates with monkeypatch.setenv; extract
a shared helper there only if the pattern is reused.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: af4264cf-fca2-4f22-aee4-dcea8bbfc596

📥 Commits

Reviewing files that changed from the base of the PR and between 9848be0 and 1325adf.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock, !**/*.lock
📒 Files selected for processing (64)
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/ISSUE_TEMPLATE/question.yml
  • .github/workflows/post-release-docs-issue.yml
  • CHANGELOG/v4.md
  • CITATION.cff
  • CONTRIBUTING.md
  • README.md
  • SECURITY.md
  • docs/README.md
  • docs/concepts/ARCHITECTURE.md
  • docs/concepts/HARNESS_INTEGRATION.md
  • docs/concepts/PHILOSOPHY.md
  • docs/concepts/ROADMAP.md
  • docs/user/COMMANDS.md
  • docs/user/CONFIG.md
  • docs/user/INSTALL.md
  • docs/user/LIMITATIONS.md
  • docs/user/MCP.md
  • docs/user/PRIVACY.md
  • docs/user/README.md
  • docs/user/SLASH_COMMANDS.md
  • pyproject.toml
  • src/aelfrice/bm25.py
  • src/aelfrice/classification.py
  • src/aelfrice/cli.py
  • src/aelfrice/db_paths.py
  • src/aelfrice/derivation.py
  • src/aelfrice/detector_thresholds.py
  • src/aelfrice/federation.py
  • src/aelfrice/feedback.py
  • src/aelfrice/hook.py
  • src/aelfrice/lifecycle.py
  • src/aelfrice/lock_expiry.py
  • src/aelfrice/mcp_cleanup.py
  • src/aelfrice/mcp_server.py
  • src/aelfrice/models.py
  • src/aelfrice/promotion.py
  • src/aelfrice/retrieval.py
  • src/aelfrice/scoring.py
  • src/aelfrice/session_resolution.py
  • src/aelfrice/setup.py
  • src/aelfrice/store.py
  • src/aelfrice/wonder/__init__.py
  • src/aelfrice/wonder/dispatch.py
  • src/aelfrice/wonder/result.py
  • tests/conftest.py
  • tests/test_cli_confirm.py
  • tests/test_cli_mcp.py
  • tests/test_confirm_posterior_shift.py
  • tests/test_derivation.py
  • tests/test_ingest_log.py
  • tests/test_llm_classifier.py
  • tests/test_lock_management.py
  • tests/test_mcp_cleanup_1422.py
  • tests/test_mcp_lock_via_worker.py
  • tests/test_mcp_remember_reader_contract_1422.py
  • tests/test_mcp_server.py
  • tests/test_mcp_wrapper_layer.py
  • tests/test_phantom_lifecycle_counts.py
  • tests/test_session_id_population_rate.py
  • tests/test_session_id_propagation.py
  • tests/test_slash_commands.py
  • tests/test_to_scope.py
  • tests/test_uninstall_dotdir.py
💤 Files with no reviewable changes (14)
  • docs/user/README.md
  • src/aelfrice/session_resolution.py
  • tests/test_slash_commands.py
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • pyproject.toml
  • src/aelfrice/mcp_server.py
  • tests/test_cli_mcp.py
  • docs/user/INSTALL.md
  • docs/user/MCP.md
  • tests/test_mcp_server.py
  • tests/test_phantom_lifecycle_counts.py
  • tests/test_to_scope.py
  • tests/test_mcp_lock_via_worker.py
  • tests/test_mcp_wrapper_layer.py

Comment thread .github/ISSUE_TEMPLATE/question.yml Outdated
Comment thread src/aelfrice/cli.py Outdated
Comment thread src/aelfrice/federation.py Outdated
Comment thread src/aelfrice/mcp_cleanup.py
Comment thread src/aelfrice/mcp_cleanup.py
Comment thread src/aelfrice/wonder/dispatch.py Outdated
Comment thread tests/test_mcp_remember_reader_contract_1422.py Outdated
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Garsecg:2026-08-06T22:54:59Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Garsecg:2026-08-06T22:55:04Z]

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 6738 changed lines (limit: 200)
  • 65 changed files (limit: 3)

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated attn:merge-conflict cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the size:override label and this comment will be removed on the next push.

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Gylf:2026-08-06T22:59:30Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Gylf:2026-08-06T22:59:35Z]

@robotrocketscience
robotrocketscience force-pushed the refactor/issue-1422-remove-mcp-surface branch from 1325adf to f4befc5 Compare August 6, 2026 23:04
@robotrocketscience

Copy link
Copy Markdown
Owner Author

All seven review threads actioned — five commits, four mutation-checked

Every one of these was real and in scope. Taking them in order of what they cost.

The one that mattered: the AC5 guard did not guard anything

MemoryStore(":memory:") creates a fresh database … This test therefore does
not load a pre-removal row containing the historical "mcp_remember" value.

Correct, and worse than stated. beliefs has no source_kind column at all
the value lives in ingest_log. So the round-trip through insert_belief /
get_belief never touched source_kind; the test would have passed with the
constant deleted, with it renamed, with any value substituted. It asserted
nothing its name or docstring claimed.

Replaced (3ee11bc0) with a row seeded by raw SQL rather than
record_ingest — going through record_ingest validates against
INGEST_SOURCE_KINDS, so it would pass by construction on exactly the change
the test exists to catch. File-backed, closed, and reopened, because opening a
store is a write: DDL and the migration sweep run on open, which is where an
unrecognised value would actually be rejected or rewritten.

While verifying this I found my own framing was overstated, and corrected it.
The module docstring called removal "the #1161 unopenable-store class". It is
not. Checked against the tree: no source column carries a SQL CHECK anywhere in
the schema; _ingest_row_to_dict just str()s the column without validating;
INGEST_SOURCE_KINDS has exactly one consumer (record_ingest, store.py:4997)
and both CORROBORATION_SOURCE_TYPES uses (:4550, :4639) are likewise write
paths. Dropping the constants could not make an existing store unopenable. The
two reachable costs are narrower — record_ingest would reject the value,
breaking replay of historical rows including the soak corpus; and
retention_class_for_source would fall through its .get default and
reclassify the row unknown instead of fact, silently, since nothing raises.
That second one is now asserted directly. The constants are still worth keeping,
but for the future-reader risk, not a present brick.

Correctness

  • cli.py unreadable config exited 0 (8f2d0fb4). Right, and the fix was
    already half-written: maybe_clean_up_mcp makes exactly this distinction at
    mcp_cleanup.py:476. Extracted it as scan_was_incomplete(notes) so the two
    cannot drift, rather than exporting the private marker. Also covers the
    partial case you did not raise — one config removed, another unread — which
    still exits non-zero, because failed is False there and the run still did
    not establish that nothing remains.
  • Non-table tool key (50ca848e). Confirmed: .get on a str raises
    AttributeError against a docstring promising False.
  • location() in the success message (b2a8421e). Confirmed, and it is the
    same project-scoped class as the defect fixed in 1325adf1 — the message is
    the undo instruction, so it named a key the file does not contain.

Prose (c0683002)

All three are on lines this PR already edited to strike the MCP surface, so they
are mine now regardless of who wrote them first.

Verification

Four mutations, each run with __pycache__ cleared:

mutation result
restore receipt.get("tool", {}).get(...) red
restore hardcoded mcpServers.{key} red
drop "mcp_remember": RETENTION_FACT red
restore return 1 if failed else 0 red

The last one survived on the first attempt — my initial test only exercised
the no-registrations branch, leaving or incomplete on the final return
untested. Added the partial-removal case, which kills it.

Full suite 7331 passed, 70 skipped, 71 xfailed. The one unrelated failure is
test_slot_conflict::test_latency_budget (p95 6.1ms vs a 5.0ms budget), which
reproduces on unmodified main at 2d1e4aea (1 of 7 runs there, 2 of 7 here) —
a pre-existing marginal perf test on a loaded machine, not something this branch
introduced. CI is the authority for that one and it passed there.

Comment thread tests/test_mcp_cleanup_1422.py Fixed
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Garsecg:2026-08-06T23:48:25Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Garsecg:2026-08-06T23:48:29Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Garsecg:2026-08-09T03:51:57Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Garsecg:2026-08-09T03:52:02Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Idnn:2026-08-09T03:52:29Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Idnn:2026-08-09T03:52:34Z]

`aelf confirm` is the one feedback surface exempt from the lock floor —
documented in docs/user/COMMANDS.md, implemented as a single
`respect_lock=False` keyword at one call site, and until now covered by
nothing. Every existing test in the file uses unlocked beliefs, where
the exemption makes no difference, so dropping the kwarg would report
success while the posterior silently never moved.

Two arms so neither can pass vacuously: confirm moves a locked belief's
alpha 1.0 -> 2.0, and plain `aelf feedback` on the same locked belief
leaves it at 1.0. Flipping the kwarg to True turns the first red.

This is a pre-existing gap, not one introduced here; it lands first
because the call site is about to be relocated.
…module

`aelf confirm` reached its implementation via
`from aelfrice.mcp_server import tool_confirm`, which made the CLI depend
on the MCP surface for a plain library operation. `tool_confirm` was a
thin wrapper: one `apply_feedback(...)` call, a ValueError branch, and a
dict assembled entirely from fields `FeedbackResult` already exposes.
`note` was never persisted — it was echoed back to the caller and
nothing else, so it is now read straight off `args`.

`respect_lock=False` is carried across deliberately and commented as
load-bearing: it is the #1168 lock-floor exemption, and the guard added
in the previous commit fails if it is dropped. `ForeignBeliefError`
subclasses `ValueError`, so one clause reproduces both the
unknown-belief and foreign-belief rejections that the old
`confirm.unknown_belief` payload carried.

First step of #1422: it removes the only src/ dependency on
`mcp_server` other than `serve()` itself.
…surfaces

Nothing here is a mechanical import swap: `mcp_server` held real logic
next to the MCP plumbing, so each file was triaged assertion by
assertion into "wire format, dies with the surface" and "behaviour that
must survive".

Behaviour ported, not dropped:
- test_ingest_log: the post-#264 re-lock contract (a second lock appends
  a log row and corroborates rather than duplicating) was asserted ONLY
  through tool_lock. The neighbouring CLI test covers a single lock and
  stops there, so the contract now has its own CLI-driven test.
- test_lock_management: the #391 demote lock-drop audit row, and the
  lock -> unlock -> re-lock round trip. Both now drive `aelf lock` /
  `aelf demote` rather than seeding a locked row, because what they
  exercise is the ingest path that makes "re-lock" mean corroborate.
- test_session_id_population_rate: the four tool_lock calls are replaced
  by four _cmd_lock calls on distinct statements, not deleted — deleting
  them drops the measured rate to 75% against its own >= 0.80 gate.
  Re-measured: 10/12 = 83.33%, composition still 12 calls.

Wire-format assertions removed with the surface, each after confirming
an equivalent exists: tool_stats phantom payloads (core + CLI tests
cover the counts and the rendering), tool_promote/tool_demote to_scope
shapes (test_to_scope's CLI block covers every case; not-found is
test_cli.py:191), tool_unlock/tool_promote shape parity, and
tool_confirm's return-dict tests (the CLI contract is test_cli_confirm).

test_confirm_posterior_shift keeps its library-level focus on the
posterior mathematics and calls apply_feedback through a helper that
carries respect_lock=False, with a docstring pointing at
test_cli_confirm for the command contract.
The surface has never started on any version of its declared dependency
range. `serve()` fails on fastmcp 1.0 and 2.0.0 with `TypeError:
FastMCP.tool() got an unexpected keyword argument 'annotations'`, on
2.10.0 with `cannot specify both default and default_factory`, and on
3.2.4 with `NameError: name 'Field' is not defined` — the last because
`from pydantic import Field` is a local of `serve()` while
`from __future__ import annotations` makes fastmcp resolve tool
annotations against module globals. Broken since d6bafcb (2026-05-08),
shipped that way in v2.0.1 and every release since. No CI job installs
the extra, so nothing could see it.

Removed: src/aelfrice/mcp_server.py, `_cmd_mcp` and its subparser, the
"mcp" entry in tests/test_slash_commands.py HIDDEN_SUBCOMMANDS (the
subparser set is asserted to equal EXPECTED u HIDDEN, so it goes red
otherwise), and the four test modules that only exercised the surface.

Deliberately NOT removed: the `mcp_remember` source_kind and its
frozenset membership in models.py, and derivation.py's handling of it.
Existing stores hold rows carrying it, and a reader that rejects a
source_kind it wrote last release makes the store unopenable — the
#1161 class, unrecoverable in the field. Production stops *writing* it
(tool_lock was its only writer); readers keep accepting it, and
tests/corpus/replay_soak/v0.1/mcp_remember_v0_1.jsonl still replays
through the soak runner's glob, which is what keeps that contract green
now that the writer is gone.
`fastmcp` and `pydantic` were declared only for `mcp_server.py`, which is
gone; neither is imported anywhere else in src/, tests/, benchmarks/ or
scripts/. uv.lock pinned fastmcp under `extra == 'mcp'` and CI runs
`uv sync --frozen`, so the lockfile has to move in the same commit or
every job fails on a stale lock. Verified with the exact CI invocation
(`uv sync --frozen --group dev --extra archive`).

No deptry per_rule_ignores entry referenced either package, so the
ignore list needs no edit.
…s gone

AC5 of #1422. Nothing produces `source_kind='mcp_remember'` any more, so
the constants read as dead code to the next person tidying up after the
removal — and deleting them is a one-line change that makes every store
holding a historical row unopenable. That is the #1161 class, and it is
not recoverable in the field.

Four assertions: the constant is still in INGEST_SOURCE_KINDS; still in
CORROBORATION_SOURCES_USER_EXPLICIT (dropping it there would silently
change whether re-asserting an old statement revives a retired belief,
#1215); `derive()` still gives such a row its locked priors; and a store
holding one still opens and reads it back. The wire string is pinned
separately, because renaming the literal orphans on-disk rows just as
effectively as deleting the constant.

Mutation-checked, all three killed: dropped from INGEST_SOURCE_KINDS ->
red; dropped from CORROBORATION_SOURCES_USER_EXPLICIT -> red; renamed to
"mcp_remember_legacy" -> red.
The README promised "any MCP host can use the included stdio server" for
a surface that has never started. Struck there and everywhere else the
docs assert MCP is a supported interface: docs/user/MCP.md deleted, the
`mcp` row removed from COMMANDS.md, the `aelfrice[mcp]` line from
INSTALL.md, the extras lists in PRIVACY/ARCHITECTURE/PHILOSOPHY, the
mcp_server row from ARCHITECTURE's module table, the doc index entries,
the bug_report.yml component options, the question.yml doc links,
SECURITY.md's MCP threat-surface bullets, CITATION.cff, CONTRIBUTING.md,
and the post-release docs checklist.

Also swept the in-code prose that named MCP as a live caller — 27
docstrings and comments across store, retrieval, feedback, promotion,
scoring, hook, cli, setup, federation, wonder and lock_expiry. A comment
saying "long-running processes (MCP server)" is a false claim about the
codebase once the server is gone, and this project treats those as
defects rather than cosmetics.

Deliberately untouched, and this is the rule rather than an oversight:
anything that is a *dated record* instead of a current claim.
`docs/audits/*` are audit snapshots of a given day; `docs/design/*`
record what was designed and decided at the time, several with explicit
Status headers and open TBDs; ROADMAP's shipped-version rows and
PHILOSOPHY's "15 MCP tools at v3.3" are release history; store.py's
#1161 narrative describes what used to break. Rewriting any of those
would falsify the record — the removal changes what is true now, not
what was true then.
Deleting the code does not clean up after it. Anyone who installed
`aelfrice[mcp]` still carries fastmcp in their uv environment, and anyone
who pasted the block from the old docs/user/MCP.md still has a host entry
pointing at a command that no longer exists.

A one-shot pass behind `~/.aelfrice/mcp-surface-removed`, fired from
`_cmd_setup_locked` next to the #733 and #1064 migrations, reports both.
Two design constraints, neither optional:

**It reports; it does not edit.** aelfrice has never written an
`mcpServers` key — `git grep mcpServers` on main returns two hits, both
lines inside the doc that told users to paste one — so no path constant
for those files exists anywhere in the tree, and the file usually holds
the user's other servers. lifecycle.py:791-797 already states the house
rule for exactly this: anything not named in the dotdir contract is
reported and never deleted. `aelf migrate --remove-mcp-config` is the
opt-in verb; it backs up to a timestamped sibling before touching
anything and says so in its output.

**It advises the reinstall rather than running it.** `maybe_migrate_to_uv`
may shell out to `uv tool install` because it is guarded to run only when
the install is NOT a uv tool install. This targets the opposite
population — the dead extra lives precisely in uv-tool installs — so the
extra is detected by reading uv's own receipt with stdlib tomllib (no
subprocess, no network) and the command is printed for the user to run.

The recognition predicate covers all four shapes this project published,
not just the current doc's: `aelf mcp`, `uv run --project <abs> aelf mcp`,
`aelf serve` (docs/INSTALL.md @99160871) and
`python -m aelfrice.mcp_server`, plus the documented-but-never-shipped
`aelf-mcp`. It never matches on the map key (users rename it) and never on
`aelf` alone (forty other verbs).

Sentinel registered in both conftest lists — `_HOME_PINS` and
`_PRECREATED_SENTINELS`. The second is the one that matters: an
exists()-guarded sentinel pinned at a fresh tmp path *arms* the guarded
side effect instead of disarming it, and CI cannot catch that because a
runner's HOME is empty. Also added to lifecycle's `_DOTDIR_INSTALL_STATE`
and the uninstall agreement test, so the file is not orphaned on teardown.

Mutation-checked, all five killed: widening the predicate to a bare
command match, matching on the map key, skipping the backup, making the
automatic pass edit, and swallowing parse errors silently.
The MCP mention there is not a claim that aelfrice ships an MCP server —
it says the onboard classifier needs no MCP roundtrip, which stays true.
Rewriting the line would re-add it as a changed line, and the line also
carries a token the discretion gate blocks on additions, so touching it
trades a correct doc for a blocked push. Left exactly as it was.
…ing the undo

Six defects in the one-shot cleanup path, all reachable on a user's
machine. Each is mutated red-then-green with __pycache__ cleared.

1. Locally-scoped registrations were invisible. _scan_file read only the
   top-level mcpServers map, but a local-scope server is stored under
   projects.<dir>.mcpServers in the same file. So the pass reported
   'nothing to clean up' on the commonest registration shape -- and then
   latched its sentinel, so it never looked again. Registration now
   carries the project it lives under, both scopes are scanned in one
   pass, and remove_registration resolves the right container (finding it
   without carrying the container would find it and be unable to remove
   it).

2. The backup was clobbered. The stamp is second-resolution, so two
   registrations removed from one file in one run resolved to the same
   backup name and the second wrote already-edited content over it. The
   pre-edit original was then gone while both messages still named that
   path as the undo. Takes the first free name instead.

3. candidate_config_paths returned ~/.mcp.json and cwd/.mcp.json with no
   dedup, so with cwd == HOME the same file was scanned twice --
   duplicating every note and making a successful removal report failure
   on the second pass.

4. The sentinel latched even when a config could not be read. A scan that
   could not read its input has not established that there is nothing to
   clean up, so it suppressed the one-shot report on exactly the machines
   that still needed it. The docstring already promised otherwise.

5. _basename did not strip a Windows .exe/.cmd/.bat suffix, so aelf.exe
   was unrecognised -- and with the map key named aelfrice the routine
   printed that aelfrice did not publish a command it did publish. The
   verb is still required, so the strip does not widen the match.

6. mcp_extra_is_installed read only the extras list, missing
   'uv tool install --with fastmcp aelfrice', which uv records as a
   sibling requirement. The CHANGELOG promises that population is
   reported.

Full suite 7314 passed.
`receipt.get("tool", {}).get(...)` calls `.get` on whatever the receipt
holds, so a hand-edited `tool = "aelfrice"` raises AttributeError instead
of the False the docstring promises. `aelf setup`'s broad handler hides
it; a direct `maybe_clean_up_mcp` call does not.
The success line hardcoded `mcpServers.<key>`, so removing a locally-scoped
entry reported `mcpServers.aelfrice` for something that actually lives at
`projects.<dir>.mcpServers.aelfrice`. The message is the undo instruction,
so it pointed at a key the file does not contain. `location()` already
spells this and every other message in the module uses it.
`find_registrations` returns notes without registrations when a config is
unreadable, so `--remove-mcp-config` printed the note, then "no aelfrice MCP
registration found", and exited 0. The user asked for an edit and one input
was never inspected; "nothing to remove" and "could not look" have different
fixes. `maybe_clean_up_mcp` already made this distinction — `scan_was_incomplete`
shares it so the two cannot drift.
…sh insert

The round-trip test asserted nothing it claimed to. `beliefs` has no
`source_kind` column — the value lives in `ingest_log` — so inserting a
derived belief and reading it back passes for any `source_kind` whatsoever.

Replaced with a row seeded by raw SQL (going through `record_ingest` would
validate against the frozenset and pass by construction on exactly the change
this guards), in a file-backed store that is closed and reopened, because
opening runs DDL and the migration sweep — where an unrecognised value would
actually be rejected or rewritten.

Also corrects the module docstring. Removing the constants is *not* the #1161
unopenable-store class: no source column carries a SQL CHECK and the row
decoder does not validate. The two reachable costs are that `record_ingest`
would reject the value, breaking replay of historical rows including the soak
corpus, and that `retention_class_for_source` would silently fall through its
default and reclassify the row `unknown` — now asserted, since nothing raises.
…ouched

question.yml pointed at docs/COMMANDS.md, which does not exist — the file is
docs/user/COMMANDS.md. federation.py said `belief_id`/`owning_scope` reach
callers through "CLI exit codes"; the exit code is 1, the message carries
them. wonder/dispatch.py documented only `--axes` and in the wrong order —
the flag takes the query as its value, and `aelf wonder QUERY` (#645) is the
primary form with `--axes QUERY` (#551) retained as an alias.

All three were pre-existing or under-stated text on lines this PR already
edited to strike the MCP surface.
@robotrocketscience
robotrocketscience force-pushed the refactor/issue-1422-remove-mcp-surface branch from 26cb8e1 to 929c2fa Compare August 9, 2026 03:55
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

merge-train: blocked

1 review thread(s) are unresolved on these files: tests/test_mcp_cleanup_1422.py. Resolve them on the PR (click 'Resolve conversation' on each) and re-add the label.

The ready-to-merge label has been removed. Address the issue above and re-add the label when you're ready for another attempt.

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Aug 9, 2026
CodeQL alert 561: the control block bound `result` a second time and never
read it. Asserting `ran` on it makes the binding load-bearing — the control
now shows the pass actually executed, not merely that the sentinel exists.
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Aug 9, 2026
@github-actions
github-actions Bot merged commit b962eeb into main Aug 9, 2026
33 checks passed
@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

merge-train: merged b962eebmain via FF push.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

attn:review Needs review (PR open, awaiting reviewer) author-Kulili PR coordination mutex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(mcp): remove the MCP surface — it has never started on any supported fastmcp version

2 participants