@contentrain/mcp 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/index.mjs +1 -1
  2. package/dist/providers/github/index.d.mts +60 -3
  3. package/dist/providers/github/index.d.mts.map +1 -1
  4. package/dist/providers/github/index.mjs +85 -5
  5. package/dist/providers/github/index.mjs.map +1 -1
  6. package/dist/server/http/index.d.mts +63 -21
  7. package/dist/server/http/index.d.mts.map +1 -1
  8. package/dist/server/http/index.mjs +156 -45
  9. package/dist/server/http/index.mjs.map +1 -1
  10. package/dist/{server-B1OJrUQ9.mjs → server-BkhhtEE2.mjs} +2 -2
  11. package/dist/{server-B1OJrUQ9.mjs.map → server-BkhhtEE2.mjs.map} +1 -1
  12. package/dist/server.mjs +1 -1
  13. package/dist/testing/conformance.d.mts +59 -0
  14. package/dist/testing/conformance.d.mts.map +1 -0
  15. package/dist/testing/conformance.mjs +59 -0
  16. package/dist/testing/conformance.mjs.map +1 -0
  17. package/package.json +10 -5
  18. package/testing/conformance/01-new-collection-entry/expected/.contentrain/config.json +15 -0
  19. package/testing/conformance/01-new-collection-entry/expected/.contentrain/content/blog/blog/en.json +6 -0
  20. package/testing/conformance/01-new-collection-entry/expected/.contentrain/context.json +22 -0
  21. package/testing/conformance/01-new-collection-entry/expected/.contentrain/meta/blog/en.json +7 -0
  22. package/testing/conformance/01-new-collection-entry/expected/.contentrain/models/blog.json +16 -0
  23. package/testing/conformance/01-new-collection-entry/scenario.json +19 -0
  24. package/testing/conformance/01-new-collection-entry/setup/.contentrain/config.json +15 -0
  25. package/testing/conformance/01-new-collection-entry/setup/.contentrain/models/blog.json +16 -0
  26. package/testing/conformance/02-new-singleton/expected/.contentrain/config.json +15 -0
  27. package/testing/conformance/02-new-singleton/expected/.contentrain/content/marketing/hero/en.json +4 -0
  28. package/testing/conformance/02-new-singleton/expected/.contentrain/context.json +22 -0
  29. package/testing/conformance/02-new-singleton/expected/.contentrain/meta/hero/en.json +5 -0
  30. package/testing/conformance/02-new-singleton/expected/.contentrain/models/hero.json +16 -0
  31. package/testing/conformance/02-new-singleton/scenario.json +18 -0
  32. package/testing/conformance/02-new-singleton/setup/.contentrain/config.json +15 -0
  33. package/testing/conformance/02-new-singleton/setup/.contentrain/models/hero.json +16 -0
  34. package/testing/conformance/03-new-dictionary/expected/.contentrain/config.json +15 -0
  35. package/testing/conformance/03-new-dictionary/expected/.contentrain/content/marketing/ui-strings/en.json +5 -0
  36. package/testing/conformance/03-new-dictionary/expected/.contentrain/context.json +22 -0
  37. package/testing/conformance/03-new-dictionary/expected/.contentrain/meta/ui-strings/en.json +5 -0
  38. package/testing/conformance/03-new-dictionary/expected/.contentrain/models/ui-strings.json +7 -0
  39. package/testing/conformance/03-new-dictionary/scenario.json +19 -0
  40. package/testing/conformance/03-new-dictionary/setup/.contentrain/config.json +15 -0
  41. package/testing/conformance/03-new-dictionary/setup/.contentrain/models/ui-strings.json +7 -0
  42. package/testing/conformance/README.md +77 -0
  43. package/testing/conformance/scenarios.json +36 -0
package/dist/server.mjs CHANGED
@@ -12,7 +12,7 @@ import "./local-DvP4Y4T-.mjs";
12
12
  import "./detect-DL9mFmaS.mjs";
