@contentrain/mcp 1.2.1 → 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 (183) hide show
  1. package/README.md +93 -25
  2. package/dist/annotations-Clhbw2lh.mjs +126 -0
  3. package/dist/annotations-Clhbw2lh.mjs.map +1 -0
  4. package/dist/{apply-manager-BkPMfpsN.mjs → apply-manager-D5ym5Jba.mjs} +7 -7
  5. package/dist/{apply-manager-BkPMfpsN.mjs.map → apply-manager-D5ym5Jba.mjs.map} +1 -1
  6. package/dist/{astro-parser-BJ-54JGe.mjs → astro-parser-B-nLl0zE.mjs} +3 -3
  7. package/dist/{astro-parser-BJ-54JGe.mjs.map → astro-parser-B-nLl0zE.mjs.map} +1 -1
  8. package/dist/{branch-lifecycle-wGJg25LT.mjs → branch-lifecycle-CFVpTpNT.mjs} +33 -3
  9. package/dist/{branch-lifecycle-wGJg25LT.mjs.map → branch-lifecycle-CFVpTpNT.mjs.map} +1 -1
  10. package/dist/{compiler-rIC4ETHv.mjs → compiler-k4yttlzM.mjs} +2 -2
  11. package/dist/{compiler-rIC4ETHv.mjs.map → compiler-k4yttlzM.mjs.map} +1 -1
  12. package/dist/{compiler-sfc.cjs-BbD04rn_.mjs → compiler-sfc.cjs-D0d_OicF.mjs} +2 -2
  13. package/dist/{compiler-sfc.cjs-BbD04rn_.mjs.map → compiler-sfc.cjs-D0d_OicF.mjs.map} +1 -1
  14. package/dist/config-BIl5Ybj0.mjs +40 -0
  15. package/dist/config-BIl5Ybj0.mjs.map +1 -0
  16. package/dist/content-manager-DqDLSNQ-.d.mts +63 -0
  17. package/dist/content-manager-DqDLSNQ-.d.mts.map +1 -0
  18. package/dist/context-J0OU9Nfp.mjs +102 -0
  19. package/dist/context-J0OU9Nfp.mjs.map +1 -0
  20. package/dist/contracts-DfL0BfrD.mjs +2 -0
  21. package/dist/core/apply-manager.mjs +10 -10
  22. package/dist/core/config.d.mts +13 -0
  23. package/dist/core/config.d.mts.map +1 -1
  24. package/dist/core/config.mjs +2 -3
  25. package/dist/core/content-manager.d.mts +2 -45
  26. package/dist/core/content-manager.mjs +3 -4
  27. package/dist/core/context.d.mts +32 -2
  28. package/dist/core/context.d.mts.map +1 -1
  29. package/dist/core/context.mjs +7 -7
  30. package/dist/core/contracts/index.d.mts +2 -0
  31. package/dist/core/contracts/index.mjs +2 -0
  32. package/dist/core/doctor.d.mts +76 -0
  33. package/dist/core/doctor.d.mts.map +1 -0
  34. package/dist/core/doctor.mjs +8 -0
  35. package/dist/core/graph-builder.mjs +3 -4
  36. package/dist/core/meta-manager.mjs +2 -3
  37. package/dist/core/model-manager.d.mts +34 -1
  38. package/dist/core/model-manager.d.mts.map +1 -1
  39. package/dist/core/model-manager.mjs +3 -4
  40. package/dist/core/ops/index.d.mts +137 -0
  41. package/dist/core/ops/index.d.mts.map +1 -0
  42. package/dist/core/ops/index.mjs +4 -0
  43. package/dist/core/overlay-reader.d.mts +42 -0
  44. package/dist/core/overlay-reader.d.mts.map +1 -0
  45. package/dist/core/overlay-reader.mjs +2 -0
  46. package/dist/core/scan-config.mjs +2 -3
  47. package/dist/core/scanner.mjs +4 -5
  48. package/dist/core/validator/index.d.mts +129 -0
  49. package/dist/core/validator/index.d.mts.map +1 -0
  50. package/dist/core/validator/index.mjs +6 -0
  51. package/dist/{detect-DinOQAmK.mjs → detect-DL9mFmaS.mjs} +2 -2
  52. package/dist/{detect-DinOQAmK.mjs.map → detect-DL9mFmaS.mjs.map} +1 -1
  53. package/dist/doctor-CgBoT6h1.mjs +338 -0
  54. package/dist/doctor-CgBoT6h1.mjs.map +1 -0
  55. package/dist/{fs-BnIQ4dKj.mjs → fs-DLbVB-Ek.mjs} +2 -2
  56. package/dist/fs-DLbVB-Ek.mjs.map +1 -0
  57. package/dist/git/branch-lifecycle.d.mts +30 -1
  58. package/dist/git/branch-lifecycle.d.mts.map +1 -1
  59. package/dist/git/branch-lifecycle.mjs +4 -5
  60. package/dist/git/transaction.d.mts +2 -2
  61. package/dist/git/transaction.d.mts.map +1 -1
  62. package/dist/git/transaction.mjs +8 -8
  63. package/dist/{graph-builder-tM7zj-SX.mjs → graph-builder-DXv2JEp-.mjs} +3 -3
  64. package/dist/{graph-builder-tM7zj-SX.mjs.map → graph-builder-DXv2JEp-.mjs.map} +1 -1
  65. package/dist/{id-DV_T9Ic8.mjs → id-CIO9s0x_.mjs} +1 -1
  66. package/dist/{id-DV_T9Ic8.mjs.map → id-CIO9s0x_.mjs.map} +1 -1
  67. package/dist/index-D0wQJSgs.d.mts +2 -0
  68. package/dist/index.mjs +22 -17
  69. package/dist/index.mjs.map +1 -1
  70. package/dist/local-DvP4Y4T-.mjs +182 -0
  71. package/dist/local-DvP4Y4T-.mjs.map +1 -0
  72. package/dist/{meta-manager--0IwAPCl.mjs → meta-manager-C900rEyG.mjs} +2 -2
  73. package/dist/{meta-manager--0IwAPCl.mjs.map → meta-manager-C900rEyG.mjs.map} +1 -1
  74. package/dist/{model-manager-DW_fOGRM.mjs → model-manager-BhLsUgaB.mjs} +241 -49
  75. package/dist/model-manager-BhLsUgaB.mjs.map +1 -0
  76. package/dist/{node-Cbq5eR5i.mjs → node-DN9hZeil.mjs} +1 -1
  77. package/dist/{node-Cbq5eR5i.mjs.map → node-DN9hZeil.mjs.map} +1 -1
  78. package/dist/ops-DjEWi5Wa.mjs +486 -0
  79. package/dist/ops-DjEWi5Wa.mjs.map +1 -0
  80. package/dist/overlay-reader-DOdyDRIv.mjs +74 -0
  81. package/dist/overlay-reader-DOdyDRIv.mjs.map +1 -0
  82. package/dist/paths-C3xNyc7z.mjs +52 -0
  83. package/dist/paths-C3xNyc7z.mjs.map +1 -0
  84. package/dist/paths-CYl5vCBD.mjs +54 -0
  85. package/dist/paths-CYl5vCBD.mjs.map +1 -0
  86. package/dist/providers/github/index.d.mts +201 -0
  87. package/dist/providers/github/index.d.mts.map +1 -0
  88. package/dist/providers/github/index.mjs +494 -0
  89. package/dist/providers/github/index.mjs.map +1 -0
  90. package/dist/providers/gitlab/index.d.mts +158 -0
  91. package/dist/providers/gitlab/index.d.mts.map +1 -0
  92. package/dist/providers/gitlab/index.mjs +361 -0
  93. package/dist/providers/gitlab/index.mjs.map +1 -0
  94. package/dist/providers/local/index.d.mts +106 -0
  95. package/dist/providers/local/index.d.mts.map +1 -0
  96. package/dist/providers/local/index.mjs +12 -0
  97. package/dist/{scan-config-QgnJ1-52.mjs → scan-config-jC6XmiqF.mjs} +2 -2
  98. package/dist/{scan-config-QgnJ1-52.mjs.map → scan-config-jC6XmiqF.mjs.map} +1 -1
  99. package/dist/{scanner-DUoXnmtI.mjs → scanner-DKMwc03k.mjs} +7 -7
  100. package/dist/{scanner-DUoXnmtI.mjs.map → scanner-DKMwc03k.mjs.map} +1 -1
  101. package/dist/serialization-B1CEzR4H.mjs +2 -0
  102. package/dist/server/http/index.d.mts +107 -0
  103. package/dist/server/http/index.d.mts.map +1 -0
  104. package/dist/server/http/index.mjs +248 -0
  105. package/dist/server/http/index.mjs.map +1 -0
  106. package/dist/{server-Dtq8vqbK.mjs → server-BkhhtEE2.mjs} +486 -355
  107. package/dist/server-BkhhtEE2.mjs.map +1 -0
  108. package/dist/server-WqGx6ait.d.mts +50 -0
  109. package/dist/server-WqGx6ait.d.mts.map +1 -0
  110. package/dist/server.d.mts +2 -7
  111. package/dist/server.mjs +22 -17
  112. package/dist/{source-map-generator-DpO-ejrt.mjs → source-map-generator-BWDva49I.mjs} +1 -1
  113. package/dist/{source-map-generator-DpO-ejrt.mjs.map → source-map-generator-BWDva49I.mjs.map} +1 -1
  114. package/dist/{svelte-parser-1bBcMx3T.mjs → svelte-parser-Dr9mmZuk.mjs} +3 -3
  115. package/dist/{svelte-parser-1bBcMx3T.mjs.map → svelte-parser-Dr9mmZuk.mjs.map} +1 -1
  116. package/dist/templates/index.mjs +1 -1
  117. package/dist/{templates-bRPzYq4M.mjs → templates-VrLVqfe5.mjs} +1 -1
  118. package/dist/{templates-bRPzYq4M.mjs.map → templates-VrLVqfe5.mjs.map} +1 -1
  119. package/dist/testing/conformance.d.mts +59 -0
  120. package/dist/testing/conformance.d.mts.map +1 -0
  121. package/dist/testing/conformance.mjs +59 -0
  122. package/dist/testing/conformance.mjs.map +1 -0
  123. package/dist/tools/annotations.d.mts +24 -0
  124. package/dist/tools/annotations.d.mts.map +1 -0
  125. package/dist/tools/annotations.mjs +2 -0
  126. package/dist/{transaction-C_Pvux78.mjs → transaction-DgkNgWSI.mjs} +61 -32
  127. package/dist/transaction-DgkNgWSI.mjs.map +1 -0
  128. package/dist/tsx-parser-BOMZE_yQ.mjs +2 -0
  129. package/dist/{tsx-parser-BWiMGAIR.mjs → tsx-parser-CQc6nr-o.mjs} +1 -1
  130. package/dist/{tsx-parser-BWiMGAIR.mjs.map → tsx-parser-CQc6nr-o.mjs.map} +1 -1
  131. package/dist/util/detect.mjs +2 -3
  132. package/dist/util/fs.mjs +1 -2
  133. package/dist/util/id.mjs +1 -1
  134. package/dist/validator-B0nNRFeU.mjs +906 -0
  135. package/dist/validator-B0nNRFeU.mjs.map +1 -0
  136. package/dist/{vue-parser-DTPTN3Yb.mjs → vue-parser-COgjArqv.mjs} +3 -3
  137. package/dist/{vue-parser-DTPTN3Yb.mjs.map → vue-parser-COgjArqv.mjs.map} +1 -1
  138. package/package.json +82 -23
  139. package/testing/conformance/01-new-collection-entry/expected/.contentrain/config.json +15 -0
  140. package/testing/conformance/01-new-collection-entry/expected/.contentrain/content/blog/blog/en.json +6 -0
  141. package/testing/conformance/01-new-collection-entry/expected/.contentrain/context.json +22 -0
  142. package/testing/conformance/01-new-collection-entry/expected/.contentrain/meta/blog/en.json +7 -0
  143. package/testing/conformance/01-new-collection-entry/expected/.contentrain/models/blog.json +16 -0
  144. package/testing/conformance/01-new-collection-entry/scenario.json +19 -0
  145. package/testing/conformance/01-new-collection-entry/setup/.contentrain/config.json +15 -0
  146. package/testing/conformance/01-new-collection-entry/setup/.contentrain/models/blog.json +16 -0
  147. package/testing/conformance/02-new-singleton/expected/.contentrain/config.json +15 -0
  148. package/testing/conformance/02-new-singleton/expected/.contentrain/content/marketing/hero/en.json +4 -0
  149. package/testing/conformance/02-new-singleton/expected/.contentrain/context.json +22 -0
  150. package/testing/conformance/02-new-singleton/expected/.contentrain/meta/hero/en.json +5 -0
  151. package/testing/conformance/02-new-singleton/expected/.contentrain/models/hero.json +16 -0
  152. package/testing/conformance/02-new-singleton/scenario.json +18 -0
  153. package/testing/conformance/02-new-singleton/setup/.contentrain/config.json +15 -0
  154. package/testing/conformance/02-new-singleton/setup/.contentrain/models/hero.json +16 -0
  155. package/testing/conformance/03-new-dictionary/expected/.contentrain/config.json +15 -0
  156. package/testing/conformance/03-new-dictionary/expected/.contentrain/content/marketing/ui-strings/en.json +5 -0
  157. package/testing/conformance/03-new-dictionary/expected/.contentrain/context.json +22 -0
  158. package/testing/conformance/03-new-dictionary/expected/.contentrain/meta/ui-strings/en.json +5 -0
  159. package/testing/conformance/03-new-dictionary/expected/.contentrain/models/ui-strings.json +7 -0
  160. package/testing/conformance/03-new-dictionary/scenario.json +19 -0
  161. package/testing/conformance/03-new-dictionary/setup/.contentrain/config.json +15 -0
  162. package/testing/conformance/03-new-dictionary/setup/.contentrain/models/ui-strings.json +7 -0
  163. package/testing/conformance/README.md +77 -0
  164. package/testing/conformance/scenarios.json +36 -0
  165. package/dist/config-B5kc2oeV.mjs +0 -27
  166. package/dist/config-B5kc2oeV.mjs.map +0 -1
  167. package/dist/context-DIDt_TxX.mjs +0 -46
  168. package/dist/context-DIDt_TxX.mjs.map +0 -1
  169. package/dist/core/content-manager.d.mts.map +0 -1
  170. package/dist/core/validator.d.mts +0 -23
  171. package/dist/core/validator.d.mts.map +0 -1
  172. package/dist/core/validator.mjs +0 -7
  173. package/dist/fs-BnIQ4dKj.mjs.map +0 -1
  174. package/dist/model-manager-DW_fOGRM.mjs.map +0 -1
  175. package/dist/serializer-CnHTtmc1.mjs +0 -2
  176. package/dist/server-Dtq8vqbK.mjs.map +0 -1
  177. package/dist/server.d.mts.map +0 -1
  178. package/dist/transaction-C_Pvux78.mjs.map +0 -1
  179. package/dist/tsx-parser-BFLv292Y.mjs +0 -2
  180. package/dist/util/serializer.d.mts +0 -2
  181. package/dist/util/serializer.mjs +0 -2
  182. package/dist/validator-SQYUV2JP.mjs +0 -581
  183. package/dist/validator-SQYUV2JP.mjs.map +0 -1
