Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Use these references instead:

When working with Contentrain content operations (models, content, normalize, validate), you MUST follow the rules and skills in this repo:

### Essential Rules (always-loaded, ~86 lines)
### Essential Rules (always-loaded)
- `packages/rules/essential/contentrain-essentials.md` — compact guardrails for every conversation

### Skills (Agent Skills standard, on-demand)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ npx contentrain studio connect # connect repo to Studio project
- **[Getting Started](https://ai.contentrain.io/getting-started)** — install, connect an agent, and run the first workflow
- **[Normalize Guide](https://ai.contentrain.io/guides/normalize)** — the main hardcoded-string rescue flow
- **[Ecosystem Map](https://ai.contentrain.io/ecosystem)** — package-to-product bridges across AI and Studio
- **[Contentrain Studio](https://ai.contentrain.io/studio)** — open-core team operations for Git-native structured content, self-hostable or available as a managed Pro/Enterprise offering
- **[Contentrain Studio](https://ai.contentrain.io/studio)** — open-core team operations for Git-native structured content, self-hostable or available as managed plans (Starter/Pro/Enterprise)
- **[Full Docs](https://ai.contentrain.io)** — guides, package reference, and framework integration

## Development
Expand Down
75 changes: 75 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,80 @@
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join, relative, resolve } from 'node:path'
import { defineConfig } from 'vitepress'

/**
* Preferred order for /llms-full.txt — mirrors the sidebar. Pages not listed
* here are appended afterwards so the corpus stays complete when a page is
* added without updating this list.
*/
const LLMS_PAGE_ORDER = [
'index',
'getting-started',
'demo',
'ecosystem',
'concepts',
'studio',
'packages/mcp',
'packages/cli',
'packages/sdk',
'packages/rules',
'packages/types',
'guides/providers',
'guides/http-transport',
'guides/embedding-mcp',
'guides/normalize',
'guides/frameworks',
'guides/i18n',
'guides/serve-ui',
'reference/model-kinds',
'reference/field-types',
'reference/config',
'reference/providers',
]

function allMarkdownPages(dir: string, root: string): string[] {
const out: string[] = []
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'public') continue
const full = join(dir, entry.name)
if (entry.isDirectory()) out.push(...allMarkdownPages(full, root))
else if (entry.name.endsWith('.md')) out.push(relative(root, full).replace(/\.md$/, ''))
}
return out
}

function stripFrontmatter(md: string): { body: string, title?: string } {
const m = /^---\n([\s\S]*?)\n---\n?/.exec(md)
if (!m) return { body: md }
const title = /(?:^|\n)title:\s*["']?([^"'\n]+)["']?/.exec(m[1])?.[1]?.trim()
return { body: md.slice(m[0].length), title }
}

/** Generate /llms-full.txt into the build output — the whole site as one markdown file. */
function buildLlmsFull(srcDir: string, outDir: string): void {
const discovered = allMarkdownPages(srcDir, srcDir)
const ordered = [
...LLMS_PAGE_ORDER.filter(p => discovered.includes(p)),
...discovered.filter(p => !LLMS_PAGE_ORDER.includes(p)).toSorted(),
]
const sections = ordered.map((page) => {
const raw = readFileSync(resolve(srcDir, `${page}.md`), 'utf-8')
const { body, title } = stripFrontmatter(raw)
const heading = title ?? /^#\s+(.+)$/m.exec(body)?.[1] ?? page
const url = page === 'index' ? 'https://ai.contentrain.io/' : `https://ai.contentrain.io/${page}`
return `# ${heading}\nSource: ${url}\n\n${body.trim()}\n`
})
const header = '# Contentrain AI — Full Documentation\n\n'
+ '> The complete ai.contentrain.io documentation as a single markdown file for LLM ingestion. '
+ 'Curated index: https://ai.contentrain.io/llms.txt\n\n'
writeFileSync(join(outDir, 'llms-full.txt'), header + sections.join('\n---\n\n'))
}

export default defineConfig({
buildEnd(siteConfig) {
buildLlmsFull(siteConfig.srcDir, siteConfig.outDir)
},

title: 'Contentrain AI',
description: 'Extract, govern, and ship structured content from your codebase.',

Expand Down Expand Up @@ -70,6 +144,7 @@ export default defineConfig({
text: 'Introduction',
items: [
{ text: 'What is Contentrain AI?', link: '/getting-started' },
{ text: '2-Minute Demo', link: '/demo' },
{ text: 'Ecosystem Map', link: '/ecosystem' },
{ text: 'Core Concepts', link: '/concepts' },
{ text: 'Contentrain Studio', link: '/studio' },
Expand Down
8 changes: 4 additions & 4 deletions docs/concepts.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Core Concepts
description: "Understand the architecture: models, content kinds, domains, locales, and the agent-driven workflow"
order: 2
order: 4
category: getting-started
slug: concepts
---
Expand Down Expand Up @@ -49,7 +49,7 @@ Contentrain AI inverts the traditional CMS workflow:
- **Project setup:** `contentrain_init`, `contentrain_scaffold`
- **Content and schema writes:** `contentrain_model_save`, `contentrain_model_delete`, `contentrain_content_save`, `contentrain_content_delete`
- **Normalize:** `contentrain_scan`, `contentrain_apply`
- **Workflow and operations:** `contentrain_validate`, `contentrain_submit`, `contentrain_merge`, `contentrain_bulk`
- **Workflow and operations:** `contentrain_validate`, `contentrain_submit`, `contentrain_merge`, `contentrain_branch_list`, `contentrain_branch_delete`, `contentrain_bulk`

MCP is **deterministic infrastructure** — it doesn't make content decisions. The agent decides what to create; MCP executes it.

Expand Down Expand Up @@ -207,7 +207,7 @@ Contentrain stores everything as plain JSON and Markdown files in your Git repo.

Web projects deploy content with `git push` — no extra infrastructure needed. But non-web platforms (iOS, Android, React Native, Flutter, desktop apps, game engines, IoT) can't read from a git repo at runtime.

For these use cases, Contentrain publishes merged content to a CDN (`cdn.contentrain.io`), delivering the same structured JSON your SDK queries — just over HTTP instead of the filesystem.
For these use cases, [Contentrain Studio](/studio)'s managed CDN publishes merged content over HTTP (`GET /api/cdn/v1/{projectId}/{path}` on the Studio host), delivering the same structured JSON your SDK queries — just over HTTP instead of the filesystem. The CDN is a Studio delivery feature (Cloudflare R2-backed), not part of the MIT core; the SDK's CDN client points at it via `cdn.url` in `config.json`.

```
Git repo → merge → CDN publish → iOS/Android/Flutter fetch → typed response
Expand All @@ -217,7 +217,7 @@ This means one content source powers your website, mobile app, and any other pla

## Contentrain Studio

[Contentrain Studio](/studio) is the open-core team operations panel for Git-native structured content. Teams can self-host the AGPL core or use a managed Pro/Enterprise offering on top of the same model.
[Contentrain Studio](/studio) is the open-core team operations panel for Git-native structured content. Teams can self-host the AGPL core or use the managed plans (Starter/Pro/Enterprise) on top of the same model.

The local open-source stack gives you:

Expand Down
2 changes: 1 addition & 1 deletion docs/ecosystem.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Ecosystem Map
description: "How Contentrain AI packages and Contentrain Studio fit together across discovery, governance, collaboration, and delivery."
order: 2
order: 3
category: getting-started
slug: ecosystem
---
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,5 +282,5 @@ Want to skip setup? Start from a production-ready template with content models,
- [Framework Integration](/guides/frameworks) — Platform-specific setup patterns

::: info Contentrain Studio
[Contentrain Studio](/studio) is the open-core team operations surface for Git-native structured content. Teams can self-host the AGPL core or use a managed Pro/Enterprise offering when they want web-based collaboration, review, media, and CDN delivery on top of the same content model.
[Contentrain Studio](/studio) is the open-core team operations surface for Git-native structured content. Teams can self-host the AGPL core or use the managed plans (Starter/Pro/Enterprise) when they want web-based collaboration, review, media, and CDN delivery on top of the same content model.
:::
12 changes: 8 additions & 4 deletions docs/guides/embedding-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const transport = new StdioServerTransport()
await server.connect(transport)
```

`createServer` also accepts an options object — `createServer({ provider, projectRoot?, instructions? })` — where `instructions` overrides the MCP server instructions string advertised to clients.

This is what `contentrain serve --stdio` does internally. Every IDE that speaks MCP (Claude Code, Cursor, Windsurf) talks to this shape.

### 2. HTTP + LocalProvider (local CI, Studio-like hosting pointed at a working tree)
Expand All @@ -63,6 +65,7 @@ const handle = await startHttpMcpServer({
port: 3333,
host: '0.0.0.0',
authToken: process.env.MCP_BEARER_TOKEN,
// path: '/mcp' — custom mount path, default '/mcp'
})

// handle.url — "http://0.0.0.0:3333/mcp"
Expand Down Expand Up @@ -153,7 +156,7 @@ const handle = await startHttpMcpServerWith({

For **multi-tenant** deployments where each request targets a different project, see the **per-request resolver** section below.

Swap in `createGitLabProvider({ auth, project })` for GitLab. Self-hosted GitLab instances pass `project.host`.
Swap in `createGitLabProvider({ auth, project })` for GitLab — `project.projectId` (numeric ID or `group/name` path) is required; self-hosted GitLab instances also pass `project.host`. Both remote providers accept an optional `contentRoot` on the repo/project ref for monorepos whose `.contentrain/` lives under a prefix.

### 3a. HTTP + per-request provider resolver (multi-tenant)

Expand Down Expand Up @@ -193,8 +196,9 @@ import { buildContextChange } from '@contentrain/mcp/core/context'
import { validateProject } from '@contentrain/mcp/core/validator'
import { CONTENTRAIN_BRANCH } from '@contentrain/types'

// Throws on invalid input; soft signals come back as per-entry advisories
const plan = await planContentSave(provider, { model, entries, config, vocabulary })
if (plan.result.some(r => r.error)) throw new Error('plan invalid')
const advisories = plan.result.flatMap(r => r.advisories ?? [])

const overlay = new OverlayReader(provider, plan.changes)
const contextChange = await buildContextChange(overlay, {
Expand Down Expand Up @@ -245,7 +249,7 @@ Tools whose requirements can never be met by the session's provider are not regi
{
"error": "contentrain_validate requires local filesystem access.",
"capability_required": "localWorktree",
"hint": "This tool is unavailable when MCP is driven by a remote provider. Use a LocalProvider or the stdio transport."
"hint": "This tool is unavailable when MCP is driven by a remote provider (e.g. GitHubProvider). Use a LocalProvider or the stdio transport."
}
```

Expand Down Expand Up @@ -366,7 +370,7 @@ No HTTP, no MCP JSON-RPC — just the core primitives.

## Troubleshooting

**`ERR_PACKAGE_PATH_NOT_EXPORTED`** — you're reaching into a subpath that isn't in `package.json#exports`. Known good subpaths: `/server`, `/server/http`, `/core/config`, `/core/context`, `/core/contracts`, `/core/model-manager`, `/core/content-manager`, `/core/validator`, `/core/ops`, `/core/overlay-reader`, `/core/scanner`, `/core/graph-builder`, `/core/apply-manager`, `/core/scan-config`, `/providers/local`, `/providers/github`, `/providers/gitlab`, `/util/detect`, `/util/fs`, `/git/transaction`, `/git/branch-lifecycle`, `/templates`.
**`ERR_PACKAGE_PATH_NOT_EXPORTED`** — you're reaching into a subpath that isn't in `package.json#exports`. Known good subpaths: `/server`, `/server/http`, `/core/config`, `/core/context`, `/core/contracts`, `/core/model-manager`, `/core/content-manager`, `/core/validator`, `/core/doctor`, `/core/ops`, `/core/overlay-reader`, `/core/scanner`, `/core/graph-builder`, `/core/apply-manager`, `/core/scan-config`, `/providers/local`, `/providers/github`, `/providers/gitlab`, `/tools/annotations`, `/tools/availability`, `/testing/conformance`, `/util/detect`, `/util/fs`, `/git/transaction`, `/git/branch-lifecycle`, `/templates`.

**Stale context.json stats after a remote commit** — you forgot `OverlayReader`. `buildContextChange(provider, op)` reads the pre-change branch; wrap with `new OverlayReader(provider, plan.changes)`.

Expand Down
10 changes: 5 additions & 5 deletions docs/guides/http-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const handle = await startHttpMcpServerWith({
// handle.close() stops the server
```

The same pattern works for `createGitLabProvider` with a `GitLabProvider`. Both require their respective optional peers (`@octokit/rest`, `@gitbeaker/rest`).
The same pattern works for `createGitLabProvider({ auth, project })` — `project.projectId` (numeric ID or `group/name` path) is required, and self-hosted instances pass `project.host`. Both providers require their respective optional peers (`@octokit/rest`, `@gitbeaker/rest`).

Multi-tenant deployments (`{ resolveProvider }`) should also set `sessionFingerprint` to bind each MCP session to the tenant it was created for — follow-up requests whose fingerprint doesn't match the session's get `404` and the client re-initializes. See the [embedding guide](/guides/embedding-mcp#3a-http--per-request-provider-resolver-multi-tenant) for the full pattern.

Expand Down Expand Up @@ -117,13 +117,13 @@ An agent running on a laptop can drive a Contentrain project that lives on a ser

## Capability gates over HTTP

Not every tool works on every provider. A tool driven by an HTTP client against a remote provider returns a structured capability error when the required capability is missing:
Not every tool works on every provider. Most gating happens at registration time: tools whose requirements a remote provider cannot meet (e.g. `contentrain_scan`, which needs `astScan` + a local project root) are filtered out of `tools/list` entirely. Two cases depend on the *input*, so they surface as structured call-time errors instead — `contentrain_validate` with `fix: true` and `contentrain_apply` in reuse mode:

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

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Read-only tools (`status`, `describe`, `describe_format`, `content_list`, `valid

- **`LocalProvider`** — Day-to-day development from an IDE. Offline-capable, zero API keys, full source-tree access for normalize.
- **`GitHubProvider`** — CI-driven content operations, Studio's hosted agent, or any automation that should push directly to a GitHub repository without a clone. Requires `@octokit/rest` (optional peer dependency) and a personal access token or GitHub App installation.
- **`GitLabProvider`** — Same as above for GitLab (SaaS or self-hosted). Requires `@gitbeaker/rest` (optional peer dependency) and a PAT / OAuth / job token.
- **`GitLabProvider`** — Same as above for GitLab (SaaS or self-hosted). Requires `@gitbeaker/rest` (optional peer dependency) and an auth token: `{ type: 'pat', token }`, `{ type: 'oauth', oauthToken }`, or `{ type: 'job', jobToken }` (CI job token). The project ref needs `projectId` (numeric ID or `group/name` path); self-hosted instances add `host`.

The choice is operational, not commercial. All three providers live in MIT; enterprise features are on top of Contentrain Studio, not behind provider gates. See [Ecosystem Map](/ecosystem) for the full package-to-product relationship.

Expand Down
13 changes: 8 additions & 5 deletions docs/guides/serve-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ The server starts on `http://localhost:3333` by default.
|---|---|---|
| `--port` | `3333` | Port number |
| `--host` | `localhost` | Host address |
| `--open=false` | `true` | Prevent auto-opening browser |
| `--open=false` | `true` | Prevent auto-opening browser (or set `CONTENTRAIN_NO_OPEN=true`) |
| `--demo` | disabled | Temporary demo project (no setup required) |
| `--stdio` | disabled | MCP stdio transport for IDE integration (no web UI) |
| `--mcpHttp` | disabled | MCP HTTP transport at `POST /mcp` (secure-by-default Bearer auth) |
| `--authToken` | — | Bearer token required on non-localhost HTTP binds |
| `--mcpHttp` | disabled | MCP HTTP transport at `POST /mcp` (Bearer auth enforced when a token is set) |
| `--authToken` | — | Bearer token; the web UI refuses to start on non-localhost binds without it |

### Check if already running

Expand Down Expand Up @@ -213,7 +213,8 @@ The serve backend exposes REST endpoints at `http://localhost:3333/api/*`. Key r
| `/api/preview/merge?branch=cr/...` | GET | Side-effect-free merge preview (FF / conflicts / already-merged) |
| `/api/history` | GET | Recent contentrain-related commits |
| `/api/context` | GET | `.contentrain/context.json` |
| `/api/normalize/*` | various | Scan / plan / approve / reject / apply / approve-branch |
| `/api/normalize/*` | various | Scan / results / plan / approve / reject / apply / approve-branch / sources / file-context |
| `/api/tree` | GET | Project source-file tree (`?ext=` filter) |

Every write route validates its body through Zod schemas (`parseOrThrow`) before reaching the MCP tool layer.

Expand Down Expand Up @@ -267,7 +268,9 @@ For Studio, CI, or remote agents, serve can expose MCP over Streamable HTTP:
npx contentrain serve --mcpHttp --authToken $(openssl rand -hex 32)
```

**Secure-by-default:** binding to a non-localhost address (`0.0.0.0` / specific IPs) without `--authToken` (or `CONTENTRAIN_AUTH_TOKEN` env var) is a hard error — the HTTP MCP endpoint exposes full project write access.
When a token is supplied, every request must send `Authorization: Bearer <token>`. Binding to `0.0.0.0` without a token logs a warning but the server still starts — the endpoint exposes full project write access, so always pass a token on non-localhost binds.

**The web UI is stricter:** the default (non-MCP) serve mode **hard-errors** on any non-localhost bind without `--authToken` / `CONTENTRAIN_AUTH_TOKEN` and refuses to start (OWASP Secure-by-Default). The UI has no per-request auth today; the token requirement is a start-up gate for exposing it beyond localhost.

See the [HTTP Transport guide](/guides/http-transport) for deployment patterns.

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,5 +194,5 @@ Agent decides what to extract → MCP validates and writes → Human reviews →
No AI markup in your code. No proprietary syntax. No vendor lock-in. If you stop using Contentrain, your content files are still plain JSON in your Git repo.

::: info Contentrain Studio
[Contentrain Studio](/studio) is the open-core team web layer for Contentrain: chat-first operations, role-based collaboration, branch review, media management, and CDN delivery on top of the same Git-native content model. Teams can self-host the AGPL core or use a managed Pro/Enterprise offering.
[Contentrain Studio](/studio) is the open-core team web layer for Contentrain: chat-first operations, role-based collaboration, branch review, media management, and CDN delivery on top of the same Git-native content model. Teams can self-host the AGPL core or use the managed plans (Starter/Pro/Enterprise).
:::
Loading
Loading