13
13
  import "./branch-lifecycle-CFVpTpNT.mjs";
14
14
  import "./annotations-Clhbw2lh.mjs";
15
- import { t as createServer } from "./server-B1OJrUQ9.mjs";
15
+ import { t as createServer } from "./server-BkhhtEE2.mjs";
16
16
  import "./validator-B0nNRFeU.mjs";
17
17
  import "./scan-config-jC6XmiqF.mjs";
18
18
  import "./graph-builder-DXv2JEp-.mjs";
@@ -0,0 +1,59 @@
1
+ //#region src/testing/conformance.d.ts
2
+ /**
3
+ * Conformance fixture loader — published as `@contentrain/mcp/testing/conformance`
4
+ * so external tools (Studio, third-party reimplementations, alt-provider
5
+ * test harnesses) can assert byte-identical output against the same
6
+ * scenarios `@contentrain/mcp` tests itself against.
7
+ *
8
+ * Each scenario directory under `testing/conformance/` is:
9
+ *
10
+ * scenario.json — { id, description, operation, input, context_tool? }
11
+ * setup/ — files seeded into a temp projectRoot before the op
12
+ * expected/ — byte-identical file tree after the op runs
13
+ *
14
+ * Consumers use the `fixturesDir` export to locate the root and the
15
+ * helpers below to iterate scenarios without hardcoding directory names.
16
+ *
17
+ * The fixtures are published as-is — no transformation, no compilation.
18
+ * They ship next to the package so consumers can read them with
19
+ * `node:fs` without additional dependencies.
20
+ */
21
+ interface ConformanceScenario {
22
+ /** Stable scenario id — e.g. `01-new-collection-entry`. */
23
+ id: string;
24
+ /** Short human-readable description pulled from `scenario.json`. */
25
+ description: string;
26
+ /** The write operation under test — `save_content`, `save_model`, `delete_content`, …. */
27
+ operation: string;
28
+ /** Tool input object — passed into the operation at runtime. */
29
+ input: Record<string, unknown>;
30
+ /** Optional MCP tool name recorded in the resulting `context.json`. */
31
+ context_tool?: string;
32
+ /** Absolute path to this scenario's directory on disk. */
33
+ dir: string;
34
+ /** Absolute path to this scenario's `setup/` tree. */
35
+ setupDir: string;
36
+ /** Absolute path to this scenario's `expected/` tree. */
37
+ expectedDir: string;
38
+ }
39
+ /**
40
+ * Resolve the conformance fixtures root. When called from the built
41
+ * package (`dist/testing/conformance.mjs`), the fixtures live at
42
+ * `../../testing/conformance`. When called from source tests
43
+ * (`src/testing/conformance.ts`), they live at
44
+ * `../../testing/conformance` as well — the layout is identical.
45
+ */
46
+ declare const fixturesDir: string;
47
+ /**
48
+ * List every scenario directory under `fixturesDir`. Returns an array
49
+ * sorted by `id` for deterministic iteration.
50
+ */
51
+ declare function listConformanceScenarios(): ConformanceScenario[];
52
+ /**
53
+ * Load a single scenario by id. Throws with a helpful message when the
54
+ * id does not resolve to a published fixture.
55
+ */
56
+ declare function loadConformanceScenario(id: string): ConformanceScenario;
57
+ //#endregion
58
+ export { ConformanceScenario, fixturesDir, listConformanceScenarios, loadConformanceScenario };
59
+ //# sourceMappingURL=conformance.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.d.mts","names":[],"sources":["../../src/testing/conformance.ts"],"mappings":";;AAwBA;;;;;;;;;;;;;;;AA4BA;;;UA5BiB,mBAAA;EA4B2E;EA1B1F,EAAA;EAgCsC;EA9BtC,WAAA;EA8B0C;EA5B1C,SAAA;EAqDc;EAnDd,KAAA,EAAO,MAAA;;EAEP,YAAA;EAiDsE;EA/CtE,GAAA;;EAEA,QAAA;;EAEA,WAAA;AAAA;;;;;;;;cAYW,WAAA;;;;;iBAMG,wBAAA,CAAA,GAA4B,mBAAA;;;;;iBAyB5B,uBAAA,CAAwB,EAAA,WAAa,mBAAA"}
@@ -0,0 +1,59 @@
1
+ import { dirname, join, resolve } from "node:path";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ /**
5
+ * Resolve the conformance fixtures root. When called from the built
6
+ * package (`dist/testing/conformance.mjs`), the fixtures live at
7
+ * `../../testing/conformance`. When called from source tests
8
+ * (`src/testing/conformance.ts`), they live at
9
+ * `../../testing/conformance` as well — the layout is identical.
10
+ */
11
+ const fixturesDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "testing", "conformance");
12
+ /**
13
+ * List every scenario directory under `fixturesDir`. Returns an array
14
+ * sorted by `id` for deterministic iteration.
15
+ */
16
+ function listConformanceScenarios() {
17
+ if (!existsSync(fixturesDir)) throw new Error(`Conformance fixtures not found at ${fixturesDir}. Is @contentrain/mcp/testing/conformance installed as a dev dependency?`);
18
+ const entries = readdirSync(fixturesDir, { withFileTypes: true });
19
+ const scenarios = [];
20
+ for (const entry of entries) {
21
+ if (!entry.isDirectory()) continue;
22
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
23
+ const dir = join(fixturesDir, entry.name);
24
+ const scenarioPath = join(dir, "scenario.json");
25
+ if (!existsSync(scenarioPath)) continue;
26
+ scenarios.push({
27
+ id: entry.name,
28
+ ...loadScenarioFile(scenarioPath),
29
+ dir,
30
+ setupDir: join(dir, "setup"),
31
+ expectedDir: join(dir, "expected")
32
+ });
33
+ }
34
+ return scenarios.toSorted((a, b) => a.id.localeCompare(b.id));
35
+ }
36
+ /**
37
+ * Load a single scenario by id. Throws with a helpful message when the
38
+ * id does not resolve to a published fixture.
39
+ */
40
+ function loadConformanceScenario(id) {
41
+ const all = listConformanceScenarios();
42
+ const match = all.find((s) => s.id === id);
43
+ if (!match) throw new Error(`Unknown conformance scenario "${id}". Known ids: ${all.map((s) => s.id).join(", ")}`);
44
+ return match;
45
+ }
46
+ function loadScenarioFile(path) {
47
+ const raw = readFileSync(path, "utf-8");
48
+ const parsed = JSON.parse(raw);
49
+ return {
50
+ description: parsed.description,
51
+ operation: parsed.operation,
52
+ input: parsed.input,
53
+ ...parsed.context_tool ? { context_tool: parsed.context_tool } : {}
54
+ };
55
+ }
56
+ //#endregion
57
+ export { fixturesDir, listConformanceScenarios, loadConformanceScenario };
58
+
59
+ //# sourceMappingURL=conformance.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.mjs","names":[],"sources":["../../src/testing/conformance.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Conformance fixture loader — published as `@contentrain/mcp/testing/conformance`\n * so external tools (Studio, third-party reimplementations, alt-provider\n * test harnesses) can assert byte-identical output against the same\n * scenarios `@contentrain/mcp` tests itself against.\n *\n * Each scenario directory under `testing/conformance/` is:\n *\n * scenario.json — { id, description, operation, input, context_tool? }\n * setup/ — files seeded into a temp projectRoot before the op\n * expected/ — byte-identical file tree after the op runs\n *\n * Consumers use the `fixturesDir` export to locate the root and the\n * helpers below to iterate scenarios without hardcoding directory names.\n *\n * The fixtures are published as-is — no transformation, no compilation.\n * They ship next to the package so consumers can read them with\n * `node:fs` without additional dependencies.\n */\n\nexport interface ConformanceScenario {\n /** Stable scenario id — e.g. `01-new-collection-entry`. */\n id: string\n /** Short human-readable description pulled from `scenario.json`. */\n description: string\n /** The write operation under test — `save_content`, `save_model`, `delete_content`, …. */\n operation: string\n /** Tool input object — passed into the operation at runtime. */\n input: Record<string, unknown>\n /** Optional MCP tool name recorded in the resulting `context.json`. */\n context_tool?: string\n /** Absolute path to this scenario's directory on disk. */\n dir: string\n /** Absolute path to this scenario's `setup/` tree. */\n setupDir: string\n /** Absolute path to this scenario's `expected/` tree. */\n expectedDir: string\n}\n\nconst MODULE_DIR = dirname(fileURLToPath(import.meta.url))\n\n/**\n * Resolve the conformance fixtures root. When called from the built\n * package (`dist/testing/conformance.mjs`), the fixtures live at\n * `../../testing/conformance`. When called from source tests\n * (`src/testing/conformance.ts`), they live at\n * `../../testing/conformance` as well — the layout is identical.\n */\nexport const fixturesDir: string = resolve(MODULE_DIR, '..', '..', 'testing', 'conformance')\n\n/**\n * List every scenario directory under `fixturesDir`. Returns an array\n * sorted by `id` for deterministic iteration.\n */\nexport function listConformanceScenarios(): ConformanceScenario[] {\n if (!existsSync(fixturesDir)) {\n throw new Error(\n `Conformance fixtures not found at ${fixturesDir}. `\n + 'Is @contentrain/mcp/testing/conformance installed as a dev dependency?',\n )\n }\n\n const entries = readdirSync(fixturesDir, { withFileTypes: true })\n const scenarios: ConformanceScenario[] = []\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue\n const dir = join(fixturesDir, entry.name)\n const scenarioPath = join(dir, 'scenario.json')\n if (!existsSync(scenarioPath)) continue\n scenarios.push({ id: entry.name, ...loadScenarioFile(scenarioPath), dir, setupDir: join(dir, 'setup'), expectedDir: join(dir, 'expected') })\n }\n return scenarios.toSorted((a, b) => a.id.localeCompare(b.id))\n}\n\n/**\n * Load a single scenario by id. Throws with a helpful message when the\n * id does not resolve to a published fixture.\n */\nexport function loadConformanceScenario(id: string): ConformanceScenario {\n const all = listConformanceScenarios()\n const match = all.find(s => s.id === id)\n if (!match) {\n throw new Error(`Unknown conformance scenario \"${id}\". Known ids: ${all.map(s => s.id).join(', ')}`)\n }\n return match\n}\n\nfunction loadScenarioFile(path: string): Omit<ConformanceScenario, 'id' | 'dir' | 'setupDir' | 'expectedDir'> {\n const raw = readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw) as {\n description: string\n operation: string\n input: Record<string, unknown>\n context_tool?: string\n }\n return {\n description: parsed.description,\n operation: parsed.operation,\n input: parsed.input,\n ...(parsed.context_tool ? { context_tool: parsed.context_tool } : {}),\n }\n}\n"],"mappings":";;;;;;;;;;AAoDA,MAAa,cAAsB,QAThB,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EASH,MAAM,MAAM,WAAW,cAAc;;;;;AAM5F,SAAgB,2BAAkD;AAChE,KAAI,CAAC,WAAW,YAAY,CAC1B,OAAM,IAAI,MACR,qCAAqC,YAAY,0EAElD;CAGH,MAAM,UAAU,YAAY,aAAa,EAAE,eAAe,MAAM,CAAC;CACjE,MAAM,YAAmC,EAAE;AAC3C,MAAK,MAAM,SAAS,SAAS;AAC3B,MAAI,CAAC,MAAM,aAAa,CAAE;AAC1B,MAAI,MAAM,KAAK,WAAW,IAAI,IAAI,MAAM,KAAK,WAAW,IAAI,CAAE;EAC9D,MAAM,MAAM,KAAK,aAAa,MAAM,KAAK;EACzC,MAAM,eAAe,KAAK,KAAK,gBAAgB;AAC/C,MAAI,CAAC,WAAW,aAAa,CAAE;AAC/B,YAAU,KAAK;GAAE,IAAI,MAAM;GAAM,GAAG,iBAAiB,aAAa;GAAE;GAAK,UAAU,KAAK,KAAK,QAAQ;GAAE,aAAa,KAAK,KAAK,WAAW;GAAE,CAAC;;AAE9I,QAAO,UAAU,UAAU,GAAG,MAAM,EAAE,GAAG,cAAc,EAAE,GAAG,CAAC;;;;;;AAO/D,SAAgB,wBAAwB,IAAiC;CACvE,MAAM,MAAM,0BAA0B;CACtC,MAAM,QAAQ,IAAI,MAAK,MAAK,EAAE,OAAO,GAAG;AACxC,KAAI,CAAC,MACH,OAAM,IAAI,MAAM,iCAAiC,GAAG,gBAAgB,IAAI,KAAI,MAAK,EAAE,GAAG,CAAC,KAAK,KAAK,GAAG;AAEtG,QAAO;;AAGT,SAAS,iBAAiB,MAAoF;CAC5G,MAAM,MAAM,aAAa,MAAM,QAAQ;CACvC,MAAM,SAAS,KAAK,MAAM,IAAI;AAM9B,QAAO;EACL,aAAa,OAAO;EACpB,WAAW,OAAO;EAClB,OAAO,OAAO;EACd,GAAI,OAAO,eAAe,EAAE,cAAc,OAAO,cAAc,GAAG,EAAE;EACrE"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@contentrain/mcp",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "mcpName": "io.github.Contentrain/contentrain",
5
5
  "license": "MIT",
