@abinnovision/payloadcms-mcpx 1.0.0-beta.10
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/LICENSE +201 -0
- package/README.md +353 -0
- package/dist/api-keys/collection.mjs +58 -0
- package/dist/api-keys/fields.mjs +137 -0
- package/dist/api-keys/key.mjs +10 -0
- package/dist/api-keys/setup-guide.mjs +54 -0
- package/dist/auth/resolve.mjs +62 -0
- package/dist/capabilities.mjs +43 -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/endpoint/handler.mjs +86 -0
- package/dist/endpoint/result.mjs +65 -0
- package/dist/endpoint/server.mjs +52 -0
- package/dist/i18n.d.mts +1 -0
- package/dist/i18n.mjs +40 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +4 -0
- package/dist/options.d.mts +2 -0
- package/dist/options.mjs +146 -0
- package/dist/plugin.d.mts +9 -0
- package/dist/plugin.mjs +43 -0
- package/dist/schema/describe.mjs +160 -0
- package/dist/schema/lexical.d.mts +1 -0
- package/dist/schema/lexical.mjs +117 -0
- package/dist/schema/pointer.mjs +80 -0
- package/dist/schema/shape.mjs +208 -0
- package/dist/schema/walk.d.mts +3 -0
- package/dist/schema/walk.mjs +165 -0
- package/dist/tools/create-document.mjs +68 -0
- package/dist/tools/describe-schema.mjs +54 -0
- package/dist/tools/find-documents.mjs +55 -0
- package/dist/tools/get-document.mjs +68 -0
- package/dist/tools/index.mjs +23 -0
- package/dist/tools/list-capabilities.mjs +68 -0
- package/dist/tools/names.mjs +14 -0
- package/dist/tools/patch-document.mjs +127 -0
- package/dist/tools/shared.mjs +97 -0
- package/dist/tools/target.d.mts +3 -0
- package/dist/tools/target.mjs +49 -0
- package/dist/tools/types.d.mts +5 -0
- package/dist/tools/validate-document.mjs +55 -0
- package/dist/types.d.mts +143 -0
- package/dist/types.mjs +7 -0
- package/dist/version.mjs +6 -0
- package/dist/write/draft-guard.d.mts +10 -0
- package/dist/write/draft-guard.mjs +113 -0
- package/dist/write/patch.mjs +219 -0
- package/dist/write/publish-blockers.d.mts +15 -0
- package/dist/write/publish-blockers.mjs +74 -0
- package/dist/write/transaction.mjs +19 -0
- package/package.json +104 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 abi group GmbH
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# @abinnovision/payloadcms-mcpx
|
|
2
|
+
|
|
3
|
+
A Payload CMS plugin that mounts an MCP (Model Context Protocol) server whose
|
|
4
|
+
tool surface stays small and accurate regardless of the size of the
|
|
5
|
+
content model.
|
|
6
|
+
|
|
7
|
+
Instead of generating one tool per collection with the full document schema
|
|
8
|
+
inlined, the plugin types its surface in three layers. The tool signatures are
|
|
9
|
+
small and static: collection slugs, locales and operations as enums, everything
|
|
10
|
+
else scalars. The field shapes are pulled on demand through `describeSchema`,
|
|
11
|
+
one node at a time, stopping at every blocks boundary. And every write is
|
|
12
|
+
resolved server-side against the real config and the real document, so unknown
|
|
13
|
+
fields, misplaced blocks and unusable rich text nodes or node fields are refused with the
|
|
14
|
+
valid alternatives listed, never silently dropped.
|
|
15
|
+
|
|
16
|
+
Writes are RFC 6902 patches that always land as drafts; publishing stays a
|
|
17
|
+
human action in the admin panel. Every write returns the publish
|
|
18
|
+
blockers: the validation failures that still prevent a human from publishing
|
|
19
|
+
the draft. Capabilities are declared twice: the plugin config decides what
|
|
20
|
+
can exist, a checkbox on each API key decides what does, and a missing checkbox
|
|
21
|
+
means no (fail-closed).
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
yarn add @abinnovision/payloadcms-mcpx
|
|
27
|
+
```
|
|
28
|
+
|
|
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`.
|
|
33
|
+
- The package is published as ESM only, matching Payload itself.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { mcpxPlugin } from "@abinnovision/payloadcms-mcpx";
|
|
39
|
+
import { buildConfig } from "payload";
|
|
40
|
+
|
|
41
|
+
export default buildConfig({
|
|
42
|
+
// ...
|
|
43
|
+
plugins: [
|
|
44
|
+
mcpxPlugin({
|
|
45
|
+
collections: {
|
|
46
|
+
pages: { read: true, write: true },
|
|
47
|
+
posts: { read: true, write: true },
|
|
48
|
+
tags: true, // shorthand for { read: true }
|
|
49
|
+
},
|
|
50
|
+
globals: {
|
|
51
|
+
"site-settings": { read: true, write: true },
|
|
52
|
+
},
|
|
53
|
+
limits: { maxLimit: 25, maxDepth: 1 },
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The plugin adds:
|
|
60
|
+
|
|
61
|
+
- a `POST /api/mcpx` endpoint speaking MCP over streamable HTTP (stateless,
|
|
62
|
+
JSON responses; `GET`/`DELETE` answer 405),
|
|
63
|
+
- an `mcpx-api-keys` collection (admin group "MCP") holding the keys and their
|
|
64
|
+
capability checkboxes,
|
|
65
|
+
- a draft guard on every collection and global, so any write carrying the MCP
|
|
66
|
+
request marker lands as a draft, including writes made by custom tools.
|
|
67
|
+
|
|
68
|
+
## API keys
|
|
69
|
+
|
|
70
|
+
Keys are created in the admin panel under MCP > API Keys. The plaintext key is
|
|
71
|
+
generated on create, stored encrypted with an HMAC index for lookup, and shown
|
|
72
|
+
to anyone who may read the key document (own keys only, by default). Each key:
|
|
73
|
+
|
|
74
|
+
- is bound to the user who created it and acts as that user: every operation
|
|
75
|
+
runs with `req.user` set to the linked user and `overrideAccess: false`, so
|
|
76
|
+
your collection access control applies unchanged;
|
|
77
|
+
- carries one checkbox per exposed collection and operation, plus one per
|
|
78
|
+
custom tool. All checkboxes default to off. A key can never enable an
|
|
79
|
+
operation the plugin config does not expose, and keys created before a
|
|
80
|
+
capability existed stay without it.
|
|
81
|
+
|
|
82
|
+
Keys authenticate only the MCP endpoint. They are deliberately not a Payload
|
|
83
|
+
auth strategy, so a key can never authenticate the REST or GraphQL API; the
|
|
84
|
+
reverse also holds: an admin session or JWT is ignored by the MCP endpoint.
|
|
85
|
+
|
|
86
|
+
Use `apiKeys.overrideCollection` to widen access (for example, admins manage
|
|
87
|
+
all keys) or add fields.
|
|
88
|
+
|
|
89
|
+
## Connecting a client
|
|
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
|
+
|
|
107
|
+
The endpoint speaks streamable HTTP with `Authorization: Bearer <key>`:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx @modelcontextprotocol/inspector
|
|
111
|
+
# transport: Streamable HTTP, URL: http://localhost:3000/api/mcpx
|
|
112
|
+
# header: Authorization: Bearer <key>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Claude Code:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
claude mcp add --transport http payload http://localhost:3000/api/mcpx \
|
|
119
|
+
--header "Authorization: Bearer <key>"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Claude Desktop (no direct HTTP header support) via `mcp-remote`:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"mcpServers": {
|
|
127
|
+
"payload": {
|
|
128
|
+
"command": "npx",
|
|
129
|
+
"args": [
|
|
130
|
+
"-y",
|
|
131
|
+
"mcp-remote",
|
|
132
|
+
"http://localhost:3000/api/mcpx",
|
|
133
|
+
"--header",
|
|
134
|
+
"Authorization: Bearer <key>"
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Tools
|
|
142
|
+
|
|
143
|
+
The surface is fixed at seven tools plus your custom ones; exposing a global
|
|
144
|
+
adds an argument, never a tool. `tools/list` reflects the key: write tools
|
|
145
|
+
disappear for read-only keys, and every `collection` and `global` enum contains
|
|
146
|
+
only the slugs the key may touch.
|
|
147
|
+
|
|
148
|
+
| Tool | Purpose | Key arguments |
|
|
149
|
+
| ------------------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
150
|
+
| `listCapabilities` | What this key may do; call first to orient. | none |
|
|
151
|
+
| `describeSchema` | Field shape of one node; `next` lists the drill-down paths. | `collection` \| `global`, `paths?`, `expand?` |
|
|
152
|
+
| `findDocuments` | Query documents. | `collection`, `where?`, `sort?`, `limit?`, `page?`, `depth?`, `select?`, `locale?`, `draft?` |
|
|
153
|
+
| `getDocument` | Read one document or a subtree of it. | `collection` + `id` \| `global`, `path?` (JSON pointer), `depth?`, `locale?`, `draft?` |
|
|
154
|
+
| `patchDocument` | Apply RFC 6902 operations to the current draft. | `collection` + `id` \| `global`, `locale`, `patches`, `expectedUpdatedAt?` |
|
|
155
|
+
| `createDocument` | Create a draft from a minimal seed. | `collection`, `locale`, `data` |
|
|
156
|
+
| `validateDocument` | Publish blockers without writing. | `collection` + `id` \| `global`, `locale` |
|
|
157
|
+
|
|
158
|
+
Rules the tools enforce and explain in their own descriptions:
|
|
159
|
+
|
|
160
|
+
- `describeSchema` paths stop at blocks fields, which list the block slugs they
|
|
161
|
+
accept; every node carries `next`, the ready-to-use paths for those blocks
|
|
162
|
+
(`/layout/sections/sectionWrapper`), so pass an entry of `next` as a `paths`
|
|
163
|
+
element to descend. A block is described as it exists at that position.
|
|
164
|
+
- Rich text paths continue the same way. A `richText` field lists the Lexical
|
|
165
|
+
node types it accepts in `nodes`, and `next` carries a path for every node
|
|
166
|
+
type that holds fields of its own: `/content/link` for a link node,
|
|
167
|
+
`/content/block/callout` and `/content/inlineBlock/badge` for the block
|
|
168
|
+
nodes. Descending returns the real field list, so a link extended through
|
|
169
|
+
`LinkFeature({ fields })` and a Lexical block are both described rather than
|
|
170
|
+
guessed. Any feature declaring `getSubFields` is picked up, custom ones
|
|
171
|
+
included. `upload` nodes are the exception: their fields depend on the
|
|
172
|
+
collection the node points at, so they are not addressable.
|
|
173
|
+
- Constraints a field declares travel with it: `minRows`/`maxRows` on arrays
|
|
174
|
+
and blocks fields, `maxLength`/`minLength` on text, `min`/`max` on numbers.
|
|
175
|
+
An array is described in its own right, so the `*` in `/items/*/title` has
|
|
176
|
+
something to read; a group or named tab only when it declares a description
|
|
177
|
+
or a constraint of its own.
|
|
178
|
+
- A `richText` field also reports `nodeOptions`, the node properties its editor
|
|
179
|
+
narrows. An editor built with `HeadingFeature({ enabledHeadingSizes: ["h4"] })`
|
|
180
|
+
answers `{ "heading": { "tag": ["h4"] } }`, and a write carrying any other
|
|
181
|
+
heading tag is refused. Lexical stores whatever tag it is given, so this is
|
|
182
|
+
the only place the restriction is checked.
|
|
183
|
+
- Field and collection `admin.description` values are included in
|
|
184
|
+
`describeSchema` and `listCapabilities`, so intent written for the admin
|
|
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.
|
|
188
|
+
- Builtin tools reject unknown arguments by name instead of silently ignoring
|
|
189
|
+
them.
|
|
190
|
+
- Every path this plugin accepts or reports is a JSON Pointer. A schema path
|
|
191
|
+
and a pointer into a document differ only in what stands in an element
|
|
192
|
+
position: a schema path writes `*` for an array element and names a block by
|
|
193
|
+
its slug, where a pointer carries a 0-based index. So `/items/*/title` is
|
|
194
|
+
written at `/items/0/title`, and `/layout/sections/hero` at
|
|
195
|
+
`/layout/sections/0`.
|
|
196
|
+
- Adding a block requires `blockType` on the value; append with `/-`.
|
|
197
|
+
- Clearing is `replace` with `null`; a list is emptied with `[]` and refuses
|
|
198
|
+
`null`. `remove` is only valid on list elements, because Payload keeps
|
|
199
|
+
fields absent from a write.
|
|
200
|
+
- Nothing in a patch batch is applied unless every operation validates first.
|
|
201
|
+
- Pass the `updatedAt` you read as `expectedUpdatedAt` so a concurrent edit is
|
|
202
|
+
refused instead of overwritten.
|
|
203
|
+
- Fields Payload maintains (`id`, `_status`, `createdAt`, `updatedAt`,
|
|
204
|
+
`deletedAt`) are never listed and never writable; `readOnly` fields are
|
|
205
|
+
listed but refused on write.
|
|
206
|
+
|
|
207
|
+
## Globals
|
|
208
|
+
|
|
209
|
+
A global is exposed the same way a collection is, and reached through the same
|
|
210
|
+
tools rather than tools of its own:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
mcpxPlugin({
|
|
214
|
+
collections: { pages: { read: true, write: true } },
|
|
215
|
+
globals: { "site-settings": { read: true, write: true } },
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Two rules follow from a global being a singleton, and because JSON Schema cannot
|
|
220
|
+
state either one, both are enforced in the handler and repeated in every
|
|
221
|
+
affected tool description:
|
|
222
|
+
|
|
223
|
+
- Pass exactly **one** of `collection` and `global`.
|
|
224
|
+
- `id` is required with `collection` and must be omitted with `global`.
|
|
225
|
+
|
|
226
|
+
Refusals name the offending argument and the slug, so one failed call teaches
|
|
227
|
+
the rule. `findDocuments` and `createDocument` stay collection-only: there is
|
|
228
|
+
nothing to list and nothing to create when the document always exists. They
|
|
229
|
+
reject a `global` argument by name.
|
|
230
|
+
|
|
231
|
+
Globals get their own `capabilities.globals.<name>` checkbox group, a separate
|
|
232
|
+
namespace from `capabilities.collections.<name>`, so a global may share a
|
|
233
|
+
camelCase name with a collection. Keys issued before a global was exposed have
|
|
234
|
+
no such group, and an absent checkbox reads as `false`, so they stay closed to
|
|
235
|
+
every global until one is ticked.
|
|
236
|
+
|
|
237
|
+
Globals always carry `updatedAt` — Payload appends it and there is no
|
|
238
|
+
`timestamps: false` for globals — so `expectedUpdatedAt` behaves as it does for
|
|
239
|
+
collections. The one exception is a global that has never been saved: it has no
|
|
240
|
+
`updatedAt` to compare against, so the first write must omit
|
|
241
|
+
`expectedUpdatedAt`, and supplying one is refused as a concurrency failure.
|
|
242
|
+
|
|
243
|
+
If `tools/list` omits `global` entirely, no global is exposed to that key; the
|
|
244
|
+
argument only appears once one is. A deployment that uses no globals sees the
|
|
245
|
+
tool schemas exactly as they were.
|
|
246
|
+
|
|
247
|
+
## Drafts and publish blockers
|
|
248
|
+
|
|
249
|
+
Draft-only writing is enforced on the Payload operation, not in the tool
|
|
250
|
+
handlers: a `beforeOperation` hook forces `draft: true` and strips `_status`
|
|
251
|
+
from every write carrying the MCP request marker, so custom tools and anything
|
|
252
|
+
else writing through the same request are covered too. A `beforeChange` hook
|
|
253
|
+
refuses any write that would still not land as a draft. Globals expose the same
|
|
254
|
+
`beforeOperation` interception point at the same position in the operation, so
|
|
255
|
+
they are guarded exactly as strongly as collections, exposed or not.
|
|
256
|
+
|
|
257
|
+
Publish blockers are advisory. Payload skips validation on draft saves (unless
|
|
258
|
+
`versions.drafts.validate` is set), so after every write the plugin re-runs
|
|
259
|
+
Payload's own field validation over the saved draft and returns the failures
|
|
260
|
+
as `publishBlockers` with paths and labels. The write stands; the client gets a
|
|
261
|
+
checklist of what remains. Three limits: only the written locale is
|
|
262
|
+
validated; field `beforeChange` hooks run again during the check, so they must
|
|
263
|
+
be pure; and the check runs privileged, so blocker paths and messages may name
|
|
264
|
+
fields the key's user cannot read (values are never included).
|
|
265
|
+
Collections with `versions.drafts.validate: true` refuse invalid drafts
|
|
266
|
+
outright; those failures come back as `validationErrors`. Both carry pointers,
|
|
267
|
+
restated from the dotted paths Payload reports internally.
|
|
268
|
+
|
|
269
|
+
Writes also report `notApplied`: pointers whose value Payload kept unchanged,
|
|
270
|
+
which happens when field-level access denies the update.
|
|
271
|
+
|
|
272
|
+
## Custom tools
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { defineMcpxTool } from "@abinnovision/payloadcms-mcpx";
|
|
276
|
+
import { z } from "zod";
|
|
277
|
+
|
|
278
|
+
const publishQueue = defineMcpxTool({
|
|
279
|
+
name: "queueForReview",
|
|
280
|
+
description: "Marks a page as ready for editorial review.",
|
|
281
|
+
inputSchema: { id: z.string() },
|
|
282
|
+
handler: async ({ args, req }) => {
|
|
283
|
+
// req.user is the key's linked user, req.context.mcpx carries the
|
|
284
|
+
// key id and capabilities. Writes through payload.update({ req })
|
|
285
|
+
// land as drafts like every other MCP write.
|
|
286
|
+
await req.payload.update({
|
|
287
|
+
collection: "pages",
|
|
288
|
+
id: args.id,
|
|
289
|
+
data: { reviewRequested: true },
|
|
290
|
+
overrideAccess: false,
|
|
291
|
+
req,
|
|
292
|
+
});
|
|
293
|
+
return { content: [{ type: "text", text: "queued" }] };
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Each custom tool gets its own checkbox on every API key, default off.
|
|
299
|
+
|
|
300
|
+
Custom tool shapes are registered as given, and the MCP SDK wraps them in a
|
|
301
|
+
non-strict object: unknown arguments are stripped before your handler runs.
|
|
302
|
+
Builtin tools reject them instead.
|
|
303
|
+
|
|
304
|
+
## Options
|
|
305
|
+
|
|
306
|
+
| Option | Default | Description |
|
|
307
|
+
| ------------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
|
308
|
+
| `collections` | required | Allow-list. `true` means `{ read: true }`. |
|
|
309
|
+
| `collections.<slug>.read` | `true` | Expose `describeSchema`, `findDocuments`, `getDocument`. |
|
|
310
|
+
| `collections.<slug>.write` | `false` | Expose `patchDocument`, `createDocument`, `validateDocument`. Requires `versions.drafts` unless `allowLiveWrites`. |
|
|
311
|
+
| `collections.<slug>.allowLiveWrites` | `false` | Permit writes to a collection without drafts (they land live). |
|
|
312
|
+
| `globals` | `{}` | Allow-list of globals. `true` means `{ read: true }`. |
|
|
313
|
+
| `globals.<slug>.read` | `true` | Expose `describeSchema`, `getDocument`. |
|
|
314
|
+
| `globals.<slug>.write` | `false` | Expose `patchDocument`, `validateDocument`. Requires `versions.drafts` unless `allowLiveWrites`. |
|
|
315
|
+
| `globals.<slug>.allowLiveWrites` | `false` | Permit writes to a global without drafts (they land live). |
|
|
316
|
+
| `userCollection` | `config.admin.user` or `users` | Auth collection the keys act as. |
|
|
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. |
|
|
319
|
+
| `apiKeys.overrideCollection` | none | Final override applied to the generated collection. |
|
|
320
|
+
| `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
|
|
321
|
+
| `limits.maxLimit` | `25` | Upper bound for `findDocuments.limit`. |
|
|
322
|
+
| `limits.maxDepth` | `1` | Upper bound for `depth` on reads. |
|
|
323
|
+
| `tools` | `[]` | Custom tools. |
|
|
324
|
+
| `auth.resolve` | none | Replace or wrap the default key resolution. |
|
|
325
|
+
| `serverInfo` | package name and version | Reported to MCP clients. |
|
|
326
|
+
|
|
327
|
+
Misconfiguration (unknown slugs, write on a collection without drafts, upload
|
|
328
|
+
collections exposed for write, tool name collisions) fails at startup with
|
|
329
|
+
`InvalidConfiguration`. Auth collections cannot be exposed at all, read
|
|
330
|
+
included: their documents carry credentials, such as the decrypted Payload API
|
|
331
|
+
key of every user.
|
|
332
|
+
|
|
333
|
+
## Security notes
|
|
334
|
+
|
|
335
|
+
- Keys are stored encrypted; lookup is by HMAC-SHA256 index derived from
|
|
336
|
+
`payload.secret`, the same scheme Payload uses for its own API keys.
|
|
337
|
+
- The endpoint authenticates with Bearer keys only; admin JWTs and cookies are
|
|
338
|
+
ignored. Keys cannot authenticate REST or GraphQL.
|
|
339
|
+
- Every operation runs under the linked user with `overrideAccess: false`.
|
|
340
|
+
- Not covered in v1: `delete` (no tool exists and none is generated), uploads.
|
|
341
|
+
Custom tools are trusted code and can do what the linked user may.
|
|
342
|
+
|
|
343
|
+
## Non-goals of v1 / roadmap
|
|
344
|
+
|
|
345
|
+
Deletes, uploads, markdown authoring for rich text, addressing a rich text node
|
|
346
|
+
by position in a patch (an editor state is written whole), schemas for `upload`
|
|
347
|
+
node fields, row addressing by id instead of index, cross-locale publish
|
|
348
|
+
blockers, pagination of `describeSchema` with `expand`, and a handler-level
|
|
349
|
+
timeout are all deliberate omissions for now.
|
|
350
|
+
|
|
351
|
+
## License
|
|
352
|
+
|
|
353
|
+
Apache-2.0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createCapabilityFields, createKeyFields, withSetupGuideTab } from "./fields.mjs";
|
|
2
|
+
import { generateApiKey, hashApiKey } from "./key.mjs";
|
|
3
|
+
//#region src/api-keys/collection.ts
|
|
4
|
+
/**
|
|
5
|
+
* Generates the key on create and keeps the lookup index in step with it.
|
|
6
|
+
* Runs after field `beforeValidate` fallbacks, so on update `data.apiKey` is the
|
|
7
|
+
* decrypted plaintext of the stored key.
|
|
8
|
+
*/ const keyBeforeChange = ({ data, operation, req }) => {
|
|
9
|
+
if (operation === "create" && typeof data["apiKey"] !== "string") data["apiKey"] = generateApiKey();
|
|
10
|
+
if (typeof data["apiKey"] === "string") data["apiKeyIndex"] = hashApiKey(req.payload.secret, data["apiKey"]);
|
|
11
|
+
return data;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The collection holding MCP API keys. It is not an auth collection on
|
|
15
|
+
* purpose: keys must only ever authenticate the MCP endpoint, never the REST
|
|
16
|
+
* or GraphQL API.
|
|
17
|
+
*/ const createApiKeysCollection = (options) => {
|
|
18
|
+
const { userCollection } = options;
|
|
19
|
+
const isUser = ({ req }) => Boolean(req.user && req.user.collection === userCollection);
|
|
20
|
+
const ownKeysOnly = ({ req }) => isUser({ req }) && req.user ? { user: { equals: req.user.id } } : false;
|
|
21
|
+
return {
|
|
22
|
+
slug: options.apiKeysSlug,
|
|
23
|
+
labels: {
|
|
24
|
+
singular: "API Key",
|
|
25
|
+
plural: "API Keys"
|
|
26
|
+
},
|
|
27
|
+
admin: {
|
|
28
|
+
group: "MCP",
|
|
29
|
+
useAsTitle: "label",
|
|
30
|
+
description: "Keys for MCP clients. Each key acts as its user and may only do what its capabilities allow."
|
|
31
|
+
},
|
|
32
|
+
access: {
|
|
33
|
+
create: isUser,
|
|
34
|
+
read: ownKeysOnly,
|
|
35
|
+
update: ownKeysOnly,
|
|
36
|
+
delete: ownKeysOnly
|
|
37
|
+
},
|
|
38
|
+
hooks: { beforeChange: [keyBeforeChange] },
|
|
39
|
+
fields: withSetupGuideTab([
|
|
40
|
+
{
|
|
41
|
+
name: "user",
|
|
42
|
+
type: "relationship",
|
|
43
|
+
relationTo: userCollection,
|
|
44
|
+
required: true,
|
|
45
|
+
access: {
|
|
46
|
+
create: () => false,
|
|
47
|
+
update: () => false
|
|
48
|
+
},
|
|
49
|
+
defaultValue: ({ req }) => isUser({ req }) ? req.user?.id : void 0,
|
|
50
|
+
admin: { description: "The user this key acts as." }
|
|
51
|
+
},
|
|
52
|
+
...createKeyFields(),
|
|
53
|
+
...createCapabilityFields(options)
|
|
54
|
+
], options)
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
export { createApiKeysCollection, keyBeforeChange };
|