@abinnovision/payloadcms-mcpx 1.1.0-beta.5 → 2.0.0-beta.7
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 +141 -448
- package/dist/{client → admin}/capability-matrix.d.mts +3 -4
- package/dist/admin/capability-matrix.mjs +354 -0
- package/dist/admin/capability-toggles.mjs +73 -0
- package/dist/{client → admin}/setup-guide.d.mts +1 -1
- package/dist/{client → admin}/setup-guide.mjs +3 -10
- package/dist/api-keys/capability-matrix.d.mts +6 -6
- package/dist/api-keys/capability-matrix.mjs +15 -85
- package/dist/api-keys/fields.mjs +15 -24
- package/dist/auth/resolve.mjs +23 -5
- package/dist/{tools/names.mjs → builtin-tool-names.mjs} +2 -1
- package/dist/capabilities.mjs +34 -42
- package/dist/define-tool.d.mts +19 -0
- package/dist/{types.mjs → define-tool.mjs} +1 -1
- package/dist/endpoint/errors.mjs +5 -6
- package/dist/endpoint/handler.mjs +49 -19
- package/dist/endpoint/server.mjs +65 -19
- package/dist/entity.d.mts +9 -0
- package/dist/guards.mjs +5 -0
- package/dist/i18n.mjs +8 -8
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +3 -2
- package/dist/options.mjs +51 -43
- package/dist/plugin.mjs +0 -1
- package/dist/request.d.mts +2 -3
- package/dist/request.mjs +2 -3
- package/dist/result.d.mts +2 -3
- package/dist/result.mjs +3 -3
- package/dist/schema/admin-hidden.mjs +91 -0
- package/dist/schema/describe.mjs +17 -33
- package/dist/schema/errors.mjs +8 -0
- package/dist/schema/index.mjs +5 -2
- package/dist/schema/lexical-pointer.mjs +18 -20
- package/dist/schema/lexical.mjs +39 -92
- package/dist/schema/outline.mjs +5 -15
- package/dist/schema/path.mjs +22 -0
- package/dist/schema/pointer.mjs +18 -25
- package/dist/schema/shape.mjs +25 -71
- package/dist/schema/walk.mjs +35 -45
- package/dist/tools/builtin.mjs +6 -4
- package/dist/tools/create-document.mjs +26 -23
- package/dist/tools/describe-schema.mjs +38 -34
- package/dist/tools/document.mjs +84 -0
- package/dist/tools/find-documents.mjs +32 -19
- package/dist/tools/find-versions.mjs +74 -0
- package/dist/tools/get-document.mjs +80 -49
- package/dist/tools/list-capabilities.mjs +25 -16
- package/dist/tools/patch-document.mjs +74 -50
- package/dist/tools/publish-document.mjs +17 -28
- package/dist/tools/query-paths.mjs +55 -0
- package/dist/tools/read-request.d.mts +10 -0
- package/dist/tools/read-request.mjs +43 -0
- package/dist/tools/shared.mjs +65 -96
- package/dist/tools/validate-document.mjs +16 -29
- package/dist/tools/versions.mjs +127 -0
- package/dist/types.d.mts +55 -55
- package/dist/write/draft-guard.mjs +17 -37
- package/dist/write/patch.mjs +27 -183
- package/dist/write/publish-blockers.mjs +7 -14
- package/dist/write/publish-intent.mjs +4 -4
- package/dist/write/row-ids.mjs +44 -0
- package/dist/write/transaction.mjs +5 -8
- package/dist/write/write-data.mjs +64 -0
- package/package.json +4 -4
- package/dist/client/capability-matrix.mjs +0 -349
- package/dist/endpoint/index.mjs +0 -4
- package/dist/tools/target.mjs +0 -45
- /package/dist/{client → admin}/index.d.mts +0 -0
- /package/dist/{client → admin}/index.mjs +0 -0
package/README.md
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
1
|
# @abinnovision/payloadcms-mcpx
|
|
2
2
|
|
|
3
|
-

