@abinnovision/payloadcms-mcpx 1.0.0-beta.7 → 1.0.0-beta.8

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 CHANGED
@@ -163,8 +163,9 @@ Rules the tools enforce and explain in their own descriptions:
163
163
  the only place the restriction is checked.
164
164
  - Field and collection `admin.description` values are included in
165
165
  `describeSchema` and `listCapabilities`, so intent written for the admin
166
- panel reaches the client. Strings and locale-keyed records pass through;
167
- functions and components are dropped.
166
+ panel reaches the client. A locale-keyed record is resolved to one string for
167
+ the request's language, falling back to the deployment's fallback language and
168
+ then to the record's first entry; functions and components are dropped.
168
169
  - Builtin tools reject unknown arguments by name instead of silently ignoring
169
170
  them.
170
171
  - Every path this plugin accepts or reports is a JSON Pointer. A schema path
@@ -0,0 +1 @@
1
+ import { PayloadRequest } from "payload";
package/dist/i18n.mjs ADDED
@@ -0,0 +1,40 @@
1
+ //#region src/i18n.ts
2
+ /**
3
+ * A locale-keyed record, once it is known to hold nothing but strings.
4
+ */ const stringRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string") ? value : void 0;
5
+ /**
6
+ * Picks the entry a language addresses, treating an empty value as absent so
7
+ * the chain continues rather than yielding a useless string.
8
+ */ const pick = (record, language) => {
9
+ for (const code of Array.isArray(language) ? language : [language]) {
10
+ const entry = record[code];
11
+ if (entry !== void 0 && entry.trim() !== "") return entry;
12
+ }
13
+ };
14
+ /**
15
+ * Resolves a static label or `admin.description` to the one string a client
16
+ * can use: the request's language, then the fallback language configured for
17
+ * the deployment, then whichever entry the record declares first.
18
+ *
19
+ * Anything that is not a string or a string-valued record is dropped. A
20
+ * description written as a function or a React component is an admin-UI
21
+ * construct that may reach client-only i18n, so it is never invoked here.
22
+ */ const translateStatic = (value, language) => {
23
+ if (typeof value === "string") return value.trim() === "" ? void 0 : value;
24
+ const record = stringRecord(value);
25
+ if (!record) return;
26
+ return pick(record, language.language) ?? pick(record, language.fallbackLanguage) ?? Object.values(record).find((entry) => entry.trim() !== "");
27
+ };
28
+ /**
29
+ * Binds {@link translateStatic} to a request's language, so a walk that
30
+ * resolves many descriptions carries no request of its own.
31
+ */ const translatorFor = (i18n) => (value) => translateStatic(value, i18n);
32
+ /**
33
+ * Translator for callers with no request in hand. Both language keys miss, so
34
+ * the chain degrades to the record's first entry.
35
+ */ const translateAny = translatorFor({
36
+ fallbackLanguage: "",
37
+ language: ""
38
+ });
39
+ //#endregion
40
+ export { translateAny, translateStatic, translatorFor };
@@ -1,4 +1,5 @@
1
1
  import { lexicalSubSchema, subSchemaNodeTypes } from "./lexical.mjs";
2
+ import { translateAny } from "../i18n.mjs";
2
3
  import { blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextField, joinPath, splitPath, targetOf } from "./walk.mjs";
3
4
  //#region src/schema/describe.ts
