mcpField
C
Contentrain

Contentrain

Remote
ai.contentrain.ioBusiness & Marketing6Updated today

About Contentrain

Contentrain is a provider-agnostic MCP engine for governing AI-generated project content through schemas and Git-backed workflows. It is for teams that want deterministic content changes, review, and consistent serialization across locales.

Key features

  1. 0127 MCP tools for content workflows
  2. 02Manage models and localized content
  3. 03Validate and normalize project content
  4. 04Git-backed branch and review flows
  5. 05Local-first with optional GitHub and GitLab

Use cases

  1. 01Maintain structured, localized project content
  2. 02Scan source files and normalize hardcoded strings
  3. 03Review content changes on branches before merging

MCP support

It provides stdio and HTTP MCP server modes, and can also be embedded with createServer(projectRoot). Available tools depend on the provider and projectRoot capabilities.

Configuration

Remote servers need no local install. Pick your client, then copy the config or install in one click.

Endpointhttps://studio.contentrain.io/api/mcp/remote
Copy install command
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.

All endpoints
Streamable HTTPhttps://studio.contentrain.io/api/mcp/remote
Streamable HTTPhttps://studio.contentrain.io/api/mcp/v1/{project_id}/mcpAuth required
Packages
npm@contentrain/mcp3.11.18

Tools

Details

From the project README

@contentrain/mcp

npm version GitHub source Docs

Provider-agnostic MCP engine for Contentrain — local-first by default, with optional GitHub and GitLab backends and an HTTP transport for remote drivers such as Studio.

Start here:

Contentrain is AI-generated content governance infrastructure:

  • agent produces content decisions
  • MCP applies deterministic filesystem and git workflow
  • humans review and merge
  • the system keeps schema, locale, and serialization consistent

This package is the runtime core behind Contentrain's MCP integration. It can be used as:

  • a stdio MCP server (contentrain-mcp)
  • an embeddable server (createServer(projectRoot))
  • a low-level toolkit for config, models, content, validation, scanning, and git transaction flow

Install

pnpm add @contentrain/mcp

Requirements:

  • Node.js 22+
  • git available on the machine

Optional parser support for higher-quality source scanning:

  • @vue/compiler-sfc
  • @astrojs/compiler
  • svelte

They are listed as optional dependencies. The scanner still works without them, but Vue/Astro/Svelte detection is stronger when they are installed.

What It Does

@contentrain/mcp manages a .contentrain/ directory in your project and exposes MCP tools for:

  • project initialization
  • model creation and deletion
  • content save, delete, and list
  • validation and auto-fix
  • normalize scan and apply flows
  • bulk operations
  • branch submission, review-mode merge, and branch-health awareness
  • project health checking (doctor)