|
|
4
|
+
|
|
5
|
+
A [Payload CMS](https://payloadcms.com/) plugin that serves an MCP (Model Context Protocol) server
|
|
6
|
+
on one `POST` endpoint. Clients authenticate with API keys that users create in the admin panel.
|
|
7
|
+
|
|
8
|
+
The server has nine tools plus any you add, and that number does not grow with the content model.
|
|
9
|
+
A client reads the shape of one collection, block or rich text node at a time through
|
|
10
|
+
`describeSchema`, and writes with RFC 6902 JSON Patch operations that the plugin checks against
|
|
11
|
+
the real config before anything is saved. Writes land as drafts unless the config allows more,
|
|
12
|
+
and each write reports what still blocks publishing.
|
|
13
|
+
[`docs/concepts.md`](./docs/concepts.md) explains the model, and
|
|
14
|
+
[`docs/security.md`](./docs/security.md) states what a key can reach.
|
|
19
15
|
|
|
20
16
|
## Install
|
|
21
17
|
|
|
22
|
-
```
|
|
18
|
+
```sh
|
|
23
19
|
yarn add @abinnovision/payloadcms-mcpx
|
|
24
20
|
```
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
| Peer | Range | Needed for |
|
|
23
|
+
| ---------------- | ------------- | -------------------------------------------------------------- |
|
|
24
|
+
| `payload` | `>=3.88.0 <4` | everything (required) |
|
|
25
|
+
| `@payloadcms/ui` | `>=3.88.0 <4` | the admin components: capability matrix and "Connect a client" |
|
|
26
|
+
| `react` | `^19` | the admin components |
|
|
31
27
|
|
|
32
|
-
|
|
28
|
+
`@payloadcms/ui` and `react` are optional. A deployment that does not serve the admin panel can
|
|
29
|
+
leave them out. The package is ESM only, like Payload.
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
List the collections and globals the plugin may reach. Anything not listed is not exposed:
|
|
36
34
|
|
|
37
35
|
```ts
|
|
36
|
+
// payload.config.ts
|
|
38
37
|
import { mcpxPlugin } from "@abinnovision/payloadcms-mcpx";
|
|
39
38
|
import { buildConfig } from "payload";
|
|
40
39
|
|
|
@@ -42,15 +41,15 @@ export default buildConfig({
|
|
|
42
41
|
// ...
|
|
43
42
|
plugins: [
|
|
44
43
|
mcpxPlugin({
|
|
44
|
+
// pages and posts have versions.drafts enabled
|
|
45
45
|
collections: {
|
|
46
|
-
pages:
|
|
47
|
-
posts: {
|
|
48
|
-
tags:
|
|
46
|
+
pages: true, // everything the collection supports
|
|
47
|
+
posts: { publish: false }, // everything except publishing
|
|
48
|
+
tags: { write: false }, // read only
|
|
49
49
|
},
|
|
50
50
|
globals: {
|
|
51
|
-
"site-settings":
|
|
51
|
+
"site-settings": true,
|
|
52
52
|
},
|
|
53
|
-
limits: { maxLimit: 25, maxDepth: 1 },
|
|
54
53
|
}),
|
|
55
54
|
],
|
|
56
55
|
});
|
|
@@ -58,438 +57,132 @@ export default buildConfig({
|
|
|
58
57
|
|
|
59
58
|
The plugin adds:
|
|
60
59
|
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
and their capability checkboxes,
|
|
65
|
-
- a draft guard on every collection and global, so any write carrying the MCP
|
|
66
|
-
request marker lands as a draft, custom tools included.
|
|
67
|
-
|
|
68
|
-
Create a key in the admin panel under MCP > API Keys, tick the capabilities it
|
|
69
|
-
should have, and copy the plaintext key shown after saving. Checkboxes default
|
|
70
|
-
to off, so a fresh key can do nothing until you say otherwise. See
|
|
71
|
-
[API keys](#api-keys) for what a key is and is not.
|
|
60
|
+
- `POST /api/mcpx`, an MCP endpoint over streamable HTTP. It is stateless and answers with JSON.
|
|
61
|
+
- An `mcpx-api-keys` collection in the admin group "MCP", holding the keys.
|
|
62
|
+
- A draft guard on every collection and global that turns MCP writes into draft saves.
|
|
72
63
|
|
|
73
|
-
|
|
64
|
+
The key form uses two admin components, so regenerate the import map:
|
|
74
65
|
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
# transport: Streamable HTTP, URL: http://localhost:3000/api/mcpx
|
|
78
|
-
# header: Authorization: Bearer <key>
|
|
66
|
+
```sh
|
|
67
|
+
payload generate:importmap
|
|
79
68
|
```
|
|
80
69
|
|
|
81
|
-
|
|
70
|
+
Without it, no key can be granted anything from the admin panel. The endpoint is unaffected.
|
|
71
|
+
|
|
72
|
+
Then create a key in the admin panel under MCP > API Keys, tick what it may do, save, and copy
|
|
73
|
+
the key from the saved document. Every checkbox starts unticked, so a new key can only call
|
|
74
|
+
`listCapabilities`.
|
|
75
|
+
|
|
76
|
+
Connect a client with the key as a bearer token. Claude Code:
|
|
82
77
|
|
|
83
|
-
```
|
|
78
|
+
```sh
|
|
84
79
|
claude mcp add --transport http payload http://localhost:3000/api/mcpx \
|
|
85
|
-
|
|
80
|
+
--header "Authorization: Bearer <key>"
|
|
86
81
|
```
|
|
87
82
|
|
|
88
|
-
Claude Desktop
|
|
89
|
-
`
|
|
90
|
-
|
|
91
|
-
```json
|
|
92
|
-
{
|
|
93
|
-
"mcpServers": {
|
|
94
|
-
"payload": {
|
|
95
|
-
"command": "npx",
|
|
96
|
-
"args": [
|
|
97
|
-
"-y",
|
|
98
|
-
"mcp-remote",
|
|
99
|
-
"http://localhost:3000/api/mcpx",
|
|
100
|
-
"--header",
|
|
101
|
-
"Authorization: Bearer <key>"
|
|
102
|
-
]
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
```
|
|
83
|
+
Claude Desktop and the MCP Inspector are covered in
|
|
84
|
+
[`docs/integration.md`](./docs/integration.md#connecting-other-clients). Saved keys also have a
|
|
85
|
+
"Connect a client" tab with ready-to-paste snippets.
|
|
107
86
|
|
|
108
87
|
## Options
|
|
109
88
|
|
|
110
|
-
| Option
|
|
111
|
-
|
|
|
112
|
-
| `collections`
|
|
113
|
-
| `collections.<slug>.read`
|
|
114
|
-
| `collections.<slug>.write`
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `globals
|
|
118
|
-
| `userCollection`
|
|
119
|
-
| `apiKeys.slug`
|
|
120
|
-
| `apiKeys.setupGuide`
|
|
121
|
-
| `apiKeys.overrideCollection` |
|
|
122
|
-
| `endpoint.path`
|
|
123
|
-
| `limits.maxLimit`
|
|
124
|
-
| `limits.maxDepth`
|
|
125
|
-
| `tools`
|
|
126
|
-
| `auth.resolve`
|
|
127
|
-
| `serverInfo`
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
| `"draft"` | writes land as drafts, nothing is ever published | refused at startup: there is no draft to write |
|
|
137
|
-
| `"live"` | writes land as drafts, and `publishDocument` is exposed | writes land on the live document |
|
|
138
|
-
|
|
139
|
-
`"live"` is the only way an MCP write reaches live content, whichever of the two
|
|
140
|
-
shapes it takes. Wherever it is set, the server instructions and the
|
|
141
|
-
`patchDocument` and `createDocument` descriptions name those slugs for the key in
|
|
142
|
-
question, so a client is never told its writes are drafts while they are not.
|
|
143
|
-
|
|
144
|
-
### Upload collections
|
|
145
|
-
|
|
146
|
-
An upload collection may be exposed for write. `patchDocument` and
|
|
147
|
-
`validateDocument` reach it, and `publishDocument` under the same `write:
|
|
148
|
-
"live"` rule as anywhere else, so an agent can edit the fields the collection
|
|
149
|
-
declares itself, such as `alt` or a credit.
|
|
150
|
-
|
|
151
|
-
Its base fields (`filename`, `url`, `filesize`, `sizes`, the focal point) are
|
|
152
|
-
neither described nor writable. `createDocument` leaves the slug out of its
|
|
153
|
-
`collection` enum and says why in its description: a create there would have to
|
|
154
|
-
carry the file, and no tool does. Upload the file in the admin panel first.
|
|
155
|
-
|
|
156
|
-
### Startup validation
|
|
157
|
-
|
|
158
|
-
Misconfiguration fails at startup with `InvalidConfiguration`: unknown slugs,
|
|
159
|
-
`write: "draft"` on a collection without drafts, `write` on a collection with
|
|
160
|
-
`timestamps: false`, tool name collisions, and `write: "live"` on an entity
|
|
161
|
-
using `versions.drafts.localizeStatus`, which is not supported yet.
|
|
162
|
-
|
|
163
|
-
Auth collections cannot be exposed at all, read included. Their documents carry
|
|
164
|
-
credentials, such as the decrypted Payload API key of every user.
|
|
89
|
+
| Option | Default | Description |
|
|
90
|
+
| ----------------------------- | ----------------------------------------- | ------------------------------------------------------------------- |
|
|
91
|
+
| `collections` | required | Collections to expose. `true` means `{}`, everything it supports. |
|
|
92
|
+
| `collections.<slug>.read` | `true` | Expose the read tools for this collection. |
|
|
93
|
+
| `collections.<slug>.write` | `true` | Expose the write tools. Without drafts, a write goes live. |
|
|
94
|
+
| `collections.<slug>.publish` | `true` with drafts | Expose `publishDocument`. Needs `versions.drafts` and `write`. |
|
|
95
|
+
| `collections.<slug>.versions` | `false` | Expose version history. Needs Payload `versions` on the entity. |
|
|
96
|
+
| `globals` | `{}` | Globals to expose, with the same four options. |
|
|
97
|
+
| `userCollection` | `config.admin.user`, then `users` | Auth collection whose users the keys act as. |
|
|
98
|
+
| `apiKeys.slug` | `mcpx-api-keys` | Slug of the key collection. |
|
|
99
|
+
| `apiKeys.setupGuide` | `true` | Add the "Connect a client" tab to saved keys. |
|
|
100
|
+
| `apiKeys.overrideCollection` | none | Function that receives the key collection and returns it. |
|
|
101
|
+
| `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
|
|
102
|
+
| `limits.maxLimit` | `25` | Highest `limit` a client may pass to a list tool. |
|
|
103
|
+
| `limits.maxDepth` | `1` | Highest `depth` a client may pass to a read tool. |
|
|
104
|
+
| `tools` | `[]` | Custom tools, see [`docs/custom-tools.md`](./docs/custom-tools.md). |
|
|
105
|
+
| `auth.resolve` | none | Replace or wrap the key lookup. |
|
|
106
|
+
| `serverInfo` | `payloadcms-mcpx` and the package version | `{ name, version }` reported to clients. |
|
|
107
|
+
|
|
108
|
+
The config only takes capabilities away: `true` exposes everything the entity supports, and a key's
|
|
109
|
+
checkboxes decide what each key may do. `publish: false` keeps MCP writes as drafts. On an entity
|
|
110
|
+
without drafts, a write changes the live document. `versions` defaults to `false`: version history
|
|
111
|
+
has no checkbox of its own and follows the key's read, so the config opts in. Read
|
|
112
|
+
[version history](./docs/security.md#version-history) before turning it on.
|
|
113
|
+
Mistakes in the options fail at startup. [`docs/integration.md`](./docs/integration.md) lists
|
|
114
|
+
every check.
|
|
165
115
|
|
|
166
116
|
## Tools
|
|
167
117
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
|
174
|
-
|
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
and
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
-
|
|
229
|
-
|
|
230
|
-
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
A `richText` field lists the Lexical node types it accepts in `nodes`, and
|
|
237
|
-
`next` carries a path for every node type that holds fields of its own:
|
|
238
|
-
`/content/link` for a link node, `/content/block/callout` and
|
|
239
|
-
`/content/inlineBlock/badge` for the block nodes. Descending returns the real
|
|
240
|
-
field list. `upload` nodes are the exception, since their fields depend on the
|
|
241
|
-
collection the node points at, so they are not addressable.
|
|
242
|
-
|
|
243
|
-
A field also reports `nodeOptions`, the node properties its editor narrows. An
|
|
244
|
-
editor built with `HeadingFeature({ enabledHeadingSizes: ["h4"] })` answers
|
|
245
|
-
`{ "heading": { "tag": ["h4"] } }`, and a write carrying any other heading tag is
|
|
246
|
-
refused. Lexical stores whatever tag it is given, so this is the only place the
|
|
247
|
-
restriction is checked.
|
|
248
|
-
|
|
249
|
-
A node must be written the way Lexical serializes it, carrying the values
|
|
250
|
-
Lexical would have written. Payload does not check that on write, so this plugin
|
|
251
|
-
does, and the refusal names the property and what belongs there. A
|
|
252
|
-
`describeSchema` response that reached a `richText` field ends with a
|
|
253
|
-
`nodeProperties` entry stating what each node type has to carry, keyed by node
|
|
254
|
-
type and in the same words the refusal uses. Its `text` entry reads:
|
|
255
|
-
|
|
256
|
-
```json
|
|
257
|
-
{
|
|
258
|
-
"detail": "a number",
|
|
259
|
-
"format": "a number",
|
|
260
|
-
"mode": "a string",
|
|
261
|
-
"style": "a string",
|
|
262
|
-
"text": "a string",
|
|
263
|
-
"type": "a string",
|
|
264
|
-
"version": "a number"
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
A field's value is addressable, so a small edit does not have to rewrite the
|
|
269
|
-
whole state. `/content/root/children/2` is a node,
|
|
270
|
-
`/content/root/children/2/tag` one of its properties, and
|
|
271
|
-
`/content/root/children/2/fields/url` a field the node carries. The root and a
|
|
272
|
-
node's `type` cannot be replaced on their own, and a node property cannot be
|
|
273
|
-
removed. A state whose root holds nothing is refused however it is written,
|
|
274
|
-
since Lexical reads it as empty and throws rather than rendering it; an empty
|
|
275
|
-
field is stored as null instead.
|
|
276
|
-
|
|
277
|
-
Node positions shift the moment anything is added or removed, and a text or
|
|
278
|
-
paragraph node carries no id to fall back on. `getDocument` with `outline`
|
|
279
|
-
answers with one line per node, its pointer, its `version` and an excerpt, so a
|
|
280
|
-
position can be chosen without holding the whole state. `expectedUpdatedAt`
|
|
281
|
-
still guards the document, and a `test` operation on a node's `type` guards the
|
|
282
|
-
position.
|
|
283
|
-
|
|
284
|
-
## Globals
|
|
285
|
-
|
|
286
|
-
A global is exposed the same way a collection is, and reached through the same
|
|
287
|
-
tools rather than tools of its own:
|
|
288
|
-
|
|
289
|
-
```ts
|
|
290
|
-
mcpxPlugin({
|
|
291
|
-
collections: { pages: { read: true, write: "draft" } },
|
|
292
|
-
globals: { "site-settings": { read: true, write: "draft" } },
|
|
293
|
-
});
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
Two rules follow from a global being a singleton. JSON Schema cannot state
|
|
297
|
-
either one, so both are enforced in the handler and repeated in every affected
|
|
298
|
-
tool description:
|
|
299
|
-
|
|
300
|
-
- Pass exactly **one** of `collection` and `global`.
|
|
301
|
-
- `id` is required with `collection` and must be omitted with `global`.
|
|
302
|
-
|
|
303
|
-
Refusals name the offending argument and the slug, so one failed call teaches
|
|
304
|
-
the rule. `findDocuments` and `createDocument` stay collection-only, since there
|
|
305
|
-
is nothing to list and nothing to create when the document always exists. They
|
|
306
|
-
reject a `global` argument by name.
|
|
307
|
-
|
|
308
|
-
Globals get their own `capabilities.globals.<name>` checkbox group, a separate
|
|
309
|
-
namespace from `capabilities.collections.<name>`, so a global may share a
|
|
310
|
-
camelCase name with a collection.
|
|
311
|
-
|
|
312
|
-
`expectedUpdatedAt` behaves as it does for collections, since Payload appends
|
|
313
|
-
`updatedAt` to every global. The exception is a global that has never been
|
|
314
|
-
saved: it has no `updatedAt` to compare against, so the first write must omit
|
|
315
|
-
`expectedUpdatedAt`, and supplying one is refused as a concurrency failure.
|
|
316
|
-
|
|
317
|
-
## API keys
|
|
318
|
-
|
|
319
|
-
Keys are created in the admin panel under MCP > API Keys. The plaintext key is
|
|
320
|
-
generated on create, stored encrypted with an HMAC index for lookup, and shown
|
|
321
|
-
to anyone who may read the key document (own keys only, by default). Each key:
|
|
322
|
-
|
|
323
|
-
- is bound to the user who created it and acts as that user. Every operation
|
|
324
|
-
runs with `req.user` set to the linked user and `overrideAccess: false`, so
|
|
325
|
-
your collection access control applies unchanged;
|
|
326
|
-
- carries one checkbox per exposed collection and operation, plus one per custom
|
|
327
|
-
tool. All checkboxes default to off. A key can never enable an operation the
|
|
328
|
-
plugin config does not expose, and keys created before a capability existed
|
|
329
|
-
stay without it. The `publish` checkbox only exists where a versioned entity
|
|
330
|
-
is configured `write: "live"`, and it counts only alongside `write`, since
|
|
331
|
-
publishing is an extension of writing.
|
|
332
|
-
|
|
333
|
-
### The capabilities matrix
|
|
334
|
-
|
|
335
|
-
The checkboxes render as one table per namespace: a row per collection, global
|
|
336
|
-
or tool, a column per operation, and a bulk toggle in each column header that
|
|
337
|
-
grants or clears the whole column at once. Clicking a row's name does the same
|
|
338
|
-
for that entity.
|
|
339
|
-
|
|
340
|
-
A cell the plugin config does not expose shows a dash instead of an empty box,
|
|
341
|
-
so a `write: false` collection reads as a refusal by config rather than a
|
|
342
|
-
capability someone forgot to tick. A column no row exposes is left out
|
|
343
|
-
altogether. Ticking `publish` ticks `write` with it, and clearing `write` clears
|
|
344
|
-
`publish`, because a publish without a write resolves to nothing.
|
|
345
|
-
|
|
346
|
-
Each operation is explained on its column header rather than beside every
|
|
347
|
-
checkbox, so the same sentence is never repeated down the table.
|
|
348
|
-
|
|
349
|
-
The matrix renders an admin component, so it has to be in the import map; see
|
|
350
|
-
[Admin components](#admin-components).
|
|
351
|
-
|
|
352
|
-
Keys authenticate only the MCP endpoint. They are deliberately not a Payload
|
|
353
|
-
auth strategy, so a key can never authenticate the REST or GraphQL API. The
|
|
354
|
-
reverse also holds: an admin session or JWT is ignored by the MCP endpoint.
|
|
355
|
-
|
|
356
|
-
Use `apiKeys.overrideCollection` to widen access (for example, admins manage all
|
|
357
|
-
keys) or add fields.
|
|
358
|
-
|
|
359
|
-
### The "Connect a client" tab
|
|
360
|
-
|
|
361
|
-
Saved keys carry a **Connect a client** tab in the admin holding the client
|
|
362
|
-
snippets from [Usage](#usage) with their own URL and key filled in, each block
|
|
363
|
-
behind a copy button. The tab only exists once the key does, so the create form
|
|
364
|
-
stays free of it. Turn it off with `apiKeys.setupGuide: false`, which also drops
|
|
365
|
-
the tabs and restores the flat form.
|
|
366
|
-
|
|
367
|
-
The URL comes from `serverURL` when the config sets one and from the browser's
|
|
368
|
-
origin otherwise.
|
|
369
|
-
|
|
370
|
-
### Admin components
|
|
371
|
-
|
|
372
|
-
The key form renders two components of its own, the capabilities matrix and the
|
|
373
|
-
setup guide, so both have to be in the import map:
|
|
374
|
-
|
|
375
|
-
```bash
|
|
376
|
-
payload generate:importmap
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
Without those entries Payload logs a missing-component error and renders nothing
|
|
380
|
-
in their place. For the setup guide that costs a tab; for the matrix it costs
|
|
381
|
-
the whole capabilities editor, leaving no way to grant a key anything from the
|
|
382
|
-
admin. The rest of the plugin is unaffected either way.
|
|
383
|
-
|
|
384
|
-
## Drafts and publishing
|
|
385
|
-
|
|
386
|
-
Every MCP write lands as a draft. That is enforced on the Payload operation
|
|
387
|
-
rather than in the tool handlers, through a `beforeOperation` hook that forces
|
|
388
|
-
`draft: true` and a `beforeChange` hook that refuses any write which would still
|
|
389
|
-
not land as a draft. Both are installed on every collection and global, so a
|
|
390
|
-
custom tool writing through the same request is covered as well.
|
|
391
|
-
|
|
392
|
-
`publishDocument` is the one way through. It refuses a document that fails
|
|
393
|
-
validation, and is refused while a human holds the document open in the admin
|
|
394
|
-
panel. Publishing covers the whole document, as the admin Publish button does,
|
|
395
|
-
but Payload only validates the locale the publish runs in, so a required field
|
|
396
|
-
left empty in another locale goes live empty. That is Payload's behaviour, not
|
|
397
|
-
something this plugin adds. There is no unpublish tool: reverting a published
|
|
398
|
-
document to a draft stays a human action.
|
|
399
|
-
|
|
400
|
-
Publish blockers are advisory. Payload skips validation on draft saves (unless
|
|
401
|
-
`versions.drafts.validate` is set), so after every write the plugin re-runs
|
|
402
|
-
Payload's own field validation over the saved draft and returns the failures as
|
|
403
|
-
`publishBlockers` with paths and labels. The write stands; the client gets a
|
|
404
|
-
checklist of what remains. Collections with `versions.drafts.validate: true`
|
|
405
|
-
refuse invalid drafts outright, and those failures come back as
|
|
406
|
-
`validationErrors` instead. Both carry pointers.
|
|
407
|
-
|
|
408
|
-
`publishBlockersUnavailable` marks a check that could not complete, which is a
|
|
409
|
-
different answer from a document with nothing wrong with it. Writes also report
|
|
410
|
-
`notApplied`: pointers whose value Payload kept unchanged, which happens when
|
|
411
|
-
field-level access denies the update.
|
|
412
|
-
|
|
413
|
-
Three limits apply to that check. Only the written locale is validated. Field
|
|
414
|
-
`beforeChange` hooks run again during it, so they must be pure. And it runs
|
|
415
|
-
privileged, so blocker paths and messages may name fields the key's user cannot
|
|
416
|
-
read, though values are never included. `validateDocument` runs the same
|
|
417
|
-
traversal without saving anything, which is why it carries no `readOnlyHint`.
|
|
418
|
-
|
|
419
|
-
## Custom tools
|
|
420
|
-
|
|
421
|
-
```ts
|
|
422
|
-
import { defineMcpxTool } from "@abinnovision/payloadcms-mcpx";
|
|
423
|
-
import { z } from "zod";
|
|
424
|
-
|
|
425
|
-
const publishQueue = defineMcpxTool({
|
|
426
|
-
name: "queueForReview",
|
|
427
|
-
description: "Marks a page as ready for editorial review.",
|
|
428
|
-
inputSchema: { id: z.string() },
|
|
429
|
-
handler: async ({ args, req }) => {
|
|
430
|
-
// req.user is the key's linked user, req.context.mcpx carries the
|
|
431
|
-
// key id and capabilities. Writes through payload.update({ req })
|
|
432
|
-
// land as drafts like every other MCP write.
|
|
433
|
-
await req.payload.update({
|
|
434
|
-
collection: "pages",
|
|
435
|
-
id: args.id,
|
|
436
|
-
data: { reviewRequested: true },
|
|
437
|
-
overrideAccess: false,
|
|
438
|
-
req,
|
|
439
|
-
});
|
|
440
|
-
return { content: [{ type: "text", text: "queued" }] };
|
|
441
|
-
},
|
|
442
|
-
});
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
Custom tools take the same route as the builtins: one `McpxTool` shape, one
|
|
446
|
-
registration loop. Each gets its own checkbox on every API key, default off.
|
|
447
|
-
|
|
448
|
-
`handler` receives `scope` alongside `args`, `req` and `extra`. The scope
|
|
449
|
-
carries what the key may touch (`readable`, `writable`, `publishable`,
|
|
450
|
-
`readableGlobals`, `writableGlobals`, `publishableGlobals`), the configured
|
|
451
|
-
locales, the limits in force and the exposed collections and globals. `req` is
|
|
452
|
-
shorthand for `scope.req`.
|
|
453
|
-
|
|
454
|
-
`inputSchema` may be a function of that scope instead of a fixed shape, which is
|
|
455
|
-
how a tool narrows an enum to what the key may read:
|
|
456
|
-
|
|
457
|
-
```ts
|
|
458
|
-
import { defineMcpxTool } from "@abinnovision/payloadcms-mcpx";
|
|
459
|
-
import { z } from "zod";
|
|
460
|
-
|
|
461
|
-
const whichCollection = defineMcpxTool({
|
|
462
|
-
name: "whichCollection",
|
|
463
|
-
description: "Echoes back one of the collections this key may read.",
|
|
464
|
-
isEnabled: (scope) =>
|
|
465
|
-
scope.capabilities.tools["whichCollection"] === true &&
|
|
466
|
-
scope.readable.length > 0,
|
|
467
|
-
inputSchema: (scope) => ({
|
|
468
|
-
collection: z.enum(scope.readable as [string, ...string[]]),
|
|
469
|
-
}),
|
|
470
|
-
handler: ({ args }) => ({
|
|
471
|
-
content: [{ type: "text", text: args.collection }],
|
|
472
|
-
}),
|
|
473
|
-
});
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
`defineMcpxTool` infers the handler's arguments from the input schema either
|
|
477
|
-
way, so `args` above is `{ collection: string }` without being told. Inference
|
|
478
|
-
reaches as far as the shape's static type, so a helper returning `z.ZodRawShape`
|
|
479
|
-
leaves `args` as `Record<string, unknown>`. Where that happens, state the
|
|
480
|
-
arguments as a type argument: `defineMcpxTool<Args>({ ... })`.
|
|
481
|
-
|
|
482
|
-
`isEnabled` decides whether the tool is registered for this key at all: a tool
|
|
483
|
-
that is not enabled never appears in `tools/list`. It defaults to the tool's own
|
|
484
|
-
checkbox, and defining it **replaces** that check, so restate
|
|
485
|
-
`scope.capabilities.tools[name]` when you still want it, as above.
|
|
486
|
-
|
|
487
|
-
Every input schema is registered strictly, custom tools included: an unknown
|
|
488
|
-
argument is rejected by name rather than stripped before the handler runs.
|
|
489
|
-
|
|
490
|
-
`jsonResult` and `errorResult` are exported so a custom tool can return results
|
|
491
|
-
shaped like a builtin's. `isMcpxRequest(req)` lets your own hooks tell an
|
|
492
|
-
MCP-originated write from any other.
|
|
118
|
+
| Tool | What it does | Needs |
|
|
119
|
+
| ------------------ | ------------------------------------------------------------------ | ------------------ |
|
|
120
|
+
| `listCapabilities` | Lists what this key may do. Clients call it first. | any key |
|
|
121
|
+
| `describeSchema` | Describes the fields at one schema path, with the paths below it. | `read` |
|
|
122
|
+
| `findDocuments` | Queries a collection with a Payload `where`, `sort` and `select`. | `read` |
|
|
123
|
+
| `getDocument` | Reads a document, a subtree of it, an old version or a diff. | `read` |
|
|
124
|
+
| `findVersions` | Lists the version history of a document or global, without bodies. | `read`, `versions` |
|
|
125
|
+
| `patchDocument` | Applies JSON Patch operations to the current draft. | `write` |
|
|
126
|
+
| `createDocument` | Creates a draft from a seed. Not for upload collections. | `write` |
|
|
127
|
+
| `validateDocument` | Lists what blocks publishing, without saving. | `write` |
|
|
128
|
+
| `publishDocument` | Publishes the current draft. | `write`, `publish` |
|
|
129
|
+
|
|
130
|
+
Collections and globals use the same tools. A tool that addresses one document takes either
|
|
131
|
+
`collection` and `id`, or `global` alone. [`docs/concepts.md`](./docs/concepts.md#tools) lists
|
|
132
|
+
every argument.
|
|
133
|
+
|
|
134
|
+
## Capabilities
|
|
135
|
+
|
|
136
|
+
A key can do something only when both the config and the key allow it.
|
|
137
|
+
|
|
138
|
+
The plugin config sets the upper bound: which collections and globals are exposed, and whether
|
|
139
|
+
each one is readable, writable and publishable. A key cannot go past it.
|
|
140
|
+
|
|
141
|
+
Each key carries one checkbox per exposed entity and operation (`read`, `write`, `publish`), and
|
|
142
|
+
one per custom tool. The admin panel shows them as a matrix with a row per entity and a column per
|
|
143
|
+
operation. A cell the config does not expose shows a dash. `publish` exists only where the config
|
|
144
|
+
exposes `write` and `publish` on an entity with drafts, and counts only when `write` is ticked as
|
|
145
|
+
well. A write checkbox on an entity without drafts is marked "Writes go live immediately."
|
|
146
|
+
|
|
147
|
+
`tools/list` follows the key. A tool the key cannot use is not listed, and each `collection` and
|
|
148
|
+
`global` argument lists only the slugs the key may use with that tool. A checkbox added by a later
|
|
149
|
+
config change starts unticked on existing keys.
|
|
150
|
+
|
|
151
|
+
Any user in the user collection can create a key and tick every capability the config exposes.
|
|
152
|
+
To restrict that, use `apiKeys.overrideCollection` as shown in
|
|
153
|
+
[`docs/security.md`](./docs/security.md#who-can-create-keys).
|
|
154
|
+
|
|
155
|
+
## Security
|
|
156
|
+
|
|
157
|
+
- Only an API key authenticates. Admin sessions and JWTs are ignored, and a key does not work on
|
|
158
|
+
the REST or GraphQL API.
|
|
159
|
+
- A key acts as its user, and Payload access control applies to what its tools read and write.
|
|
160
|
+
- Relations are populated only into collections the key can read.
|
|
161
|
+
- The read tools leave out fields with `admin.hidden`. `findDocuments` refuses a `where` or `sort`
|
|
162
|
+
that names one, or that goes through a relation into a collection the key cannot read.
|
|
163
|
+
- After a write, the tools re-read the document with full access to report publish blockers.
|
|
164
|
+
Blocker messages can name fields the user cannot read.
|
|
165
|
+
- The request body is limited to 4 MB, a JSON-RPC batch to 10 messages, `patchDocument` to 500
|
|
166
|
+
operations and `describeSchema` to 400 paths.
|
|
167
|
+
- Custom tools are trusted code and must apply access control themselves.
|
|
168
|
+
|
|
169
|
+
Open limitations: a filter through a relation into a readable collection ignores that collection's
|
|
170
|
+
row-level `read` rule, rich text is not walked for hidden fields, and `admin.hidden` on a row, tab
|
|
171
|
+
or collapsible does not reach the fields inside it. Details:
|
|
172
|
+
[`docs/security.md`](./docs/security.md).
|
|
173
|
+
|
|
174
|
+
## Documentation
|
|
175
|
+
|
|
176
|
+
Upgrading from 1.x: [Upgrading to 2.0](./docs/integration.md#upgrading-to-20).
|
|
177
|
+
|
|
178
|
+
- [`docs/concepts.md`](./docs/concepts.md): tool arguments, schema paths and pointers, patching,
|
|
179
|
+
rich text, globals, uploads, drafts and publishing, versions and diffs.
|
|
180
|
+
- [`docs/integration.md`](./docs/integration.md): the endpoint, startup validation, API keys in the
|
|
181
|
+
admin panel, and custom key resolution.
|
|
182
|
+
- [`docs/custom-tools.md`](./docs/custom-tools.md): defining your own tools.
|
|
183
|
+
- [`docs/security.md`](./docs/security.md): authentication, access, limits and known limitations.
|
|
184
|
+
- [`docs/limitations.md`](./docs/limitations.md): what the plugin does not do.
|
|
185
|
+
- [`docs/standards.md`](./docs/standards.md): test and comment standards for contributors.
|
|
493
186
|
|
|
494
187
|
## License
|
|
495
188
|
|