4
5
  /**
@@ -115,9 +116,12 @@ import { blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextFie
115
116
  /**
116
117
  * Describes a collection or global root, one block reached through a schema
117
118
  * path, or the fields a Lexical node carries.
118
- */ const describeNode = (config, ref, schemaPath = "") => {
119
+ *
120
+ * Curried on the translator that resolves each `admin.description`, so a
121
+ * request binds its language once and the walk itself stays request-free.
122
+ */ const nodeDescriber = (translate = translateAny) => (config, ref, schemaPath = "") => {
119
123
  const { blockType, fields } = fieldsAtSchemaPath(config, targetOf(config, ref), schemaPath);
120
- const descriptors = describeFields(fields);
124
+ const descriptors = describeFields(fields, translate);
121
125
  const next = descriptors.flatMap((descriptor) => branchesOf(fields, descriptor, schemaPath).map((branch) => branch.path));
122
126
  return {
123
127
  ...blockType === void 0 ? {} : { blockType },
@@ -153,4 +157,4 @@ import { blockOf, blockSlugsOf, describeFields, findBlocksField, findRichTextFie
153
157
  };
154
158
  };
155
159
  //#endregion
156
- export { describeNode, reachableSchemaPaths };
160
+ export { nodeDescriber, reachableSchemaPaths };
@@ -1,2 +1,3 @@
1
1
  import "./lexical.mjs";
2
+ import "../i18n.mjs";
2
3
  import "payload";
@@ -1,4 +1,5 @@
1
1
  import { allowedNodeTypes, nodeOptions } from "./lexical.mjs";
2
+ import { translateAny } from "../i18n.mjs";
2
3
  import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
3
4
  //#region src/schema/walk.ts
4
5
  /**
@@ -46,16 +47,8 @@ import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
46
47
  };
47
48
  const isSkipped = (field) => !("name" in field) || field.type === "join" || RESERVED_FIELD_NAMES.has(field.name) || fieldIsVirtual(field) || fieldIsHiddenOrDisabled(field);
48
49
  const isReadOnly = (field) => "admin" in field && field.admin.readOnly === true;
49
- /**
50
- * The `admin.description` of a field or collection, when it is serializable:
51
- * a string or a locale-keyed record. Functions and components are admin-UI
52
- * constructs and are dropped.
53
- */ const staticDescription = (description) => {
54
- if (typeof description === "string") return description;
55
- return typeof description === "object" && description !== null && Object.values(description).every((entry) => typeof entry === "string") ? description : void 0;
56
- };
57
- const describeBase = (field, path, readOnly) => {
58
- const description = staticDescription("admin" in field ? field.admin.description : void 0);
50
+ const describeBase = (field, { path, readOnly, translate }) => {
51
+ const description = translate("admin" in field ? field.admin.description : void 0);
59
52
  return {
60
53
  path,
61
54
  type: field.type,
@@ -65,8 +58,8 @@ const describeBase = (field, path, readOnly) => {
65
58
  ...readOnly ? { readOnly: true } : {}
66
59
  };
67
60
  };
68
- const describeLeaf = (field, path, readOnly) => {
69
- const descriptor = describeBase(field, path, readOnly);
61
+ const describeLeaf = (field, at) => {
62
+ const descriptor = describeBase(field, at);
70
63
  if (field.type === "select" || field.type === "radio") descriptor.options = field.options.map((option) => typeof option === "string" ? option : option.value);
71
64
  if (field.type === "relationship" || field.type === "upload") descriptor.relationTo = field.relationTo;
72
65
  if ((field.type === "select" || field.type === "relationship" || field.type === "upload") && field.hasMany === true) descriptor.hasMany = true;
@@ -109,21 +102,34 @@ const withRows = (descriptor, field) => ({
109
102
  * constraint. The walk stops at every blocks field and names the slugs instead
110
103
  * of descending, which keeps a node proportional to the number of blocks it
111
104
  * allows rather than to the size of their definitions.
112
- */ const describeFields = (fields, prefix = [], parentReadOnly = false) => fields.flatMap((field) => {
113
- if (isSkipped(field)) return [];
114
- const readOnly = parentReadOnly || isReadOnly(field);
115
- const path = [...prefix, field.name];
116
- if (field.type === "tab" || field.type === "group") {
117
- const own = describeBase(field, joinPath(path), readOnly);
118
- return [...isInformative(own) ? [own] : [], ...describeFields(field.flattenedFields, path, readOnly)];
119
- }
120
- if (field.type === "array") return [withRows(describeBase(field, joinPath(path), readOnly), field), ...describeFields(field.flattenedFields, [...path, "*"], readOnly)];
121
- if (field.type === "blocks") return [withRows({
122
- ...describeBase(field, joinPath(path), readOnly),
123
- blocks: blockSlugsOf(field)
124
- }, field)];
125
- return [describeLeaf(field, joinPath(path), readOnly)];
126
- });
105
+ *
106
+ * `translate` resolves each `admin.description` to the request's language.
107
+ * Callers that walk for paths alone leave it out and get the language-agnostic
108
+ * default, so a missing argument costs language selection, never the
109
+ * description itself.
110
+ */ const describeFields = (fields, translate = translateAny) => {
111
+ const walk = (current, prefix, parentReadOnly) => current.flatMap((field) => {
112
+ if (isSkipped(field)) return [];
113
+ const readOnly = parentReadOnly || isReadOnly(field);
114
+ const path = [...prefix, field.name];
115
+ const at = {
116
+ path: joinPath(path),
117
+ readOnly,
118
+ translate
119
+ };
120
+ if (field.type === "tab" || field.type === "group") {
121
+ const own = describeBase(field, at);
122
+ return [...isInformative(own) ? [own] : [], ...walk(field.flattenedFields, path, readOnly)];
123
+ }
124
+ if (field.type === "array") return [withRows(describeBase(field, at), field), ...walk(field.flattenedFields, [...path, "*"], readOnly)];
125
+ if (field.type === "blocks") return [withRows({
126
+ ...describeBase(field, at),
127
+ blocks: blockSlugsOf(field)
128
+ }, field)];
129
+ return [describeLeaf(field, at)];
130
+ });
131
+ return walk(fields, [], false);
132
+ };
127
133
  /**
128
134
  * The descriptors that address a value, which is what every walk resolving a
129
135
  * path against a document needs. A container describes a position rather than
@@ -156,4 +162,4 @@ const targetOf = (config, ref) => {
156
162
  return found;
157
163
  };
158
164
  //#endregion
159
- export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, staticDescription, targetOf };
165
+ export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, targetOf };
@@ -1,7 +1,8 @@
1
+ import { translatorFor } from "../i18n.mjs";
1
2
  import { jsonResult } from "../endpoint/result.mjs";
2
3
  import { targetShape } from "./shared.mjs";
3
4
  import { refOf, resolveTarget } from "./target.mjs";
4
- import { describeNode, reachableSchemaPaths } from "../schema/describe.mjs";
5
+ import { nodeDescriber, reachableSchemaPaths } from "../schema/describe.mjs";
5
6
  import { z } from "zod";
6
7
  //#region src/tools/describe-schema.ts
7
8
  const describeSchema = {
@@ -33,6 +34,7 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
33
34
  handler: (args, scope) => {
34
35
  const ref = refOf(resolveTarget(scope, args, "read"));
35
36
  const { config } = scope.req.payload;
37
+ const describeNode = nodeDescriber(translatorFor(scope.req.i18n));
36
38
  const expanded = args.expand === true ? reachableSchemaPaths(config, ref) : void 0;
37
39
  const nodes = (expanded?.paths ?? (args.paths && args.paths.length > 0 ? args.paths : [""])).map((schemaPath) => {
38
40
  try {
@@ -1,4 +1,4 @@
1
- import { staticDescription } from "../schema/walk.mjs";
1
+ import { translatorFor } from "../i18n.mjs";
2
2
  import { jsonResult } from "../endpoint/result.mjs";
3
3
  import { translateLabel } from "./shared.mjs";
4
4
  import { hasDraftValidationEnabled } from "payload/shared";
@@ -16,12 +16,13 @@ A global is a singleton: it has no id, is not listed by findDocuments and cannot
16
16
  inputSchema: () => ({}),
17
17
  handler: (_args, scope) => {
18
18
  const { payload } = scope.req;
19
+ const translate = translatorFor(scope.req.i18n);
19
20
  const collections = scope.options.collections.flatMap((entry) => {
20
21
  const capability = scope.capabilities.collections[entry.slug];
21
22
  const collection = payload.collections[entry.slug];
22
23
  if (!capability || !collection || !(capability.read || capability.write)) return [];
23
24
  const { config } = collection;
24
- const description = staticDescription(config.admin.description);
25
+ const description = translate(config.admin.description);
25
26
  return [{
26
27
  slug: entry.slug,
27
28
  labels: {
@@ -40,7 +41,7 @@ A global is a singleton: it has no id, is not listed by findDocuments and cannot
40
41
  const capability = scope.capabilities.globals[entry.slug];
41
42
  const config = payload.globals.config.find((candidate) => candidate.slug === entry.slug);
42
43
  if (!capability || !config || !(capability.read || capability.write)) return [];
43
- const description = staticDescription(config.admin.description);
44
+ const description = translate(config.admin.description);
44
45
  return [{
45
46
  slug: entry.slug,
46
47
  label: translateLabel(scope, config.label, entry.slug),
@@ -1,3 +1,4 @@
1
+ import { translateStatic } from "../i18n.mjs";
1
2
  import { NotFound } from "payload";
2
3
  import { z } from "zod";
3
4
  //#region src/tools/shared.ts
@@ -90,9 +91,7 @@ const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.option
90
91
  i18n,
91
92
  t
92
93
  }) : label;
93
- if (typeof resolved === "string") return resolved;
94
- if (resolved && typeof resolved === "object") return resolved[i18n.language] ?? Object.values(resolved)[0] ?? fallback;
95
- return fallback;
94
+ return translateStatic(resolved, i18n) ?? fallback;
96
95
  };
97
96
  //#endregion
98
97
  export { depthShape, idSchema, idShape, localeOf, localeShape, readTarget, slugEnum, targetShape, translateLabel };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@abinnovision/payloadcms-mcpx",
4
- "version": "1.0.0-beta.7",
4
+ "version": "1.0.0-beta.8",
5
5
  "description": "Payload CMS plugin exposing a fixed, schema-aware MCP tool surface with draft-only writes and per-API-key capabilities.",
6
6
  "keywords": [
7
7
  "payload",