@abinnovision/payloadcms-mcpx 2.1.0-beta.1 → 2.1.0-beta.2
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 +5 -1
- package/dist/options.mjs +42 -4
- package/dist/plugin.d.mts +2 -1
- package/dist/plugin.mjs +7 -2
- package/dist/types.d.mts +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,8 @@ leave them out. The package is ESM only, like Payload.
|
|
|
30
30
|
|
|
31
31
|
## Setup
|
|
32
32
|
|
|
33
|
-
List the collections and globals the plugin may reach. Anything not listed is not exposed
|
|
33
|
+
List the collections and globals the plugin may reach. Anything not listed is not exposed, apart
|
|
34
|
+
from Payload's folders, which are readable where an exposed collection uses them (see `folders`):
|
|
34
35
|
|
|
35
36
|
```ts
|
|
36
37
|
// payload.config.ts
|
|
@@ -94,6 +95,7 @@ Claude Desktop and the MCP Inspector are covered in
|
|
|
94
95
|
| `collections.<slug>.publish` | `true` with drafts | Expose `publishDocument`. Needs `versions.drafts` and `write`. |
|
|
95
96
|
| `collections.<slug>.delete` | `false` | Expose `deleteDocument`. Needs `read`. Each delete is approved in the admin. `"unattended"` also offers trashing without approval; needs `trash`. |
|
|
96
97
|
| `globals` | `{}` | Globals to expose, with `read`, `write` and `publish`. |
|
|
98
|
+
| `folders` | read-only where used | Payload's folders, where an exposed collection uses them. `{ write: true }` adds create and rename, never delete. `false` hides them. |
|
|
97
99
|
| `userCollection` | `config.admin.user`, then `users` | Auth collection whose users the keys act as. |
|
|
98
100
|
| `apiKeys.slug` | `mcpx-api-keys` | Slug of the key collection. |
|
|
99
101
|
| `apiKeys.setupGuide` | `true` | Add the "Connect a client" button to saved keys. |
|
|
@@ -171,6 +173,8 @@ To restrict that, use `apiKeys.overrideCollection` as shown in
|
|
|
171
173
|
the REST or GraphQL API.
|
|
172
174
|
- A key acts as its user, and Payload access control applies to what its tools read and write.
|
|
173
175
|
- Relations are populated only into collections the key can read.
|
|
176
|
+
- Readable folders expose every folder name and the `folderType` options, which are the slugs of
|
|
177
|
+
the collections that use folders. They do not expose the documents in a folder.
|
|
174
178
|
- The read tools leave out fields with `admin.hidden`. `findDocuments` refuses a `where` or `sort`
|
|
175
179
|
that names one, or that goes through a relation into a collection the key cannot read.
|
|
176
180
|
- After a write, the tools re-read the document with full access to report publish blockers.
|
package/dist/options.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { hasDraftsEnabled, hasLocalizeStatusEnabled } from "payload/shared";
|
|
|
5
5
|
//#region src/options.ts
|
|
6
6
|
const DEFAULT_API_KEYS_SLUG = "mcpx-api-keys";
|
|
7
7
|
const DEFAULT_ENDPOINT_PATH = "/mcpx";
|
|
8
|
+
const DEFAULT_FOLDERS_SLUG = "payload-folders";
|
|
8
9
|
const DEFAULT_MAX_LIMIT = 25;
|
|
9
10
|
const DEFAULT_MAX_DEPTH = 1;
|
|
10
11
|
const TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*$/;
|
|
@@ -83,12 +84,13 @@ const normalizeCapabilities = (kind, config, raw) => {
|
|
|
83
84
|
};
|
|
84
85
|
};
|
|
85
86
|
const bySlug = (configs) => new Map(configs.map((config) => [config.slug, config]));
|
|
86
|
-
const
|
|
87
|
+
const foldersSlugOf = ({ folders }) => (folders === false ? void 0 : folders?.slug) ?? DEFAULT_FOLDERS_SLUG;
|
|
88
|
+
const normalizeEntities = (kind, configs, entries, apiKeysSlug, foldersSlug) => {
|
|
87
89
|
const fieldNames = /* @__PURE__ */ new Set();
|
|
88
90
|
return Object.entries(entries).flatMap(([slug, raw]) => {
|
|
89
91
|
if (raw === void 0) return [];
|
|
90
92
|
const config = configs.get(slug);
|
|
91
|
-
if (!config) return fail(`Exposed ${kind.toLowerCase()} "${slug}" does not exist.`);
|
|
93
|
+
if (!config) return fail(slug === foldersSlug ? `Collection "${slug}" is Payload's folder collection. Use the folders option instead.` : slug.startsWith("payload-") ? `${kind} "${slug}" is internal to Payload and cannot be exposed.` : `Exposed ${kind.toLowerCase()} "${slug}" does not exist.`);
|
|
92
94
|
assertExposable(kind, config, apiKeysSlug);
|
|
93
95
|
const normalized = {
|
|
94
96
|
slug,
|
|
@@ -105,6 +107,40 @@ const normalizeEntities = (kind, configs, entries, apiKeysSlug) => {
|
|
|
105
107
|
return [normalized];
|
|
106
108
|
});
|
|
107
109
|
};
|
|
110
|
+
const normalizeFolders = (config, collections, raw, slug) => {
|
|
111
|
+
const usesFolders = (config.collections ?? []).some((collection) => Boolean(collection.folders) && collections.some((entity) => entity.slug === collection.slug));
|
|
112
|
+
if (raw === false) return [];
|
|
113
|
+
if (config.folders === false || !usesFolders) {
|
|
114
|
+
if (raw !== void 0) fail("folders is set, but no exposed collection uses folders. Remove the option.");
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
if (raw !== void 0 && !isPlainObject(raw)) fail(`folders has ${JSON.stringify(raw)}. Use false or { write }.`);
|
|
118
|
+
const settings = isPlainObject(raw) ? raw : {};
|
|
119
|
+
const unknownKey = Object.keys(settings).find((key) => key !== "write");
|
|
120
|
+
if (unknownKey !== void 0) fail(`folders has the unknown option "${unknownKey}". The only option is write.`);
|
|
121
|
+
const fieldName = toCamelCase(slug);
|
|
122
|
+
if (collections.some((entity) => entity.fieldName === fieldName)) fail(`folders maps to capability field "${fieldName}", which an exposed collection already uses.`);
|
|
123
|
+
const write = settings["write"] ?? false;
|
|
124
|
+
if (typeof write !== "boolean") fail(`folders.write has ${JSON.stringify(write)}. Use true or false.`);
|
|
125
|
+
return [{
|
|
126
|
+
slug,
|
|
127
|
+
...normalizeCapabilities("Collection", {
|
|
128
|
+
slug,
|
|
129
|
+
fields: []
|
|
130
|
+
}, { write }),
|
|
131
|
+
isUpload: false,
|
|
132
|
+
fieldName
|
|
133
|
+
}];
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Checks the sanitized folder collection against what {@link normalizeFolders}
|
|
137
|
+
* assumed. `folders.collectionOverrides` can change it after this plugin ran.
|
|
138
|
+
*/ const assertFolderCollection = (config, options) => {
|
|
139
|
+
const { folders } = config;
|
|
140
|
+
if (!folders || !options.collections.some((entity) => entity.slug === folders.slug)) return;
|
|
141
|
+
const collection = config.collections.find((candidate) => candidate.slug === folders.slug);
|
|
142
|
+
if (collection?.auth || collection?.upload || collection?.versions) fail(`Folder collection "${folders.slug}" has auth, upload or versions from folders.collectionOverrides, which mcpx does not support. Set folders: false.`);
|
|
143
|
+
};
|
|
108
144
|
const assertUserCollection = (collections, slug) => {
|
|
109
145
|
const collection = collections.get(slug);
|
|
110
146
|
if (!collection) fail(`User collection "${slug}" does not exist.`);
|
|
@@ -136,11 +172,13 @@ const normalizeLimits = (limits) => {
|
|
|
136
172
|
const apiKeysSlug = options.apiKeys?.slug ?? DEFAULT_API_KEYS_SLUG;
|
|
137
173
|
const userCollection = options.userCollection ?? config.admin?.user ?? "users";
|
|
138
174
|
const collectionConfigs = bySlug(config.collections ?? []);
|
|
175
|
+
const foldersSlug = foldersSlugOf(config);
|
|
139
176
|
if (collectionConfigs.has(apiKeysSlug)) fail(`API key collection slug "${apiKeysSlug}" is already taken.`);
|
|
140
177
|
assertUserCollection(collectionConfigs, userCollection);
|
|
141
178
|
const tools = options.tools ?? [];
|
|
142
179
|
assertTools(tools, reservedToolNames);
|
|
143
|
-
const
|
|
180
|
+
const exposed = normalizeEntities("Collection", collectionConfigs, options.collections, apiKeysSlug, foldersSlug);
|
|
181
|
+
const collections = [...exposed, ...normalizeFolders(config, exposed, options.folders, foldersSlug)];
|
|
144
182
|
const confirmations = collections.some((entity) => entity.delete);
|
|
145
183
|
if (confirmations && options.auth?.resolve) fail("delete needs approval on the API key document, which a custom auth.resolve does not support. Remove delete: true or auth.resolve.");
|
|
146
184
|
return {
|
|
@@ -162,4 +200,4 @@ const normalizeLimits = (limits) => {
|
|
|
162
200
|
};
|
|
163
201
|
};
|
|
164
202
|
//#endregion
|
|
165
|
-
export { normalizeOptions, toCamelCase };
|
|
203
|
+
export { assertFolderCollection, normalizeOptions, toCamelCase };
|
package/dist/plugin.d.mts
CHANGED
|
@@ -3,7 +3,8 @@ import { McpxPluginOptions } from "./types.mjs";
|
|
|
3
3
|
/**
|
|
4
4
|
* Mounts the MCP, upload and download endpoints, and the confirmation
|
|
5
5
|
* endpoints where a collection exposes `delete`. Adds the API key collection
|
|
6
|
-
* and installs the draft guard on every collection and global.
|
|
6
|
+
* and installs the draft guard on every collection and global. Checks the
|
|
7
|
+
* folder collection once Payload has built it.
|
|
7
8
|
*/
|
|
8
9
|
export declare const mcpxPlugin: (options: McpxPluginOptions) => import("payload").Plugin;
|
|
9
10
|
//#endregion
|
package/dist/plugin.mjs
CHANGED
|
@@ -3,14 +3,15 @@ import { BUILTIN_TOOLS } from "./tools/builtin.mjs";
|
|
|
3
3
|
import { confirmationEndpoints } from "./endpoint/confirmations.mjs";
|
|
4
4
|
import { createMcpxHandler, methodNotAllowed } from "./endpoint/handler.mjs";
|
|
5
5
|
import { createDownloadHandler, createUploadHandler } from "./endpoint/upload.mjs";
|
|
6
|
-
import { normalizeOptions } from "./options.mjs";
|
|
6
|
+
import { assertFolderCollection, normalizeOptions } from "./options.mjs";
|
|
7
7
|
import { installDraftGuards, installGlobalDraftGuards } from "./write/draft-guard.mjs";
|
|
8
8
|
import { definePlugin } from "payload";
|
|
9
9
|
//#region src/plugin.ts
|
|
10
10
|
/**
|
|
11
11
|
* Mounts the MCP, upload and download endpoints, and the confirmation
|
|
12
12
|
* endpoints where a collection exposes `delete`. Adds the API key collection
|
|
13
|
-
* and installs the draft guard on every collection and global.
|
|
13
|
+
* and installs the draft guard on every collection and global. Checks the
|
|
14
|
+
* folder collection once Payload has built it.
|
|
14
15
|
*/ const mcpxPlugin = definePlugin({
|
|
15
16
|
slug: "@abinnovision/payloadcms-mcpx",
|
|
16
17
|
order: 100,
|
|
@@ -22,6 +23,10 @@ import { definePlugin } from "payload";
|
|
|
22
23
|
...config,
|
|
23
24
|
collections: installDraftGuards([...config.collections ?? [], apiKeysCollection]),
|
|
24
25
|
globals: installGlobalDraftGuards(config.globals ?? []),
|
|
26
|
+
onInit: async (payload) => {
|
|
27
|
+
assertFolderCollection(payload.config, normalized);
|
|
28
|
+
await config.onInit?.(payload);
|
|
29
|
+
},
|
|
25
30
|
endpoints: [
|
|
26
31
|
...config.endpoints ?? [],
|
|
27
32
|
{
|
package/dist/types.d.mts
CHANGED
|
@@ -274,6 +274,15 @@ export type McpxPluginOptions = {
|
|
|
274
274
|
* Allow-list of globals. `true` is shorthand for `{}`.
|
|
275
275
|
*/
|
|
276
276
|
globals?: Partial<Record<GlobalSlug, McpxGlobalOptions | true>>;
|
|
277
|
+
/**
|
|
278
|
+
* Payload's folder collection, exposed read-only by default where an exposed
|
|
279
|
+
* collection has `folders: true`. `{ write: true }` also lets keys create and
|
|
280
|
+
* rename folders; folders are never deleted. `false` hides them. An object
|
|
281
|
+
* is refused where no exposed collection uses folders.
|
|
282
|
+
*/
|
|
283
|
+
folders?: false | {
|
|
284
|
+
write?: boolean;
|
|
285
|
+
};
|
|
277
286
|
/**
|
|
278
287
|
* Collection the keys act as. Default `config.admin.user`, then `users`.
|
|
279
288
|
*/
|
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": "2.1.0-beta.
|
|
4
|
+
"version": "2.1.0-beta.2",
|
|
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",
|