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
6 changes: 5 additions & 1 deletion docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,18 @@ Some tools need more than a git provider can offer — normalize has to walk you

| Capability | LocalProvider | GitHubProvider | GitLabProvider | Tools that require it |
|---|---|---|---|---|
| `localWorktree` | ✓ | — | — | `init`, `scaffold`, `validate --fix`, `submit`, `merge`, `bulk` |
| `localWorktree` | ✓ | — | — | `validate --fix`, `submit`, `merge`, `branch_list`, `branch_delete` |
| `sourceRead` | ✓ | — | — | `apply` (extract mode) |
| `sourceWrite` | ✓ | — | — | `apply` (reuse mode) |
| `astScan` | ✓ | — | — | `scan` |
| `pushRemote` | ✓ | ✓ | ✓ | `submit` |
| `branchProtection` | — | ✓ | ✓ | merge fallback detection |
| `pullRequestFallback` | — | ✓ | ✓ | merge fallback creation |

A separate requirement — a local `projectRoot` on disk, not a capability flag — gates `init`, `scaffold`, `doctor`, and `bulk`. Tool listing is capability-aware: `tools/list` only advertises tools the resolved provider + `projectRoot` pair can satisfy, so a remote-provider session simply doesn't show the tools it couldn't run (see [MCP Tools](/packages/mcp) and `TOOL_REQUIREMENTS` in `@contentrain/mcp/tools/availability`).

The five `contentrain_media_*` tools gate on a separate optional facet, `RepoProvider.media` (an object, not a capability flag). They appear only on providers that expose a media stack — Studio MCP Cloud — and never on Local / GitHub / GitLab.

Read-only tools (`status`, `describe`, `describe_format`, `content_list`, `validate` without `--fix`) work on every provider. Write tools (`content_save`, `content_delete`, `model_save`, `model_delete`) work over any provider too — a remote provider posts the changes as a single atomic commit and always returns `action: pending-review` so Studio (or whoever orchestrates the server) drives the merge.

