rpg0-dotcms-mcp 1.1.0 → 1.1.2
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 +162 -0
- package/dotcms.js +41 -0
- package/hooks/confirm-write.mjs +2 -1
- package/index.js +65 -4
- package/package.json +25 -3
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# rpg0-dotcms-mcp
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server for [dotCMS](https://www.dotcms.com). It lets an AI assistant (GitHub Copilot, Claude Code, Claude Desktop, Cursor, …) inspect content types and their fields, and read — and optionally write — Velocity (VTL) files in a dotCMS instance.
|
|
4
|
+
|
|
5
|
+
- **Read-only by default.** Write tools are only exposed when you opt in.
|
|
6
|
+
- **Writes are plan-first.** A change is planned and shown as a diff before anything is saved, and is limited to folders you allow.
|
|
7
|
+
- **No install step.** Runs over stdio through `npx`.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Node.js 18 or newer
|
|
12
|
+
- A dotCMS instance reachable from the machine that runs your AI client
|
|
13
|
+
- A dotCMS API token (dotCMS admin → your user → *API Access Tokens*)
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
DOTCMS_URL=https://your-dotcms.example.com DOTCMS_TOKEN=your-token npx -y rpg0-dotcms-mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The server speaks MCP over stdio, so you normally don't run it by hand — your AI client starts it using one of the configurations below.
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
All configuration is through environment variables.
|
|
26
|
+
|
|
27
|
+
| Variable | Default | Purpose |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `DOTCMS_URL` | `http://localhost:8080` | Base URL of the dotCMS instance |
|
|
30
|
+
| `DOTCMS_TOKEN` | *(none)* | API token, sent as a `Bearer` token |
|
|
31
|
+
| `DOTCMS_ALLOW_WRITE` | `false` | Set to `true` to expose the write tools |
|
|
32
|
+
| `DOTCMS_WRITE_ROOTS` | `/application/apivtl/,/application/vtl/custom-fields/` | Comma-separated folders the write tools are limited to |
|
|
33
|
+
| `DOTCMS_READ_EXTENSIONS` | `vtl` | Comma-separated file extensions the read tools may return |
|
|
34
|
+
| `DOTCMS_CONFIRM` | *(on)* | Set to `off` to skip the server's own confirmation prompt before a write |
|
|
35
|
+
| `DOTCMS_PLANS_DIR` | `~/.dotcms-mcp/plans` | Where pending change plans are stored |
|
|
36
|
+
|
|
37
|
+
## Client setup
|
|
38
|
+
|
|
39
|
+
### GitHub Copilot (VS Code)
|
|
40
|
+
|
|
41
|
+
Create `.vscode/mcp.json` in your workspace, then start the server from the editor and use Copilot Chat in Agent mode. VS Code prompts for the token once and stores it securely.
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"inputs": [
|
|
46
|
+
{
|
|
47
|
+
"id": "dotcms-token",
|
|
48
|
+
"type": "promptString",
|
|
49
|
+
"description": "dotCMS API token",
|
|
50
|
+
"password": true
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"servers": {
|
|
54
|
+
"dotcms": {
|
|
55
|
+
"type": "stdio",
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["-y", "rpg0-dotcms-mcp"],
|
|
58
|
+
"env": {
|
|
59
|
+
"DOTCMS_URL": "https://your-dotcms.example.com",
|
|
60
|
+
"DOTCMS_TOKEN": "${input:dotcms-token}"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Claude Code
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
claude mcp add dotcms --env DOTCMS_URL=https://your-dotcms.example.com --env DOTCMS_TOKEN=your-token -- npx -y rpg0-dotcms-mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Claude Desktop, Cursor and other clients
|
|
74
|
+
|
|
75
|
+
Add the server to the client's MCP configuration:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"dotcms": {
|
|
81
|
+
"command": "npx",
|
|
82
|
+
"args": ["-y", "rpg0-dotcms-mcp"],
|
|
83
|
+
"env": {
|
|
84
|
+
"DOTCMS_URL": "https://your-dotcms.example.com",
|
|
85
|
+
"DOTCMS_TOKEN": "your-token"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Tools
|
|
93
|
+
|
|
94
|
+
### Always available (read-only)
|
|
95
|
+
|
|
96
|
+
| Tool | What it does |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `list_content_types` | Lists content types, with optional name filter, base-type filter and paging |
|
|
99
|
+
| `get_content_type_fields` | Returns the fields of a content type (variable, type, data type, required/unique/indexed flags, field variables, relationships). Layout-only fields are hidden unless requested |
|
|
100
|
+
| `list_vtl_files` | Finds VTL files by folder and/or file name, with paging |
|
|
101
|
+
| `read_vtl_file` | Returns the source of a VTL file by path or identifier, optionally for a specific site or language |
|
|
102
|
+
|
|
103
|
+
The two file tools only return files whose extension is listed in `DOTCMS_READ_EXTENSIONS`.
|
|
104
|
+
|
|
105
|
+
### Only with `DOTCMS_ALLOW_WRITE=true`
|
|
106
|
+
|
|
107
|
+
| Tool | What it does |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `plan_vtl_change` | Prepares a create or update of a VTL file and returns a plan: target, publish flag, line diff and warnings. Writes nothing |
|
|
110
|
+
| `apply_vtl_change` | Applies a previously created plan by its `planId` |
|
|
111
|
+
|
|
112
|
+
## How writing works
|
|
113
|
+
|
|
114
|
+
1. The assistant calls `plan_vtl_change` with the full new file content. The server compares it with the current file and stores a plan.
|
|
115
|
+
2. The plan is shown: instance, site, path, language, whether it publishes, and the changed lines.
|
|
116
|
+
3. The assistant calls `apply_vtl_change` with the `planId`. Only what the plan contains can be written.
|
|
117
|
+
|
|
118
|
+
Safeguards:
|
|
119
|
+
|
|
120
|
+
- Paths outside `DOTCMS_WRITE_ROOTS` are rejected.
|
|
121
|
+
- The site must be a plain host name.
|
|
122
|
+
- A plan is valid for 30 minutes, can be used once, and only in the server session that created it. A plan file that was edited on disk is refused.
|
|
123
|
+
- By default a change is saved as a working version; the live file is only replaced when publishing is explicitly requested.
|
|
124
|
+
- Before writing, the server asks you to confirm through your client when the client supports MCP elicitation (VS Code does). Declining writes nothing. Set `DOTCMS_CONFIRM=off` to turn this prompt off.
|
|
125
|
+
|
|
126
|
+
If your client does not support elicitation, its own tool-approval prompt is the only check before `apply_vtl_change` runs, so keep approval enabled for that tool and do not auto-approve it.
|
|
127
|
+
|
|
128
|
+
### Optional: confirmation hook for Claude Code
|
|
129
|
+
|
|
130
|
+
The package ships a `PreToolUse` hook that shows the full plan and asks for approval every time `apply_vtl_change` is called, and denies calls without a valid plan. If you use it, you can set `DOTCMS_CONFIRM=off` to avoid being asked twice. Add it to your Claude Code `settings.json`:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"hooks": {
|
|
135
|
+
"PreToolUse": [
|
|
136
|
+
{
|
|
137
|
+
"matcher": "mcp__.*__apply_vtl_change",
|
|
138
|
+
"hooks": [
|
|
139
|
+
{
|
|
140
|
+
"type": "command",
|
|
141
|
+
"command": "npx -y -p rpg0-dotcms-mcp rpg0-dotcms-mcp-confirm-write"
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Troubleshooting
|
|
151
|
+
|
|
152
|
+
- **`Cannot reach dotCMS at …`** — the instance isn't running or isn't reachable from this machine. Check `DOTCMS_URL`.
|
|
153
|
+
- **401 / 403 responses** — the token is missing, expired, or belongs to a different instance. Tokens are per instance.
|
|
154
|
+
- **Write tools are missing** — set `DOTCMS_ALLOW_WRITE=true` and restart the server.
|
|
155
|
+
- **A write is rejected** — the path is outside `DOTCMS_WRITE_ROOTS`, or the plan expired; create a new plan.
|
|
156
|
+
|
|
157
|
+
## Security notes
|
|
158
|
+
|
|
159
|
+
- Give the token only the permissions the assistant needs; use a read-only user if you don't need writes.
|
|
160
|
+
- Use `https` for any dotCMS that isn't on your own machine; the server warns at startup when the token would travel over plain `http`.
|
|
161
|
+
- Never commit the token. Prefer your client's secret prompt or an environment variable.
|
|
162
|
+
- The server sends requests only to `DOTCMS_URL`.
|
package/dotcms.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// DOTCMS_ALLOW_WRITE "true" to expose the create/update tools (default: read-only)
|
|
7
7
|
// DOTCMS_WRITE_ROOTS comma-separated folders the write tools are limited to
|
|
8
8
|
// (default: /application/apivtl/,/application/vtl/custom-fields/)
|
|
9
|
+
// DOTCMS_READ_EXTENSIONS comma-separated file extensions the read tools may return (default: vtl)
|
|
10
|
+
// DOTCMS_CONFIRM "off" to skip the server's own confirmation prompt before a write
|
|
11
|
+
// (default: ask through the client when it supports MCP elicitation)
|
|
9
12
|
|
|
10
13
|
export const BASE_URL = (process.env.DOTCMS_URL || "http://localhost:8080").replace(/\/+$/, "");
|
|
11
14
|
export const TOKEN = process.env.DOTCMS_TOKEN || "";
|
|
@@ -18,6 +21,42 @@ export const WRITE_ROOTS = (
|
|
|
18
21
|
.filter(Boolean)
|
|
19
22
|
.map((root) => `/${root}/`);
|
|
20
23
|
|
|
24
|
+
export const READ_EXTENSIONS = (process.env.DOTCMS_READ_EXTENSIONS || "vtl")
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((ext) => ext.trim().replace(/^\./, "").toLowerCase())
|
|
27
|
+
.filter((ext) => /^[a-z0-9]+$/.test(ext));
|
|
28
|
+
export const CONFIRM = !/^(off|false|0|no)$/i.test(process.env.DOTCMS_CONFIRM || "");
|
|
29
|
+
|
|
30
|
+
/** True when the token would travel unencrypted to another machine. */
|
|
31
|
+
export const INSECURE_TRANSPORT = (() => {
|
|
32
|
+
try {
|
|
33
|
+
const { protocol, hostname } = new URL(BASE_URL);
|
|
34
|
+
const local = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".localhost");
|
|
35
|
+
return !!TOKEN && protocol === "http:" && !local;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
|
|
41
|
+
/** A site is a host name; anything else could change where `site:folder` points. */
|
|
42
|
+
export function assertSite(site) {
|
|
43
|
+
if (!/^[A-Za-z0-9]([A-Za-z0-9._-]{0,251}[A-Za-z0-9])?$/.test(String(site ?? ""))) {
|
|
44
|
+
throw new Error(`Invalid site '${site}': pass the site host name, e.g. 'demo.dotcms.com'.`);
|
|
45
|
+
}
|
|
46
|
+
return site;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Throws unless the file name ends in one of DOTCMS_READ_EXTENSIONS. */
|
|
50
|
+
export function assertReadable(fileName) {
|
|
51
|
+
const ext = String(fileName ?? "").split(".").pop().toLowerCase();
|
|
52
|
+
if (!String(fileName ?? "").includes(".") || !READ_EXTENSIONS.includes(ext)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`'${fileName}' is not a ${READ_EXTENSIONS.map((e) => `.${e}`).join(" / ")} file. ` +
|
|
55
|
+
"This server only reads the extensions listed in DOTCMS_READ_EXTENSIONS.",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
21
60
|
const authHeaders = (extra = {}) => (TOKEN ? { ...extra, Authorization: `Bearer ${TOKEN}` } : extra);
|
|
22
61
|
|
|
23
62
|
async function send(path, init = {}) {
|
|
@@ -174,6 +213,7 @@ export function parseWritePath(path) {
|
|
|
174
213
|
* through the dotCMS workflow API. Not published unless `publish` is true.
|
|
175
214
|
*/
|
|
176
215
|
export async function saveFile({ site, folder, fileName, content, identifier, languageId, publish }) {
|
|
216
|
+
assertSite(site);
|
|
177
217
|
const contentlet = {
|
|
178
218
|
contentType: "FileAsset",
|
|
179
219
|
title: fileName,
|
|
@@ -199,6 +239,7 @@ export async function saveFile({ site, folder, fileName, content, identifier, la
|
|
|
199
239
|
/** Make sure a folder exists. Returns an error message instead of throwing. */
|
|
200
240
|
export async function ensureFolder(site, folder) {
|
|
201
241
|
try {
|
|
242
|
+
assertSite(site);
|
|
202
243
|
await api(`/api/v1/folder/createfolders/${encodeURIComponent(site)}`, {
|
|
203
244
|
method: "POST",
|
|
204
245
|
body: [folder],
|
package/hooks/confirm-write.mjs
CHANGED
|
@@ -28,7 +28,8 @@ try {
|
|
|
28
28
|
} catch {
|
|
29
29
|
process.exit(0); // not a hook payload: stay out of the way
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
// Whatever name the server was registered under.
|
|
32
|
+
if (!/^mcp__.+__apply_vtl_change$/.test(String(input.tool_name ?? ""))) process.exit(0);
|
|
32
33
|
|
|
33
34
|
let plan;
|
|
34
35
|
try {
|
package/index.js
CHANGED
|
@@ -10,13 +10,19 @@
|
|
|
10
10
|
|
|
11
11
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import crypto from "node:crypto";
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
import {
|
|
15
16
|
ALLOW_WRITE,
|
|
16
17
|
BASE_URL,
|
|
18
|
+
CONFIRM,
|
|
19
|
+
INSECURE_TRANSPORT,
|
|
20
|
+
READ_EXTENSIONS,
|
|
17
21
|
TOKEN,
|
|
18
22
|
WRITE_ROOTS,
|
|
19
23
|
api,
|
|
24
|
+
assertReadable,
|
|
25
|
+
assertSite,
|
|
20
26
|
downloadFile,
|
|
21
27
|
ensureFolder,
|
|
22
28
|
escapeLucene,
|
|
@@ -84,7 +90,7 @@ const mapFile = (f) => ({
|
|
|
84
90
|
const ok = (data) => ({ content: [{ type: "text", text: JSON.stringify(data, null, 2) }] });
|
|
85
91
|
const fail = (err) => ({ isError: true, content: [{ type: "text", text: err.message }] });
|
|
86
92
|
|
|
87
|
-
const server = new McpServer({ name: "dotcms", version: "1.1.
|
|
93
|
+
const server = new McpServer({ name: "dotcms", version: "1.1.2" });
|
|
88
94
|
|
|
89
95
|
// ---------------------------------------------------------------------------
|
|
90
96
|
// Read-only tools
|
|
@@ -189,7 +195,7 @@ server.registerTool(
|
|
|
189
195
|
.optional()
|
|
190
196
|
.describe("Only files whose path contains this folder, e.g. '/application/vtl' or 'containers/banner'"),
|
|
191
197
|
name: z.string().optional().describe("Only files whose file name contains this text, e.g. 'banner'"),
|
|
192
|
-
extension: z.string().default("vtl").describe(
|
|
198
|
+
extension: z.string().default("vtl").describe(`File extension to look for (default 'vtl'; allowed: ${READ_EXTENSIONS.join(", ")})`),
|
|
193
199
|
page: z.number().int().min(1).default(1).describe("Page number"),
|
|
194
200
|
perPage: z.number().int().min(1).max(100).default(50).describe("Items per page"),
|
|
195
201
|
},
|
|
@@ -197,6 +203,7 @@ server.registerTool(
|
|
|
197
203
|
},
|
|
198
204
|
async ({ folder, name, extension, page, perPage }) => {
|
|
199
205
|
try {
|
|
206
|
+
assertReadable(`x.${extension.replace(/^\./, "")}`);
|
|
200
207
|
const ext = escapeLucene(extension.replace(/^\./, "").toLowerCase());
|
|
201
208
|
const clauses = [`+path:*.${ext}`];
|
|
202
209
|
if (folder) {
|
|
@@ -240,6 +247,7 @@ server.registerTool(
|
|
|
240
247
|
if (!path && !identifier) throw new Error("Pass either 'path' or 'identifier'.");
|
|
241
248
|
const files = await findFiles({ path, identifier, site });
|
|
242
249
|
const { file, languages } = pickVersion(files, { what: identifier || path, site, languageId });
|
|
250
|
+
assertReadable(file.fileName || file.name || file.path);
|
|
243
251
|
|
|
244
252
|
const content = await downloadFile(file);
|
|
245
253
|
const truncated = content.length > MAX_FILE_CHARS;
|
|
@@ -263,6 +271,37 @@ server.registerTool(
|
|
|
263
271
|
if (ALLOW_WRITE) {
|
|
264
272
|
const roots = WRITE_ROOTS.join(" and ");
|
|
265
273
|
|
|
274
|
+
// Fingerprints of the plans this process created. apply_vtl_change refuses a plan whose
|
|
275
|
+
// file on disk no longer matches, so editing a plan file cannot change what gets written.
|
|
276
|
+
const planFingerprints = new Map();
|
|
277
|
+
const fingerprint = (plan) => crypto.createHash("sha256").update(JSON.stringify(plan)).digest("hex");
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Ask the user through the client (MCP elicitation) when it can. Returns "confirmed",
|
|
281
|
+
* "declined", or "unsupported" when the client cannot ask — its own tool approval is then
|
|
282
|
+
* the only check.
|
|
283
|
+
*/
|
|
284
|
+
async function confirmWithUser(plan) {
|
|
285
|
+
if (!CONFIRM || !server.server.getClientCapabilities()?.elicitation) return "unsupported";
|
|
286
|
+
let answer;
|
|
287
|
+
try {
|
|
288
|
+
answer = await server.server.elicitInput({
|
|
289
|
+
message: `Apply this change to dotCMS?\n\n${renderPlan(plan)}`,
|
|
290
|
+
requestedSchema: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
293
|
+
apply: { type: "boolean", title: "Apply this change", description: `${plan.action} ${plan.path} on ${plan.site}` },
|
|
294
|
+
},
|
|
295
|
+
required: ["apply"],
|
|
296
|
+
},
|
|
297
|
+
}, { timeout: 10 * 60 * 1000 }); // give the user time to read the diff
|
|
298
|
+
} catch (err) {
|
|
299
|
+
if (/does not support/i.test(err.message)) return "unsupported";
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
return answer.action === "accept" && answer.content?.apply === true ? "confirmed" : "declined";
|
|
303
|
+
}
|
|
304
|
+
|
|
266
305
|
server.registerTool(
|
|
267
306
|
"plan_vtl_change",
|
|
268
307
|
{
|
|
@@ -296,6 +335,7 @@ if (ALLOW_WRITE) {
|
|
|
296
335
|
async ({ path, site, content, languageId, publish }) => {
|
|
297
336
|
try {
|
|
298
337
|
const target = parseWritePath(path);
|
|
338
|
+
if (site !== undefined) assertSite(site);
|
|
299
339
|
const files = await findFiles({ path: target.full, site });
|
|
300
340
|
|
|
301
341
|
let action = "CREATE";
|
|
@@ -352,6 +392,7 @@ if (ALLOW_WRITE) {
|
|
|
352
392
|
warnings,
|
|
353
393
|
hints,
|
|
354
394
|
});
|
|
395
|
+
planFingerprints.set(plan.id, fingerprint(plan));
|
|
355
396
|
|
|
356
397
|
return {
|
|
357
398
|
content: [
|
|
@@ -360,7 +401,7 @@ if (ALLOW_WRITE) {
|
|
|
360
401
|
text:
|
|
361
402
|
renderPlan(plan) +
|
|
362
403
|
`\n\nplanId: ${plan.id}\nNothing has been written. Show this plan to the user; to carry it out call ` +
|
|
363
|
-
"apply_vtl_change with this planId
|
|
404
|
+
"apply_vtl_change with this planId.",
|
|
364
405
|
},
|
|
365
406
|
],
|
|
366
407
|
};
|
|
@@ -377,7 +418,8 @@ if (ALLOW_WRITE) {
|
|
|
377
418
|
description:
|
|
378
419
|
"Step 2 of 2. WRITES to dotCMS: creates or updates the VTL file exactly as described by a plan from " +
|
|
379
420
|
"plan_vtl_change. Takes only the planId, so nothing but the planned content can be written. " +
|
|
380
|
-
"
|
|
421
|
+
"Only call it after the user has seen the plan and agreed to it. When the client supports it, the server " +
|
|
422
|
+
"also asks the user to confirm. A plan works once, only in the server session that made it, and expires after 30 minutes; " +
|
|
381
423
|
"it is rejected if the file changed in dotCMS after the plan was made.",
|
|
382
424
|
inputSchema: {
|
|
383
425
|
planId: z.string().min(1).describe("planId returned by plan_vtl_change"),
|
|
@@ -387,9 +429,24 @@ if (ALLOW_WRITE) {
|
|
|
387
429
|
async ({ planId }) => {
|
|
388
430
|
try {
|
|
389
431
|
const plan = loadPlan(planId);
|
|
432
|
+
if (planFingerprints.get(plan.id) !== fingerprint(plan)) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`Plan ${plan.id} was not made by this server session, or its file was changed afterwards. Make a new plan with plan_vtl_change.`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
390
437
|
if (plan.instance !== BASE_URL) {
|
|
391
438
|
throw new Error(`Plan ${plan.id} was made for ${plan.instance}, but this server is connected to ${BASE_URL}.`);
|
|
392
439
|
}
|
|
440
|
+
// Never trust the stored plan for where it may write: check it again.
|
|
441
|
+
const target = parseWritePath(plan.path);
|
|
442
|
+
if (target.folder !== plan.folder || target.fileName !== plan.fileName) {
|
|
443
|
+
throw new Error(`Plan ${plan.id} is inconsistent. Make a new plan with plan_vtl_change.`);
|
|
444
|
+
}
|
|
445
|
+
assertSite(plan.site);
|
|
446
|
+
|
|
447
|
+
if ((await confirmWithUser(plan)) === "declined") {
|
|
448
|
+
throw new Error(`The user declined plan ${plan.id}. Nothing was written.`);
|
|
449
|
+
}
|
|
393
450
|
|
|
394
451
|
// The plan must still describe reality.
|
|
395
452
|
const files = (await findFiles({ path: plan.path, site: plan.site })).filter(
|
|
@@ -419,6 +476,7 @@ if (ALLOW_WRITE) {
|
|
|
419
476
|
throw err;
|
|
420
477
|
}
|
|
421
478
|
deletePlan(plan.id);
|
|
479
|
+
planFingerprints.delete(plan.id);
|
|
422
480
|
|
|
423
481
|
return ok({
|
|
424
482
|
result: `${plan.action === "CREATE" ? "Created" : "Updated"} ${plan.path} on ${plan.site}`,
|
|
@@ -444,3 +502,6 @@ console.error(
|
|
|
444
502
|
`dotcms-mcp running (${BASE_URL}${TOKEN ? "" : ", no DOTCMS_TOKEN set"}, ` +
|
|
445
503
|
`${ALLOW_WRITE ? `writes enabled under ${WRITE_ROOTS.join(", ")}` : "read-only"})`,
|
|
446
504
|
);
|
|
505
|
+
if (INSECURE_TRANSPORT) {
|
|
506
|
+
console.error("WARNING: DOTCMS_URL is plain http on a remote host — the API token is sent unencrypted. Use https.");
|
|
507
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rpg0-dotcms-mcp",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "MCP server for
|
|
3
|
+
"version": "1.1.2",
|
|
4
|
+
"description": "MCP server for dotCMS: inspect content types and fields, and read or safely write Velocity (VTL) files from AI assistants such as GitHub Copilot and Claude",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"mcp-server",
|
|
9
|
+
"dotcms",
|
|
10
|
+
"cms",
|
|
11
|
+
"velocity",
|
|
12
|
+
"vtl",
|
|
13
|
+
"content-types",
|
|
14
|
+
"copilot",
|
|
15
|
+
"claude",
|
|
16
|
+
"ai"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/DenysDatsiv/dotcms-mcp#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/DenysDatsiv/dotcms-mcp.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/DenysDatsiv/dotcms-mcp/issues"
|
|
25
|
+
},
|
|
26
|
+
"author": "Denys Datsiv",
|
|
5
27
|
"type": "module",
|
|
6
28
|
"main": "index.js",
|
|
7
29
|
"bin": {
|
|
@@ -13,6 +35,6 @@
|
|
|
13
35
|
"engines": { "node": ">=18" },
|
|
14
36
|
"dependencies": {
|
|
15
37
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
16
|
-
"zod": "^
|
|
38
|
+
"zod": "^4.6.6"
|
|
17
39
|
}
|
|
18
40
|
}
|