Skip to content

feat(mcp): wave 5 — apply_merge tool + extract applyMerge service - #339

Merged
thewrz merged 2 commits into
mainfrom
feat/mcp-contract-wave5
Jul 3, 2026
Merged

feat(mcp): wave 5 — apply_merge tool + extract applyMerge service#339
thewrz merged 2 commits into
mainfrom
feat/mcp-contract-wave5

Conversation

@thewrz

@thewrz thewrz commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Why

Wave 5 of the MCP contract (ADR-044/045): make the last spec-level write — applying an accepted merge — reachable as an MCP tool, so an agent can run the full redline loop (get_spec_diff → review → apply_merge).

Stacked on #338 (Wave 4). Base is feat/mcp-contract-wave4; review only the Wave 5 commit. Retarget as the stack merges.

What

  • apply_merge (write) ← POST /specs/{}/merge. Pass specId, the DiffResult from get_spec_diff, and accept (the change UUIDs to apply). Returns { applied, rejected }.
  • Refactor: the merge orchestration (one transaction — composed edit gate ADR-018 + optimistic precondition, applyAccepted, content_version bump) was inlined in the REST mergeHandler. Extracted into a shared applyMerge() service in src/merge, called by both the REST route and the MCP tool — so the MCP handler doesn't re-implement raw transaction/SQL (the boundary smell CodeRabbit flagged in Wave 3). The REST handler now just maps outcomes to HTTP.
  • Schema DRY: the nested DiffResultSchema + merge fields move to src/ast/merge-schemas.ts (MergeFieldsShape), reused by the REST strict body schema and the MCP tool (which also carries specId) — the big diff schema lives in one place.
  • Edit-gate + merge errors (stale / forbidden / invalid-accepted-change / merge-conflict) map to tool errors; return-shape matches the REST sibling. No openapi.yaml change. Contract green (INV-1/2/2b/3); REST merge tests unchanged (refactor is behavior-preserving).

Testing

  • New merge-tool.integration.test.ts (5 cases) + REST merge tests + contract pass (106 integration total); 1340 unit
  • pnpm lint clean (eslint + tsc + prettier)
  • Codex adversarial review (refactor-equivalence + transaction-safety focus)
  • CodeRabbit review
  • CI green

🤖 Co-authored by Claude Opus 4.8. Stacked on #338.

Summary by CodeRabbit

  • New Features

    • Added a new merge action that supports applying accepted changes with version checks.
    • Exposed the same merge capability in the MCP tool interface for automation.
  • Bug Fixes

    • Improved handling for missing items, invalid inputs, stale versions, and other merge failures with clearer responses.
    • Merge results now consistently return how many changes were applied or rejected.
  • Tests

    • Added coverage for successful merges, rejected changes, validation errors, and concurrency conflicts.

Burn down the last MCP_UNEXPOSED spec op — POST /specs/{}/merge — into the
apply_merge tool (write tier), stacked on wave 4.

The merge orchestration (one transaction: composed edit gate ADR-018 + optimistic
precondition, applyAccepted, content_version bump) was inlined in the REST
mergeHandler. Extracted it into a shared applyMerge(specId, accept, diff,
expectedVersion) service in src/merge, so the REST route and the MCP tool share
one code path instead of the MCP handler re-implementing raw transaction/SQL
(the boundary smell flagged in wave 3). The REST handler now just maps outcomes.

The nested DiffResultSchema + merge fields move to src/ast/merge-schemas.ts
(MergeFieldsShape), reused by the REST strict body schema and the MCP tool (which
also carries specId) — the big diff schema lives in one place. Edit-gate and merge
errors (stale/forbidden/InvalidAcceptedChange/MergeError) map to tool errors;
apply_merge returns { applied, rejected } like its REST sibling. Contract green
(INV-1/2/2b/3); REST merge tests unchanged (refactor is behavior-preserving).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR centralizes merge application logic into a new applyMerge service in src/merge/apply-merge.ts, which handles transaction management, optimistic concurrency, and accepted-change application. The REST merge endpoint (src/api/merge.ts) is refactored to delegate to this service. New shared Zod schemas (DiffResultSchema, MergeFieldsShape, MergeBodySchema) are added in src/ast/merge-schemas.ts. An MCP apply_merge tool is introduced with handler, registration, capability tiering, contract-map exposure, and integration tests.

Changes

Centralize merge orchestration and expose apply_merge MCP tool

