@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.
Files changed (71) hide show
  1. package/README.md +140 -448
  2. package/dist/admin/capability-matrix.d.mts +33 -0
  3. package/dist/admin/capability-matrix.mjs +354 -0
  4. package/dist/admin/capability-toggles.mjs +73 -0
  5. package/dist/{client → admin}/setup-guide.d.mts +4 -2
  6. package/dist/{client → admin}/setup-guide.mjs +3 -10
  7. package/dist/api-keys/capability-matrix.d.mts +12 -8
  8. package/dist/api-keys/capability-matrix.mjs +17 -85
  9. package/dist/api-keys/collection.mjs +2 -1
  10. package/dist/api-keys/fields.mjs +36 -24
  11. package/dist/auth/resolve.mjs +44 -6
  12. package/dist/{tools/names.mjs → builtin-tool-names.mjs} +2 -1
  13. package/dist/capabilities.mjs +41 -43
  14. package/dist/define-tool.d.mts +23 -0
  15. package/dist/{types.mjs → define-tool.mjs} +1 -1
  16. package/dist/endpoint/errors.mjs +5 -6
  17. package/dist/endpoint/handler.mjs +49 -19
  18. package/dist/endpoint/server.mjs +72 -20
  19. package/dist/entity.d.mts +9 -0
  20. package/dist/guards.mjs +7 -0
  21. package/dist/i18n.mjs +10 -8
  22. package/dist/index.d.mts +5 -2
  23. package/dist/index.mjs +3 -2
  24. package/dist/options.mjs +52 -44
  25. package/dist/plugin.mjs +0 -1
  26. package/dist/request.d.mts +2 -3
  27. package/dist/request.mjs +2 -3
  28. package/dist/result.d.mts +2 -3
  29. package/dist/result.mjs +3 -3
  30. package/dist/schema/admin-hidden.mjs +91 -0
  31. package/dist/schema/describe.mjs +17 -33
  32. package/dist/schema/errors.mjs +8 -0
  33. package/dist/schema/index.mjs +5 -2
  34. package/dist/schema/lexical-pointer.mjs +18 -20
  35. package/dist/schema/lexical.mjs +44 -93
  36. package/dist/schema/outline.mjs +5 -15
  37. package/dist/schema/path.mjs +28 -0
  38. package/dist/schema/pointer.mjs +18 -25
  39. package/dist/schema/shape.mjs +25 -71
  40. package/dist/schema/walk.mjs +43 -45
  41. package/dist/tools/builtin.mjs +6 -4
  42. package/dist/tools/create-document.mjs +26 -23
  43. package/dist/tools/describe-schema.mjs +38 -34
  44. package/dist/tools/document.mjs +124 -0
  45. package/dist/tools/find-documents.mjs +32 -19
  46. package/dist/tools/find-versions.mjs +74 -0
  47. package/dist/tools/get-document.mjs +80 -49
  48. package/dist/tools/list-capabilities.mjs +25 -16
  49. package/dist/tools/patch-document.mjs +75 -49
  50. package/dist/tools/publish-document.mjs +26 -32
  51. package/dist/tools/query-paths.mjs +57 -0
  52. package/dist/tools/read-request.d.mts +10 -0
  53. package/dist/tools/read-request.mjs +43 -0
  54. package/dist/tools/shared.mjs +74 -96
  55. package/dist/tools/validate-document.mjs +18 -30
  56. package/dist/tools/versions.mjs +136 -0
  57. package/dist/types.d.mts +119 -73
  58. package/dist/write/draft-guard.mjs +23 -41
  59. package/dist/write/patch.mjs +27 -183
  60. package/dist/write/publish-blockers.mjs +7 -14
  61. package/dist/write/publish-intent.mjs +6 -4
  62. package/dist/write/row-ids.mjs +46 -0
  63. package/dist/write/transaction.mjs +5 -8
  64. package/dist/write/write-data.mjs +64 -0
  65. package/package.json +6 -5
  66. package/dist/client/capability-matrix.d.mts +0 -26
  67. package/dist/client/capability-matrix.mjs +0 -349
  68. package/dist/endpoint/index.mjs +0 -4
  69. package/dist/tools/target.mjs +0 -45
  70. /package/dist/{client → admin}/index.d.mts +0 -0
  71. /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 means
