Phase 14a: MCP boundary hardening + CLI command polish#39
Merged
Conversation
… polish
Closes the P2 "MCP entrypoint owns a private provider contract" finding
and folds four CLI gap-fillers into the same PR so the new shared
`openMcpSession` helper ships alongside the boundary work that makes
it safe to commit to.
### `@contentrain/types` — one shared MergeResult
- `MergeResult` gains an optional `sync?: SyncResult` field. Remote
providers (GitHub, GitLab) omit it; LocalProvider populates it so
selective-sync bookkeeping survives the trip through the shared
`RepoProvider.mergeBranch()` boundary.
### `@contentrain/mcp` — provider boundary
- `LocalProvider` now `implements RepoProvider` for real: the seven
branch-ops methods (`listBranches`, `createBranch`, `deleteBranch`,
`getBranchDiff`, `mergeBranch`, `isMerged`, `getDefaultBranch`)
wrap existing simple-git + transaction helpers through a new
`providers/local/branch-ops.ts` module that mirrors the shape of
`providers/github/branch-ops.ts`.
- `mergeBranch(branch, into)` asserts `into === CONTENTRAIN_BRANCH` —
the local flow merges feature branches into the content-tracking
branch and advances the base branch via `update-ref`, so arbitrary
merge targets would bypass that invariant.
- `server.ts`: the private `ToolProvider = RepoReader & RepoWriter &
{ capabilities }` alias collapses to `type ToolProvider =
RepoProvider`. Tool handlers now depend on the shared surface
directly; the alias stays re-exported so existing `ToolProvider`
imports do not migrate.
- `providers/local/types.ts`: `LocalSelectiveSyncResult` is removed
in favour of the shared `SyncResult`. `workflowOverride` types with
the shared `WorkflowMode` union instead of the duplicated literal.
Same swap in `git/transaction.ts` so the whole write path speaks
one union.
### `contentrain` — four new commands + shared MCP client
- `utils/mcp-client.ts` — shared `openMcpSession(projectRoot)` helper
built on `InMemoryTransport.createLinkedPair()`. Each command
opens a session, calls one tool, and closes in a `finally` block.
- `contentrain merge <branch>` — scriptable single-branch sibling to
the interactive `contentrain diff`. Same `mergeBranch()` helper, so
dirty-file protection + selective-sync warnings are identical.
`--yes` skips the confirmation prompt for CI/agents.
- `contentrain describe <model>` — wraps `contentrain_describe`.
Human-readable metadata + fields + stats + import snippet. Flags:
`--sample`, `--locale`, `--json`.
- `contentrain describe-format` — wraps `contentrain_describe_format`.
Quick format-spec primer for humans pairing with an agent.
- `contentrain scaffold --template <id>` — wraps
`contentrain_scaffold`. Interactive template picker when no flag
is passed; `--locales en,tr,de`, `--no-sample`, `--json`.
- `commands/status.ts`: branch-health thresholds (50/80) now come
from `checkBranchHealth()` instead of being duplicated inline. The
JSON output surfaces the full `branch_health` object so CI
consumers see the same warning/blocked state as text mode.
### Verification
- `pnpm -r typecheck` across types, mcp, cli → 0 errors.
- `oxlint` across mcp/cli/types src + tests → 0 warnings on 349 files.
- `@contentrain/types` vitest → 110/110.
- `contentrain` vitest → 130/130. Includes the 11 new command tests
(merge, describe, scaffold) and the updated status test against
the `checkBranchHealth()` mock.
- `@contentrain/mcp` fast suite (`tests/providers tests/git tests/core
tests/util tests/server tests/serialization-parity tests/conformance`)
→ **450 passed / 2 skipped / 30 files**. Includes the new
`tests/providers/local/branch-ops.test.ts` (7/7): contract shape,
prefix-filtered branch listing, create/delete round-trip, diff
status mapping (added/modified), post-merge `isMerged` flip,
`mergeBranch` target guard, and config-driven `getDefaultBranch`.
### Tool surface
No changes. Same 16 MCP tools, same arg schemas, same response shapes.
The boundary changes are purely internal — existing consumers
(Studio, CLI, IDE agents) observe identical behaviour.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LocalProvidernow implements the fullRepoProvider(7 branch-ops methods).ToolProvidercollapses toRepoProvider.LocalSelectiveSyncResult+'review'|'auto-merge'literals replaced by sharedSyncResult+WorkflowModefrom@contentrain/types.MergeResultgains optionalsync?field so selective-sync bookkeeping survives the shared contract.utils/mcp-client.tsin-memory session helper; newmerge,describe,describe-format,scaffoldcommands;statusnow delegates tocheckBranchHealth()for shared 50/80 thresholds.MergeResult.syncextension; no breaking changes.Details
@contentrain/mcpproviders/local/branch-ops.tsmirrors the shape ofproviders/github/branch-ops.ts.LocalProviderwraps these + existing transaction helpers.mergeBranch(branch, into)assertsinto === CONTENTRAIN_BRANCH— the local flow advances the base branch viaupdate-refintransaction.mergeBranch, so arbitrary merge targets would bypass that invariant.server.tsToolProviderkept as a re-export ofRepoProvider— existing imports do not migrate.LocalSelectiveSyncResultremoved in favour of sharedSyncResult;workflowOverridetyped withWorkflowModeintransaction.ts+providers/local/types.ts.contentrainutils/mcp-client.ts—openMcpSession(projectRoot)built onInMemoryTransport.createLinkedPair(). Each new command opens, calls one tool, closes infinally.contentrain merge <branch>— scriptable sibling to interactivecontentrain diff. SamemergeBranch()helper, so dirty-file protection + sync warnings are identical.--yesskips confirm.contentrain describe <model>— wrapscontentrain_describe. Flags:--sample,--locale,--json.contentrain describe-format— wrapscontentrain_describe_format.contentrain scaffold --template <id>— wrapscontentrain_scaffold. Interactive picker when no flag;--locales,--no-sample,--json.commands/status.ts— branch-health thresholds now come fromcheckBranchHealth()instead of duplicated inline. JSON output surfacesbranch_healthfor CI.Tool surface
No changes. Same 16 MCP tools, same arg schemas, same response shapes. Internal refactor only.
Test plan
pnpm -r typecheck(types, mcp, cli) → 0 errorsoxlintacross mcp/cli/types src + tests → 0 warnings on 349 files@contentrain/typesvitest → 110/110contentrainvitest → 130/130 (includes 11 new command tests + updated status health test)@contentrain/mcpfast suite (providers/git/core/util/server/serialization-parity/conformance) → 450 passed / 2 skipped / 30 filestests/providers/local/branch-ops.test.ts(7/7): contract shape, prefix listing, create/delete round-trip, diff status mapping, post-mergeisMergedflip,mergeBranchtarget guard, config-drivengetDefaultBranch🤖 Generated with Claude Code