@abinnovision/payloadcms-mcpx 2.0.0-beta.19 → 2.0.0-beta.22
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 +2 -2
- package/dist/admin/capability-matrix.mjs +213 -316
- package/dist/admin/capability-toggles.mjs +24 -31
- package/dist/admin/confirmations-panel.mjs +61 -71
- package/dist/admin/confirmations.mjs +2 -1
- package/dist/admin/endpoint-url.mjs +7 -0
- package/dist/admin/setup-guide.d.mts +3 -3
- package/dist/admin/setup-guide.mjs +51 -33
- package/dist/api-keys/capability-matrix.d.mts +3 -6
- package/dist/api-keys/capability-matrix.mjs +9 -48
- package/dist/api-keys/collection.mjs +9 -7
- package/dist/api-keys/confirmation-view.mjs +2 -1
- package/dist/api-keys/fields.mjs +36 -66
- package/dist/auth/resolve.mjs +29 -10
- package/dist/capabilities.d.mts +2 -0
- package/dist/capabilities.mjs +62 -19
- package/dist/endpoint/confirmations.mjs +14 -14
- package/dist/endpoint/handler.mjs +6 -22
- package/dist/endpoint/http.mjs +27 -0
- package/dist/endpoint/upload.mjs +11 -43
- package/dist/grants/confirmation.mjs +11 -42
- package/dist/grants/grant.mjs +58 -40
- package/dist/guards.mjs +8 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/options.mjs +34 -54
- package/dist/plugin.mjs +2 -1
- package/dist/request.mjs +5 -1
- package/dist/result.mjs +11 -1
- package/dist/schema/admin-hidden.mjs +4 -4
- package/dist/schema/describe.mjs +12 -18
- package/dist/schema/index.mjs +5 -6
- package/dist/schema/lexical-pointer.mjs +8 -8
- package/dist/schema/lexical.mjs +15 -13
- package/dist/schema/outline.mjs +4 -5
- package/dist/schema/path.mjs +4 -1
- package/dist/schema/pointer.mjs +11 -17
- package/dist/schema/shape.mjs +38 -50
- package/dist/schema/walk.mjs +57 -18
- package/dist/tools/create-document.mjs +16 -44
- package/dist/tools/delete-document.mjs +14 -17
- package/dist/tools/describe-schema.mjs +7 -5
- package/dist/tools/document.mjs +130 -44
- package/dist/tools/duplicate-document.mjs +11 -10
- package/dist/tools/find-documents.mjs +15 -20
- package/dist/tools/find-versions.mjs +10 -14
- package/dist/tools/get-document.mjs +8 -12
- package/dist/tools/links.mjs +2 -1
- package/dist/tools/list-capabilities.mjs +2 -1
- package/dist/tools/patch-document.mjs +12 -44
- package/dist/tools/publish-document.mjs +9 -27
- package/dist/tools/query-paths.mjs +1 -1
- package/dist/tools/run-confirmed.mjs +38 -50
- package/dist/tools/shared.mjs +43 -41
- package/dist/tools/validate-document.mjs +4 -6
- package/dist/tools/versions.mjs +11 -6
- package/dist/types.d.mts +15 -28
- package/dist/upload/file.mjs +10 -18
- package/dist/write/draft-guard.mjs +20 -20
- package/dist/write/patch.mjs +4 -4
- package/dist/write/publish-intent.mjs +22 -28
- package/dist/write/row-ids.mjs +3 -1
- package/dist/write/write-data.mjs +18 -31
- package/package.json +7 -7
- package/dist/builtin-tool-names.mjs +0 -19
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { propOf } from "../guards.mjs";
|
|
2
2
|
import { isMcpxRequest } from "../request.mjs";
|
|
3
|
+
import { hasPublishIntent, hasTrashIntent, takeDuplicateIntent, takePublishIntent, takeTrashIntent, withTrashIntent } from "./publish-intent.mjs";
|
|
3
4
|
import { APIError } from "payload";
|
|
4
5
|
import { hasDraftsEnabled } from "payload/shared";
|
|
5
6
|
//#region src/write/draft-guard.ts
|
|
@@ -58,7 +59,7 @@ const refuseUnlessExpected = (req, slug, data) => {
|
|
|
58
59
|
const publishing = takePublishIntent(data);
|
|
59
60
|
const trashing = takeTrashIntent(data);
|
|
60
61
|
if (!isMcpxRequest(req) || trashing) return;
|
|
61
|
-
const status = data
|
|
62
|
+
const status = propOf(data, "_status");
|
|
62
63
|
const expected = publishing ? "published" : "draft";
|
|
63
64
|
if (status === expected) return;
|
|
64
65
|
req.payload.logger.warn(`[payloadcms-mcpx] Refused a write to ${slug} that would not have been a ${expected} (_status: ${String(status)}).`);
|
|
@@ -76,30 +77,29 @@ const refusePublishGlobal = ({ data, global, req }) => {
|
|
|
76
77
|
refuseUnlessExpected(req, global.slug, next);
|
|
77
78
|
return next;
|
|
78
79
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
* (it does nothing outside MCP requests) and `refusePublish` wherever drafts
|
|
82
|
-
* exist. It runs on the final collection list, so no collection escapes it, and
|
|
83
|
-
* both hooks are appended last, so a user hook cannot override them.
|
|
84
|
-
*/ const installDraftGuards = (collections) => collections.map((collection) => ({
|
|
85
|
-
...collection,
|
|
80
|
+
const appendGuards = (entities, guards) => entities.map((entity) => ({
|
|
81
|
+
...entity,
|
|
86
82
|
hooks: {
|
|
87
|
-
...
|
|
88
|
-
beforeOperation: [...
|
|
89
|
-
...hasDraftsEnabled(
|
|
83
|
+
...entity.hooks,
|
|
84
|
+
beforeOperation: [...entity.hooks?.beforeOperation ?? [], guards.beforeOperation],
|
|
85
|
+
...hasDraftsEnabled(entity) ? { beforeChange: [...entity.hooks?.beforeChange ?? [], guards.beforeChange] } : {}
|
|
90
86
|
}
|
|
91
87
|
}));
|
|
92
88
|
/**
|
|
89
|
+
* Attaches the draft guard to every collection: `forceDraftWrite` everywhere
|
|
90
|
+
* (it does nothing outside MCP requests) and `refusePublish` wherever drafts
|
|
91
|
+
* exist. It runs on the final collection list, so no collection escapes it.
|
|
92
|
+
*/ const installDraftGuards = (collections) => appendGuards(collections, {
|
|
93
|
+
beforeChange: refusePublish,
|
|
94
|
+
beforeOperation: forceDraftWrite
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
93
97
|
* Attaches the guard to every global, exposed or not: a custom tool running on
|
|
94
98
|
* an MCP request must not be able to publish through a global the plugin config
|
|
95
99
|
* never mentioned.
|
|
96
|
-
*/ const installGlobalDraftGuards = (globals) => globals
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
beforeOperation: [...global.hooks?.beforeOperation ?? [], forceDraftWriteGlobal],
|
|
101
|
-
...hasDraftsEnabled(global) ? { beforeChange: [...global.hooks?.beforeChange ?? [], refusePublishGlobal] } : {}
|
|
102
|
-
}
|
|
103
|
-
}));
|
|
100
|
+
*/ const installGlobalDraftGuards = (globals) => appendGuards(globals, {
|
|
101
|
+
beforeChange: refusePublishGlobal,
|
|
102
|
+
beforeOperation: forceDraftWriteGlobal
|
|
103
|
+
});
|
|
104
104
|
//#endregion
|
|
105
105
|
export { forceDraftWrite, forceDraftWriteGlobal, installDraftGuards, installGlobalDraftGuards, refusePublish, refusePublishGlobal };
|
package/dist/write/patch.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { isIndexSegment, joinPath, prototypeSegmentProblem, splitPath } from "../schema/path.mjs";
|
|
2
|
-
import { RESERVED_FIELD_NAMES, descriptorsUnder } from "../schema/walk.mjs";
|
|
3
1
|
import { SchemaError } from "../schema/errors.mjs";
|
|
2
|
+
import { isIndexSegment, parentPointer, prototypeSegmentProblem, splitPath } from "../schema/path.mjs";
|
|
3
|
+
import { RESERVED_FIELD_NAMES, descriptorsUnder } from "../schema/walk.mjs";
|
|
4
4
|
import { resolveDataPointer } from "../schema/pointer.mjs";
|
|
5
5
|
import { EMPTY_ROOT, validateWriteValue } from "../schema/shape.mjs";
|
|
6
6
|
import "../schema/index.mjs";
|
|
@@ -37,7 +37,7 @@ const resolvesReadOnly = (resolution) => {
|
|
|
37
37
|
};
|
|
38
38
|
const isReadOnlyPointer = (config, target) => resolvesReadOnly(resolveDataPointer(config, {
|
|
39
39
|
doc: target.doc,
|
|
40
|
-
pointer: isElementPointer(target.pointer) ?
|
|
40
|
+
pointer: isElementPointer(target.pointer) ? parentPointer(target.pointer) : target.pointer,
|
|
41
41
|
ref: target.ref
|
|
42
42
|
}));
|
|
43
43
|
const resolutionAt = (config, target) => {
|
|
@@ -60,7 +60,7 @@ const droppedFieldProblem = (config, target) => {
|
|
|
60
60
|
return `"${target.pointer}" is a field, not a list element, and removing it would do nothing. The patched document is written whole, and Payload keeps any field absent from a write rather than clearing it. Use "replace" with null to clear a field, or with [] to empty a list.`;
|
|
61
61
|
};
|
|
62
62
|
const emptiesTheRoot = (config, target) => {
|
|
63
|
-
const list =
|
|
63
|
+
const list = parentPointer(target.pointer);
|
|
64
64
|
const owner = resolutionAt(config, {
|
|
65
65
|
...target,
|
|
66
66
|
pointer: list
|
|
@@ -1,37 +1,31 @@
|
|
|
1
1
|
import { isPlainObject } from "../guards.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
//#region src/write/publish-intent.ts
|
|
4
|
-
const PUBLISH_INTENT = "__mcpxPublishIntent";
|
|
5
|
-
const TRASH_INTENT = "__mcpxTrashIntent";
|
|
6
|
-
const DUPLICATE_INTENT = "__mcpxDuplicateIntent";
|
|
7
4
|
const TOKEN = randomUUID();
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
const intent = (key) => ({
|
|
6
|
+
has: (data) => isPlainObject(data) && data[key] === TOKEN,
|
|
7
|
+
take: (data) => {
|
|
8
|
+
if (!isPlainObject(data) || data[key] !== TOKEN) return false;
|
|
9
|
+
delete data[key];
|
|
10
|
+
return true;
|
|
11
|
+
},
|
|
12
|
+
with: (data) => ({
|
|
13
|
+
...data,
|
|
14
|
+
[key]: TOKEN
|
|
15
|
+
})
|
|
16
16
|
});
|
|
17
|
-
const
|
|
17
|
+
const publish = intent("__mcpxPublishIntent");
|
|
18
|
+
const trash = intent("__mcpxTrashIntent");
|
|
19
|
+
const duplicate = intent("__mcpxDuplicateIntent");
|
|
20
|
+
const withPublishIntent = publish.with;
|
|
21
|
+
const hasPublishIntent = publish.has;
|
|
18
22
|
/**
|
|
19
23
|
* Called by the last hook that needs the marker, which removes it.
|
|
20
|
-
*/ const takePublishIntent =
|
|
21
|
-
const withTrashIntent =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
/**
|
|
27
|
-
* The counterpart of {@link takePublishIntent}.
|
|
28
|
-
*/ const takeTrashIntent = (data) => take(data, TRASH_INTENT);
|
|
29
|
-
const withDuplicateIntent = (data) => ({
|
|
30
|
-
...data,
|
|
31
|
-
[DUPLICATE_INTENT]: TOKEN
|
|
32
|
-
});
|
|
33
|
-
/**
|
|
34
|
-
* Removes the marker, so it never reaches the operation.
|
|
35
|
-
*/ const takeDuplicateIntent = (data) => take(data, DUPLICATE_INTENT);
|
|
24
|
+
*/ const takePublishIntent = publish.take;
|
|
25
|
+
const withTrashIntent = trash.with;
|
|
26
|
+
const hasTrashIntent = trash.has;
|
|
27
|
+
const takeTrashIntent = trash.take;
|
|
28
|
+
const withDuplicateIntent = duplicate.with;
|
|
29
|
+
const takeDuplicateIntent = duplicate.take;
|
|
36
30
|
//#endregion
|
|
37
31
|
export { hasPublishIntent, hasTrashIntent, takeDuplicateIntent, takePublishIntent, takeTrashIntent, withDuplicateIntent, withPublishIntent, withTrashIntent };
|
package/dist/write/row-ids.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isPlainObject } from "../guards.mjs";
|
|
2
|
+
import { editorRoot } from "../schema/lexical.mjs";
|
|
3
|
+
import "../schema/index.mjs";
|
|
2
4
|
//#region src/write/row-ids.ts
|
|
3
|
-
const isRichTextState = (value) =>
|
|
5
|
+
const isRichTextState = (value) => Array.isArray(editorRoot(value)?.["children"]);
|
|
4
6
|
const walkRows = (value, visit, isRow = false) => {
|
|
5
7
|
if (Array.isArray(value)) {
|
|
6
8
|
for (const entry of value) walkRows(entry, visit, true);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isPlainObject } from "../guards.mjs";
|
|
2
2
|
import { splitPath } from "../schema/path.mjs";
|
|
3
|
-
import { ROW_KEYS,
|
|
3
|
+
import { ROW_KEYS, blockForRow, classifyKey, descriptorsUnder, findFieldAt } from "../schema/walk.mjs";
|
|
4
4
|
import "../schema/index.mjs";
|
|
5
5
|
//#region src/write/write-data.ts
|
|
6
6
|
const pickDescribed = (config, value, at) => {
|
|
@@ -10,43 +10,30 @@ const pickDescribed = (config, value, at) => {
|
|
|
10
10
|
if (isRow) {
|
|
11
11
|
for (const key of ROW_KEYS) if (Object.hasOwn(value, key)) result[key] = value[key];
|
|
12
12
|
}
|
|
13
|
+
const pick = (row, at) => isPlainObject(row) ? pickDescribed(config, row, at) : row;
|
|
13
14
|
for (const [key, entry] of Object.entries(value)) {
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return block && isPlainObject(row) ? pickDescribed(config, row, {
|
|
15
|
+
const found = classifyKey(relative, prefix, key);
|
|
16
|
+
if (found.kind === "none") continue;
|
|
17
|
+
if (found.kind === "leaf") {
|
|
18
|
+
const field = found.descriptor.type === "blocks" ? findFieldAt(fields, splitPath(found.descriptor.path), "blocks") : void 0;
|
|
19
|
+
result[key] = field && Array.isArray(entry) ? entry.map((row) => {
|
|
20
|
+
const block = blockForRow(config, field, row);
|
|
21
|
+
return block ? pick(row, {
|
|
22
22
|
fields: block.flattenedFields,
|
|
23
23
|
prefix: [],
|
|
24
24
|
isRow: true
|
|
25
25
|
}) : row;
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
if (exact) {
|
|
30
|
-
result[key] = entry;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (candidates.some(({ parts }) => parts[1] === "*")) {
|
|
34
|
-
result[key] = Array.isArray(entry) ? entry.map((row) => isPlainObject(row) ? pickDescribed(config, row, {
|
|
35
|
-
fields,
|
|
36
|
-
prefix: [
|
|
37
|
-
...prefix,
|
|
38
|
-
key,
|
|
39
|
-
"*"
|
|
40
|
-
],
|
|
41
|
-
isRow: true
|
|
42
|
-
}) : row) : entry;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
result[key] = isPlainObject(entry) ? pickDescribed(config, entry, {
|
|
26
|
+
}) : entry;
|
|
27
|
+
} else if (found.kind === "rows") result[key] = Array.isArray(entry) ? entry.map((row) => pick(row, {
|
|
46
28
|
fields,
|
|
47
|
-
prefix:
|
|
29
|
+
prefix: found.prefix,
|
|
30
|
+
isRow: true
|
|
31
|
+
})) : entry;
|
|
32
|
+
else result[key] = pick(entry, {
|
|
33
|
+
fields,
|
|
34
|
+
prefix: found.prefix,
|
|
48
35
|
isRow: false
|
|
49
|
-
})
|
|
36
|
+
});
|
|
50
37
|
}
|
|
51
38
|
return result;
|
|
52
39
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@abinnovision/payloadcms-mcpx",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.22",
|
|
5
5
|
"description": "Payload CMS plugin exposing a fixed, schema-aware MCP tool surface with draft-only writes and per-API-key capabilities.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"payload",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"prettier": "@abinnovision/prettier-config",
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
60
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
61
61
|
"rfc6902": "^5.3.0",
|
|
62
62
|
"zod": "^4.6.5"
|
|
63
63
|
},
|
|
@@ -69,20 +69,20 @@
|
|
|
69
69
|
"@payloadcms/db-sqlite": "3.90.2",
|
|
70
70
|
"@payloadcms/richtext-lexical": "3.90.2",
|
|
71
71
|
"@payloadcms/ui": "3.90.2",
|
|
72
|
-
"@swc/core": "^1.16.
|
|
72
|
+
"@swc/core": "^1.16.13",
|
|
73
73
|
"@types/node": "^26.6.3",
|
|
74
74
|
"@types/react": "^19.3.0",
|
|
75
|
-
"@vitest/coverage-v8": "^5.0.
|
|
76
|
-
"eslint": "^10.
|
|
75
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
76
|
+
"eslint": "^10.12.0",
|
|
77
77
|
"payload": "3.90.2",
|
|
78
78
|
"prettier": "^3.9.9",
|
|
79
|
-
"publint": "^0.3.
|
|
79
|
+
"publint": "^0.3.25",
|
|
80
80
|
"react": "^19.3.0",
|
|
81
81
|
"react-dom": "^19.3.0",
|
|
82
82
|
"tsdown": "^0.23.0",
|
|
83
83
|
"typescript": "^6.0.3",
|
|
84
84
|
"unplugin-swc": "^2.0.0",
|
|
85
|
-
"vitest": "^5.0.
|
|
85
|
+
"vitest": "^5.0.3"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
88
|
"@payloadcms/ui": ">=3.88.0 <4",
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
//#region src/builtin-tool-names.ts
|
|
2
|
-
/**
|
|
3
|
-
* Names of the builtin tools. Custom tools must not reuse them.
|
|
4
|
-
*/ const BUILTIN_TOOL_NAMES = [
|
|
5
|
-
"listCapabilities",
|
|
6
|
-
"describeSchema",
|
|
7
|
-
"findDocuments",
|
|
8
|
-
"getDocument",
|
|
9
|
-
"findVersions",
|
|
10
|
-
"patchDocument",
|
|
11
|
-
"createDocument",
|
|
12
|
-
"duplicateDocument",
|
|
13
|
-
"validateDocument",
|
|
14
|
-
"publishDocument",
|
|
15
|
-
"deleteDocument",
|
|
16
|
-
"runConfirmed"
|
|
17
|
-
];
|
|
18
|
-
//#endregion
|
|
19
|
-
export { BUILTIN_TOOL_NAMES };
|