@abinnovision/payloadcms-mcpx 1.0.1-beta.2 → 1.1.0-beta.3

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 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 tab renders an admin component, so it has to be in the import map:
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 that entry Payload logs a missing-component error and renders nothing
355
- else; the rest of the plugin is unaffected. The URL comes from `serverURL` when
356
- the config sets one and from the browser's origin otherwise.
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 };
@@ -1,4 +1,5 @@
1
- import { CAPABILITIES_FIELD, canCreate, canPublish, canWrite } from "../capabilities.mjs";
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. Only operations the plugin config exposes get a checkbox, so
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
- * An upload collection gets the same checkboxes as any other, only worded for
104
- * what `write` reaches there: a document's own fields, never `createDocument`,
105
- * because the file comes from the admin panel.
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 collectionGroups = options.collections.map((collection) => ({
108
- name: collection.fieldName,
110
+ const matrix = createCapabilityMatrix(options);
111
+ const entityGroups = (rows) => rows.map((row) => ({
112
+ name: row.fieldName,
109
113
  type: "group",
110
- label: collection.slug,
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 globalGroups = options.globals.map((global) => ({
118
- name: global.fieldName,
119
- type: "group",
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: { description: "What this key may do. Unchecked means refused, whatever the plugin config allows." },
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 };
@@ -1,2 +1,3 @@
1
+ import { McpxCapabilityMatrix } from "./capability-matrix.mjs";
1
2
  import { McpxSetupGuide } from "./setup-guide.mjs";
2
- export { McpxSetupGuide };
3
+ export { McpxCapabilityMatrix, McpxSetupGuide };
@@ -1,2 +1,3 @@
1
+ import { McpxCapabilityMatrix } from "./capability-matrix.mjs";
1
2
  import { McpxSetupGuide } from "./setup-guide.mjs";
2
- export { McpxSetupGuide };
3
+ export { McpxCapabilityMatrix, McpxSetupGuide };
@@ -0,0 +1,2 @@
1
+ import "./types.mjs";
2
+ import "payload";
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.1-beta.2",
4
+ "version": "1.1.0-beta.3",
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",