@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,11 +1,10 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
2
|
import { jsonResult } from "../result.mjs";
|
|
3
|
-
import { entityShape, idShape, localeOf, localeShape } from "./shared.mjs";
|
|
4
|
-
import { collectLocaleBlockers, identityOf, readDraft, resolveDocument, staleReadResult } from "./document.mjs";
|
|
5
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
7
|
import { withTransaction } from "../write/transaction.mjs";
|
|
8
|
-
import { z } from "zod";
|
|
9
8
|
/**
|
|
10
9
|
* Publishes a draft. Available where the config exposes `publish` on an entity
|
|
11
10
|
* with drafts and the key has both the `write` and `publish` checkboxes.
|
|
@@ -16,16 +15,15 @@ import { z } from "zod";
|
|
|
16
15
|
destructiveHint: true,
|
|
17
16
|
openWorldHint: false
|
|
18
17
|
},
|
|
19
|
-
isEnabled: (scope) => scope
|
|
18
|
+
isEnabled: (scope) => reaches(scope, "publish"),
|
|
20
19
|
inputSchema: (scope) => ({
|
|
21
20
|
...entityShape(scope, "publish"),
|
|
22
21
|
...idShape(scope, "publish"),
|
|
23
22
|
...localeShape(scope, { required: false }),
|
|
24
|
-
|
|
23
|
+
...expectedUpdatedAtShape
|
|
25
24
|
}),
|
|
26
25
|
handler: async ({ args, scope }) => {
|
|
27
26
|
const target = resolveDocument(scope, args, "publish");
|
|
28
|
-
const { payload } = scope.req;
|
|
29
27
|
const locale = localeOf(scope, args.locale);
|
|
30
28
|
const { localization } = scope;
|
|
31
29
|
return await withTransaction(scope.req, async () => {
|
|
@@ -35,24 +33,11 @@ import { z } from "zod";
|
|
|
35
33
|
});
|
|
36
34
|
const stale = staleReadResult(doc, args.expectedUpdatedAt, "The document changed since you read it. Read it again before publishing.");
|
|
37
35
|
if (stale) return stale;
|
|
38
|
-
|
|
36
|
+
await updateTarget(scope, target, {
|
|
39
37
|
data: withPublishIntent({}),
|
|
40
|
-
depth: 0,
|
|
41
38
|
draft: false,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
req: scope.req,
|
|
45
|
-
...locale === void 0 ? {} : { locale },
|
|
46
|
-
...args.locale === void 0 ? {} : { publishSpecificLocale: args.locale }
|
|
47
|
-
};
|
|
48
|
-
if (target.kind === "collection") await payload.update({
|
|
49
|
-
...write,
|
|
50
|
-
collection: target.slug,
|
|
51
|
-
id: target.id
|
|
52
|
-
});
|
|
53
|
-
else await payload.updateGlobal({
|
|
54
|
-
...write,
|
|
55
|
-
slug: target.slug
|
|
39
|
+
locale,
|
|
40
|
+
publishSpecificLocale: args.locale
|
|
56
41
|
});
|
|
57
42
|
const saved = await readDraft(scope, {
|
|
58
43
|
target,
|
|
@@ -61,16 +46,13 @@ import { z } from "zod";
|
|
|
61
46
|
});
|
|
62
47
|
const others = await collectLocaleBlockers(scope, target, localization?.locales.filter((entry) => entry !== locale) ?? []);
|
|
63
48
|
return jsonResult({
|
|
64
|
-
...
|
|
65
|
-
status: saved["_status"],
|
|
66
|
-
updatedAt: saved["updatedAt"],
|
|
49
|
+
...documentSummary(target, saved),
|
|
67
50
|
...await documentLinks(scope.req, {
|
|
68
51
|
target,
|
|
69
52
|
doc: saved,
|
|
70
53
|
locale
|
|
71
54
|
}),
|
|
72
|
-
...others
|
|
73
|
-
...others.unavailable ? { otherLocaleBlockersUnavailable: true } : {}
|
|
55
|
+
...blockerFields(others, "otherLocaleBlockers")
|
|
74
56
|
});
|
|
75
57
|
});
|
|
76
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);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { defineMcpxTool, isToolEnabled, toolInputSchema } from "../define-tool.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { grantContext } from "../grants/grant.mjs";
|
|
3
3
|
import { claimConfirmation, readConfirmation } from "../grants/confirmation.mjs";
|
|
4
|
-
import { jsonResult } from "../result.mjs";
|
|
4
|
+
import { jsonResult, parseResult } from "../result.mjs";
|
|
5
|
+
import { withRequestLocale } from "./document.mjs";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
import { APIError } from "payload";
|
|
7
8
|
//#region src/tools/run-confirmed.ts
|
|
@@ -11,14 +12,6 @@ const CONFIRMED = {
|
|
|
11
12
|
confirmed: true,
|
|
12
13
|
request: () => Promise.reject(/* @__PURE__ */ new Error("A confirmed run cannot request a confirmation."))
|
|
13
14
|
};
|
|
14
|
-
const parseResult = (result) => {
|
|
15
|
-
const [first] = result.content;
|
|
16
|
-
try {
|
|
17
|
-
return first?.type === "text" ? JSON.parse(first.text) : {};
|
|
18
|
-
} catch {
|
|
19
|
-
return { text: first?.type === "text" ? first.text : void 0 };
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
15
|
const execute = async (tool, confirmation, scope, extra) => {
|
|
23
16
|
const { req } = scope;
|
|
24
17
|
const args = toolInputSchema(tool, scope).safeParse(confirmation.args);
|
|
@@ -26,42 +19,40 @@ const execute = async (tool, confirmation, scope, extra) => {
|
|
|
26
19
|
status: "skipped",
|
|
27
20
|
reason: NO_LONGER_ALLOWED
|
|
28
21
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (fallbackLocale !== void 0) req.fallbackLocale = fallbackLocale;
|
|
64
|
-
}
|
|
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
|
+
});
|
|
65
56
|
};
|
|
66
57
|
const claimApproved = async (req, slug, apiKeyId, id) => {
|
|
67
58
|
const { payload } = req;
|
|
@@ -95,10 +86,7 @@ const claimApproved = async (req, slug, apiKeyId, id) => {
|
|
|
95
86
|
inputSchema: { ids: z.array(z.string().max(64)).min(1).max(50).describe("\"id\" of each \"confirmation\".") },
|
|
96
87
|
handler: async ({ args, scope, extra }) => {
|
|
97
88
|
const { req } = scope;
|
|
98
|
-
const {
|
|
99
|
-
const slug = grantKvSlug(payload.config);
|
|
100
|
-
const apiKeyId = req.context.mcpx?.apiKeyId;
|
|
101
|
-
if (slug === void 0 || apiKeyId === void 0) throw new Error("Confirmations need a database KV and a key.");
|
|
89
|
+
const { slug, apiKeyId } = grantContext(req);
|
|
102
90
|
const outcomes = [];
|
|
103
91
|
for (const id of args.ids) {
|
|
104
92
|
const confirmation = await claimApproved(req, slug, apiKeyId, id);
|
package/dist/tools/shared.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { canCreate, isLiveWrite } from "../capabilities.mjs";
|
|
2
1
|
import { acceptsFiles } from "../upload/file.mjs";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
//#region src/tools/shared.ts
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* A value outside the list fails schema validation before a handler runs, so a
|
|
7
6
|
* client only ever sees what its key may touch.
|
|
8
|
-
*/ const
|
|
7
|
+
*/ const stringEnum = (values) => z.enum(values);
|
|
9
8
|
/**
|
|
10
9
|
* Payload's id type follows the adapter, so both forms are handed on as read.
|
|
11
10
|
*/ const idSchema = z.union([z.string(), z.number()]);
|
|
@@ -15,7 +14,10 @@ const slugsOf = (scope, key) => ({
|
|
|
15
14
|
});
|
|
16
15
|
const slugsWhere = (scope, predicate, allowed) => {
|
|
17
16
|
const pick = (entities, slugs) => entities.filter((entity) => slugs.includes(entity.slug) && predicate(entity)).map((entity) => entity.slug);
|
|
18
|
-
return
|
|
17
|
+
return {
|
|
18
|
+
collections: pick(scope.exposure.collections, allowed.collections),
|
|
19
|
+
globals: pick(scope.exposure.globals, allowed.globals)
|
|
20
|
+
};
|
|
19
21
|
};
|
|
20
22
|
/**
|
|
21
23
|
* Collections this key may write but not create in: upload collections whose
|
|
@@ -29,52 +31,33 @@ const widen = (branch) => branch;
|
|
|
29
31
|
* The one list the shape helpers and {@link resolveEntity} both read.
|
|
30
32
|
*/ const slugsFor = (scope, operation) => {
|
|
31
33
|
switch (operation) {
|
|
32
|
-
case "create": return {
|
|
33
|
-
collections:
|
|
34
|
-
collections: scope.collections.writable,
|
|
35
|
-
globals: []
|
|
36
|
-
}),
|
|
34
|
+
case "create": return slugsWhere(scope, (entity) => entity.write && (!entity.isUpload || acceptsFiles(scope, entity.slug)), {
|
|
35
|
+
collections: scope.collections.writable,
|
|
37
36
|
globals: []
|
|
38
|
-
};
|
|
37
|
+
});
|
|
39
38
|
case "delete": return {
|
|
40
39
|
collections: scope.collections.deletable,
|
|
41
40
|
globals: []
|
|
42
41
|
};
|
|
43
42
|
case "publish": return slugsOf(scope, "publishable");
|
|
44
43
|
case "read": return slugsOf(scope, "readable");
|
|
45
|
-
case "versions": return
|
|
46
|
-
collections: slugsWhere(scope, (entity) => entity.hasVersions, {
|
|
47
|
-
collections: scope.collections.readable,
|
|
48
|
-
globals: []
|
|
49
|
-
}),
|
|
50
|
-
globals: slugsWhere(scope, (entity) => entity.hasVersions, {
|
|
51
|
-
collections: [],
|
|
52
|
-
globals: scope.globals.readable
|
|
53
|
-
})
|
|
54
|
-
};
|
|
44
|
+
case "versions": return slugsWhere(scope, (entity) => entity.hasVersions, slugsOf(scope, "readable"));
|
|
55
45
|
case "write": return slugsOf(scope, "writable");
|
|
56
46
|
}
|
|
57
47
|
};
|
|
58
48
|
/**
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
collections: slugsWhere(scope, (entity) => entity.hasDrafts, {
|
|
65
|
-
collections,
|
|
66
|
-
globals: []
|
|
67
|
-
}),
|
|
68
|
-
globals: slugsWhere(scope, (entity) => entity.hasDrafts, {
|
|
69
|
-
collections: [],
|
|
70
|
-
globals
|
|
71
|
-
})
|
|
72
|
-
};
|
|
49
|
+
* Whether `operation` reaches any collection or global.
|
|
50
|
+
*/ const reaches = (scope, operation) => {
|
|
51
|
+
const { collections, globals } = slugsFor(scope, operation);
|
|
52
|
+
return collections.length + globals.length > 0;
|
|
73
53
|
};
|
|
74
54
|
/**
|
|
75
55
|
* Slugs reachable by `operation` whose writes go live, because they have no
|
|
76
56
|
* draft to write.
|
|
77
|
-
*/ const liveWriteSlugs = (scope, operation) =>
|
|
57
|
+
*/ const liveWriteSlugs = (scope, operation) => {
|
|
58
|
+
const { collections, globals } = slugsWhere(scope, (entity) => entity.liveWrite, slugsFor(scope, operation));
|
|
59
|
+
return [...collections, ...globals];
|
|
60
|
+
};
|
|
78
61
|
/**
|
|
79
62
|
* Stated on the tools that write, since a client calling them must know before
|
|
80
63
|
* the call which writes are public.
|
|
@@ -89,11 +72,11 @@ const widen = (branch) => branch;
|
|
|
89
72
|
* `globalRule` states that rule on `global` for a tool without an `id`.
|
|
90
73
|
*/ const entityShape = (scope, operation, globalRule) => {
|
|
91
74
|
const { collections, globals } = slugsFor(scope, operation);
|
|
92
|
-
if (globals.length === 0) return widen({ collection:
|
|
93
|
-
if (collections.length === 0) return widen({ global:
|
|
94
|
-
const global =
|
|
75
|
+
if (globals.length === 0) return widen({ collection: stringEnum(collections) });
|
|
76
|
+
if (collections.length === 0) return widen({ global: stringEnum(globals) });
|
|
77
|
+
const global = stringEnum(globals).optional();
|
|
95
78
|
return widen({
|
|
96
|
-
collection:
|
|
79
|
+
collection: stringEnum(collections).optional(),
|
|
97
80
|
global: globalRule === void 0 ? global : global.describe(globalRule)
|
|
98
81
|
});
|
|
99
82
|
};
|
|
@@ -111,7 +94,7 @@ const widen = (branch) => branch;
|
|
|
111
94
|
*/ const READ_LOCALE_DESCRIPTION = "Default: the default locale. A value missing in this locale is shown from the default locale.";
|
|
112
95
|
const localeShape = (scope, options) => {
|
|
113
96
|
if (!scope.localization) return widen({});
|
|
114
|
-
const enumerated =
|
|
97
|
+
const enumerated = stringEnum(scope.localization.locales);
|
|
115
98
|
const locale = options.required ? enumerated : enumerated.optional();
|
|
116
99
|
return widen({ locale: options.description === void 0 ? locale : locale.describe(options.description) });
|
|
117
100
|
};
|
|
@@ -120,6 +103,25 @@ const localeShape = (scope, options) => {
|
|
|
120
103
|
* it can write back, and populating a relation costs a query.
|
|
121
104
|
*/ const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.limits.maxDepth).optional().describe("Default 0. A relationship into a collection this key cannot read stays an id.") });
|
|
122
105
|
/**
|
|
106
|
+
* For the writes that refuse a document changed since the client read it.
|
|
107
|
+
*/ const expectedUpdatedAtShape = { expectedUpdatedAt: z.string().optional().describe("\"updatedAt\" from your last read. Refused if the document changed since.") };
|
|
108
|
+
/**
|
|
109
|
+
* `limit` is bounded by the configured ceiling, so the client sees it instead
|
|
110
|
+
* of being clamped silently.
|
|
111
|
+
*/ const pageShape = (scope) => ({
|
|
112
|
+
limit: z.number().int().min(1).max(scope.limits.maxLimit).optional().describe("Default 10."),
|
|
113
|
+
page: z.number().int().min(1).optional()
|
|
114
|
+
});
|
|
115
|
+
/**
|
|
116
|
+
* The paging fields of a result.
|
|
117
|
+
*/ const pageFields = (result) => ({
|
|
118
|
+
totalDocs: result.totalDocs,
|
|
119
|
+
page: result.page,
|
|
120
|
+
totalPages: result.totalPages,
|
|
121
|
+
limit: result.limit,
|
|
122
|
+
hasNextPage: result.hasNextPage
|
|
123
|
+
});
|
|
124
|
+
/**
|
|
123
125
|
* The locale to operate on: the explicit argument, else the request's, else
|
|
124
126
|
* the default. `undefined` when localization is off.
|
|
125
127
|
*/ const localeOf = (scope, locale) => {
|
|
@@ -129,4 +131,4 @@ const localeShape = (scope, options) => {
|
|
|
129
131
|
return requested && locales.includes(requested) ? requested : defaultLocale;
|
|
130
132
|
};
|
|
131
133
|
//#endregion
|
|
132
|
-
export { READ_LOCALE_DESCRIPTION, depthShape,
|
|
134
|
+
export { READ_LOCALE_DESCRIPTION, depthShape, entityShape, expectedUpdatedAtShape, idSchema, idShape, liveWriteSentence, liveWriteSlugs, localeOf, localeShape, pageFields, pageShape, patchOnlySlugs, reaches, slugsFor, slugsWhere, stringEnum, widen };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { defineMcpxTool } from "../define-tool.mjs";
|
|
2
2
|
import { jsonResult } from "../result.mjs";
|
|
3
3
|
import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
4
|
-
import { entityShape, idShape, localeOf, localeShape } from "./shared.mjs";
|
|
5
|
-
import { collectLocaleBlockers,
|
|
4
|
+
import { entityShape, idShape, localeOf, localeShape, reaches } from "./shared.mjs";
|
|
5
|
+
import { collectLocaleBlockers, documentSummary, readDraft, resolveDocument } from "./document.mjs";
|
|
6
6
|
/**
|
|
7
7
|
* Gated on write, which implies read, because publish blockers only mean
|
|
8
8
|
* something to a caller who can act on them.
|
|
@@ -15,7 +15,7 @@ import { collectLocaleBlockers, identityOf, readDraft, resolveDocument } from ".
|
|
|
15
15
|
name: "validateDocument",
|
|
16
16
|
description: `Lists what still blocks publishing one document or global, without saving. Checks one locale when "locale" is given, otherwise every locale. Returns "publishBlockers", each with a pointer and a message, and a "locale" when every locale was checked. An empty list means it can be published, unless "publishBlockersUnavailable" is true: then the check itself failed. The check runs the field hooks a save runs.`,
|
|
17
17
|
annotations: { openWorldHint: false },
|
|
18
|
-
isEnabled: (scope) => scope
|
|
18
|
+
isEnabled: (scope) => reaches(scope, "write"),
|
|
19
19
|
inputSchema: (scope) => ({
|
|
20
20
|
...entityShape(scope, "write"),
|
|
21
21
|
...idShape(scope, "write"),
|
|
@@ -39,9 +39,7 @@ import { collectLocaleBlockers, identityOf, readDraft, resolveDocument } from ".
|
|
|
39
39
|
entity: target
|
|
40
40
|
});
|
|
41
41
|
return jsonResult({
|
|
42
|
-
...
|
|
43
|
-
status: doc["_status"],
|
|
44
|
-
updatedAt: doc["updatedAt"],
|
|
42
|
+
...documentSummary(target, doc),
|
|
45
43
|
publishBlockers: validation.blockers,
|
|
46
44
|
...validation.unavailable ? { publishBlockersUnavailable: true } : {}
|
|
47
45
|
});
|
package/dist/tools/versions.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { definedProps, isPlainObject } from "../guards.mjs";
|
|
1
2
|
import { stripAdminHidden } from "../schema/admin-hidden.mjs";
|
|
2
3
|
import "../schema/index.mjs";
|
|
3
4
|
import { mcpxReadRequest } from "./read-request.mjs";
|
|
@@ -21,8 +22,10 @@ const NEWEST_FIRST = ["-updatedAt", "-id"];
|
|
|
21
22
|
draft: options.draft,
|
|
22
23
|
overrideAccess: false,
|
|
23
24
|
req: mcpxReadRequest(scope),
|
|
24
|
-
...
|
|
25
|
-
|
|
25
|
+
...definedProps({
|
|
26
|
+
select: options.select,
|
|
27
|
+
locale: read.locale
|
|
28
|
+
})
|
|
26
29
|
};
|
|
27
30
|
const doc = await (read.target.kind === "collection" ? scope.req.payload.findByID({
|
|
28
31
|
...shared,
|
|
@@ -51,7 +54,7 @@ const assertReadable = async (scope, read) => {
|
|
|
51
54
|
* `parent` comes back as a raw id or, populated, as the document itself.
|
|
52
55
|
*/ const isVersionOf = (version, id) => {
|
|
53
56
|
const parent = version["parent"];
|
|
54
|
-
const parentId =
|
|
57
|
+
const parentId = isPlainObject(parent) ? parent["id"] : parent;
|
|
55
58
|
return (typeof parentId === "string" || typeof parentId === "number") && String(parentId) === String(id);
|
|
56
59
|
};
|
|
57
60
|
/**
|
|
@@ -68,7 +71,7 @@ const assertReadable = async (scope, read) => {
|
|
|
68
71
|
disableErrors: true,
|
|
69
72
|
overrideAccess: false,
|
|
70
73
|
req: mcpxReadRequest(scope),
|
|
71
|
-
...
|
|
74
|
+
...definedProps({ locale: read.locale })
|
|
72
75
|
};
|
|
73
76
|
const version = await (read.target.kind === "collection" ? payload.findVersionByID({
|
|
74
77
|
...shared,
|
|
@@ -96,8 +99,10 @@ const assertReadable = async (scope, read) => {
|
|
|
96
99
|
overrideAccess: false,
|
|
97
100
|
req: mcpxReadRequest(scope),
|
|
98
101
|
where,
|
|
99
|
-
...
|
|
100
|
-
|
|
102
|
+
...definedProps({
|
|
103
|
+
page: options.page,
|
|
104
|
+
locale: read.locale
|
|
105
|
+
})
|
|
101
106
|
};
|
|
102
107
|
return await (read.target.kind === "collection" ? scope.req.payload.findVersions({
|
|
103
108
|
...shared,
|
package/dist/types.d.mts
CHANGED
|
@@ -12,17 +12,6 @@ declare module "payload" {
|
|
|
12
12
|
"@abinnovision/payloadcms-mcpx": McpxPluginOptions;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
* How far an exposed entity lets MCP writes reach.
|
|
17
|
-
*
|
|
18
|
-
* - `false`: no write tool touches it.
|
|
19
|
-
* - `"draft"`: writes land as drafts and nothing MCP does changes what the
|
|
20
|
-
* public sees. Requires `versions.drafts`.
|
|
21
|
-
* - `"live"`: MCP may change live content. On an entity with drafts that
|
|
22
|
-
* exposes `publishDocument`. On one without, there is no draft, so the write
|
|
23
|
-
* itself is permitted and lands live.
|
|
24
|
-
*/
|
|
25
|
-
export type McpxWriteMode = "draft" | "live" | false;
|
|
26
15
|
/**
|
|
27
16
|
* What an entity exposes. The config names the entities that are reachable and
|
|
28
17
|
* only takes capabilities away; a key's checkboxes decide per key. `true` is
|
|
@@ -62,12 +51,12 @@ export interface McpxCollectionOptions {
|
|
|
62
51
|
export type McpxGlobalOptions = Omit<McpxCollectionOptions, "delete">;
|
|
63
52
|
export type McpxToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;
|
|
64
53
|
/**
|
|
65
|
-
* What the config exposes, before an API key's checkboxes narrow it.
|
|
54
|
+
* What the config exposes, before an API key's checkboxes narrow it. The
|
|
55
|
+
* operations are the config's side of {@link McpxEntityCapabilities}: `write`
|
|
56
|
+
* covers draft and live writes, and `publish` is only true where drafts exist.
|
|
66
57
|
*/
|
|
67
|
-
export interface McpxExposedEntity {
|
|
58
|
+
export interface McpxExposedEntity extends McpxEntityCapabilities {
|
|
68
59
|
slug: string;
|
|
69
|
-
read: boolean;
|
|
70
|
-
write: McpxWriteMode;
|
|
71
60
|
hasDrafts: boolean;
|
|
72
61
|
/**
|
|
73
62
|
* The entity has Payload `versions`, with or without drafts, and is
|
|
@@ -75,13 +64,9 @@ export interface McpxExposedEntity {
|
|
|
75
64
|
*/
|
|
76
65
|
hasVersions: boolean;
|
|
77
66
|
/**
|
|
78
|
-
*
|
|
67
|
+
* Writes go live immediately, as the entity keeps no drafts.
|
|
79
68
|
*/
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* The config lets a key move documents to trash without approval.
|
|
83
|
-
*/
|
|
84
|
-
deleteUnattended: boolean;
|
|
69
|
+
liveWrite: boolean;
|
|
85
70
|
/**
|
|
86
71
|
* An upload document is a file, which only an upload grant can supply.
|
|
87
72
|
*/
|
|
@@ -92,7 +77,7 @@ export interface McpxExposedEntity {
|
|
|
92
77
|
fieldName: string;
|
|
93
78
|
}
|
|
94
79
|
/**
|
|
95
|
-
* The slugs a key may read, write and
|
|
80
|
+
* The slugs a key may read, write, publish and delete.
|
|
96
81
|
*/
|
|
97
82
|
export interface McpxScopeSlugs {
|
|
98
83
|
readable: string[];
|
|
@@ -299,8 +284,9 @@ export type McpxPluginOptions = {
|
|
|
299
284
|
*/
|
|
300
285
|
slug?: string;
|
|
301
286
|
/**
|
|
302
|
-
* Add a "Connect a client"
|
|
303
|
-
* client config. Default `true`. The snippets contain
|
|
287
|
+
* Add a "Connect a client" button to saved keys, opening a drawer with
|
|
288
|
+
* ready-to-paste MCP client config. Default `true`. The snippets contain
|
|
289
|
+
* the key in full.
|
|
304
290
|
*/
|
|
305
291
|
setupGuide?: boolean;
|
|
306
292
|
/**
|
|
@@ -346,9 +332,10 @@ export type McpxPluginOptions = {
|
|
|
346
332
|
diagnostics?: boolean;
|
|
347
333
|
};
|
|
348
334
|
/**
|
|
349
|
-
*
|
|
335
|
+
* One entity's operations, as the config exposes them or as a key may use
|
|
336
|
+
* them. Globals reuse this shape.
|
|
350
337
|
*/
|
|
351
|
-
export interface
|
|
338
|
+
export interface McpxEntityCapabilities {
|
|
352
339
|
read: boolean;
|
|
353
340
|
write: boolean;
|
|
354
341
|
/**
|
|
@@ -368,8 +355,8 @@ export interface McpxCollectionCapabilities {
|
|
|
368
355
|
* In force for one request: the plugin config and the key checkboxes together.
|
|
369
356
|
*/
|
|
370
357
|
export interface McpxResolvedCapabilities {
|
|
371
|
-
collections: Record<string,
|
|
372
|
-
globals: Record<string,
|
|
358
|
+
collections: Record<string, McpxEntityCapabilities>;
|
|
359
|
+
globals: Record<string, McpxEntityCapabilities>;
|
|
373
360
|
tools: Record<string, boolean>;
|
|
374
361
|
}
|
|
375
362
|
/**
|
package/dist/upload/file.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { grantContext, issueGrant } from "../grants/grant.mjs";
|
|
2
|
+
import { publicOrigin } from "../request.mjs";
|
|
2
3
|
import { errorResult, jsonResult } from "../result.mjs";
|
|
3
4
|
import { SchemaError } from "../schema/errors.mjs";
|
|
4
5
|
import { resolveDataPointer } from "../schema/pointer.mjs";
|
|
@@ -24,7 +25,7 @@ const fileSchema = (maxBytes) => z.strictObject({
|
|
|
24
25
|
const allowedMimeTypes = (scope, slug) => {
|
|
25
26
|
if (!scope.uploads || !scope.collections.writable.includes(slug)) return [];
|
|
26
27
|
const entity = scope.exposure.collections.find((candidate) => candidate.slug === slug);
|
|
27
|
-
const config = scope.req.payload.
|
|
28
|
+
const config = scope.req.payload.collections[slug]?.config;
|
|
28
29
|
return entity?.isUpload === true && Array.isArray(config?.upload.mimeTypes) ? config.upload.mimeTypes : [];
|
|
29
30
|
};
|
|
30
31
|
/**
|
|
@@ -115,20 +116,10 @@ const isClientWritable = (scope, target, path) => {
|
|
|
115
116
|
return blockers.length === 0 ? void 0 : errorResult("Nothing was written: the document fails validation.", { validationErrors: blockers });
|
|
116
117
|
};
|
|
117
118
|
const handoffUrl = (req, path) => {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
url.pathname = `${request.pathname.replace(/\/+$/, "")}/${path}`;
|
|
119
|
+
const url = new URL(publicOrigin(req));
|
|
120
|
+
url.pathname = `${new URL(req.url ?? "").pathname.replace(/\/+$/, "")}/${path}`;
|
|
121
121
|
return url.toString();
|
|
122
122
|
};
|
|
123
|
-
const grantContext = (req) => {
|
|
124
|
-
const slug = grantKvSlug(req.payload.config);
|
|
125
|
-
const apiKeyId = req.context.mcpx?.apiKeyId;
|
|
126
|
-
if (slug === void 0 || apiKeyId === void 0) throw new Error("A handoff was offered without a database KV or key.");
|
|
127
|
-
return {
|
|
128
|
-
slug,
|
|
129
|
-
apiKeyId
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
123
|
/**
|
|
133
124
|
* Stores the call as a grant and returns where to PUT its file. Nothing else
|
|
134
125
|
* is written: the PUT runs the same call again with the bytes attached.
|
|
@@ -137,11 +128,12 @@ const grantContext = (req) => {
|
|
|
137
128
|
const { payload } = req;
|
|
138
129
|
const { slug, apiKeyId } = grantContext(req);
|
|
139
130
|
const url = handoffUrl(req, "upload");
|
|
140
|
-
const {
|
|
131
|
+
const { id, exp } = await issueGrant(payload, slug, {
|
|
141
132
|
kind: "upload",
|
|
142
133
|
apiKeyId,
|
|
143
134
|
tool: call.tool,
|
|
144
135
|
args: call.args,
|
|
136
|
+
file: call.args.file,
|
|
145
137
|
locale: req.locale ?? null,
|
|
146
138
|
fallbackLocale: req.fallbackLocale ?? null
|
|
147
139
|
});
|
|
@@ -150,7 +142,7 @@ const grantContext = (req) => {
|
|
|
150
142
|
method: "PUT",
|
|
151
143
|
headers: {
|
|
152
144
|
"content-type": call.args.file.mimeType,
|
|
153
|
-
"x-mcpx-grant":
|
|
145
|
+
"x-mcpx-grant": id
|
|
154
146
|
},
|
|
155
147
|
expiresAt: new Date(exp).toISOString(),
|
|
156
148
|
maxBytes: uploadMaxBytes(payload.config)
|
|
@@ -168,7 +160,7 @@ const grantContext = (req) => {
|
|
|
168
160
|
const { req } = scope;
|
|
169
161
|
const { slug, apiKeyId } = grantContext(req);
|
|
170
162
|
const url = handoffUrl(req, "file");
|
|
171
|
-
const {
|
|
163
|
+
const { id, exp } = await issueGrant(req.payload, slug, {
|
|
172
164
|
kind: "download",
|
|
173
165
|
apiKeyId,
|
|
174
166
|
...target,
|
|
@@ -177,7 +169,7 @@ const grantContext = (req) => {
|
|
|
177
169
|
return {
|
|
178
170
|
url,
|
|
179
171
|
method: "GET",
|
|
180
|
-
headers: { "x-mcpx-grant":
|
|
172
|
+
headers: { "x-mcpx-grant": id },
|
|
181
173
|
expiresAt: new Date(exp).toISOString()
|
|
182
174
|
};
|
|
183
175
|
};
|