@abinnovision/payloadcms-mcpx 1.0.0-beta.7 → 1.0.0-beta.9
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 +23 -2
- package/dist/api-keys/collection.mjs +3 -3
- package/dist/api-keys/fields.mjs +35 -1
- package/dist/api-keys/setup-guide.mjs +54 -0
- package/dist/client/index.d.mts +2 -0
- package/dist/client/index.mjs +2 -0
- package/dist/client/setup-guide.d.mts +14 -0
- package/dist/client/setup-guide.mjs +87 -0
- package/dist/i18n.d.mts +1 -0
- package/dist/i18n.mjs +40 -0
- package/dist/options.mjs +1 -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/dist/types.d.mts +5 -0
- package/package.json +22 -7
package/README.md
CHANGED
|
@@ -27,6 +27,9 @@ yarn add @abinnovision/payloadcms-mcpx
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
- Peer dependency: `payload >=3.88.0 <4`.
|
|
30
|
+
- `@payloadcms/ui` and `react` are optional peers, needed only by the admin
|
|
31
|
+
setup guide. A headless install can leave them out and set
|
|
32
|
+
`apiKeys.setupGuide: false`.
|
|
30
33
|
- The package is published as ESM only, matching Payload itself.
|
|
31
34
|
|
|
32
35
|
## Usage
|
|
@@ -85,6 +88,22 @@ all keys) or add fields.
|
|
|
85
88
|
|
|
86
89
|
## Connecting a client
|
|
87
90
|
|
|
91
|
+
Saved keys carry a **Connect a client** tab in the admin holding these same
|
|
92
|
+
instructions with their own URL and key filled in, each block behind a copy
|
|
93
|
+
button. The tab only exists once the key does, so the create form stays free of
|
|
94
|
+
it. Turn it off with `apiKeys.setupGuide: false`, which also drops the tabs and
|
|
95
|
+
restores the flat form.
|
|
96
|
+
|
|
97
|
+
The tab renders an admin component, so it has to be in the import map:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
payload generate:importmap
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Without that entry Payload logs a missing-component error and renders nothing
|
|
104
|
+
else; the rest of the plugin is unaffected. The URL comes from `serverURL` when
|
|
105
|
+
the config sets one and from the browser's origin otherwise.
|
|
106
|
+
|
|
88
107
|
The endpoint speaks streamable HTTP with `Authorization: Bearer <key>`:
|
|
89
108
|
|
|
90
109
|
```bash
|
|
@@ -163,8 +182,9 @@ Rules the tools enforce and explain in their own descriptions:
|
|
|
163
182
|
the only place the restriction is checked.
|
|
164
183
|
- Field and collection `admin.description` values are included in
|
|
165
184
|
`describeSchema` and `listCapabilities`, so intent written for the admin
|
|
166
|
-
panel reaches the client.
|
|
167
|
-
|
|
185
|
+
panel reaches the client. A locale-keyed record is resolved to one string for
|
|
186
|
+
the request's language, falling back to the deployment's fallback language and
|
|
187
|
+
then to the record's first entry; functions and components are dropped.
|
|
168
188
|
- Builtin tools reject unknown arguments by name instead of silently ignoring
|
|
169
189
|
them.
|
|
170
190
|
- Every path this plugin accepts or reports is a JSON Pointer. A schema path
|
|
@@ -295,6 +315,7 @@ Builtin tools reject them instead.
|
|
|
295
315
|
| `globals.<slug>.allowLiveWrites` | `false` | Permit writes to a global without drafts (they land live). |
|
|
296
316
|
| `userCollection` | `config.admin.user` or `users` | Auth collection the keys act as. |
|
|
297
317
|
| `apiKeys.slug` | `mcpx-api-keys` | Slug of the generated key collection. |
|
|
318
|
+
| `apiKeys.setupGuide` | `true` | Add a "Connect a client" tab to saved keys. Needs the import map. |
|
|
298
319
|
| `apiKeys.overrideCollection` | none | Final override applied to the generated collection. |
|
|
299
320
|
| `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
|
|
300
321
|
| `limits.maxLimit` | `25` | Upper bound for `findDocuments.limit`. |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createCapabilityFields, createKeyFields } from "./fields.mjs";
|
|
1
|
+
import { createCapabilityFields, createKeyFields, withSetupGuideTab } from "./fields.mjs";
|
|
2
2
|
import { generateApiKey, hashApiKey } from "./key.mjs";
|
|
3
3
|
//#region src/api-keys/collection.ts
|
|
4
4
|
/**
|
|
@@ -36,7 +36,7 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
|
|
|
36
36
|
delete: ownKeysOnly
|
|
37
37
|
},
|
|
38
38
|
hooks: { beforeChange: [keyBeforeChange] },
|
|
39
|
-
fields: [
|
|
39
|
+
fields: withSetupGuideTab([
|
|
40
40
|
{
|
|
41
41
|
name: "user",
|
|
42
42
|
type: "relationship",
|
|
@@ -51,7 +51,7 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
|
|
|
51
51
|
},
|
|
52
52
|
...createKeyFields(),
|
|
53
53
|
...createCapabilityFields(options)
|
|
54
|
-
]
|
|
54
|
+
], options)
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
57
|
//#endregion
|
package/dist/api-keys/fields.mjs
CHANGED
|
@@ -15,6 +15,7 @@ const checkbox = (name, description) => ({
|
|
|
15
15
|
defaultValue: false,
|
|
16
16
|
admin: { description }
|
|
17
17
|
});
|
|
18
|
+
const SETUP_GUIDE_FIELD = "setupGuide";
|
|
18
19
|
/**
|
|
19
20
|
* Fields every key carries. Key generation and the HMAC index live in the
|
|
20
21
|
* collection-level `beforeChange` hook (see `collection.ts`), because sibling
|
|
@@ -56,6 +57,39 @@ const checkbox = (name, description) => ({
|
|
|
56
57
|
}
|
|
57
58
|
];
|
|
58
59
|
/**
|
|
60
|
+
* Wraps the key fields and the setup guide in unnamed tabs, so the wide
|
|
61
|
+
* snippets get the full form width without pushing the key itself out of view.
|
|
62
|
+
* Unnamed on purpose: named tabs would nest the data and move `capabilities`
|
|
63
|
+
* off the document root, which capability resolution reads.
|
|
64
|
+
*
|
|
65
|
+
* The guide tab is conditioned on the update operation. On create there is no
|
|
66
|
+
* key to hand out, and a tab leading to an empty panel is worse than no tab.
|
|
67
|
+
*/ const withSetupGuideTab = (keyFields, options) => {
|
|
68
|
+
if (!options.setupGuide) return keyFields;
|
|
69
|
+
return [{
|
|
70
|
+
type: "tabs",
|
|
71
|
+
tabs: [{
|
|
72
|
+
label: "Key",
|
|
73
|
+
fields: keyFields
|
|
74
|
+
}, {
|
|
75
|
+
label: "Connect a client",
|
|
76
|
+
admin: { condition: (_data, _siblingData, { operation }) => operation === "update" },
|
|
77
|
+
fields: [{
|
|
78
|
+
name: SETUP_GUIDE_FIELD,
|
|
79
|
+
type: "ui",
|
|
80
|
+
admin: {
|
|
81
|
+
disableListColumn: true,
|
|
82
|
+
components: { Field: {
|
|
83
|
+
path: "@abinnovision/payloadcms-mcpx/client",
|
|
84
|
+
exportName: "McpxSetupGuide",
|
|
85
|
+
clientProps: { endpointPath: options.endpointPath }
|
|
86
|
+
} }
|
|
87
|
+
}
|
|
88
|
+
}]
|
|
89
|
+
}]
|
|
90
|
+
}];
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
59
93
|
* One checkbox per exposed operation, grouped per collection, per global and
|
|
60
94
|
* per custom tool. Only operations the plugin config exposes get a checkbox, so
|
|
61
95
|
* a key can never enable more than the config allows. Everything defaults to
|
|
@@ -100,4 +134,4 @@ const checkbox = (name, description) => ({
|
|
|
100
134
|
}];
|
|
101
135
|
};
|
|
102
136
|
//#endregion
|
|
103
|
-
export { createCapabilityFields, createKeyFields };
|
|
137
|
+
export { SETUP_GUIDE_FIELD, createCapabilityFields, createKeyFields, withSetupGuideTab };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/api-keys/setup-guide.ts
|
|
2
|
+
const KEY_PLACEHOLDER = "<your-key>";
|
|
3
|
+
/**
|
|
4
|
+
* Server name for the client config. MCP clients key their config by this, so
|
|
5
|
+
* it has to survive labels with spaces or punctuation.
|
|
6
|
+
*/ const toServerName = (label) => {
|
|
7
|
+
const slug = (label ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
8
|
+
return slug === "" ? "payload" : slug;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The connection instructions for one key, split into independently copyable
|
|
12
|
+
* blocks. Kept a pure builder so the admin component holds only rendering and
|
|
13
|
+
* the snippets stay unit-testable.
|
|
14
|
+
*/ const buildSetupGuide = (input) => {
|
|
15
|
+
const key = typeof input.apiKey === "string" ? input.apiKey : KEY_PLACEHOLDER;
|
|
16
|
+
const name = toServerName(input.label);
|
|
17
|
+
const url = input.endpointUrl;
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
id: "endpoint",
|
|
21
|
+
title: "Endpoint",
|
|
22
|
+
description: "Streamable HTTP. Point any MCP client at this URL.",
|
|
23
|
+
snippet: url
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "header",
|
|
27
|
+
title: "Authorization header",
|
|
28
|
+
description: "The only accepted credential; cookies and JWTs are ignored.",
|
|
29
|
+
snippet: `Authorization: Bearer ${key}`
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "claude-code",
|
|
33
|
+
title: "Claude Code",
|
|
34
|
+
snippet: [`claude mcp add --transport http ${name} ${url} \\`, ` --header "Authorization: Bearer ${key}"`].join("\n")
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "claude-desktop",
|
|
38
|
+
title: "Claude Desktop",
|
|
39
|
+
description: "Has no direct header support, so it goes through mcp-remote.",
|
|
40
|
+
snippet: JSON.stringify({ mcpServers: { [name]: {
|
|
41
|
+
command: "npx",
|
|
42
|
+
args: [
|
|
43
|
+
"-y",
|
|
44
|
+
"mcp-remote",
|
|
45
|
+
url,
|
|
46
|
+
"--header",
|
|
47
|
+
`Authorization: Bearer ${key}`
|
|
48
|
+
]
|
|
49
|
+
} } }, null, 2)
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { KEY_PLACEHOLDER, buildSetupGuide, toServerName };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
//#region src/client/setup-guide.d.ts
|
|
3
|
+
interface McpxSetupGuideProps {
|
|
4
|
+
/** Endpoint path below the API route, from the plugin options. */
|
|
5
|
+
endpointPath: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Per-key connection instructions on the API key edit view. Renders nothing
|
|
9
|
+
* until the document is saved, because before that there is no key to hand to
|
|
10
|
+
* a client.
|
|
11
|
+
*/
|
|
12
|
+
declare const McpxSetupGuide: React.FC<McpxSetupGuideProps>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { McpxSetupGuide, type McpxSetupGuideProps };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { buildSetupGuide } from "../api-keys/setup-guide.mjs";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { CopyToClipboard, useConfig, useDocumentInfo, useFormFields } from "@payloadcms/ui";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
//#region src/client/setup-guide.tsx
|
|
7
|
+
const asString = (value) => typeof value === "string" ? value : void 0;
|
|
8
|
+
/**
|
|
9
|
+
* Reads `serverURL` when the config sets one and falls back to the browser's
|
|
10
|
+
* origin. The fallback has to wait for mount: this component is server-rendered
|
|
11
|
+
* first, where `window` does not exist.
|
|
12
|
+
*/ const useOrigin = (serverUrl) => {
|
|
13
|
+
const [origin, setOrigin] = useState(serverUrl);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (serverUrl === "") setOrigin(window.location.origin);
|
|
16
|
+
}, [serverUrl]);
|
|
17
|
+
return origin;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Payload's own theme variables, so the panel follows the admin's light and
|
|
21
|
+
* dark themes without shipping a stylesheet consumers would have to transpile.
|
|
22
|
+
*/ const styles = {
|
|
23
|
+
lead: { marginBottom: "calc(var(--base) * 0.75)" },
|
|
24
|
+
section: { marginBottom: "calc(var(--base) * 0.75)" },
|
|
25
|
+
sectionHeader: {
|
|
26
|
+
display: "flex",
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
gap: "calc(var(--base) * 0.25)"
|
|
29
|
+
},
|
|
30
|
+
description: {
|
|
31
|
+
margin: "calc(var(--base) * 0.15) 0",
|
|
32
|
+
color: "var(--theme-elevation-500)"
|
|
33
|
+
},
|
|
34
|
+
snippet: {
|
|
35
|
+
margin: 0,
|
|
36
|
+
padding: "calc(var(--base) * 0.4)",
|
|
37
|
+
background: "var(--theme-elevation-50)",
|
|
38
|
+
border: "1px solid var(--theme-elevation-150)",
|
|
39
|
+
borderRadius: "3px",
|
|
40
|
+
fontFamily: "var(--font-mono)",
|
|
41
|
+
overflowX: "auto",
|
|
42
|
+
whiteSpace: "pre-wrap",
|
|
43
|
+
wordBreak: "break-all"
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Per-key connection instructions on the API key edit view. Renders nothing
|
|
48
|
+
* until the document is saved, because before that there is no key to hand to
|
|
49
|
+
* a client.
|
|
50
|
+
*/ const McpxSetupGuide = ({ endpointPath }) => {
|
|
51
|
+
const { id } = useDocumentInfo();
|
|
52
|
+
const { config } = useConfig();
|
|
53
|
+
const apiKey = useFormFields(([fields]) => fields["apiKey"]?.value);
|
|
54
|
+
const label = useFormFields(([fields]) => fields["label"]?.value);
|
|
55
|
+
const origin = useOrigin(config.serverURL);
|
|
56
|
+
if (id === void 0) return null;
|
|
57
|
+
const sections = buildSetupGuide({
|
|
58
|
+
endpointUrl: `${origin.replace(/\/+$/, "")}${config.routes.api}${endpointPath}`,
|
|
59
|
+
apiKey: asString(apiKey),
|
|
60
|
+
label: asString(label)
|
|
61
|
+
});
|
|
62
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
63
|
+
className: "field-type",
|
|
64
|
+
children: [/*#__PURE__*/ jsx("p", {
|
|
65
|
+
style: styles.lead,
|
|
66
|
+
children: "Every snippet below contains this key in full. Treat it like a password."
|
|
67
|
+
}), sections.map((section) => /*#__PURE__*/ jsxs("section", {
|
|
68
|
+
style: styles.section,
|
|
69
|
+
children: [
|
|
70
|
+
/*#__PURE__*/ jsxs("div", {
|
|
71
|
+
style: styles.sectionHeader,
|
|
72
|
+
children: [/*#__PURE__*/ jsx("strong", { children: section.title }), /*#__PURE__*/ jsx(CopyToClipboard, { value: section.snippet })]
|
|
73
|
+
}),
|
|
74
|
+
section.description ? /*#__PURE__*/ jsx("p", {
|
|
75
|
+
style: styles.description,
|
|
76
|
+
children: section.description
|
|
77
|
+
}) : null,
|
|
78
|
+
/*#__PURE__*/ jsx("pre", {
|
|
79
|
+
style: styles.snippet,
|
|
80
|
+
children: section.snippet
|
|
81
|
+
})
|
|
82
|
+
]
|
|
83
|
+
}, section.id))]
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
//#endregion
|
|
87
|
+
export { McpxSetupGuide };
|
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/options.mjs
CHANGED
|
@@ -132,6 +132,7 @@ const normalizeLimits = (limits) => {
|
|
|
132
132
|
userCollection,
|
|
133
133
|
apiKeysSlug,
|
|
134
134
|
endpointPath: options.endpoint?.path ?? DEFAULT_ENDPOINT_PATH,
|
|
135
|
+
setupGuide: options.apiKeys?.setupGuide ?? true,
|
|
135
136
|
limits: normalizeLimits(options.limits),
|
|
136
137
|
tools,
|
|
137
138
|
auth: options.auth,
|
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/dist/types.d.mts
CHANGED
|
@@ -90,6 +90,11 @@ type McpxPluginOptions = {
|
|
|
90
90
|
apiKeys?: {
|
|
91
91
|
/** Slug of the generated API key collection. Default `mcpx-api-keys`. */
|
|
92
92
|
slug?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Add a "Connect a client" tab to saved keys, holding ready-to-paste MCP
|
|
95
|
+
* client config. Default `true`. The snippets contain the key in full.
|
|
96
|
+
*/
|
|
97
|
+
setupGuide?: boolean;
|
|
93
98
|
/** Final override applied to the generated collection. */
|
|
94
99
|
overrideCollection?: (collection: CollectionConfig) => CollectionConfig;
|
|
95
100
|
};
|
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.9",
|
|
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",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
".": {
|
|
27
27
|
"types": "./dist/index.d.mts",
|
|
28
28
|
"default": "./dist/index.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./client": {
|
|
31
|
+
"types": "./dist/client/index.d.mts",
|
|
32
|
+
"default": "./dist/client/index.mjs"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"files": [
|
|
@@ -35,17 +39,17 @@
|
|
|
35
39
|
],
|
|
36
40
|
"scripts": {
|
|
37
41
|
"build": "tsdown",
|
|
38
|
-
"format:check": "prettier --check 'src/**/*.ts' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
|
|
39
|
-
"format:fix": "prettier --write 'src/**/*.ts' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
|
|
40
|
-
"lint:check": "eslint 'src/**/*.ts' 'test/**/*.ts'",
|
|
41
|
-
"lint:fix": "eslint 'src/**/*.ts' 'test/**/*.ts' --fix",
|
|
42
|
+
"format:check": "prettier --check 'src/**/*.{ts,tsx}' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
|
|
43
|
+
"format:fix": "prettier --write 'src/**/*.{ts,tsx}' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
|
|
44
|
+
"lint:check": "eslint 'src/**/*.{ts,tsx}' 'test/**/*.ts'",
|
|
45
|
+
"lint:fix": "eslint 'src/**/*.{ts,tsx}' 'test/**/*.ts' --fix",
|
|
42
46
|
"test-integration": "vitest --run --config test/integration/vitest.config.mts",
|
|
43
47
|
"test-unit": "vitest --run --coverage --config vitest.config.mts",
|
|
44
48
|
"test-unit:watch": "vitest --config vitest.config.mts",
|
|
45
49
|
"typecheck": "tsc --noEmit"
|
|
46
50
|
},
|
|
47
51
|
"lint-staged": {
|
|
48
|
-
"{src,test}/**/*.ts": [
|
|
52
|
+
"{src,test}/**/*.{ts,tsx}": [
|
|
49
53
|
"eslint --fix",
|
|
50
54
|
"prettier --write"
|
|
51
55
|
],
|
|
@@ -63,6 +67,7 @@
|
|
|
63
67
|
"@arethetypeswrong/core": "^0.18.5",
|
|
64
68
|
"@payloadcms/db-sqlite": "3.88.0",
|
|
65
69
|
"@payloadcms/richtext-lexical": "3.88.0",
|
|
70
|
+
"@payloadcms/ui": "3.88.0",
|
|
66
71
|
"@swc/core": "^1.16.1",
|
|
67
72
|
"@types/node": "^26.2.0",
|
|
68
73
|
"@types/react": "^19.2.18",
|
|
@@ -79,7 +84,17 @@
|
|
|
79
84
|
"vitest": "^4.1.10"
|
|
80
85
|
},
|
|
81
86
|
"peerDependencies": {
|
|
82
|
-
"
|
|
87
|
+
"@payloadcms/ui": ">=3.88.0 <4",
|
|
88
|
+
"payload": ">=3.88.0 <4",
|
|
89
|
+
"react": "^19"
|
|
90
|
+
},
|
|
91
|
+
"peerDependenciesMeta": {
|
|
92
|
+
"@payloadcms/ui": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"react": {
|
|
96
|
+
"optional": true
|
|
97
|
+
}
|
|
83
98
|
},
|
|
84
99
|
"publishConfig": {
|
|
85
100
|
"ghpr": true,
|