@abinnovision/payloadcms-mcpx 1.1.0-beta.5 → 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 -448
- 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} +2 -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 -44
- 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 +6 -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 +74 -0
- package/dist/tools/get-document.mjs +80 -49
- package/dist/tools/list-capabilities.mjs +25 -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 +74 -96
- package/dist/tools/validate-document.mjs +18 -30
- package/dist/tools/versions.mjs +136 -0
- package/dist/types.d.mts +119 -73
- 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,57 +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
|
+
*/
|
|
67
|
+
hasVersions: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* An upload document is a file, and no tool here can supply one.
|
|
70
|
+
*/
|
|
54
71
|
isUpload: boolean;
|
|
55
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Name of the capability group on the key document.
|
|
74
|
+
*/
|
|
56
75
|
fieldName: string;
|
|
57
76
|
}
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The slugs a key may read, write and publish.
|
|
79
|
+
*/
|
|
80
|
+
export interface McpxScopeSlugs {
|
|
62
81
|
readable: string[];
|
|
63
82
|
writable: string[];
|
|
64
83
|
publishable: string[];
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
};
|
|
71
100
|
limits: {
|
|
72
101
|
maxLimit: number;
|
|
73
102
|
maxDepth: number;
|
|
@@ -83,9 +112,13 @@ export interface McpxToolScope {
|
|
|
83
112
|
* per request, leaving no static shape to infer from.
|
|
84
113
|
*/
|
|
85
114
|
export interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.infer<z.ZodObject<Shape>>> {
|
|
86
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* camelCase, unique, not one of the builtin tool names.
|
|
117
|
+
*/
|
|
87
118
|
name: string;
|
|
88
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Built per request so it can state what this key's writes do.
|
|
121
|
+
*/
|
|
89
122
|
description: string | ((scope: McpxToolScope) => string);
|
|
90
123
|
annotations?: ToolAnnotations;
|
|
91
124
|
/**
|
|
@@ -97,39 +130,32 @@ export interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.
|
|
|
97
130
|
/**
|
|
98
131
|
* Built per request so enums can be narrowed to what the key may touch.
|
|
99
132
|
* Registered strictly either way: an unknown argument is rejected by name
|
|
100
|
-
*
|
|
133
|
+
* instead of stripped.
|
|
101
134
|
*/
|
|
102
135
|
inputSchema?: Shape | ((scope: McpxToolScope) => z.ZodRawShape);
|
|
103
136
|
handler(ctx: {
|
|
104
137
|
args: Args;
|
|
105
138
|
scope: McpxToolScope;
|
|
106
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* Shorthand for `scope.req`.
|
|
141
|
+
*/
|
|
107
142
|
req: PayloadRequest;
|
|
108
143
|
extra: McpxToolExtra;
|
|
109
144
|
}): CallToolResult | Promise<CallToolResult>;
|
|
110
145
|
}
|
|
111
|
-
/** Argument type erased, so a registry can hold tools of differing shapes. */
|
|
112
|
-
export type McpxAnyTool = McpxTool<z.ZodRawShape, never>;
|
|
113
|
-
/** Fixed shape; arguments inferred from it. */
|
|
114
|
-
export declare function defineMcpxTool<Shape extends z.ZodRawShape>(tool: McpxTool<Shape> & {
|
|
115
|
-
inputSchema?: Shape;
|
|
116
|
-
}): McpxTool<Shape>;
|
|
117
|
-
/** Per-request shape returned as an object literal; arguments inferred from it. */
|
|
118
|
-
export declare function defineMcpxTool<Shape extends z.ZodRawShape>(tool: McpxTool<Shape> & {
|
|
119
|
-
inputSchema: (scope: McpxToolScope) => Shape;
|
|
120
|
-
}): McpxAnyTool;
|
|
121
146
|
/**
|
|
122
|
-
*
|
|
123
|
-
* builtins do. Nothing to infer from, so state the arguments instead.
|
|
147
|
+
* Argument type erased, so a registry can hold tools of differing shapes.
|
|
124
148
|
*/
|
|
125
|
-
export
|
|
126
|
-
inputSchema: (scope: McpxToolScope) => z.ZodRawShape;
|
|
127
|
-
}): McpxAnyTool;
|
|
149
|
+
export type McpxAnyTool = McpxTool<z.ZodRawShape, never>;
|
|
128
150
|
export interface McpxAuthResult {
|
|
129
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Must carry `collection`.
|
|
153
|
+
*/
|
|
130
154
|
user: TypedUser;
|
|
131
|
-
apiKeyId:
|
|
132
|
-
/**
|
|
155
|
+
apiKeyId: DocumentId;
|
|
156
|
+
/**
|
|
157
|
+
* The `capabilities` group as stored on the key document.
|
|
158
|
+
*/
|
|
133
159
|
capabilities: unknown;
|
|
134
160
|
}
|
|
135
161
|
/**
|
|
@@ -139,36 +165,56 @@ export interface McpxAuthResult {
|
|
|
139
165
|
* to `Record<string, unknown>`, which interfaces do not satisfy.
|
|
140
166
|
*/
|
|
141
167
|
export type McpxPluginOptions = {
|
|
142
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Allow-list of collections. `true` is shorthand for `{}`.
|
|
170
|
+
*/
|
|
143
171
|
collections: Partial<Record<CollectionSlug, McpxCollectionOptions | true>>;
|
|
144
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Allow-list of globals. `true` is shorthand for `{}`.
|
|
174
|
+
*/
|
|
145
175
|
globals?: Partial<Record<GlobalSlug, McpxGlobalOptions | true>>;
|
|
146
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Collection the keys act as. Default `config.admin.user`, then `users`.
|
|
178
|
+
*/
|
|
147
179
|
userCollection?: CollectionSlug;
|
|
148
180
|
apiKeys?: {
|
|
149
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* Slug of the generated API key collection. Default `mcpx-api-keys`.
|
|
183
|
+
*/
|
|
150
184
|
slug?: string;
|
|
151
185
|
/**
|
|
152
186
|
* Add a "Connect a client" tab to saved keys, holding ready-to-paste MCP
|
|
153
187
|
* client config. Default `true`. The snippets contain the key in full.
|
|
154
188
|
*/
|
|
155
189
|
setupGuide?: boolean;
|
|
156
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Final override applied to the generated collection.
|
|
192
|
+
*/
|
|
157
193
|
overrideCollection?: (collection: CollectionConfig) => CollectionConfig;
|
|
158
194
|
};
|
|
159
195
|
endpoint?: {
|
|
160
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Endpoint path below the API route. Default `/mcpx`.
|
|
198
|
+
*/
|
|
161
199
|
path?: string;
|
|
162
200
|
};
|
|
163
201
|
limits?: {
|
|
164
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Upper bound for `findDocuments.limit`. Default 25.
|
|
204
|
+
*/
|
|
165
205
|
maxLimit?: number;
|
|
166
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Upper bound for `depth` on reads. Default 1.
|
|
208
|
+
*/
|
|
167
209
|
maxDepth?: number;
|
|
168
210
|
};
|
|
169
211
|
tools?: McpxAnyTool[];
|
|
170
212
|
auth?: {
|
|
171
|
-
/**
|
|
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
|
+
*/
|
|
172
218
|
resolve?: (args: {
|
|
173
219
|
req: PayloadRequest;
|
|
174
220
|
resolveDefault: () => Promise<McpxAuthResult | null>;
|
|
@@ -179,30 +225,30 @@ export type McpxPluginOptions = {
|
|
|
179
225
|
version?: string;
|
|
180
226
|
};
|
|
181
227
|
};
|
|
182
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* What a key may do with one entity. Globals reuse this shape.
|
|
230
|
+
*/
|
|
183
231
|
export interface McpxCollectionCapabilities {
|
|
184
232
|
read: boolean;
|
|
185
233
|
write: boolean;
|
|
186
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* Only ever true where the config lets writes publish and drafts exist.
|
|
236
|
+
*/
|
|
187
237
|
publish: boolean;
|
|
188
238
|
}
|
|
189
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* In force for one request: the plugin config and the key checkboxes together.
|
|
241
|
+
*/
|
|
190
242
|
export interface McpxResolvedCapabilities {
|
|
191
243
|
collections: Record<string, McpxCollectionCapabilities>;
|
|
192
244
|
globals: Record<string, McpxCollectionCapabilities>;
|
|
193
245
|
tools: Record<string, boolean>;
|
|
194
246
|
}
|
|
195
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Stamped on `req.context.mcpx`; see {@link isMcpxRequest}.
|
|
249
|
+
*/
|
|
196
250
|
export interface McpxRequestContext {
|
|
197
|
-
apiKeyId:
|
|
251
|
+
apiKeyId: DocumentId;
|
|
198
252
|
capabilities: McpxResolvedCapabilities;
|
|
199
253
|
}
|
|
200
|
-
/** One reason a human could not publish the draft as it stands. */
|
|
201
|
-
export interface PublishBlocker {
|
|
202
|
-
/** Resolved field label path, e.g. "Layout > Block 2 (Hero) > Title". */
|
|
203
|
-
field?: string;
|
|
204
|
-
message: string;
|
|
205
|
-
/** JSON Pointer to the offending value, e.g. "/layout/2/title". */
|
|
206
|
-
path: string;
|
|
207
|
-
}
|
|
208
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: {
|