@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.
- package/README.md +93 -25
- package/dist/annotations-Clhbw2lh.mjs +126 -0
- package/dist/annotations-Clhbw2lh.mjs.map +1 -0
- package/dist/{apply-manager-BkPMfpsN.mjs → apply-manager-D5ym5Jba.mjs} +7 -7
- package/dist/{apply-manager-BkPMfpsN.mjs.map → apply-manager-D5ym5Jba.mjs.map} +1 -1
- package/dist/{astro-parser-BJ-54JGe.mjs → astro-parser-B-nLl0zE.mjs} +3 -3
- package/dist/{astro-parser-BJ-54JGe.mjs.map → astro-parser-B-nLl0zE.mjs.map} +1 -1
- package/dist/{branch-lifecycle-wGJg25LT.mjs → branch-lifecycle-CFVpTpNT.mjs} +33 -3
- package/dist/{branch-lifecycle-wGJg25LT.mjs.map → branch-lifecycle-CFVpTpNT.mjs.map} +1 -1
- package/dist/{compiler-rIC4ETHv.mjs → compiler-k4yttlzM.mjs} +2 -2
- package/dist/{compiler-rIC4ETHv.mjs.map → compiler-k4yttlzM.mjs.map} +1 -1
- package/dist/{compiler-sfc.cjs-BbD04rn_.mjs → compiler-sfc.cjs-D0d_OicF.mjs} +2 -2
- package/dist/{compiler-sfc.cjs-BbD04rn_.mjs.map → compiler-sfc.cjs-D0d_OicF.mjs.map} +1 -1
- package/dist/config-BIl5Ybj0.mjs +40 -0
- package/dist/config-BIl5Ybj0.mjs.map +1 -0
- package/dist/content-manager-DqDLSNQ-.d.mts +63 -0
- package/dist/content-manager-DqDLSNQ-.d.mts.map +1 -0
- package/dist/context-J0OU9Nfp.mjs +102 -0
- package/dist/context-J0OU9Nfp.mjs.map +1 -0
- package/dist/contracts-DfL0BfrD.mjs +2 -0
- package/dist/core/apply-manager.mjs +10 -10
- package/dist/core/config.d.mts +13 -0
- package/dist/core/config.d.mts.map +1 -1
- package/dist/core/config.mjs +2 -3
- package/dist/core/content-manager.d.mts +2 -45
- package/dist/core/content-manager.mjs +3 -4
- package/dist/core/context.d.mts +32 -2
- package/dist/core/context.d.mts.map +1 -1
- package/dist/core/context.mjs +7 -7
- package/dist/core/contracts/index.d.mts +2 -0
- package/dist/core/contracts/index.mjs +2 -0
- package/dist/core/doctor.d.mts +76 -0
- package/dist/core/doctor.d.mts.map +1 -0
- package/dist/core/doctor.mjs +8 -0
- package/dist/core/graph-builder.mjs +3 -4
- package/dist/core/meta-manager.mjs +2 -3
- package/dist/core/model-manager.d.mts +34 -1
- package/dist/core/model-manager.d.mts.map +1 -1
- package/dist/core/model-manager.mjs +3 -4
- package/dist/core/ops/index.d.mts +137 -0
- package/dist/core/ops/index.d.mts.map +1 -0
- package/dist/core/ops/index.mjs +4 -0
- package/dist/core/overlay-reader.d.mts +42 -0
- package/dist/core/overlay-reader.d.mts.map +1 -0
- package/dist/core/overlay-reader.mjs +2 -0
- package/dist/core/scan-config.mjs +2 -3
- package/dist/core/scanner.mjs +4 -5
- package/dist/core/validator/index.d.mts +129 -0
- package/dist/core/validator/index.d.mts.map +1 -0
- package/dist/core/validator/index.mjs +6 -0
- package/dist/{detect-DinOQAmK.mjs → detect-DL9mFmaS.mjs} +2 -2
- package/dist/{detect-DinOQAmK.mjs.map → detect-DL9mFmaS.mjs.map} +1 -1
- package/dist/doctor-CgBoT6h1.mjs +338 -0
- package/dist/doctor-CgBoT6h1.mjs.map +1 -0
- package/dist/{fs-BnIQ4dKj.mjs → fs-DLbVB-Ek.mjs} +2 -2
- package/dist/fs-DLbVB-Ek.mjs.map +1 -0
- package/dist/git/branch-lifecycle.d.mts +30 -1
- package/dist/git/branch-lifecycle.d.mts.map +1 -1
- package/dist/git/branch-lifecycle.mjs +4 -5
- package/dist/git/transaction.d.mts +2 -2
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +8 -8
- package/dist/{graph-builder-tM7zj-SX.mjs → graph-builder-DXv2JEp-.mjs} +3 -3
- package/dist/{graph-builder-tM7zj-SX.mjs.map → graph-builder-DXv2JEp-.mjs.map} +1 -1
- package/dist/{id-DV_T9Ic8.mjs → id-CIO9s0x_.mjs} +1 -1
- package/dist/{id-DV_T9Ic8.mjs.map → id-CIO9s0x_.mjs.map} +1 -1
- package/dist/index-D0wQJSgs.d.mts +2 -0
- package/dist/index.mjs +22 -17
- package/dist/index.mjs.map +1 -1
- package/dist/local-DvP4Y4T-.mjs +182 -0
- package/dist/local-DvP4Y4T-.mjs.map +1 -0
- package/dist/{meta-manager--0IwAPCl.mjs → meta-manager-C900rEyG.mjs} +2 -2
- package/dist/{meta-manager--0IwAPCl.mjs.map → meta-manager-C900rEyG.mjs.map} +1 -1
- package/dist/{model-manager-DW_fOGRM.mjs → model-manager-BhLsUgaB.mjs} +241 -49
- package/dist/model-manager-BhLsUgaB.mjs.map +1 -0
- package/dist/{node-Cbq5eR5i.mjs → node-DN9hZeil.mjs} +1 -1
- package/dist/{node-Cbq5eR5i.mjs.map → node-DN9hZeil.mjs.map} +1 -1
- package/dist/ops-DjEWi5Wa.mjs +486 -0
- package/dist/ops-DjEWi5Wa.mjs.map +1 -0
- package/dist/overlay-reader-DOdyDRIv.mjs +74 -0
- package/dist/overlay-reader-DOdyDRIv.mjs.map +1 -0
- package/dist/paths-C3xNyc7z.mjs +52 -0
- package/dist/paths-C3xNyc7z.mjs.map +1 -0
- package/dist/paths-CYl5vCBD.mjs +54 -0
- package/dist/paths-CYl5vCBD.mjs.map +1 -0
- package/dist/providers/github/index.d.mts +201 -0
- package/dist/providers/github/index.d.mts.map +1 -0
- package/dist/providers/github/index.mjs +494 -0
- package/dist/providers/github/index.mjs.map +1 -0
- package/dist/providers/gitlab/index.d.mts +158 -0
- package/dist/providers/gitlab/index.d.mts.map +1 -0
- package/dist/providers/gitlab/index.mjs +361 -0
- package/dist/providers/gitlab/index.mjs.map +1 -0
- package/dist/providers/local/index.d.mts +106 -0
- package/dist/providers/local/index.d.mts.map +1 -0
- package/dist/providers/local/index.mjs +12 -0
- package/dist/{scan-config-QgnJ1-52.mjs → scan-config-jC6XmiqF.mjs} +2 -2
- package/dist/{scan-config-QgnJ1-52.mjs.map → scan-config-jC6XmiqF.mjs.map} +1 -1
- package/dist/{scanner-DUoXnmtI.mjs → scanner-DKMwc03k.mjs} +7 -7
- package/dist/{scanner-DUoXnmtI.mjs.map → scanner-DKMwc03k.mjs.map} +1 -1
- package/dist/serialization-B1CEzR4H.mjs +2 -0
- package/dist/server/http/index.d.mts +107 -0
- package/dist/server/http/index.d.mts.map +1 -0
- package/dist/server/http/index.mjs +248 -0
- package/dist/server/http/index.mjs.map +1 -0
- package/dist/{server-Dtq8vqbK.mjs → server-BkhhtEE2.mjs} +486 -355
- package/dist/server-BkhhtEE2.mjs.map +1 -0
- package/dist/server-WqGx6ait.d.mts +50 -0
- package/dist/server-WqGx6ait.d.mts.map +1 -0
- package/dist/server.d.mts +2 -7
- package/dist/server.mjs +22 -17
- package/dist/{source-map-generator-DpO-ejrt.mjs → source-map-generator-BWDva49I.mjs} +1 -1
- package/dist/{source-map-generator-DpO-ejrt.mjs.map → source-map-generator-BWDva49I.mjs.map} +1 -1
- package/dist/{svelte-parser-1bBcMx3T.mjs → svelte-parser-Dr9mmZuk.mjs} +3 -3
- package/dist/{svelte-parser-1bBcMx3T.mjs.map → svelte-parser-Dr9mmZuk.mjs.map} +1 -1
- package/dist/templates/index.mjs +1 -1
- package/dist/{templates-bRPzYq4M.mjs → templates-VrLVqfe5.mjs} +1 -1
- package/dist/{templates-bRPzYq4M.mjs.map → templates-VrLVqfe5.mjs.map} +1 -1
- package/dist/testing/conformance.d.mts +59 -0
- package/dist/testing/conformance.d.mts.map +1 -0
- package/dist/testing/conformance.mjs +59 -0
- package/dist/testing/conformance.mjs.map +1 -0
- package/dist/tools/annotations.d.mts +24 -0
- package/dist/tools/annotations.d.mts.map +1 -0
- package/dist/tools/annotations.mjs +2 -0
- package/dist/{transaction-C_Pvux78.mjs → transaction-DgkNgWSI.mjs} +61 -32
- package/dist/transaction-DgkNgWSI.mjs.map +1 -0
- package/dist/tsx-parser-BOMZE_yQ.mjs +2 -0
- package/dist/{tsx-parser-BWiMGAIR.mjs → tsx-parser-CQc6nr-o.mjs} +1 -1
- package/dist/{tsx-parser-BWiMGAIR.mjs.map → tsx-parser-CQc6nr-o.mjs.map} +1 -1
- package/dist/util/detect.mjs +2 -3
- package/dist/util/fs.mjs +1 -2
- package/dist/util/id.mjs +1 -1
- package/dist/validator-B0nNRFeU.mjs +906 -0
- package/dist/validator-B0nNRFeU.mjs.map +1 -0
- package/dist/{vue-parser-DTPTN3Yb.mjs → vue-parser-COgjArqv.mjs} +3 -3
- package/dist/{vue-parser-DTPTN3Yb.mjs.map → vue-parser-COgjArqv.mjs.map} +1 -1
- package/package.json +82 -23
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/config.json +15 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/content/blog/blog/en.json +6 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/context.json +22 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/meta/blog/en.json +7 -0
- package/testing/conformance/01-new-collection-entry/expected/.contentrain/models/blog.json +16 -0
- package/testing/conformance/01-new-collection-entry/scenario.json +19 -0
- package/testing/conformance/01-new-collection-entry/setup/.contentrain/config.json +15 -0
- package/testing/conformance/01-new-collection-entry/setup/.contentrain/models/blog.json +16 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/config.json +15 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/content/marketing/hero/en.json +4 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/context.json +22 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/meta/hero/en.json +5 -0
- package/testing/conformance/02-new-singleton/expected/.contentrain/models/hero.json +16 -0
- package/testing/conformance/02-new-singleton/scenario.json +18 -0
- package/testing/conformance/02-new-singleton/setup/.contentrain/config.json +15 -0
- package/testing/conformance/02-new-singleton/setup/.contentrain/models/hero.json +16 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/config.json +15 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/content/marketing/ui-strings/en.json +5 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/context.json +22 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/meta/ui-strings/en.json +5 -0
- package/testing/conformance/03-new-dictionary/expected/.contentrain/models/ui-strings.json +7 -0
- package/testing/conformance/03-new-dictionary/scenario.json +19 -0
- package/testing/conformance/03-new-dictionary/setup/.contentrain/config.json +15 -0
- package/testing/conformance/03-new-dictionary/setup/.contentrain/models/ui-strings.json +7 -0
- package/testing/conformance/README.md +77 -0
- package/testing/conformance/scenarios.json +36 -0
- package/dist/config-B5kc2oeV.mjs +0 -27
- package/dist/config-B5kc2oeV.mjs.map +0 -1
- package/dist/context-DIDt_TxX.mjs +0 -46
- package/dist/context-DIDt_TxX.mjs.map +0 -1
- package/dist/core/content-manager.d.mts.map +0 -1
- package/dist/core/validator.d.mts +0 -23
- package/dist/core/validator.d.mts.map +0 -1
- package/dist/core/validator.mjs +0 -7
- package/dist/fs-BnIQ4dKj.mjs.map +0 -1
- package/dist/model-manager-DW_fOGRM.mjs.map +0 -1
- package/dist/serializer-CnHTtmc1.mjs +0 -2
- package/dist/server-Dtq8vqbK.mjs.map +0 -1
- package/dist/server.d.mts.map +0 -1
- package/dist/transaction-C_Pvux78.mjs.map +0 -1
- package/dist/tsx-parser-BFLv292Y.mjs +0 -2
- package/dist/util/serializer.d.mts +0 -2
- package/dist/util/serializer.mjs +0 -2
- package/dist/validator-SQYUV2JP.mjs +0 -581
- package/dist/validator-SQYUV2JP.mjs.map +0 -1
|
@@ -1,145 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as
|
|
3
|
-
import { n as
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { n as
|
|
8
|
-
import { n as
|
|
9
|
-
import {
|
|
10
|
-
import { t as
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { n as
|
|
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
|
-
|
|
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
|
|
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(
|
|
140
|
-
const models = await listModels(
|
|
141
|
-
const context = await readContext(projectRoot);
|
|
142
|
-
const vocabulary = await readVocabulary(
|
|
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
|
-
|
|
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 (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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(
|
|
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(
|
|
108
|
+
const config = await readConfig(provider);
|
|
202
109
|
const effectiveLocale = locale ?? config?.locales.default ?? "en";
|
|
203
|
-
const stats = await countEntries(
|
|
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(
|
|
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(
|
|
344
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
|
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
|
|
798
|
-
|
|
826
|
+
const result = await commitThroughProvider(provider, {
|
|
827
|
+
branch,
|
|
828
|
+
changes: savePlan.changes,
|
|
829
|
+
message,
|
|
830
|
+
contextPayload
|
|
799
831
|
});
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
933
|
+
const result = await commitThroughProvider(provider, {
|
|
934
|
+
branch,
|
|
935
|
+
changes: deletePlan.changes,
|
|
936
|
+
message,
|
|
937
|
+
contextPayload
|
|
897
938
|
});
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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:
|
|
912
|
-
commit:
|
|
913
|
-
...
|
|
950
|
+
action: workflowAction,
|
|
951
|
+
commit: commitSha,
|
|
952
|
+
...sync ? { sync } : {}
|
|
914
953
|
},
|
|
915
|
-
files_removed:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1057
|
+
plan = await planContentSave(provider, {
|
|
1058
|
+
model,
|
|
1059
|
+
entries: input.entries,
|
|
1060
|
+
config,
|
|
1061
|
+
vocabulary
|
|
1024
1062
|
});
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
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:
|
|
1215
|
+
files_removed: deletePlan.result,
|
|
1126
1216
|
git: {
|
|
1127
1217
|
branch,
|
|
1128
|
-
action:
|
|
1129
|
-
commit:
|
|
1130
|
-
...
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
}
|
|
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 (!
|
|
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.
|
|
2125
|
+
var version = "1.4.0";
|
|
2016
2126
|
//#endregion
|
|
2017
2127
|
//#region src/server.ts
|
|
2018
|
-
function createServer(
|
|
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-
|
|
2166
|
+
//# sourceMappingURL=server-BkhhtEE2.mjs.map
|