`content_list` with `resolve: true` requires local filesystem access today because relation hydration walks other models' content files. The reader-backed path rejects it with a descriptive error.
Expand Down
2 changes: 1 addition & 1 deletion docs/packages/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ Serves REST endpoints (`/api/status`, `/api/content`, `/api/branches`, `/api/doc
contentrain serve --stdio
```

For IDE agents. Same 19 tools, stdio transport.
For IDE agents. Uses `LocalProvider`, so it serves the 19 core tools over stdio (the `contentrain_media_*` tools need a media-capable provider such as Studio MCP Cloud).

#### MCP HTTP

Expand Down
15 changes: 13 additions & 2 deletions docs/packages/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Third-party developers can implement custom providers by implementing these inte

| Interface / Type | Purpose |
|-----------|---------|
| `RepoProvider` | Full provider contract: read, write, branch, merge, diff operations |
| `RepoProvider` | Full provider contract: read, write, branch, merge, diff operations, plus an optional `media?: MediaProvider` facet |
| `RepoReader` | Read-only interface (readFile, listDirectory, fileExists) |
| `RepoWriter` | Write interface (applyPlan for atomic commits) |
| `ProviderCapabilities` | Capability flags (localWorktree, sourceRead, sourceWrite, pushRemote, branchProtection, pullRequestFallback, astScan) |
Expand All @@ -129,10 +129,21 @@ Third-party developers can implement custom providers by implementing these inte
| `Commit` | Result of a commit operation (sha, message, author, timestamp) |
| `Branch` | Git branch metadata (name, sha, protected) |
| `FileDiff` | File change within a plan (path, status, before, after) |
| `MergeResult` | Merge outcome (merged flag, sha, pullRequestUrl, optional `sync?: SyncResult` for LocalProvider) |
| `MergeResult` | Merge outcome (merged flag, sha, pullRequestUrl, optional `sync?: SyncResult` for LocalProvider, optional `remote?` source-branch cleanup outcome) |
| `SyncResult` | Selective file sync result (synced, skipped, optional warning) |
| `CommitAuthor` | Commit author metadata (name, email) |

Media facet types (implemented by providers exposing a media stack — drives the `contentrain_media_*` tools):

| Interface / Type | Purpose |
|-----------|---------|
| `MediaProvider` | Optional `RepoProvider.media` facet: `list` / `get` / `ingest` / `update` / `delete` |
| `MediaAsset` | One asset — `id`, `path` (`media/...`), optional `url`, `mime`, `size`, `alt`, `tags`, `createdAt`, `meta` |
| `MediaListOptions` | List filters (`search`, `tag`, `limit`, `cursor`) |
| `MediaListResult` | List page (`assets`, optional `nextCursor`, `total`) |
| `MediaIngestInput` | URL-based ingest input (`url`, optional `filename`, `alt`, `tags`) |
| `MediaUpdateInput` | Metadata patch (`alt`, `tags`, `filename`) |

Pre-built capability set:

- `LOCAL_CAPABILITIES` — Full capability set for LocalProvider (all seven capabilities enabled). Exported from `@contentrain/types` for custom providers that back onto the local filesystem.
Expand Down
50 changes: 48 additions & 2 deletions docs/reference/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,25 @@ interface RepoProvider extends RepoReader, RepoWriter {
// providers, where media stays a relative path.
readonly mediaBaseUrl?: string

// Optional media facet. Present only on providers whose backend exposes a
// media stack (e.g. Studio MCP Cloud); drives the `contentrain_media_*`
// tools, which are not registered when this is absent.
readonly media?: MediaProvider

listBranches(prefix?: string): Promise<Branch[]>
createBranch(name: string, fromRef?: string): Promise<void>
deleteBranch(name: string): Promise<void>
getBranchDiff(branch: string, base?: string): Promise<FileDiff[]>
mergeBranch(branch: string, into: string): Promise<MergeResult>
// opts.removeSourceBranch deletes the source after a successful merge
// (opt-in, best-effort — reported via MergeResult.remote). Long-lived
// branches (into, contentrain, the default branch) are never deleted.
mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult>
isMerged(branch: string, into?: string): Promise<boolean>
getDefaultBranch(): Promise<string>
}
```

`MergeResult` is `{ merged, sha, pullRequestUrl, sync? }`. GitHubProvider fills `sha` on direct merges; GitLabProvider fills both `sha` and `pullRequestUrl` (merges via MR). LocalProvider populates `sync: SyncResult` to describe file syncing to the working tree. When branch protection blocks a direct merge, any provider may return `merged: false` with a `pullRequestUrl` fallback.
`MergeResult` is `{ merged, sha, pullRequestUrl, sync?, remote? }`. GitHubProvider fills `sha` on direct merges; GitLabProvider fills both `sha` and `pullRequestUrl` (merges via MR). LocalProvider populates `sync: SyncResult` to describe file syncing to the working tree. When branch protection blocks a direct merge, any provider may return `merged: false` with a `pullRequestUrl` fallback. `remote` reports the outcome of the optional post-merge source-branch cleanup (`deleted`, plus `skipped` for an expected no-op or `warning` for a non-fatal failure).

## Capabilities

Expand Down Expand Up @@ -127,6 +135,39 @@ export const LOCAL_CAPABILITIES: ProviderCapabilities = {
}
```

## Media facet (optional)

`RepoProvider.media` is an optional facet — not a capability flag. When a provider implements it (hosted providers such as Studio MCP Cloud), the `contentrain_media_*` tools are registered; when it is absent (Local / GitHub / GitLab), those tools never appear in `tools/list`. It is a deterministic passthrough to the backend's media stack — MCP makes no media decisions.

```ts
interface MediaProvider {
list(opts?: MediaListOptions): Promise<MediaListResult>
get(id: string): Promise<MediaAsset | null>
ingest(input: MediaIngestInput): Promise<MediaAsset> // URL-based; provider owns SSRF/MIME/size policy
update(id: string, patch: MediaUpdateInput): Promise<MediaAsset>
delete(id: string): Promise<void>
}

interface MediaAsset {
id: string
path: string // repo-relative storage path content fields reference (media/...)
url?: string // absolute delivery URL when resolvable (see mediaBaseUrl)
mime?: string
size?: number
alt?: string
tags?: string[]
createdAt?: string
meta?: Record<string, unknown> // provider-defined extras (dimensions, blurhash, variants)
}

interface MediaListOptions { search?: string; tag?: string; limit?: number; cursor?: string }
interface MediaListResult { assets: MediaAsset[]; nextCursor?: string; total?: number }
interface MediaIngestInput { url: string; filename?: string; alt?: string; tags?: string[] }
interface MediaUpdateInput { alt?: string; tags?: string[]; filename?: string }
```

MCP has no binary channel, so ingest is URL-based: the agent supplies a source URL and the provider fetches it server-side under its own SSRF/MIME/size policy. See [MCP Tools → Media Tools](/packages/mcp) for the tool-level reference.

## Supporting types

```ts
Expand Down Expand Up @@ -155,6 +196,11 @@ interface MergeResult {
sha: string | null
pullRequestUrl: string | null
sync?: SyncResult // Only populated by LocalProvider
remote?: { // Post-merge source-branch cleanup outcome
deleted: boolean
skipped?: string // Expected no-op (cleanup disabled, no remote, ref gone)
warning?: string // Non-fatal failure (offline, auth) — merge still succeeded
}
}

interface SyncResult {
Expand Down
Loading