@abinnovision/payloadcms-mcpx 1.1.0-beta.6 → 2.0.0-beta.10
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 +140 -470
- package/dist/admin/capability-matrix.d.mts +33 -0
- package/dist/admin/capability-matrix.mjs +354 -0
- package/dist/admin/capability-toggles.mjs +73 -0
- package/dist/{client → admin}/setup-guide.d.mts +4 -2
- package/dist/{client → admin}/setup-guide.mjs +3 -10
- package/dist/api-keys/capability-matrix.d.mts +12 -8
- package/dist/api-keys/capability-matrix.mjs +17 -85
- package/dist/api-keys/collection.mjs +2 -1
- package/dist/api-keys/fields.mjs +36 -24
- package/dist/auth/resolve.mjs +44 -6
- package/dist/{tools/names.mjs → builtin-tool-names.mjs} +1 -1
- package/dist/capabilities.mjs +41 -43
- package/dist/define-tool.d.mts +23 -0
- package/dist/{types.mjs → define-tool.mjs} +1 -1
- package/dist/endpoint/errors.mjs +5 -6
- package/dist/endpoint/handler.mjs +49 -19
- package/dist/endpoint/server.mjs +72 -20
- package/dist/entity.d.mts +9 -0
- package/dist/guards.mjs +7 -0
- package/dist/i18n.mjs +10 -8
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +3 -2
- package/dist/options.mjs +52 -46
- package/dist/plugin.mjs +0 -1
- package/dist/request.d.mts +2 -3
- package/dist/request.mjs +2 -3
- package/dist/result.d.mts +2 -3
- package/dist/result.mjs +3 -3
- package/dist/schema/admin-hidden.mjs +91 -0
- package/dist/schema/describe.mjs +17 -33
- package/dist/schema/errors.mjs +8 -0
- package/dist/schema/index.mjs +5 -2
- package/dist/schema/lexical-pointer.mjs +18 -20
- package/dist/schema/lexical.mjs +44 -93
- package/dist/schema/outline.mjs +5 -15
- package/dist/schema/path.mjs +28 -0
- package/dist/schema/pointer.mjs +18 -25
- package/dist/schema/shape.mjs +25 -71
- package/dist/schema/walk.mjs +43 -45
- package/dist/tools/builtin.mjs +4 -4
- package/dist/tools/create-document.mjs +26 -23
- package/dist/tools/describe-schema.mjs +38 -34
- package/dist/tools/document.mjs +124 -0
- package/dist/tools/find-documents.mjs +32 -19
- package/dist/tools/find-versions.mjs +22 -18
- package/dist/tools/get-document.mjs +39 -32
- package/dist/tools/list-capabilities.mjs +23 -16
- package/dist/tools/patch-document.mjs +75 -49
- package/dist/tools/publish-document.mjs +26 -32
- package/dist/tools/query-paths.mjs +57 -0
- package/dist/tools/read-request.d.mts +10 -0
- package/dist/tools/read-request.mjs +43 -0
- package/dist/tools/shared.mjs +67 -99
- package/dist/tools/validate-document.mjs +18 -30
- package/dist/tools/versions.mjs +37 -20
- package/dist/types.d.mts +118 -74
- package/dist/write/draft-guard.mjs +23 -41
- package/dist/write/patch.mjs +27 -183
- package/dist/write/publish-blockers.mjs +7 -14
- package/dist/write/publish-intent.mjs +6 -4
- package/dist/write/row-ids.mjs +46 -0
- package/dist/write/transaction.mjs +5 -8
- package/dist/write/write-data.mjs +64 -0
- package/package.json +6 -5
- package/dist/client/capability-matrix.d.mts +0 -26
- package/dist/client/capability-matrix.mjs +0 -349
- package/dist/endpoint/index.mjs +0 -4
- package/dist/tools/target.mjs +0 -45
- /package/dist/{client → admin}/index.d.mts +0 -0
- /package/dist/{client → admin}/index.mjs +0 -0
package/dist/types.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DocumentId } from "./entity.mjs";
|
|
1
2
|
import { CollectionConfig, CollectionSlug, GlobalSlug, PayloadRequest, TypedUser } from "payload";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
@@ -17,59 +18,85 @@ declare module "payload" {
|
|
|
17
18
|
* - `false`: no write tool touches it.
|
|
18
19
|
* - `"draft"`: writes land as drafts and nothing MCP does changes what the
|
|
19
20
|
* public sees. Requires `versions.drafts`.
|
|
20
|
-
* - `"live"`: MCP may change live content. On an entity with drafts that
|
|
21
|
-
* `publishDocument
|
|
22
|
-
*
|
|
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.
|
|
23
24
|
*/
|
|
24
25
|
export type McpxWriteMode = "draft" | "live" | false;
|
|
25
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* What an entity exposes. The config names the entities that are reachable and
|
|
28
|
+
* only takes capabilities away; a key's checkboxes decide per key. `true` is
|
|
29
|
+
* shorthand for `{}`, which exposes everything the entity supports.
|
|
30
|
+
*/
|
|
26
31
|
export interface McpxCollectionOptions {
|
|
27
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Expose `describeSchema`, `findDocuments`, `getDocument`. Default `true`.
|
|
34
|
+
*/
|
|
28
35
|
read?: boolean;
|
|
29
36
|
/**
|
|
30
37
|
* Expose `patchDocument`, `validateDocument` and, unless this is an upload
|
|
31
|
-
* collection, `createDocument
|
|
32
|
-
* `
|
|
38
|
+
* collection, `createDocument`. Without drafts a write changes live
|
|
39
|
+
* content. Default `true`.
|
|
33
40
|
*/
|
|
34
|
-
write?:
|
|
35
|
-
}
|
|
36
|
-
/** A singleton, so neither `findDocuments` nor `createDocument` reaches one. */
|
|
37
|
-
export interface McpxGlobalOptions {
|
|
38
|
-
/** Expose `describeSchema` and `getDocument`. Default `true`. */
|
|
39
|
-
read?: boolean;
|
|
41
|
+
write?: boolean;
|
|
40
42
|
/**
|
|
41
|
-
* Expose `
|
|
42
|
-
*
|
|
43
|
+
* Expose `publishDocument`, which promotes a draft to live content. Needs
|
|
44
|
+
* `versions.drafts` and `write`. Default `true` where the entity has drafts.
|
|
45
|
+
* `false` keeps writes as drafts and is refused where there are none.
|
|
43
46
|
*/
|
|
44
|
-
|
|
47
|
+
publish?: boolean;
|
|
45
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The same options. A singleton, so neither `findDocuments` nor
|
|
51
|
+
* `createDocument` reaches one.
|
|
52
|
+
*/
|
|
53
|
+
export type McpxGlobalOptions = McpxCollectionOptions;
|
|
46
54
|
export type McpxToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;
|
|
47
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* What the config exposes, before an API key's checkboxes narrow it.
|
|
57
|
+
*/
|
|
48
58
|
export interface McpxExposedEntity {
|
|
49
59
|
slug: string;
|
|
50
60
|
read: boolean;
|
|
51
61
|
write: McpxWriteMode;
|
|
52
62
|
hasDrafts: boolean;
|
|
53
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* The entity has Payload `versions`, with or without drafts, and is
|
|
65
|
+
* readable. Says nothing about drafts; see `hasDrafts`.
|
|
66
|
+
*/
|
|
54
67
|
hasVersions: boolean;
|
|
55
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* An upload document is a file, and no tool here can supply one.
|
|
70
|
+
*/
|
|
56
71
|
isUpload: boolean;
|
|
57
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Name of the capability group on the key document.
|
|
74
|
+
*/
|
|
58
75
|
fieldName: string;
|
|
59
76
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The slugs a key may read, write and publish.
|
|
79
|
+
*/
|
|
80
|
+
export interface McpxScopeSlugs {
|
|
64
81
|
readable: string[];
|
|
65
82
|
writable: string[];
|
|
66
83
|
publishable: string[];
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* What a tool knows about the current request.
|
|
87
|
+
*/
|
|
88
|
+
export interface McpxToolScope {
|
|
89
|
+
req: PayloadRequest;
|
|
90
|
+
capabilities: McpxResolvedCapabilities;
|
|
91
|
+
collections: McpxScopeSlugs;
|
|
92
|
+
globals: McpxScopeSlugs;
|
|
93
|
+
/**
|
|
94
|
+
* `null` when localization is off.
|
|
95
|
+
*/
|
|
96
|
+
localization: null | {
|
|
97
|
+
locales: string[];
|
|
98
|
+
defaultLocale: string;
|
|
99
|
+
};
|
|
73
100
|
limits: {
|
|
74
101
|
maxLimit: number;
|
|
75
102
|
maxDepth: number;
|
|
@@ -85,9 +112,13 @@ export interface McpxToolScope {
|
|
|
85
112
|
* per request, leaving no static shape to infer from.
|
|
86
113
|
*/
|
|
87
114
|
export interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.infer<z.ZodObject<Shape>>> {
|
|
88
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* camelCase, unique, not one of the builtin tool names.
|
|
117
|
+
*/
|
|
89
118
|
name: string;
|
|
90
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Built per request so it can state what this key's writes do.
|
|
121
|
+
*/
|
|
91
122
|
description: string | ((scope: McpxToolScope) => string);
|
|
92
123
|
annotations?: ToolAnnotations;
|
|
93
124
|
/**
|
|
@@ -99,39 +130,32 @@ export interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.
|
|
|
99
130
|
/**
|
|
100
131
|
* Built per request so enums can be narrowed to what the key may touch.
|
|
101
132
|
* Registered strictly either way: an unknown argument is rejected by name
|
|
102
|
-
*
|
|
133
|
+
* instead of stripped.
|
|
103
134
|
*/
|
|
104
135
|
inputSchema?: Shape | ((scope: McpxToolScope) => z.ZodRawShape);
|
|
105
136
|
handler(ctx: {
|
|
106
137
|
args: Args;
|
|
107
138
|
scope: McpxToolScope;
|
|
108
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Shorthand for `scope.req`.
|
|
141
|
+
*/
|
|
109
142
|
req: PayloadRequest;
|
|
110
143
|
extra: McpxToolExtra;
|
|
111
144
|
}): CallToolResult | Promise<CallToolResult>;
|
|
112
145
|
}
|
|
113
|
-
/** Argument type erased, so a registry can hold tools of differing shapes. */
|
|
114
|
-
export type McpxAnyTool = McpxTool<z.ZodRawShape, never>;
|
|
115
|
-
/** Fixed shape; arguments inferred from it. */
|
|
116
|
-
export declare function defineMcpxTool<Shape extends z.ZodRawShape>(tool: McpxTool<Shape> & {
|
|
117
|
-
inputSchema?: Shape;
|
|
118
|
-
}): McpxTool<Shape>;
|
|
119
|
-
/** Per-request shape returned as an object literal; arguments inferred from it. */
|
|
120
|
-
export declare function defineMcpxTool<Shape extends z.ZodRawShape>(tool: McpxTool<Shape> & {
|
|
121
|
-
inputSchema: (scope: McpxToolScope) => Shape;
|
|
122
|
-
}): McpxAnyTool;
|
|
123
146
|
/**
|
|
124
|
-
*
|
|
125
|
-
* builtins do. Nothing to infer from, so state the arguments instead.
|
|
147
|
+
* Argument type erased, so a registry can hold tools of differing shapes.
|
|
126
148
|
*/
|
|
127
|
-
export
|
|
128
|
-
inputSchema: (scope: McpxToolScope) => z.ZodRawShape;
|
|
129
|
-
}): McpxAnyTool;
|
|
149
|
+
export type McpxAnyTool = McpxTool<z.ZodRawShape, never>;
|
|
130
150
|
export interface McpxAuthResult {
|
|
131
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Must carry `collection`.
|
|
153
|
+
*/
|
|
132
154
|
user: TypedUser;
|
|
133
|
-
apiKeyId:
|
|
134
|
-
/**
|
|
155
|
+
apiKeyId: DocumentId;
|
|
156
|
+
/**
|
|
157
|
+
* The `capabilities` group as stored on the key document.
|
|
158
|
+
*/
|
|
135
159
|
capabilities: unknown;
|
|
136
160
|
}
|
|
137
161
|
/**
|
|
@@ -141,36 +165,56 @@ export interface McpxAuthResult {
|
|
|
141
165
|
* to `Record<string, unknown>`, which interfaces do not satisfy.
|
|
142
166
|
*/
|
|
143
167
|
export type McpxPluginOptions = {
|
|
144
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Allow-list of collections. `true` is shorthand for `{}`.
|
|
170
|
+
*/
|
|
145
171
|
collections: Partial<Record<CollectionSlug, McpxCollectionOptions | true>>;
|
|
146
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Allow-list of globals. `true` is shorthand for `{}`.
|
|
174
|
+
*/
|
|
147
175
|
globals?: Partial<Record<GlobalSlug, McpxGlobalOptions | true>>;
|
|
148
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Collection the keys act as. Default `config.admin.user`, then `users`.
|
|
178
|
+
*/
|
|
149
179
|
userCollection?: CollectionSlug;
|
|
150
180
|
apiKeys?: {
|
|
151
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* Slug of the generated API key collection. Default `mcpx-api-keys`.
|
|
183
|
+
*/
|
|
152
184
|
slug?: string;
|
|
153
185
|
/**
|
|
154
186
|
* Add a "Connect a client" tab to saved keys, holding ready-to-paste MCP
|
|
155
187
|
* client config. Default `true`. The snippets contain the key in full.
|
|
156
188
|
*/
|
|
157
189
|
setupGuide?: boolean;
|
|
158
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Final override applied to the generated collection.
|
|
192
|
+
*/
|
|
159
193
|
overrideCollection?: (collection: CollectionConfig) => CollectionConfig;
|
|
160
194
|
};
|
|
161
195
|
endpoint?: {
|
|
162
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Endpoint path below the API route. Default `/mcpx`.
|
|
198
|
+
*/
|
|
163
199
|
path?: string;
|
|
164
200
|
};
|
|
165
201
|
limits?: {
|
|
166
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Upper bound for `findDocuments.limit`. Default 25.
|
|
204
|
+
*/
|
|
167
205
|
maxLimit?: number;
|
|
168
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Upper bound for `depth` on reads. Default 1.
|
|
208
|
+
*/
|
|
169
209
|
maxDepth?: number;
|
|
170
210
|
};
|
|
171
211
|
tools?: McpxAnyTool[];
|
|
172
212
|
auth?: {
|
|
173
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* Replace or wrap the default key resolution. Return `null` for 401. A
|
|
215
|
+
* result whose user has no `id` or whose `collection` is not the user
|
|
216
|
+
* collection, or that has no `apiKeyId`, is also answered with 401.
|
|
217
|
+
*/
|
|
174
218
|
resolve?: (args: {
|
|
175
219
|
req: PayloadRequest;
|
|
176
220
|
resolveDefault: () => Promise<McpxAuthResult | null>;
|
|
@@ -181,30 +225,30 @@ export type McpxPluginOptions = {
|
|
|
181
225
|
version?: string;
|
|
182
226
|
};
|
|
183
227
|
};
|
|
184
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* What a key may do with one entity. Globals reuse this shape.
|
|
230
|
+
*/
|
|
185
231
|
export interface McpxCollectionCapabilities {
|
|
186
232
|
read: boolean;
|
|
187
233
|
write: boolean;
|
|
188
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Only ever true where the config lets writes publish and drafts exist.
|
|
236
|
+
*/
|
|
189
237
|
publish: boolean;
|
|
190
238
|
}
|
|
191
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* In force for one request: the plugin config and the key checkboxes together.
|
|
241
|
+
*/
|
|
192
242
|
export interface McpxResolvedCapabilities {
|
|
193
243
|
collections: Record<string, McpxCollectionCapabilities>;
|
|
194
244
|
globals: Record<string, McpxCollectionCapabilities>;
|
|
195
245
|
tools: Record<string, boolean>;
|
|
196
246
|
}
|
|
197
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Stamped on `req.context.mcpx`; see {@link isMcpxRequest}.
|
|
249
|
+
*/
|
|
198
250
|
export interface McpxRequestContext {
|
|
199
|
-
apiKeyId:
|
|
251
|
+
apiKeyId: DocumentId;
|
|
200
252
|
capabilities: McpxResolvedCapabilities;
|
|
201
253
|
}
|
|
202
|
-
/** One reason a human could not publish the draft as it stands. */
|
|
203
|
-
export interface PublishBlocker {
|
|
204
|
-
/** Resolved field label path, e.g. "Layout > Block 2 (Hero) > Title". */
|
|
205
|
-
field?: string;
|
|
206
|
-
message: string;
|
|
207
|
-
/** JSON Pointer to the offending value, e.g. "/layout/2/title". */
|
|
208
|
-
path: string;
|
|
209
|
-
}
|
|
210
254
|
//#endregion
|
|
@@ -3,30 +3,16 @@ import { isMcpxRequest } from "../request.mjs";
|
|
|
3
3
|
import { APIError } from "payload";
|
|
4
4
|
import { hasDraftsEnabled } from "payload/shared";
|
|
5
5
|
//#region src/write/draft-guard.ts
|
|
6
|
-
|
|
6
|
+
const STRIPPED_ARGS = /* @__PURE__ */ new Set([
|
|
7
7
|
"where",
|
|
8
8
|
"publishAllLocales",
|
|
9
|
-
"publishSpecificLocale",
|
|
10
9
|
"unpublishAllLocales",
|
|
11
10
|
"duplicateFromID",
|
|
12
11
|
"selectedLocales",
|
|
13
12
|
"overwriteExistingFiles"
|
|
14
13
|
]);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* `publishDocument` asked for.
|
|
18
|
-
*
|
|
19
|
-
* `draft` alone is not enough: Payload's update path only saves a draft when
|
|
20
|
-
* `data._status !== "published"`, so `_status` is dropped and left to Payload.
|
|
21
|
-
* Writing it here rather than in the tool keeps the tool honest, since this is
|
|
22
|
-
* the only thing that can grant a publish.
|
|
23
|
-
*
|
|
24
|
-
* Not covered: deletes, `duplicate`, files (the local API lifts `file` and
|
|
25
|
-
* `filePath` onto `req` before this runs), and anything going straight to
|
|
26
|
-
* `payload.db`. `restoreVersion` is caught by {@link refusePublish} instead,
|
|
27
|
-
* because it runs the collection's `beforeChange` hooks.
|
|
28
|
-
*/ const scrubWriteArgs = (args, publishing) => {
|
|
29
|
-
const next = Object.fromEntries(Object.entries(args).filter(([key]) => !STRIPPED_ARGS.has(key)));
|
|
14
|
+
const scrubWriteArgs = (args, publishing) => {
|
|
15
|
+
const next = Object.fromEntries(Object.entries(args).filter(([key]) => !STRIPPED_ARGS.has(key) && (publishing || key !== "publishSpecificLocale")));
|
|
30
16
|
if (next["data"] && typeof next["data"] === "object") {
|
|
31
17
|
const { _status: _ignoredStatus, deletedAt: _ignoredDeletedAt, ...data } = next["data"];
|
|
32
18
|
next["data"] = publishing ? {
|
|
@@ -47,25 +33,22 @@ const forceDraftWrite = (hookArgs) => {
|
|
|
47
33
|
return scrubWriteArgs(args, publishing);
|
|
48
34
|
};
|
|
49
35
|
/**
|
|
50
|
-
* The global counterpart of {@link forceDraftWrite}
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `_status` stripped, which makes {@link refusePublishGlobal} the alarm that
|
|
55
|
-
* actually holds the line. `publishDocument` therefore passes `draft: false` at
|
|
56
|
-
* the call site, and this hook puts `_status` back rather than stripping it.
|
|
36
|
+
* The global counterpart of {@link forceDraftWrite}. `updateGlobal` reads
|
|
37
|
+
* `draft` and the publish arguments before this hook runs, so only changes to
|
|
38
|
+
* `data` take effect. A write to a global with drafts that did not ask for a
|
|
39
|
+
* draft save is therefore refused here, and `_status` is restored for a publish.
|
|
57
40
|
*/ const forceDraftWriteGlobal = (hookArgs) => {
|
|
58
|
-
const { operation, req } = hookArgs;
|
|
41
|
+
const { global, operation, req } = hookArgs;
|
|
59
42
|
const args = hookArgs.args;
|
|
60
43
|
if (!isMcpxRequest(req) || operation !== "update") return args;
|
|
61
|
-
|
|
44
|
+
const publishing = hasPublishIntent(args["data"]);
|
|
45
|
+
if (!publishing && args["draft"] !== true && hasDraftsEnabled(global)) {
|
|
46
|
+
req.payload.logger.warn(`[payloadcms-mcpx] Refused a write to ${global.slug} that did not ask for a draft save.`);
|
|
47
|
+
throw new APIError("MCP clients may only write drafts. This write was refused because it would not have been saved as one. Use publishDocument to publish.", 403);
|
|
48
|
+
}
|
|
49
|
+
return scrubWriteArgs(args, publishing);
|
|
62
50
|
};
|
|
63
|
-
|
|
64
|
-
* Throws instead of correcting `_status`, because Payload has already chosen
|
|
65
|
-
* the write branch by the time a `beforeChange` hook runs. Unreachable for a
|
|
66
|
-
* collection if {@link forceDraftWrite} did its job; the guarantee itself for a
|
|
67
|
-
* global. Last hook that needs the marker, so it takes it off.
|
|
68
|
-
*/ const refuseUnlessExpected = (req, slug, data) => {
|
|
51
|
+
const refuseUnlessExpected = (req, slug, data) => {
|
|
69
52
|
const publishing = takePublishIntent(data);
|
|
70
53
|
if (!isMcpxRequest(req)) return;
|
|
71
54
|
const status = data._status;
|
|
@@ -75,8 +58,8 @@ const forceDraftWrite = (hookArgs) => {
|
|
|
75
58
|
throw new APIError(publishing ? "This publish was refused because it would not have saved a published document." : "MCP clients may only write drafts. This write was refused because it would not have been saved as one. Use publishDocument to publish.", 403);
|
|
76
59
|
};
|
|
77
60
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
61
|
+
* Runs {@link refuseUnlessExpected} on every collection write. Returns `data`
|
|
62
|
+
* unchanged; the hook exists for its throw.
|
|
80
63
|
*/ const refusePublish = ({ collection, data, req }) => {
|
|
81
64
|
refuseUnlessExpected(req, collection.slug, data);
|
|
82
65
|
return data;
|
|
@@ -88,9 +71,9 @@ const refusePublishGlobal = ({ data, global, req }) => {
|
|
|
88
71
|
};
|
|
89
72
|
/**
|
|
90
73
|
* Attaches the draft guard to every collection: `forceDraftWrite` everywhere
|
|
91
|
-
* (it
|
|
92
|
-
* exist.
|
|
93
|
-
*
|
|
74
|
+
* (it does nothing outside MCP requests) and `refusePublish` wherever drafts
|
|
75
|
+
* exist. It runs on the final collection list, so no collection escapes it, and
|
|
76
|
+
* both hooks are appended last, so a user hook cannot override them.
|
|
94
77
|
*/ const installDraftGuards = (collections) => collections.map((collection) => ({
|
|
95
78
|
...collection,
|
|
96
79
|
hooks: {
|
|
@@ -100,10 +83,9 @@ const refusePublishGlobal = ({ data, global, req }) => {
|
|
|
100
83
|
}
|
|
101
84
|
}));
|
|
102
85
|
/**
|
|
103
|
-
* Attaches the guard to every global, exposed or not
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* mentioned.
|
|
86
|
+
* Attaches the guard to every global, exposed or not: a custom tool running on
|
|
87
|
+
* an MCP request must not be able to publish through a global the plugin config
|
|
88
|
+
* never mentioned.
|
|
107
89
|
*/ const installGlobalDraftGuards = (globals) => globals.map((global) => ({
|
|
108
90
|
...global,
|
|
109
91
|
hooks: {
|