@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 +3 -2
- package/dist/i18n.d.mts +1 -0
- package/dist/i18n.mjs +40 -0
- package/dist/schema/describe.mjs +7 -3
- package/dist/schema/walk.d.mts +1 -0
- package/dist/schema/walk.mjs +34 -28
- package/dist/tools/describe-schema.mjs +3 -1
- package/dist/tools/list-capabilities.mjs +4 -3
- package/dist/tools/shared.mjs +2 -3
- package/package.json +1 -1
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.
|
|
167
|
-
|
|
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
|
package/dist/i18n.d.mts
ADDED
|
@@ -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 };
|
package/dist/schema/describe.mjs
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
160
|
+
export { nodeDescriber, reachableSchemaPaths };
|
package/dist/schema/walk.d.mts
CHANGED
package/dist/schema/walk.mjs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
69
|
-
const descriptor = describeBase(field,
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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,
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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 =
|
|
44
|
+
const description = translate(config.admin.description);
|
|
44
45
|
return [{
|
|
45
46
|
slug: entry.slug,
|
|
46
47
|
label: translateLabel(scope, config.label, entry.slug),
|
package/dist/tools/shared.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|