6
- "description": "Local-first MCP server for AI-generated content governance — 13 deterministic tools for any platform",
6
+ "description": "Local-first MCP server for AI-generated content governance — 17 deterministic tools, stdio + HTTP transports, Local / GitHub / GitLab providers",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -120,6 +120,10 @@
120
120
  "types": "./dist/tools/annotations.d.mts",
121
121
  "import": "./dist/tools/annotations.mjs"
122
122
  },
123
+ "./testing/conformance": {
124
+ "types": "./dist/testing/conformance.d.mts",
125
+ "import": "./dist/testing/conformance.mjs"
126
+ },
123
127
  "./templates": {
124
128
  "types": "./dist/templates/index.d.mts",
125
129
  "import": "./dist/templates/index.mjs"
@@ -140,7 +144,8 @@
140
144
  "main": "./dist/index.mjs",
141
145
  "types": "./dist/index.d.mts",
142
146
  "files": [
143
- "dist"
147
+ "dist",
148
+ "testing"
144
149
  ],
145
150
  "dependencies": {
146
151
  "@modelcontextprotocol/sdk": "^1.12.0",
@@ -177,8 +182,8 @@
177
182
  "vitest": "^3.0.0"
178
183
  },
179
184
  "scripts": {
180
- "build": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/tools/annotations.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript",
181
- "dev": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/tools/annotations.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript --watch",
185
+ "build": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/tools/annotations.ts src/testing/conformance.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript",
186
+ "dev": "tsdown src/index.ts src/server.ts src/core/config.ts src/core/context.ts src/core/model-manager.ts src/core/content-manager.ts src/core/meta-manager.ts src/core/validator/index.ts src/core/scanner.ts src/core/scan-config.ts src/core/doctor.ts src/core/graph-builder.ts src/core/apply-manager.ts src/util/detect.ts src/util/fs.ts src/util/id.ts src/git/transaction.ts src/git/branch-lifecycle.ts src/tools/annotations.ts src/testing/conformance.ts src/templates/index.ts src/core/contracts/index.ts src/core/ops/index.ts src/core/overlay-reader.ts src/providers/local/index.ts src/providers/github/index.ts src/providers/gitlab/index.ts src/server/http/index.ts --format esm --dts --external typescript --watch",
182
187
  "test": "vitest run",
183
188
  "typecheck": "tsc --noEmit",
184
189
  "clean": "rm -rf dist"
@@ -0,0 +1,15 @@
1
+ {
2
+ "domains": [
3
+ "blog"
4
+ ],
5
+ "locales": {
6
+ "default": "en",
7
+ "supported": [
8
+ "en",
9
+ "tr"
10
+ ]
11
+ },
12
+ "stack": "vue-nuxt",
13
+ "version": 1,
14
+ "workflow": "auto-merge"
15
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "abc123def456": {
3
+ "body": "First post content",
4
+ "title": "Hello World"
5
+ }
6
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "lastOperation": {
3
+ "entries": [
4
+ "abc123def456"
5
+ ],
6
+ "locale": "en",
7
+ "model": "blog",
8
+ "source": "mcp-local",
9
+ "timestamp": "2026-01-01T00:00:00.000Z",
10
+ "tool": "contentrain_content_save"
11
+ },
12
+ "stats": {
13
+ "entries": 1,
14
+ "lastSync": "2026-01-01T00:00:00.000Z",
15
+ "locales": [
16
+ "en",
17
+ "tr"
18
+ ],
19
+ "models": 1
20
+ },
21
+ "version": "1"
22
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "abc123def456": {
3
+ "source": "agent",
4
+ "status": "draft",
5
+ "updated_by": "contentrain-mcp"
6
+ }
7
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "domain": "blog",
3
+ "fields": {
4
+ "body": {
5
+ "type": "text"
6
+ },
7
+ "title": {
8
+ "required": true,
9
+ "type": "string"
10
+ }
11
+ },
12
+ "i18n": true,
13
+ "id": "blog",
14
+ "kind": "collection",
15
+ "name": "Blog"
16
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "context_tool": "contentrain_content_save",
3
+ "description": "Save a new collection entry for a model",
4
+ "id": "01-new-collection-entry",
5
+ "input": {
6
+ "entries": [
7
+ {
8
+ "data": {
9
+ "body": "First post content",
10
+ "title": "Hello World"
11
+ },
12
+ "id": "abc123def456",
13
+ "locale": "en"
14
+ }
15
+ ],
16
+ "model": "blog"
17
+ },
18
+ "operation": "save_content"
19
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "domains": [
3
+ "blog"
4
+ ],
5
+ "locales": {
6
+ "default": "en",
7
+ "supported": [
8
+ "en",
9
+ "tr"
10
+ ]
11
+ },
12
+ "stack": "vue-nuxt",
13
+ "version": 1,
14
+ "workflow": "auto-merge"
15
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "domain": "blog",
3
+ "fields": {
4
+ "body": {
5
+ "type": "text"
6
+ },
7
+ "title": {
8
+ "required": true,
9
+ "type": "string"
10
+ }
11
+ },
12
+ "i18n": true,
13
+ "id": "blog",
14
+ "kind": "collection",
15
+ "name": "Blog"
16
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "domains": [
3
+ "marketing"
4
+ ],
5
+ "locales": {
6
+ "default": "en",
7
+ "supported": [
8
+ "en",
9
+ "tr"
10
+ ]
11
+ },
12
+ "stack": "vue-nuxt",
13
+ "version": 1,
14
+ "workflow": "auto-merge"
15
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "subtitle": "Build the web with AI agents",
3
+ "title": "Welcome to Contentrain"
4
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "lastOperation": {
3
+ "entries": [
4
+ "en"
5
+ ],
6
+ "locale": "en",
7
+ "model": "hero",
8
+ "source": "mcp-local",
9
+ "timestamp": "2026-01-01T00:00:00.000Z",
10
+ "tool": "contentrain_content_save"
11
+ },
12
+ "stats": {
13
+ "entries": 1,
14
+ "lastSync": "2026-01-01T00:00:00.000Z",
15
+ "locales": [
16
+ "en",
17
+ "tr"
18
+ ],
19
+ "models": 1
20
+ },
21
+ "version": "1"
22
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "source": "agent",
3
+ "status": "draft",
4
+ "updated_by": "contentrain-mcp"
5
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "domain": "marketing",
3
+ "fields": {
4
+ "subtitle": {
5
+ "type": "text"
6
+ },
7
+ "title": {
8
+ "required": true,
9
+ "type": "string"
10
+ }
11
+ },
12
+ "i18n": true,
13
+ "id": "hero",
14
+ "kind": "singleton",
15
+ "name": "Hero"
16
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "context_tool": "contentrain_content_save",
3
+ "description": "Save a new singleton content block",
4
+ "id": "02-new-singleton",
5
+ "input": {
6
+ "entries": [
7
+ {
8
+ "data": {
9
+ "subtitle": "Build the web with AI agents",
10
+ "title": "Welcome to Contentrain"
11
+ },
12
+ "locale": "en"
13
+ }
14
+ ],
15
+ "model": "hero"
16
+ },
17
+ "operation": "save_content"
18
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "domains": [
3
+ "marketing"
4
+ ],
5
+ "locales": {
6
+ "default": "en",
7
+ "supported": [
8
+ "en",
9
+ "tr"
10
+ ]
11
+ },
12
+ "stack": "vue-nuxt",
13
+ "version": 1,
14
+ "workflow": "auto-merge"
15
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "domain": "marketing",
3
+ "fields": {
4
+ "subtitle": {
5
+ "type": "text"
6
+ },
7
+ "title": {
8
+ "required": true,
9
+ "type": "string"
10
+ }
11
+ },
12
+ "i18n": true,
13
+ "id": "hero",
14
+ "kind": "singleton",
15
+ "name": "Hero"
16
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "domains": [
3
+ "marketing"
4
+ ],
5
+ "locales": {
6
+ "default": "en",
7
+ "supported": [
8
+ "en",
9
+ "tr"
10
+ ]
11
+ },
12
+ "stack": "vue-nuxt",
13
+ "version": 1,
14
+ "workflow": "auto-merge"
15
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "hero.cta": "Get started",
3
+ "hero.subtitle": "Build the web with AI agents",
4
+ "hero.title": "Welcome to Contentrain"
5
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "lastOperation": {
3
+ "entries": [
4
+ "en"
5
+ ],
6
+ "locale": "en",
7
+ "model": "ui-strings",
8
+ "source": "mcp-local",
9
+ "timestamp": "2026-01-01T00:00:00.000Z",
10
+ "tool": "contentrain_content_save"
11
+ },
12
+ "stats": {
13
+ "entries": 1,
14
+ "lastSync": "2026-01-01T00:00:00.000Z",
15
+ "locales": [
16
+ "en",
17
+ "tr"
18
+ ],
19
+ "models": 1
20
+ },
21
+ "version": "1"
22
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "source": "agent",
3
+ "status": "draft",
4
+ "updated_by": "contentrain-mcp"
5
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "domain": "marketing",
3
+ "i18n": true,
4
+ "id": "ui-strings",
5
+ "kind": "dictionary",
6
+ "name": "UI Strings"
7
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "context_tool": "contentrain_content_save",
3
+ "description": "Create a dictionary with multiple string keys",
4
+ "id": "03-new-dictionary",
5
+ "input": {
6
+ "entries": [
7
+ {
8
+ "data": {
9
+ "hero.cta": "Get started",
10
+ "hero.subtitle": "Build the web with AI agents",
11
+ "hero.title": "Welcome to Contentrain"
12
+ },
13
+ "locale": "en"
14
+ }
15
+ ],
16
+ "model": "ui-strings"
17
+ },
18
+ "operation": "save_content"
19
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "domains": [
3
+ "marketing"
4
+ ],
5
+ "locales": {
6
+ "default": "en",
7
+ "supported": [
8
+ "en",
9
+ "tr"
10
+ ]
11
+ },
12
+ "stack": "vue-nuxt",
13
+ "version": 1,
14
+ "workflow": "auto-merge"
15
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "domain": "marketing",
3
+ "i18n": true,
4
+ "id": "ui-strings",
5
+ "kind": "dictionary",
6
+ "name": "UI Strings"
7
+ }
@@ -0,0 +1,77 @@
1
+ # Conformance Fixtures
2
+
3
+ Byte-identical conformance test suite for content operations. Each fixture describes an operation and its expected on-disk file tree after execution. Used by `packages/mcp/tests/conformance.test.ts`.
4
+
5
+ **Purpose** — guarantee byte-level parity between providers (LocalProvider today; GitHubProvider + GitLabProvider in later phases) for the same operation. Any silent drift in serialization, meta generation, context.json, or path resolution shows up as a failed fixture.
6
+
7
+ ## Directory layout
8
+
9
+ ```
10
+ conformance/
11
+ ├── README.md — this file
12
+ ├── scenarios.json — human-readable index
13
+ ├── 01-new-collection-entry/
14
+ │ ├── scenario.json — input spec
15
+ │ ├── setup/ — files copied into temp projectRoot before op runs (optional)
16
+ │ └── expected/ — byte-identical file tree after op (GENERATED, committed)
17
+ ├── 02-new-singleton/
18
+ │ └── ...
19
+ └── 03-new-dictionary/
20
+ └── ...
21
+ ```
22
+
23
+ ## Scenario schema
24
+
25
+ ```jsonc
26
+ {
27
+ "id": "01-new-collection-entry",
28
+ "description": "Save a new collection entry",
29
+ "operation": "save_content", // save_content | save_model | delete_content
30
+ "context_tool": "contentrain_content_save", // tool name recorded in context.json (optional)
31
+ "input": { // operation-specific params
32
+ "model": "blog",
33
+ "entries": [
34
+ { "id": "...", "locale": "en", "data": { ... } }
35
+ ]
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## Determinism rules
41
+
42
+ The harness fixes non-deterministic sources so output is reproducible:
43
+
44
+ - `Date.now()` / `new Date()` returns `2026-01-01T00:00:00.000Z` (vitest fake timers)
45
+ - `CONTENTRAIN_SOURCE=mcp-local` env var is set
46
+ - `Math.random()` is NOT stubbed globally — scenarios that need determinism must pass explicit entry IDs instead of relying on `generateEntryId()`
47
+ - Git is not initialized — core write functions don't require it
48
+
49
+ ## Regenerating `expected/`
50
+
51
+ When an intentional behavior change lands (e.g., schema bump, new default field), regenerate the expected fixtures and review the diff:
52
+
53
+ ```bash
54
+ GENERATE_FIXTURES=1 pnpm --filter @contentrain/mcp test -- conformance
55
+ ```
56
+
57
+ Then `git diff` the `expected/` directories. Each byte difference must be a conscious decision.
58
+
59
+ ## Adding a new fixture
60
+
61
+ 1. `mkdir NN-short-name/setup/.contentrain`
62
+ 2. Write `scenario.json` at the fixture root
63
+ 3. Optionally add setup files under `setup/`
64
+ 4. `GENERATE_FIXTURES=1 pnpm --filter @contentrain/mcp test -- conformance` to populate `expected/`
65
+ 5. Review generated `expected/` files manually — they become the contract
66
+ 6. Commit both `setup/` and `expected/`
67
+
68
+ ## Provider matrix (future)
69
+
70
+ Today the harness runs scenarios against the current MCP write path (direct fs + `writeContent` + `writeContext`). Post-refactor phases will add:
71
+
72
+ - Phase 2 — run through plan/apply API, assert `FileChange[]` → same `expected/`
73
+ - Phase 3 — LocalProvider.applyPlan → same `expected/`
74
+ - Phase 5 — GitHubProvider.applyPlan (mocked) → same `expected/`
75
+ - Phase 8 — GitLab/Bitbucket providers → same `expected/`
76
+
77
+ The fixtures themselves never change between providers. Only the runner wiring does.