@abinnovision/payloadcms-mcpx 1.0.0-beta.8 → 1.0.0-beta.9

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
@@ -27,6 +27,9 @@ yarn add @abinnovision/payloadcms-mcpx
27
27
  ```
28
28
 
29
29
  - Peer dependency: `payload >=3.88.0 <4`.
30
+ - `@payloadcms/ui` and `react` are optional peers, needed only by the admin
31
+ setup guide. A headless install can leave them out and set
32
+ `apiKeys.setupGuide: false`.
30
33
  - The package is published as ESM only, matching Payload itself.
31
34
 
32
35
  ## Usage
@@ -85,6 +88,22 @@ all keys) or add fields.
85
88
 
86
89
  ## Connecting a client
87
90
 
91
+ Saved keys carry a **Connect a client** tab in the admin holding these same
92
+ instructions with their own URL and key filled in, each block behind a copy
93
+ button. The tab only exists once the key does, so the create form stays free of
94
+ it. Turn it off with `apiKeys.setupGuide: false`, which also drops the tabs and
95
+ restores the flat form.
96
+
97
+ The tab renders an admin component, so it has to be in the import map:
98
+
99
+ ```bash
100
+ payload generate:importmap
101
+ ```
102
+
103
+ Without that entry Payload logs a missing-component error and renders nothing
104
+ else; the rest of the plugin is unaffected. The URL comes from `serverURL` when
105
+ the config sets one and from the browser's origin otherwise.
106
+
88
107
  The endpoint speaks streamable HTTP with `Authorization: Bearer <key>`:
89
108
 
90
109
  ```bash
@@ -296,6 +315,7 @@ Builtin tools reject them instead.
296
315
  | `globals.<slug>.allowLiveWrites` | `false` | Permit writes to a global without drafts (they land live). |
297
316
  | `userCollection` | `config.admin.user` or `users` | Auth collection the keys act as. |
298
317
  | `apiKeys.slug` | `mcpx-api-keys` | Slug of the generated key collection. |
318
+ | `apiKeys.setupGuide` | `true` | Add a "Connect a client" tab to saved keys. Needs the import map. |
299
319
  | `apiKeys.overrideCollection` | none | Final override applied to the generated collection. |
300
320
  | `endpoint.path` | `/mcpx` | Endpoint path below the API route. |
301
321
  | `limits.maxLimit` | `25` | Upper bound for `findDocuments.limit`. |
@@ -1,4 +1,4 @@
1
- import { createCapabilityFields, createKeyFields } from "./fields.mjs";
1
+ import { createCapabilityFields, createKeyFields, withSetupGuideTab } from "./fields.mjs";
2
2
  import { generateApiKey, hashApiKey } from "./key.mjs";
3
3
  //#region src/api-keys/collection.ts
