@abinnovision/payloadcms-mcpx 1.0.0-beta.11 → 1.0.0-beta.13
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 +103 -44
- package/dist/api-keys/fields.mjs +17 -4
- package/dist/capabilities.mjs +22 -3
- package/dist/endpoint/{result.mjs → errors.mjs} +4 -21
- package/dist/endpoint/handler.mjs +4 -2
- package/dist/endpoint/index.mjs +4 -0
- package/dist/endpoint/server.mjs +10 -5
- package/dist/index.d.mts +4 -5
- package/dist/index.mjs +2 -2
- package/dist/options.mjs +26 -9
- package/dist/plugin.mjs +1 -0
- package/dist/{write/draft-guard.d.mts → request.d.mts} +2 -2
- package/dist/request.mjs +8 -0
- package/dist/{endpoint/result.d.mts → result.d.mts} +1 -2
- package/dist/result.mjs +22 -0
- package/dist/schema/index.mjs +6 -0
- package/dist/schema/pointer.mjs +1 -1
- package/dist/schema/walk.mjs +11 -15
- package/dist/tools/{index.mjs → builtin.mjs} +4 -2
- package/dist/tools/create-document.mjs +14 -9
- package/dist/tools/describe-schema.mjs +3 -3
- package/dist/tools/find-documents.mjs +1 -2
- package/dist/tools/get-document.mjs +2 -2
- package/dist/tools/list-capabilities.mjs +3 -2
- package/dist/tools/names.mjs +2 -1
- package/dist/tools/patch-document.mjs +14 -15
- package/dist/tools/publish-document.mjs +81 -0
- package/dist/tools/shared.mjs +50 -5
- package/dist/tools/target.mjs +4 -4
- package/dist/tools/validate-document.mjs +8 -9
- package/dist/types.d.mts +44 -23
- package/dist/write/draft-guard.mjs +71 -36
- package/dist/write/patch.mjs +131 -57
- package/dist/write/publish-blockers.mjs +10 -3
- package/dist/write/publish-intent.mjs +39 -0
- package/dist/write/transaction.mjs +7 -1
- package/package.json +1 -1
- package/dist/i18n.d.mts +0 -1
- package/dist/schema/lexical.d.mts +0 -1
- package/dist/schema/walk.d.mts +0 -3
- package/dist/tools/target.d.mts +0 -3
- package/dist/write/publish-blockers.d.mts +0 -15
package/README.md
CHANGED
|
@@ -13,12 +13,14 @@ resolved server-side against the real config and the real document, so unknown
|
|
|
13
13
|
fields, misplaced blocks and unusable rich text nodes or node fields are refused with the
|
|
14
14
|
valid alternatives listed, never silently dropped.
|
|
15
15
|
|
|
16
|
-
Writes are RFC 6902 patches that
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
Writes are RFC 6902 patches that land as drafts. One config axis decides how far
|
|
17
|
+
they reach: `write: "draft"` never changes live content, `write: "live"` does —
|
|
18
|
+
by exposing `publishDocument` where versions exist, and by permitting the write
|
|
19
|
+
at all where they do not. Every write returns the publish blockers: the
|
|
20
|
+
validation failures that still prevent the draft from being published.
|
|
21
|
+
Capabilities are declared twice: the plugin config decides what can exist, a
|
|
22
|
+
checkbox on each API key decides what does, and a missing checkbox means no
|
|
23
|
+
(fail-closed).
|
|
22
24
|
|
|
23
25
|
## Install
|
|
24
26
|
|
|
@@ -43,12 +45,12 @@ export default buildConfig({
|
|
|
43
45
|
plugins: [
|
|
44
46
|
mcpxPlugin({
|
|
45
47
|
collections: {
|
|
46
|
-
pages: { read: true, write:
|
|
47
|
-
posts: { read: true, write:
|
|
48
|
+
pages: { read: true, write: "live" }, // may be published through MCP
|
|
49
|
+
posts: { read: true, write: "draft" }, // drafts only
|
|
48
50
|
tags: true, // shorthand for { read: true }
|
|
49
51
|
},
|
|
50
52
|
globals: {
|
|
51
|
-
"site-settings": { read: true, write:
|
|
53
|
+
"site-settings": { read: true, write: "draft" },
|
|
52
54
|
},
|
|
53
55
|
limits: { maxLimit: 25, maxDepth: 1 },
|
|
54
56
|
}),
|
|
@@ -77,7 +79,10 @@ to anyone who may read the key document (own keys only, by default). Each key:
|
|
|
77
79
|
- carries one checkbox per exposed collection and operation, plus one per
|
|
78
80
|
custom tool. All checkboxes default to off. A key can never enable an
|
|
79
81
|
operation the plugin config does not expose, and keys created before a
|
|
80
|
-
capability existed stay without it.
|
|
82
|
+
capability existed stay without it. The `publish` checkbox only exists where
|
|
83
|
+
a versioned entity is configured `write: "live"`, so a key issued before
|
|
84
|
+
publishing was possible stays closed to it, and it counts only alongside
|
|
85
|
+
`write`: publishing is an extension of writing, not a capability of its own.
|
|
81
86
|
|
|
82
87
|
Keys authenticate only the MCP endpoint. They are deliberately not a Payload
|
|
83
88
|
auth strategy, so a key can never authenticate the REST or GraphQL API; the
|
|
@@ -140,7 +145,7 @@ Claude Desktop (no direct HTTP header support) via `mcp-remote`:
|
|
|
140
145
|
|
|
141
146
|
## Tools
|
|
142
147
|
|
|
143
|
-
The surface is fixed at
|
|
148
|
+
The surface is fixed at eight tools plus your custom ones; exposing a global
|
|
144
149
|
adds an argument, never a tool. `tools/list` reflects the key: write tools
|
|
145
150
|
disappear for read-only keys, and every `collection` and `global` enum contains
|
|
146
151
|
only the slugs the key may touch.
|
|
@@ -153,7 +158,8 @@ only the slugs the key may touch.
|
|
|
153
158
|
| `getDocument` | Read one document or a subtree of it. | `collection` + `id` \| `global`, `path?` (JSON pointer), `depth?`, `locale?`, `draft?` |
|
|
154
159
|
| `patchDocument` | Apply RFC 6902 operations to the current draft. | `collection` + `id` \| `global`, `locale`, `patches`, `expectedUpdatedAt?` |
|
|
155
160
|
| `createDocument` | Create a draft from a minimal seed. | `collection`, `locale`, `data` |
|
|
156
|
-
| `validateDocument` | Publish blockers without
|
|
161
|
+
| `validateDocument` | Publish blockers without saving anything. | `collection` + `id` \| `global`, `locale` |
|
|
162
|
+
| `publishDocument` | Publish the current draft. | `collection` + `id` \| `global`, `expectedUpdatedAt?` |
|
|
157
163
|
|
|
158
164
|
Rules the tools enforce and explain in their own descriptions:
|
|
159
165
|
|
|
@@ -244,15 +250,40 @@ If `tools/list` omits `global` entirely, no global is exposed to that key; the
|
|
|
244
250
|
argument only appears once one is. A deployment that uses no globals sees the
|
|
245
251
|
tool schemas exactly as they were.
|
|
246
252
|
|
|
247
|
-
## Drafts and
|
|
253
|
+
## Drafts and publishing
|
|
248
254
|
|
|
249
255
|
Draft-only writing is enforced on the Payload operation, not in the tool
|
|
250
256
|
handlers: a `beforeOperation` hook forces `draft: true` and strips `_status`
|
|
251
257
|
from every write carrying the MCP request marker, so custom tools and anything
|
|
252
258
|
else writing through the same request are covered too. A `beforeChange` hook
|
|
253
|
-
refuses any write that would still not land as a draft.
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
refuses any write that would still not land as a draft.
|
|
260
|
+
|
|
261
|
+
The two hooks are not equally load-bearing on both sides. `updateGlobal` reads
|
|
262
|
+
`draft` and the publish arguments off its argument bag _before_ it runs
|
|
263
|
+
`beforeOperation`, and re-reads only `data` afterwards, so for a global the
|
|
264
|
+
correction cannot apply and the `beforeChange` refusal is what actually holds
|
|
265
|
+
the line. Both are installed on every collection and global, exposed or not.
|
|
266
|
+
|
|
267
|
+
`publishDocument` is the one way through, and it opens the door for exactly one
|
|
268
|
+
operation: the tool records an intent naming the entity and id it is about to
|
|
269
|
+
publish, in `AsyncLocalStorage` rather than on the request, and the guard
|
|
270
|
+
consults it. A concurrent call in the same JSON-RPC batch runs in another async
|
|
271
|
+
context and sees nothing, a nested write to a different document meets the
|
|
272
|
+
unguarded rules, and the intent is claimed once so a re-entrant write to the
|
|
273
|
+
same document cannot ride along. It is not a security boundary — a custom tool
|
|
274
|
+
holds the whole `payload` instance — but no ordinary write can widen itself into
|
|
275
|
+
a publish by accident.
|
|
276
|
+
|
|
277
|
+
Publishing covers the whole document, as the admin Publish button does, but
|
|
278
|
+
Payload only validates the locale the publish runs in. A required field left
|
|
279
|
+
empty in another locale therefore goes live empty; that is Payload's behaviour,
|
|
280
|
+
not something this plugin adds. `publishDocument` refuses a document that fails
|
|
281
|
+
validation and reports `validationErrors` with JSON Pointers. It is refused
|
|
282
|
+
while a human holds the document open in the admin panel, and republishing an
|
|
283
|
+
unchanged document is accepted but writes another version.
|
|
284
|
+
|
|
285
|
+
There is no unpublish tool. Reverting a published document to a draft stays a
|
|
286
|
+
human action.
|
|
256
287
|
|
|
257
288
|
Publish blockers are advisory. Payload skips validation on draft saves (unless
|
|
258
289
|
`versions.drafts.validate` is set), so after every write the plugin re-runs
|
|
@@ -266,6 +297,12 @@ Collections with `versions.drafts.validate: true` refuse invalid drafts
|
|
|
266
297
|
outright; those failures come back as `validationErrors`. Both carry pointers,
|
|
267
298
|
restated from the dotted paths Payload reports internally.
|
|
268
299
|
|
|
300
|
+
`publishBlockersUnavailable` marks a check that could not complete, which is
|
|
301
|
+
not the same answer as a document with nothing wrong with it. `validateDocument`
|
|
302
|
+
runs the same traversal without saving anything, so it is not free of side
|
|
303
|
+
effects: field `beforeValidate` and `beforeChange` hooks run, and it carries no
|
|
304
|
+
`readOnlyHint` for that reason.
|
|
305
|
+
|
|
269
306
|
Writes also report `notApplied`: pointers whose value Payload kept unchanged,
|
|
270
307
|
which happens when field-level access denies the update.
|
|
271
308
|
|
|
@@ -301,9 +338,9 @@ Custom tools take the same route as the builtins: one `McpxTool` shape, one
|
|
|
301
338
|
registration loop. Anything a builtin does, a custom tool can do.
|
|
302
339
|
|
|
303
340
|
`handler` receives `scope` alongside `args`, `req` and `extra`. The scope
|
|
304
|
-
carries what the key may touch (`readable`, `writable`, `
|
|
305
|
-
`writableGlobals`), the configured
|
|
306
|
-
exposed collections and globals. `req` is shorthand for `scope.req`.
|
|
341
|
+
carries what the key may touch (`readable`, `writable`, `publishable`,
|
|
342
|
+
`readableGlobals`, `writableGlobals`, `publishableGlobals`), the configured
|
|
343
|
+
locales, the limits in force and the exposed collections and globals. `req` is shorthand for `scope.req`.
|
|
307
344
|
|
|
308
345
|
`inputSchema` may be a function of that scope instead of a fixed shape, which
|
|
309
346
|
is how a tool narrows an enum to what the key may read:
|
|
@@ -356,30 +393,48 @@ results shaped like a builtin's.
|
|
|
356
393
|
|
|
357
394
|
## Options
|
|
358
395
|
|
|
359
|
-
| Option
|
|
360
|
-
|
|
|
361
|
-
| `collections`
|
|
362
|
-
| `collections.<slug>.read`
|
|
363
|
-
| `collections.<slug>.write`
|
|
364
|
-
| `
|
|
365
|
-
| `globals`
|
|
366
|
-
| `globals.<slug>.
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
369
|
-
| `
|
|
370
|
-
| `apiKeys.
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
`
|
|
396
|
+
| Option | Default | Description |
|
|
397
|
+
| ---------------------------- | ------------------------------ | ----------------------------------------------------------------- |
|
|
398
|
+
| `collections` | required | Allow-list. `true` means `{ read: true }`. |
|
|
399
|
+
| `collections.<slug>.read` | `true` | Expose `describeSchema`, `findDocuments`, `getDocument`. |
|
|
400
|
+
| `collections.<slug>.write` | `false` | `"draft"` or `"live"`. See below. |
|
|
401
|
+
| `globals` | `{}` | Allow-list of globals. `true` means `{ read: true }`. |
|
|
402
|
+
| `globals.<slug>.read` | `true` | Expose `describeSchema`, `getDocument`. |
|
|
403
|
+
| `globals.<slug>.write` | `false` | `"draft"` or `"live"`. See below. |
|
|
404
|
+
| `userCollection` | `config.admin.user` or `users` | Auth collection the keys act as. |
|
|
405
|
+
| `apiKeys.slug` | `mcpx-api-keys` | Slug of the generated key collection. |
|
|
406
|
+
| `apiKeys.setupGuide` | `true` | Add a "Connect a client" tab to saved keys. Needs the import map. |
|
|
407
|
+
| `apiKeys.overrideCollection` | none | Final override applied to the generated collection. |
|
|
408
|
+
| `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
|
|
409
|
+
| `limits.maxLimit` | `25` | Upper bound for `findDocuments.limit`. |
|
|
410
|
+
| `limits.maxDepth` | `1` | Upper bound for `depth` on reads. |
|
|
411
|
+
| `tools` | `[]` | Custom tools, defined the same way as the builtins. |
|
|
412
|
+
| `auth.resolve` | none | Replace or wrap the default key resolution. |
|
|
413
|
+
| `serverInfo` | package name and version | Reported to MCP clients. |
|
|
414
|
+
|
|
415
|
+
`write` is one axis: how far MCP writes to this entity reach.
|
|
416
|
+
|
|
417
|
+
| `write` | With `versions.drafts` | Without |
|
|
418
|
+
| --------- | ------------------------------------------------------- | ---------------------------------------------- |
|
|
419
|
+
| `false` | no write tool reaches it | no write tool reaches it |
|
|
420
|
+
| `"draft"` | writes land as drafts, nothing is ever published | refused at startup: there is no draft to write |
|
|
421
|
+
| `"live"` | writes land as drafts, and `publishDocument` is exposed | writes land on the live document |
|
|
422
|
+
|
|
423
|
+
`"live"` is the only way an MCP write reaches live content, whichever of the two
|
|
424
|
+
shapes it takes. Wherever it is set, the server instructions and the
|
|
425
|
+
`patchDocument` and `createDocument` descriptions name those slugs for the key in
|
|
426
|
+
question, so a client is never told its writes are drafts while they are not,
|
|
427
|
+
nor that publishing is out of reach when it is not.
|
|
428
|
+
|
|
429
|
+
Migrating from the previous option shape: `write: true` becomes
|
|
430
|
+
`write: "draft"`, and `write: true` with `allowLiveWrites: true` becomes
|
|
431
|
+
`write: "live"`. A versioned entity moved to `write: "live"` gains a `publish`
|
|
432
|
+
checkbox on every key, unticked, so nothing publishes until someone says so.
|
|
433
|
+
|
|
434
|
+
Misconfiguration (unknown slugs, `write: "draft"` on a collection without
|
|
435
|
+
drafts, upload collections exposed for write, tool name collisions) fails at
|
|
436
|
+
startup with `InvalidConfiguration`. So does `write: "live"` on an entity using
|
|
437
|
+
`versions.drafts.localizeStatus`, which is not supported yet. Auth collections cannot be exposed at all, read
|
|
383
438
|
included: their documents carry credentials, such as the decrypted Payload API
|
|
384
439
|
key of every user.
|
|
385
440
|
|
|
@@ -390,12 +445,16 @@ key of every user.
|
|
|
390
445
|
- The endpoint authenticates with Bearer keys only; admin JWTs and cookies are
|
|
391
446
|
ignored. Keys cannot authenticate REST or GraphQL.
|
|
392
447
|
- Every operation runs under the linked user with `overrideAccess: false`.
|
|
448
|
+
- Payload has no separate publish permission: at its access layer, anyone who
|
|
449
|
+
may update a document may publish it. The `publish` checkbox is this plugin's
|
|
450
|
+
fence, not Payload's.
|
|
393
451
|
- Not covered in v1: `delete` (no tool exists and none is generated), uploads.
|
|
394
452
|
Custom tools are trusted code and can do what the linked user may.
|
|
395
453
|
|
|
396
454
|
## Non-goals of v1 / roadmap
|
|
397
455
|
|
|
398
|
-
|
|
456
|
+
Unpublishing, `versions.drafts.localizeStatus`, deletes, uploads, markdown
|
|
457
|
+
authoring for rich text, addressing a rich text node
|
|
399
458
|
by position in a patch (an editor state is written whole), schemas for `upload`
|
|
400
459
|
node fields, row addressing by id instead of index, cross-locale publish
|
|
401
460
|
blockers, pagination of `describeSchema` with `expand`, and a handler-level
|
package/dist/api-keys/fields.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CAPABILITIES_FIELD } from "../capabilities.mjs";
|
|
1
|
+
import { CAPABILITIES_FIELD, canPublish, canWrite } from "../capabilities.mjs";
|
|
2
2
|
//#region src/api-keys/fields.ts
|
|
3
3
|
const encryptKey = ({ req, value }) => typeof value === "string" ? req.payload.encrypt(value) : value;
|
|
4
4
|
const decryptKey = ({ req, value }) => {
|
|
@@ -89,25 +89,38 @@ const SETUP_GUIDE_FIELD = "setupGuide";
|
|
|
89
89
|
}]
|
|
90
90
|
}];
|
|
91
91
|
};
|
|
92
|
+
const PUBLISH_DESCRIPTION = "Publish the current draft. Changes what the public sees.";
|
|
92
93
|
/**
|
|
93
94
|
* One checkbox per exposed operation, grouped per collection, per global and
|
|
94
95
|
* per custom tool. Only operations the plugin config exposes get a checkbox, so
|
|
95
96
|
* a key can never enable more than the config allows. Everything defaults to
|
|
96
97
|
* off, which is why a key issued before a capability existed stays closed to it.
|
|
98
|
+
*
|
|
99
|
+
* An entity without versions gets no `publish` checkbox even under
|
|
100
|
+
* `write: "live"`: there is no draft to promote there, the write itself is the
|
|
101
|
+
* live change, and a second checkbox would only make `write` a dead setting.
|
|
97
102
|
*/ const createCapabilityFields = (options) => {
|
|
98
103
|
const collectionGroups = options.collections.map((collection) => ({
|
|
99
104
|
name: collection.fieldName,
|
|
100
105
|
type: "group",
|
|
101
106
|
label: collection.slug,
|
|
102
|
-
fields: [
|
|
107
|
+
fields: [
|
|
108
|
+
...collection.read ? [checkbox("read", "Describe, find and read documents.")] : [],
|
|
109
|
+
...canWrite(collection) ? [checkbox("write", "Create, patch and validate drafts.")] : [],
|
|
110
|
+
...canPublish(collection) ? [checkbox("publish", PUBLISH_DESCRIPTION)] : []
|
|
111
|
+
]
|
|
103
112
|
}));
|
|
104
113
|
const globalGroups = options.globals.map((global) => ({
|
|
105
114
|
name: global.fieldName,
|
|
106
115
|
type: "group",
|
|
107
116
|
label: global.slug,
|
|
108
|
-
fields: [
|
|
117
|
+
fields: [
|
|
118
|
+
...global.read ? [checkbox("read", "Describe and read this global.")] : [],
|
|
119
|
+
...canWrite(global) ? [checkbox("write", "Patch and validate this global's draft.")] : [],
|
|
120
|
+
...canPublish(global) ? [checkbox("publish", PUBLISH_DESCRIPTION)] : []
|
|
121
|
+
]
|
|
109
122
|
}));
|
|
110
|
-
const toolCheckboxes = options.tools.map((tool) => checkbox(tool.name, tool.description));
|
|
123
|
+
const toolCheckboxes = options.tools.map((tool) => checkbox(tool.name, typeof tool.description === "string" ? tool.description : tool.name));
|
|
111
124
|
const groups = [
|
|
112
125
|
...collectionGroups.length > 0 ? [{
|
|
113
126
|
name: "collections",
|
package/dist/capabilities.mjs
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
//#region src/capabilities.ts
|
|
2
2
|
/** Name of the capability group on the key document. */ const CAPABILITIES_FIELD = "capabilities";
|
|
3
|
+
/** Whether any write tool reaches this entity. */ const canWrite = (entity) => entity.write !== false;
|
|
4
|
+
/**
|
|
5
|
+
* Whether `publishDocument` reaches this entity: the config lets MCP change
|
|
6
|
+
* live content and there is a draft to promote.
|
|
7
|
+
*/ const canPublish = (entity) => entity.write === "live" && entity.hasDrafts;
|
|
8
|
+
/**
|
|
9
|
+
* Whether an ordinary write to this entity changes the live document. With no
|
|
10
|
+
* versions there is no draft to land on, so `write: "live"` is what permits the
|
|
11
|
+
* write at all and every write is live.
|
|
12
|
+
*/ const isLiveWrite = (entity) => entity.write === "live" && !entity.hasDrafts;
|
|
3
13
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
14
|
const flag = (group, name) => isRecord(group) && group[name] === true;
|
|
5
15
|
/**
|
|
16
|
+
* Publishing is an extension of writing, never a capability of its own: a key
|
|
17
|
+
* that may publish may also edit the draft it publishes. Both checkboxes are
|
|
18
|
+
* therefore required, on top of the config exposing publishing at all.
|
|
19
|
+
*/ const publishFlag = (entity, group) => canPublish(entity) && flag(group, "write") && flag(group, "publish");
|
|
20
|
+
/**
|
|
6
21
|
* Capabilities in force for a key: the plugin config decides what can exist,
|
|
7
22
|
* the key's checkboxes decide what does. A missing checkbox is `false`, so keys
|
|
8
23
|
* issued before a capability existed stay closed.
|
|
@@ -15,7 +30,8 @@ const flag = (group, name) => isRecord(group) && group[name] === true;
|
|
|
15
30
|
const group = isRecord(collectionsGroup) ? collectionsGroup[collection.fieldName] : void 0;
|
|
16
31
|
collections[collection.slug] = {
|
|
17
32
|
read: collection.read && flag(group, "read"),
|
|
18
|
-
write: collection
|
|
33
|
+
write: canWrite(collection) && flag(group, "write"),
|
|
34
|
+
publish: publishFlag(collection, group)
|
|
19
35
|
};
|
|
20
36
|
}
|
|
21
37
|
const globals = {};
|
|
@@ -23,7 +39,8 @@ const flag = (group, name) => isRecord(group) && group[name] === true;
|
|
|
23
39
|
const group = isRecord(globalsGroup) ? globalsGroup[global.fieldName] : void 0;
|
|
24
40
|
globals[global.slug] = {
|
|
25
41
|
read: global.read && flag(group, "read"),
|
|
26
|
-
write: global
|
|
42
|
+
write: canWrite(global) && flag(group, "write"),
|
|
43
|
+
publish: publishFlag(global, group)
|
|
27
44
|
};
|
|
28
45
|
}
|
|
29
46
|
const tools = {};
|
|
@@ -37,7 +54,9 @@ const flag = (group, name) => isRecord(group) && group[name] === true;
|
|
|
37
54
|
const pick = (entries, operation) => Object.entries(entries).filter(([, value]) => value[operation]).map(([slug]) => slug);
|
|
38
55
|
const readableSlugs = (capabilities) => pick(capabilities.collections, "read");
|
|
39
56
|
const writableSlugs = (capabilities) => pick(capabilities.collections, "write");
|
|
57
|
+
const publishableSlugs = (capabilities) => pick(capabilities.collections, "publish");
|
|
40
58
|
const readableGlobalSlugs = (capabilities) => pick(capabilities.globals, "read");
|
|
41
59
|
const writableGlobalSlugs = (capabilities) => pick(capabilities.globals, "write");
|
|
60
|
+
const publishableGlobalSlugs = (capabilities) => pick(capabilities.globals, "publish");
|
|
42
61
|
//#endregion
|
|
43
|
-
export { CAPABILITIES_FIELD, readableGlobalSlugs, readableSlugs, resolveCapabilities, writableGlobalSlugs, writableSlugs };
|
|
62
|
+
export { CAPABILITIES_FIELD, canPublish, canWrite, isLiveWrite, publishableGlobalSlugs, publishableSlugs, readableGlobalSlugs, readableSlugs, resolveCapabilities, writableGlobalSlugs, writableSlugs };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { errorResult } from "../result.mjs";
|
|
1
2
|
import { pointerFromPayloadPath } from "../schema/walk.mjs";
|
|
3
|
+
import "../schema/index.mjs";
|
|
2
4
|
import { APIError, ValidationError } from "payload";
|
|
3
|
-
//#region src/endpoint/
|
|
5
|
+
//#region src/endpoint/errors.ts
|
|
4
6
|
/**
|
|
5
7
|
* A JSON-RPC error response for failures that happen before the MCP server
|
|
6
8
|
* is involved (auth, method, body parsing).
|
|
@@ -20,25 +22,6 @@ import { APIError, ValidationError } from "payload";
|
|
|
20
22
|
});
|
|
21
23
|
};
|
|
22
24
|
/**
|
|
23
|
-
* A successful tool result carrying `value` as JSON text.
|
|
24
|
-
*/ const jsonResult = (value) => ({ content: [{
|
|
25
|
-
type: "text",
|
|
26
|
-
text: JSON.stringify(value)
|
|
27
|
-
}] });
|
|
28
|
-
/**
|
|
29
|
-
* A failed tool result. `extras` travel alongside the message so the client
|
|
30
|
-
* can act on them (problems, validation errors, the current `updatedAt`).
|
|
31
|
-
*/ const errorResult = (message, extras = {}) => ({
|
|
32
|
-
content: [{
|
|
33
|
-
type: "text",
|
|
34
|
-
text: JSON.stringify({
|
|
35
|
-
error: message,
|
|
36
|
-
...extras
|
|
37
|
-
})
|
|
38
|
-
}],
|
|
39
|
-
isError: true
|
|
40
|
-
});
|
|
41
|
-
/**
|
|
42
25
|
* Maps an exception thrown by a tool to a result the client can read.
|
|
43
26
|
*
|
|
44
27
|
* Payload's public errors keep their message and status; a `ValidationError`
|
|
@@ -62,4 +45,4 @@ import { APIError, ValidationError } from "payload";
|
|
|
62
45
|
return errorResult("Internal error");
|
|
63
46
|
};
|
|
64
47
|
//#endregion
|
|
65
|
-
export {
|
|
48
|
+
export { jsonRpcError, toToolError };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { publishableGlobalSlugs, publishableSlugs, readableGlobalSlugs, readableSlugs, resolveCapabilities, writableGlobalSlugs, writableSlugs } from "../capabilities.mjs";
|
|
2
|
+
import { jsonRpcError } from "./errors.mjs";
|
|
3
3
|
import { resolveApiKeyAuth } from "../auth/resolve.mjs";
|
|
4
4
|
import { createMcpServer } from "./server.mjs";
|
|
5
5
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
@@ -11,8 +11,10 @@ const buildScope = (req, options, capabilities) => {
|
|
|
11
11
|
capabilities,
|
|
12
12
|
readable: readableSlugs(capabilities),
|
|
13
13
|
writable: writableSlugs(capabilities),
|
|
14
|
+
publishable: publishableSlugs(capabilities),
|
|
14
15
|
readableGlobals: readableGlobalSlugs(capabilities),
|
|
15
16
|
writableGlobals: writableGlobalSlugs(capabilities),
|
|
17
|
+
publishableGlobals: publishableGlobalSlugs(capabilities),
|
|
16
18
|
locales: localization ? localization.localeCodes : null,
|
|
17
19
|
defaultLocale: localization ? localization.defaultLocale : null,
|
|
18
20
|
limits: options.limits,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsonRpcError, toToolError } from "./errors.mjs";
|
|
2
|
+
import { createMcpServer, isToolEnabled, toolDescription, toolInputSchema } from "./server.mjs";
|
|
3
|
+
import { createMcpxHandler, methodNotAllowed } from "./handler.mjs";
|
|
4
|
+
export { createMcpServer, createMcpxHandler, isToolEnabled, jsonRpcError, methodNotAllowed, toToolError, toolDescription, toolInputSchema };
|
package/dist/endpoint/server.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { toToolError } from "./
|
|
2
|
-
import {
|
|
1
|
+
import { toToolError } from "./errors.mjs";
|
|
2
|
+
import { draftSentence } from "../tools/shared.mjs";
|
|
3
|
+
import { BUILTIN_TOOLS } from "../tools/builtin.mjs";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
5
|
import { z } from "zod";
|
|
5
6
|
//#region src/endpoint/server.ts
|
|
@@ -10,6 +11,10 @@ import { z } from "zod";
|
|
|
10
11
|
* scope to narrow enums to what the key may touch.
|
|
11
12
|
*/ const toolInputSchema = (tool, scope) => z.strictObject(typeof tool.inputSchema === "function" ? tool.inputSchema(scope) : tool.inputSchema ?? {});
|
|
12
13
|
/**
|
|
14
|
+
* A tool's description, which may be built from the scope so it can name the
|
|
15
|
+
* targets this key writes live.
|
|
16
|
+
*/ const toolDescription = (tool, scope) => typeof tool.description === "function" ? tool.description(scope) : tool.description;
|
|
17
|
+
/**
|
|
13
18
|
* Whether the key may call the tool. A tool that does not decide for itself is
|
|
14
19
|
* gated by its own checkbox on the key, which is how the tools from
|
|
15
20
|
* `options.tools` work; the builtins derive it from the key's collection and
|
|
@@ -26,7 +31,7 @@ import { z } from "zod";
|
|
|
26
31
|
const server = new McpServer({
|
|
27
32
|
name: options.serverInfo.name,
|
|
28
33
|
version: options.serverInfo.version
|
|
29
|
-
}, { instructions:
|
|
34
|
+
}, { instructions: `Start with listCapabilities, then describeSchema for the collection or global you work on. ${draftSentence(scope)}` });
|
|
30
35
|
const guarded = (run) => async () => {
|
|
31
36
|
try {
|
|
32
37
|
return await run();
|
|
@@ -37,7 +42,7 @@ import { z } from "zod";
|
|
|
37
42
|
for (const tool of [...BUILTIN_TOOLS, ...options.tools]) {
|
|
38
43
|
if (!isToolEnabled(tool, scope)) continue;
|
|
39
44
|
server.registerTool(tool.name, {
|
|
40
|
-
description: tool
|
|
45
|
+
description: toolDescription(tool, scope),
|
|
41
46
|
inputSchema: toolInputSchema(tool, scope),
|
|
42
47
|
...tool.annotations ? { annotations: tool.annotations } : {}
|
|
43
48
|
}, (args, extra) => guarded(() => tool.handler({
|
|
@@ -50,4 +55,4 @@ import { z } from "zod";
|
|
|
50
55
|
return server;
|
|
51
56
|
};
|
|
52
57
|
//#endregion
|
|
53
|
-
export { createMcpServer, isToolEnabled, toolInputSchema };
|
|
58
|
+
export { createMcpServer, isToolEnabled, toolDescription, toolInputSchema };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { McpxAnyTool, McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxExposedEntity, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxTool, McpxToolExtra, McpxToolScope, defineMcpxTool } from "./types.mjs";
|
|
1
|
+
import { McpxAnyTool, McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxExposedEntity, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxTool, McpxToolExtra, McpxToolScope, McpxWriteMode, PublishBlocker, defineMcpxTool } from "./types.mjs";
|
|
3
2
|
import { mcpxPlugin } from "./plugin.mjs";
|
|
4
|
-
import { isMcpxRequest } from "./
|
|
5
|
-
import {
|
|
6
|
-
export {
|
|
3
|
+
import { isMcpxRequest } from "./request.mjs";
|
|
4
|
+
import { errorResult, jsonResult } from "./result.mjs";
|
|
5
|
+
export { McpxAnyTool, McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxExposedEntity, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxTool, McpxToolExtra, McpxToolScope, McpxWriteMode, PublishBlocker, defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { errorResult, jsonResult } from "./
|
|
1
|
+
import { errorResult, jsonResult } from "./result.mjs";
|
|
2
2
|
import { defineMcpxTool } from "./types.mjs";
|
|
3
|
-
import { isMcpxRequest } from "./
|
|
3
|
+
import { isMcpxRequest } from "./request.mjs";
|
|
4
4
|
import { mcpxPlugin } from "./plugin.mjs";
|
|
5
5
|
export { defineMcpxTool, errorResult, isMcpxRequest, jsonResult, mcpxPlugin };
|
package/dist/options.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BUILTIN_TOOL_NAMES } from "./tools/names.mjs";
|
|
2
2
|
import "./version.mjs";
|
|
3
3
|
import { InvalidConfiguration } from "payload";
|
|
4
|
-
import { hasDraftsEnabled } from "payload/shared";
|
|
4
|
+
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";
|
|
@@ -24,11 +24,29 @@ const fail = (message) => {
|
|
|
24
24
|
if (slug === apiKeysSlug || slug.startsWith("payload-")) fail(`Collection "${slug}" cannot be exposed.`);
|
|
25
25
|
if (collection.auth) fail(`Auth collection "${slug}" cannot be exposed. Its documents carry credentials.`);
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* The write mode, checked at runtime as well as in the type. JS callers get no
|
|
29
|
+
* type checking, and a typo reading as "no write" would be a silent downgrade.
|
|
30
|
+
*/ const normalizeWriteMode = (kind, slug, value) => {
|
|
31
|
+
if (value === void 0 || value === false) return false;
|
|
32
|
+
if (value === "draft" || value === "live") return value;
|
|
33
|
+
return fail(`${kind} "${slug}" has write: ${JSON.stringify(value)}. Use false, "draft" or "live".`);
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* `localizeStatus` makes `_status` a localized field, which flips Payload's
|
|
37
|
+
* `publishAllLocales` default to false and turns `_status` into a locale-keyed
|
|
38
|
+
* object. Publishing would then cover one locale while reporting success, and
|
|
39
|
+
* the tool responses model `_status` as a string. Refused until both are
|
|
40
|
+
* handled.
|
|
41
|
+
*/ const assertPublishable = (kind, config) => {
|
|
42
|
+
if (hasLocalizeStatusEnabled(config)) fail(`${kind} "${config.slug}" has versions.drafts.localizeStatus enabled, which write: "live" does not support yet.`);
|
|
43
|
+
};
|
|
27
44
|
const assertWritable = (collection, options) => {
|
|
28
45
|
const { slug } = collection;
|
|
29
46
|
if (collection.upload) fail(`Upload collection "${slug}" cannot be exposed for write.`);
|
|
30
47
|
if (collection.timestamps === false) fail(`Collection "${slug}" has timestamps disabled, which write tools need for concurrency checks.`);
|
|
31
|
-
if (
|
|
48
|
+
if (options.write === "draft" && !options.hasDrafts) fail(`Collection "${slug}" has no drafts. Enable versions.drafts or set write: "live".`);
|
|
49
|
+
if (options.write === "live") assertPublishable("Collection", collection);
|
|
32
50
|
};
|
|
33
51
|
/**
|
|
34
52
|
* Refuses globals that must never be reachable. Globals cannot be auth or
|
|
@@ -41,7 +59,8 @@ const assertWritable = (collection, options) => {
|
|
|
41
59
|
* `createdAt`/`updatedAt`, so the concurrency check the collection path guards
|
|
42
60
|
* for is always available here. Drafts are the only requirement left.
|
|
43
61
|
*/ const assertGlobalWritable = (global, options) => {
|
|
44
|
-
if (
|
|
62
|
+
if (options.write === "draft" && !options.hasDrafts) fail(`Global "${global.slug}" has no drafts. Enable versions.drafts or set write: "live".`);
|
|
63
|
+
if (options.write === "live") assertPublishable("Global", global);
|
|
45
64
|
};
|
|
46
65
|
const normalizeCollections = (config, options, apiKeysSlug) => {
|
|
47
66
|
const collections = config.collections ?? [];
|
|
@@ -56,12 +75,11 @@ const normalizeCollections = (config, options, apiKeysSlug) => {
|
|
|
56
75
|
const normalized = {
|
|
57
76
|
slug,
|
|
58
77
|
read: settings.read ?? true,
|
|
59
|
-
write: settings.write
|
|
60
|
-
allowLiveWrites: settings.allowLiveWrites ?? false,
|
|
78
|
+
write: normalizeWriteMode("Collection", slug, settings.write),
|
|
61
79
|
hasDrafts,
|
|
62
80
|
fieldName: toCamelCase(slug)
|
|
63
81
|
};
|
|
64
|
-
if (normalized.write) assertWritable(collection, normalized);
|
|
82
|
+
if (normalized.write !== false) assertWritable(collection, normalized);
|
|
65
83
|
if (fieldNames.has(normalized.fieldName)) fail(`Collection "${slug}" maps to capability field "${normalized.fieldName}", which another exposed collection already uses.`);
|
|
66
84
|
fieldNames.add(normalized.fieldName);
|
|
67
85
|
return [normalized];
|
|
@@ -80,12 +98,11 @@ const normalizeGlobals = (config, options) => {
|
|
|
80
98
|
const normalized = {
|
|
81
99
|
slug,
|
|
82
100
|
read: settings.read ?? true,
|
|
83
|
-
write: settings.write
|
|
84
|
-
allowLiveWrites: settings.allowLiveWrites ?? false,
|
|
101
|
+
write: normalizeWriteMode("Global", slug, settings.write),
|
|
85
102
|
hasDrafts,
|
|
86
103
|
fieldName: toCamelCase(slug)
|
|
87
104
|
};
|
|
88
|
-
if (normalized.write) assertGlobalWritable(global, normalized);
|
|
105
|
+
if (normalized.write !== false) assertGlobalWritable(global, normalized);
|
|
89
106
|
if (fieldNames.has(normalized.fieldName)) fail(`Global "${slug}" maps to capability field "${normalized.fieldName}", which another exposed global already uses.`);
|
|
90
107
|
fieldNames.add(normalized.fieldName);
|
|
91
108
|
return [normalized];
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createApiKeysCollection } from "./api-keys/collection.mjs";
|
|
2
2
|
import { createMcpxHandler, methodNotAllowed } from "./endpoint/handler.mjs";
|
|
3
|
+
import "./endpoint/index.mjs";
|
|
3
4
|
import { normalizeOptions } from "./options.mjs";
|
|
4
5
|
import { installDraftGuards, installGlobalDraftGuards } from "./write/draft-guard.mjs";
|
|
5
6
|
import { definePlugin } from "payload";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
//#region src/
|
|
1
|
+
import { PayloadRequest } from "payload";
|
|
2
|
+
//#region src/request.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Whether a request originated from the MCP endpoint. The endpoint stamps
|
|
5
5
|
* `req.context.mcpx`, which travels into every local API call made with the
|
package/dist/request.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/request.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a request originated from the MCP endpoint. The endpoint stamps
|
|
4
|
+
* `req.context.mcpx`, which travels into every local API call made with the
|
|
5
|
+
* same `req`, including those made by custom tools.
|
|
6
|
+
*/ const isMcpxRequest = (req) => req.context.mcpx !== void 0;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { isMcpxRequest };
|
package/dist/result.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/result.ts
|
|
2
|
+
/**
|
|
3
|
+
* A successful tool result carrying `value` as JSON text.
|
|
4
|
+
*/ const jsonResult = (value) => ({ content: [{
|
|
5
|
+
type: "text",
|
|
6
|
+
text: JSON.stringify(value)
|
|
7
|
+
}] });
|
|
8
|
+
/**
|
|
9
|
+
* A failed tool result. `extras` travel alongside the message so the client
|
|
10
|
+
* can act on them (problems, validation errors, the current `updatedAt`).
|
|
11
|
+
*/ const errorResult = (message, extras = {}) => ({
|
|
12
|
+
content: [{
|
|
13
|
+
type: "text",
|
|
14
|
+
text: JSON.stringify({
|
|
15
|
+
error: message,
|
|
16
|
+
...extras
|
|
17
|
+
})
|
|
18
|
+
}],
|
|
19
|
+
isError: true
|
|
20
|
+
});
|
|
21
|
+
//#endregion
|
|
22
|
+
export { errorResult, jsonResult };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { allowedNodeTypes, lexicalSubSchema, nodeOptions, subSchemaNodeTypes } from "./lexical.mjs";
|
|
2
|
+
import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, pointerFromPayloadPath, splitPath, targetOf } from "./walk.mjs";
|
|
3
|
+
import { nodeDescriber, reachableSchemaPaths } from "./describe.mjs";
|
|
4
|
+
import { resolveDataPointer } from "./pointer.mjs";
|
|
5
|
+
import { validateWriteValue } from "./shape.mjs";
|
|
6
|
+
export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, allowedNodeTypes, blockOf, blockSlugsOf, describeAddressableFields, describeFields, findBlocksField, findRichTextField, joinPath, lexicalSubSchema, nodeDescriber, nodeOptions, pointerFromPayloadPath, reachableSchemaPaths, resolveDataPointer, splitPath, subSchemaNodeTypes, targetOf, validateWriteValue };
|