@abinnovision/payloadcms-mcpx 1.0.0-beta.10 → 1.0.0-beta.12

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 (38) hide show
  1. package/README.md +72 -7
  2. package/dist/api-keys/fields.mjs +1 -1
  3. package/dist/endpoint/{result.mjs → errors.mjs} +4 -21
  4. package/dist/endpoint/handler.mjs +8 -4
  5. package/dist/endpoint/index.mjs +4 -0
  6. package/dist/endpoint/server.mjs +31 -25
  7. package/dist/index.d.mts +4 -4
  8. package/dist/index.mjs +4 -3
  9. package/dist/plugin.mjs +1 -0
  10. package/dist/{write/draft-guard.d.mts → request.d.mts} +2 -2
  11. package/dist/request.mjs +8 -0
  12. package/dist/result.d.mts +13 -0
  13. package/dist/result.mjs +22 -0
  14. package/dist/schema/index.mjs +6 -0
  15. package/dist/schema/pointer.mjs +1 -1
  16. package/dist/schema/walk.mjs +11 -15
  17. package/dist/tools/{index.mjs → builtin.mjs} +7 -4
  18. package/dist/tools/create-document.mjs +18 -12
  19. package/dist/tools/describe-schema.mjs +7 -6
  20. package/dist/tools/find-documents.mjs +6 -6
  21. package/dist/tools/get-document.mjs +6 -5
  22. package/dist/tools/list-capabilities.mjs +8 -8
  23. package/dist/tools/patch-document.mjs +18 -17
  24. package/dist/tools/shared.mjs +31 -11
  25. package/dist/tools/validate-document.mjs +12 -12
  26. package/dist/types.d.mts +109 -11
  27. package/dist/types.mjs +3 -4
  28. package/dist/write/draft-guard.mjs +2 -6
  29. package/dist/write/patch.mjs +131 -57
  30. package/dist/write/publish-blockers.mjs +10 -3
  31. package/package.json +1 -1
  32. package/dist/i18n.d.mts +0 -1
  33. package/dist/options.d.mts +0 -2
  34. package/dist/schema/lexical.d.mts +0 -1
  35. package/dist/schema/walk.d.mts +0 -3
  36. package/dist/tools/target.d.mts +0 -3
  37. package/dist/tools/types.d.mts +0 -5
  38. package/dist/write/publish-blockers.d.mts +0 -15
package/README.md CHANGED
@@ -13,8 +13,9 @@ resolved server-side against the real config and the real document, so unknown
13
13
  fields, misplaced blocks and unusable rich text nodes or node fields are refused with the
14
14
  valid alternatives listed, never silently dropped.
15
15
 
16
- Writes are RFC 6902 patches that always land as drafts; publishing stays a
17
- human action in the admin panel. Every write returns the publish
16
+ Writes are RFC 6902 patches that land as drafts, with `allowLiveWrites` as the
17
+ one documented exception; publishing stays a human action in the admin panel.
18
+ Every write returns the publish
18
19
  blockers: the validation failures that still prevent a human from publishing
19
20
  the draft. Capabilities are declared twice: the plugin config decides what
20
21
  can exist, a checkbox on each API key decides what does, and a missing checkbox
@@ -153,7 +154,7 @@ only the slugs the key may touch.
153
154
  | `getDocument` | Read one document or a subtree of it. | `collection` + `id` \| `global`, `path?` (JSON pointer), `depth?`, `locale?`, `draft?` |
154
155
  | `patchDocument` | Apply RFC 6902 operations to the current draft. | `collection` + `id` \| `global`, `locale`, `patches`, `expectedUpdatedAt?` |
155
156
  | `createDocument` | Create a draft from a minimal seed. | `collection`, `locale`, `data` |
156
- | `validateDocument` | Publish blockers without writing. | `collection` + `id` \| `global`, `locale` |
157
+ | `validateDocument` | Publish blockers without saving anything. | `collection` + `id` \| `global`, `locale` |
157
158
 
158
159
  Rules the tools enforce and explain in their own descriptions:
159
160
 
@@ -266,6 +267,12 @@ Collections with `versions.drafts.validate: true` refuse invalid drafts
266
267
  outright; those failures come back as `validationErrors`. Both carry pointers,
267
268
  restated from the dotted paths Payload reports internally.
268
269
 
270
+ `publishBlockersUnavailable` marks a check that could not complete, which is
271
+ not the same answer as a document with nothing wrong with it. `validateDocument`
272
+ runs the same traversal without saving anything, so it is not free of side
273
+ effects: field `beforeValidate` and `beforeChange` hooks run, and it carries no
274
+ `readOnlyHint` for that reason.
275
+
269
276
  Writes also report `notApplied`: pointers whose value Payload kept unchanged,