4
4
  /**
@@ -36,7 +36,7 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
36
36
  delete: ownKeysOnly
37
37
  },
38
38
  hooks: { beforeChange: [keyBeforeChange] },
39
- fields: [
39
+ fields: withSetupGuideTab([
40
40
  {
41
41
  name: "user",
42
42
  type: "relationship",
@@ -51,7 +51,7 @@ import { generateApiKey, hashApiKey } from "./key.mjs";
51
51
  },
52
52
  ...createKeyFields(),
53
53
  ...createCapabilityFields(options)
54
- ]
54
+ ], options)
55
55
  };
56
56
  };
57
57
  //#endregion
@@ -15,6 +15,7 @@ const checkbox = (name, description) => ({
15
15
  defaultValue: false,
16
16
  admin: { description }
17
17
  });
18
+ const SETUP_GUIDE_FIELD = "setupGuide";
18
19
  /**
19
20
  * Fields every key carries. Key generation and the HMAC index live in the
20
21
  * collection-level `beforeChange` hook (see `collection.ts`), because sibling
@@ -56,6 +57,39 @@ const checkbox = (name, description) => ({
56
57
  }
57
58
  ];
58
59
  /**
60
+ * Wraps the key fields and the setup guide in unnamed tabs, so the wide
61
+ * snippets get the full form width without pushing the key itself out of view.
62
+ * Unnamed on purpose: named tabs would nest the data and move `capabilities`
63
+ * off the document root, which capability resolution reads.
64
+ *
65
+ * The guide tab is conditioned on the update operation. On create there is no
66
+ * key to hand out, and a tab leading to an empty panel is worse than no tab.
67
+ */ const withSetupGuideTab = (keyFields, options) => {
68
+ if (!options.setupGuide) return keyFields;
69
+ return [{
70
+ type: "tabs",
71
+ tabs: [{
72
+ label: "Key",
73
+ fields: keyFields
74
+ }, {
75
+ label: "Connect a client",
76
+ admin: { condition: (_data, _siblingData, { operation }) => operation === "update" },
77
+ fields: [{
78
+ name: SETUP_GUIDE_FIELD,
79
+ type: "ui",
80
+ admin: {
81
+ disableListColumn: true,
82
+ components: { Field: {
83
+ path: "@abinnovision/payloadcms-mcpx/client",
84
+ exportName: "McpxSetupGuide",
85
+ clientProps: { endpointPath: options.endpointPath }
86
+ } }
87
+ }
88
+ }]
89
+ }]
90
+ }];
91
+ };
92
+ /**
59
93
  * One checkbox per exposed operation, grouped per collection, per global and
60
94
  * per custom tool. Only operations the plugin config exposes get a checkbox, so
61
95
  * a key can never enable more than the config allows. Everything defaults to
@@ -100,4 +134,4 @@ const checkbox = (name, description) => ({
100
134
  }];
101
135
  };
102
136
  //#endregion
103
- export { createCapabilityFields, createKeyFields };
137
+ export { SETUP_GUIDE_FIELD, createCapabilityFields, createKeyFields, withSetupGuideTab };
@@ -0,0 +1,54 @@
1
+ //#region src/api-keys/setup-guide.ts
2
+ const KEY_PLACEHOLDER = "<your-key>";
3
+ /**
4
+ * Server name for the client config. MCP clients key their config by this, so
5
+ * it has to survive labels with spaces or punctuation.
6
+ */ const toServerName = (label) => {
7
+ const slug = (label ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
8
+ return slug === "" ? "payload" : slug;
9
+ };
10
+ /**
11
+ * The connection instructions for one key, split into independently copyable
12
+ * blocks. Kept a pure builder so the admin component holds only rendering and
13
+ * the snippets stay unit-testable.
14
+ */ const buildSetupGuide = (input) => {
15
+ const key = typeof input.apiKey === "string" ? input.apiKey : KEY_PLACEHOLDER;
16
+ const name = toServerName(input.label);
17
+ const url = input.endpointUrl;
18
+ return [
19
+ {
20
+ id: "endpoint",
21
+ title: "Endpoint",
22
+ description: "Streamable HTTP. Point any MCP client at this URL.",
23
+ snippet: url
24
+ },
25
+ {
26
+ id: "header",
27
+ title: "Authorization header",
28
+ description: "The only accepted credential; cookies and JWTs are ignored.",
29
+ snippet: `Authorization: Bearer ${key}`
30
+ },
31
+ {
32
+ id: "claude-code",
33
+ title: "Claude Code",
34
+ snippet: [`claude mcp add --transport http ${name} ${url} \\`, ` --header "Authorization: Bearer ${key}"`].join("\n")
35
+ },
36
+ {
37
+ id: "claude-desktop",
38
+ title: "Claude Desktop",
39
+ description: "Has no direct header support, so it goes through mcp-remote.",
40
+ snippet: JSON.stringify({ mcpServers: { [name]: {
41
+ command: "npx",
42
+ args: [
43
+ "-y",
44
+ "mcp-remote",
45
+ url,
46
+ "--header",
47
+ `Authorization: Bearer ${key}`
48
+ ]
49
+ } } }, null, 2)
50
+ }
51
+ ];
52
+ };
53
+ //#endregion
54
+ export { KEY_PLACEHOLDER, buildSetupGuide, toServerName };
@@ -0,0 +1,2 @@
1
+ import { McpxSetupGuide, McpxSetupGuideProps } from "./setup-guide.mjs";
2
+ export { McpxSetupGuide, type McpxSetupGuideProps };
@@ -0,0 +1,2 @@
1
+ import { McpxSetupGuide } from "./setup-guide.mjs";
2
+ export { McpxSetupGuide };
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ //#region src/client/setup-guide.d.ts
3
+ interface McpxSetupGuideProps {
4
+ /** Endpoint path below the API route, from the plugin options. */
5
+ endpointPath: string;
6
+ }
7
+ /**
8
+ * Per-key connection instructions on the API key edit view. Renders nothing
9
+ * until the document is saved, because before that there is no key to hand to
10
+ * a client.
11
+ */
12
+ declare const McpxSetupGuide: React.FC<McpxSetupGuideProps>;
13
+ //#endregion
14
+ export { McpxSetupGuide, type McpxSetupGuideProps };
@@ -0,0 +1,87 @@
1
+ "use client";
2
+ import { buildSetupGuide } from "../api-keys/setup-guide.mjs";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { CopyToClipboard, useConfig, useDocumentInfo, useFormFields } from "@payloadcms/ui";
5
+ import { useEffect, useState } from "react";
6
+ //#region src/client/setup-guide.tsx
7
+ const asString = (value) => typeof value === "string" ? value : void 0;
8
+ /**
9
+ * Reads `serverURL` when the config sets one and falls back to the browser's
10
+ * origin. The fallback has to wait for mount: this component is server-rendered
11
+ * first, where `window` does not exist.
12
+ */ const useOrigin = (serverUrl) => {
13
+ const [origin, setOrigin] = useState(serverUrl);
14
+ useEffect(() => {
15
+ if (serverUrl === "") setOrigin(window.location.origin);
16
+ }, [serverUrl]);
17
+ return origin;
18
+ };
19
+ /**
20
+ * Payload's own theme variables, so the panel follows the admin's light and
21
+ * dark themes without shipping a stylesheet consumers would have to transpile.
22
+ */ const styles = {
23
+ lead: { marginBottom: "calc(var(--base) * 0.75)" },
24
+ section: { marginBottom: "calc(var(--base) * 0.75)" },
25
+ sectionHeader: {
26
+ display: "flex",
27
+ alignItems: "center",
28
+ gap: "calc(var(--base) * 0.25)"
29
+ },
30
+ description: {
31
+ margin: "calc(var(--base) * 0.15) 0",
32
+ color: "var(--theme-elevation-500)"
33
+ },
34
+ snippet: {
35
+ margin: 0,
36
+ padding: "calc(var(--base) * 0.4)",
37
+ background: "var(--theme-elevation-50)",
38
+ border: "1px solid var(--theme-elevation-150)",
39
+ borderRadius: "3px",
40
+ fontFamily: "var(--font-mono)",
41
+ overflowX: "auto",
42
+ whiteSpace: "pre-wrap",
43
+ wordBreak: "break-all"
44
+ }
45
+ };
46
+ /**
47
+ * Per-key connection instructions on the API key edit view. Renders nothing
48
+ * until the document is saved, because before that there is no key to hand to
49
+ * a client.
50
+ */ const McpxSetupGuide = ({ endpointPath }) => {
51
+ const { id } = useDocumentInfo();
52
+ const { config } = useConfig();
53
+ const apiKey = useFormFields(([fields]) => fields["apiKey"]?.value);
54
+ const label = useFormFields(([fields]) => fields["label"]?.value);
55
+ const origin = useOrigin(config.serverURL);
56
+ if (id === void 0) return null;
57
+ const sections = buildSetupGuide({
58
+ endpointUrl: `${origin.replace(/\/+$/, "")}${config.routes.api}${endpointPath}`,
59
+ apiKey: asString(apiKey),
60
+ label: asString(label)
61
+ });
62
+ return /*#__PURE__*/ jsxs("div", {
63
+ className: "field-type",
64
+ children: [/*#__PURE__*/ jsx("p", {
65
+ style: styles.lead,
66
+ children: "Every snippet below contains this key in full. Treat it like a password."
67
+ }), sections.map((section) => /*#__PURE__*/ jsxs("section", {
68
+ style: styles.section,
69
+ children: [
70
+ /*#__PURE__*/ jsxs("div", {
71
+ style: styles.sectionHeader,
72
+ children: [/*#__PURE__*/ jsx("strong", { children: section.title }), /*#__PURE__*/ jsx(CopyToClipboard, { value: section.snippet })]
73
+ }),
74
+ section.description ? /*#__PURE__*/ jsx("p", {
75
+ style: styles.description,
76
+ children: section.description
77
+ }) : null,
78
+ /*#__PURE__*/ jsx("pre", {
79
+ style: styles.snippet,
80
+ children: section.snippet
81
+ })
82
+ ]
83
+ }, section.id))]
84
+ });
85
+ };
86
+ //#endregion
87
+ export { McpxSetupGuide };
package/dist/options.mjs CHANGED
@@ -132,6 +132,7 @@ const normalizeLimits = (limits) => {
132
132
  userCollection,
133
133
  apiKeysSlug,
134
134
  endpointPath: options.endpoint?.path ?? DEFAULT_ENDPOINT_PATH,
135
+ setupGuide: options.apiKeys?.setupGuide ?? true,
135
136
  limits: normalizeLimits(options.limits),
136
137
  tools,
137
138
  auth: options.auth,
package/dist/types.d.mts CHANGED
@@ -90,6 +90,11 @@ type McpxPluginOptions = {
90
90
  apiKeys?: {
91
91
  /** Slug of the generated API key collection. Default `mcpx-api-keys`. */
92
92
  slug?: string;
93
+ /**
94
+ * Add a "Connect a client" tab to saved keys, holding ready-to-paste MCP
95
+ * client config. Default `true`. The snippets contain the key in full.
96
+ */
97
+ setupGuide?: boolean;
93
98
  /** Final override applied to the generated collection. */
94
99
  overrideCollection?: (collection: CollectionConfig) => CollectionConfig;
95
100
  };
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.0-beta.8",
4
+ "version": "1.0.0-beta.9",
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",
@@ -26,6 +26,10 @@
26
26
  ".": {
27
27
  "types": "./dist/index.d.mts",
28
28
  "default": "./dist/index.mjs"
29
+ },
30
+ "./client": {
31
+ "types": "./dist/client/index.d.mts",
32
+ "default": "./dist/client/index.mjs"
29
33
  }
30
34
  },
31
35
  "files": [
@@ -35,17 +39,17 @@
35
39
  ],
36
40
  "scripts": {
37
41
  "build": "tsdown",
38
- "format:check": "prettier --check 'src/**/*.ts' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
39
- "format:fix": "prettier --write 'src/**/*.ts' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
40
- "lint:check": "eslint 'src/**/*.ts' 'test/**/*.ts'",
41
- "lint:fix": "eslint 'src/**/*.ts' 'test/**/*.ts' --fix",
42
+ "format:check": "prettier --check 'src/**/*.{ts,tsx}' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
43
+ "format:fix": "prettier --write 'src/**/*.{ts,tsx}' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
44
+ "lint:check": "eslint 'src/**/*.{ts,tsx}' 'test/**/*.ts'",
45
+ "lint:fix": "eslint 'src/**/*.{ts,tsx}' 'test/**/*.ts' --fix",
42
46
  "test-integration": "vitest --run --config test/integration/vitest.config.mts",
43
47
  "test-unit": "vitest --run --coverage --config vitest.config.mts",
44
48
  "test-unit:watch": "vitest --config vitest.config.mts",
45
49
  "typecheck": "tsc --noEmit"
46
50
  },
47
51
  "lint-staged": {
48
- "{src,test}/**/*.ts": [
52
+ "{src,test}/**/*.{ts,tsx}": [
49
53
  "eslint --fix",
50
54
  "prettier --write"
51
55
  ],
@@ -63,6 +67,7 @@
63
67
  "@arethetypeswrong/core": "^0.18.5",
64
68
  "@payloadcms/db-sqlite": "3.88.0",
65
69
  "@payloadcms/richtext-lexical": "3.88.0",
70
+ "@payloadcms/ui": "3.88.0",
66
71
  "@swc/core": "^1.16.1",
67
72
  "@types/node": "^26.2.0",
68
73
  "@types/react": "^19.2.18",
@@ -79,7 +84,17 @@
79
84
  "vitest": "^4.1.10"
80
85
  },
81
86
  "peerDependencies": {
82
- "payload": ">=3.88.0 <4"
87
+ "@payloadcms/ui": ">=3.88.0 <4",
88
+ "payload": ">=3.88.0 <4",
89
+ "react": "^19"
90
+ },
91
+ "peerDependenciesMeta": {
92
+ "@payloadcms/ui": {
93
+ "optional": true
94
+ },
95
+ "react": {
96
+ "optional": true
97
+ }
83
98
  },
84
99
  "publishConfig": {
85
100
  "ghpr": true,