@abinnovision/payloadcms-mcpx 1.0.1-beta.2 → 1.1.0-beta.5
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 +30 -4
- package/dist/api-keys/capability-matrix.d.mts +32 -0
- package/dist/api-keys/capability-matrix.mjs +128 -0
- package/dist/api-keys/fields.mjs +28 -26
- package/dist/client/capability-matrix.d.mts +26 -0
- package/dist/client/capability-matrix.mjs +349 -0
- package/dist/client/index.d.mts +2 -1
- package/dist/client/index.mjs +2 -1
- package/dist/options.d.mts +2 -0
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -330,6 +330,25 @@ to anyone who may read the key document (own keys only, by default). Each key:
|
|
|
330
330
|
is configured `write: "live"`, and it counts only alongside `write`, since
|
|
331
331
|
publishing is an extension of writing.
|
|
332
332
|
|
|
333
|
+
### The capabilities matrix
|
|
334
|
+
|
|
335
|
+
The checkboxes render as one table per namespace: a row per collection, global
|
|
336
|
+
or tool, a column per operation, and a bulk toggle in each column header that
|
|
337
|
+
grants or clears the whole column at once. Clicking a row's name does the same
|
|
338
|
+
for that entity.
|
|
339
|
+
|
|
340
|
+
A cell the plugin config does not expose shows a dash instead of an empty box,
|
|
341
|
+
so a `write: false` collection reads as a refusal by config rather than a
|
|
342
|
+
capability someone forgot to tick. A column no row exposes is left out
|
|
343
|
+
altogether. Ticking `publish` ticks `write` with it, and clearing `write` clears
|
|
344
|
+
`publish`, because a publish without a write resolves to nothing.
|
|
345
|
+
|
|
346
|
+
Each operation is explained on its column header rather than beside every
|
|
347
|
+
checkbox, so the same sentence is never repeated down the table.
|
|
348
|
+
|
|
349
|
+
The matrix renders an admin component, so it has to be in the import map; see
|
|
350
|
+
[Admin components](#admin-components).
|
|
351
|
+
|
|
333
352
|
Keys authenticate only the MCP endpoint. They are deliberately not a Payload
|
|
334
353
|
auth strategy, so a key can never authenticate the REST or GraphQL API. The
|
|
335
354
|
reverse also holds: an admin session or JWT is ignored by the MCP endpoint.
|
|
@@ -345,15 +364,22 @@ behind a copy button. The tab only exists once the key does, so the create form
|
|
|
345
364
|
stays free of it. Turn it off with `apiKeys.setupGuide: false`, which also drops
|
|
346
365
|
the tabs and restores the flat form.
|
|
347
366
|
|
|
348
|
-
The
|
|
367
|
+
The URL comes from `serverURL` when the config sets one and from the browser's
|
|
368
|
+
origin otherwise.
|
|
369
|
+
|
|
370
|
+
### Admin components
|
|
371
|
+
|
|
372
|
+
The key form renders two components of its own, the capabilities matrix and the
|
|
373
|
+
setup guide, so both have to be in the import map:
|
|
349
374
|
|
|
350
375
|
```bash
|
|
351
376
|
payload generate:importmap
|
|
352
377
|
```
|
|
353
378
|
|
|
354
|
-
Without
|
|
355
|
-
|
|
356
|
-
the
|
|
379
|
+
Without those entries Payload logs a missing-component error and renders nothing
|
|
380
|
+
in their place. For the setup guide that costs a tab; for the matrix it costs
|
|
381
|
+
the whole capabilities editor, leaving no way to grant a key anything from the
|
|
382
|
+
admin. The rest of the plugin is unaffected either way.
|
|
357
383
|
|
|
358
384
|
## Drafts and publishing
|
|
359
385
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import "../options.mjs";
|
|
2
|
+
//#region src/api-keys/capability-matrix.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* One entity's row. The three booleans say what the plugin config exposes, not
|
|
5
|
+
* what the key has been granted: a `false` renders as a dash rather than an
|
|
6
|
+
* empty box, so absence reads as a refusal by config rather than a gap.
|
|
7
|
+
*/
|
|
8
|
+
export interface CapabilityRow {
|
|
9
|
+
fieldName: string;
|
|
10
|
+
/** The slug, which is what MCP clients send and what refusals name. */
|
|
11
|
+
label: string;
|
|
12
|
+
read: boolean;
|
|
13
|
+
write: boolean;
|
|
14
|
+
publish: boolean;
|
|
15
|
+
/** Said only where a row departs from what its column header promises. */
|
|
16
|
+
hint?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface CapabilityTool {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Everything the admin component needs to draw the matrix, derived from the
|
|
24
|
+
* plugin options at config time. Crosses the server/client boundary as a client
|
|
25
|
+
* prop, so every leaf has to stay JSON-serializable.
|
|
26
|
+
*/
|
|
27
|
+
export interface CapabilityMatrix {
|
|
28
|
+
collections: CapabilityRow[];
|
|
29
|
+
globals: CapabilityRow[];
|
|
30
|
+
tools: CapabilityTool[];
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { CAPABILITIES_FIELD, canCreate, canPublish, canWrite } from "../capabilities.mjs";
|
|
2
|
+
//#region src/api-keys/capability-matrix.ts
|
|
3
|
+
/**
|
|
4
|
+
* The group's `admin.description`, which the matrix renders in Payload's own
|
|
5
|
+
* description slot under the heading. It is the one place the two markings in
|
|
6
|
+
* the table are explained, so both belong in it.
|
|
7
|
+
*/ const CAPABILITIES_DESCRIPTION = "What this key may do. An unticked box is a refusal, and a dash means the plugin config does not expose that operation at all.";
|
|
8
|
+
/**
|
|
9
|
+
* The three operations a collection or global can expose, described once. The
|
|
10
|
+
* generated checkboxes and the matrix column headers both read these, so the
|
|
11
|
+
* wording a key's owner sees cannot drift from what the field config carries.
|
|
12
|
+
*/ const CAPABILITY_OPERATIONS = [
|
|
13
|
+
{
|
|
14
|
+
id: "read",
|
|
15
|
+
label: "Read",
|
|
16
|
+
description: "Describe, find and read."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "write",
|
|
20
|
+
label: "Write",
|
|
21
|
+
description: "Create, patch and validate drafts."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "publish",
|
|
25
|
+
label: "Publish",
|
|
26
|
+
description: "Promote the current draft to what the public sees."
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
const UPLOAD_HINT = "Files are uploaded in the admin panel.";
|
|
30
|
+
const toRow = (entity) => ({
|
|
31
|
+
fieldName: entity.fieldName,
|
|
32
|
+
label: entity.slug,
|
|
33
|
+
read: entity.read,
|
|
34
|
+
write: canWrite(entity),
|
|
35
|
+
publish: canPublish(entity),
|
|
36
|
+
...canWrite(entity) && !canCreate(entity) ? { hint: UPLOAD_HINT } : {}
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* The single description of what the config exposes. `createCapabilityFields`
|
|
40
|
+
* generates its checkboxes from this, and the admin component draws from it, so
|
|
41
|
+
* a cell can never appear without a field behind it.
|
|
42
|
+
*/ const createCapabilityMatrix = (options) => ({
|
|
43
|
+
collections: options.collections.map(toRow),
|
|
44
|
+
globals: options.globals.map(toRow),
|
|
45
|
+
tools: options.tools.map((tool) => ({
|
|
46
|
+
name: tool.name,
|
|
47
|
+
description: typeof tool.description === "string" ? tool.description : tool.name
|
|
48
|
+
}))
|
|
49
|
+
});
|
|
50
|
+
const cellPath = (basePath, namespace, fieldName, operation) => `${basePath}.${namespace}.${fieldName}.${operation}`;
|
|
51
|
+
const toolPath = (basePath, name) => `${basePath}.tools.${name}`;
|
|
52
|
+
/** Every path the matrix can address, in a stable order. */ const capabilityPaths = (matrix, basePath = CAPABILITIES_FIELD) => [...["collections", "globals"].flatMap((namespace) => matrix[namespace].flatMap((row) => CAPABILITY_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => cellPath(basePath, namespace, row.fieldName, operation.id)))), ...matrix.tools.map((tool) => toolPath(basePath, tool.name))];
|
|
53
|
+
const stateOf = (paths, values) => {
|
|
54
|
+
if (paths.length === 0) return "off";
|
|
55
|
+
const granted = paths.filter((path) => values[path] === true).length;
|
|
56
|
+
if (granted === 0) return "off";
|
|
57
|
+
return granted === paths.length ? "on" : "mixed";
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Whether a column's bulk toggle reads as on, off or indeterminate. Non-exposed
|
|
61
|
+
* cells are ignored: a column of two granted cells and one dash is fully on.
|
|
62
|
+
*/ const columnState = (matrix, basePath, namespace, operation, values) => stateOf(matrix[namespace].filter((row) => row[operation]).map((row) => cellPath(basePath, namespace, row.fieldName, operation)), values);
|
|
63
|
+
/**
|
|
64
|
+
* Whether an entity's row toggle reads as on, off or indeterminate, over the
|
|
65
|
+
* operations the config exposes for that row.
|
|
66
|
+
*/ const rowState = (basePath, namespace, row, values) => stateOf(CAPABILITY_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => cellPath(basePath, namespace, row.fieldName, operation.id)), values);
|
|
67
|
+
const toolsState = (matrix, basePath, values) => stateOf(matrix.tools.map((tool) => toolPath(basePath, tool.name)), values);
|
|
68
|
+
/**
|
|
69
|
+
* Publishing is an extension of writing, and `publishFlag` in `capabilities.ts`
|
|
70
|
+
* discards a publish without a write. Ticking publish therefore ticks write,
|
|
71
|
+
* and clearing write clears publish, so the form cannot save a combination the
|
|
72
|
+
* server ignores.
|
|
73
|
+
*/ const reconcile = (next, touched) => {
|
|
74
|
+
if (touched.has("publish") && next.publish) return {
|
|
75
|
+
...next,
|
|
76
|
+
write: true
|
|
77
|
+
};
|
|
78
|
+
if (touched.has("write") && !next.write) return {
|
|
79
|
+
...next,
|
|
80
|
+
publish: false
|
|
81
|
+
};
|
|
82
|
+
return next;
|
|
83
|
+
};
|
|
84
|
+
const rowActions = (basePath, namespace, row, values, changes) => {
|
|
85
|
+
const current = {
|
|
86
|
+
read: values[cellPath(basePath, namespace, row.fieldName, "read")] === true,
|
|
87
|
+
write: values[cellPath(basePath, namespace, row.fieldName, "write")] === true,
|
|
88
|
+
publish: values[cellPath(basePath, namespace, row.fieldName, "publish")] === true
|
|
89
|
+
};
|
|
90
|
+
const touched = /* @__PURE__ */ new Set();
|
|
91
|
+
const next = { ...current };
|
|
92
|
+
for (const operation of CAPABILITY_OPERATIONS) {
|
|
93
|
+
const change = changes[operation.id];
|
|
94
|
+
if (change !== void 0 && row[operation.id]) {
|
|
95
|
+
next[operation.id] = change;
|
|
96
|
+
touched.add(operation.id);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const reconciled = reconcile(next, touched);
|
|
100
|
+
return CAPABILITY_OPERATIONS.filter((operation) => row[operation.id] && reconciled[operation.id] !== current[operation.id]).map((operation) => ({
|
|
101
|
+
type: "UPDATE",
|
|
102
|
+
path: cellPath(basePath, namespace, row.fieldName, operation.id),
|
|
103
|
+
value: reconciled[operation.id]
|
|
104
|
+
}));
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* The form-state updates one click implies, as literal actions. Every toggle in
|
|
108
|
+
* the matrix — cell, row, column or tool — comes through here, so the publish
|
|
109
|
+
* rule holds for bulk grants as much as for a single box.
|
|
110
|
+
*
|
|
111
|
+
* Only cells whose value actually changes get an action, which keeps a bulk
|
|
112
|
+
* toggle from marking the form modified when it changes nothing.
|
|
113
|
+
*/ const buildToggleActions = (matrix, basePath, values, intent) => {
|
|
114
|
+
if (intent.kind === "tool" || intent.kind === "tools") return matrix.tools.filter((tool) => intent.kind === "tools" || tool.name === intent.name).filter((tool) => values[toolPath(basePath, tool.name)] === true !== intent.value).map((tool) => ({
|
|
115
|
+
type: "UPDATE",
|
|
116
|
+
path: toolPath(basePath, tool.name),
|
|
117
|
+
value: intent.value
|
|
118
|
+
}));
|
|
119
|
+
const rows = matrix[intent.namespace].filter((row) => intent.kind === "column" || row.fieldName === intent.fieldName);
|
|
120
|
+
const changes = intent.kind === "row" ? {
|
|
121
|
+
read: intent.value,
|
|
122
|
+
write: intent.value,
|
|
123
|
+
publish: intent.value
|
|
124
|
+
} : { [intent.operation]: intent.value };
|
|
125
|
+
return rows.flatMap((row) => rowActions(basePath, intent.namespace, row, values, changes));
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
128
|
+
export { CAPABILITIES_DESCRIPTION, CAPABILITY_OPERATIONS, buildToggleActions, capabilityPaths, cellPath, columnState, createCapabilityMatrix, rowState, toolPath, toolsState };
|
package/dist/api-keys/fields.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CAPABILITIES_FIELD
|
|
1
|
+
import { CAPABILITIES_FIELD } from "../capabilities.mjs";
|
|
2
|
+
import { CAPABILITIES_DESCRIPTION, CAPABILITY_OPERATIONS, createCapabilityMatrix } from "./capability-matrix.mjs";
|
|
2
3
|
//#region src/api-keys/fields.ts
|
|
3
4
|
const encryptKey = ({ req, value }) => typeof value === "string" ? req.payload.encrypt(value) : value;
|
|
4
5
|
const decryptKey = ({ req, value }) => {
|
|
@@ -89,10 +90,11 @@ const checkbox = (name, description) => ({
|
|
|
89
90
|
}]
|
|
90
91
|
}];
|
|
91
92
|
};
|
|
92
|
-
const PUBLISH_DESCRIPTION = "Publish the current draft. Changes what the public sees.";
|
|
93
93
|
/**
|
|
94
94
|
* One checkbox per exposed operation, grouped per collection, per global and
|
|
95
|
-
* per custom tool
|
|
95
|
+
* per custom tool, generated from the same {@link createCapabilityMatrix}
|
|
96
|
+
* descriptor the admin component draws, so a cell can never appear without a
|
|
97
|
+
* field behind it. Only operations the plugin config exposes get a checkbox, so
|
|
96
98
|
* a key can never enable more than the config allows. Everything defaults to
|
|
97
99
|
* off, which is why a key issued before a capability existed stays closed to it.
|
|
98
100
|
*
|
|
@@ -100,31 +102,21 @@ const PUBLISH_DESCRIPTION = "Publish the current draft. Changes what the public
|
|
|
100
102
|
* `write: "live"`: there is no draft to promote there, the write itself is the
|
|
101
103
|
* live change, and a second checkbox would only make `write` a dead setting.
|
|
102
104
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
105
|
+
* The group renders as a matrix rather than as nested boxes; see
|
|
106
|
+
* `src/client/capability-matrix.tsx`. The descriptions below therefore reach
|
|
107
|
+
* the screen on the matrix column headers, and survive here for a consumer who
|
|
108
|
+
* replaces the component through `apiKeys.overrideCollection`.
|
|
106
109
|
*/ const createCapabilityFields = (options) => {
|
|
107
|
-
const
|
|
108
|
-
|
|
110
|
+
const matrix = createCapabilityMatrix(options);
|
|
111
|
+
const entityGroups = (rows) => rows.map((row) => ({
|
|
112
|
+
name: row.fieldName,
|
|
109
113
|
type: "group",
|
|
110
|
-
label:
|
|
111
|
-
fields: [
|
|
112
|
-
...collection.read ? [checkbox("read", "Describe, find and read documents.")] : [],
|
|
113
|
-
...canWrite(collection) ? [checkbox("write", canCreate(collection) ? "Create, patch and validate drafts." : "Patch and validate drafts. The file itself is uploaded in the admin panel.")] : [],
|
|
114
|
-
...canPublish(collection) ? [checkbox("publish", PUBLISH_DESCRIPTION)] : []
|
|
115
|
-
]
|
|
114
|
+
label: row.label,
|
|
115
|
+
fields: CAPABILITY_OPERATIONS.filter((operation) => row[operation.id]).map((operation) => checkbox(operation.id, operation.description))
|
|
116
116
|
}));
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
label: global.slug,
|
|
121
|
-
fields: [
|
|
122
|
-
...global.read ? [checkbox("read", "Describe and read this global.")] : [],
|
|
123
|
-
...canWrite(global) ? [checkbox("write", "Patch and validate this global's draft.")] : [],
|
|
124
|
-
...canPublish(global) ? [checkbox("publish", PUBLISH_DESCRIPTION)] : []
|
|
125
|
-
]
|
|
126
|
-
}));
|
|
127
|
-
const toolCheckboxes = options.tools.map((tool) => checkbox(tool.name, typeof tool.description === "string" ? tool.description : tool.name));
|
|
117
|
+
const collectionGroups = entityGroups(matrix.collections);
|
|
118
|
+
const globalGroups = entityGroups(matrix.globals);
|
|
119
|
+
const toolCheckboxes = matrix.tools.map((tool) => checkbox(tool.name, tool.description));
|
|
128
120
|
const groups = [
|
|
129
121
|
...collectionGroups.length > 0 ? [{
|
|
130
122
|
name: "collections",
|
|
@@ -146,7 +138,17 @@ const PUBLISH_DESCRIPTION = "Publish the current draft. Changes what the public
|
|
|
146
138
|
return [{
|
|
147
139
|
name: CAPABILITIES_FIELD,
|
|
148
140
|
type: "group",
|
|
149
|
-
admin: {
|
|
141
|
+
admin: {
|
|
142
|
+
description: CAPABILITIES_DESCRIPTION,
|
|
143
|
+
components: { Field: {
|
|
144
|
+
path: "@abinnovision/payloadcms-mcpx/client",
|
|
145
|
+
exportName: "McpxCapabilityMatrix",
|
|
146
|
+
clientProps: {
|
|
147
|
+
matrix,
|
|
148
|
+
withinTab: options.setupGuide
|
|
149
|
+
}
|
|
150
|
+
} }
|
|
151
|
+
},
|
|
150
152
|
fields: groups
|
|
151
153
|
}];
|
|
152
154
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CapabilityMatrix } from "../api-keys/capability-matrix.mjs";
|
|
2
|
+
import React from "react";
|
|
3
|
+
//#region src/client/capability-matrix.d.ts
|
|
4
|
+
interface McpxCapabilityMatrixProps {
|
|
5
|
+
/** Built from the plugin options at config time. */
|
|
6
|
+
matrix: CapabilityMatrix;
|
|
7
|
+
/** The group's own config, supplied by Payload. Carries the description. */
|
|
8
|
+
field?: {
|
|
9
|
+
admin?: {
|
|
10
|
+
description?: unknown;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/** The `capabilities` group's own path, supplied by Payload. */
|
|
14
|
+
path?: string;
|
|
15
|
+
readOnly?: boolean;
|
|
16
|
+
/** Whether the key form is split into tabs, from the plugin options. */
|
|
17
|
+
withinTab?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* What an API key may do, as one table per namespace: a row per entity, a
|
|
21
|
+
* column per operation, and a bulk toggle in each column header. The nested
|
|
22
|
+
* checkbox fields this replaces still back every cell, so the stored document
|
|
23
|
+
* is unchanged; only the rendering is.
|
|
24
|
+
*/
|
|
25
|
+
export declare const McpxCapabilityMatrix: React.FC<McpxCapabilityMatrixProps>;
|
|
26
|
+
//#endregion
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { CAPABILITIES_FIELD } from "../capabilities.mjs";
|
|
3
|
+
import { CAPABILITY_OPERATIONS, buildToggleActions, capabilityPaths, cellPath, columnState, rowState, toolPath, toolsState } from "../api-keys/capability-matrix.mjs";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { CheckboxInput, FieldDescription, FieldLabel, useForm, useFormFields } from "@payloadcms/ui";
|
|
6
|
+
import { useCallback, useMemo } from "react";
|
|
7
|
+
//#region src/client/capability-matrix.tsx
|
|
8
|
+
const NOT_EXPOSED = "The plugin config does not expose this.";
|
|
9
|
+
const BASE_CLASS = "mcpx-capabilities";
|
|
10
|
+
const SHEET = `
|
|
11
|
+
.${BASE_CLASS} tbody tr:hover { background: var(--theme-elevation-50); }
|
|
12
|
+
.${BASE_CLASS}__toggle:hover { text-decoration: underline; }
|
|
13
|
+
.${BASE_CLASS} section:last-of-type { margin-bottom: 0; }
|
|
14
|
+
`;
|
|
15
|
+
const CELL_PADDING = "calc(var(--base) * 0.6)";
|
|
16
|
+
const EDGE_PADDING = "calc(var(--base) * 0.8)";
|
|
17
|
+
/**
|
|
18
|
+
* Payload's own theme variables, so the matrix follows the admin's light and
|
|
19
|
+
* dark themes without shipping a stylesheet consumers would have to transpile.
|
|
20
|
+
* The header block borrows the admin table's treatment: muted, raised a step
|
|
21
|
+
* off the page and closed with a rule.
|
|
22
|
+
*
|
|
23
|
+
* The only literal left is the small-caps section heading, which has no
|
|
24
|
+
* counterpart in the admin to borrow from.
|
|
25
|
+
*/ const styles = {
|
|
26
|
+
section: { marginBottom: "var(--base)" },
|
|
27
|
+
table: {
|
|
28
|
+
tableLayout: "fixed",
|
|
29
|
+
width: "auto",
|
|
30
|
+
borderCollapse: "separate",
|
|
31
|
+
borderSpacing: 0,
|
|
32
|
+
textAlign: "left"
|
|
33
|
+
},
|
|
34
|
+
head: { background: "var(--theme-elevation-50)" },
|
|
35
|
+
/** The section name, which heads the label column instead of a stray title. */ headTitle: {
|
|
36
|
+
padding: `${CELL_PADDING} ${CELL_PADDING} ${CELL_PADDING} ${EDGE_PADDING}`,
|
|
37
|
+
verticalAlign: "middle",
|
|
38
|
+
fontSize: "0.8em",
|
|
39
|
+
letterSpacing: "0.08em",
|
|
40
|
+
textTransform: "uppercase",
|
|
41
|
+
color: "var(--theme-elevation-400)",
|
|
42
|
+
fontWeight: "normal"
|
|
43
|
+
},
|
|
44
|
+
headOperation: {
|
|
45
|
+
padding: CELL_PADDING,
|
|
46
|
+
verticalAlign: "middle",
|
|
47
|
+
color: "var(--theme-elevation-400)",
|
|
48
|
+
fontWeight: "normal",
|
|
49
|
+
textAlign: "center",
|
|
50
|
+
whiteSpace: "nowrap"
|
|
51
|
+
},
|
|
52
|
+
headLabel: {
|
|
53
|
+
padding: `${CELL_PADDING} ${CELL_PADDING} ${CELL_PADDING} ${EDGE_PADDING}`,
|
|
54
|
+
verticalAlign: "middle",
|
|
55
|
+
color: "var(--theme-elevation-500)",
|
|
56
|
+
fontWeight: "normal"
|
|
57
|
+
},
|
|
58
|
+
/** Closes the header block, across every column rather than the first. */ rule: { borderBottom: "1px solid var(--theme-border-color)" },
|
|
59
|
+
topStart: { borderStartStartRadius: "var(--style-radius-s)" },
|
|
60
|
+
topEnd: { borderStartEndRadius: "var(--style-radius-s)" },
|
|
61
|
+
rowHeader: {
|
|
62
|
+
padding: `${CELL_PADDING} ${CELL_PADDING} ${CELL_PADDING} ${EDGE_PADDING}`,
|
|
63
|
+
verticalAlign: "middle",
|
|
64
|
+
fontWeight: "normal",
|
|
65
|
+
wordBreak: "break-word"
|
|
66
|
+
},
|
|
67
|
+
rowButton: {
|
|
68
|
+
padding: 0,
|
|
69
|
+
border: 0,
|
|
70
|
+
background: "none",
|
|
71
|
+
color: "inherit",
|
|
72
|
+
font: "inherit",
|
|
73
|
+
cursor: "pointer",
|
|
74
|
+
textAlign: "start",
|
|
75
|
+
textDecoration: "none"
|
|
76
|
+
},
|
|
77
|
+
hint: {
|
|
78
|
+
display: "block",
|
|
79
|
+
color: "var(--theme-elevation-400)"
|
|
80
|
+
},
|
|
81
|
+
cell: {
|
|
82
|
+
padding: CELL_PADDING,
|
|
83
|
+
verticalAlign: "middle",
|
|
84
|
+
textAlign: "center"
|
|
85
|
+
},
|
|
86
|
+
lastCell: { paddingInlineEnd: EDGE_PADDING },
|
|
87
|
+
dash: {
|
|
88
|
+
color: "var(--theme-elevation-400)",
|
|
89
|
+
cursor: "help"
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Payload's own checkbox, which carries the admin's styling and the partial
|
|
94
|
+
* state the column and row toggles need. `name` is what it renders as the
|
|
95
|
+
* input's `title`, so it doubles as the control's accessible name.
|
|
96
|
+
*/ const Box = ({ label, onChange, readOnly, state }) => /*#__PURE__*/ jsx(CheckboxInput, {
|
|
97
|
+
checked: state === "on",
|
|
98
|
+
name: label,
|
|
99
|
+
onToggle: onChange,
|
|
100
|
+
partialChecked: state === "mixed",
|
|
101
|
+
readOnly
|
|
102
|
+
});
|
|
103
|
+
/** Both in `--base` multiples, so the columns keep Payload's rhythm. */ const LABEL_WIDTH = "calc(var(--base) * 9)";
|
|
104
|
+
const OPERATION_WIDTH = "calc(var(--base) * 3.5)";
|
|
105
|
+
/** Fixed widths, so every table lines its columns up with the ones above it. */ const Columns = ({ count }) => /*#__PURE__*/ jsxs("colgroup", { children: [/*#__PURE__*/ jsx("col", { style: { width: LABEL_WIDTH } }), Array.from({ length: count }, (_, index) => /*#__PURE__*/ jsx("col", { style: { width: OPERATION_WIDTH } }, index))] });
|
|
106
|
+
/** The rightmost cell carries the table's outer padding, as Payload's does. */ const cellStyle = (isLast) => isLast ? {
|
|
107
|
+
...styles.cell,
|
|
108
|
+
...styles.lastCell
|
|
109
|
+
} : styles.cell;
|
|
110
|
+
const Dash = () => /*#__PURE__*/ jsx("span", {
|
|
111
|
+
"aria-label": NOT_EXPOSED,
|
|
112
|
+
style: styles.dash,
|
|
113
|
+
title: NOT_EXPOSED,
|
|
114
|
+
children: "—"
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* What an API key may do, as one table per namespace: a row per entity, a
|
|
118
|
+
* column per operation, and a bulk toggle in each column header. The nested
|
|
119
|
+
* checkbox fields this replaces still back every cell, so the stored document
|
|
120
|
+
* is unchanged; only the rendering is.
|
|
121
|
+
*/ const McpxCapabilityMatrix = ({ field, matrix, path = CAPABILITIES_FIELD, readOnly = false, withinTab = false }) => {
|
|
122
|
+
const { dispatchFields, setModified } = useForm();
|
|
123
|
+
const paths = useMemo(() => capabilityPaths(matrix, path), [matrix, path]);
|
|
124
|
+
const encoded = useFormFields(([fields]) => paths.map((cell) => fields[cell]?.value === true ? "1" : "0").join(""));
|
|
125
|
+
const values = useMemo(() => Object.fromEntries(paths.map((cell, index) => [cell, encoded[index] === "1"])), [paths, encoded]);
|
|
126
|
+
const toggle = useCallback((intent) => {
|
|
127
|
+
const actions = buildToggleActions(matrix, path, values, intent);
|
|
128
|
+
if (actions.length === 0) return;
|
|
129
|
+
for (const action of actions) dispatchFields(action);
|
|
130
|
+
setModified(true);
|
|
131
|
+
}, [
|
|
132
|
+
matrix,
|
|
133
|
+
path,
|
|
134
|
+
values,
|
|
135
|
+
dispatchFields,
|
|
136
|
+
setModified
|
|
137
|
+
]);
|
|
138
|
+
const namespaces = [{
|
|
139
|
+
id: "collections",
|
|
140
|
+
title: "Collections"
|
|
141
|
+
}, {
|
|
142
|
+
id: "globals",
|
|
143
|
+
title: "Globals"
|
|
144
|
+
}].filter((namespace) => matrix[namespace.id].length > 0);
|
|
145
|
+
const toolState = toolsState(matrix, path, values);
|
|
146
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
147
|
+
className: [
|
|
148
|
+
"field-type",
|
|
149
|
+
"group-field",
|
|
150
|
+
"group-field--top-level",
|
|
151
|
+
withinTab && "group-field--within-tab",
|
|
152
|
+
BASE_CLASS
|
|
153
|
+
].filter(Boolean).join(" "),
|
|
154
|
+
children: [/*#__PURE__*/ jsx("style", { children: SHEET }), /*#__PURE__*/ jsxs("div", {
|
|
155
|
+
className: "group-field__wrap",
|
|
156
|
+
children: [
|
|
157
|
+
/*#__PURE__*/ jsx("div", {
|
|
158
|
+
className: "group-field__header",
|
|
159
|
+
children: /*#__PURE__*/ jsxs("header", { children: [/*#__PURE__*/ jsx("h3", {
|
|
160
|
+
className: "group-field__title",
|
|
161
|
+
children: /*#__PURE__*/ jsx(FieldLabel, {
|
|
162
|
+
as: "span",
|
|
163
|
+
label: "Capabilities"
|
|
164
|
+
})
|
|
165
|
+
}), typeof field?.admin?.description === "string" ? /*#__PURE__*/ jsx(FieldDescription, {
|
|
166
|
+
description: field.admin.description,
|
|
167
|
+
path
|
|
168
|
+
}) : null] })
|
|
169
|
+
}),
|
|
170
|
+
namespaces.map((namespace) => {
|
|
171
|
+
const rows = matrix[namespace.id];
|
|
172
|
+
const columns = CAPABILITY_OPERATIONS.filter((operation) => rows.some((row) => row[operation.id]));
|
|
173
|
+
return /*#__PURE__*/ jsx("section", {
|
|
174
|
+
style: styles.section,
|
|
175
|
+
children: /*#__PURE__*/ jsxs("table", {
|
|
176
|
+
style: styles.table,
|
|
177
|
+
children: [
|
|
178
|
+
/*#__PURE__*/ jsx(Columns, { count: columns.length }),
|
|
179
|
+
/*#__PURE__*/ jsxs("thead", {
|
|
180
|
+
style: styles.head,
|
|
181
|
+
children: [/*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
|
|
182
|
+
scope: "col",
|
|
183
|
+
style: {
|
|
184
|
+
...styles.headTitle,
|
|
185
|
+
...styles.topStart
|
|
186
|
+
},
|
|
187
|
+
children: namespace.title
|
|
188
|
+
}), columns.map((operation, column) => /*#__PURE__*/ jsx("th", {
|
|
189
|
+
scope: "col",
|
|
190
|
+
style: column === columns.length - 1 ? {
|
|
191
|
+
...styles.headOperation,
|
|
192
|
+
...styles.topEnd
|
|
193
|
+
} : styles.headOperation,
|
|
194
|
+
title: operation.description,
|
|
195
|
+
children: operation.label
|
|
196
|
+
}, operation.id))] }), /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
|
|
197
|
+
scope: "row",
|
|
198
|
+
style: {
|
|
199
|
+
...styles.headLabel,
|
|
200
|
+
...styles.rule
|
|
201
|
+
},
|
|
202
|
+
children: "All"
|
|
203
|
+
}), columns.map((operation, column) => {
|
|
204
|
+
const state = columnState(matrix, path, namespace.id, operation.id, values);
|
|
205
|
+
return /*#__PURE__*/ jsx("td", {
|
|
206
|
+
style: {
|
|
207
|
+
...cellStyle(column === columns.length - 1),
|
|
208
|
+
...styles.rule
|
|
209
|
+
},
|
|
210
|
+
children: /*#__PURE__*/ jsx(Box, {
|
|
211
|
+
label: `${operation.label} every ${namespace.title.toLowerCase()} entry`,
|
|
212
|
+
onChange: () => {
|
|
213
|
+
toggle({
|
|
214
|
+
kind: "column",
|
|
215
|
+
namespace: namespace.id,
|
|
216
|
+
operation: operation.id,
|
|
217
|
+
value: state !== "on"
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
readOnly,
|
|
221
|
+
state
|
|
222
|
+
})
|
|
223
|
+
}, operation.id);
|
|
224
|
+
})] })]
|
|
225
|
+
}),
|
|
226
|
+
/*#__PURE__*/ jsx("tbody", { children: rows.map((row) => {
|
|
227
|
+
const state = rowState(path, namespace.id, row, values);
|
|
228
|
+
return /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsxs("th", {
|
|
229
|
+
scope: "row",
|
|
230
|
+
style: styles.rowHeader,
|
|
231
|
+
children: [/*#__PURE__*/ jsx("button", {
|
|
232
|
+
className: `${BASE_CLASS}__toggle`,
|
|
233
|
+
disabled: readOnly,
|
|
234
|
+
onClick: () => {
|
|
235
|
+
toggle({
|
|
236
|
+
kind: "row",
|
|
237
|
+
namespace: namespace.id,
|
|
238
|
+
fieldName: row.fieldName,
|
|
239
|
+
value: state !== "on"
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
style: styles.rowButton,
|
|
243
|
+
title: `Toggle every capability for ${row.label}`,
|
|
244
|
+
type: "button",
|
|
245
|
+
children: row.label
|
|
246
|
+
}), row.hint ? /*#__PURE__*/ jsx("span", {
|
|
247
|
+
style: styles.hint,
|
|
248
|
+
children: row.hint
|
|
249
|
+
}) : null]
|
|
250
|
+
}), columns.map((operation, column) => /*#__PURE__*/ jsx("td", {
|
|
251
|
+
style: cellStyle(column === columns.length - 1),
|
|
252
|
+
children: row[operation.id] ? /*#__PURE__*/ jsx(Box, {
|
|
253
|
+
label: `${operation.label} ${row.label}`,
|
|
254
|
+
onChange: () => {
|
|
255
|
+
toggle({
|
|
256
|
+
kind: "cell",
|
|
257
|
+
namespace: namespace.id,
|
|
258
|
+
fieldName: row.fieldName,
|
|
259
|
+
operation: operation.id,
|
|
260
|
+
value: values[cellPath(path, namespace.id, row.fieldName, operation.id)] !== true
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
readOnly,
|
|
264
|
+
state: values[cellPath(path, namespace.id, row.fieldName, operation.id)] === true ? "on" : "off"
|
|
265
|
+
}) : /*#__PURE__*/ jsx(Dash, {})
|
|
266
|
+
}, operation.id))] }, row.fieldName);
|
|
267
|
+
}) })
|
|
268
|
+
]
|
|
269
|
+
})
|
|
270
|
+
}, namespace.id);
|
|
271
|
+
}),
|
|
272
|
+
matrix.tools.length > 0 ? /*#__PURE__*/ jsx("section", {
|
|
273
|
+
style: styles.section,
|
|
274
|
+
children: /*#__PURE__*/ jsxs("table", {
|
|
275
|
+
style: styles.table,
|
|
276
|
+
children: [
|
|
277
|
+
/*#__PURE__*/ jsx(Columns, { count: 1 }),
|
|
278
|
+
/*#__PURE__*/ jsxs("thead", {
|
|
279
|
+
style: styles.head,
|
|
280
|
+
children: [/*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
|
|
281
|
+
scope: "col",
|
|
282
|
+
style: {
|
|
283
|
+
...styles.headTitle,
|
|
284
|
+
...styles.topStart
|
|
285
|
+
},
|
|
286
|
+
children: "Tools"
|
|
287
|
+
}), /*#__PURE__*/ jsx("th", {
|
|
288
|
+
scope: "col",
|
|
289
|
+
style: {
|
|
290
|
+
...styles.headOperation,
|
|
291
|
+
...styles.topEnd
|
|
292
|
+
},
|
|
293
|
+
children: "Enabled"
|
|
294
|
+
})] }), /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsx("th", {
|
|
295
|
+
scope: "row",
|
|
296
|
+
style: {
|
|
297
|
+
...styles.headLabel,
|
|
298
|
+
...styles.rule
|
|
299
|
+
},
|
|
300
|
+
children: "All"
|
|
301
|
+
}), /*#__PURE__*/ jsx("td", {
|
|
302
|
+
style: {
|
|
303
|
+
...cellStyle(true),
|
|
304
|
+
...styles.rule
|
|
305
|
+
},
|
|
306
|
+
children: /*#__PURE__*/ jsx(Box, {
|
|
307
|
+
label: "Enable every tool",
|
|
308
|
+
onChange: () => {
|
|
309
|
+
toggle({
|
|
310
|
+
kind: "tools",
|
|
311
|
+
value: toolState !== "on"
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
readOnly,
|
|
315
|
+
state: toolState
|
|
316
|
+
})
|
|
317
|
+
})] })]
|
|
318
|
+
}),
|
|
319
|
+
/*#__PURE__*/ jsx("tbody", { children: matrix.tools.map((tool) => /*#__PURE__*/ jsxs("tr", { children: [/*#__PURE__*/ jsxs("th", {
|
|
320
|
+
scope: "row",
|
|
321
|
+
style: styles.rowHeader,
|
|
322
|
+
children: [tool.name, /*#__PURE__*/ jsx("span", {
|
|
323
|
+
style: styles.hint,
|
|
324
|
+
children: tool.description
|
|
325
|
+
})]
|
|
326
|
+
}), /*#__PURE__*/ jsx("td", {
|
|
327
|
+
style: cellStyle(true),
|
|
328
|
+
children: /*#__PURE__*/ jsx(Box, {
|
|
329
|
+
label: `Enable ${tool.name}`,
|
|
330
|
+
onChange: () => {
|
|
331
|
+
toggle({
|
|
332
|
+
kind: "tool",
|
|
333
|
+
name: tool.name,
|
|
334
|
+
value: values[toolPath(path, tool.name)] !== true
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
readOnly,
|
|
338
|
+
state: values[toolPath(path, tool.name)] === true ? "on" : "off"
|
|
339
|
+
})
|
|
340
|
+
})] }, tool.name)) })
|
|
341
|
+
]
|
|
342
|
+
})
|
|
343
|
+
}) : null
|
|
344
|
+
]
|
|
345
|
+
})]
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
//#endregion
|
|
349
|
+
export { McpxCapabilityMatrix };
|
package/dist/client/index.d.mts
CHANGED
package/dist/client/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@abinnovision/payloadcms-mcpx",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0-beta.5",
|
|
5
5
|
"description": "Payload CMS plugin exposing a fixed, schema-aware MCP tool surface with draft-only writes and per-API-key capabilities.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"payload",
|
|
@@ -59,25 +59,25 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
61
61
|
"rfc6902": "^5.3.0",
|
|
62
|
-
"zod": "^4.5
|
|
62
|
+
"zod": "^4.6.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@abinnovision/eslint-config-base": "^3.4.2",
|
|
66
66
|
"@abinnovision/prettier-config": "^2.2.0",
|
|
67
67
|
"@arethetypeswrong/core": "^0.18.5",
|
|
68
|
-
"@payloadcms/db-sqlite": "3.
|
|
69
|
-
"@payloadcms/richtext-lexical": "3.
|
|
70
|
-
"@payloadcms/ui": "3.
|
|
68
|
+
"@payloadcms/db-sqlite": "3.89.0",
|
|
69
|
+
"@payloadcms/richtext-lexical": "3.89.0",
|
|
70
|
+
"@payloadcms/ui": "3.89.0",
|
|
71
71
|
"@swc/core": "^1.16.2",
|
|
72
72
|
"@types/node": "^26.5.1",
|
|
73
|
-
"@types/react": "^19.
|
|
73
|
+
"@types/react": "^19.3.0",
|
|
74
74
|
"@vitest/coverage-v8": "^5.0.0",
|
|
75
75
|
"eslint": "^10.10.0",
|
|
76
|
-
"payload": "3.
|
|
76
|
+
"payload": "3.89.0",
|
|
77
77
|
"prettier": "^3.9.5",
|
|
78
78
|
"publint": "^0.3.24",
|
|
79
|
-
"react": "^19.
|
|
80
|
-
"react-dom": "^19.
|
|
79
|
+
"react": "^19.3.0",
|
|
80
|
+
"react-dom": "^19.3.0",
|
|
81
81
|
"tsdown": "^0.23.0",
|
|
82
82
|
"typescript": "^6.0.3",
|
|
83
83
|
"unplugin-swc": "^1.6.0",
|