270
277
  which happens when field-level access denies the update.
271
278
 
@@ -297,9 +304,62 @@ const publishQueue = defineMcpxTool({
297
304
 
298
305
  Each custom tool gets its own checkbox on every API key, default off.
299
306
 
300
- Custom tool shapes are registered as given, and the MCP SDK wraps them in a
301
- non-strict object: unknown arguments are stripped before your handler runs.
302
- Builtin tools reject them instead.
307
+ Custom tools take the same route as the builtins: one `McpxTool` shape, one
308
+ registration loop. Anything a builtin does, a custom tool can do.
309
+
310
+ `handler` receives `scope` alongside `args`, `req` and `extra`. The scope
311
+ carries what the key may touch (`readable`, `writable`, `readableGlobals`,
312
+ `writableGlobals`), the configured locales, the limits in force and the
313
+ exposed collections and globals. `req` is shorthand for `scope.req`.
314
+
315
+ `inputSchema` may be a function of that scope instead of a fixed shape, which
316
+ is how a tool narrows an enum to what the key may read:
317
+
318
+ ```ts
319
+ import { defineMcpxTool } from "@abinnovision/payloadcms-mcpx";
320
+ import { z } from "zod";
321
+
322
+ const whichCollection = defineMcpxTool({
323
+ name: "whichCollection",
324
+ description: "Echoes back one of the collections this key may read.",
325
+ isEnabled: (scope) =>
326
+ scope.capabilities.tools["whichCollection"] === true &&
327
+ scope.readable.length > 0,
328
+ inputSchema: (scope) => ({
329
+ collection: z.enum(scope.readable as [string, ...string[]]),
330
+ }),
331
+ handler: ({ args }) => ({
332
+ content: [{ type: "text", text: args.collection }],
333
+ }),
334
+ });
335
+ ```
336
+
337
+ `defineMcpxTool` defines every tool, builtin ones included, and infers the
338
+ handler's arguments from the input schema either way: from a fixed shape, or
339
+ from the object literal a per-request shape returns. Above, `args` is
340
+ `{ collection: string }` without being told.
341
+
342
+ Inference reaches as far as the shape's static type. A helper returning
343
+ `z.ZodRawShape` erases that type and leaves `args` as
344
+ `Record<string, unknown>`, so the builtins' shape helpers declare the superset
345
+ they produce instead: which keys a helper emits depends on the key's scope,
346
+ and the declared type states what a handler must cope with across every scope.
347
+ Their arguments stay derived from their schema that way, and cannot drift from
348
+ it. If your own helpers erase, state the arguments as a type argument:
349
+ `defineMcpxTool<Args>({ ... })`.
350
+
351
+ `isEnabled` decides whether the tool is registered for this key at all: a tool
352
+ that is not enabled never appears in `tools/list`. It defaults to the tool's
353
+ own checkbox, which is what the builtins replace to derive their availability
354
+ from the key's collection and global capabilities. Defining it **replaces**
355
+ the checkbox check, so restate `scope.capabilities.tools[name]` when you still
356
+ want it, as above.
357
+
358
+ Every input schema is registered strictly, custom tools included: an unknown
359
+ argument is rejected by name rather than stripped before the handler runs.
360
+
361
+ `jsonResult` and `errorResult` are exported so a custom tool can return
362
+ results shaped like a builtin's.
303
363
 
304
364
  ## Options
305
365
 
@@ -320,10 +380,15 @@ Builtin tools reject them instead.
320
380
  | `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
321
381
  | `limits.maxLimit` | `25` | Upper bound for `findDocuments.limit`. |
322
382
  | `limits.maxDepth` | `1` | Upper bound for `depth` on reads. |
323
- | `tools` | `[]` | Custom tools. |
383
+ | `tools` | `[]` | Custom tools, defined the same way as the builtins. |
324
384
  | `auth.resolve` | none | Replace or wrap the default key resolution. |
325
385
  | `serverInfo` | package name and version | Reported to MCP clients. |
326
386
 
387
+ `allowLiveWrites` is the only way an MCP write reaches live content. Where it is
388
+ set, the server instructions and the `patchDocument` and `createDocument`
389
+ descriptions name those slugs for the key in question, so a client is never told
390
+ its writes are drafts while they are not.
391
+
327
392
  Misconfiguration (unknown slugs, write on a collection without drafts, upload
328
393
  collections exposed for write, tool name collisions) fails at startup with
329
394
  `InvalidConfiguration`. Auth collections cannot be exposed at all, read
@@ -107,7 +107,7 @@ const SETUP_GUIDE_FIELD = "setupGuide";
107
107
  label: global.slug,
108
108
  fields: [...global.read ? [checkbox("read", "Describe and read this global.")] : [], ...global.write ? [checkbox("write", "Patch and validate this global's draft.")] : []]
109
109
  }));
110
- const toolCheckboxes = options.tools.map((tool) => checkbox(tool.name, tool.description));
110
+ const toolCheckboxes = options.tools.map((tool) => checkbox(tool.name, typeof tool.description === "string" ? tool.description : tool.name));
111
111
  const groups = [
112
112
  ...collectionGroups.length > 0 ? [{
113
113
  name: "collections",
@@ -1,6 +1,8 @@
1
+ import { errorResult } from "../result.mjs";
1
2
  import { pointerFromPayloadPath } from "../schema/walk.mjs";
3
+ import "../schema/index.mjs";
2
4
  import { APIError, ValidationError } from "payload";
3
- //#region src/endpoint/result.ts
5
+ //#region src/endpoint/errors.ts
4
6
  /**
5
7
  * A JSON-RPC error response for failures that happen before the MCP server
6
8
  * is involved (auth, method, body parsing).
@@ -20,25 +22,6 @@ import { APIError, ValidationError } from "payload";
20
22
  });
21
23
  };
22
24
  /**
23
- * A successful tool result carrying `value` as JSON text.
24
- */ const jsonResult = (value) => ({ content: [{
25
- type: "text",
26
- text: JSON.stringify(value)
27
- }] });
28
- /**
29
- * A failed tool result. `extras` travel alongside the message so the client
30
- * can act on them (problems, validation errors, the current `updatedAt`).
31
- */ const errorResult = (message, extras = {}) => ({
32
- content: [{
33
- type: "text",
34
- text: JSON.stringify({
35
- error: message,
36
- ...extras
37
- })
38
- }],
39
- isError: true
40
- });
41
- /**
42
25
  * Maps an exception thrown by a tool to a result the client can read.
43
26
  *
44
27
  * Payload's public errors keep their message and status; a `ValidationError`
@@ -62,4 +45,4 @@ import { APIError, ValidationError } from "payload";
62
45
  return errorResult("Internal error");
63
46
  };
64
47
  //#endregion
65
- export { errorResult, jsonResult, jsonRpcError, toToolError };
48
+ export { jsonRpcError, toToolError };
@@ -1,6 +1,6 @@
1
1
  import { readableGlobalSlugs, readableSlugs, resolveCapabilities, writableGlobalSlugs, writableSlugs } from "../capabilities.mjs";
2
+ import { jsonRpcError } from "./errors.mjs";
2
3
  import { resolveApiKeyAuth } from "../auth/resolve.mjs";
3
- import { jsonRpcError } from "./result.mjs";
4
4
  import { createMcpServer } from "./server.mjs";
5
5
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
6
6
  //#region src/endpoint/handler.ts
@@ -8,14 +8,18 @@ const buildScope = (req, options, capabilities) => {
8
8
  const { localization } = req.payload.config;
9
9
  return {
10
10
  req,
11
- options,
12
11
  capabilities,
13
12
  readable: readableSlugs(capabilities),
14
13
  writable: writableSlugs(capabilities),
15
14
  readableGlobals: readableGlobalSlugs(capabilities),
16
15
  writableGlobals: writableGlobalSlugs(capabilities),
17
16
  locales: localization ? localization.localeCodes : null,
18
- defaultLocale: localization ? localization.defaultLocale : null
17
+ defaultLocale: localization ? localization.defaultLocale : null,
18
+ limits: options.limits,
19
+ exposure: {
20
+ collections: options.collections,
21
+ globals: options.globals
22
+ }
19
23
  };
20
24
  };
21
25
  /**
@@ -68,7 +72,7 @@ const buildScope = (req, options, capabilities) => {
68
72
  code: -32600,
69
73
  message: "Invalid request: a JSON body is required."
70
74
  });
71
- const server = createMcpServer(buildScope(req, options, capabilities));
75
+ const server = createMcpServer(buildScope(req, options, capabilities), options);
72
76
  const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
73
77
  await server.connect(transport);
74
78
  const headers = new Headers(req.headers);
@@ -0,0 +1,4 @@
1
+ import { jsonRpcError, toToolError } from "./errors.mjs";
2
+ import { createMcpServer, isToolEnabled, toolDescription, toolInputSchema } from "./server.mjs";
3
+ import { createMcpxHandler, methodNotAllowed } from "./handler.mjs";
4
+ export { createMcpServer, createMcpxHandler, isToolEnabled, jsonRpcError, methodNotAllowed, toToolError, toolDescription, toolInputSchema };
@@ -1,24 +1,37 @@
1
- import { toToolError } from "./result.mjs";
2
- import { BUILTIN_TOOLS } from "../tools/index.mjs";
1
+ import { toToolError } from "./errors.mjs";
2
+ import { draftSentence } from "../tools/shared.mjs";
3
+ import { BUILTIN_TOOLS } from "../tools/builtin.mjs";
3
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
5
  import { z } from "zod";
5
6
  //#region src/endpoint/server.ts
6
7
  /**
7
- * Builds a builtin tool's input schema as a strict object, so an unknown
8
- * argument is rejected with its name instead of being silently stripped and
9
- * the tool answering as if it had not been passed.
10
- */ const builtinInputSchema = (tool, scope) => z.strictObject(tool.inputSchema(scope));
8
+ * Builds a tool's input schema as a strict object, so an unknown argument is
9
+ * rejected with its name instead of being silently stripped and the tool
10
+ * answering as if it had not been passed. A tool may build its shape from the
11
+ * scope to narrow enums to what the key may touch.
12
+ */ const toolInputSchema = (tool, scope) => z.strictObject(typeof tool.inputSchema === "function" ? tool.inputSchema(scope) : tool.inputSchema ?? {});
11
13
  /**
12
- * Builds the MCP server for one request. Tools are registered against the
13
- * key's capabilities, so `tools/list` shows exactly what the key may call and
14
- * every `collection` enum is limited to what it may touch.
15
- */ const createMcpServer = (scope) => {
16
- const { req, options, capabilities } = scope;
14
+ * A tool's description, which may be built from the scope so it can name the
15
+ * targets this key writes live.
16
+ */ const toolDescription = (tool, scope) => typeof tool.description === "function" ? tool.description(scope) : tool.description;
17
+ /**
18
+ * Whether the key may call the tool. A tool that does not decide for itself is
19
+ * gated by its own checkbox on the key, which is how the tools from
20
+ * `options.tools` work; the builtins derive it from the key's collection and
21
+ * global capabilities instead.
22
+ */ const isToolEnabled = (tool, scope) => tool.isEnabled ? tool.isEnabled(scope) : scope.capabilities.tools[tool.name] === true;
23
+ /**
24
+ * Builds the MCP server for one request. Builtin and configured tools take the
25
+ * same route: each is registered against the key's capabilities, so
26
+ * `tools/list` shows exactly what the key may call and every `collection` enum
27
+ * is limited to what it may touch.
28
+ */ const createMcpServer = (scope, options) => {
29
+ const { req } = scope;
17
30
  const { logger } = req.payload;
18
31
  const server = new McpServer({
19
32
  name: options.serverInfo.name,
20
33
  version: options.serverInfo.version
21
- }, { instructions: "Start with listCapabilities, then describeSchema for the collection or global you work on. Writes always land as drafts; a human publishes." });
34
+ }, { instructions: `Start with listCapabilities, then describeSchema for the collection or global you work on. ${draftSentence(scope)}` });
22
35
  const guarded = (run) => async () => {
23
36
  try {
24
37
  return await run();
@@ -26,22 +39,15 @@ import { z } from "zod";
26
39
  return toToolError(error, logger);
27
40
  }
28
41
  };
29
- for (const tool of BUILTIN_TOOLS) {
30
- if (!tool.isEnabled(scope)) continue;
31
- server.registerTool(tool.name, {
32
- description: tool.description,
33
- inputSchema: builtinInputSchema(tool, scope),
34
- annotations: tool.annotations
35
- }, (args) => guarded(() => tool.handler(args, scope))());
36
- }
37
- for (const tool of options.tools) {
38
- if (capabilities.tools[tool.name] !== true) continue;
42
+ for (const tool of [...BUILTIN_TOOLS, ...options.tools]) {
43
+ if (!isToolEnabled(tool, scope)) continue;
39
44
  server.registerTool(tool.name, {
40
- description: tool.description,
41
- inputSchema: tool.inputSchema ?? {},
45
+ description: toolDescription(tool, scope),
46
+ inputSchema: toolInputSchema(tool, scope),
42
47
  ...tool.annotations ? { annotations: tool.annotations } : {}
43
48
  }, (args, extra) => guarded(() => tool.handler({
44
49
  args,
50
+ scope,
45
51
  req,
46
52
  extra
47
53
  }))());
@@ -49,4 +55,4 @@ import { z } from "zod";
49
55
  return server;
50
56
  };
51
57
  //#endregion
52
- export { builtinInputSchema, createMcpServer };
58
+ export { createMcpServer, isToolEnabled, toolDescription, toolInputSchema };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxTool, McpxToolExtra, defineMcpxTool } from "./types.mjs";
1
+ import { McpxAnyTool, McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxExposedEntity, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxTool, McpxToolExtra, McpxToolScope, PublishBlocker, defineMcpxTool } from "./types.mjs";
2
2
  import { mcpxPlugin } from "./plugin.mjs";
3
- import { isMcpxRequest } from "./write/draft-guard.mjs";
4
- import { PublishBlocker } from "./write/publish-blockers.mjs";
5
- export { type McpxAuthResult, type McpxCollectionCapabilities, type McpxCollectionOptions, type McpxGlobalOptions, type McpxPluginOptions, type McpxRequestContext, type McpxResolvedCapabilities, type McpxTool, type McpxToolExtra, type PublishBlocker, defineMcpxTool, isMcpxRequest, mcpxPlugin };
3
+ import { isMcpxRequest } from "./request.mjs";
4
+ import { errorResult, jsonResult } from "./result.mjs";
5
+ export { McpxAnyTool, McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxExposedEntity, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxTool, McpxToolExtra, McpxToolScope, PublishBlocker, defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin };
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { isMcpxRequest } from "./write/draft-guard.mjs";
2
- import { mcpxPlugin } from "./plugin.mjs";
1
+ import { errorResult, jsonResult } from "./result.mjs";
3
2
  import { defineMcpxTool } from "./types.mjs";
4
- export { defineMcpxTool, isMcpxRequest, mcpxPlugin };
3
+ import { isMcpxRequest } from "./request.mjs";
4
+ import { mcpxPlugin } from "./plugin.mjs";
5
+ export { defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin };
package/dist/plugin.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createApiKeysCollection } from "./api-keys/collection.mjs";
2
2
  import { createMcpxHandler, methodNotAllowed } from "./endpoint/handler.mjs";
3
+ import "./endpoint/index.mjs";
3
4
  import { normalizeOptions } from "./options.mjs";
4
5
  import { installDraftGuards, installGlobalDraftGuards } from "./write/draft-guard.mjs";
5
6
  import { definePlugin } from "payload";
@@ -1,5 +1,5 @@
1
- import { CollectionConfig, PayloadRequest } from "payload";
2
- //#region src/write/draft-guard.d.ts
1
+ import { PayloadRequest } from "payload";
2
+ //#region src/request.d.ts
3
3
  /**
4
4
  * Whether a request originated from the MCP endpoint. The endpoint stamps
5
5
  * `req.context.mcpx`, which travels into every local API call made with the
@@ -0,0 +1,8 @@
1
+ //#region src/request.ts
2
+ /**
3
+ * Whether a request originated from the MCP endpoint. The endpoint stamps
4
+ * `req.context.mcpx`, which travels into every local API call made with the
5
+ * same `req`, including those made by custom tools.
6
+ */ const isMcpxRequest = (req) => req.context.mcpx !== void 0;
7
+ //#endregion
8
+ export { isMcpxRequest };
@@ -0,0 +1,13 @@
1
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ //#region src/result.d.ts
3
+ /**
4
+ * A successful tool result carrying `value` as JSON text.
5
+ */
6
+ declare const jsonResult: (value: unknown) => CallToolResult;
7
+ /**
8
+ * A failed tool result. `extras` travel alongside the message so the client
9
+ * can act on them (problems, validation errors, the current `updatedAt`).
10
+ */
11
+ declare const errorResult: (message: string, extras?: Record<string, unknown>) => CallToolResult;
12
+ //#endregion
13
+ export { errorResult, jsonResult };
@@ -0,0 +1,22 @@
1
+ //#region src/result.ts
2
+ /**
3
+ * A successful tool result carrying `value` as JSON text.
4
+ */ const jsonResult = (value) => ({ content: [{
5
+ type: "text",
6
+ text: JSON.stringify(value)
7
+ }] });
8
+ /**
9
+ * A failed tool result. `extras` travel alongside the message so the client
10
+ * can act on them (problems, validation errors, the current `updatedAt`).
11
+ */ const errorResult = (message, extras = {}) => ({
12
+ content: [{
13
+ type: "text",
14
+ text: JSON.stringify({
15
+ error: message,
16
+ ...extras
17
+ })
18
+ }],
19
+ isError: true
20
+ });
21
+ //#endregion
22
+ export { errorResult, jsonResult };
@@ -0,0 +1,6 @@
1
+ import { allowedNodeTypes, lexicalSubSchema, nodeOptions, subSchemaNodeTypes } from "./lexical.mjs";
2
+ import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, targetOf } from "./walk.mjs";
3
+ import { nodeDescriber, reachableSchemaPaths } from "./describe.mjs";
4
+ import { resolveDataPointer } from "./pointer.mjs";
5
+ import { validateWriteValue } from "./shape.mjs";
6
+ export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, allowedNodeTypes, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, lexicalSubSchema, nodeDescriber, nodeOptions, pointerFromPayloadPath, reachableSchemaPaths, resolveDataPointer, splitPath, subSchemaNodeTypes, targetOf, validateWriteValue };
@@ -43,7 +43,7 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isI
43
43
  if (isSubtreePrefix(descriptors, segments)) return {
44
44
  ...blockType === void 0 ? {} : { blockType },
45
45
  fields,
46
- prefix: segments
46
+ prefix: segments.map((segment) => isIndexSegment(segment) ? "*" : segment)
47
47
  };
48
48
  throw new Error(`"${joinPath(segments)}" is not a field here. Available: ${descriptors.map((descriptor) => descriptor.path).join(", ")}`);
49
49
  }
@@ -83,7 +83,7 @@ const withRows = (descriptor, field) => ({
83
83
  * Whether a descriptor stands for a construct that only holds other fields.
84
84
  *
85
85
  * These describe a position rather than a value, so everything that resolves a
86
- * path to something writable skips them; only {@link describeNode} reports
86
+ * path to something writable skips them; only {@link nodeDescriber} reports
87
87
  * them, to carry what the container itself declares.
88
88
  */ const isContainer = (descriptor) => descriptor.type === "array" || descriptor.type === "group" || descriptor.type === "tab";
89
89
  /**
@@ -133,29 +133,25 @@ const withRows = (descriptor, field) => ({
133
133
  /**
134
134
  * The descriptors that address a value, which is what every walk resolving a
135
135
  * path against a document needs. A container describes a position rather than
136
- * a value, so only {@link describeNode} reports one.
136
+ * a value, so only {@link nodeDescriber} reports one.
137
137
  */ const describeAddressableFields = (fields) => describeFields(fields).filter((descriptor) => !isContainer(descriptor));
138
138
  /**
139
- * Locates the blocks field that a resolved descriptor path refers to.
140
- */ const findBlocksField = (fields, path) => {
139
+ * Locates the field of `type` that a resolved descriptor path refers to.
140
+ */ const findFieldAt = (fields, path, type) => {
141
141
  for (const field of fields) {
142
142
  if (!("name" in field) || field.name !== path[0]) continue;
143
- if (field.type === "blocks" && path.length === 1) return field;
144
- if (field.type === "tab" || field.type === "group") return findBlocksField(field.flattenedFields, path.slice(1));
145
- if (field.type === "array" && path[1] === "*") return findBlocksField(field.flattenedFields, path.slice(2));
143
+ if (field.type === type && path.length === 1) return field;
144
+ if (field.type === "tab" || field.type === "group") return findFieldAt(field.flattenedFields, path.slice(1), type);
145
+ if (field.type === "array" && path[1] === "*") return findFieldAt(field.flattenedFields, path.slice(2), type);
146
146
  }
147
147
  };
148
148
  /**
149
+ * Locates the blocks field that a resolved descriptor path refers to.
150
+ */ const findBlocksField = (fields, path) => findFieldAt(fields, path, "blocks");
151
+ /**
149
152
  * Locates the rich text field that a resolved descriptor path refers to, so
150
153
  * its editor can be introspected for the fields its nodes carry.
151
- */ const findRichTextField = (fields, path) => {
152
- for (const field of fields) {
153
- if (!("name" in field) || field.name !== path[0]) continue;
154
- if (field.type === "richText" && path.length === 1) return field;
155
- if (field.type === "tab" || field.type === "group") return findRichTextField(field.flattenedFields, path.slice(1));
156
- if (field.type === "array" && path[1] === "*") return findRichTextField(field.flattenedFields, path.slice(2));
157
- }
158
- };
154
+ */ const findRichTextField = (fields, path) => findFieldAt(fields, path, "richText");
159
155
  const targetOf = (config, ref) => {
160
156
  const found = ref.kind === "collection" ? config.collections.find((candidate) => candidate.slug === ref.slug) : config.globals.find((candidate) => candidate.slug === ref.slug);
161
157
  if (!found) throw new Error(`Unknown ${ref.kind} "${ref.slug}".`);
@@ -5,11 +5,14 @@ import { getDocument } from "./get-document.mjs";
5
5
  import { listCapabilities } from "./list-capabilities.mjs";
6
6
  import { patchDocument } from "./patch-document.mjs";
7
7
  import { validateDocument } from "./validate-document.mjs";
8
- //#region src/tools/index.ts
8
+ //#region src/tools/builtin.ts
9
9
  /**
10
- * The builtin tools in registration order. The surface is fixed: adding a
11
- * collection, block or field never changes it. Typed over `never` because
12
- * each tool validates its own arguments through its input schema.
10
+ * The builtin tools in registration order. They are ordinary {@link McpxTool}s
11
+ * that ship with the plugin and register through the same loop as the tools
12
+ * from `options.tools`; only their `isEnabled` differs, deriving from the
13
+ * key's collection and global capabilities rather than a checkbox of their
14
+ * own. The surface is fixed: adding a collection, block or field never
15
+ * changes it.
13
16
  */ const BUILTIN_TOOLS = [
14
17
  listCapabilities,
15
18
  describeSchema,
@@ -1,14 +1,19 @@
1
- import { errorResult, jsonResult } from "../endpoint/result.mjs";
2
- import { localeOf, localeShape, readTarget, slugEnum } from "./shared.mjs";
3
- import { resolveTarget } from "./target.mjs";
1
+ import { errorResult, jsonResult } from "../result.mjs";
4
2
  import { validateWriteValue } from "../schema/shape.mjs";
3
+ import "../schema/index.mjs";
4
+ import { draftSentence, localeOf, localeShape, readTarget, slugEnum } from "./shared.mjs";
5
+ import { resolveTarget } from "./target.mjs";
6
+ import { defineMcpxTool } from "../types.mjs";
5
7
  import { stripRowIds } from "../write/patch.mjs";
6
8
  import { collectPublishBlockers } from "../write/publish-blockers.mjs";
7
9
  import { z } from "zod";
8
10
  //#region src/tools/create-document.ts
9
- const createDocument = {
11
+ const DESCRIPTION = (scope) => `Creates a new document from a minimal seed. Only the fields describeSchema lists may appear in "data"; unknown keys are refused with the valid siblings, and "id" is Payload's to assign. The document may be incomplete: the response lists "publishBlockers", which patchDocument can then work through, and "publishBlockersUnavailable" when that check itself failed. Use this when no document exists yet; prefer patching an existing draft otherwise.
12
+
13
+ ${draftSentence(scope)}`;
14
+ const createDocument = defineMcpxTool({
10
15
  name: "createDocument",
11
- description: `Creates a new document as a draft from a minimal seed. Only the fields describeSchema lists may appear in "data"; unknown keys are refused with the valid siblings. The draft may be incomplete: the response lists "publishBlockers", which patchDocument can then work through. Use this when no document exists yet; prefer patching an existing draft otherwise.`,
16
+ description: DESCRIPTION,
12
17
  annotations: {
13
18
  readOnlyHint: false,
14
19
  destructiveHint: false,
@@ -24,22 +29,22 @@ const createDocument = {
24
29
  }),
25
30
  data: z.record(z.string(), z.unknown()).describe("Initial field values, as describeSchema lists them.")
26
31
  }),
27
- handler: async (args, scope) => {
32
+ handler: async ({ args, scope }) => {
28
33
  const target = resolveTarget(scope, { collection: args.collection }, "write");
29
34
  const { payload } = scope.req;
30
35
  const locale = localeOf(scope, args.locale);
31
- const { id: _ignored, ...seed } = args.data;
36
+ if ("id" in args.data) return errorResult("Nothing was created.", { problems: ["/id: Payload assigns the id; it cannot be supplied."] });
32
37
  const problems = validateWriteValue(payload.config, {
33
38
  pointer: "",
34
39
  resolution: {
35
40
  fields: target.config.flattenedFields,
36
41
  prefix: []
37
42
  }
38
- }, seed);
43
+ }, args.data);
39
44
  if (problems.length > 0) return errorResult("Nothing was created.", { problems });
40
45
  const created = await payload.create({
41
46
  collection: args.collection,
42
- data: stripRowIds(seed),
47
+ data: stripRowIds(args.data),
43
48
  depth: 0,
44
49
  draft: true,
45
50
  overrideAccess: false,
@@ -52,7 +57,7 @@ const createDocument = {
52
57
  locale,
53
58
  privileged: true
54
59
  });
55
- const publishBlockers = await collectPublishBlockers(scope.req, {
60
+ const validation = await collectPublishBlockers(scope.req, {
56
61
  doc: saved,
57
62
  entity: target
58
63
  });
@@ -60,9 +65,10 @@ const createDocument = {
60
65
  id: saved["id"],
61
66
  status: saved["_status"],
62
67
  updatedAt: saved["updatedAt"],
63
- ...publishBlockers.length > 0 ? { publishBlockers } : {}
68
+ ...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
69
+ ...validation.unavailable ? { publishBlockersUnavailable: true } : {}
64
70
  });
65
71
  }
66
- };
72
+ });
67
73
  //#endregion
68
74
  export { createDocument };
@@ -1,11 +1,12 @@
1
+ import { jsonResult } from "../result.mjs";
1
2
  import { translatorFor } from "../i18n.mjs";
2
- import { jsonResult } from "../endpoint/result.mjs";
3
+ import { nodeDescriber, reachableSchemaPaths } from "../schema/describe.mjs";
4
+ import "../schema/index.mjs";
3
5
  import { targetShape } from "./shared.mjs";
4
6
  import { refOf, resolveTarget } from "./target.mjs";
5
- import { nodeDescriber, reachableSchemaPaths } from "../schema/describe.mjs";
7
+ import { defineMcpxTool } from "../types.mjs";
6
8
  import { z } from "zod";
7
- //#region src/tools/describe-schema.ts
8
- const describeSchema = {
9
+ const describeSchema = defineMcpxTool({
9
10
  name: "describeSchema",
10
11
  description: `Describes the writable shape of a document, one node at a time.
11
12
 
@@ -31,7 +32,7 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
31
32
  paths: z.array(z.string()).optional().describe("Schema paths to describe, e.g. \"/layout/sections/sectionWrapper\". Omit for the collection root."),
32
33
  expand: z.boolean().optional().describe("Return every node reachable from the root in one response. Ignores paths.")
33
34
  }),
