@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 +20 -0
- package/dist/api-keys/collection.mjs +3 -3
- package/dist/api-keys/fields.mjs +35 -1
- package/dist/api-keys/setup-guide.mjs +54 -0
- package/dist/client/index.d.mts +2 -0
- package/dist/client/index.mjs +2 -0
- package/dist/client/setup-guide.d.mts +14 -0
- package/dist/client/setup-guide.mjs +87 -0
- package/dist/options.mjs +1 -0
- package/dist/types.d.mts +5 -0
- package/package.json +22 -7
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
|
package/dist/api-keys/fields.mjs
CHANGED
|
@@ -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,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.
|
|
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
|
-
"
|
|
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,
|