@@ -1,145 +1,47 @@
1
- import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-BnIQ4dKj.mjs";
2
- import { n as readVocabulary, t as readConfig } from "./config-B5kc2oeV.mjs";
3
- import { n as readMeta, r as writeMeta } from "./meta-manager--0IwAPCl.mjs";
4
- import { S as writeContent, _ as resolveMdFilePath, a as fieldDefZodSchema, c as validateModelDefinition, d as listContent, g as resolveLocaleStrategy, h as resolveJsonFilePath, i as deleteModel, l as writeModel, m as resolveContentDir, n as checkReferences, o as listModels, r as countEntries, s as readModel, u as deleteContent } from "./model-manager-DW_fOGRM.mjs";
5
- import { t as readContext } from "./context-DIDt_TxX.mjs";
6
- import { t as detectStack } from "./detect-DinOQAmK.mjs";
7
- import { n as cleanupMergedBranches, t as checkBranchHealth } from "./branch-lifecycle-wGJg25LT.mjs";
8
- import { n as listTemplates, t as getTemplate } from "./templates-bRPzYq4M.mjs";
9
- import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-C_Pvux78.mjs";
10
- import { t as validateProject } from "./validator-SQYUV2JP.mjs";
11
- import { t as buildGraph } from "./graph-builder-tM7zj-SX.mjs";
12
- import { n as scanSummary, t as scanCandidates } from "./scanner-DUoXnmtI.mjs";
13
- import { n as applyExtract, r as applyReuse } from "./apply-manager-BkPMfpsN.mjs";
1
+ import { i as planContentDelete, n as planModelDelete, r as planContentSave, t as planModelSave } from "./ops-DjEWi5Wa.mjs";
2
+ import { n as contentFilePath, t as contentDirPath } from "./paths-CYl5vCBD.mjs";
3
+ import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-DLbVB-Ek.mjs";
4
+ import { n as readVocabulary, t as readConfig } from "./config-BIl5Ybj0.mjs";
5
+ import { S as writeContent, _ as resolveMdFilePath, a as fieldDefZodSchema, c as validateModelDefinition, d as listContent, g as resolveLocaleStrategy, h as resolveJsonFilePath, l as writeModel, m as resolveContentDir, n as checkReferences, o as listModels, r as countEntries, s as readModel, u as deleteContent } from "./model-manager-BhLsUgaB.mjs";
6
+ import { n as readMeta, r as writeMeta } from "./meta-manager-C900rEyG.mjs";
7
+ import { n as readContext, t as buildContextChange } from "./context-J0OU9Nfp.mjs";
8
+ import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-DgkNgWSI.mjs";
9
+ import { t as LocalProvider } from "./local-DvP4Y4T-.mjs";
10
+ import { t as detectStack } from "./detect-DL9mFmaS.mjs";
11
+ import { n as checkBranchHealth, r as cleanupMergedBranches } from "./branch-lifecycle-CFVpTpNT.mjs";
12
+ import { t as TOOL_ANNOTATIONS } from "./annotations-Clhbw2lh.mjs";
13
+ import { n as listTemplates, t as getTemplate } from "./templates-VrLVqfe5.mjs";
14
+ import { t as OverlayReader } from "./overlay-reader-DOdyDRIv.mjs";
15
+ import { t as validateProject } from "./validator-B0nNRFeU.mjs";
16
+ import { t as buildGraph } from "./graph-builder-DXv2JEp-.mjs";
17
+ import { n as scanSummary, t as scanCandidates } from "./scanner-DKMwc03k.mjs";
18
+ import { n as applyExtract, r as applyReuse } from "./apply-manager-D5ym5Jba.mjs";
19
+ import { t as runDoctor } from "./doctor-CgBoT6h1.mjs";
14
20
  import { join } from "node:path";