All write operations are designed around git-backed safety:

  • a dedicated contentrain branch serves as the content state single source of truth
  • each write creates a temporary worktree on a feature branch forked from contentrain (branch name: cr/{operation}/{model}/{locale}/{timestamp}-{suffix})
  • auto-merge: feature merges into contentrain, baseBranch advanced via update-ref, .contentrain/ files selectively synced to developer's working tree when the base branch is the one checked out
  • baseBranch = CONTENTRAIN_BRANCH env → repository.default_branch → remote HEAD → main → master → checked-out branch (last resort). A write made on a feature branch never merges, moves or pushes that branch
  • plans are read from the working tree but committed on the fetched contentrain tip: JSON changes are carried over key by key, so entries another writer pushed are kept; a same-value conflict writes nothing and returns CONTENT_WORKING_TREE_STALE
  • on a checked-out feature branch, which writes do not update, .contentrain/** is read from the contentrain ref instead (source files still from the working tree); read tools report it as content_source
  • review: feature branch pushed to remote for team review; once merged (or deleted), its remote copy is removed too — best-effort, opt out with remoteBranchCleanup: false in config.json
  • local mode (git.push: false or CONTENTRAIN_NO_PUSH=1): nothing is ever pushed or deleted on the remote, and every result says so — see Git environment and local mode
  • every local write's git block names the resolved base_branch; when it is not the checked-out branch, the result's warning says your working tree was left alone
  • commit hooks: skipped on machine commits by default; git.verify: true or CONTENTRAIN_VERIFY=1 runs them, and a rejecting hook fails the write with its output — see Commit hooks
  • merged-branch detection survives base-history rewrites (ancestry check with a patch-id fallback), so rebases/squashes don't strand stale branches
  • developer's working tree is never mutated during MCP git operations (no stash, no checkout, no merge)
  • context.json never lands on feature branches — it is regenerated on the contentrain branch after merge (locally by the transaction layer; in remote flows by the orchestrator that owns the merge)
  • canonical JSON output — sorted keys, 2-space indent, trailing newline
  • validation + next-step hints surfaced to the caller

Tool Surface

27 MCP tools — 22 core + 5 media — with annotations (readOnlyHint, destructiveHint, idempotentHint, and openWorldHint: false everywhere except contentrain_media_ingest, which fetches a caller-supplied URL server-side) for client safety hints.

Tool listing is capability-aware. tools/list only advertises tools the resolved provider + projectRoot pair can actually satisfy. A local stdio server lists the 22 core tools; a session driven by a remote provider (GitHub/GitLab, no local checkout) lists only the remote-safe subset — status, describe, describe_format, model_save, model_delete, content_save, content_delete, content_list, vocabulary_save, vocabulary_delete, validate. The requirement map lives in TOOL_REQUIREMENTS (@contentrain/mcp/tools/availability).

ToolPurposeRead-onlyDestructive
contentrain_statusProject status, config, models, branch health, contextYes—
contentrain_describeFull schema and sample data for a modelYes—
contentrain_describe_formatFile-format and storage contract referenceYes—
contentrain_doctorProject health report (env, structure, models, orphans, local + remote branches, SDK)Yes—
contentrain_initCreate .contentrain/ structure and base config——
contentrain_scaffoldApply a starter template such as blog, docs, landing, saas——
contentrain_model_saveCreate or update a model definition——
contentrain_model_deleteDelete a model definition—Yes
contentrain_content_saveSave content entries for any model kind——
contentrain_content_deleteDelete content entries—Yes
contentrain_content_listRead content entriesYes—
contentrain_validateValidate project content, optionally auto-fix structural issues——
contentrain_submitPush cr/* branches to remote, then lazily prune merged local + remote leftovers——
contentrain_mergeMerge a review-mode branch into contentrain locally (by exact branch or model); deletes its remote copy——
contentrain_reconcileContent-aware three-way merge of a diverged contentrain ↔ base pair (dry_run first, resolutions second)——
contentrain_branch_listList pending cr/* branches with merge status (remote: true adds remote view)Yes—
contentrain_branch_deleteDelete a stale/failed cr/* branch locally and on the remote (contentrain branch protected)—Yes
contentrain_scanGraph- and candidate-based hardcoded string scanYes—
contentrain_applyNormalize extract/reuse execution with dry-run support——
contentrain_bulkBulk locale copy, status updates, and deletes (dry_run previews)——
contentrain_vocabulary_saveAdd or update canonical vocabulary terms (merges with what exists)——
contentrain_vocabulary_deleteRemove canonical vocabulary terms by slug (confirm: true)—Yes
contentrain_media_listList media assets (search, tag filter, cursor pagination)Yes—
contentrain_media_getGet one media asset by idYes—
contentrain_media_ingestIngest an asset from a source URL (provider fetches server-side)——
contentrain_media_updateUpdate asset metadata (alt, tags, filename)——
contentrain_media_deleteDelete an asset from the media stack—Yes

The five contentrain_media_* tools are a deterministic passthrough to the provider's optional media facet (RepoProvider.media) and are registered only when the provider exposes one (e.g. Studio MCP Cloud). Local stdio servers and plain GitHub/GitLab providers never list them. Ingest is URL-based (MCP has no binary channel); the provider implementation owns SSRF/MIME/size policy for the fetch.

Quick Start

Configure via CLI (recommended)
npx contentrain setup claude-code   # or: cursor, vscode, windsurf, copilot

This auto-creates the correct MCP config file for your IDE. See CLI docs for details.

Run as a standalone MCP server
CONTENTRAIN_PROJECT_ROOT=/path/to/project npx -y @contentrain/mcp

If CONTENTRAIN_PROJECT_ROOT is omitted, the current working directory is used.

Git environment and local mode

These apply to the local provider (stdio, LocalProvider):

VariableDefaultEffect
CONTENTRAIN_BRANCHunsetBase branch the contentrain branch advances. Wins over repository.default_branch, then remote HEAD → main → master → the checked-out branch. Per process; never written to config
CONTENTRAIN_REMOTEoriginRemote used for every fetch and push (and remote branch cleanup)
CONTENTRAIN_NO_PUSHunset1/true turns local mode on; 0/false turns it off. Any other value is ignored
CONTENTRAIN_VERIFYunset1/true runs the repository's commit hooks on Contentrain's commits; 0/false skips them. Wins over git.verify. Any other value is ignored

Local mode keeps every write on local branches: no branch push, no review push, no push after an auto-merge, approve or reconcile, and no remote branch delete or prune. Fetching is not a push and still happens when a remote exists. Turn it on for a project with "git": { "push": false } in .contentrain/config.json, or for one run with CONTENTRAIN_NO_PUSH=1.

Precedence: the env wins over the config in both directions. CONTENTRAIN_NO_PUSH=1 turns pushing off even when config allows it; CONTENTRAIN_NO_PUSH=0 turns it back on over git.push: false. With neither, pushing is on.

It is never a silent success. Each write's git block reports remote_push: "disabled" with remote_note: "not pushed (local mode)", and next_steps carries a LOCAL MODE line. contentrain_submit returns an error instead of pushing; land review branches with contentrain_merge. contentrain_merge and contentrain_branch_delete delete the local branch as usual and report remote_skipped: "local-mode", leaving any remote copy untouched.

Commit hooks

By default Contentrain's machine commits (the cr/* content commit, the context.json commit, a reconcile merge commit, the seed commit of an empty repo) skip the repository's commit hooks (pre-commit, prepare-commit-msg, commit-msg). That keeps existing projects working: these commits are made in a temporary worktree without your node_modules, and their [contentrain] … subject fails a strict commitlint. Merges still run the merge hooks and pushes still run pre-push, as before.

To run commit hooks on every machine commit, set "git": { "verify": true } in .contentrain/config.json, or CONTENTRAIN_VERIFY=1 for one run (CONTENTRAIN_VERIFY=0 skips them over the config). With hooks on:

  • a hook that rejects the content commit fails the write: nothing lands, and the tool error carries the hook's output. Nothing is retried without hooks;
  • a hook that rejects only the follow-up context.json commit (the content is already on contentrain) is reported in the result's warning, not swallowed;
  • the hooks must work in a worktree under the system temp directory — a hook that runs npx lint-staged needs the tool available there.
Embed the server in your own process
import { createServer } from '@contentrain/mcp/server'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

const server = createServer(process.cwd())
const transport = new StdioServerTransport()

await server.connect(transport)

createServer also accepts an options object: { provider, projectRoot?, instructions? }. instructions sets the MCP instructions string clients receive at initialize (defaults to defaultInstructions for the tools the server registers — with every tool available, DEFAULT_INSTRUCTIONS — kept under 512 characters; pass '' to omit). The HTTP entries (startHttpMcpServer, startHttpMcpServerWith) take the same instructions option.

Example MCP Flow

Typical agent workflow:

  1. Call contentrain_status
  2. If needed, call contentrain_init
  3. Create models with contentrain_model_save or contentrain_scaffold
  4. Save content with contentrain_content_save
  5. Validate with contentrain_validate
  6. For hardcoded strings, use contentrain_scan then contentrain_apply
  7. Push review branches with contentrain_submit

Normalize Flow

Normalize is intentionally split into two phases:

1. Extract

contentrain_scan finds candidate strings.

contentrain_apply with mode: "extract":

  • creates or updates models
  • writes content entries
  • records source tracking
  • creates a review branch (cr/normalize/extract/{domain}/{timestamp})
2. Reuse

contentrain_apply with mode: "reuse":

  • patches source files using agent-provided expressions
  • adds imports when needed
  • enforces patch path safety and scope checks
  • creates a separate review branch (cr/normalize/reuse/{model}/{locale}/{timestamp})

This split keeps content extraction separate from source rewriting.

Transport / provider requirements

Normalize (contentrain_scan and contentrain_apply) requires local disk access — AST scanners walk the source tree and patch files in place. It runs only on a LocalProvider (stdio transport, or HTTP transport configured with a LocalProvider).

Remote providers such as GitHubProvider expose astScan: false, sourceRead: false, and sourceWrite: false. Calling these tools over a remote provider returns a uniform capability error:

{
  "error": "contentrain_scan requires local filesystem access.",
  "capability_required": "astScan",
  "hint": "This tool is unavailable when MCP is driven by a remote provider (e.g. GitHubProvider). Use a LocalProvider or the stdio transport."
}

Agents driving a remote transport should fall back to a local transport (or a local checkout) before invoking normalize.

Remote Providers

MCP supports three backends behind the same RepoProvider contract:

  • LocalProvider — simple-git + worktree. Every tool (normalize included) works on it. Stdio transport defaults to this.
  • GitHubProvider — Octokit over the Git Data + Repos APIs. No clone, no worktree. @octokit/rest ships as an optional peer dependency.
  • GitLabProvider — gitbeaker over the GitLab REST API. No clone, no worktree. @gitbeaker/rest ships as an optional peer dependency. Supports gitlab.com and self-hosted CE / EE.

Each remote provider implements the same surface: reader (readFile / listDirectory / fileExists), writer (applyPlan — one atomic commit), branch ops (list / create / delete / diff / merge / isMerged / getDefaultBranch). mergeBranch goes straight through on GitHub; on GitLab it opens an MR and immediately accepts it so the final MergeResult shape matches either way. The GitHub provider also takes a full commit SHA as applyPlan's base (and createBranch's fromRef) for compare-and-set writes: a missing branch forks from that commit, and a branch that has moved away from it refuses the write with PROVIDER_CONFLICT.

GitLab — installation & usage
pnpm add @gitbeaker/rest
import { createGitLabProvider } from '@contentrain/mcp/providers/gitlab'
import { createServer } from '@contentrain/mcp/server'

const provider = await createGitLabProvider({
  auth: { type: 'pat', token: process.env.GITLAB_TOKEN! },
  project: {
    projectId: 'acme/site',             // or numeric project ID
    host: 'https://gitlab.company.com', // omit for gitlab.com
  },
})

const server = createServer({ provider })
// serve over stdio or the HTTP transport from @contentrain/mcp/server/http

Capabilities: sourceRead, sourceWrite, astScan, localWorktree are all false; pushRemote, branchProtection, pullRequestFallback are true. Normalize / scan / apply reject with a capability error on GitLabProvider — fall back to a local transport for those flows.

Bitbucket — coming soon

Bitbucket Cloud + Data Center support is on the roadmap. Until the provider ships, use the contentrain_describe_format tool to drive Contentrain content operations manually from a Bitbucket checkout via the LocalProvider path.

Core Exports

The package also exposes low-level modules for embedding and advanced use:

  • @contentrain/mcp/server
  • @contentrain/mcp/server/http
  • @contentrain/mcp/core/config
  • @contentrain/mcp/core/context
  • @contentrain/mcp/core/model-manager
  • @contentrain/mcp/core/content-manager
  • @contentrain/mcp/core/validator
  • @contentrain/mcp/core/scanner
  • @contentrain/mcp/core/graph-builder
  • @contentrain/mcp/core/apply-manager
  • @contentrain/mcp/core/scan-config
  • @contentrain/mcp/core/doctor
  • @contentrain/mcp/core/contracts
  • @contentrain/mcp/core/ops — plan APIs (including planReconcile and its bindRef reader adapter) plus content-root-relative path helpers: contentDirPath, contentFilePath, documentFilePath, metaFilePath
  • @contentrain/mcp/core/overlay-reader
  • @contentrain/mcp/core/migration — the allowlisted migration write path: createMigrationWriter, scopeHash, decide. See Migration writes below
  • @contentrain/mcp/util/detect
  • @contentrain/mcp/util/fs
  • @contentrain/mcp/git/transaction
  • @contentrain/mcp/git/branch-lifecycle — branch health/cleanup plus the remote cr/* lifecycle: deleteRemoteBranch, listRemoteCrBranches, pruneMergedRemoteBranches, isRefMerged, classifyMergedBranches
  • @contentrain/mcp/git/base-branch — resolveBaseBranch, the one base-branch resolver every MCP write and contentrain status / serve use (env → repository.default_branch → origin/HEAD → main → master → checked-out branch); resolveBaseBranchSource (the same, plus which rung answered); resolveInitDefaultBranch (what init records as repository.default_branch, or null)
  • @contentrain/mcp/git/errors
  • @contentrain/mcp/git/reconcile — reconcileBranches, the local reconcile executor
  • @contentrain/mcp/tools/annotations
  • @contentrain/mcp/templates
  • @contentrain/mcp/providers/local
  • @contentrain/mcp/providers/github
  • @contentrain/mcp/providers/gitlab

These are intended for Contentrain tooling and advanced integrations, not for direct manual editing of .contentrain/ files.

Migration Writes

A migration does not deliver content, it delivers a codebase: src/, public/, package.json, a lockfile, deploy configuration. The content write path was never built for any of that, and widening it would quietly move a security boundary the whole product rests on — "Contentrain only writes .contentrain/" is a promise, not an implementation detail.

@contentrain/mcp/core/migration is the gate instead. It wraps a provider whose applyPlan can already write any path, and puts consent in front of it.

import { createMigrationWriter, scopeHash } from '@contentrain/mcp/core/migration'

const scope = {
  allow: ['src/**', 'public/**', 'package.json', 'astro.config.mjs'],
  branch: 'migration/acme',
  base: 'main',
}

// Show the user `scope`, not the hash. Then bind their consent to it:
const approval = {
  scope_hash: await scopeHash(scope),
  approver: { kind: 'human', id: userId, role: 'owner' },
  approved_at: new Date().toISOString(),
}

const writer = createMigrationWriter(provider, scope, approval)
await writer.applyPlan({ ...plan, step: 'emit', actor })
writer.audit  // which file, in which step, by which actor, in which commit

What it guarantees:

ScopeOnly paths an allowlist pattern covers. .contentrain/** is not implicit — a migration that wants the store asks for it, and the person approving sees that it does
ConsentOne approval, bound to the exact scope by scope_hash. An approval of src/** cannot be replayed against a scope that also has .github/workflows/**
BranchA migration/* ref. The contentrain branch is never a target and never a base — including through ApplyPlanInput's default, which a migration must not inherit
TrailOne MigrationAuditEntry per file: step, path, action, the pattern that allowed it, actor, timestamp, commit
UndoOne branch, delivered as one PR, revertable with one git revert

Every refusal happens before the provider is called, and a plan is refused whole: a plan with one path outside the scope writes nothing at all, not even its allowed part. A half-applied migration is harder to recover from than one that never started.

The path matcher is a security control and is written to fail closed. Traversal, absolute paths, backslash separators, percent-encoding, empty segments and control characters are refused before any pattern is consulted; patterns are anchored at both ends, so src/* does not cover src/a/b and nothing matches by being a prefix; and regex metacharacters in a pattern are literal, so an allowlist entry cannot silently cover more than it reads.

Design Constraints

Key design decisions in this package:

  • local-first by default — stdio transport + LocalProvider works without any network dependency
  • provider-agnostic engine — the same core tools run over LocalProvider, GitHubProvider, or GitLabProvider behind a single RepoProvider contract; media tools ride the provider's optional media facet
  • remote provider SDKs (@octokit/rest, @gitbeaker/rest) are optional peer dependencies — pulled in only when their provider is used
  • JSON-only content storage
  • git-backed write workflow (worktree transaction locally, single atomic commit over the Git Data / REST APIs remotely)
  • canonical serialization — byte-deterministic output, sorted keys, trailing newline
  • framework-agnostic MCP layer
  • agent decides content semantics, MCP enforces deterministic execution
  • capability gates — tools that need source-tree access (normalize, scan, apply, doctor) reject with a uniform capability_required error on remote providers

Development

From the monorepo root:

pnpm --filter @contentrain/mcp build
pnpm --filter @contentrain/mcp test
pnpm --filter @contentrain/mcp typecheck
pnpm exec oxlint packages/mcp/src packages/mcp/tests

Related Packages

  • contentrain — CLI and local review tooling
  • @contentrain/query — generated runtime query SDK
  • @contentrain/rules — IDE/agent rules and prompts
  • @contentrain/types — shared schema and model types

Documentation

Full documentation at ai.contentrain.io/packages/mcp.

License

MIT

Locale coverage (model.locales)

contentrain_validate checks an i18n: true model's parity against every locale in config.locales.supported. A model that declares locales — a subset of that list — is checked against the subset instead, which is how a partially-translated site states the truth rather than failing on translations it never had. Absent, as on every model that predates the field, means the whole project list.

A translation that was never written is a warning: an entry of a collection that one locale has and another lacks (Entry parity), like a document's missing translation. A locale with no content file (Locale file missing) and a dictionary key missing from a locale (Key parity) are errors. Every message names the list it was evaluated against, so the two cases read apart:

Locale file missing: tr.json (checked against the model's own locales [en, tr])
Entry parity: entry "a1b2c3" exists in en but missing in tr (checked against the project's supported locales [en, tr, da])

contentrain_model_save accepts locales and rejects a locale outside config.locales.supported. contentrain_validate fix:true reports a broken declaration but never invents one: narrowing a model's coverage is a content decision, and the only value the tool could derive — the locales that happen to have files today — would write the current gaps into the schema and silence the errors that reveal them.

Runtime model configuration

Structural contentrain_model_save edits preserve existing top-level form and comments blocks. These blocks belong to the runtime provider; the structural tool does not enable or change them. Its response lists preserved_blocks when configuration was carried forward.

Comments

Markdown supported

New to MCP?

From what the protocol is to installing your first server, these answers cover most first questions.

An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.