34
- handler: (args, scope) => {
35
+ handler: ({ args, scope }) => {
35
36
  const ref = refOf(resolveTarget(scope, args, "read"));
36
37
  const { config } = scope.req.payload;
37
38
  const describeNode = nodeDescriber(translatorFor(scope.req.i18n));
@@ -49,6 +50,6 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
49
50
  if (expanded?.truncated) nodes.push({ error: `Result truncated after ${String(400)} nodes. Request explicit paths instead.` });
50
51
  return Promise.resolve(jsonResult(nodes));
51
52
  }
52
- };
53
+ });
53
54
  //#endregion
54
55
  export { describeSchema };
@@ -1,9 +1,9 @@
1
- import { jsonResult } from "../endpoint/result.mjs";
1
+ import { jsonResult } from "../result.mjs";
2
2
  import { depthShape, localeOf, localeShape, slugEnum } from "./shared.mjs";
3
3
  import { resolveTarget } from "./target.mjs";
4
+ import { defineMcpxTool } from "../types.mjs";
4
5
  import { z } from "zod";
5
- //#region src/tools/find-documents.ts
6
- const findDocuments = {
6
+ const findDocuments = defineMcpxTool({
7
7
  name: "findDocuments",
8
8
  description: `Finds documents in a collection. "where" is a Payload query object, e.g. {"title":{"contains":"home"}} or {"and":[...]}; "select" picks fields, e.g. {"title":true}. Drafts are included by default so unpublished work is visible. Keep depth at 0 unless populated relationships are needed; ids are enough for writes.`,
9
9
  annotations: {
@@ -15,7 +15,7 @@ const findDocuments = {
15
15
  collection: slugEnum(scope.readable).describe("Collection to search."),
16
16
  where: z.record(z.string(), z.unknown()).optional().describe("Payload where query."),
17
17
  sort: z.string().optional().describe("Sort field, prefix with \"-\" for descending."),
18
- limit: z.number().int().min(1).max(scope.options.limits.maxLimit).optional().describe(`Documents per page. Default 10, at most ${String(scope.options.limits.maxLimit)}.`),
18
+ limit: z.number().int().min(1).max(scope.limits.maxLimit).optional().describe(`Documents per page. Default 10, at most ${String(scope.limits.maxLimit)}.`),
19
19
  page: z.number().int().min(1).optional().describe("Page number, from 1."),
20
20
  ...depthShape(scope),
21
21
  select: z.record(z.string(), z.unknown()).optional().describe("Fields to return, e.g. {\"title\":true}."),
@@ -25,7 +25,7 @@ const findDocuments = {
25
25
  }),
26
26
  draft: z.boolean().optional().describe("Include the latest drafts. Default true.")
27
27
  }),
28
- handler: async (args, scope) => {
28
+ handler: async ({ args, scope }) => {
29
29
  resolveTarget(scope, { collection: args.collection }, "read");
30
30
  const locale = localeOf(scope, args.locale);
31
31
  const result = await scope.req.payload.find({
@@ -50,6 +50,6 @@ const findDocuments = {
50
50
  hasNextPage: result.hasNextPage
51
51
  });
52
52
  }
53
- };
53
+ });
54
54
  //#endregion
55
55
  export { findDocuments };