15
21
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
- import { z } from "zod";
17
- import { appendFile, readFile, writeFile } from "node:fs/promises";
18
22
  import { CONTENTRAIN_BRANCH } from "@contentrain/types";
23
+ import { appendFile, readFile, writeFile } from "node:fs/promises";
19
24
  import { simpleGit } from "simple-git";
20
- //#region src/tools/annotations.ts
21
- /**
22
- * Centralized tool annotations registry.
23
- * MCP clients use these hints to distinguish read-only vs. write vs. destructive tools.
24
- */
25
- const TOOL_ANNOTATIONS = {
26
- contentrain_status: {
27
- title: "Project Status",
28
- readOnlyHint: true,
29
- destructiveHint: false,
30
- idempotentHint: true
31
- },
32
- contentrain_describe: {
33
- title: "Describe Model",
34
- readOnlyHint: true,
35
- destructiveHint: false,
36
- idempotentHint: true
37
- },
38
- contentrain_describe_format: {
39
- title: "Describe Format",
40
- readOnlyHint: true,
41
- destructiveHint: false,
42
- idempotentHint: true
43
- },
44
- contentrain_init: {
45
- title: "Initialize Project",
46
- readOnlyHint: false,
47
- destructiveHint: false,
48
- idempotentHint: false
49
- },
50
- contentrain_scaffold: {
51
- title: "Scaffold Template",
52
- readOnlyHint: false,
53
- destructiveHint: false,
54
- idempotentHint: false
55
- },
56
- contentrain_model_save: {
57
- title: "Save Model",
58
- readOnlyHint: false,
59
- destructiveHint: false,
60
- idempotentHint: true
61
- },
62
- contentrain_model_delete: {
63
- title: "Delete Model",
64
- readOnlyHint: false,
65
- destructiveHint: true,
66
- idempotentHint: false
67
- },
68
- contentrain_content_save: {
69
- title: "Save Content",
70
- readOnlyHint: false,
71
- destructiveHint: false,
72
- idempotentHint: true
73
- },
74
- contentrain_content_delete: {
75
- title: "Delete Content",
76
- readOnlyHint: false,
77
- destructiveHint: true,
78
- idempotentHint: false
79
- },
80
- contentrain_content_list: {
81
- title: "List Content",
82
- readOnlyHint: true,
83
- destructiveHint: false,
84
- idempotentHint: true
85
- },
86
- contentrain_validate: {
87
- title: "Validate Project",
88
- readOnlyHint: false,
89
- destructiveHint: false,
90
- idempotentHint: true
91
- },
92
- contentrain_submit: {
93
- title: "Submit Branches",
94
- readOnlyHint: false,
95
- destructiveHint: false,
96
- idempotentHint: true
97
- },
98
- contentrain_merge: {
99
- title: "Merge Branch",
100
- readOnlyHint: false,
101
- destructiveHint: false,
102
- idempotentHint: false
103
- },
104
- contentrain_scan: {
105
- title: "Scan Source Code",
106
- readOnlyHint: true,
107
- destructiveHint: false,
108
- idempotentHint: true
109
- },
110
- contentrain_apply: {
111
- title: "Apply Normalize",
112
- readOnlyHint: false,
113
- destructiveHint: false,
114
- idempotentHint: false
115
- },
116
- contentrain_bulk: {
117
- title: "Bulk Operations",
118
- readOnlyHint: false,
119
- destructiveHint: false,
120
- idempotentHint: false
121
- }
122
- };
123
- //#endregion
25
+ import { z } from "zod";
124
26
  //#region src/tools/context.ts
125
- function registerContextTools(server, projectRoot) {
27
+ function registerContextTools(server, provider, projectRoot) {
126
28
  server.tool("contentrain_status", "Get full project status (read-only). Returns config, models, context. Do NOT manually edit .contentrain/ based on this output.", {}, TOOL_ANNOTATIONS["contentrain_status"], async () => {
127
- if (!await pathExists(join(contentrainDir(projectRoot), "config.json"))) {
128
- const detectedStack = await detectStack(projectRoot);
29
+ if (!await provider.fileExists(".contentrain/config.json")) {
30
+ const detectedStack = projectRoot ? await detectStack(projectRoot) : null;
129
31
  return { content: [{
130
32
  type: "text",
131
33
  text: JSON.stringify({
132
34
  initialized: false,
133
- detected_stack: detectedStack,
35
+ ...detectedStack ? { detected_stack: detectedStack } : {},
134
36
  suggestion: "Run contentrain_init to set up .contentrain/ structure",
135
37
  next_steps: ["Run contentrain_init"]
136
38
  }, null, 2)
137
39
  }] };
138
40
  }
139
- const config = await readConfig(projectRoot);
140
- const models = await listModels(projectRoot);
141
- const context = await readContext(projectRoot);
142
- const vocabulary = await readVocabulary(projectRoot);
41
+ const config = await readConfig(provider);
42
+ const models = await listModels(provider);
43
+ const context = projectRoot ? await readContext(projectRoot) : await readContext(provider);
44
+ const vocabulary = await readVocabulary(provider);
143
45
  const errors = [];
144
46
  if (!config) errors.push(".contentrain/config.json missing");
145
47
  const result = {
@@ -156,20 +58,25 @@ function registerContextTools(server, projectRoot) {
156
58
  lastOperation: context.lastOperation,
157
59
  stats: context.stats
158
60
  } : null,
159
- vocabulary_size: vocabulary ? Object.keys(vocabulary.terms).length : 0
61
+ vocabulary: vocabulary && Object.keys(vocabulary.terms).length > 0 ? {
62
+ size: Object.keys(vocabulary.terms).length,
63
+ terms: vocabulary.terms
64
+ } : { size: 0 }
160
65
  };
161
- if (await pathExists(join(projectRoot, ".git"))) try {
162
- const cleanup = await cleanupMergedBranches(projectRoot);
163
- const health = await checkBranchHealth(projectRoot);
164
- result["branches"] = {
165
- total: health.total,
166
- merged: health.merged,
167
- unmerged: health.unmerged,
168
- cleaned_up: cleanup.deleted
169
- };
170
- if (health.message) result["branch_warning"] = health.message;
171
- if (health.blocked) errors.push(health.message);
172
- } catch {}
66
+ if (projectRoot) {
67
+ if (await pathExists(join(projectRoot, ".git"))) try {
68
+ const cleanup = await cleanupMergedBranches(projectRoot);
69
+ const health = await checkBranchHealth(projectRoot);
70
+ result["branches"] = {
71
+ total: health.total,
72
+ merged: health.merged,
73
+ unmerged: health.unmerged,
74
+ cleaned_up: cleanup.deleted
75
+ };
76
+ if (health.message) result["branch_warning"] = health.message;
77
+ if (health.blocked) errors.push(health.message);
78
+ } catch {}
79
+ }
173
80
  if (errors.length > 0) result["validation"] = {
174
81
  errors: errors.length,
175
82
  warnings: 0,
@@ -190,7 +97,7 @@ function registerContextTools(server, projectRoot) {
190
97
  include_sample: z.boolean().optional().default(false).describe("Include one sample entry"),
191
98
  locale: z.string().optional().describe("Locale for sample content (default: config default locale)")
192
99
  }, TOOL_ANNOTATIONS["contentrain_describe"], async ({ model: modelId, include_sample, locale }) => {
193
- const modelDef = await readModel(projectRoot, modelId);
100
+ const modelDef = await readModel(provider, modelId);
194
101
  if (!modelDef) return {
195
102
  content: [{
196
103
  type: "text",
@@ -198,9 +105,9 @@ function registerContextTools(server, projectRoot) {
198
105
  }],
199
106
  isError: true
200
107
  };
201
- const config = await readConfig(projectRoot);
108
+ const config = await readConfig(provider);
202
109
  const effectiveLocale = locale ?? config?.locales.default ?? "en";
203
- const stats = await countEntries(projectRoot, modelDef);
110
+ const stats = await countEntries(provider, modelDef);
204
111
  const result = {
205
112
  id: modelDef.id,
206
113
  name: modelDef.name,
@@ -215,10 +122,17 @@ function registerContextTools(server, projectRoot) {
215
122
  }
216
123
  };
217
124
  if (include_sample) {
218
- const sample = await getSample(projectRoot, modelDef, effectiveLocale);
125
+ const sample = await getSample(provider, modelDef, effectiveLocale);
219
126
  if (sample) result["sample"] = sample;
220
127
  }
221
128
  result["import_snippet"] = generateImportSnippet(modelDef, config?.stack ?? "other", effectiveLocale);
129
+ if (modelDef.kind === "dictionary") {
130
+ const vocabulary = await readVocabulary(provider);
131
+ if (vocabulary && Object.keys(vocabulary.terms).length > 0) result["vocabulary_hint"] = {
132
+ note: "Check these approved terms before creating new dictionary keys",
133
+ terms: vocabulary.terms
134
+ };
135
+ }
222
136
  return { content: [{
223
137
  type: "text",
224
138
  text: JSON.stringify(result, null, 2)
@@ -335,16 +249,28 @@ function registerContextTools(server, projectRoot) {
335
249
  "4. Determine file path using locale_strategy and i18n flag",
336
250
  "5. Parse JSON (collection/singleton/dictionary) or Markdown frontmatter (document)",
337
251
  "6. Optionally read .contentrain/meta/{model-id}/{locale}.json for status/metadata"
338
- ] }
252
+ ] },
253
+ vocabulary: {
254
+ description: "Canonical terms for content consistency across models and locales.",
255
+ file: ".contentrain/vocabulary.json",
256
+ format: {
257
+ version: "number",
258
+ terms: "Record<category, Record<slug, translation_value>>"
259
+ },
260
+ usage: [
261
+ "Check vocabulary before creating dictionary entries — reuse canonical terms",
262
+ "If a new term is needed, consider adding it to vocabulary first",
263
+ "Vocabulary applies across ALL dictionary models and ALL locales"
264
+ ]
265
+ }
339
266
  }, null, 2)
340
267
  }] };
341
268
  });
342
269
  }
