payload-mcp-toolkit 0.8.0 → 0.9.0
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 +100 -237
- package/dist/api-keys.js +16 -0
- package/dist/api-keys.js.map +1 -1
- package/dist/auth-strategy.js +49 -49
- package/dist/auth-strategy.js.map +1 -1
- package/dist/components/AgentConnectPill.d.ts +3 -0
- package/dist/components/AgentConnectPill.js +121 -0
- package/dist/components/AgentConnectPill.js.map +1 -0
- package/dist/components/CollectionScopesMatrix.js.map +1 -1
- package/dist/components/ConsentPermissions.d.ts +10 -0
- package/dist/components/ConsentPermissions.js +165 -0
- package/dist/components/ConsentPermissions.js.map +1 -0
- package/dist/components/GlobalScopesMatrix.js.map +1 -1
- package/dist/components/OAuthView.d.ts +4 -0
- package/dist/components/OAuthView.js +269 -0
- package/dist/components/OAuthView.js.map +1 -0
- package/dist/components/ScopesTable.d.ts +28 -0
- package/dist/components/ScopesTable.js +165 -130
- package/dist/components/ScopesTable.js.map +1 -1
- package/dist/components/agentInstructions.d.ts +2 -0
- package/dist/components/agentInstructions.js +34 -0
- package/dist/components/agentInstructions.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/oauth.css +111 -0
- package/dist/conflict-detection.js +13 -13
- package/dist/conflict-detection.js.map +1 -1
- package/dist/draft-workflow.js +26 -26
- package/dist/draft-workflow.js.map +1 -1
- package/dist/endpoint.d.ts +5 -0
- package/dist/endpoint.js +21 -13
- package/dist/endpoint.js.map +1 -1
- package/dist/hash.js +15 -15
- package/dist/hash.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +67 -3
- package/dist/index.js.map +1 -1
- package/dist/introspection.js +47 -47
- package/dist/introspection.js.map +1 -1
- package/dist/oauth-permissions.d.ts +52 -0
- package/dist/oauth-permissions.js +100 -0
- package/dist/oauth-permissions.js.map +1 -0
- package/dist/oauth-store.d.ts +19 -0
- package/dist/oauth-store.js +122 -0
- package/dist/oauth-store.js.map +1 -0
- package/dist/oauth.d.ts +19 -0
- package/dist/oauth.js +523 -0
- package/dist/oauth.js.map +1 -0
- package/dist/prompts.js +5 -5
- package/dist/prompts.js.map +1 -1
- package/dist/registry.js +7 -1
- package/dist/registry.js.map +1 -1
- package/dist/resources.js +10 -10
- package/dist/resources.js.map +1 -1
- package/dist/scope/audit-log.js +9 -9
- package/dist/scope/audit-log.js.map +1 -1
- package/dist/scope/policy.js +7 -7
- package/dist/scope/policy.js.map +1 -1
- package/dist/tools/_helpers.d.ts +10 -0
- package/dist/tools/_helpers.js +53 -41
- package/dist/tools/_helpers.js.map +1 -1
- package/dist/tools/_layout-helpers.js +33 -33
- package/dist/tools/_layout-helpers.js.map +1 -1
- package/dist/tools/create-document.js +20 -19
- package/dist/tools/create-document.js.map +1 -1
- package/dist/tools/delete-document.js +10 -9
- package/dist/tools/delete-document.js.map +1 -1
- package/dist/tools/find-document.js +18 -15
- package/dist/tools/find-document.js.map +1 -1
- package/dist/tools/find-global.js +13 -13
- package/dist/tools/find-global.js.map +1 -1
- package/dist/tools/global-versions.js +8 -7
- package/dist/tools/global-versions.js.map +1 -1
- package/dist/tools/patch-global-layout.js +9 -9
- package/dist/tools/patch-global-layout.js.map +1 -1
- package/dist/tools/patch-layout.js +15 -14
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/tools/publish-draft.js +2 -1
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/publish-global-draft.js +4 -4
- package/dist/tools/publish-global-draft.js.map +1 -1
- package/dist/tools/resolve-reference.js.map +1 -1
- package/dist/tools/safe-delete.js +14 -14
- package/dist/tools/safe-delete.js.map +1 -1
- package/dist/tools/schedule-publish.js +21 -20
- package/dist/tools/schedule-publish.js.map +1 -1
- package/dist/tools/search-content.js +12 -12
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/update-document.js +6 -5
- package/dist/tools/update-document.js.map +1 -1
- package/dist/tools/update-global.js +9 -9
- package/dist/tools/update-global.js.map +1 -1
- package/dist/tools/upload-media.js +2 -1
- package/dist/tools/upload-media.js.map +1 -1
- package/dist/tools/versions.js +10 -8
- package/dist/tools/versions.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.js.map +1 -1
- package/docs/oauth.md +96 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# payload-mcp-toolkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Payload CMS v3 plugin that lets AI apps such as Claude and ChatGPT read and edit your content over MCP.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It reads your Payload config when the server starts and builds tools, prompts and resources from your collections, globals and blocks. It serves them at `/api/mcp`. Clients connect in one of two ways:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **API keys** for scripts and local agents. Each key has its own permissions.
|
|
8
|
+
- **Website sign-in (OAuth)** for Claude and ChatGPT connectors. Users sign in with their site account and choose what the AI may do. Optional. See [docs/oauth.md](docs/oauth.md).
|
|
9
|
+
|
|
10
|
+
Payload access control applies to every call. Tools run as the signed-in user or the key's user, with `overrideAccess: false`.
|
|
8
11
|
|
|
9
12
|
## Install
|
|
10
13
|
|
|
@@ -12,174 +15,125 @@ It is the standalone successor to the toolkit's earlier wrapper around `@payload
|
|
|
12
15
|
pnpm add payload-mcp-toolkit
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
Peer dependencies: `payload` ^3, `zod` ^3.25 or ^4.
|
|
16
|
-
`@modelcontextprotocol/sdk` 1.23 or newer, which this package depends on.)
|
|
17
|
-
|
|
18
|
-
## Configure — zero config
|
|
18
|
+
Peer dependencies: `payload` ^3, `@payloadcms/ui` ^3, `zod` ^3.25 or ^4.
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
// payload.config.ts
|
|
22
22
|
import { mcpToolkitPlugin } from 'payload-mcp-toolkit'
|
|
23
23
|
|
|
24
24
|
export default buildConfig({
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
admin: { user: 'users' }, // your auth collection
|
|
25
|
+
serverURL: process.env.SITE_URL, // used for preview links and the Host check
|
|
26
|
+
admin: { user: 'users' },
|
|
28
27
|
plugins: [mcpToolkitPlugin()],
|
|
29
28
|
})
|
|
30
29
|
```
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
This adds:
|
|
33
32
|
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
- Builds tools / prompts / resources from your introspected schema.
|
|
33
|
+
- The `payload-mcp-api-keys` collection (admin: **MCP → API Keys**) and a bearer strategy on your user collection.
|
|
34
|
+
- `POST /api/mcp`. `GET /api/mcp` returns 405.
|
|
35
|
+
- Tools, prompts and resources built from your schema.
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
The plugin works out the rest from your config:
|
|
40
38
|
|
|
41
|
-
- **
|
|
42
|
-
- **Preview
|
|
43
|
-
- **
|
|
44
|
-
- **User collection** — `admin.user`.
|
|
39
|
+
- **Drafts:** collections and globals with `versions.drafts` save changes as drafts. Others publish on save.
|
|
40
|
+
- **Preview links:** taken from `admin.livePreview.url`, or `admin.preview`.
|
|
41
|
+
- **Blocks:** every blocks field, at any depth, so the AI can build valid layouts.
|
|
45
42
|
|
|
46
|
-
|
|
43
|
+
Schema changes add database tables or columns. On Postgres or SQLite with `push: false`, generate and commit a Payload migration after installing or upgrading.
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
## API keys
|
|
49
46
|
|
|
50
|
-
|
|
47
|
+
Create a key in **MCP → API Keys**. The admin shows the full key once. After that you see only its first 8 characters.
|
|
51
48
|
|
|
52
49
|
```http
|
|
53
|
-
POST /api/mcp
|
|
54
|
-
Authorization: Bearer <
|
|
50
|
+
POST /api/mcp
|
|
51
|
+
Authorization: Bearer <key>
|
|
55
52
|
Content-Type: application/json
|
|
56
53
|
```
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
Configure each key's permissions through typed admin fields — no JSON to hand-edit.
|
|
61
|
-
|
|
62
|
-
| Field | Effect |
|
|
63
|
-
|---|---|
|
|
64
|
-
| `preset` | Role preset: **Read-only**, **Editor** (read + create + update on collections; read-only on globals — see below), **Admin** (all actions on both), or **Custom** (use the override fields below). Required. Defaults to **Custom** so new keys deny everything until explicitly scoped. Switching away from Custom **clears every override field on save** (collectionScopes, globalScopes, toolAllow, toolDeny); switching back to Custom starts from a fresh deny-all baseline — reconfigure the matrices before saving. |
|
|
65
|
-
| `collectionScopes` | Array of `{ slug, actions[] }`. Only honoured when preset is **Custom**. Each row whitelists a collection and the actions (`read` / `create` / `update` / `delete`) allowed on it. An empty `actions[]` denies all actions on that collection. Listed collections are a *whitelist* — collections not in the list are denied. (Pre-v0.6 rows using `{ collection, actions[] }` are tolerated via a one-release legacy fallback; resave them to migrate.) |
|
|
66
|
-
| `globalScopes` | Array of `{ slug, actions[] }`. Only honoured when preset is **Custom** *and* the host config has at least one global. Globals only support `read` and `update` (no `create` / `delete` — they're singletons). Same whitelist semantics as `collectionScopes`. (Pre-v0.6 rows using `{ global, actions[] }` are tolerated via the same legacy fallback.) |
|
|
67
|
-
| `toolAllow` | Multi-select. Only honoured when preset is **Custom**. If set, only these tools are callable with this key. An empty list under Custom is treated as deny-all on the tools axis **only when no collection or global scopes are set** (the fresh-Custom-key sentinel); when collection or global scopes are populated, an empty list collapses to "no tool restriction" so the resource scopes alone gate access. To deny every tool while keeping resource scopes, enumerate them in `toolDeny` instead. |
|
|
68
|
-
| `toolDeny` | Multi-select. Always applied on top of any preset. Tools listed here are blocked regardless of preset / collection / global scopes. |
|
|
69
|
-
|
|
70
|
-
The collection and tool dropdowns are populated at plugin-init time from your live Payload config + the toolkit's registered tools. Adding a collection or custom tool requires a dev-server / app restart for it to surface in the dropdowns.
|
|
71
|
-
|
|
72
|
-
The same shape is editable programmatically via Payload's REST and GraphQL APIs against the `payload-mcp-api-keys` collection — useful for seeding keys from CI or scripted provisioning.
|
|
73
|
-
|
|
74
|
-
### Lifecycle fields
|
|
75
|
-
|
|
76
|
-
| Field | Effect |
|
|
77
|
-
|---|---|
|
|
78
|
-
| `name`, `description` | Human-readable identifier in the admin list. |
|
|
79
|
-
| `expiresAt` | Authentication rejects keys past this date. |
|
|
80
|
-
| `revokedAt` | Authentication rejects keys when set. |
|
|
81
|
-
| `lastUsedAt` | Updated fire-and-forget on each successful auth. |
|
|
82
|
-
| `keyPrefix` | First 8 chars of the plaintext, for audit-log identification. |
|
|
83
|
-
|
|
84
|
-
## What the plugin adds
|
|
85
|
-
|
|
86
|
-
**Auto-generated prompts:**
|
|
55
|
+
Each key has a **preset**:
|
|
87
56
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
57
|
+
| Preset | Collections | Globals |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Read-only | read | read |
|
|
60
|
+
| Editor | read, create, update | read |
|
|
61
|
+
| Admin | read, create, update, delete | read, update |
|
|
62
|
+
| Custom | only what you tick | only what you tick |
|
|
91
63
|
|
|
92
|
-
**
|
|
64
|
+
New keys start as **Custom** with nothing ticked, so they can do nothing until you choose. Custom gives you a collection matrix, a global matrix, a tool allow list and a tool deny list. The deny list applies to every preset.
|
|
93
65
|
|
|
94
|
-
|
|
66
|
+
Editor keys cannot change globals. One bad write to a global (site name, footer, navigation) shows on every page. To let a key edit a specific global, use Custom.
|
|
95
67
|
|
|
96
|
-
|
|
97
|
-
- `createDocument` — local-API based creation for any collection. JSON-string `data`. Defaults to `draft: true` on draft-enabled collections.
|
|
98
|
-
- `updateDocument` — local-API based update. Replaces the upstream plugin's `update<Resource>` tools, which crash on collections containing richText/upload/blocks fields.
|
|
99
|
-
- `patchLayout` — surgical append/prepend/insertAt/replaceAt against any blocks-typed field. Validates each block recursively against the introspected nesting map.
|
|
100
|
-
- `uploadMedia` — fetch a public HTTPS image, validate (SSRF-safe with a streaming size cap), create a Media doc.
|
|
68
|
+
A key limited to some collections or globals also has these rules:
|
|
101
69
|
|
|
102
|
-
|
|
103
|
-
- `findDocument`
|
|
104
|
-
-
|
|
105
|
-
- `searchContent` — natural-language editor triage (status, recency, missing fields, free text).
|
|
70
|
+
- It gets linked entries as IDs only.
|
|
71
|
+
- `findDocument` refuses filters with a dotted path.
|
|
72
|
+
- Search, reference and upload tools are off, because they reach across every collection.
|
|
106
73
|
|
|
107
|
-
|
|
108
|
-
- `publishDraft` — flip `_status` from draft to published. Recovers from Payload's post-write field-validator quirk (validator throws *after* the new version row commits in some draft+versions setups): on a caught error, the tool re-reads the doc with `draft: false` and only downgrades to a "published-with-warning" response when the live row reflects the current attempt (strictly newer `updatedAt`), so a stale prior publish cannot mask a real failure.
|
|
109
|
-
- `schedulePublish` — auto-registered for collections with drafts AND a `publishedAt` date field. Stamps a future `publishedAt`; you wire up the actual flip via Payload Jobs Queue / cron / `beforeRead`.
|
|
110
|
-
- `listVersions` — recent saved versions of a draft document.
|
|
111
|
-
- `restoreVersion` — roll a document back to a saved version (creates a new version, so reversible).
|
|
112
|
-
- `safeDelete` — relationship-aware delete. Walks the relationship graph; refuses with a structured impact summary if the doc has inbound references. Override with `confirm: true`.
|
|
113
|
-
- `deleteDocument` — fast unsafe delete (no relationship walk). Use only when you know the doc has no inbound references; prefer `safeDelete` for general use.
|
|
74
|
+
Keys also have `expiresAt`, `revokedAt` and `lastUsedAt`.
|
|
114
75
|
|
|
115
|
-
|
|
116
|
-
- `findGlobal` — read any global by slug. Stamps a preview URL on draft documents when `admin.livePreview` / `admin.preview` is configured.
|
|
117
|
-
- `updateGlobal` — partial-merge update; same prose JSON contract as `updateDocument`. Draft-enabled globals default to `'always-draft'`.
|
|
118
|
-
- `patchGlobalLayout` — surgical block-array edits on any blocks-typed field inside a global, at any nesting depth (e.g. `footer.sections`). Registered only when at least one global has a blocks field.
|
|
119
|
-
- `publishGlobalDraft`, `listGlobalVersions`, `restoreGlobalVersion` — registered only for globals with `versions: { drafts: true }`. `publishGlobalDraft` uses the same post-write validation recovery as `publishDraft`, with `fallbackLocale: false` on the verify read so localized globals report the literal `_status` of the requested locale.
|
|
76
|
+
## Website sign-in (OAuth)
|
|
120
77
|
|
|
121
|
-
|
|
78
|
+
```ts
|
|
79
|
+
mcpToolkitPlugin({
|
|
80
|
+
oauth: {
|
|
81
|
+
canAuthorize: ({ user }) => user?.role === 'admin', // who may connect; checked on every request
|
|
82
|
+
access: 'editor', // most the site allows; default 'read-only'
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
```
|
|
122
86
|
|
|
123
|
-
|
|
87
|
+
Users add `https://YOUR-SITE/api/mcp` as a custom connector in Claude, or in ChatGPT with Developer mode on. Then they sign in and approve access. The approve screen offers the same choices as a Custom key, capped at `access`. Delete and global writes are never allowed this way.
|
|
124
88
|
|
|
125
|
-
|
|
89
|
+
The plugin also adds:
|
|
126
90
|
|
|
127
|
-
|
|
91
|
+
- `/admin/mcp-connections`, where users copy the connector URL and agent instructions, and disconnect.
|
|
92
|
+
- A compact "Connect your AI agent" prompt in the admin sidebar.
|
|
128
93
|
|
|
129
|
-
|
|
94
|
+
Setup needs a migration, discovery rewrites and two security headers. See [docs/oauth.md](docs/oauth.md).
|
|
130
95
|
|
|
131
|
-
##
|
|
96
|
+
## Tools
|
|
132
97
|
|
|
133
|
-
|
|
98
|
+
Tools for versions and publishing appear only for collections and globals with drafts. Global tools appear only when the config has globals.
|
|
134
99
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
mediaUpload: { maxFileSize: 25 * 1024 * 1024, collectionSlug: 'images' },
|
|
157
|
-
domainPrompts: [
|
|
158
|
-
{ name: 'siteVocabulary', title: 'Site Vocabulary', description: 'Site-specific terms.', content: '...' },
|
|
159
|
-
],
|
|
160
|
-
})
|
|
161
|
-
```
|
|
100
|
+
| Tool | What it does |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `findDocument` | Read by ID or `where` filter. Draft results include a preview link. |
|
|
103
|
+
| `searchContent` | Find entries by text, status, recent changes or missing fields. |
|
|
104
|
+
| `resolveReference` | Look up IDs by name, title or slug, for relationship fields. |
|
|
105
|
+
| `createDocument` | Create an entry. Draft collections save a draft. |
|
|
106
|
+
| `updateDocument` | Update an entry, including rich text, upload and blocks fields. |
|
|
107
|
+
| `patchLayout` | Append, insert or replace blocks in a blocks field. Checks each block against your schema. |
|
|
108
|
+
| `uploadMedia` | Fetch a public HTTPS image and create a media entry. Size-capped while downloading. |
|
|
109
|
+
| `publishDraft`, `schedulePublish` | Publish a draft now, or set a future `publishedAt`. You run the scheduled publish yourself (Jobs Queue or cron). |
|
|
110
|
+
| `listVersions`, `restoreVersion` | List saved versions and roll back. |
|
|
111
|
+
| `safeDelete` | Delete only if nothing links to the entry, unless `confirm: true`. |
|
|
112
|
+
| `deleteDocument` | Delete without checking links. |
|
|
113
|
+
| `findGlobal`, `updateGlobal`, `patchGlobalLayout` | The same for globals. |
|
|
114
|
+
| `publishGlobalDraft`, `listGlobalVersions`, `restoreGlobalVersion` | Draft tools for globals. |
|
|
115
|
+
|
|
116
|
+
**Prompts:** `contentModelOverview`, `blockCompositionGuide`, `draftWorkflowGuide`.
|
|
117
|
+
|
|
118
|
+
**Resources:** `collections://schema`, `collections://relationships`, `blocks://catalog`, `blocks://nesting`, `globals://schema`.
|
|
119
|
+
|
|
120
|
+
## Options
|
|
162
121
|
|
|
163
122
|
| Option | Description |
|
|
164
123
|
|---|---|
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `preview.disabled` |
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `mediaUpload.collectionSlug` | Default `'media'`. |
|
|
176
|
-
| `customTools` | Extra tools registered alongside the built-ins. See [Custom tools](#custom-tools). |
|
|
124
|
+
| `oauth` | Website sign-in. See above. |
|
|
125
|
+
| `exclude.collections`, `exclude.globals` | Hide collections and globals from MCP. |
|
|
126
|
+
| `customTools` | Your own tools. See below. |
|
|
127
|
+
| `draftBehavior` | Per-collection override, for example `{ posts: 'always-publish' }`. |
|
|
128
|
+
| `preview.siteUrl`, `preview.disabled` | Base URL for preview links (default `serverURL`), or turn them off. |
|
|
129
|
+
| `mediaUpload.maxFileSize`, `mediaUpload.collectionSlug` | Default 10 MB and `'media'`. |
|
|
130
|
+
| `domainPrompts` | Extra prompts with site vocabulary. |
|
|
131
|
+
| `userCollection` | Override `admin.user`. |
|
|
132
|
+
| `apiKeyCollection.slug`, `apiKeyCollection.userCollection` | Rename the keys collection or link keys to another user collection. |
|
|
133
|
+
| `auth.allowedOrigins` | Origins allowed by the `Origin` check. Unset means server-to-server only. |
|
|
177
134
|
|
|
178
135
|
## Custom tools
|
|
179
136
|
|
|
180
|
-
Pass your own tools through `customTools` and they register next to the built-in
|
|
181
|
-
ones:
|
|
182
|
-
|
|
183
137
|
```ts
|
|
184
138
|
import { mcpToolkitPlugin, jsonResponse, type ToolFactoryOutput } from 'payload-mcp-toolkit'
|
|
185
139
|
import { z } from 'zod'
|
|
@@ -188,137 +142,46 @@ const countActiveMembers: ToolFactoryOutput = {
|
|
|
188
142
|
name: 'countActiveMembers',
|
|
189
143
|
description: 'Number of members with an active membership.',
|
|
190
144
|
parameters: { since: z.string().optional().describe('ISO date.') },
|
|
191
|
-
// 'account', not 'collection': the target is hard-coded in the handler, so
|
|
192
|
-
// there is no argument for the scope check to read. See Scope routing below.
|
|
193
145
|
routing: { kind: 'account', action: 'read' },
|
|
194
146
|
handler: async (args, req) => {
|
|
195
|
-
const { totalDocs } = await req.payload.count({
|
|
196
|
-
collection: 'memberships',
|
|
197
|
-
user: req.user,
|
|
198
|
-
overrideAccess: false,
|
|
199
|
-
})
|
|
147
|
+
const { totalDocs } = await req.payload.count({ collection: 'memberships', user: req.user, overrideAccess: false })
|
|
200
148
|
return jsonResponse({ totalDocs })
|
|
201
149
|
},
|
|
202
150
|
}
|
|
203
151
|
|
|
204
|
-
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
What you get for free:
|
|
208
|
-
|
|
209
|
-
- **The same wrapper as the built-ins** — the scope check runs before your
|
|
210
|
-
handler, `req.context.source` is stamped `'mcp'`, and every call (success,
|
|
211
|
-
failure, scope rejection) lands in the structured audit log.
|
|
212
|
-
- **A slot in the API-key scope dropdowns** — your tool name appears in
|
|
213
|
-
**Tool allow** / **Tool deny** alongside the built-ins.
|
|
214
|
-
- **A boot-time name check** — reusing a built-in name throws instead of
|
|
215
|
-
silently shadowing that tool.
|
|
216
|
-
|
|
217
|
-
The field-by-field contract:
|
|
218
|
-
|
|
219
|
-
| Field | Notes |
|
|
220
|
-
|---|---|
|
|
221
|
-
| `name` | Must be unique across built-in and custom tools. |
|
|
222
|
-
| `description` | Shown to the model in `tools/list`. Say when to reach for it. |
|
|
223
|
-
| `parameters` | A raw Zod shape (`{ key: z.string() }`) or a `z.object({...})`. Both are accepted. |
|
|
224
|
-
| `routing` | `{kind, action}` — which scope axis gates the tool. `kind` is `'collection'`, `'global'`, or `'account'`. |
|
|
225
|
-
| `handler` | `(args, req, extra) => McpTextResponse`. Read `req.payload` / `req.user` per call; do not close over them at boot. |
|
|
226
|
-
|
|
227
|
-
### Scope routing
|
|
228
|
-
|
|
229
|
-
Scope routing reads the target resource from the call's own arguments. A
|
|
230
|
-
`collection`-routed tool **must** take a required `collection` argument (a
|
|
231
|
-
`global`-routed tool, a required `slug`); the registry reads that value to
|
|
232
|
-
decide whether the key's scopes permit the call.
|
|
233
|
-
|
|
234
|
-
A `collection`- or `global`-routed tool called without that argument is
|
|
235
|
-
**denied**, whatever the key's scopes say. There is no target to check, so the
|
|
236
|
-
check cannot pass. Use `routing.kind: 'account'` for a tool whose target is
|
|
237
|
-
fixed in the handler or spans the whole install — account-routed tools are
|
|
238
|
-
gated by the key's preset instead.
|
|
239
|
-
|
|
240
|
-
Making the argument optional is the trap: the call then reaches the scope check
|
|
241
|
-
with no target and is refused every time.
|
|
242
|
-
|
|
243
|
-
Run queries as the authenticated user (`user: req.user, overrideAccess: false`)
|
|
244
|
-
so Payload's own access rules still apply inside the tool. `overrideAccess:
|
|
245
|
-
true` hands an MCP client more reach than the user behind its API key.
|
|
246
|
-
|
|
247
|
-
## Upgrading from 0.7.0
|
|
248
|
-
|
|
249
|
-
v0.7.1 is a patch release; no API or breaking config changes. The behavioural changes worth knowing:
|
|
250
|
-
|
|
251
|
-
- **Preset-switch clears overrides on save.** Switching an API key away from Custom now nulls `collectionScopes`, `globalScopes`, `toolAllow`, and `toolDeny` on save (admin UI conditional-field trap fix — previously, stale Custom-era values silently survived the switch and continued to narrow access). Switching back to Custom starts from a fresh deny-all baseline; reconfigure the matrices before saving.
|
|
252
|
-
- **Empty `toolAllow` under Custom + populated resource scopes no longer denies all tools.** When the key carries collection or global scopes and `toolAllow` is empty, it is treated as "no tool restriction" so the resource scopes alone determine what is callable. The fresh-Custom-key sentinel (no scopes anywhere → deny-all) still applies.
|
|
253
|
-
- **Legacy non-Custom rows with populated overrides emit a one-time warn.** Keys persisted before v0.7.1 that carry populated `collectionScopes` / `globalScopes` / `toolAllow` arrays under a non-Custom preset still narrow access as written (fail-closed safe), but `composeScopes` now logs `mcp.auth.legacy_non_custom_override` once per process to flag them for audit. Re-save affected keys in admin to align persisted state with the v0.7.1 semantics.
|
|
254
|
-
- **Publish tools recover from Payload's post-write validator throw deterministically.** Both `publishDraft` and `publishGlobalDraft` snapshot the document's `updatedAt` before the update and only downgrade a caught error to a `[publishDraft:published_with_warning]` / `[publishGlobalDraft:published_with_warning]` response when the live row reflects the current attempt (strictly newer `updatedAt`). MCP clients can branch on the stable token prefix without regex-matching prose.
|
|
255
|
-
|
|
256
|
-
## Upgrading from 0.6
|
|
257
|
-
|
|
258
|
-
v0.7 renames the exported plugin factory so the public symbol matches the package name. Pure rename — no options, runtime behaviour, or scope semantics changed.
|
|
259
|
-
|
|
260
|
-
```diff
|
|
261
|
-
- import { contentToolkitPlugin } from 'payload-mcp-toolkit'
|
|
262
|
-
+ import { mcpToolkitPlugin } from 'payload-mcp-toolkit'
|
|
263
|
-
|
|
264
|
-
- plugins: [contentToolkitPlugin()],
|
|
265
|
-
+ plugins: [mcpToolkitPlugin()],
|
|
152
|
+
mcpToolkitPlugin({ customTools: [countActiveMembers] })
|
|
266
153
|
```
|
|
267
154
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
v0.6 adds globals support across the MCP surface. The changes most likely to surprise an upgrade:
|
|
271
|
-
|
|
272
|
-
- **`editor` preset is read-only on globals.** Editor-tier keys cannot `updateGlobal` or `patchGlobalLayout`. Use the `admin` preset or a Custom key with explicit `globalScopes` for editor-tier writes. See [Why `editor` is read-only on globals](#why-editor-is-read-only-on-globals) for the rationale.
|
|
273
|
-
- **Audit log field rename.** The per-tool audit field `collectionArg` is replaced by `targetSlug` + `targetKind` (`'collection' | 'global' | 'account' | undefined`). Operators with SIEM rules / dashboards filtering on `collectionArg` must update their queries. The old field is gone — there is no compatibility alias, because the original field misreported for global operations.
|
|
274
|
-
- **`tools.allow` without an explicit resource scope is now a deny.** Previously `tools: { allow: ['updateDocument'] }` with no `collections` map and no preset implicitly allowed `updateDocument` on every collection. The fix lands now and applies symmetrically across collections and globals. If your keys rely on the `tools.allow`-only shape (not a documented configuration), add an explicit `collections` / `globals` map or a `preset`.
|
|
275
|
-
- **Production deploys need a migration.** Run `pnpm payload migrate:create` after upgrading to capture the new `globalScopes` JSONB column on `payload-mcp-api-keys`. Local dev with `push: true` syncs on the next `pnpm dev`.
|
|
155
|
+
Custom tools go through the same permission check and audit log as the built-in tools. They also appear in the API-key and sign-in tool lists.
|
|
276
156
|
|
|
277
|
-
|
|
157
|
+
- **`routing`** says which permission gates the tool.
|
|
158
|
+
- `collection` tools must take a required `collection` argument.
|
|
159
|
+
- `global` tools must take a required `slug`.
|
|
160
|
+
- Use `account` when the target is fixed in the handler or spans the whole site. Without the argument, a collection or global tool is always denied.
|
|
161
|
+
- **`handler`** should read `req.payload` and `req.user` on each call. Query with `user: req.user, overrideAccess: false`.
|
|
162
|
+
- **Names** must be unique. Reusing a built-in name throws at startup.
|
|
278
163
|
|
|
279
|
-
|
|
164
|
+
## Upgrading
|
|
280
165
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
- import { mcpPlugin } from '@payloadcms/plugin-mcp'
|
|
284
|
-
- // ...
|
|
285
|
-
- plugins: [mcpToolkitPlugin(), mcpPlugin({ ... })],
|
|
286
|
-
+ plugins: [mcpToolkitPlugin()],
|
|
287
|
-
```
|
|
288
|
-
2. **Drop the dependency** from `package.json`:
|
|
289
|
-
```bash
|
|
290
|
-
pnpm remove @payloadcms/plugin-mcp
|
|
291
|
-
```
|
|
292
|
-
3. **Existing API keys keep authenticating zero-touch.** The `payload-mcp-api-keys` slug, `apiKey` / `apiKeyIndex` columns, and HMAC formula are all preserved.
|
|
293
|
-
4. **Re-scope each key** — see the [API keys](#api-keys) section. Open each existing key in admin, pick a preset (or **Custom** with explicit collection / tool overrides), and save. Until you do, keys carry no scopes and authenticate at full access.
|
|
294
|
-
5. **Browser MCP clients are not yet fully supported.** Server-to-server callers (no `Origin` header — backend scripts, Claude Desktop's local connector) work as before and require no opt-in. Browser-based clients additionally need CORS response headers and `OPTIONS` preflight handling, which haven't landed yet — see [Known limitations](#known-limitations).
|
|
295
|
-
|
|
296
|
-
If you forget step 1, the plugin throws on boot with the same message — it refuses to register two MCP plugins racing for the `payload-mcp-api-keys` slug.
|
|
166
|
+
- **0.8 → 0.9:** OAuth is new and off by default. Without it, nothing changes for API-key sites, except one rule. Keys limited to some collections or globals now get linked entries as IDs only, and `findDocument` refuses dotted filter paths for them. Regenerate your import map.
|
|
167
|
+
- **Older versions:** see [CHANGELOG.md](CHANGELOG.md).
|
|
297
168
|
|
|
298
169
|
## Known limitations
|
|
299
170
|
|
|
300
|
-
-
|
|
171
|
+
- Browser-based MCP clients are not supported yet. The endpoint does not send CORS headers or answer `OPTIONS` preflight. Server-to-server clients and hosted connectors (Claude, ChatGPT) are not affected.
|
|
301
172
|
|
|
302
173
|
## Development
|
|
303
174
|
|
|
304
|
-
|
|
175
|
+
The `dev/` folder is a working Payload and Next.js app that uses the plugin source directly.
|
|
305
176
|
|
|
306
177
|
```bash
|
|
307
178
|
pnpm install
|
|
308
179
|
cp dev/.env.example dev/.env
|
|
309
|
-
pnpm dev
|
|
310
|
-
pnpm test
|
|
311
|
-
pnpm build
|
|
180
|
+
pnpm dev # http://localhost:3000
|
|
181
|
+
pnpm test
|
|
182
|
+
pnpm build
|
|
312
183
|
```
|
|
313
184
|
|
|
314
|
-
The dev harness ships with a realistic CMS schema:
|
|
315
|
-
|
|
316
|
-
- `Pages` — block-based layout (FullWidth, TwoColumn, CtaBanner, HeadingOnly), drafts enabled.
|
|
317
|
-
- `Posts` — title/slug/excerpt/content/cover/category/authors/tags/SEO, drafts enabled.
|
|
318
|
-
- `Authors`, `Categories`, `Media`, `Users` — taxonomy + auth.
|
|
319
|
-
- `SiteSettings` — global with site name, logo, social, footer.
|
|
320
|
-
- 5 leaf blocks (Heading, RichText, Image, ButtonGroup, Quote) and 4 section blocks.
|
|
321
|
-
|
|
322
185
|
## License
|
|
323
186
|
|
|
324
187
|
MIT
|
package/dist/api-keys.js
CHANGED
|
@@ -152,6 +152,22 @@ const isCustomPreset = (data)=>!!data && typeof data === 'object' && data.preset
|
|
|
152
152
|
disableLocalStrategy: true,
|
|
153
153
|
useAPIKey: true
|
|
154
154
|
},
|
|
155
|
+
// Every MCP request authenticates by looking up this column. Payload's
|
|
156
|
+
// `useAPIKey` adds `apiKeyIndex` but does not index it, so each call cost a
|
|
157
|
+
// sequential scan over the whole key table — and the table only grows.
|
|
158
|
+
// Declared here rather than added by hand in a host migration, so Payload's
|
|
159
|
+
// schema builder knows about it and never offers to drop it.
|
|
160
|
+
//
|
|
161
|
+
// Not unique: rows exist with a null `apiKeyIndex` (a key row saved before
|
|
162
|
+
// `enableAPIKey` is ticked), and a unique index would let only one of them
|
|
163
|
+
// exist at a time.
|
|
164
|
+
indexes: [
|
|
165
|
+
{
|
|
166
|
+
fields: [
|
|
167
|
+
'apiKeyIndex'
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
],
|
|
155
171
|
hooks: {
|
|
156
172
|
beforeValidate: [
|
|
157
173
|
({ data, originalDoc })=>{
|
package/dist/api-keys.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api-keys.ts"],"sourcesContent":["import type { CollectionBeforeValidateHook, CollectionConfig, Field } from 'payload'\n\nexport const API_KEYS_DEFAULT_SLUG = 'payload-mcp-api-keys'\n\nexport interface CreateApiKeysCollectionOptions {\n /**\n * Collection slug. Defaults to `payload-mcp-api-keys` for zero-touch\n * compatibility with rows created by `@payloadcms/plugin-mcp` v0.3.x.\n */\n slug?: string\n /**\n * Slug of the user collection that API keys link to. Required.\n */\n userCollection: string\n /**\n * Collection slugs offered to the collection-scopes matrix component.\n * Snapshotted at plugin-init time from the host Payload config; adding a\n * collection requires a dev-server restart for it to surface in the\n * admin UI.\n */\n availableCollections: string[]\n /**\n * Tool names offered as options for the `toolAllow` / `toolDeny` selects.\n * Sourced from the toolkit's registered tools at plugin init.\n */\n availableTools: string[]\n /**\n * Global slugs offered to the global-scopes matrix component. Optional —\n * direct callers of the factory that pre-date globals support continue\n * to work; sites with no globals get an empty array and the second\n * matrix table is not rendered.\n */\n availableGlobals?: string[]\n}\n\nconst PRESET_OPTIONS = [\n { label: 'Read-only', value: 'read-only' },\n { label: 'Editor (read + create + update)', value: 'editor' },\n { label: 'Admin (all actions)', value: 'admin' },\n { label: 'Custom (use overrides below)', value: 'custom' },\n] as const\n\nconst isCustomPreset = (data: unknown): boolean =>\n !!data && typeof data === 'object' && (data as { preset?: unknown }).preset === 'custom'\n\n/**\n * Builds the `payload-mcp-api-keys` collection used by the v0.4 standalone\n * plugin. Reuses Payload's built-in `useAPIKey: true` so the underlying\n * `apiKey` / `apiKeyIndex` columns match what `@payloadcms/plugin-mcp`\n * v0.3.x wrote — existing rows authenticate without re-issue.\n *\n * Layout:\n * - Main column: name, description, preset, scopes matrix (custom only),\n * tools collapsible (custom only).\n * - Sidebar: user relationship, key prefix, expiresAt, revokedAt,\n * lastUsedAt — identity + lifecycle metadata kept out of the\n * scope-editing flow.\n */\nexport function createApiKeysCollection(\n options: CreateApiKeysCollectionOptions,\n): CollectionConfig {\n if (!options || !options.userCollection) {\n throw new Error(\n 'createApiKeysCollection: `userCollection` is required (slug of the user collection that owns API keys).',\n )\n }\n if (!Array.isArray(options.availableCollections)) {\n throw new Error(\n 'createApiKeysCollection: `availableCollections` is required (slugs of collections that scope overrides may target).',\n )\n }\n if (!Array.isArray(options.availableTools)) {\n throw new Error(\n 'createApiKeysCollection: `availableTools` is required (names of registered MCP tools).',\n )\n }\n\n const slug = options.slug ?? API_KEYS_DEFAULT_SLUG\n const toolOptions = options.availableTools.map((t) => ({ label: t, value: t }))\n const availableGlobals = Array.isArray(options.availableGlobals)\n ? options.availableGlobals\n : []\n\n const presetField: Field = {\n name: 'preset',\n type: 'select',\n required: true,\n defaultValue: 'custom',\n options: PRESET_OPTIONS as unknown as { label: string; value: string }[],\n admin: {\n description:\n 'Role preset. \"Custom\" unlocks the per-collection matrix and the tool overrides below. ' +\n 'Switching away from Custom CLEARS every override on save (collectionScopes, globalScopes, ' +\n 'toolAllow, toolDeny); switching back to Custom starts from a fresh deny-all baseline, so ' +\n 'reconfigure the matrices and tool lists before saving.',\n },\n }\n\n // Stored shape: Array<{ slug: string; actions: ('read'|'create'|'update'|'delete')[] }>\n // The default Payload UI for an `array` would force users to add rows\n // one at a time; the custom matrix component renders all available\n // collections at once with a checkbox grid (rows × actions).\n //\n // `availableCollections` is forwarded via `clientProps` — Payload v3's\n // sanctioned escape hatch for serializable static data that the client\n // component needs at render time.\n const collectionScopesField: Field = {\n name: 'collectionScopes',\n type: 'json',\n admin: {\n condition: isCustomPreset,\n components: {\n Field: {\n path: 'payload-mcp-toolkit/client',\n exportName: 'CollectionScopesMatrix',\n clientProps: {\n availableCollections: options.availableCollections,\n },\n },\n },\n },\n }\n\n // Mirrors `collectionScopes` exactly — one additive JSONB column with a\n // default of `'[]'`, default-rendered by `GlobalScopesMatrix`. Hidden\n // under non-custom presets. Stored shape:\n // Array<{ slug: string; actions: ('read'|'update')[] }>\n // No `availableGlobals.length > 0` gate: `ScopesTable` renders its own\n // empty-state message when zero items are passed, so the field surfaces\n // under Custom regardless of host config, matching the collection variant.\n const globalScopesField: Field = {\n name: 'globalScopes',\n type: 'json',\n admin: {\n condition: isCustomPreset,\n components: {\n Field: {\n path: 'payload-mcp-toolkit/client',\n exportName: 'GlobalScopesMatrix',\n clientProps: {\n availableGlobals,\n },\n },\n },\n },\n }\n\n const toolsCollapsible: Field = {\n type: 'collapsible',\n label: 'Tool overrides',\n admin: {\n condition: isCustomPreset,\n description:\n 'Per-tool whitelist / blacklist. Layered on top of preset and collection scopes.',\n initCollapsed: true,\n },\n fields: [\n {\n name: 'toolAllow',\n type: 'select',\n hasMany: true,\n options: toolOptions,\n admin: {\n description:\n 'If set, only these tools are callable with this key. Leave empty to allow any tool ' +\n 'the collection or global scopes permit. Under the Custom preset, an empty list is ' +\n 'treated as deny-all ONLY when no collection or global scopes are set (the fresh- ' +\n 'Custom-key sentinel); when collection or global scopes are populated, an empty list ' +\n 'collapses to \"no tool restriction\" so the resource scopes alone determine what is ' +\n 'callable — to deny every tool while keeping resource scopes, enumerate them in ' +\n 'toolDeny instead. Preset-mode keys created via the REST API with an empty list are ' +\n 'coerced to \"no restriction\".',\n },\n },\n {\n name: 'toolDeny',\n type: 'select',\n hasMany: true,\n options: toolOptions,\n admin: {\n description: 'These tools are blocked regardless of any other scope.',\n },\n },\n ],\n }\n\n return {\n slug,\n admin: {\n group: 'MCP',\n useAsTitle: 'name',\n description:\n 'API keys for MCP clients. Scopes control which collections and tools each key can access.',\n defaultColumns: ['name', 'user', 'keyPrefix', 'preset', 'lastUsedAt', 'expiresAt', 'revokedAt'],\n },\n auth: {\n disableLocalStrategy: true,\n useAPIKey: true,\n },\n hooks: {\n beforeValidate: [\n (({ data, originalDoc }) => {\n // The override fields (collectionScopes, globalScopes, toolAllow,\n // toolDeny) are conditionally rendered only under the Custom preset\n // (`condition: isCustomPreset`). Under any other preset they are\n // hidden in the admin UI, which means two things:\n //\n // 1. The admin form omits hidden fields from its payload on save,\n // so `data` only carries the visible fields — we can't\n // \"collapse the empty array we see in `data`\" because we never\n // see it at all. The stale value lives on `originalDoc`.\n // 2. A Custom→Admin switch silently keeps the prior\n // `toolAllow:[...]` / `collectionScopes:[...]`, and\n // `composeScopes` then emits a scope gate that rejects calls\n // the user clearly intended to allow.\n //\n // Fix: when the preset is non-Custom, explicitly write `null` into\n // `data` for every override axis (regardless of what `data` carries\n // or what originalDoc holds). Payload persists nulls, so the stale\n // values are erased on every save. The Custom-preset branch below\n // keeps the explicit-empty-means-deny semantic intact.\n if (!data) return data\n const d = data as Record<string, unknown>\n const orig = (originalDoc ?? {}) as Record<string, unknown>\n const preset = d.preset ?? orig.preset\n\n // `readField` falls through to originalDoc when `data` omits the\n // key entirely (admin form skipping hidden fields), but honours\n // an explicit null/empty in `data` over originalDoc.\n const readField = (key: string): unknown =>\n key in d ? d[key] : orig[key]\n const isNonEmptyArray = (v: unknown): boolean =>\n Array.isArray(v) && v.length > 0\n // Null, undefined, or `[]` — the three shapes that mean \"the user\n // expressed no tool restriction\". Anything else (a bare string, a\n // number) is malformed and must reach Payload's validator rather than\n // being silently read as \"no restriction\".\n const isUnset = (v: unknown): boolean =>\n v === null || v === undefined || (Array.isArray(v) && v.length === 0)\n\n const OVERRIDE_AXES = [\n 'collectionScopes',\n 'globalScopes',\n 'toolAllow',\n 'toolDeny',\n ] as const\n\n if (preset !== 'custom') {\n for (const axis of OVERRIDE_AXES) d[axis] = null\n return data\n }\n\n // Custom preset: the Tools collapsible is labelled as an *override*\n // layered on top of collection / global scopes, and its description\n // says \"Leave empty to allow any tool the collection scopes permit.\"\n // Payload's hasMany-select default of `[]` would otherwise turn the\n // Tools section into a mandatory whitelist — a user who configures\n // collection scopes and never opens the collapsible would silently\n // store `toolAllow:[]`, which `composeScopes` honours as deny-all on\n // the tools axis and rejects every call.\n //\n // Resolve the mismatch by coercing a non-populated `toolAllow` to\n // null whenever the key carries any concrete resource scope\n // (collection or global entries). \"Non-populated\" covers both `[]`\n // (what the admin form sends) and a missing key (what a key created\n // through the Local API sends) — Payload reads an unset hasMany\n // select back as `[]` either way, so without this a scripted key\n // with collection scopes and no tool list would deny every tool.\n // The fresh-Custom-key sentinel in `composeScopes` still covers the\n // \"no scopes at all\" case (everything null → deny-all), so users who\n // genuinely want deny-all do not regress.\n const hasResourceScope =\n isNonEmptyArray(readField('collectionScopes')) ||\n isNonEmptyArray(readField('globalScopes'))\n if (hasResourceScope && isUnset(readField('toolAllow'))) {\n d.toolAllow = null\n }\n return data\n }) as CollectionBeforeValidateHook,\n ],\n },\n labels: {\n plural: 'API Keys',\n singular: 'API Key',\n },\n fields: [\n // Main column.\n {\n name: 'name',\n type: 'text',\n required: true,\n admin: { description: 'Human label for this key (e.g. \"Editorial team — Claude Desktop\").' },\n },\n {\n name: 'description',\n type: 'textarea',\n admin: { description: 'Optional notes about the purpose of this key.' },\n },\n presetField,\n collectionScopesField,\n globalScopesField,\n toolsCollapsible,\n\n // Sidebar — identity + lifecycle.\n {\n name: 'user',\n type: 'relationship',\n relationTo: options.userCollection,\n required: true,\n admin: {\n position: 'sidebar',\n description:\n 'The user this key authenticates as. Tool calls use this user for access checks on target collections.',\n },\n },\n {\n name: 'keyPrefix',\n type: 'text',\n index: true,\n admin: {\n position: 'sidebar',\n readOnly: true,\n description:\n 'First 8 characters of the API key — used in audit logs to identify the key without exposing the full secret.',\n },\n hooks: {\n beforeChange: [\n ({ data, originalDoc, value }) => {\n if (typeof value === 'string' && value.length > 0) return value\n const incomingKey = (data as { apiKey?: unknown } | undefined)?.apiKey\n if (typeof incomingKey === 'string' && incomingKey.length >= 8) {\n return incomingKey.slice(0, 8)\n }\n const existing = (originalDoc as { keyPrefix?: unknown } | undefined)?.keyPrefix\n return typeof existing === 'string' ? existing : undefined\n },\n ],\n },\n },\n {\n name: 'expiresAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n description: 'Optional expiry. Requests authenticated with an expired key are rejected.',\n },\n },\n {\n name: 'revokedAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n description: 'Set to revoke a key. Revoked keys are rejected at auth time.',\n },\n },\n {\n name: 'lastUsedAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n readOnly: true,\n description:\n 'Updated on each successful authentication. Fire-and-forget; not on the request hot path.',\n },\n },\n ],\n }\n}\n"],"names":["API_KEYS_DEFAULT_SLUG","PRESET_OPTIONS","label","value","isCustomPreset","data","preset","createApiKeysCollection","options","userCollection","Error","Array","isArray","availableCollections","availableTools","slug","toolOptions","map","t","availableGlobals","presetField","name","type","required","defaultValue","admin","description","collectionScopesField","condition","components","Field","path","exportName","clientProps","globalScopesField","toolsCollapsible","initCollapsed","fields","hasMany","group","useAsTitle","defaultColumns","auth","disableLocalStrategy","useAPIKey","hooks","beforeValidate","originalDoc","d","orig","readField","key","isNonEmptyArray","v","length","isUnset","undefined","OVERRIDE_AXES","axis","hasResourceScope","toolAllow","labels","plural","singular","relationTo","position","index","readOnly","beforeChange","incomingKey","apiKey","slice","existing","keyPrefix"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/api-keys.ts"],"sourcesContent":["import type { CollectionBeforeValidateHook, CollectionConfig, Field } from 'payload'\n\nexport const API_KEYS_DEFAULT_SLUG = 'payload-mcp-api-keys'\n\nexport interface CreateApiKeysCollectionOptions {\n /**\n * Collection slug. Defaults to `payload-mcp-api-keys` for zero-touch\n * compatibility with rows created by `@payloadcms/plugin-mcp` v0.3.x.\n */\n slug?: string\n /**\n * Slug of the user collection that API keys link to. Required.\n */\n userCollection: string\n /**\n * Collection slugs offered to the collection-scopes matrix component.\n * Snapshotted at plugin-init time from the host Payload config; adding a\n * collection requires a dev-server restart for it to surface in the\n * admin UI.\n */\n availableCollections: string[]\n /**\n * Tool names offered as options for the `toolAllow` / `toolDeny` selects.\n * Sourced from the toolkit's registered tools at plugin init.\n */\n availableTools: string[]\n /**\n * Global slugs offered to the global-scopes matrix component. Optional —\n * direct callers of the factory that pre-date globals support continue\n * to work; sites with no globals get an empty array and the second\n * matrix table is not rendered.\n */\n availableGlobals?: string[]\n}\n\nconst PRESET_OPTIONS = [\n { label: 'Read-only', value: 'read-only' },\n { label: 'Editor (read + create + update)', value: 'editor' },\n { label: 'Admin (all actions)', value: 'admin' },\n { label: 'Custom (use overrides below)', value: 'custom' },\n] as const\n\nconst isCustomPreset = (data: unknown): boolean =>\n !!data && typeof data === 'object' && (data as { preset?: unknown }).preset === 'custom'\n\n/**\n * Builds the `payload-mcp-api-keys` collection used by the v0.4 standalone\n * plugin. Reuses Payload's built-in `useAPIKey: true` so the underlying\n * `apiKey` / `apiKeyIndex` columns match what `@payloadcms/plugin-mcp`\n * v0.3.x wrote — existing rows authenticate without re-issue.\n *\n * Layout:\n * - Main column: name, description, preset, scopes matrix (custom only),\n * tools collapsible (custom only).\n * - Sidebar: user relationship, key prefix, expiresAt, revokedAt,\n * lastUsedAt — identity + lifecycle metadata kept out of the\n * scope-editing flow.\n */\nexport function createApiKeysCollection(\n options: CreateApiKeysCollectionOptions,\n): CollectionConfig {\n if (!options || !options.userCollection) {\n throw new Error(\n 'createApiKeysCollection: `userCollection` is required (slug of the user collection that owns API keys).',\n )\n }\n if (!Array.isArray(options.availableCollections)) {\n throw new Error(\n 'createApiKeysCollection: `availableCollections` is required (slugs of collections that scope overrides may target).',\n )\n }\n if (!Array.isArray(options.availableTools)) {\n throw new Error(\n 'createApiKeysCollection: `availableTools` is required (names of registered MCP tools).',\n )\n }\n\n const slug = options.slug ?? API_KEYS_DEFAULT_SLUG\n const toolOptions = options.availableTools.map((t) => ({ label: t, value: t }))\n const availableGlobals = Array.isArray(options.availableGlobals)\n ? options.availableGlobals\n : []\n\n const presetField: Field = {\n name: 'preset',\n type: 'select',\n required: true,\n defaultValue: 'custom',\n options: PRESET_OPTIONS as unknown as { label: string; value: string }[],\n admin: {\n description:\n 'Role preset. \"Custom\" unlocks the per-collection matrix and the tool overrides below. ' +\n 'Switching away from Custom CLEARS every override on save (collectionScopes, globalScopes, ' +\n 'toolAllow, toolDeny); switching back to Custom starts from a fresh deny-all baseline, so ' +\n 'reconfigure the matrices and tool lists before saving.',\n },\n }\n\n // Stored shape: Array<{ slug: string; actions: ('read'|'create'|'update'|'delete')[] }>\n // The default Payload UI for an `array` would force users to add rows\n // one at a time; the custom matrix component renders all available\n // collections at once with a checkbox grid (rows × actions).\n //\n // `availableCollections` is forwarded via `clientProps` — Payload v3's\n // sanctioned escape hatch for serializable static data that the client\n // component needs at render time.\n const collectionScopesField: Field = {\n name: 'collectionScopes',\n type: 'json',\n admin: {\n condition: isCustomPreset,\n components: {\n Field: {\n path: 'payload-mcp-toolkit/client',\n exportName: 'CollectionScopesMatrix',\n clientProps: {\n availableCollections: options.availableCollections,\n },\n },\n },\n },\n }\n\n // Mirrors `collectionScopes` exactly — one additive JSONB column with a\n // default of `'[]'`, default-rendered by `GlobalScopesMatrix`. Hidden\n // under non-custom presets. Stored shape:\n // Array<{ slug: string; actions: ('read'|'update')[] }>\n // No `availableGlobals.length > 0` gate: `ScopesTable` renders its own\n // empty-state message when zero items are passed, so the field surfaces\n // under Custom regardless of host config, matching the collection variant.\n const globalScopesField: Field = {\n name: 'globalScopes',\n type: 'json',\n admin: {\n condition: isCustomPreset,\n components: {\n Field: {\n path: 'payload-mcp-toolkit/client',\n exportName: 'GlobalScopesMatrix',\n clientProps: {\n availableGlobals,\n },\n },\n },\n },\n }\n\n const toolsCollapsible: Field = {\n type: 'collapsible',\n label: 'Tool overrides',\n admin: {\n condition: isCustomPreset,\n description:\n 'Per-tool whitelist / blacklist. Layered on top of preset and collection scopes.',\n initCollapsed: true,\n },\n fields: [\n {\n name: 'toolAllow',\n type: 'select',\n hasMany: true,\n options: toolOptions,\n admin: {\n description:\n 'If set, only these tools are callable with this key. Leave empty to allow any tool ' +\n 'the collection or global scopes permit. Under the Custom preset, an empty list is ' +\n 'treated as deny-all ONLY when no collection or global scopes are set (the fresh- ' +\n 'Custom-key sentinel); when collection or global scopes are populated, an empty list ' +\n 'collapses to \"no tool restriction\" so the resource scopes alone determine what is ' +\n 'callable — to deny every tool while keeping resource scopes, enumerate them in ' +\n 'toolDeny instead. Preset-mode keys created via the REST API with an empty list are ' +\n 'coerced to \"no restriction\".',\n },\n },\n {\n name: 'toolDeny',\n type: 'select',\n hasMany: true,\n options: toolOptions,\n admin: {\n description: 'These tools are blocked regardless of any other scope.',\n },\n },\n ],\n }\n\n return {\n slug,\n admin: {\n group: 'MCP',\n useAsTitle: 'name',\n description:\n 'API keys for MCP clients. Scopes control which collections and tools each key can access.',\n defaultColumns: ['name', 'user', 'keyPrefix', 'preset', 'lastUsedAt', 'expiresAt', 'revokedAt'],\n },\n auth: {\n disableLocalStrategy: true,\n useAPIKey: true,\n },\n // Every MCP request authenticates by looking up this column. Payload's\n // `useAPIKey` adds `apiKeyIndex` but does not index it, so each call cost a\n // sequential scan over the whole key table — and the table only grows.\n // Declared here rather than added by hand in a host migration, so Payload's\n // schema builder knows about it and never offers to drop it.\n //\n // Not unique: rows exist with a null `apiKeyIndex` (a key row saved before\n // `enableAPIKey` is ticked), and a unique index would let only one of them\n // exist at a time.\n indexes: [{ fields: ['apiKeyIndex'] }],\n hooks: {\n beforeValidate: [\n (({ data, originalDoc }) => {\n // The override fields (collectionScopes, globalScopes, toolAllow,\n // toolDeny) are conditionally rendered only under the Custom preset\n // (`condition: isCustomPreset`). Under any other preset they are\n // hidden in the admin UI, which means two things:\n //\n // 1. The admin form omits hidden fields from its payload on save,\n // so `data` only carries the visible fields — we can't\n // \"collapse the empty array we see in `data`\" because we never\n // see it at all. The stale value lives on `originalDoc`.\n // 2. A Custom→Admin switch silently keeps the prior\n // `toolAllow:[...]` / `collectionScopes:[...]`, and\n // `composeScopes` then emits a scope gate that rejects calls\n // the user clearly intended to allow.\n //\n // Fix: when the preset is non-Custom, explicitly write `null` into\n // `data` for every override axis (regardless of what `data` carries\n // or what originalDoc holds). Payload persists nulls, so the stale\n // values are erased on every save. The Custom-preset branch below\n // keeps the explicit-empty-means-deny semantic intact.\n if (!data) return data\n const d = data as Record<string, unknown>\n const orig = (originalDoc ?? {}) as Record<string, unknown>\n const preset = d.preset ?? orig.preset\n\n // `readField` falls through to originalDoc when `data` omits the\n // key entirely (admin form skipping hidden fields), but honours\n // an explicit null/empty in `data` over originalDoc.\n const readField = (key: string): unknown =>\n key in d ? d[key] : orig[key]\n const isNonEmptyArray = (v: unknown): boolean =>\n Array.isArray(v) && v.length > 0\n // Null, undefined, or `[]` — the three shapes that mean \"the user\n // expressed no tool restriction\". Anything else (a bare string, a\n // number) is malformed and must reach Payload's validator rather than\n // being silently read as \"no restriction\".\n const isUnset = (v: unknown): boolean =>\n v === null || v === undefined || (Array.isArray(v) && v.length === 0)\n\n const OVERRIDE_AXES = [\n 'collectionScopes',\n 'globalScopes',\n 'toolAllow',\n 'toolDeny',\n ] as const\n\n if (preset !== 'custom') {\n for (const axis of OVERRIDE_AXES) d[axis] = null\n return data\n }\n\n // Custom preset: the Tools collapsible is labelled as an *override*\n // layered on top of collection / global scopes, and its description\n // says \"Leave empty to allow any tool the collection scopes permit.\"\n // Payload's hasMany-select default of `[]` would otherwise turn the\n // Tools section into a mandatory whitelist — a user who configures\n // collection scopes and never opens the collapsible would silently\n // store `toolAllow:[]`, which `composeScopes` honours as deny-all on\n // the tools axis and rejects every call.\n //\n // Resolve the mismatch by coercing a non-populated `toolAllow` to\n // null whenever the key carries any concrete resource scope\n // (collection or global entries). \"Non-populated\" covers both `[]`\n // (what the admin form sends) and a missing key (what a key created\n // through the Local API sends) — Payload reads an unset hasMany\n // select back as `[]` either way, so without this a scripted key\n // with collection scopes and no tool list would deny every tool.\n // The fresh-Custom-key sentinel in `composeScopes` still covers the\n // \"no scopes at all\" case (everything null → deny-all), so users who\n // genuinely want deny-all do not regress.\n const hasResourceScope =\n isNonEmptyArray(readField('collectionScopes')) ||\n isNonEmptyArray(readField('globalScopes'))\n if (hasResourceScope && isUnset(readField('toolAllow'))) {\n d.toolAllow = null\n }\n return data\n }) as CollectionBeforeValidateHook,\n ],\n },\n labels: {\n plural: 'API Keys',\n singular: 'API Key',\n },\n fields: [\n // Main column.\n {\n name: 'name',\n type: 'text',\n required: true,\n admin: { description: 'Human label for this key (e.g. \"Editorial team — Claude Desktop\").' },\n },\n {\n name: 'description',\n type: 'textarea',\n admin: { description: 'Optional notes about the purpose of this key.' },\n },\n presetField,\n collectionScopesField,\n globalScopesField,\n toolsCollapsible,\n\n // Sidebar — identity + lifecycle.\n {\n name: 'user',\n type: 'relationship',\n relationTo: options.userCollection,\n required: true,\n admin: {\n position: 'sidebar',\n description:\n 'The user this key authenticates as. Tool calls use this user for access checks on target collections.',\n },\n },\n {\n name: 'keyPrefix',\n type: 'text',\n index: true,\n admin: {\n position: 'sidebar',\n readOnly: true,\n description:\n 'First 8 characters of the API key — used in audit logs to identify the key without exposing the full secret.',\n },\n hooks: {\n beforeChange: [\n ({ data, originalDoc, value }) => {\n if (typeof value === 'string' && value.length > 0) return value\n const incomingKey = (data as { apiKey?: unknown } | undefined)?.apiKey\n if (typeof incomingKey === 'string' && incomingKey.length >= 8) {\n return incomingKey.slice(0, 8)\n }\n const existing = (originalDoc as { keyPrefix?: unknown } | undefined)?.keyPrefix\n return typeof existing === 'string' ? existing : undefined\n },\n ],\n },\n },\n {\n name: 'expiresAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n description: 'Optional expiry. Requests authenticated with an expired key are rejected.',\n },\n },\n {\n name: 'revokedAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n description: 'Set to revoke a key. Revoked keys are rejected at auth time.',\n },\n },\n {\n name: 'lastUsedAt',\n type: 'date',\n admin: {\n position: 'sidebar',\n readOnly: true,\n description:\n 'Updated on each successful authentication. Fire-and-forget; not on the request hot path.',\n },\n },\n ],\n }\n}\n"],"names":["API_KEYS_DEFAULT_SLUG","PRESET_OPTIONS","label","value","isCustomPreset","data","preset","createApiKeysCollection","options","userCollection","Error","Array","isArray","availableCollections","availableTools","slug","toolOptions","map","t","availableGlobals","presetField","name","type","required","defaultValue","admin","description","collectionScopesField","condition","components","Field","path","exportName","clientProps","globalScopesField","toolsCollapsible","initCollapsed","fields","hasMany","group","useAsTitle","defaultColumns","auth","disableLocalStrategy","useAPIKey","indexes","hooks","beforeValidate","originalDoc","d","orig","readField","key","isNonEmptyArray","v","length","isUnset","undefined","OVERRIDE_AXES","axis","hasResourceScope","toolAllow","labels","plural","singular","relationTo","position","index","readOnly","beforeChange","incomingKey","apiKey","slice","existing","keyPrefix"],"mappings":"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"}
|