Layer / File(s) Summary
Shared merge validation schemas
src/ast/merge-schemas.ts, src/ast/index.ts
Adds ParagraphDiffSchema, ModifiedDiffSchema, DiffResultSchema, MergeFieldsShape, and strict MergeBodySchema/MergeBody type, re-exported from the ast index.
applyMerge transaction service
src/merge/apply-merge.ts, src/merge/index.ts
Implements applyMerge to find a spec, gate writes with optimistic concurrency, apply accepted changes in a transaction, conditionally bump content_version, and rollback on error; re-exports applyMerge and ApplyMergeOutcome.
REST merge handler refactor
src/api/merge.ts
Replaces manual transaction/edit-gate logic with a single applyMerge call, mapping outcomes to 404/200 responses and errors via mergeErrorResponse.
MCP apply_merge tool
src/mcp/merge-handlers.ts, src/mcp/merge-tools.ts, src/mcp/tools.ts, src/mcp/capabilities.ts, src/mcp/contract-map.ts
Adds handleApplyMerge and ApplyMergeShape, registers the apply_merge tool, wires registration into registerTools, tiers it as write, and exposes it in the REST↔MCP contract map.
MCP apply_merge integration tests
src/mcp/merge-tool.integration.test.ts
Adds fixtures and tests covering happy-path apply, empty-accept rejection, unknown UUID, stale version, and validation failures.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant mergeHandler
  participant handleApplyMerge
  participant applyMerge
  participant DB

  alt REST request
    Client->>mergeHandler: POST /specs/:id/merge
    mergeHandler->>applyMerge: applyMerge(id, accept, diff, expectedVersion)
  else MCP tool call
    Client->>handleApplyMerge: apply_merge(args)
    handleApplyMerge->>applyMerge: applyMerge(specId, accept, diff, expectedVersion)
  end
  applyMerge->>DB: find spec, begin transaction
  applyMerge->>DB: assertSpecWritable, applyAccepted
  applyMerge->>DB: update content_version, commit
  DB-->>applyMerge: outcome
  applyMerge-->>mergeHandler: outcome
  applyMerge-->>handleApplyMerge: outcome
  mergeHandler-->>Client: 200/404 response
  handleApplyMerge-->>Client: success payload or toolError
Loading

Possibly related PRs

  • wrzonance/SpecR#161: Implements the core merge engine and DiffResult shape consumed by the applyMerge orchestration introduced here.
  • wrzonance/SpecR#184: Introduced the original POST /specs/:id/merge handler that this PR refactors to delegate to the new applyMerge service.
  • wrzonance/SpecR#334: Introduced the MCP capability-tier and contract-map parity work that this PR's apply_merge tool plugs into.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding the apply_merge tool and extracting the shared applyMerge service.
✨ 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 feat/mcp-contract-wave5

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

Two P3 findings from the Codex adversarial review (no P1/P2; refactor confirmed
behavior-equivalent + transaction-safe):

- applyMerge accepted an injected `db` but ran the existence check on the global
  findSpecById/pool — a caller injecting another pool would get split read/write.
  Neither caller injects, so remove the param (YAGNI) and use the global pool
  throughout — the inconsistency is gone.
- apply_merge tool description implied every accepted merge bumps contentVersion;
  reworded to say it bumps only when at least one change is actually applied.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Codex adversarial review (GPT-5.5 xhigh) — outcomes

No P1/P2. Codex confirmed the refactor is behavior-equivalent and transaction-safe: BEGINassertSpecWritable (before applyAccepted) → conditional version bump only when applied > 0COMMIT, with best-effort ROLLBACK and client.release() in finally; error-mapping order correct (InvalidAcceptedChangeError before MergeError); REST strict-body validation preserved.

Two P3s, both fixed:

  1. applyMerge accepted an injected db but ran the existence check on the global findSpecById/pool — a caller injecting another pool would split read/write. Removed the unused param (neither caller injects) so the global pool is used throughout.
  2. Reworded the apply_merge description — contentVersion bumps only when at least one change is actually applied (the code already did this; the text overstated it).

@thewrz
thewrz marked this pull request as ready for review July 2, 2026 22:07
@thewrz

thewrz commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🧹 Nitpick comments (1)
src/merge/apply-merge.ts (1)

42-48: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Swallowed rollback failure has no observability.

If ROLLBACK itself fails, the error is silently discarded with only a comment. Consider logging via the pino logger (per repo convention) so a failed rollback isn't invisible in production.

As per path instructions, src/**/*.ts: "Do not use console.* in src/ outside tests and scripts; use the pino logger at src/lib/logger.ts instead." — while no console.* is used here, adding a logger.warn on rollback failure would align with that logging convention.

🤖 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/merge/apply-merge.ts` around lines 42 - 48, The rollback failure in the
`apply-merge` error path is silently swallowed, so add pino-based observability
when `client.query('ROLLBACK')` throws inside `apply-merge.ts`. Update the
nested `try/catch` in `apply-merge` to log a warning through the shared logger
from `src/lib/logger.ts` before continuing with the original error rethrow, so a
failed rollback is visible without changing the existing control flow.

Source: Path instructions

🤖 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.

Nitpick comments:
In `@src/merge/apply-merge.ts`:
- Around line 42-48: The rollback failure in the `apply-merge` error path is
silently swallowed, so add pino-based observability when
`client.query('ROLLBACK')` throws inside `apply-merge.ts`. Update the nested
`try/catch` in `apply-merge` to log a warning through the shared logger from
`src/lib/logger.ts` before continuing with the original error rethrow, so a
failed rollback is visible without changing the existing control flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d199938f-a1e6-46c0-bc5e-f2dd6a80cc60

📥 Commits

Reviewing files that changed from the base of the PR and between 79064d6 and 60dcfc2.

📒 Files selected for processing (11)
  • src/api/merge.ts
  • src/ast/index.ts
  • src/ast/merge-schemas.ts
  • src/mcp/capabilities.ts
  • src/mcp/contract-map.ts
  • src/mcp/merge-handlers.ts
  • src/mcp/merge-tool.integration.test.ts
  • src/mcp/merge-tools.ts
  • src/mcp/tools.ts
  • src/merge/apply-merge.ts
  • src/merge/index.ts

Base automatically changed from feat/mcp-contract-wave4 to main July 3, 2026 02:33
@thewrz
thewrz merged commit e0ca552 into main Jul 3, 2026
6 checks passed
@thewrz
thewrz deleted the feat/mcp-contract-wave5 branch July 3, 2026 02:33
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