343
- async function getSample(projectRoot, model, locale) {
344
- const { readJson, readDir } = await import("./util/fs.mjs");
345
- const cDir = resolveContentDir(projectRoot, model);
270
+ async function getSample(reader, model, locale) {
271
+ const cDir = contentDirPath(model);
346
272
  if (model.kind === "collection") {
347
- const data = await readJson(resolveJsonFilePath(cDir, model, locale));
273
+ const data = await tryReadJson(reader, contentFilePath(model, locale));
348
274
  if (!data) return null;
349
275
  const firstKey = Object.keys(data)[0];
350
276
  return firstKey ? {
@@ -352,18 +278,18 @@ async function getSample(projectRoot, model, locale) {
352
278
  ...data[firstKey]
353
279
  } : null;
354
280
  }
355
- if (model.kind === "singleton" || model.kind === "dictionary") return readJson(resolveJsonFilePath(cDir, model, locale));
281
+ if (model.kind === "singleton" || model.kind === "dictionary") return tryReadJson(reader, contentFilePath(model, locale));
356
282
  if (model.kind === "document") {
357
283
  const strategy = resolveLocaleStrategy(model);
358
284
  if (!model.i18n) {
359
- const slug = (await readDir(cDir)).find((item) => item.endsWith(".md"))?.replace(/\.md$/u, "");
285
+ const slug = (await reader.listDirectory(cDir)).find((item) => item.endsWith(".md"))?.replace(/\.md$/u, "");
360
286
  return slug ? {
361
287
  slug,
362
288
  locale
363
289
  } : null;
364
290
  }
365
291
  if (strategy === "file") {
366
- const slug = (await readDir(cDir))[0];
292
+ const slug = (await reader.listDirectory(cDir))[0];
367
293
  return slug ? {
368
294
  slug,
369
295
  locale
@@ -371,20 +297,20 @@ async function getSample(projectRoot, model, locale) {
371
297
  }
372
298
  if (strategy === "suffix") {
373
299
  const suffix = `.${locale}.md`;
374
- const slug = (await readDir(cDir)).find((file) => file.endsWith(suffix))?.slice(0, -suffix.length);
300
+ const slug = (await reader.listDirectory(cDir)).find((file) => file.endsWith(suffix))?.slice(0, -suffix.length);
375
301
  return slug ? {
376
302
  slug,
377
303
  locale
378
304
  } : null;
379
305
  }
380
306
  if (strategy === "directory") {
381
- const slug = (await readDir(join(cDir, locale))).find((item) => item.endsWith(".md"))?.replace(/\.md$/u, "");
307
+ const slug = (await reader.listDirectory(`${cDir}/${locale}`)).find((item) => item.endsWith(".md"))?.replace(/\.md$/u, "");
382
308
  return slug ? {
383
309
  slug,
384
310
  locale
385
311
  } : null;
386
312
  }
387
- const slug = (await readDir(cDir)).find((item) => item.endsWith(".md"))?.replace(/\.md$/u, "");
313
+ const slug = (await reader.listDirectory(cDir)).find((item) => item.endsWith(".md"))?.replace(/\.md$/u, "");
388
314
  return slug ? {
389
315
  slug,
390
316
  locale
@@ -392,6 +318,13 @@ async function getSample(projectRoot, model, locale) {
392
318
  }
393
319
  return null;
394
320
  }
321
+ async function tryReadJson(reader, path) {
322
+ try {
323
+ return JSON.parse(await reader.readFile(path));
324
+ } catch {
325
+ return null;
326
+ }
327
+ }
395
328
  function buildContentPath(model, locale) {
396
329
  const strategy = model.locale_strategy ?? "file";
397
330
  const baseDir = model.content_path ?? `.contentrain/content/${model.domain}/${model.id}`;
@@ -429,13 +362,36 @@ function generateImportSnippet(model, stack, locale) {
429
362
  return snippets;
430
363
  }
431
364
  //#endregion
365
+ //#region src/tools/guards.ts
366
+ /**
367
+ * Emit a uniform "capability not available" response for tools that
368
+ * require local filesystem access but are being driven by a remote
369
+ * provider (HTTP + GitHubProvider at the moment). The agent can use
370
+ * `capability_required` to decide whether to retry against a different
371
+ * transport or surface the limitation to the user.
372
+ */
373
+ function capabilityError(tool, capability) {
374
+ return {
375
+ content: [{
376
+ type: "text",
377
+ text: JSON.stringify({
378
+ error: `${tool} requires local filesystem access.`,
379
+ capability_required: capability,
380
+ hint: "This tool is unavailable when MCP is driven by a remote provider (e.g. GitHubProvider). Use a LocalProvider or the stdio transport."
381
+ })
382
+ }],
383
+ isError: true
384
+ };
385
+ }
386
+ //#endregion
432
387
  //#region src/tools/setup.ts
433
- function registerSetupTools(server, projectRoot) {
388
+ function registerSetupTools(server, _provider, projectRoot) {
434
389
  server.tool("contentrain_init", "Initialize .contentrain/ structure. Changes are auto-committed to git — do NOT manually create .contentrain/ files.", {
435
390
  stack: z.string().optional().describe("Framework stack (nuxt, next, astro, svelte, react-vite, other). Auto-detected if omitted."),
436
391
  locales: z.array(z.string()).optional().describe("Supported locales. Default: [\"en\"]"),
437
392
  domains: z.array(z.string()).optional().describe("Content domains. Default: auto-suggested")
438
393
  }, TOOL_ANNOTATIONS["contentrain_init"], async ({ stack, locales, domains }) => {
394
+ if (!projectRoot) return capabilityError("contentrain_init", "localWorktree");
439
395
  if (await pathExists(join(contentrainDir(projectRoot), "config.json"))) return {
440
396
  content: [{
441
397
  type: "text",
@@ -449,7 +405,15 @@ function registerSetupTools(server, projectRoot) {
449
405
  const detectedStack = stack ?? await detectStack(projectRoot);
450
406
  const supportedLocales = locales ?? ["en"];
451
407
  const suggestedDomains = domains ?? suggestDomains(projectRoot);
452
- if (!await pathExists(join(projectRoot, ".git"))) await simpleGit(projectRoot).init();
408
+ const hasGit = await pathExists(join(projectRoot, ".git"));
409
+ const git = simpleGit(projectRoot);
410
+ if (!hasGit) await git.init();
411
+ if (!await git.raw([
412
+ "rev-list",
413
+ "-n",
414
+ "1",
415
+ "--all"
416
+ ]).then((out) => out.trim().length > 0).catch(() => false)) await git.commit("initial commit", { "--allow-empty": null });
453
417
  const initHealth = await checkBranchHealth(projectRoot);
454
418
  if (initHealth.blocked) return {
455
419
  content: [{
@@ -562,6 +526,7 @@ function registerSetupTools(server, projectRoot) {
562
526
  locales: z.array(z.string()).optional().describe("Override locales"),
563
527
  with_sample_content: z.boolean().optional().default(true).describe("Include sample content (default: true)")
564
528
  }, TOOL_ANNOTATIONS["contentrain_scaffold"], async ({ template: templateId, locales, with_sample_content }) => {
529
+ if (!projectRoot) return capabilityError("contentrain_scaffold", "localWorktree");
565
530
  const config = await readConfig(projectRoot);
566
531
  if (!config) return {
567
532
  content: [{
@@ -717,9 +682,63 @@ async function mergeVocabulary(projectRoot, newTerms, _locales) {
717
682
  return added;
718
683
  }
719
684
  //#endregion
685
+ //#region src/tools/commit-plan.ts
686
+ /**
687
+ * Commit a plan's changes through whichever provider is wired into the
688
+ * tool handler. Encapsulates the LocalProvider vs remote RepoProvider
689
+ * dispatch that every write-side tool (content_save, content_delete,
690
+ * model_save, model_delete) used to inline:
691
+ *
692
+ * - **LocalProvider** — goes through the worktree-backed transaction.
693
+ * `context` payload is threaded as an extra write-through and the
694
+ * transaction layer decides `auto-merged` vs `pending-review` based on
695
+ * the project's configured workflow. Selective-sync result is surfaced
696
+ * to the caller via `sync`.
697
+ *
698
+ * - **Any other RepoProvider** — the context.json write becomes an extra
699
+ * `FileChange` bundled into the plan so the whole commit lands
700
+ * atomically through the generic `RepoWriter.applyPlan`. Remote flows
701
+ * always report `pending-review`; Studio (or whatever orchestrator is
702
+ * driving the server) owns the merge.
703
+ *
704
+ * The return shape is deliberately uniform so callers don't have to
705
+ * branch on provider type again.
706
+ */
707
+ async function commitThroughProvider(provider, input) {
708
+ const { branch, changes, message, contextPayload } = input;
709
+ if (provider instanceof LocalProvider) {
710
+ const result = await provider.applyPlan({
711
+ branch,
712
+ changes,
713
+ message,
714
+ context: contextPayload
715
+ });
716
+ return {
717
+ commitSha: result.sha,
718
+ workflowAction: result.workflowAction,
719
+ sync: result.sync
720
+ };
721
+ }
722
+ const contextChange = await buildContextChange(new OverlayReader(provider, changes), contextPayload);
723
+ const allChanges = [...changes, contextChange].toSorted((a, b) => a.path.localeCompare(b.path));
724
+ return {
725
+ commitSha: (await provider.applyPlan({
726
+ branch,
727
+ changes: allChanges,
728
+ message,
729
+ author: {
730
+ name: "Contentrain",
731
+ email: "mcp@contentrain.io"
732
+ },
733
+ base: CONTENTRAIN_BRANCH
734
+ })).sha,
735
+ workflowAction: "pending-review"
736
+ };
737
+ }
738
+ //#endregion
720
739
  //#region src/tools/model.ts
721
740
  const fieldDefSchema = fieldDefZodSchema;
722
- function registerModelTools(server, projectRoot) {
741
+ function registerModelTools(server, provider, projectRoot) {
723
742
  server.tool("contentrain_model_save", "Create or update a model definition. Changes are auto-committed to git — do NOT manually edit .contentrain/ files after calling this tool.", {
724
743
  id: z.string().describe("Model ID (kebab-case, e.g. \"blog-post\")"),
725
744
  name: z.string().describe("Human-readable name"),
@@ -741,7 +760,7 @@ function registerModelTools(server, projectRoot) {
741
760
  "none"
742
761
  ]).optional().describe("How locale is encoded in file names. Default: \"file\"")
743
762
  }, TOOL_ANNOTATIONS["contentrain_model_save"], async (input) => {
744
- const config = await readConfig(projectRoot);
763
+ const config = await readConfig(provider);
745
764
  if (!config) return {
746
765
  content: [{
747
766
  type: "text",
@@ -767,19 +786,20 @@ function registerModelTools(server, projectRoot) {
767
786
  }],
768
787
  isError: true
769
788
  };
770
- const health = await checkBranchHealth(projectRoot);
771
- if (health.blocked) return {
772
- content: [{
773
- type: "text",
774
- text: JSON.stringify({
775
- error: health.message,
776
- action: "blocked",
777
- hint: "Merge or delete old contentrain/* branches before creating new ones."
778
- }, null, 2)
779
- }],
780
- isError: true
781
- };
782
- const action = await readModel(projectRoot, input.id) ? "updated" : "created";
789
+ if (provider instanceof LocalProvider) {
790
+ const health = await checkBranchHealth(provider.projectRoot);
791
+ if (health.blocked) return {
792
+ content: [{
793
+ type: "text",
794
+ text: JSON.stringify({
795
+ error: health.message,
796
+ action: "blocked",
797
+ hint: "Merge or delete old contentrain/* branches before creating new ones."
798
+ }, null, 2)
799
+ }],
800
+ isError: true
801
+ };
802
+ }
783
803
  const model = {
784
804
  id: input.id,
785
805
  name: input.name,
@@ -791,52 +811,28 @@ function registerModelTools(server, projectRoot) {
791
811
  content_path: input.content_path,
792
812
  locale_strategy: input.locale_strategy
793
813
  };
814
+ const savePlan = await planModelSave(provider, { model });
815
+ const action = savePlan.result.action;
794
816
  const branch = buildBranchName("model", input.id);
795
- const tx = await createTransaction(projectRoot, branch);
817
+ const message = `[contentrain] ${action}: ${input.id}`;
818
+ const contextPayload = {
819
+ tool: "contentrain_model_save",
820
+ model: input.id
821
+ };
822
+ let commitSha;
823
+ let workflowAction;
824
+ let sync;
796
825
  try {
797
- await tx.write(async (wt) => {
798
- await writeModel(wt, model);
826
+ const result = await commitThroughProvider(provider, {
827
+ branch,
828
+ changes: savePlan.changes,
829
+ message,
830
+ contextPayload
799
831
  });
800
- await tx.commit(`[contentrain] ${action}: ${input.id}`, {
801
- tool: "contentrain_model_save",
802
- model: input.id
803
- });
804
- const gitResult = await tx.complete();
805
- const defaultLocale = config.locales.default;
806
- const contentDir = resolveContentDir(projectRoot, model);
807
- const contentPath = model.content_path ?? `.contentrain/content/${input.domain}/${input.id}`;
808
- let exampleFilePath;
809
- if (model.kind === "document") exampleFilePath = resolveMdFilePath(contentDir, model, defaultLocale, "{slug}");
810
- else exampleFilePath = resolveJsonFilePath(contentDir, model, defaultLocale);
811
- const displayPath = exampleFilePath.replace(projectRoot + "/", "").replace(projectRoot, "");
812
- const importSnippet = { generic: `import data from '${displayPath}'` };
813
- if (config.stack === "nuxt") importSnippet["nuxt"] = model.kind === "document" ? `const { data } = await useAsyncData(() => queryContent('${model.domain}/${model.id}').locale('${defaultLocale}').find())` : `const { data } = await useFetch('/api/content/${model.id}?locale=${defaultLocale}')`;
814
- return { content: [{
815
- type: "text",
816
- text: JSON.stringify({
817
- status: "committed",
818
- message: "Model saved and committed to git. Do NOT manually edit .contentrain/ files.",
819
- action,
820
- model: input.id,
821
- validation: {
822
- valid: true,
823
- errors: []
824
- },
825
- git: {
826
- branch,
827
- action: gitResult.action,
828
- commit: gitResult.commit,
829
- ...gitResult.sync ? { sync: gitResult.sync } : {}
830
- },
831
- context_updated: true,
832
- content_path: contentPath + "/",
833
- example_file: displayPath,
834
- import_snippet: importSnippet,
835
- next_steps: ["Add content with contentrain_content_save"]
836
- }, null, 2)
837
- }] };
832
+ commitSha = result.commitSha;
833
+ workflowAction = result.workflowAction;
834
+ sync = result.sync;
838
835
  } catch (error) {
839
- await tx.cleanup();
840
836
  return {
841
837
  content: [{
842
838
  type: "text",
@@ -844,41 +840,76 @@ function registerModelTools(server, projectRoot) {
844
840
  }],
845
841
  isError: true
846
842
  };
847
- } finally {
848
- await tx.cleanup();
849
843
  }
844
+ const defaultLocale = config.locales.default;
845
+ const contentPath = model.content_path ?? `.contentrain/content/${input.domain}/${input.id}`;
846
+ let displayPath;
847
+ let importSnippet;
848
+ if (projectRoot) {
849
+ const contentDir = resolveContentDir(projectRoot, model);
850
+ displayPath = (model.kind === "document" ? resolveMdFilePath(contentDir, model, defaultLocale, "{slug}") : resolveJsonFilePath(contentDir, model, defaultLocale)).replace(projectRoot + "/", "").replace(projectRoot, "");
851
+ importSnippet = { generic: `import data from '${displayPath}'` };
852
+ if (config.stack === "nuxt") importSnippet["nuxt"] = model.kind === "document" ? `const { data } = await useAsyncData(() => queryContent('${model.domain}/${model.id}').locale('${defaultLocale}').find())` : `const { data } = await useFetch('/api/content/${model.id}?locale=${defaultLocale}')`;
853
+ }
854
+ return { content: [{
855
+ type: "text",
856
+ text: JSON.stringify({
857
+ status: "committed",
858
+ message: "Model saved and committed to git. Do NOT manually edit .contentrain/ files.",
859
+ action,
860
+ model: input.id,
861
+ validation: {
862
+ valid: true,
863
+ errors: []
864
+ },
865
+ git: {
866
+ branch,
867
+ action: workflowAction,
868
+ commit: commitSha,
869
+ ...sync ? { sync } : {}
870
+ },
871
+ context_updated: true,
872
+ content_path: contentPath + "/",
873
+ ...displayPath ? { example_file: displayPath } : {},
874
+ ...importSnippet ? { import_snippet: importSnippet } : {},
875
+ next_steps: ["Add content with contentrain_content_save"]
876
+ }, null, 2)
877
+ }] };
850
878
  });
851
879
  server.tool("contentrain_model_delete", "Delete a model and its content/meta. Changes are auto-committed to git — do NOT manually edit .contentrain/ files.", {
852
880
  model: z.string().describe("Model ID to delete"),
853
881
  confirm: z.literal(true).describe("Must be true to confirm deletion")
854
882
  }, TOOL_ANNOTATIONS["contentrain_model_delete"], async ({ model: modelId }) => {
855
- if (!await readConfig(projectRoot)) return {
883
+ if (!await readConfig(provider)) return {
856
884
  content: [{
857
885
  type: "text",
858
886
  text: JSON.stringify({ error: "Project not initialized." })
859
887
  }],
860
888
  isError: true
861
889
  };
862
- if (!await readModel(projectRoot, modelId)) return {
890
+ const existing = await readModel(provider, modelId);
891
+ if (!existing) return {
863
892
  content: [{
864
893
  type: "text",
865
894
  text: JSON.stringify({ error: `Model "${modelId}" not found` })
866
895
  }],
867
896
  isError: true
868
897
  };
869
- const deleteHealth = await checkBranchHealth(projectRoot);
870
- if (deleteHealth.blocked) return {
871
- content: [{
872
- type: "text",
873
- text: JSON.stringify({
874
- error: deleteHealth.message,
875
- action: "blocked",
876
- hint: "Merge or delete old contentrain/* branches before creating new ones."
877
- }, null, 2)
878
- }],
879
- isError: true
880
- };
881
- const refs = await checkReferences(projectRoot, modelId);
898
+ if (provider instanceof LocalProvider) {
899
+ const deleteHealth = await checkBranchHealth(provider.projectRoot);
900
+ if (deleteHealth.blocked) return {
901
+ content: [{
902
+ type: "text",
903
+ text: JSON.stringify({
904
+ error: deleteHealth.message,
905
+ action: "blocked",
906
+ hint: "Merge or delete old contentrain/* branches before creating new ones."
907
+ }, null, 2)
908
+ }],
909
+ isError: true
910
+ };
911
+ }
912
+ const refs = await checkReferences(provider, modelId);
882
913
  if (refs.length > 0) return { content: [{
883
914
  type: "text",
884
915
  text: JSON.stringify({
@@ -888,18 +919,26 @@ function registerModelTools(server, projectRoot) {
888
919
  next_steps: ["Remove relation fields from referencing models first"]
889
920
  }, null, 2)
890
921
  }] };
922
+ const deletePlan = await planModelDelete(provider, { model: existing });
891
923
  const branch = buildBranchName("model", modelId);
892
- const tx = await createTransaction(projectRoot, branch);
924
+ const message = `[contentrain] delete: ${modelId}`;
925
+ const contextPayload = {
926
+ tool: "contentrain_model_delete",
927
+ model: modelId
928
+ };
929
+ let commitSha;
930
+ let workflowAction;
931
+ let sync;
893
932
  try {
894
- let filesRemoved = [];
895
- await tx.write(async (wt) => {
896
- filesRemoved = await deleteModel(wt, modelId);
933
+ const result = await commitThroughProvider(provider, {
934
+ branch,
935
+ changes: deletePlan.changes,
936
+ message,
937
+ contextPayload
897
938
  });
898
- await tx.commit(`[contentrain] delete: ${modelId}`, {
899
- tool: "contentrain_model_delete",
900
- model: modelId
901
- });
902
- const gitResult = await tx.complete();
939
+ commitSha = result.commitSha;
940
+ workflowAction = result.workflowAction;
941
+ sync = result.sync;
903
942
  return { content: [{
904
943
  type: "text",
905
944
  text: JSON.stringify({
@@ -908,16 +947,15 @@ function registerModelTools(server, projectRoot) {
908
947
  deleted: true,
909
948
  git: {
910
949
  branch,
911
- action: gitResult.action,
912
- commit: gitResult.commit,
913
- ...gitResult.sync ? { sync: gitResult.sync } : {}
950
+ action: workflowAction,
951
+ commit: commitSha,
952
+ ...sync ? { sync } : {}
914
953
  },
915
- files_removed: filesRemoved,
954
+ files_removed: deletePlan.result,
916
955
  context_updated: true
917
956
  }, null, 2)
918
957
  }] };
919
958
  } catch (error) {
920
- await tx.cleanup();
921
959
  return {
922
960
  content: [{
923
961
  type: "text",
@@ -925,15 +963,13 @@ function registerModelTools(server, projectRoot) {
925
963
  }],
926
964
  isError: true
927
965
  };
928
- } finally {
929
- await tx.cleanup();
930
966
  }
931
967
  });
932
968
  }
933
969
  const validateModel = validateModelDefinition;
934
970
  //#endregion
935
971
  //#region src/tools/content.ts
936
- function registerContentTools(server, projectRoot) {
972
+ function registerContentTools(server, provider, projectRoot) {
937
973
  server.tool("contentrain_content_save", "Save content entries. Entry format varies by model kind: DICTIONARY — provide \"locale\" and \"data\" (flat key-value, all string values); \"id\" and \"slug\" are ignored; data keys are the identities. COLLECTION — provide \"locale\" and \"data\"; \"id\" is optional (auto-generated if omitted); \"slug\" is ignored. DOCUMENT — provide \"slug\" (required), \"locale\", and \"data\"; use the \"body\" key inside data for markdown content. SINGLETON — provide only \"locale\" and \"data\". Changes are auto-committed to git — do NOT manually edit .contentrain/ files after calling this tool.", {
938
974
  model: z.string().describe("Model ID"),
939
975
  entries: z.array(z.object({
@@ -945,7 +981,7 @@ function registerContentTools(server, projectRoot) {
945
981
  expire_at: z.string().optional().describe("ISO 8601 date for scheduled expiry (stored in meta, must be after publish_at)")
946
982
  })).describe("Content entries to save")
947
983
  }, TOOL_ANNOTATIONS["contentrain_content_save"], async (input) => {
948
- const config = await readConfig(projectRoot);
984
+ const config = await readConfig(provider);
949
985
  if (!config) return {
950
986
  content: [{
951
987
  type: "text",
@@ -953,7 +989,7 @@ function registerContentTools(server, projectRoot) {
953
989
  }],
954
990
  isError: true
955
991
  };
956
- const model = await readModel(projectRoot, input.model);
992
+ const model = await readModel(provider, input.model);
957
993
  if (!model) return {
958
994
  content: [{
959
995
  type: "text",
@@ -1001,57 +1037,61 @@ function registerContentTools(server, projectRoot) {
1001
1037
  if (entry.publish_at !== void 0) entry.data["publish_at"] = entry.publish_at;
1002
1038
  if (entry.expire_at !== void 0) entry.data["expire_at"] = entry.expire_at;
1003
1039
  }
1004
- const health = await checkBranchHealth(projectRoot);
1005
- if (health.blocked) return {
1006
- content: [{
1007
- type: "text",
1008
- text: JSON.stringify({
1009
- error: health.message,
1010
- action: "blocked",
1011
- hint: "Merge or delete old contentrain/* branches before creating new ones."
1012
- }, null, 2)
1013
- }],
1014
- isError: true
1015
- };
1016
- const branch = buildBranchName("content", input.model);
1017
- const tx = await createTransaction(projectRoot, branch);
1040
+ if (provider instanceof LocalProvider) {
1041
+ const health = await checkBranchHealth(provider.projectRoot);
1042
+ if (health.blocked) return {
1043
+ content: [{
1044
+ type: "text",
1045
+ text: JSON.stringify({
1046
+ error: health.message,
1047
+ action: "blocked",
1048
+ hint: "Merge or delete old contentrain/* branches before creating new ones."
1049
+ }, null, 2)
1050
+ }],
1051
+ isError: true
1052
+ };
1053
+ }
1054
+ const vocabulary = await readVocabulary(provider);
1055
+ let plan;
1018
1056
  try {
1019
- let results;
1020
- let entryIds = [];
1021
- await tx.write(async (wt) => {
1022
- results = await writeContent(wt, model, input.entries, config);
1023
- entryIds = results.map((r) => r.id ?? r.slug ?? r.locale).filter(Boolean);
1057
+ plan = await planContentSave(provider, {
1058
+ model,
1059
+ entries: input.entries,
1060
+ config,
1061
+ vocabulary
1024
1062
  });
1025
- await tx.commit(`[contentrain] content: ${input.model}`, {
1026
- tool: "contentrain_content_save",
1027
- model: input.model,
1028
- locale: input.entries[0]?.locale,
1029
- entries: entryIds
1063
+ } catch (error) {
1064
+ return {
1065
+ content: [{
1066
+ type: "text",
1067
+ text: JSON.stringify({ error: `Content save failed: ${error instanceof Error ? error.message : String(error)}` })
1068
+ }],
1069
+ isError: true
1070
+ };
1071
+ }
1072
+ const entryIds = plan.result.map((r) => r.id ?? r.slug ?? r.locale).filter((v) => Boolean(v));
1073
+ const branch = buildBranchName("content", input.model);
1074
+ const message = `[contentrain] content: ${input.model}`;
1075
+ const contextPayload = {
1076
+ tool: "contentrain_content_save",
1077
+ model: input.model,
1078
+ locale: input.entries[0]?.locale,
1079
+ entries: entryIds
1080
+ };
1081
+ let commitSha;
1082
+ let workflowAction;
1083
+ let sync;
1084
+ try {
1085
+ const result = await commitThroughProvider(provider, {
1086
+ branch,
1087
+ changes: plan.changes,
1088
+ message,
1089
+ contextPayload
1030
1090
  });
1031
- const gitResult = await tx.complete();
1032
- const validationResult = await validateProject(projectRoot, { model: input.model });
1033
- return { content: [{
1034
- type: "text",
1035
- text: JSON.stringify({
1036
- status: "committed",
1037
- message: "Content saved and committed to git. Do NOT manually edit .contentrain/ files.",
1038
- results,
1039
- git: {
1040
- branch,
1041
- action: gitResult.action,
1042
- commit: gitResult.commit,
1043
- ...gitResult.sync ? { sync: gitResult.sync } : {}
1044
- },
1045
- validation: {
1046
- valid: validationResult.valid,
1047
- errors: validationResult.issues.filter((i) => i.severity === "error").map((i) => i.message)
1048
- },
1049
- context_updated: true,
1050
- next_steps: [...!validationResult.valid ? ["WARNING: Content has validation errors — run contentrain_validate for details"] : [], ...model.kind === "collection" ? ["Use contentrain_content_list to verify", "Add more entries or publish"] : ["Use contentrain_content_list to verify"]]
1051
- }, null, 2)
1052
- }] };
1091
+ commitSha = result.commitSha;
1092
+ workflowAction = result.workflowAction;
1093
+ sync = result.sync;
1053
1094
  } catch (error) {
1054
- await tx.cleanup();
1055
1095
  return {
1056
1096
  content: [{
1057
1097
  type: "text",
@@ -1059,9 +1099,37 @@ function registerContentTools(server, projectRoot) {
1059
1099
  }],
1060
1100
  isError: true
1061
1101
  };
1062
- } finally {
1063
- await tx.cleanup();
1064
1102
  }
1103
+ const validationResult = projectRoot ? await validateProject(projectRoot, { model: input.model }) : await validateProject(new OverlayReader(provider, plan.changes), { model: input.model });
1104
+ const allAdvisories = plan.result.flatMap((r) => r.advisories ?? []);
1105
+ return { content: [{
1106
+ type: "text",
1107
+ text: JSON.stringify({
1108
+ status: "committed",
1109
+ message: "Content saved and committed to git. Do NOT manually edit .contentrain/ files.",
1110
+ results: plan.result,
1111
+ git: {
1112
+ branch,
1113
+ action: workflowAction,
1114
+ commit: commitSha,
1115
+ ...sync ? { sync } : {}
1116
+ },
1117
+ ...allAdvisories.length > 0 ? {
1118
+ advisories: allAdvisories,
1119
+ advisory_note: "Save succeeded. Review these warnings and consider consolidating duplicate values."
1120
+ } : {},
1121
+ validation: {
1122
+ valid: validationResult.valid,
1123
+ errors: validationResult.issues.filter((i) => i.severity === "error").map((i) => i.message)
1124
+ },
1125
+ context_updated: true,
1126
+ next_steps: [
1127
+ ...allAdvisories.length > 0 ? ["ADVISORY: Duplicate values detected — review advisories above"] : [],
1128
+ ...!validationResult.valid ? ["WARNING: Content has validation errors — run contentrain_validate for details"] : [],
1129
+ ...model.kind === "collection" ? ["Use contentrain_content_list to verify", "Add more entries or publish"] : ["Use contentrain_content_list to verify"]
1130
+ ]
1131
+ }, null, 2)
1132
+ }] };
1065
1133
  });
1066
1134
  server.tool("contentrain_content_delete", "Delete content entries. For dictionaries, use \"keys\" to remove specific keys (omit to delete entire locale file). Changes are auto-committed to git — do NOT manually edit .contentrain/ files after calling this tool.", {
1067
1135
  model: z.string().describe("Model ID"),
@@ -1071,14 +1139,14 @@ function registerContentTools(server, projectRoot) {
1071
1139
  keys: z.array(z.string()).optional().describe("Dictionary only: specific keys to remove. Omit to delete entire locale file."),
1072
1140
  confirm: z.literal(true).describe("Must be true to confirm deletion")
1073
1141
  }, TOOL_ANNOTATIONS["contentrain_content_delete"], async (input) => {
1074
- if (!await readConfig(projectRoot)) return {
1142
+ if (!await readConfig(provider)) return {
1075
1143
  content: [{
1076
1144
  type: "text",
1077
1145
  text: JSON.stringify({ error: "Project not initialized." })
1078
1146
  }],
1079
1147
  isError: true
1080
1148
  };
1081
- const model = await readModel(projectRoot, input.model);
1149
+ const model = await readModel(provider, input.model);
1082
1150
  if (!model) return {
1083
1151
  content: [{
1084
1152
  type: "text",
@@ -1086,54 +1154,75 @@ function registerContentTools(server, projectRoot) {
1086
1154
  }],
1087
1155
  isError: true
1088
1156
  };
1089
- const deleteHealth = await checkBranchHealth(projectRoot);
1090
- if (deleteHealth.blocked) return {
1091
- content: [{
1092
- type: "text",
1093
- text: JSON.stringify({
1094
- error: deleteHealth.message,
1095
- action: "blocked",
1096
- hint: "Merge or delete old contentrain/* branches before creating new ones."
1097
- }, null, 2)
1098
- }],
1099
- isError: true
1100
- };
1101
- const branch = buildBranchName("content", input.model);
1102
- const tx = await createTransaction(projectRoot, branch);
1157
+ if (provider instanceof LocalProvider) {
1158
+ const deleteHealth = await checkBranchHealth(provider.projectRoot);
1159
+ if (deleteHealth.blocked) return {
1160
+ content: [{
1161
+ type: "text",
1162
+ text: JSON.stringify({
1163
+ error: deleteHealth.message,
1164
+ action: "blocked",
1165
+ hint: "Merge or delete old contentrain/* branches before creating new ones."
1166
+ }, null, 2)
1167
+ }],
1168
+ isError: true
1169
+ };
1170
+ }
1171
+ let deletePlan;
1103
1172
  try {
1104
- let removed = [];
1105
- await tx.write(async (wt) => {
1106
- removed = await deleteContent(wt, model, {
1107
- id: input.id,
1108
- slug: input.slug,
1109
- locale: input.locale,
1110
- keys: input.keys
1111
- });
1173
+ deletePlan = await planContentDelete(provider, {
1174
+ model,
1175
+ id: input.id,
1176
+ slug: input.slug,
1177
+ locale: input.locale,
1178
+ keys: input.keys
1112
1179
  });
1113
- await tx.commit(`[contentrain] delete content: ${input.model}`, {
1114
- tool: "contentrain_content_delete",
1115
- model: input.model,
1116
- locale: input.locale
1180
+ } catch (error) {
1181
+ return {
1182
+ content: [{
1183
+ type: "text",
1184
+ text: JSON.stringify({ error: `Delete failed: ${error instanceof Error ? error.message : String(error)}` })
1185
+ }],
1186
+ isError: true
1187
+ };
1188
+ }
1189
+ const branch = buildBranchName("content", input.model);
1190
+ const message = `[contentrain] delete content: ${input.model}`;
1191
+ const contextPayload = {
1192
+ tool: "contentrain_content_delete",
1193
+ model: input.model,
1194
+ locale: input.locale
1195
+ };
1196
+ let commitSha;
1197
+ let workflowAction;
1198
+ let sync;
1199
+ try {
1200
+ const result = await commitThroughProvider(provider, {
1201
+ branch,
1202
+ changes: deletePlan.changes,
1203
+ message,
1204
+ contextPayload
1117
1205
  });
1118
- const gitResult = await tx.complete();
1206
+ commitSha = result.commitSha;
1207
+ workflowAction = result.workflowAction;
1208
+ sync = result.sync;
1119
1209
  return { content: [{
1120
1210
  type: "text",
1121
1211
  text: JSON.stringify({
1122
1212
  status: "committed",
1123
1213
  message: "Content deleted and committed to git. Do NOT manually edit .contentrain/ files.",
1124
1214
  deleted: true,
1125
- files_removed: removed,
1215
+ files_removed: deletePlan.result,
1126
1216
  git: {
1127
1217
  branch,
1128
- action: gitResult.action,
1129
- commit: gitResult.commit,
1130
- ...gitResult.sync ? { sync: gitResult.sync } : {}
1218
+ action: workflowAction,
1219
+ commit: commitSha,
1220
+ ...sync ? { sync } : {}
1131
1221
  },
1132
1222
  context_updated: true
1133
1223
  }, null, 2)
1134
1224
  }] };
1135
1225
  } catch (error) {
1136
- await tx.cleanup();
1137
1226
  return {
1138
1227
  content: [{
1139
1228
  type: "text",
@@ -1141,8 +1230,6 @@ function registerContentTools(server, projectRoot) {
1141
1230
  }],
1142
1231
  isError: true
1143
1232
  };
1144
- } finally {
1145
- await tx.cleanup();
1146
1233
  }
1147
1234
  });
1148
1235
  server.tool("contentrain_content_list", "List content entries (read-only). Returns data from .contentrain/ — do NOT manually create or modify content files.", {
@@ -1153,7 +1240,7 @@ function registerContentTools(server, projectRoot) {
1153
1240
  limit: z.number().optional().describe("Max entries to return"),
1154
1241
  offset: z.number().optional().describe("Skip N entries")
1155
1242
  }, TOOL_ANNOTATIONS["contentrain_content_list"], async (input) => {
1156
- const config = await readConfig(projectRoot);
1243
+ const config = await readConfig(provider);
1157
1244
  if (!config) return {
1158
1245
  content: [{
1159
1246
  type: "text",
@@ -1161,7 +1248,7 @@ function registerContentTools(server, projectRoot) {
1161
1248
  }],
1162
1249
  isError: true
1163
1250
  };
1164
- const model = await readModel(projectRoot, input.model);
1251
+ const model = await readModel(provider, input.model);
1165
1252
  if (!model) return {
1166
1253
  content: [{
1167
1254
  type: "text",
@@ -1170,13 +1257,14 @@ function registerContentTools(server, projectRoot) {
1170
1257
  isError: true
1171
1258
  };
1172
1259
  try {
1173
- const result = await listContent(projectRoot, model, {
1260
+ const listOpts = {
1174
1261
  locale: input.locale,
1175
1262
  filter: input.filter,
1176
1263
  resolve: input.resolve,
1177
1264
  limit: input.limit,
1178
1265
  offset: input.offset
1179
- }, config);
1266
+ };
1267
+ const result = projectRoot ? await listContent(projectRoot, model, listOpts, config) : await listContent(provider, model, listOpts, config);
1180
1268
  return { content: [{
1181
1269
  type: "text",
1182
1270
  text: JSON.stringify(result, null, 2)
@@ -1194,12 +1282,13 @@ function registerContentTools(server, projectRoot) {
1194
1282
  }
1195
1283
  //#endregion
1196
1284
  //#region src/tools/workflow.ts
1197
- function registerWorkflowTools(server, projectRoot) {
1285
+ function registerWorkflowTools(server, provider, projectRoot) {
1198
1286
  server.tool("contentrain_validate", "Validate project content against model schemas. Detects required field violations, type mismatches, broken relations, secret leaks, i18n parity issues, and more. If fix:true, auto-fixes structural issues (canonical sort, orphan meta, missing locale files) — do NOT manually edit .contentrain/ files.", {
1199
1287
  model: z.string().optional().describe("Model ID to validate (omit for all models)"),
1200
1288
  fix: z.boolean().optional().describe("Auto-fix structural issues (canonical sort, orphan meta, missing locale files). Default: false")
1201
1289
  }, TOOL_ANNOTATIONS["contentrain_validate"], async (input) => {
1202
- if (!await readConfig(projectRoot)) return {
1290
+ if (input.fix && !projectRoot) return capabilityError("contentrain_validate", "localWorktree");
1291
+ if (!await readConfig(provider)) return {
1203
1292
  content: [{
1204
1293
  type: "text",
1205
1294
  text: JSON.stringify({ error: "Project not initialized. Run contentrain_init first." })
@@ -1208,7 +1297,7 @@ function registerWorkflowTools(server, projectRoot) {
1208
1297
  };
1209
1298
  try {
1210
1299
  let result;
1211
- if (input.fix) {
1300
+ if (input.fix && projectRoot) {
1212
1301
  const fixHealth = await checkBranchHealth(projectRoot);
1213
1302
  if (fixHealth.blocked) return {
1214
1303
  content: [{
@@ -1264,7 +1353,10 @@ function registerWorkflowTools(server, projectRoot) {
1264
1353
  await tx.cleanup();
1265
1354
  }
1266
1355
  }
1267
- if (!result) result = await validateProject(projectRoot, {
1356
+ if (!result) result = projectRoot ? await validateProject(projectRoot, {
1357
+ model: input.model,
1358
+ fix: false
1359
+ }) : await validateProject(provider, {
1268
1360
  model: input.model,
1269
1361
  fix: false
1270
1362
  });
@@ -1295,6 +1387,7 @@ function registerWorkflowTools(server, projectRoot) {
1295
1387
  branches: z.array(z.string()).optional().describe("Specific branch names to push (omit for all contentrain/* branches)"),
1296
1388
  message: z.string().optional().describe("Optional message for the push operation")
1297
1389
  }, TOOL_ANNOTATIONS["contentrain_submit"], async (input) => {
1390
+ if (!provider.capabilities.localWorktree || !provider.capabilities.pushRemote || !projectRoot) return capabilityError("contentrain_submit", "localWorktree");
1298
1391
  const config = await readConfig(projectRoot);
1299
1392
  if (!config) return {
1300
1393
  content: [{
@@ -1407,6 +1500,7 @@ function registerWorkflowTools(server, projectRoot) {
1407
1500
  branch: z.string().describe("Branch name to merge (e.g. cr/normalize/extract/...)"),
1408
1501
  confirm: z.literal(true).describe("Must be true to confirm the merge")
1409
1502
  }, TOOL_ANNOTATIONS["contentrain_merge"], async (input) => {
1503
+ if (!provider.capabilities.localWorktree || !projectRoot) return capabilityError("contentrain_merge", "localWorktree");
1410
1504
  if (!await readConfig(projectRoot)) return {
1411
1505
  content: [{
1412
1506
  type: "text",
@@ -1453,7 +1547,7 @@ function registerWorkflowTools(server, projectRoot) {
1453
1547
  }
1454
1548
  //#endregion
1455
1549
  //#region src/tools/normalize.ts
1456
- function registerNormalizeTools(server, projectRoot) {
1550
+ function registerNormalizeTools(server, provider, projectRoot) {
1457
1551
  server.tool("contentrain_scan", "Scan project source code for content strings. Three modes: \"graph\" builds import/component graph for project intelligence, \"candidates\" extracts string literals with pre-filtering and pagination, \"summary\" provides quick overview stats. Read-only — no changes to disk or git. MCP finds strings deterministically; the agent decides what is content. Recommended workflow: start with \"summary\" or \"graph\" for orientation, then paginate through \"candidates\" to evaluate strings.", {
1458
1552
  mode: z.enum([
1459
1553
  "graph",
@@ -1468,6 +1562,7 @@ function registerNormalizeTools(server, projectRoot) {
1468
1562
  min_length: z.number().optional().describe("Candidates mode: minimum string length. Default: 2"),
1469
1563
  max_length: z.number().optional().describe("Candidates mode: maximum string length. Default: 500")
1470
1564
  }, TOOL_ANNOTATIONS["contentrain_scan"], async (input) => {
1565
+ if (!provider.capabilities.astScan || !projectRoot) return capabilityError("contentrain_scan", "astScan");
1471
1566
  if (!await readConfig(projectRoot)) return {
1472
1567
  content: [{
1473
1568
  type: "text",
@@ -1594,6 +1689,8 @@ function registerNormalizeTools(server, projectRoot) {
1594
1689
  import_statement: z.string().optional().describe("Import to add if needed")
1595
1690
  })).optional().describe("Reuse mode: patches to apply (max 100)")
1596
1691
  }, TOOL_ANNOTATIONS["contentrain_apply"], async (input) => {
1692
+ const capability = input.mode === "reuse" ? "sourceWrite" : "sourceRead";
1693
+ if (!provider.capabilities[capability] || !projectRoot) return capabilityError("contentrain_apply", capability);
1597
1694
  if (!await readConfig(projectRoot)) return {
1598
1695
  content: [{
1599
1696
  type: "text",
@@ -1695,7 +1792,7 @@ function registerNormalizeTools(server, projectRoot) {
1695
1792
  }
1696
1793
  //#endregion
1697
1794
  //#region src/tools/bulk.ts
1698
- function registerBulkTools(server, projectRoot) {
1795
+ function registerBulkTools(server, _provider, projectRoot) {
1699
1796
  server.tool("contentrain_bulk", "Batch operations on content entries. All operations are auto-committed to git.", {
1700
1797
  operation: z.enum([
1701
1798
  "copy_locale",
@@ -1715,6 +1812,7 @@ function registerBulkTools(server, projectRoot) {
1715
1812
  ]).optional().describe("New status for update_status"),
1716
1813
  confirm: z.boolean().optional().describe("Must be true for delete_entries")
1717
1814
  }, TOOL_ANNOTATIONS["contentrain_bulk"], async (input) => {
1815
+ if (!projectRoot) return capabilityError("contentrain_bulk", "localWorktree");
1718
1816
  const config = await readConfig(projectRoot);
1719
1817
  if (!config) return {
1720
1818
  content: [{
@@ -2011,25 +2109,58 @@ function registerBulkTools(server, projectRoot) {
2011
2109
  });
2012
2110
  }
2013
2111
  //#endregion
2112
+ //#region src/tools/doctor.ts
2113
+ function registerDoctorTools(server, _provider, projectRoot) {
2114
+ server.tool("contentrain_doctor", "Project health report (read-only). Returns structured checks: git, node, .contentrain/ structure, model parse, orphan content, branch pressure, SDK freshness. Pass `usage: true` for a deeper analysis of content-key references in source files (unused keys, duplicate dictionary values, locale coverage). Local-filesystem only — unavailable over remote providers.", { usage: z.boolean().optional().default(false).describe("Run the heavier usage-analysis branch (unused keys, duplicate values, missing locales). Default: false.") }, TOOL_ANNOTATIONS["contentrain_doctor"], async ({ usage }) => {
2115
+ if (!projectRoot) return capabilityError("contentrain_doctor", "localWorktree");
2116
+ const report = await runDoctor(projectRoot, { usage });
2117
+ return { content: [{
2118
+ type: "text",
2119
+ text: JSON.stringify(report, null, 2)
2120
+ }] };
2121
+ });
2122
+ }
2123
+ //#endregion
2014
2124
  //#region package.json
2015
- var version = "1.2.1";
2125
+ var version = "1.4.0";
2016
2126
  //#endregion
2017
2127
  //#region src/server.ts
2018
- function createServer(projectRoot) {
2128
+ function createServer(input) {
2129
+ const { provider, projectRoot } = resolveServerContext(input);
2019
2130
  const server = new McpServer({
2020
2131
  name: "contentrain-mcp",
2021
2132
  version
2022
2133
  });
2023
- registerContextTools(server, projectRoot);
2024
- registerSetupTools(server, projectRoot);
2025
- registerModelTools(server, projectRoot);
2026
- registerContentTools(server, projectRoot);
2027
- registerWorkflowTools(server, projectRoot);
2028
- registerNormalizeTools(server, projectRoot);
2029
- registerBulkTools(server, projectRoot);
2134
+ registerContextTools(server, provider, projectRoot);
2135
+ registerSetupTools(server, provider, projectRoot);
2136
+ registerModelTools(server, provider, projectRoot);
2137
+ registerContentTools(server, provider, projectRoot);
2138
+ registerWorkflowTools(server, provider, projectRoot);
2139
+ registerNormalizeTools(server, provider, projectRoot);
2140
+ registerBulkTools(server, provider, projectRoot);
2141
+ registerDoctorTools(server, provider, projectRoot);
2030
2142
  return server;
2031
2143
  }
2144
+ function resolveServerContext(input) {
2145
+ if (typeof input === "string") return {
2146
+ provider: new LocalProvider(input),
2147
+ projectRoot: input
2148
+ };
2149
+ if (input.provider) {
2150
+ let projectRoot = input.projectRoot;
2151
+ if (!projectRoot && input.provider instanceof LocalProvider) projectRoot = input.provider.projectRoot;
2152
+ return {
2153
+ provider: input.provider,
2154
+ projectRoot
2155
+ };
2156
+ }
2157
+ if (input.projectRoot) return {
2158
+ provider: new LocalProvider(input.projectRoot),
2159
+ projectRoot: input.projectRoot
2160
+ };
2161
+ throw new Error("createServer: either `provider` or `projectRoot` must be provided");
2162
+ }
2032
2163
  //#endregion
2033
2164
  export { createServer as t };
2034
2165
 
2035
- //# sourceMappingURL=server-Dtq8vqbK.mjs.map
2166
+ //# sourceMappingURL=server-BkhhtEE2.mjs.map