21
- * `publishDocument` is exposed; on one without, where there is no draft to
22
- * land on, it means the write itself is permitted and lands live.
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
- /** A key can only enable what the config exposes here. */
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
- /** Expose `describeSchema`, `findDocuments`, `getDocument`. Default `true`. */
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`, and how far those writes reach. Default
32
- * `false`.
38
+ * collection, `createDocument`. Without drafts a write changes live
39
+ * content. Default `true`.
33
40
  */
34
- write?: McpxWriteMode;
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 `patchDocument` and `validateDocument`, and how far those writes
42
- * reach. Default `false`.
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
- write?: McpxWriteMode;
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
- /** What the config exposes, before an API key's checkboxes narrow it. */
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
- /** An upload document is a file, and no tool here can supply one. */
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
- /** Name of the capability group on the key document. */
72
+ /**
73
+ * Name of the capability group on the key document.
74
+ */
56
75
  fieldName: string;
57
76
  }
58
- /** What a tool knows about the current request. */
59
- export interface McpxToolScope {
60
- req: PayloadRequest;
61
- capabilities: McpxResolvedCapabilities;
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
- readableGlobals: string[];
66
- writableGlobals: string[];
67
- publishableGlobals: string[];
68
- /** `null` when localization is off. */
69
- locales: null | string[];
70
- defaultLocale: null | string;
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
- /** camelCase, unique, not one of the builtin tool names. */
115
+ /**
116
+ * camelCase, unique, not one of the builtin tool names.
117
+ */
87
118
  name: string;
88
- /** Built per request so it can state what this key's writes actually do. */
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
- * rather than stripped.
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
- /** Shorthand for `scope.req`. */
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
- * Per-request shape built from helpers that erase to `z.ZodRawShape`, as the
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 declare function defineMcpxTool<Args>(tool: McpxTool<z.ZodRawShape, Args> & {
126
- inputSchema: (scope: McpxToolScope) => z.ZodRawShape;
127
- }): McpxAnyTool;
149
+ export type McpxAnyTool = McpxTool<z.ZodRawShape, never>;
128
150
  export interface McpxAuthResult {
129
- /** Must carry `collection`. */
151
+ /**
152
+ * Must carry `collection`.
153
+ */
130
154
  user: TypedUser;
131
- apiKeyId: number | string;
132
- /** The `capabilities` group as stored on the key document. */
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
- /** Allow-list of collections. `true` is shorthand for `{ read: true }`. */
168
+ /**
169
+ * Allow-list of collections. `true` is shorthand for `{}`.
170
+ */
143
171
  collections: Partial<Record<CollectionSlug, McpxCollectionOptions | true>>;
144
- /** Allow-list of globals. `true` is shorthand for `{ read: true }`. */
172
+ /**
173
+ * Allow-list of globals. `true` is shorthand for `{}`.
174
+ */
145
175
  globals?: Partial<Record<GlobalSlug, McpxGlobalOptions | true>>;
146
- /** Collection the keys act as. Default `config.admin.user`, then `users`. */
176
+ /**
177
+ * Collection the keys act as. Default `config.admin.user`, then `users`.
178
+ */
147
179
  userCollection?: CollectionSlug;
148
180
  apiKeys?: {
149
- /** Slug of the generated API key collection. Default `mcpx-api-keys`. */
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
- /** Final override applied to the generated collection. */
190
+ /**
191
+ * Final override applied to the generated collection.
192
+ */
157
193
  overrideCollection?: (collection: CollectionConfig) => CollectionConfig;
158
194
  };
159
195
  endpoint?: {
160
- /** Endpoint path below the API route. Default `/mcpx`. */
196
+ /**
197
+ * Endpoint path below the API route. Default `/mcpx`.
198
+ */
161
199
  path?: string;
162
200
  };
163
201
  limits?: {
164
- /** Upper bound for `findDocuments.limit`. Default 25. */
202
+ /**
203
+ * Upper bound for `findDocuments.limit`. Default 25.
204
+ */
165
205
  maxLimit?: number;
166
- /** Upper bound for `depth` on reads. Default 1. */
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
- /** Replace or wrap the default key resolution. Return `null` for 401. */
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
- /** What a key may do with one entity. Globals reuse this shape. */
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
- /** Only ever true where the config sets `write: "live"` and drafts exist. */
234
+ /**
235
+ * Only ever true where the config lets writes publish and drafts exist.
236
+ */
187
237
  publish: boolean;
188
238
  }
189
- /** In force for one request: plugin config AND key checkboxes. */
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
- /** Stamped on `req.context.mcpx`; see {@link isMcpxRequest}. */
247
+ /**
248
+ * Stamped on `req.context.mcpx`; see {@link isMcpxRequest}.
249
+ */
196
250
  export interface McpxRequestContext {
197
- apiKeyId: number | string;
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
- /** Cleared on every MCP write, publishes included, so none can be smuggled in. */ const STRIPPED_ARGS = /* @__PURE__ */ new Set([
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
- * Forces every MCP write into a draft save, unless it is the one write
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}, with one difference that
51
- * decides where the guarantee lives: `updateGlobal` destructures `draft` and
52
- * the publish arguments *before* it runs `beforeOperation` and re-reads only
53
- * `data` afterwards, so setting them here is a no-op. What lands is `data` with
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
- return scrubWriteArgs(args, hasPublishIntent(args["data"]));
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
- * Installs {@link refuseUnlessExpected} on every collection write. Returns
79
- * `data` unchanged when the write is allowed; the hook exists for its throw.
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 is a no-op outside MCP requests) and `refusePublish` wherever drafts
92
- * exist. Applied to the built collection list so nothing can join later
93
- * without being covered. Both are appended last, so a user hook cannot win.
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, for the same reason
104
- * `installDraftGuards` covers every collection: a custom tool running on an MCP
105
- * request must not be able to publish through a global the plugin config never
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: {