@abinnovision/payloadcms-mcpx 2.0.0-beta.9 → 2.1.0-beta.1
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/CHANGELOG.md +43 -0
- package/README.md +56 -36
- package/dist/admin/capability-matrix.d.mts +5 -3
- package/dist/admin/capability-matrix.mjs +374 -272
- package/dist/admin/capability-toggles.mjs +79 -42
- package/dist/admin/confirmations-panel.mjs +232 -0
- package/dist/admin/confirmations.d.mts +15 -0
- package/dist/admin/confirmations.mjs +71 -0
- package/dist/admin/endpoint-url.mjs +7 -0
- package/dist/admin/index.d.mts +2 -1
- package/dist/admin/index.mjs +2 -1
- 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 +7 -9
- package/dist/api-keys/capability-matrix.mjs +15 -37
- package/dist/api-keys/collection.mjs +16 -18
- package/dist/api-keys/confirmation-view.mjs +7 -0
- package/dist/api-keys/fields.mjs +50 -57
- package/dist/auth/resolve.mjs +69 -40
- package/dist/capabilities.d.mts +2 -0
- package/dist/capabilities.mjs +64 -16
- package/dist/define-tool.d.mts +4 -4
- package/dist/define-tool.mjs +8 -1
- package/dist/endpoint/confirmations.mjs +124 -0
- package/dist/endpoint/handler.mjs +25 -27
- package/dist/endpoint/http.mjs +27 -0
- package/dist/endpoint/server.mjs +48 -29
- package/dist/endpoint/upload.mjs +104 -0
- package/dist/guards.mjs +8 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/leases/confirmation.mjs +74 -0
- package/dist/leases/grant.mjs +29 -0
- package/dist/leases/lease.mjs +115 -0
- package/dist/options.mjs +68 -61
- package/dist/plugin.d.mts +3 -2
- package/dist/plugin.mjs +19 -4
- 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/builtin.mjs +7 -1
- package/dist/tools/create-document.mjs +38 -26
- package/dist/tools/delete-document.mjs +154 -0
- package/dist/tools/describe-schema.mjs +7 -5
- package/dist/tools/document.mjs +131 -45
- package/dist/tools/duplicate-document.mjs +50 -0
- package/dist/tools/find-documents.mjs +15 -20
- package/dist/tools/find-versions.mjs +11 -15
- package/dist/tools/get-document.mjs +31 -15
- package/dist/tools/links.mjs +53 -0
- package/dist/tools/list-capabilities.mjs +17 -7
- package/dist/tools/patch-document.mjs +57 -53
- package/dist/tools/publish-document.mjs +17 -29
- package/dist/tools/query-paths.mjs +1 -1
- package/dist/tools/run-confirmed.mjs +114 -0
- package/dist/tools/shared.mjs +53 -46
- package/dist/tools/validate-document.mjs +6 -8
- package/dist/tools/versions.mjs +11 -6
- package/dist/types.d.mts +158 -43
- package/dist/upload/file.mjs +178 -0
- package/dist/version.mjs +4 -2
- package/dist/write/draft-guard.mjs +35 -28
- package/dist/write/patch.mjs +4 -4
- package/dist/write/publish-intent.mjs +23 -11
- package/dist/write/row-ids.mjs +3 -1
- package/dist/write/write-data.mjs +18 -31
- package/package.json +14 -14
- package/dist/builtin-tool-names.mjs +0 -16
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
2
|
import { errorResult, jsonResult } from "../result.mjs";
|
|
3
|
-
import { JSON_POINTER_PATTERN, prototypeSegmentProblem, splitPath } from "../schema/path.mjs";
|
|
4
|
-
import { findRichTextField } from "../schema/walk.mjs";
|
|
5
3
|
import { SchemaError } from "../schema/errors.mjs";
|
|
4
|
+
import { JSON_POINTER_PATTERN, prototypeSegmentProblem, splitPath } from "../schema/path.mjs";
|
|
5
|
+
import { findFieldAt } from "../schema/walk.mjs";
|
|
6
6
|
import { lexicalOutline } from "../schema/outline.mjs";
|
|
7
7
|
import { resolveDataPointer } from "../schema/pointer.mjs";
|
|
8
8
|
import "../schema/index.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import { downloadHandoff, downloadSlugs } from "../upload/file.mjs";
|
|
10
|
+
import { READ_LOCALE_DESCRIPTION, depthShape, entityShape, idSchema, idShape, localeOf, localeShape, reaches, slugsFor, widen } from "./shared.mjs";
|
|
10
11
|
import { identityOf, resolveDocument } from "./document.mjs";
|
|
11
12
|
import { diffDocuments, loadPublished, loadVersion, readLive } from "./versions.mjs";
|
|
12
|
-
import { APIError } from "payload";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
+
import { APIError } from "payload";
|
|
14
15
|
import { Pointer } from "rfc6902";
|
|
15
16
|
//#region src/tools/get-document.ts
|
|
16
17
|
const OUTLINE_ERROR = "\"outline\" applies to a rich text field; give \"path\" for one.";
|
|
@@ -18,10 +19,6 @@ const VERSION_PARAGRAPH = `
|
|
|
18
19
|
|
|
19
20
|
"versionId" and "diffFrom" work where findVersions does. To revert, call getDocument with the old version as "versionId", the version findVersions marks "latest" as "diffFrom" and the locale you will write, then apply the returned "patch" with patchDocument.`;
|
|
20
21
|
const DESCRIPTION = `Reads one document or global, or with "path" only the value at that pointer. Returns the latest draft by default, with the "updatedAt" a write takes as "expectedUpdatedAt".`;
|
|
21
|
-
const exposesVersions = (scope) => {
|
|
22
|
-
const { collections, globals } = slugsFor(scope, "versions");
|
|
23
|
-
return collections.length + globals.length > 0;
|
|
24
|
-
};
|
|
25
22
|
const assertVersionArgs = (scope, target, args) => {
|
|
26
23
|
if (args.versionId === void 0 && args.diffFrom === void 0) return;
|
|
27
24
|
const { collections, globals } = slugsFor(scope, "versions");
|
|
@@ -40,24 +37,36 @@ const diffResult = async (scope, read, diff, doc) => {
|
|
|
40
37
|
});
|
|
41
38
|
};
|
|
42
39
|
const versionShape = (scope) => {
|
|
43
|
-
if (!
|
|
40
|
+
if (!reaches(scope, "versions")) return widen({});
|
|
44
41
|
return widen({
|
|
45
42
|
versionId: idSchema.optional().describe("From findVersions. Not with \"draft\"."),
|
|
46
43
|
diffFrom: idSchema.optional().describe("Version id, or \"published\" for the newest version published in any locale. Returns {from, to, patch}, the JSON Patch from that version to what is read.")
|
|
47
44
|
});
|
|
48
45
|
};
|
|
46
|
+
const downloadShape = (scope) => {
|
|
47
|
+
const slugs = downloadSlugs(scope);
|
|
48
|
+
return widen(slugs.length === 0 ? {} : { download: z.literal(true).optional().describe(`Only for ${slugs.join(", ")}. Adds "download": GET its "url" with its "headers" once, within 5 minutes, for the file. Not with "path", "versionId" or "diffFrom".`) });
|
|
49
|
+
};
|
|
50
|
+
const assertDownloadArgs = (scope, target, args) => {
|
|
51
|
+
if (target.kind === "global" || !downloadSlugs(scope).includes(target.slug)) throw new APIError(`"${target.slug}" has no file to download.`, 400);
|
|
52
|
+
if (args.path !== void 0 || args.versionId !== void 0 || args.diffFrom !== void 0) throw new APIError("\"download\" cannot be combined with \"path\", \"versionId\" or \"diffFrom\".", 400);
|
|
53
|
+
};
|
|
54
|
+
const withDownload = async (scope, doc, target) => typeof doc["filename"] === "string" ? jsonResult({
|
|
55
|
+
...doc,
|
|
56
|
+
download: await downloadHandoff(scope, target)
|
|
57
|
+
}) : errorResult("This document has no file to download.");
|
|
49
58
|
/**
|
|
50
59
|
* With `path` the handler returns the subtree plus the `id`, `_status` and
|
|
51
60
|
* `updatedAt` a client needs to write back, so reading one branch still gives
|
|
52
61
|
* the `expectedUpdatedAt` timestamp without a second call.
|
|
53
62
|
*/ const getDocument = defineMcpxTool({
|
|
54
63
|
name: "getDocument",
|
|
55
|
-
description: (scope) =>
|
|
64
|
+
description: (scope) => reaches(scope, "versions") ? DESCRIPTION + VERSION_PARAGRAPH : DESCRIPTION,
|
|
56
65
|
annotations: {
|
|
57
66
|
readOnlyHint: true,
|
|
58
67
|
openWorldHint: false
|
|
59
68
|
},
|
|
60
|
-
isEnabled: (scope) => scope
|
|
69
|
+
isEnabled: (scope) => reaches(scope, "read"),
|
|
61
70
|
inputSchema: (scope) => ({
|
|
62
71
|
...entityShape(scope, "read"),
|
|
63
72
|
...idShape(scope, "read"),
|
|
@@ -69,11 +78,13 @@ const versionShape = (scope) => {
|
|
|
69
78
|
}),
|
|
70
79
|
draft: z.boolean().optional().describe("Default true. false reads the published version."),
|
|
71
80
|
outline: z.boolean().optional().describe("With \"path\" at a rich text field: list each node's pointer, type, \"version\" and text."),
|
|
72
|
-
...versionShape(scope)
|
|
81
|
+
...versionShape(scope),
|
|
82
|
+
...downloadShape(scope)
|
|
73
83
|
}),
|
|
74
84
|
handler: async ({ args, scope }) => {
|
|
75
85
|
const target = resolveDocument(scope, args, "read");
|
|
76
86
|
assertVersionArgs(scope, target, args);
|
|
87
|
+
if (args.download) assertDownloadArgs(scope, target, args);
|
|
77
88
|
const prototyped = args.path ? prototypeSegmentProblem(args.path) : void 0;
|
|
78
89
|
if (prototyped !== void 0) return errorResult(prototyped);
|
|
79
90
|
let pointer;
|
|
@@ -96,12 +107,17 @@ const versionShape = (scope) => {
|
|
|
96
107
|
}, doc);
|
|
97
108
|
if (pointer === void 0) {
|
|
98
109
|
if (args.outline) return errorResult(OUTLINE_ERROR);
|
|
99
|
-
return
|
|
110
|
+
return args.download && args.id !== void 0 ? await withDownload(scope, doc, {
|
|
111
|
+
collection: target.slug,
|
|
112
|
+
id: args.id,
|
|
113
|
+
draft: args.draft ?? true,
|
|
114
|
+
locale: read.locale ?? null
|
|
115
|
+
}) : jsonResult(doc);
|
|
100
116
|
}
|
|
101
117
|
const path = pointer.toString();
|
|
102
118
|
const value = pointer.get(doc);
|
|
103
119
|
const envelope = {
|
|
104
|
-
...identityOf(target
|
|
120
|
+
...identityOf(target),
|
|
105
121
|
status: doc["_status"],
|
|
106
122
|
updatedAt: doc["updatedAt"],
|
|
107
123
|
path
|
|
@@ -121,7 +137,7 @@ const versionShape = (scope) => {
|
|
|
121
137
|
if (!(error instanceof SchemaError)) throw error;
|
|
122
138
|
return errorResult(error.message);
|
|
123
139
|
}
|
|
124
|
-
const field = resolution.descriptor?.type === "richText" && !resolution.lexical ?
|
|
140
|
+
const field = resolution.descriptor?.type === "richText" && !resolution.lexical ? findFieldAt(resolution.fields, splitPath(resolution.descriptor.path), "richText") : void 0;
|
|
125
141
|
if (!field) return errorResult(OUTLINE_ERROR);
|
|
126
142
|
return jsonResult({
|
|
127
143
|
...envelope,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { publicOrigin } from "../request.mjs";
|
|
2
|
+
import { formatAdminURL } from "payload/shared";
|
|
3
|
+
//#region src/tools/links.ts
|
|
4
|
+
const livePreviewOf = (req, target) => {
|
|
5
|
+
if (target.config.admin.livePreview) return target.config.admin.livePreview;
|
|
6
|
+
const root = req.payload.config.admin.livePreview;
|
|
7
|
+
return (target.kind === "collection" ? root?.collections : root?.globals)?.includes(target.slug) ? root : void 0;
|
|
8
|
+
};
|
|
9
|
+
const previewOf = async (req, args) => {
|
|
10
|
+
const { target, doc, locale } = args;
|
|
11
|
+
const { preview } = target.config.admin;
|
|
12
|
+
if (preview) return await preview(doc, {
|
|
13
|
+
locale: locale ?? "",
|
|
14
|
+
req,
|
|
15
|
+
token: null
|
|
16
|
+
});
|
|
17
|
+
const url = livePreviewOf(req, target)?.url;
|
|
18
|
+
if (typeof url !== "function") return url;
|
|
19
|
+
const { localization } = req.payload.config;
|
|
20
|
+
const entry = localization ? localization.locales.find((item) => item.code === locale) : void 0;
|
|
21
|
+
return await url({
|
|
22
|
+
...target.kind === "collection" ? { collectionConfig: target.config } : { globalConfig: target.config },
|
|
23
|
+
data: doc,
|
|
24
|
+
locale: entry ?? {
|
|
25
|
+
code: locale ?? "",
|
|
26
|
+
label: locale ?? ""
|
|
27
|
+
},
|
|
28
|
+
payload: req.payload,
|
|
29
|
+
req
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The admin panel URL of a written document, and its preview URL where the
|
|
34
|
+
* entity configures one. Both sit at the origin of `serverURL` when set, else
|
|
35
|
+
* of the MCP request. A preview that fails is left out: the write has landed.
|
|
36
|
+
*/ const documentLinks = async (req, args) => {
|
|
37
|
+
const { target, doc, locale } = args;
|
|
38
|
+
const { config } = req.payload;
|
|
39
|
+
const origin = publicOrigin(req);
|
|
40
|
+
const path = target.kind === "collection" ? `/collections/${target.slug}/${String(doc["id"])}` : `/globals/${target.slug}`;
|
|
41
|
+
const adminUrl = formatAdminURL({
|
|
42
|
+
adminRoute: config.routes.admin,
|
|
43
|
+
path,
|
|
44
|
+
serverURL: origin
|
|
45
|
+
});
|
|
46
|
+
const preview = await previewOf(req, args).catch(() => void 0);
|
|
47
|
+
return {
|
|
48
|
+
adminUrl: locale ? `${adminUrl}?locale=${encodeURIComponent(locale)}` : adminUrl,
|
|
49
|
+
...preview ? { previewUrl: new URL(preview, origin).toString() } : {}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
export { documentLinks };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
|
-
import { canCreate } from "../capabilities.mjs";
|
|
3
2
|
import { jsonResult } from "../result.mjs";
|
|
4
3
|
import { translateStatic, translatorFor } from "../i18n.mjs";
|
|
4
|
+
import { slugsFor } from "./shared.mjs";
|
|
5
|
+
import { globalConfig } from "./document.mjs";
|
|
6
|
+
import { MCPX_ISSUES_URL, MCPX_REPOSITORY_URL } from "../version.mjs";
|
|
5
7
|
import { hasDraftValidationEnabled } from "payload/shared";
|
|
6
8
|
//#region src/tools/list-capabilities.ts
|
|
7
9
|
const DESCRIPTION = `Lists what this key may do. Call it first. Returns each collection and global the key can read or write, with whether it can create, publish, has drafts and exposes versions to findVersions, and a collection's "idType". Also returns the locales, the "limits" on page size and depth, and the enabled custom tools.`;
|
|
10
|
+
const DIAGNOSTICS = ` Its "server" block holds the server version with repository and issue links, for a bug report.`;
|
|
8
11
|
const translateLabel = (scope, label, fallback) => {
|
|
9
12
|
const { i18n, t } = scope.req;
|
|
10
13
|
const resolved = typeof label === "function" ? label({
|
|
@@ -21,7 +24,7 @@ const translateLabel = (scope, label, fallback) => {
|
|
|
21
24
|
* grows.
|
|
22
25
|
*/ const listCapabilities = defineMcpxTool({
|
|
23
26
|
name: "listCapabilities",
|
|
24
|
-
description: DESCRIPTION,
|
|
27
|
+
description: (scope) => scope.diagnostics ? `${DESCRIPTION}${DIAGNOSTICS}` : DESCRIPTION,
|
|
25
28
|
annotations: {
|
|
26
29
|
readOnlyHint: true,
|
|
27
30
|
openWorldHint: false
|
|
@@ -31,10 +34,11 @@ const translateLabel = (scope, label, fallback) => {
|
|
|
31
34
|
handler: ({ scope }) => {
|
|
32
35
|
const { payload } = scope.req;
|
|
33
36
|
const translate = translatorFor(scope.req.i18n);
|
|
37
|
+
const creatable = slugsFor(scope, "create").collections;
|
|
34
38
|
const collections = scope.exposure.collections.flatMap((entry) => {
|
|
35
39
|
const capability = scope.capabilities.collections[entry.slug];
|
|
36
40
|
const collection = payload.collections[entry.slug];
|
|
37
|
-
if (!capability || !collection || !
|
|
41
|
+
if (!capability || !collection || !capability.read) return [];
|
|
38
42
|
const { config } = collection;
|
|
39
43
|
const description = translate(config.admin.description);
|
|
40
44
|
return [{
|
|
@@ -46,7 +50,7 @@ const translateLabel = (scope, label, fallback) => {
|
|
|
46
50
|
...description === void 0 ? {} : { description },
|
|
47
51
|
read: capability.read,
|
|
48
52
|
write: capability.write,
|
|
49
|
-
create:
|
|
53
|
+
create: creatable.includes(entry.slug),
|
|
50
54
|
publish: capability.publish,
|
|
51
55
|
drafts: entry.hasDrafts,
|
|
52
56
|
versions: entry.hasVersions,
|
|
@@ -56,8 +60,8 @@ const translateLabel = (scope, label, fallback) => {
|
|
|
56
60
|
});
|
|
57
61
|
const globals = scope.exposure.globals.flatMap((entry) => {
|
|
58
62
|
const capability = scope.capabilities.globals[entry.slug];
|
|
59
|
-
const config = payload
|
|
60
|
-
if (!capability || !config || !
|
|
63
|
+
const config = globalConfig(payload, entry.slug);
|
|
64
|
+
if (!capability || !config || !capability.read) return [];
|
|
61
65
|
const description = translate(config.admin.description);
|
|
62
66
|
return [{
|
|
63
67
|
slug: entry.slug,
|
|
@@ -79,7 +83,13 @@ const translateLabel = (scope, label, fallback) => {
|
|
|
79
83
|
default: scope.localization.defaultLocale
|
|
80
84
|
} : null,
|
|
81
85
|
limits: scope.limits,
|
|
82
|
-
tools: Object.entries(scope.capabilities.tools).filter(([, enabled]) => enabled).map(([name]) => name)
|
|
86
|
+
tools: Object.entries(scope.capabilities.tools).filter(([, enabled]) => enabled).map(([name]) => name),
|
|
87
|
+
...scope.diagnostics ? { server: {
|
|
88
|
+
name: scope.diagnostics.name,
|
|
89
|
+
version: scope.diagnostics.version,
|
|
90
|
+
repository: MCPX_REPOSITORY_URL,
|
|
91
|
+
issues: MCPX_ISSUES_URL
|
|
92
|
+
} } : {}
|
|
83
93
|
});
|
|
84
94
|
}
|
|
85
95
|
});
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
2
|
import { isPlainObject } from "../guards.mjs";
|
|
3
|
-
import { errorResult
|
|
3
|
+
import { errorResult } from "../result.mjs";
|
|
4
4
|
import { JSON_POINTER_PATTERN } from "../schema/path.mjs";
|
|
5
5
|
import "../schema/index.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { applyPatchOperations, isElementPointer } from "../write/patch.mjs";
|
|
6
|
+
import { assertAcceptsFile, fileShape, fileSlugs, fileWriteRefusal, uploadHandoff, uploadedFile } from "../upload/file.mjs";
|
|
7
|
+
import { entityShape, expectedUpdatedAtShape, idShape, liveWriteSentence, localeOf, localeShape, reaches } from "./shared.mjs";
|
|
8
|
+
import { readDraft, resolveDocument, staleReadResult, updateTarget, writtenResult } from "./document.mjs";
|
|
10
9
|
import { withTransaction } from "../write/transaction.mjs";
|
|
10
|
+
import { applyPatchOperations, isElementPointer } from "../write/patch.mjs";
|
|
11
11
|
import { buildWriteData } from "../write/write-data.mjs";
|
|
12
|
-
import { Forbidden, NotFound } from "payload";
|
|
13
12
|
import { z } from "zod";
|
|
13
|
+
import { APIError, Forbidden, NotFound } from "payload";
|
|
14
14
|
import { Pointer } from "rfc6902";
|
|
15
15
|
//#region src/tools/patch-document.ts
|
|
16
16
|
const DESCRIPTION = (scope) => `Applies JSON Patch operations to one document or global, e.g. {"op":"replace","path":"/title","value":"Home"}. If any operation fails, nothing is written and the response lists the problems.
|
|
@@ -21,7 +21,7 @@ Read the document with getDocument right before patching. Append to a list with
|
|
|
21
21
|
|
|
22
22
|
In a rich text field a pointer continues into the rich text state: "/content/root/children/2" is a node, "/content/root/children/2/tag" one of its properties and "/content/root/children/3/fields/tone" a field of a block node. getDocument "outline" lists each node's pointer and "version". Build nodes from describeSchema "nodeProperties". Check a position before writing to it, e.g. {"op":"test","path":"/content/root/children/2/type","value":"heading"}.
|
|
23
23
|
|
|
24
|
-
The response carries "updatedAt" and, if any, "publishBlockers": what must be fixed before publishing. "publishBlockersUnavailable" means that check failed. The write stands either way. "notApplied" lists pointers whose value did not change or cannot be read back, for example where field access denies the update.`;
|
|
24
|
+
The response carries "updatedAt" and, if any, "publishBlockers": what must be fixed before publishing. "publishBlockersUnavailable" means that check failed. The write stands either way. "notApplied" lists pointers whose value did not change or cannot be read back, for example where field access denies the update. The response links the document with "adminUrl" and "previewUrl".`;
|
|
25
25
|
const POINTER = z.string().regex(JSON_POINTER_PATTERN);
|
|
26
26
|
const PATCHES_LIMIT = 500;
|
|
27
27
|
/**
|
|
@@ -75,7 +75,8 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
|
|
|
75
75
|
* document before writing anything, runs the write in a transaction, then
|
|
76
76
|
* re-reads the saved document to report which pointers survived and what still
|
|
77
77
|
* blocks publishing. The draft guard, not this tool, decides where the write
|
|
78
|
-
* lands.
|
|
78
|
+
* lands. With `file`, the patches and the file land as one update, and the
|
|
79
|
+
* transaction commits right after it because Payload has moved files by then.
|
|
79
80
|
*/ const patchDocument = defineMcpxTool({
|
|
80
81
|
name: "patchDocument",
|
|
81
82
|
description: DESCRIPTION,
|
|
@@ -85,7 +86,7 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
|
|
|
85
86
|
idempotentHint: false,
|
|
86
87
|
openWorldHint: false
|
|
87
88
|
},
|
|
88
|
-
isEnabled: (scope) => scope
|
|
89
|
+
isEnabled: (scope) => reaches(scope, "write"),
|
|
89
90
|
inputSchema: (scope) => ({
|
|
90
91
|
...entityShape(scope, "write"),
|
|
91
92
|
...idShape(scope, "write"),
|
|
@@ -93,71 +94,74 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
|
|
|
93
94
|
required: true,
|
|
94
95
|
description: "Localized fields are written in this locale only."
|
|
95
96
|
}),
|
|
96
|
-
patches: z.array(PATCH_OPERATION_SCHEMA).min(1).max(PATCHES_LIMIT),
|
|
97
|
-
|
|
97
|
+
patches: fileSlugs(scope).length === 0 ? z.array(PATCH_OPERATION_SCHEMA).min(1).max(PATCHES_LIMIT) : z.array(PATCH_OPERATION_SCHEMA).max(PATCHES_LIMIT).describe("May be empty when \"file\" is given."),
|
|
98
|
+
...expectedUpdatedAtShape,
|
|
99
|
+
...fileShape(scope, (slugs) => `Replaces the file of a document in ${slugs}. The call returns an "upload" for the bytes instead of writing.`)
|
|
98
100
|
}),
|
|
99
101
|
handler: async ({ args, scope }) => {
|
|
100
102
|
const target = resolveDocument(scope, args, "write");
|
|
101
103
|
const { payload } = scope.req;
|
|
102
104
|
const locale = localeOf(scope, args.locale);
|
|
103
105
|
const patches = args.patches;
|
|
104
|
-
|
|
106
|
+
const { file } = args;
|
|
107
|
+
const bytes = uploadedFile(scope.req);
|
|
108
|
+
if (file !== void 0) assertAcceptsFile(scope, target, file);
|
|
109
|
+
if (patches.length === 0 && file === void 0) throw new APIError("\"patches\" may be empty only when \"file\" is given.", 400);
|
|
110
|
+
const report = async (next) => {
|
|
111
|
+
const readable = await readDraft(scope, {
|
|
112
|
+
target,
|
|
113
|
+
locale
|
|
114
|
+
}).catch((error) => {
|
|
115
|
+
if (error instanceof NotFound || error instanceof Forbidden) return;
|
|
116
|
+
throw error;
|
|
117
|
+
});
|
|
118
|
+
const notApplied = readable ? notAppliedPointers(patches, next, readable) : [];
|
|
119
|
+
return await writtenResult(scope, {
|
|
120
|
+
target,
|
|
121
|
+
locale,
|
|
122
|
+
extra: notApplied.length > 0 ? { notApplied } : {}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
const outcome = await withTransaction(scope.req, async () => {
|
|
105
126
|
const doc = await readDraft(scope, {
|
|
106
127
|
target,
|
|
107
128
|
locale
|
|
108
129
|
});
|
|
109
130
|
const stale = staleReadResult(doc, args.expectedUpdatedAt, "The document changed since you read it. Read it again and re-apply the patch.");
|
|
110
|
-
if (stale) return stale;
|
|
131
|
+
if (stale) return { done: stale };
|
|
111
132
|
const applied = applyPatchOperations(payload.config, {
|
|
112
133
|
doc,
|
|
113
134
|
patches,
|
|
114
135
|
ref: target
|
|
115
136
|
});
|
|
116
|
-
if ("problems" in applied) return errorResult("No operation was applied.", { problems: applied.problems });
|
|
117
|
-
|
|
137
|
+
if ("problems" in applied) return { done: errorResult("No operation was applied.", { problems: applied.problems }) };
|
|
138
|
+
if (file !== void 0 && target.kind === "collection") {
|
|
139
|
+
const refusal = await fileWriteRefusal(scope, {
|
|
140
|
+
entity: target,
|
|
141
|
+
id: target.id,
|
|
142
|
+
data: applied.next
|
|
143
|
+
});
|
|
144
|
+
if (refusal) return { done: refusal };
|
|
145
|
+
if (!bytes) return { handoff: {
|
|
146
|
+
...args,
|
|
147
|
+
file,
|
|
148
|
+
expectedUpdatedAt: doc["updatedAt"]
|
|
149
|
+
} };
|
|
150
|
+
}
|
|
151
|
+
await updateTarget(scope, target, {
|
|
118
152
|
data: buildWriteData(payload.config, target.config, applied.next),
|
|
119
|
-
depth: 0,
|
|
120
153
|
draft: true,
|
|
121
|
-
overrideAccess: false,
|
|
122
|
-
req: scope.req,
|
|
123
|
-
...locale === void 0 ? {} : { locale }
|
|
124
|
-
};
|
|
125
|
-
if (target.kind === "collection") await payload.update({
|
|
126
|
-
...write,
|
|
127
|
-
collection: target.slug,
|
|
128
|
-
id: target.id
|
|
129
|
-
});
|
|
130
|
-
else await payload.updateGlobal({
|
|
131
|
-
...write,
|
|
132
|
-
fallbackLocale: false,
|
|
133
|
-
slug: target.slug
|
|
134
|
-
});
|
|
135
|
-
const saved = await readDraft(scope, {
|
|
136
|
-
target,
|
|
137
154
|
locale,
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
const readable = await readDraft(scope, {
|
|
141
|
-
target,
|
|
142
|
-
locale
|
|
143
|
-
}).catch((error) => {
|
|
144
|
-
if (error instanceof NotFound || error instanceof Forbidden) return;
|
|
145
|
-
throw error;
|
|
146
|
-
});
|
|
147
|
-
const notApplied = readable ? notAppliedPointers(patches, applied.next, readable) : [];
|
|
148
|
-
const validation = await collectPublishBlockers(scope.req, {
|
|
149
|
-
doc: saved,
|
|
150
|
-
entity: target
|
|
151
|
-
});
|
|
152
|
-
return jsonResult({
|
|
153
|
-
...identityOf(target, saved["id"]),
|
|
154
|
-
status: saved["_status"],
|
|
155
|
-
updatedAt: saved["updatedAt"],
|
|
156
|
-
...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
|
|
157
|
-
...validation.unavailable ? { publishBlockersUnavailable: true } : {},
|
|
158
|
-
...notApplied.length > 0 ? { notApplied } : {}
|
|
155
|
+
file: bytes
|
|
159
156
|
});
|
|
157
|
+
return bytes === void 0 ? { done: await report(applied.next) } : { written: applied.next };
|
|
158
|
+
});
|
|
159
|
+
if ("done" in outcome) return outcome.done;
|
|
160
|
+
if ("handoff" in outcome) return await uploadHandoff(scope, {
|
|
161
|
+
tool: "patchDocument",
|
|
162
|
+
args: outcome.handoff
|
|
160
163
|
});
|
|
164
|
+
return await report(outcome.written);
|
|
161
165
|
}
|
|
162
166
|
});
|
|
163
167
|
//#endregion
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
2
|
import { jsonResult } from "../result.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { documentLinks } from "./links.mjs";
|
|
4
|
+
import { entityShape, expectedUpdatedAtShape, idShape, localeOf, localeShape, reaches } from "./shared.mjs";
|
|
5
|
+
import { blockerFields, collectLocaleBlockers, documentSummary, readDraft, resolveDocument, staleReadResult, updateTarget } from "./document.mjs";
|
|
6
6
|
import { withPublishIntent } from "../write/publish-intent.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { withTransaction } from "../write/transaction.mjs";
|
|
8
8
|
/**
|
|
9
9
|
* Publishes a draft. Available where the config exposes `publish` on an entity
|
|
10
10
|
* with drafts and the key has both the `write` and `publish` checkboxes.
|
|
11
11
|
*/ const publishDocument = defineMcpxTool({
|
|
12
12
|
name: "publishDocument",
|
|
13
|
-
description: `Makes the current draft of one document or global public, in every locale or only in "locale". A scoped publish keeps the other locales of localized fields as last published, and a never-published document goes live in full. A draft with publish blockers is refused, so call validateDocument first. Payload checks only the published locale, so "otherLocaleBlockers" lists what other locales still lack. There is no unpublish.`,
|
|
13
|
+
description: `Makes the current draft of one document or global public, in every locale or only in "locale". A scoped publish keeps the other locales of localized fields as last published, and a never-published document goes live in full. A draft with publish blockers is refused, so call validateDocument first. Payload checks only the published locale, so "otherLocaleBlockers" lists what other locales still lack. There is no unpublish. The response links the document with "adminUrl" and "previewUrl".`,
|
|
14
14
|
annotations: {
|
|
15
15
|
destructiveHint: true,
|
|
16
16
|
openWorldHint: false
|
|
17
17
|
},
|
|
18
|
-
isEnabled: (scope) => scope
|
|
18
|
+
isEnabled: (scope) => reaches(scope, "publish"),
|
|
19
19
|
inputSchema: (scope) => ({
|
|
20
20
|
...entityShape(scope, "publish"),
|
|
21
21
|
...idShape(scope, "publish"),
|
|
22
22
|
...localeShape(scope, { required: false }),
|
|
23
|
-
|
|
23
|
+
...expectedUpdatedAtShape
|
|
24
24
|
}),
|
|
25
25
|
handler: async ({ args, scope }) => {
|
|
26
26
|
const target = resolveDocument(scope, args, "publish");
|
|
27
|
-
const { payload } = scope.req;
|
|
28
27
|
const locale = localeOf(scope, args.locale);
|
|
29
28
|
const { localization } = scope;
|
|
30
29
|
return await withTransaction(scope.req, async () => {
|
|
@@ -34,24 +33,11 @@ import { z } from "zod";
|
|
|
34
33
|
});
|
|
35
34
|
const stale = staleReadResult(doc, args.expectedUpdatedAt, "The document changed since you read it. Read it again before publishing.");
|
|
36
35
|
if (stale) return stale;
|
|
37
|
-
|
|
36
|
+
await updateTarget(scope, target, {
|
|
38
37
|
data: withPublishIntent({}),
|
|
39
|
-
depth: 0,
|
|
40
38
|
draft: false,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
req: scope.req,
|
|
44
|
-
...locale === void 0 ? {} : { locale },
|
|
45
|
-
...args.locale === void 0 ? {} : { publishSpecificLocale: args.locale }
|
|
46
|
-
};
|
|
47
|
-
if (target.kind === "collection") await payload.update({
|
|
48
|
-
...write,
|
|
49
|
-
collection: target.slug,
|
|
50
|
-
id: target.id
|
|
51
|
-
});
|
|
52
|
-
else await payload.updateGlobal({
|
|
53
|
-
...write,
|
|
54
|
-
slug: target.slug
|
|
39
|
+
locale,
|
|
40
|
+
publishSpecificLocale: args.locale
|
|
55
41
|
});
|
|
56
42
|
const saved = await readDraft(scope, {
|
|
57
43
|
target,
|
|
@@ -60,11 +46,13 @@ import { z } from "zod";
|
|
|
60
46
|
});
|
|
61
47
|
const others = await collectLocaleBlockers(scope, target, localization?.locales.filter((entry) => entry !== locale) ?? []);
|
|
62
48
|
return jsonResult({
|
|
63
|
-
...
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
49
|
+
...documentSummary(target, saved),
|
|
50
|
+
...await documentLinks(scope.req, {
|
|
51
|
+
target,
|
|
52
|
+
doc: saved,
|
|
53
|
+
locale
|
|
54
|
+
}),
|
|
55
|
+
...blockerFields(others, "otherLocaleBlockers")
|
|
68
56
|
});
|
|
69
57
|
});
|
|
70
58
|
}
|
|
@@ -48,7 +48,7 @@ const isRefusedPath = (scope, args, seen = /* @__PURE__ */ new Set()) => {
|
|
|
48
48
|
const reaches = (path) => isRefusedPath(scope, {
|
|
49
49
|
collection: args.collection,
|
|
50
50
|
path: normalise(path),
|
|
51
|
-
locale: args.locale
|
|
51
|
+
locale: args.locale
|
|
52
52
|
});
|
|
53
53
|
for (const path of wherePaths(args.where)) if (reaches(path)) throw new APIError(`This key cannot query through "${path}".`, 400);
|
|
54
54
|
for (const path of sortPaths(args.sort)) if (reaches(path) || normalise(path).split(".").some(reaches)) throw new APIError(`This key cannot query through "${path}".`, 400);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { defineMcpxTool, isToolEnabled, toolInputSchema } from "../define-tool.mjs";
|
|
2
|
+
import { leaseContext } from "../leases/lease.mjs";
|
|
3
|
+
import { claimConfirmation, readConfirmation } from "../leases/confirmation.mjs";
|
|
4
|
+
import { jsonResult, parseResult } from "../result.mjs";
|
|
5
|
+
import { withRequestLocale } from "./document.mjs";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { APIError } from "payload";
|
|
8
|
+
//#region src/tools/run-confirmed.ts
|
|
9
|
+
const DESCRIPTION = `Runs calls that returned a "confirmation", once the user approved them in the admin panel. Returns one result per id, in the order given: "done" with the call's "result", "pending" while it awaits a decision, "skipped" with a "reason" when it no longer passes its checks, or "refused" when it was rejected, expired, already run or is unknown. A pending id stays usable; no other id can run again.`;
|
|
10
|
+
const NO_LONGER_ALLOWED = "This key no longer allows this call.";
|
|
11
|
+
const CONFIRMED = {
|
|
12
|
+
confirmed: true,
|
|
13
|
+
request: () => Promise.reject(/* @__PURE__ */ new Error("A confirmed run cannot request a confirmation."))
|
|
14
|
+
};
|
|
15
|
+
const execute = async (tool, confirmation, scope, extra) => {
|
|
16
|
+
const { req } = scope;
|
|
17
|
+
const args = toolInputSchema(tool, scope).safeParse(confirmation.args);
|
|
18
|
+
if (!args.success) return {
|
|
19
|
+
status: "skipped",
|
|
20
|
+
reason: NO_LONGER_ALLOWED
|
|
21
|
+
};
|
|
22
|
+
return await withRequestLocale(req, async () => {
|
|
23
|
+
req.locale = confirmation.locale;
|
|
24
|
+
req.fallbackLocale = confirmation.fallbackLocale;
|
|
25
|
+
try {
|
|
26
|
+
const result = await tool.handler({
|
|
27
|
+
args: args.data,
|
|
28
|
+
scope,
|
|
29
|
+
req,
|
|
30
|
+
extra,
|
|
31
|
+
confirmation: CONFIRMED
|
|
32
|
+
});
|
|
33
|
+
const data = parseResult(result);
|
|
34
|
+
return result.isError === true ? {
|
|
35
|
+
status: "skipped",
|
|
36
|
+
reason: String(data["error"])
|
|
37
|
+
} : {
|
|
38
|
+
status: "done",
|
|
39
|
+
result: data
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof APIError && error.isPublic) return {
|
|
43
|
+
status: "skipped",
|
|
44
|
+
reason: error.message
|
|
45
|
+
};
|
|
46
|
+
req.payload.logger.error({
|
|
47
|
+
err: error,
|
|
48
|
+
msg: "[payloadcms-mcpx] Confirmed call failed."
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
status: "skipped",
|
|
52
|
+
reason: "Internal error"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const claimApproved = async (req, slug, apiKeyId, id) => {
|
|
58
|
+
const { payload } = req;
|
|
59
|
+
try {
|
|
60
|
+
const confirmation = await readConfirmation(payload, slug, apiKeyId, id);
|
|
61
|
+
if (confirmation?.state === "pending") return { status: "pending" };
|
|
62
|
+
return confirmation?.state === "approved" && await claimConfirmation(payload, slug, apiKeyId, id, confirmation.exp) ? confirmation : { status: "refused" };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
payload.logger.error({
|
|
65
|
+
err: error,
|
|
66
|
+
msg: "[payloadcms-mcpx] Could not claim a confirmed call."
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
status: "skipped",
|
|
70
|
+
reason: "Internal error"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Runs approved calls of the confirmable tools in `tools`, which is read per
|
|
76
|
+
* call so the list may include this tool. Registered when the key may call one
|
|
77
|
+
* of them. Each call claims its entry first, so it runs at most once.
|
|
78
|
+
*/ const createRunConfirmed = (tools) => defineMcpxTool({
|
|
79
|
+
name: "runConfirmed",
|
|
80
|
+
description: DESCRIPTION,
|
|
81
|
+
annotations: {
|
|
82
|
+
destructiveHint: true,
|
|
83
|
+
openWorldHint: false
|
|
84
|
+
},
|
|
85
|
+
isEnabled: (scope) => tools().some((tool) => tool.confirm !== void 0 && isToolEnabled(tool, scope)),
|
|
86
|
+
inputSchema: { ids: z.array(z.string().max(64)).min(1).max(50).describe("\"id\" of each \"confirmation\".") },
|
|
87
|
+
handler: async ({ args, scope, extra }) => {
|
|
88
|
+
const { req } = scope;
|
|
89
|
+
const { slug, apiKeyId } = leaseContext(req);
|
|
90
|
+
const outcomes = [];
|
|
91
|
+
for (const id of args.ids) {
|
|
92
|
+
const confirmation = await claimApproved(req, slug, apiKeyId, id);
|
|
93
|
+
if (!("tool" in confirmation)) {
|
|
94
|
+
outcomes.push({
|
|
95
|
+
id,
|
|
96
|
+
...confirmation
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const tool = tools().find((candidate) => candidate.name === confirmation.tool && candidate.confirm !== void 0);
|
|
101
|
+
const result = tool && isToolEnabled(tool, scope) ? await execute(tool, confirmation, scope, extra) : {
|
|
102
|
+
status: "skipped",
|
|
103
|
+
reason: NO_LONGER_ALLOWED
|
|
104
|
+
};
|
|
105
|
+
outcomes.push({
|
|
106
|
+
id,
|
|
107
|
+
...result
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return jsonResult({ results: outcomes });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
//#endregion
|
|
114
|
+
export { createRunConfirmed };
|