@abinnovision/payloadcms-mcpx 2.0.0-beta.8 → 2.0.0
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 +17 -7
- 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 +6 -4
- package/dist/admin/setup-guide.mjs +51 -33
- package/dist/api-keys/capability-matrix.d.mts +12 -10
- package/dist/api-keys/capability-matrix.mjs +17 -37
- package/dist/api-keys/collection.mjs +18 -19
- package/dist/api-keys/confirmation-view.mjs +7 -0
- package/dist/api-keys/fields.mjs +67 -53
- package/dist/auth/resolve.mjs +79 -30
- package/dist/capabilities.d.mts +2 -0
- package/dist/capabilities.mjs +69 -15
- package/dist/define-tool.d.mts +10 -6
- 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 +52 -27
- package/dist/endpoint/upload.mjs +104 -0
- package/dist/grants/confirmation.mjs +75 -0
- package/dist/grants/grant.mjs +137 -0
- package/dist/guards.mjs +11 -2
- package/dist/i18n.mjs +3 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/options.mjs +73 -62
- 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 +21 -15
- package/dist/schema/outline.mjs +4 -5
- package/dist/schema/path.mjs +13 -4
- package/dist/schema/pointer.mjs +11 -17
- package/dist/schema/shape.mjs +39 -51
- package/dist/schema/walk.mjs +68 -21
- 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 +147 -21
- 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 +60 -54
- package/dist/tools/publish-document.mjs +23 -30
- package/dist/tools/query-paths.mjs +4 -2
- package/dist/tools/run-confirmed.mjs +114 -0
- package/dist/tools/shared.mjs +66 -50
- package/dist/tools/validate-document.mjs +10 -11
- package/dist/tools/versions.mjs +24 -10
- package/dist/types.d.mts +228 -60
- package/dist/upload/file.mjs +177 -0
- package/dist/version.mjs +4 -2
- package/dist/write/draft-guard.mjs +46 -37
- package/dist/write/patch.mjs +4 -4
- package/dist/write/publish-intent.mjs +25 -11
- package/dist/write/row-ids.mjs +6 -2
- package/dist/write/write-data.mjs +18 -31
- package/package.json +15 -14
- package/dist/builtin-tool-names.mjs +0 -16
package/dist/types.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DocumentId } from "./entity.mjs";
|
|
2
|
-
import { CollectionConfig, CollectionSlug, GlobalSlug, PayloadRequest, TypedUser } from "payload";
|
|
3
2
|
import { z } from "zod";
|
|
3
|
+
import { CollectionConfig, CollectionSlug, GlobalSlug, PayloadRequest, TypedUser } from "payload";
|
|
4
4
|
import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
5
5
|
import { CallToolResult, ServerNotification, ServerRequest, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
//#region src/types.d.ts
|
|
@@ -12,29 +12,21 @@ 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
|
|
29
18
|
* shorthand for `{}`, which exposes everything the entity supports.
|
|
30
19
|
*/
|
|
31
20
|
export interface McpxCollectionOptions {
|
|
32
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Expose `describeSchema`, `findDocuments`, `getDocument`. Default `true`.
|
|
23
|
+
*/
|
|
33
24
|
read?: boolean;
|
|
34
25
|
/**
|
|
35
|
-
* Expose `patchDocument`, `validateDocument` and
|
|
36
|
-
*
|
|
37
|
-
*
|
|
26
|
+
* Expose `patchDocument`, `validateDocument`, `createDocument` and
|
|
27
|
+
* `duplicateDocument`. An upload collection is created in only where MCP
|
|
28
|
+
* accepts its files, and is never duplicated. Without drafts
|
|
29
|
+
* a write changes live content. Needs `read`. Default `true` while `read` is.
|
|
38
30
|
*/
|
|
39
31
|
write?: boolean;
|
|
40
32
|
/**
|
|
@@ -43,39 +35,67 @@ export interface McpxCollectionOptions {
|
|
|
43
35
|
* `false` keeps writes as drafts and is refused where there are none.
|
|
44
36
|
*/
|
|
45
37
|
publish?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Expose `deleteDocument`, which deletes one document once the key's user
|
|
40
|
+
* approved the call in the admin panel. On a collection with `trash` it
|
|
41
|
+
* moves the document to trash. `"unattended"` also lets a key set to it move
|
|
42
|
+
* documents to trash without approval; it needs `trash`. Needs `read`.
|
|
43
|
+
* Default `false`.
|
|
44
|
+
*/
|
|
45
|
+
delete?: boolean | "unattended";
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The same options without `delete`. A singleton, so neither `findDocuments`,
|
|
49
|
+
* `createDocument` nor `deleteDocument` reaches one.
|
|
50
|
+
*/
|
|
51
|
+
export type McpxGlobalOptions = Omit<McpxCollectionOptions, "delete">;
|
|
49
52
|
export type McpxToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;
|
|
50
|
-
/**
|
|
51
|
-
|
|
53
|
+
/**
|
|
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.
|
|
57
|
+
*/
|
|
58
|
+
export interface McpxExposedEntity extends McpxEntityCapabilities {
|
|
52
59
|
slug: string;
|
|
53
|
-
read: boolean;
|
|
54
|
-
write: McpxWriteMode;
|
|
55
60
|
hasDrafts: boolean;
|
|
56
61
|
/**
|
|
57
62
|
* The entity has Payload `versions`, with or without drafts, and is
|
|
58
63
|
* readable. Says nothing about drafts; see `hasDrafts`.
|
|
59
64
|
*/
|
|
60
65
|
hasVersions: boolean;
|
|
61
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Writes go live immediately, as the entity keeps no drafts.
|
|
68
|
+
*/
|
|
69
|
+
liveWrite: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* An upload document is a file, which only an upload grant can supply.
|
|
72
|
+
*/
|
|
62
73
|
isUpload: boolean;
|
|
63
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Name of the capability group on the key document.
|
|
76
|
+
*/
|
|
64
77
|
fieldName: string;
|
|
65
78
|
}
|
|
66
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* The slugs a key may read, write, publish and delete.
|
|
81
|
+
*/
|
|
67
82
|
export interface McpxScopeSlugs {
|
|
68
83
|
readable: string[];
|
|
69
84
|
writable: string[];
|
|
70
85
|
publishable: string[];
|
|
86
|
+
deletable: string[];
|
|
71
87
|
}
|
|
72
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* What a tool knows about the current request.
|
|
90
|
+
*/
|
|
73
91
|
export interface McpxToolScope {
|
|
74
92
|
req: PayloadRequest;
|
|
75
93
|
capabilities: McpxResolvedCapabilities;
|
|
76
94
|
collections: McpxScopeSlugs;
|
|
77
95
|
globals: McpxScopeSlugs;
|
|
78
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* `null` when localization is off.
|
|
98
|
+
*/
|
|
79
99
|
localization: null | {
|
|
80
100
|
locales: string[];
|
|
81
101
|
defaultLocale: string;
|
|
@@ -84,20 +104,34 @@ export interface McpxToolScope {
|
|
|
84
104
|
maxLimit: number;
|
|
85
105
|
maxDepth: number;
|
|
86
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* Whether files can be uploaded at all: the KV is database-backed and no
|
|
109
|
+
* custom `auth.resolve` is configured. Confirmations need the same.
|
|
110
|
+
*/
|
|
111
|
+
uploads: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* `null` when `diagnostics` is off.
|
|
114
|
+
*/
|
|
115
|
+
diagnostics: null | {
|
|
116
|
+
name: string;
|
|
117
|
+
version: string;
|
|
118
|
+
};
|
|
87
119
|
exposure: {
|
|
88
120
|
collections: McpxExposedEntity[];
|
|
89
121
|
globals: McpxExposedEntity[];
|
|
90
122
|
};
|
|
91
123
|
}
|
|
92
124
|
/**
|
|
93
|
-
*
|
|
94
|
-
* `req.context.mcpx` set. `Args` only needs stating when `inputSchema` is built
|
|
95
|
-
* per request, leaving no static shape to infer from.
|
|
125
|
+
* What every tool declares, apart from `confirm` and the handler.
|
|
96
126
|
*/
|
|
97
|
-
|
|
98
|
-
/**
|
|
127
|
+
interface McpxToolBase<Shape extends z.ZodRawShape> {
|
|
128
|
+
/**
|
|
129
|
+
* camelCase, unique, not one of the builtin tool names.
|
|
130
|
+
*/
|
|
99
131
|
name: string;
|
|
100
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Built per request so it can state what this key's writes do.
|
|
134
|
+
*/
|
|
101
135
|
description: string | ((scope: McpxToolScope) => string);
|
|
102
136
|
annotations?: ToolAnnotations;
|
|
103
137
|
/**
|
|
@@ -112,21 +146,117 @@ export interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.
|
|
|
112
146
|
* instead of stripped.
|
|
113
147
|
*/
|
|
114
148
|
inputSchema?: Shape | ((scope: McpxToolScope) => z.ZodRawShape);
|
|
115
|
-
handler(ctx: {
|
|
116
|
-
args: Args;
|
|
117
|
-
scope: McpxToolScope;
|
|
118
|
-
/** Shorthand for `scope.req`. */
|
|
119
|
-
req: PayloadRequest;
|
|
120
|
-
extra: McpxToolExtra;
|
|
121
|
-
}): CallToolResult | Promise<CallToolResult>;
|
|
122
149
|
}
|
|
123
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* What a handler receives.
|
|
152
|
+
*/
|
|
153
|
+
export interface McpxToolContext<Args> {
|
|
154
|
+
args: Args;
|
|
155
|
+
scope: McpxToolScope;
|
|
156
|
+
/**
|
|
157
|
+
* Shorthand for `scope.req`.
|
|
158
|
+
*/
|
|
159
|
+
req: PayloadRequest;
|
|
160
|
+
extra: McpxToolExtra;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Handed to the handler of a tool with `confirm`. Internal for now.
|
|
164
|
+
*/
|
|
165
|
+
export interface McpxConfirmation {
|
|
166
|
+
/**
|
|
167
|
+
* `true` when `runConfirmed` runs an approved call.
|
|
168
|
+
*/
|
|
169
|
+
confirmed: boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Stores the call with `args` for the key's user to approve and returns
|
|
172
|
+
* the result that hands its id and URL to the client. Throws in a
|
|
173
|
+
* confirmed run.
|
|
174
|
+
*/
|
|
175
|
+
request: (args: Record<string, unknown>) => Promise<CallToolResult>;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* A tool as `defineMcpxTool` takes it. `Confirm` is inferred from `confirm`,
|
|
179
|
+
* so only a tool that sets it gets `confirmation` in its handler.
|
|
180
|
+
*/
|
|
181
|
+
export type McpxToolDefinition<Shape extends z.ZodRawShape, Args, Confirm extends McpxToolConfirm | undefined> = McpxToolBase<Shape> & {
|
|
182
|
+
/**
|
|
183
|
+
* Internal: only builtin tools are confirmable for now. The handler gets
|
|
184
|
+
* `confirmation`: a call outside a confirmed run stores itself through
|
|
185
|
+
* `confirmation.request`, the key's user approves it in the admin panel,
|
|
186
|
+
* and `runConfirmed` runs the handler again with `confirmation.confirmed`
|
|
187
|
+
* set.
|
|
188
|
+
*/
|
|
189
|
+
confirm?: Confirm;
|
|
190
|
+
handler(ctx: McpxToolContext<Args> & (Confirm extends McpxToolConfirm ? {
|
|
191
|
+
confirmation: McpxConfirmation;
|
|
192
|
+
} : unknown)): CallToolResult | Promise<CallToolResult>;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* A tool, builtin or custom. Runs with `req.user` resolved from the key and
|
|
196
|
+
* `req.context.mcpx` set. `Args` only needs stating when `inputSchema` is built
|
|
197
|
+
* per request, leaving no static shape to infer from.
|
|
198
|
+
*/
|
|
199
|
+
export type McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.infer<z.ZodObject<Shape>>> = McpxToolDefinition<Shape, Args, undefined> | (McpxToolDefinition<Shape, Args, McpxToolConfirm> & {
|
|
200
|
+
confirm: McpxToolConfirm;
|
|
201
|
+
});
|
|
202
|
+
/**
|
|
203
|
+
* How the admin panel shows one stored call of a confirmable tool.
|
|
204
|
+
*/
|
|
205
|
+
export interface McpxConfirmationSummary {
|
|
206
|
+
/**
|
|
207
|
+
* What the call acts on, such as a collection's label.
|
|
208
|
+
*/
|
|
209
|
+
label: string;
|
|
210
|
+
/**
|
|
211
|
+
* `undefined` when the target cannot be read.
|
|
212
|
+
*/
|
|
213
|
+
title?: string;
|
|
214
|
+
/**
|
|
215
|
+
* The target's `_status`, where it has drafts.
|
|
216
|
+
*/
|
|
217
|
+
status?: string;
|
|
218
|
+
id?: string;
|
|
219
|
+
/**
|
|
220
|
+
* Admin path that opens the target.
|
|
221
|
+
*/
|
|
222
|
+
href?: string;
|
|
223
|
+
/**
|
|
224
|
+
* The call cannot be undone.
|
|
225
|
+
*/
|
|
226
|
+
permanent: boolean;
|
|
227
|
+
/**
|
|
228
|
+
* Written by the client, so shown as such.
|
|
229
|
+
*/
|
|
230
|
+
reason?: string;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Internal for now; see {@link McpxTool.confirm}.
|
|
234
|
+
*/
|
|
235
|
+
export interface McpxToolConfirm {
|
|
236
|
+
/**
|
|
237
|
+
* Heading of the tool's group in the admin panel.
|
|
238
|
+
*/
|
|
239
|
+
label: string;
|
|
240
|
+
/**
|
|
241
|
+
* Read at view time with the access of the key's user.
|
|
242
|
+
*/
|
|
243
|
+
summarize(call: {
|
|
244
|
+
args: Record<string, unknown>;
|
|
245
|
+
}, req: PayloadRequest): Promise<McpxConfirmationSummary>;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Argument type erased, so a registry can hold tools of differing shapes.
|
|
249
|
+
*/
|
|
124
250
|
export type McpxAnyTool = McpxTool<z.ZodRawShape, never>;
|
|
125
251
|
export interface McpxAuthResult {
|
|
126
|
-
/**
|
|
252
|
+
/**
|
|
253
|
+
* Must carry `collection`.
|
|
254
|
+
*/
|
|
127
255
|
user: TypedUser;
|
|
128
256
|
apiKeyId: DocumentId;
|
|
129
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* The `capabilities` group as stored on the key document.
|
|
259
|
+
*/
|
|
130
260
|
capabilities: unknown;
|
|
131
261
|
}
|
|
132
262
|
/**
|
|
@@ -136,31 +266,48 @@ export interface McpxAuthResult {
|
|
|
136
266
|
* to `Record<string, unknown>`, which interfaces do not satisfy.
|
|
137
267
|
*/
|
|
138
268
|
export type McpxPluginOptions = {
|
|
139
|
-
/**
|
|
269
|
+
/**
|
|
270
|
+
* Allow-list of collections. `true` is shorthand for `{}`.
|
|
271
|
+
*/
|
|
140
272
|
collections: Partial<Record<CollectionSlug, McpxCollectionOptions | true>>;
|
|
141
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* Allow-list of globals. `true` is shorthand for `{}`.
|
|
275
|
+
*/
|
|
142
276
|
globals?: Partial<Record<GlobalSlug, McpxGlobalOptions | true>>;
|
|
143
|
-
/**
|
|
277
|
+
/**
|
|
278
|
+
* Collection the keys act as. Default `config.admin.user`, then `users`.
|
|
279
|
+
*/
|
|
144
280
|
userCollection?: CollectionSlug;
|
|
145
281
|
apiKeys?: {
|
|
146
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* Slug of the generated API key collection. Default `mcpx-api-keys`.
|
|
284
|
+
*/
|
|
147
285
|
slug?: string;
|
|
148
286
|
/**
|
|
149
|
-
* Add a "Connect a client"
|
|
150
|
-
* 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.
|
|
151
290
|
*/
|
|
152
291
|
setupGuide?: boolean;
|
|
153
|
-
/**
|
|
292
|
+
/**
|
|
293
|
+
* Final override applied to the generated collection.
|
|
294
|
+
*/
|
|
154
295
|
overrideCollection?: (collection: CollectionConfig) => CollectionConfig;
|
|
155
296
|
};
|
|
156
297
|
endpoint?: {
|
|
157
|
-
/**
|
|
298
|
+
/**
|
|
299
|
+
* Endpoint path below the API route. Default `/mcpx`.
|
|
300
|
+
*/
|
|
158
301
|
path?: string;
|
|
159
302
|
};
|
|
160
303
|
limits?: {
|
|
161
|
-
/**
|
|
304
|
+
/**
|
|
305
|
+
* Upper bound for `findDocuments.limit`. Default 25.
|
|
306
|
+
*/
|
|
162
307
|
maxLimit?: number;
|
|
163
|
-
/**
|
|
308
|
+
/**
|
|
309
|
+
* Upper bound for `depth` on reads. Default 1.
|
|
310
|
+
*/
|
|
164
311
|
maxDepth?: number;
|
|
165
312
|
};
|
|
166
313
|
tools?: McpxAnyTool[];
|
|
@@ -179,21 +326,42 @@ export type McpxPluginOptions = {
|
|
|
179
326
|
name?: string;
|
|
180
327
|
version?: string;
|
|
181
328
|
};
|
|
329
|
+
/**
|
|
330
|
+
* Expose the server version plus repository and issue links to clients. Default `true`.
|
|
331
|
+
*/
|
|
332
|
+
diagnostics?: boolean;
|
|
182
333
|
};
|
|
183
|
-
/**
|
|
184
|
-
|
|
334
|
+
/**
|
|
335
|
+
* One entity's operations, as the config exposes them or as a key may use
|
|
336
|
+
* them. Globals reuse this shape.
|
|
337
|
+
*/
|
|
338
|
+
export interface McpxEntityCapabilities {
|
|
185
339
|
read: boolean;
|
|
186
340
|
write: boolean;
|
|
187
|
-
/**
|
|
341
|
+
/**
|
|
342
|
+
* Only ever true where the config lets writes publish and drafts exist.
|
|
343
|
+
*/
|
|
188
344
|
publish: boolean;
|
|
345
|
+
/**
|
|
346
|
+
* Always `false` on a global.
|
|
347
|
+
*/
|
|
348
|
+
delete: boolean;
|
|
349
|
+
/**
|
|
350
|
+
* Moves to trash without approval. Only ever true where `delete` is.
|
|
351
|
+
*/
|
|
352
|
+
deleteUnattended: boolean;
|
|
189
353
|
}
|
|
190
|
-
/**
|
|
354
|
+
/**
|
|
355
|
+
* In force for one request: the plugin config and the key checkboxes together.
|
|
356
|
+
*/
|
|
191
357
|
export interface McpxResolvedCapabilities {
|
|
192
|
-
collections: Record<string,
|
|
193
|
-
globals: Record<string,
|
|
358
|
+
collections: Record<string, McpxEntityCapabilities>;
|
|
359
|
+
globals: Record<string, McpxEntityCapabilities>;
|
|
194
360
|
tools: Record<string, boolean>;
|
|
195
361
|
}
|
|
196
|
-
/**
|
|
362
|
+
/**
|
|
363
|
+
* Stamped on `req.context.mcpx`; see {@link isMcpxRequest}.
|
|
364
|
+
*/
|
|
197
365
|
export interface McpxRequestContext {
|
|
198
366
|
apiKeyId: DocumentId;
|
|
199
367
|
capabilities: McpxResolvedCapabilities;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { grantContext, issueGrant } from "../grants/grant.mjs";
|
|
2
|
+
import { publicOrigin } from "../request.mjs";
|
|
3
|
+
import { errorResult, jsonResult } from "../result.mjs";
|
|
4
|
+
import { SchemaError } from "../schema/errors.mjs";
|
|
5
|
+
import { resolveDataPointer } from "../schema/pointer.mjs";
|
|
6
|
+
import "../schema/index.mjs";
|
|
7
|
+
import { collectPublishBlockers } from "../write/publish-blockers.mjs";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { APIError } from "payload";
|
|
10
|
+
import { hasDraftValidationEnabled, hasDraftsEnabled, validateMimeType } from "payload/shared";
|
|
11
|
+
//#region src/upload/file.ts
|
|
12
|
+
const MAX_BYTES = 26214400;
|
|
13
|
+
const FILENAME = /^[^/\\\p{Cc}]+$/u;
|
|
14
|
+
const MIME_TYPE = /^[\w.+-]+\/[\w.+-]+$/;
|
|
15
|
+
const uploadedFiles = /* @__PURE__ */ new WeakMap();
|
|
16
|
+
const fileSchema = (maxBytes) => z.strictObject({
|
|
17
|
+
filename: z.string().max(255).regex(FILENAME),
|
|
18
|
+
mimeType: z.string().max(255).regex(MIME_TYPE),
|
|
19
|
+
size: z.number().int().min(1).max(maxBytes)
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* The largest file accepted: Payload's `upload.limits.fileSize` if set, and
|
|
23
|
+
* never more than 25 MB, since the body is buffered.
|
|
24
|
+
*/ const uploadMaxBytes = (config) => Math.min(config.upload.limits?.fileSize ?? MAX_BYTES, MAX_BYTES);
|
|
25
|
+
const allowedMimeTypes = (scope, slug) => {
|
|
26
|
+
if (!scope.uploads || !scope.collections.writable.includes(slug)) return [];
|
|
27
|
+
const entity = scope.exposure.collections.find((candidate) => candidate.slug === slug);
|
|
28
|
+
const config = scope.req.payload.collections[slug]?.config;
|
|
29
|
+
return entity?.isUpload === true && Array.isArray(config?.upload.mimeTypes) ? config.upload.mimeTypes : [];
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Whether this key may send a file to `slug`: a writable upload collection
|
|
33
|
+
* that sets `upload.mimeTypes`.
|
|
34
|
+
*/ const acceptsFiles = (scope, slug) => allowedMimeTypes(scope, slug).length > 0;
|
|
35
|
+
/**
|
|
36
|
+
* The collections this key may send files to.
|
|
37
|
+
*/ const fileSlugs = (scope) => scope.collections.writable.filter((slug) => acceptsFiles(scope, slug));
|
|
38
|
+
/**
|
|
39
|
+
* The `file` argument, present only when the key may send a file somewhere.
|
|
40
|
+
*/ const fileShape = (scope, description) => {
|
|
41
|
+
const slugs = fileSlugs(scope);
|
|
42
|
+
return slugs.length === 0 ? {} : { file: fileSchema(uploadMaxBytes(scope.req.payload.config)).optional().describe(description(slugs.join(", "))) };
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Throws for a `file` sent to an entity that does not accept one, or declared
|
|
46
|
+
* with a type outside its `upload.mimeTypes`. Payload stores the declared
|
|
47
|
+
* type, so it is held to the same list as the content.
|
|
48
|
+
*/ const assertAcceptsFile = (scope, target, file) => {
|
|
49
|
+
const mimeTypes = target.kind === "global" ? [] : allowedMimeTypes(scope, target.slug);
|
|
50
|
+
if (mimeTypes.length === 0) throw new APIError(`"${target.slug}" does not accept files through MCP: it is not an upload collection, or it sets no upload.mimeTypes.`, 400);
|
|
51
|
+
if (!validateMimeType(file.mimeType, mimeTypes)) throw new APIError(`"${target.slug}" accepts only ${mimeTypes.join(", ")}, not "${file.mimeType}".`, 400);
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Attaches the claimed bytes to the request the tool runs on.
|
|
55
|
+
*/ const setUploadedFile = (req, file) => {
|
|
56
|
+
uploadedFiles.set(req, file);
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The bytes for this call, or `undefined` before they were sent.
|
|
60
|
+
*/ const uploadedFile = (req) => uploadedFiles.get(req);
|
|
61
|
+
const sharesPublishedFile = async (req, entity, id) => {
|
|
62
|
+
if (!hasDraftsEnabled(entity.config)) return false;
|
|
63
|
+
const read = {
|
|
64
|
+
collection: entity.slug,
|
|
65
|
+
id,
|
|
66
|
+
depth: 0,
|
|
67
|
+
overrideAccess: true,
|
|
68
|
+
disableErrors: true,
|
|
69
|
+
select: {
|
|
70
|
+
filename: true,
|
|
71
|
+
_status: true
|
|
72
|
+
},
|
|
73
|
+
req
|
|
74
|
+
};
|
|
75
|
+
const [latest, published] = await Promise.all([req.payload.findByID({
|
|
76
|
+
...read,
|
|
77
|
+
draft: true
|
|
78
|
+
}), req.payload.findByID({
|
|
79
|
+
...read,
|
|
80
|
+
draft: false
|
|
81
|
+
})]);
|
|
82
|
+
return latest?.["_status"] === "draft" && published?.["_status"] === "published" && latest["filename"] === published["filename"];
|
|
83
|
+
};
|
|
84
|
+
const isClientWritable = (scope, target, path) => {
|
|
85
|
+
try {
|
|
86
|
+
const resolution = resolveDataPointer(scope.req.payload.config, {
|
|
87
|
+
ref: target.entity,
|
|
88
|
+
doc: target.data,
|
|
89
|
+
pointer: path
|
|
90
|
+
});
|
|
91
|
+
return resolution.readOnly !== true && resolution.descriptor?.readOnly !== true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error instanceof SchemaError) return false;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Refuses a file write Payload would turn into data loss: a replace that
|
|
99
|
+
* deletes the published file, or a write that fails validation after the old
|
|
100
|
+
* file is gone. Payload validates only after it moves files, so the merged
|
|
101
|
+
* data is validated here first wherever Payload will validate it, keeping only
|
|
102
|
+
* failures the client can fix. Once the bytes are attached, Payload validates
|
|
103
|
+
* with every hook and the file.
|
|
104
|
+
*/ const fileWriteRefusal = async (scope, target) => {
|
|
105
|
+
const { entity, id } = target;
|
|
106
|
+
if (id !== void 0 && await sharesPublishedFile(scope.req, entity, id)) return errorResult("The file was not replaced: the latest draft still uses the published file, which the replace would delete. Publish or discard the draft first.");
|
|
107
|
+
if (uploadedFile(scope.req) !== void 0 || hasDraftsEnabled(entity.config) && !hasDraftValidationEnabled(entity.config)) return;
|
|
108
|
+
const { blockers: found } = await collectPublishBlockers(scope.req, {
|
|
109
|
+
doc: {
|
|
110
|
+
...target.data,
|
|
111
|
+
...id === void 0 ? {} : { id }
|
|
112
|
+
},
|
|
113
|
+
entity
|
|
114
|
+
});
|
|
115
|
+
const blockers = found.filter((blocker) => isClientWritable(scope, target, blocker.path));
|
|
116
|
+
return blockers.length === 0 ? void 0 : errorResult("Nothing was written: the document fails validation.", { validationErrors: blockers });
|
|
117
|
+
};
|
|
118
|
+
const handoffUrl = (req, path) => {
|
|
119
|
+
const url = new URL(publicOrigin(req));
|
|
120
|
+
url.pathname = `${new URL(req.url ?? "").pathname.replace(/\/+$/, "")}/${path}`;
|
|
121
|
+
return url.toString();
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Stores the call as a grant and returns where to PUT its file. Nothing else
|
|
125
|
+
* is written: the PUT runs the same call again with the bytes attached.
|
|
126
|
+
*/ const uploadHandoff = async (scope, call) => {
|
|
127
|
+
const { req } = scope;
|
|
128
|
+
const { payload } = req;
|
|
129
|
+
const { slug, apiKeyId } = grantContext(req);
|
|
130
|
+
const url = handoffUrl(req, "upload");
|
|
131
|
+
const { id, exp } = await issueGrant(payload, slug, {
|
|
132
|
+
kind: "upload",
|
|
133
|
+
apiKeyId,
|
|
134
|
+
tool: call.tool,
|
|
135
|
+
args: call.args,
|
|
136
|
+
file: call.args.file,
|
|
137
|
+
locale: req.locale ?? null,
|
|
138
|
+
fallbackLocale: req.fallbackLocale ?? null
|
|
139
|
+
});
|
|
140
|
+
return jsonResult({ upload: {
|
|
141
|
+
url,
|
|
142
|
+
method: "PUT",
|
|
143
|
+
headers: {
|
|
144
|
+
"content-type": call.args.file.mimeType,
|
|
145
|
+
"x-mcpx-grant": id
|
|
146
|
+
},
|
|
147
|
+
expiresAt: new Date(exp).toISOString(),
|
|
148
|
+
maxBytes: uploadMaxBytes(payload.config)
|
|
149
|
+
} });
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* The upload collections whose files this key may download. A collection
|
|
153
|
+
* with its own top-level `download` field is left out, since the handoff
|
|
154
|
+
* would overwrite it in the result.
|
|
155
|
+
*/ const downloadSlugs = (scope) => scope.uploads ? scope.collections.readable.filter((slug) => scope.exposure.collections.find((candidate) => candidate.slug === slug)?.isUpload === true && !scope.req.payload.collections[slug]?.config.flattenedFields.some((field) => field.name === "download")) : [];
|
|
156
|
+
/**
|
|
157
|
+
* Stores a grant for the file of a document just read and returns where to
|
|
158
|
+
* GET it.
|
|
159
|
+
*/ const downloadHandoff = async (scope, target) => {
|
|
160
|
+
const { req } = scope;
|
|
161
|
+
const { slug, apiKeyId } = grantContext(req);
|
|
162
|
+
const url = handoffUrl(req, "file");
|
|
163
|
+
const { id, exp } = await issueGrant(req.payload, slug, {
|
|
164
|
+
kind: "download",
|
|
165
|
+
apiKeyId,
|
|
166
|
+
...target,
|
|
167
|
+
fallbackLocale: req.fallbackLocale ?? null
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
url,
|
|
171
|
+
method: "GET",
|
|
172
|
+
headers: { "x-mcpx-grant": id },
|
|
173
|
+
expiresAt: new Date(exp).toISOString()
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
//#endregion
|
|
177
|
+
export { acceptsFiles, assertAcceptsFile, downloadHandoff, downloadSlugs, fileShape, fileSlugs, fileWriteRefusal, setUploadedFile, uploadHandoff, uploadMaxBytes, uploadedFile };
|
package/dist/version.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//#region src/version.ts
|
|
2
2
|
/**
|
|
3
3
|
* Package version injected at build time; sources under Vitest report "dev".
|
|
4
|
-
*/ const MCPX_VERSION = "
|
|
4
|
+
*/ const MCPX_VERSION = "2.0.0";
|
|
5
|
+
const MCPX_REPOSITORY_URL = "https://github.com/abinnovision/payloadcms-commons/tree/main/packages/mcpx";
|
|
6
|
+
const MCPX_ISSUES_URL = "https://github.com/abinnovision/payloadcms-commons/issues";
|
|
5
7
|
//#endregion
|
|
6
|
-
export { MCPX_VERSION };
|
|
8
|
+
export { MCPX_ISSUES_URL, MCPX_REPOSITORY_URL, MCPX_VERSION };
|