@abinnovision/payloadcms-mcpx 1.0.0-beta.10

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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +353 -0
  3. package/dist/api-keys/collection.mjs +58 -0
  4. package/dist/api-keys/fields.mjs +137 -0
  5. package/dist/api-keys/key.mjs +10 -0
  6. package/dist/api-keys/setup-guide.mjs +54 -0
  7. package/dist/auth/resolve.mjs +62 -0
  8. package/dist/capabilities.mjs +43 -0
  9. package/dist/client/index.d.mts +2 -0
  10. package/dist/client/index.mjs +2 -0
  11. package/dist/client/setup-guide.d.mts +14 -0
  12. package/dist/client/setup-guide.mjs +87 -0
  13. package/dist/endpoint/handler.mjs +86 -0
  14. package/dist/endpoint/result.mjs +65 -0
  15. package/dist/endpoint/server.mjs +52 -0
  16. package/dist/i18n.d.mts +1 -0
  17. package/dist/i18n.mjs +40 -0
  18. package/dist/index.d.mts +5 -0
  19. package/dist/index.mjs +4 -0
  20. package/dist/options.d.mts +2 -0
  21. package/dist/options.mjs +146 -0
  22. package/dist/plugin.d.mts +9 -0
  23. package/dist/plugin.mjs +43 -0
  24. package/dist/schema/describe.mjs +160 -0
  25. package/dist/schema/lexical.d.mts +1 -0
  26. package/dist/schema/lexical.mjs +117 -0
  27. package/dist/schema/pointer.mjs +80 -0
  28. package/dist/schema/shape.mjs +208 -0
  29. package/dist/schema/walk.d.mts +3 -0
  30. package/dist/schema/walk.mjs +165 -0
  31. package/dist/tools/create-document.mjs +68 -0
  32. package/dist/tools/describe-schema.mjs +54 -0
  33. package/dist/tools/find-documents.mjs +55 -0
  34. package/dist/tools/get-document.mjs +68 -0
  35. package/dist/tools/index.mjs +23 -0
  36. package/dist/tools/list-capabilities.mjs +68 -0
  37. package/dist/tools/names.mjs +14 -0
  38. package/dist/tools/patch-document.mjs +127 -0
  39. package/dist/tools/shared.mjs +97 -0
  40. package/dist/tools/target.d.mts +3 -0
  41. package/dist/tools/target.mjs +49 -0
  42. package/dist/tools/types.d.mts +5 -0
  43. package/dist/tools/validate-document.mjs +55 -0
  44. package/dist/types.d.mts +143 -0
  45. package/dist/types.mjs +7 -0
  46. package/dist/version.mjs +6 -0
  47. package/dist/write/draft-guard.d.mts +10 -0
  48. package/dist/write/draft-guard.mjs +113 -0
  49. package/dist/write/patch.mjs +219 -0
  50. package/dist/write/publish-blockers.d.mts +15 -0
  51. package/dist/write/publish-blockers.mjs +74 -0
  52. package/dist/write/transaction.mjs +19 -0
  53. package/package.json +104 -0
@@ -0,0 +1,68 @@
1
+ import { translatorFor } from "../i18n.mjs";
2
+ import { jsonResult } from "../endpoint/result.mjs";
3
+ import { translateLabel } from "./shared.mjs";
4
+ import { hasDraftValidationEnabled } from "payload/shared";
5
+ //#region src/tools/list-capabilities.ts
6
+ const listCapabilities = {
7
+ name: "listCapabilities",
8
+ description: `Lists what this key may do: the collections and globals it can read or write, their draft behaviour and id type, the configured locales, the limits in force and the custom tools available. Call it first to orient; nothing here changes with the content model.
9
+
10
+ A global is a singleton: it has no id, is not listed by findDocuments and cannot be created. Address one with the "global" argument where a collection document would take "collection" and "id".`,
11
+ annotations: {
12
+ readOnlyHint: true,
13
+ openWorldHint: false
14
+ },
15
+ isEnabled: () => true,
16
+ inputSchema: () => ({}),
17
+ handler: (_args, scope) => {
18
+ const { payload } = scope.req;
19
+ const translate = translatorFor(scope.req.i18n);
20
+ const collections = scope.options.collections.flatMap((entry) => {
21
+ const capability = scope.capabilities.collections[entry.slug];
22
+ const collection = payload.collections[entry.slug];
23
+ if (!capability || !collection || !(capability.read || capability.write)) return [];
24
+ const { config } = collection;
25
+ const description = translate(config.admin.description);
26
+ return [{
27
+ slug: entry.slug,
28
+ labels: {
29
+ singular: translateLabel(scope, config.labels.singular, entry.slug),
30
+ plural: translateLabel(scope, config.labels.plural, entry.slug)
31
+ },
32
+ ...description === void 0 ? {} : { description },
33
+ read: capability.read,
34
+ write: capability.write,
35
+ drafts: entry.hasDrafts,
36
+ draftValidation: hasDraftValidationEnabled(config),
37
+ idType: collection.customIDType ?? payload.db.defaultIDType
38
+ }];
39
+ });
40
+ const globals = scope.options.globals.flatMap((entry) => {
41
+ const capability = scope.capabilities.globals[entry.slug];
42
+ const config = payload.globals.config.find((candidate) => candidate.slug === entry.slug);
43
+ if (!capability || !config || !(capability.read || capability.write)) return [];
44
+ const description = translate(config.admin.description);
45
+ return [{
46
+ slug: entry.slug,
47
+ label: translateLabel(scope, config.label, entry.slug),
48
+ ...description === void 0 ? {} : { description },
49
+ read: capability.read,
50
+ write: capability.write,
51
+ drafts: entry.hasDrafts,
52
+ draftValidation: hasDraftValidationEnabled(config)
53
+ }];
54
+ });
55
+ return Promise.resolve(jsonResult({
56
+ collections,
57
+ ...globals.length > 0 ? { globals } : {},
58
+ locales: scope.locales ? {
59
+ codes: scope.locales,
60
+ default: scope.defaultLocale
61
+ } : null,
62
+ limits: scope.options.limits,
63
+ tools: Object.entries(scope.capabilities.tools).filter(([, enabled]) => enabled).map(([name]) => name)
64
+ }));
65
+ }
66
+ };
67
+ //#endregion
68
+ export { listCapabilities };
@@ -0,0 +1,14 @@
1
+ //#region src/tools/names.ts
2
+ /**
3
+ * Names of the builtin tools. Custom tools must not reuse them.
4
+ */ const BUILTIN_TOOL_NAMES = [
5
+ "listCapabilities",
6
+ "describeSchema",
7
+ "findDocuments",
8
+ "getDocument",
9
+ "patchDocument",
10
+ "createDocument",
11
+ "validateDocument"
12
+ ];
13
+ //#endregion
14
+ export { BUILTIN_TOOL_NAMES };
@@ -0,0 +1,127 @@
1
+ import { errorResult, jsonResult } from "../endpoint/result.mjs";
2
+ import { idShape, localeOf, localeShape, readTarget, targetShape } from "./shared.mjs";
3
+ import { refOf, requireIdFor, resolveTarget } from "./target.mjs";
4
+ import { PATCH_OPERATION_SCHEMA, applyPatchToCopy, buildWriteData, findPatchProblems, isElementPointer } from "../write/patch.mjs";
5
+ import { collectPublishBlockers } from "../write/publish-blockers.mjs";
6
+ import { withTransaction } from "../write/transaction.mjs";
7
+ import { z } from "zod";
8
+ import { Pointer } from "rfc6902";
9
+ //#region src/tools/patch-document.ts
10
+ const DESCRIPTION = `Applies RFC 6902 JSON Patch operations to one document.
11
+
12
+ Pass exactly one of "collection" and "global". "id" is required with "collection" and must be omitted with "global", because a global is a singleton.
13
+
14
+ The write always lands as a draft and is never published, whatever it contains; publishing stays a human action in the admin panel.
15
+
16
+ Only the fields describeSchema lists can be addressed. A pointer that does not resolve is refused with the fields that are valid at that point, and nothing is applied unless every operation in the batch validates first. describeSchema reports field paths in this same pointer syntax; a path becomes a pointer into a document by replacing each "*" and each block slug with its 0-based index.
17
+
18
+ Adding a block requires "blockType" on the value. Append with "/-" as the last segment. To clear a field use "replace" with null; an array or blocks field refuses null and is emptied with [] instead. "remove" is only for list elements, because a field left out of a write is kept rather than cleared. Read the document first to learn the indices, and pass its "updatedAt" as expectedUpdatedAt so a concurrent edit is refused rather than overwritten.
19
+
20
+ A successful write may come back with "publishBlockers": everything still wrong with the draft, such as required fields left empty. Those do not fail the write, because a draft is allowed to be incomplete, but a human cannot publish the document until the list is empty. "notApplied" lists pointers whose value Payload kept unchanged, which happens when field-level access denies the update.`;
21
+ const sameInstant = (left, right) => typeof left === "string" && new Date(left).getTime() === new Date(right).getTime();
22
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
23
+ /**
24
+ * Whether the intended value survived the write. The saved document is
25
+ * allowed to carry more than was sent: Payload assigns fresh row ids and
26
+ * backfills defaults and nulls on save, so `id` keys are ignored and only
27
+ * the keys the client sent are compared. Null and absent count as equal.
28
+ */ const survives = (expected, actual) => {
29
+ if (expected === void 0 || expected === null) return actual === void 0 || actual === null;
30
+ if (Array.isArray(expected)) return Array.isArray(actual) && expected.length === actual.length && expected.every((entry, index) => survives(entry, actual[index]));
31
+ if (isPlainObject(expected)) return isPlainObject(actual) && Object.entries(expected).every(([key, value]) => key === "id" || survives(value, actual[key]));
32
+ return isPlainObject(actual) || Array.isArray(actual) ? false : JSON.stringify(expected) === JSON.stringify(actual);
33
+ };
34
+ /**
35
+ * Pointers whose intended value did not survive the write. Element pointers
36
+ * are skipped: an append pointer (`/-`) does not resolve against the saved
37
+ * document.
38
+ */ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operation) => {
39
+ if (operation.op !== "add" && operation.op !== "replace" || isElementPointer(operation.path)) return [];
40
+ const pointer = Pointer.fromJSON(operation.path);
41
+ const expected = pointer.get(intended);
42
+ const actual = pointer.get(saved);
43
+ return survives(expected, actual) ? [] : [operation.path];
44
+ });
45
+ const patchDocument = {
46
+ name: "patchDocument",
47
+ description: DESCRIPTION,
48
+ annotations: {
49
+ readOnlyHint: false,
50
+ destructiveHint: false,
51
+ idempotentHint: false,
52
+ openWorldHint: false
53
+ },
54
+ isEnabled: (scope) => scope.writable.length + scope.writableGlobals.length > 0,
55
+ inputSchema: (scope) => ({
56
+ ...targetShape(scope, "write", {
57
+ collection: "Collection holding the document.",
58
+ global: "Global to patch."
59
+ }),
60
+ ...idShape(scope, "write"),
61
+ ...localeShape(scope, {
62
+ required: true,
63
+ description: "Locale the patch applies to. Localized fields write here only."
64
+ }),
65
+ patches: z.array(PATCH_OPERATION_SCHEMA).min(1).describe("Operations, applied in order."),
66
+ expectedUpdatedAt: z.string().optional().describe("The updatedAt read before patching. The write is refused if the document has changed since.")
67
+ }),
68
+ handler: async (args, scope) => {
69
+ const target = resolveTarget(scope, args, "write");
70
+ const id = requireIdFor(target, args.id);
71
+ const { payload } = scope.req;
72
+ const locale = localeOf(scope, args.locale);
73
+ return await withTransaction(scope.req, async () => {
74
+ const doc = await readTarget(scope, {
75
+ target,
76
+ id,
77
+ locale
78
+ });
79
+ if (args.expectedUpdatedAt !== void 0 && !sameInstant(doc["updatedAt"], args.expectedUpdatedAt)) return errorResult("The document changed since you read it. Read it again and re-apply the patch.", { updatedAt: doc["updatedAt"] });
80
+ const problems = findPatchProblems(payload.config, {
81
+ doc,
82
+ patches: args.patches,
83
+ ref: refOf(target)
84
+ });
85
+ if (problems.length > 0) return errorResult("No operation was applied.", { problems });
86
+ const applied = applyPatchToCopy(doc, args.patches);
87
+ if ("problems" in applied) return errorResult("No operation was applied.", { problems: applied.problems });
88
+ const write = {
89
+ data: buildWriteData(payload.config, target.config, applied.next),
90
+ depth: 0,
91
+ draft: true,
92
+ overrideAccess: false,
93
+ req: scope.req,
94
+ ...locale === void 0 ? {} : { locale }
95
+ };
96
+ if (target.kind === "collection") await payload.update({
97
+ ...write,
98
+ collection: target.slug,
99
+ id
100
+ });
101
+ else await payload.updateGlobal({
102
+ ...write,
103
+ slug: target.slug
104
+ });
105
+ const saved = await readTarget(scope, {
106
+ target,
107
+ id,
108
+ locale,
109
+ privileged: true
110
+ });
111
+ const notApplied = notAppliedPointers(args.patches, applied.next, saved);
112
+ const publishBlockers = await collectPublishBlockers(scope.req, {
113
+ doc: saved,
114
+ entity: target
115
+ });
116
+ return jsonResult({
117
+ ...target.kind === "collection" ? { id: saved["id"] } : { global: target.slug },
118
+ status: saved["_status"],
119
+ updatedAt: saved["updatedAt"],
120
+ ...publishBlockers.length > 0 ? { publishBlockers } : {},
121
+ ...notApplied.length > 0 ? { notApplied } : {}
122
+ });
123
+ });
124
+ }
125
+ };
126
+ //#endregion
127
+ export { patchDocument };
@@ -0,0 +1,97 @@
1
+ import { translateStatic } from "../i18n.mjs";
2
+ import { NotFound } from "payload";
3
+ import { z } from "zod";
4
+ //#region src/tools/shared.ts
5
+ const slugEnum = (slugs) => z.enum(slugs);
6
+ const idSchema = z.union([z.string(), z.number()]).describe("Document id.");
7
+ const slugsFor = (scope, operation) => ({
8
+ collections: operation === "read" ? scope.readable : scope.writable,
9
+ globals: operation === "read" ? scope.readableGlobals : scope.writableGlobals
10
+ });
11
+ /**
12
+ * The `collection` and `global` arguments.
13
+ *
14
+ * When the key can reach no global, `global` is left out of the shape entirely
15
+ * and `collection` stays required, mirroring how {@link localeShape} omits
16
+ * `locale` when localization is off. A deployment without globals therefore
17
+ * sees exactly the schema it saw before. Only the mixed case makes either
18
+ * argument optional, and the handler enforces the exclusivity there.
19
+ */ const targetShape = (scope, operation, descriptions) => {
20
+ const { collections, globals } = slugsFor(scope, operation);
21
+ if (globals.length === 0) return { collection: slugEnum(collections).describe(descriptions.collection) };
22
+ if (collections.length === 0) return { global: slugEnum(globals).describe(descriptions.global) };
23
+ return {
24
+ collection: slugEnum(collections).optional().describe(descriptions.collection),
25
+ global: slugEnum(globals).optional().describe(descriptions.global)
26
+ };
27
+ };
28
+ /**
29
+ * The `id` argument, which only a collection document has. Omitted when the key
30
+ * can reach no collection, required when it can reach no global, and optional
31
+ * in between, where `requireIdFor` enforces the dependency.
32
+ */ const idShape = (scope, operation) => {
33
+ const { collections, globals } = slugsFor(scope, operation);
34
+ if (collections.length === 0) return {};
35
+ if (globals.length === 0) return { id: idSchema };
36
+ return { id: idSchema.optional().describe("Document id. Required with \"collection\"; must be omitted with \"global\".") };
37
+ };
38
+ /**
39
+ * The `locale` argument, present only when localization is configured.
40
+ */ const localeShape = (scope, options) => {
41
+ if (!scope.locales) return {};
42
+ const locale = z.enum(scope.locales);
43
+ return { locale: (options.required ? locale : locale.optional()).describe(options.description) };
44
+ };
45
+ const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.options.limits.maxDepth).optional().describe(`Relationship population depth. Default 0, at most ${String(scope.options.limits.maxDepth)}.`) });
46
+ /**
47
+ * The locale to operate on: the explicit argument, else the request's, else
48
+ * the default. `undefined` when localization is off.
49
+ */ const localeOf = (scope, locale) => {
50
+ if (!scope.locales) return;
51
+ const requested = locale ?? scope.req.locale;
52
+ return (requested && scope.locales.includes(requested) ? requested : scope.defaultLocale) ?? void 0;
53
+ };
54
+ /**
55
+ * Reads the current draft in a fixed locale with no fallback, which is the
56
+ * shape that may be written back or validated without mixing locales.
57
+ */ const readTarget = async (scope, args) => {
58
+ const { payload } = scope.req;
59
+ const privileged = args.privileged === true;
60
+ const shared = {
61
+ depth: 0,
62
+ draft: true,
63
+ ...args.locale === void 0 ? {} : {
64
+ locale: args.locale,
65
+ fallbackLocale: false
66
+ },
67
+ overrideAccess: privileged,
68
+ showHiddenFields: privileged,
69
+ req: scope.req
70
+ };
71
+ if (args.target.kind === "collection") {
72
+ const doc = await payload.findByID({
73
+ ...shared,
74
+ collection: args.target.slug,
75
+ id: args.id,
76
+ disableErrors: true
77
+ });
78
+ if (!doc) throw new NotFound(scope.req.t);
79
+ return doc;
80
+ }
81
+ return await payload.findGlobal({
82
+ ...shared,
83
+ slug: args.target.slug
84
+ });
85
+ };
86
+ /**
87
+ * Resolves a collection label for the request's language.
88
+ */ const translateLabel = (scope, label, fallback) => {
89
+ const { i18n, t } = scope.req;
90
+ const resolved = typeof label === "function" ? label({
91
+ i18n,
92
+ t
93
+ }) : label;
94
+ return translateStatic(resolved, i18n) ?? fallback;
95
+ };
96
+ //#endregion
97
+ export { depthShape, idSchema, idShape, localeOf, localeShape, readTarget, slugEnum, targetShape, translateLabel };
@@ -0,0 +1,3 @@
1
+ import "./types.mjs";
2
+ import "../schema/walk.mjs";
3
+ import "payload";
@@ -0,0 +1,49 @@
1
+ import { APIError, Forbidden } from "payload";
2
+ //#region src/tools/target.ts
3
+ const refOf = (target) => ({
4
+ kind: target.kind,
5
+ slug: target.slug
6
+ });
7
+ /**
8
+ * Resolves the `collection`/`global` arguments to one entity and checks the key
9
+ * may perform `operation` on it.
10
+ *
11
+ * A tool's `inputSchema` returns a raw shape, which leaves no top-level
12
+ * `.refine` to express "exactly one of collection and global". The rule is
13
+ * enforced here instead, with a message naming the offending arguments so one
14
+ * failed call teaches it.
15
+ */ const resolveTarget = (scope, args, operation) => {
16
+ const { collection, global } = args;
17
+ if (collection !== void 0 && global !== void 0) throw new APIError("Pass either \"collection\" or \"global\", not both.", 400);
18
+ if (collection === void 0 && global === void 0) throw new APIError("One of \"collection\" or \"global\" is required. Call listCapabilities to see which slugs are available.", 400);
19
+ if (collection !== void 0) {
20
+ const allowed = operation === "read" ? scope.readable : scope.writable;
21
+ const found = scope.req.payload.collections[collection];
22
+ if (!allowed.includes(collection) || !found) throw new Forbidden(scope.req.t);
23
+ return {
24
+ kind: "collection",
25
+ slug: collection,
26
+ config: found.config
27
+ };
28
+ }
29
+ const slug = global;
30
+ const allowed = operation === "read" ? scope.readableGlobals : scope.writableGlobals;
31
+ const found = scope.req.payload.globals.config.find((candidate) => candidate.slug === slug);
32
+ if (!allowed.includes(slug) || !found) throw new Forbidden(scope.req.t);
33
+ return {
34
+ kind: "global",
35
+ slug,
36
+ config: found
37
+ };
38
+ };
39
+ /**
40
+ * Checks `id` against the resolved target. A collection document needs one; a
41
+ * global is a singleton and must not carry one. The schema cannot express the
42
+ * dependency, so it is stated here and in every affected tool description.
43
+ */ const requireIdFor = (target, id) => {
44
+ if (target.kind === "collection" && id === void 0) throw new APIError(`"id" is required when "collection" is "${target.slug}".`, 400);
45
+ if (target.kind === "global" && id !== void 0) throw new APIError(`"id" must be omitted when "global" is "${target.slug}"; a global is a singleton.`, 400);
46
+ return target.kind === "collection" ? id : void 0;
47
+ };
48
+ //#endregion
49
+ export { refOf, requireIdFor, resolveTarget };
@@ -0,0 +1,5 @@
1
+ import "../types.mjs";
2
+ import "../options.mjs";
3
+ import { PayloadRequest } from "payload";
4
+ import { z } from "zod";
5
+ import { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
@@ -0,0 +1,55 @@
1
+ import { jsonResult } from "../endpoint/result.mjs";
2
+ import { idShape, localeOf, localeShape, readTarget, targetShape } from "./shared.mjs";
3
+ import { requireIdFor, resolveTarget } from "./target.mjs";
4
+ import { collectPublishBlockers } from "../write/publish-blockers.mjs";
5
+ //#region src/tools/validate-document.ts
6
+ const validateDocument = {
7
+ name: "validateDocument",
8
+ description: `Reports what still prevents a human from publishing the draft, without writing anything. The same list patchDocument returns after a write; use it to check work or to answer "is this ready".
9
+
10
+ Pass exactly one of "collection" and "global". "id" is required with "collection" and must be omitted with "global", because a global is a singleton.`,
11
+ annotations: {
12
+ readOnlyHint: true,
13
+ openWorldHint: false
14
+ },
15
+ isEnabled: (scope) => scope.writable.length + scope.writableGlobals.length > 0,
16
+ inputSchema: (scope) => ({
17
+ ...targetShape(scope, "write", {
18
+ collection: "Collection holding the document.",
19
+ global: "Global to validate."
20
+ }),
21
+ ...idShape(scope, "write"),
22
+ ...localeShape(scope, {
23
+ required: true,
24
+ description: "Locale to validate."
25
+ })
26
+ }),
27
+ handler: async (args, scope) => {
28
+ const target = resolveTarget(scope, args, "write");
29
+ const id = requireIdFor(target, args.id);
30
+ const locale = localeOf(scope, args.locale);
31
+ await readTarget(scope, {
32
+ target,
33
+ id,
34
+ locale
35
+ });
36
+ const doc = await readTarget(scope, {
37
+ target,
38
+ id,
39
+ locale,
40
+ privileged: true
41
+ });
42
+ const publishBlockers = await collectPublishBlockers(scope.req, {
43
+ doc,
44
+ entity: target
45
+ });
46
+ return jsonResult({
47
+ ...target.kind === "collection" ? { id: doc["id"] } : { global: target.slug },
48
+ status: doc["_status"],
49
+ updatedAt: doc["updatedAt"],
50
+ publishBlockers
51
+ });
52
+ }
53
+ };
54
+ //#endregion
55
+ export { validateDocument };
@@ -0,0 +1,143 @@
1
+ import { CollectionConfig, CollectionSlug, GlobalSlug, PayloadRequest, TypedUser } from "payload";
2
+ import { z } from "zod";
3
+ import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
4
+ import { CallToolResult, ServerNotification, ServerRequest, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
5
+ //#region src/types.d.ts
6
+ declare module "payload" {
7
+ interface RequestContext {
8
+ mcpx?: McpxRequestContext;
9
+ }
10
+ interface RegisteredPlugins {
11
+ "@abinnovision/payloadcms-mcpx": McpxPluginOptions;
12
+ }
13
+ }
14
+ /**
15
+ * What an exposed collection offers to MCP clients. A key can only enable
16
+ * what the config exposes here.
17
+ */
18
+ interface McpxCollectionOptions {
19
+ /**
20
+ * Expose `describeSchema`, `findDocuments` and `getDocument`. Default `true`.
21
+ */
22
+ read?: boolean;
23
+ /**
24
+ * Expose `patchDocument`, `createDocument` and `validateDocument`. Default
25
+ * `false`. Requires `versions.drafts` unless `allowLiveWrites` is set.
26
+ */
27
+ write?: boolean;
28
+ /**
29
+ * Permit writes to a collection without drafts. Such writes land on the live
30
+ * document because there is no draft to land on. Default `false`.
31
+ */
32
+ allowLiveWrites?: boolean;
33
+ }
34
+ /**
35
+ * What an exposed global offers to MCP clients. Structurally the same as
36
+ * {@link McpxCollectionOptions}, kept separate because the tools it names
37
+ * differ: a global is a singleton, so neither `findDocuments` nor
38
+ * `createDocument` reaches one.
39
+ */
40
+ interface McpxGlobalOptions {
41
+ /** Expose `describeSchema` and `getDocument`. Default `true`. */
42
+ read?: boolean;
43
+ /**
44
+ * Expose `patchDocument` and `validateDocument`. Default `false`. Requires
45
+ * `versions.drafts` unless `allowLiveWrites` is set.
46
+ */
47
+ write?: boolean;
48
+ /**
49
+ * Permit writes to a global without drafts. Such writes land on the live
50
+ * document because there is no draft to land on. Default `false`.
51
+ */
52
+ allowLiveWrites?: boolean;
53
+ }
54
+ type McpxToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;
55
+ /**
56
+ * A custom tool. It is gated by its own checkbox on every API key and runs
57
+ * with `req.user` resolved from the key and `req.context.mcpx` set.
58
+ */
59
+ interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape> {
60
+ /** camelCase, unique, not one of the builtin tool names. */
61
+ name: string;
62
+ description: string;
63
+ inputSchema?: Shape;
64
+ annotations?: ToolAnnotations;
65
+ handler(ctx: {
66
+ args: z.infer<z.ZodObject<Shape>>;
67
+ req: PayloadRequest;
68
+ extra: McpxToolExtra;
69
+ }): CallToolResult | Promise<CallToolResult>;
70
+ }
71
+ /**
72
+ * Identity helper that infers the argument type of a custom tool's handler
73
+ * from its input schema.
74
+ */
75
+ declare const defineMcpxTool: <Shape extends z.ZodRawShape>(tool: McpxTool<Shape>) => McpxTool<Shape>;
76
+ /**
77
+ * Outcome of resolving an API key. `user` must carry `collection`.
78
+ */
79
+ interface McpxAuthResult {
80
+ user: TypedUser;
81
+ apiKeyId: number | string;
82
+ /** The `capabilities` group as stored on the key document. */
83
+ capabilities: unknown;
84
+ }
85
+ type McpxPluginOptions = {
86
+ /** Allow-list of collections. `true` is shorthand for `{ read: true }`. */
87
+ collections: Partial<Record<CollectionSlug, McpxCollectionOptions | true>>;
88
+ /** Allow-list of globals. `true` is shorthand for `{ read: true }`. */
89
+ globals?: Partial<Record<GlobalSlug, McpxGlobalOptions | true>>;
90
+ /** Collection the keys act as. Default `config.admin.user`, then `users`. */
91
+ userCollection?: CollectionSlug;
92
+ apiKeys?: {
93
+ /** Slug of the generated API key collection. Default `mcpx-api-keys`. */
94
+ slug?: string;
95
+ /**
96
+ * Add a "Connect a client" tab to saved keys, holding ready-to-paste MCP
97
+ * client config. Default `true`. The snippets contain the key in full.
98
+ */
99
+ setupGuide?: boolean;
100
+ /** Final override applied to the generated collection. */
101
+ overrideCollection?: (collection: CollectionConfig) => CollectionConfig;
102
+ };
103
+ endpoint?: {
104
+ /** Endpoint path below the API route. Default `/mcpx`. */
105
+ path?: string;
106
+ };
107
+ limits?: {
108
+ /** Upper bound for `findDocuments.limit`. Default 25. */
109
+ maxLimit?: number;
110
+ /** Upper bound for `depth` on reads. Default 1. */
111
+ maxDepth?: number;
112
+ };
113
+ tools?: McpxTool[];
114
+ auth?: {
115
+ /** Replace or wrap the default key resolution. Return `null` for 401. */
116
+ resolve?: (args: {
117
+ req: PayloadRequest;
118
+ resolveDefault: () => Promise<McpxAuthResult | null>;
119
+ }) => Promise<McpxAuthResult | null>;
120
+ };
121
+ serverInfo?: {
122
+ name?: string;
123
+ version?: string;
124
+ };
125
+ };
126
+ interface McpxCollectionCapabilities {
127
+ read: boolean;
128
+ write: boolean;
129
+ }
130
+ /**
131
+ * Capabilities in force for one request: plugin config AND key checkboxes.
132
+ */
133
+ interface McpxResolvedCapabilities {
134
+ collections: Record<string, McpxCollectionCapabilities>;
135
+ globals: Record<string, McpxCollectionCapabilities>;
136
+ tools: Record<string, boolean>;
137
+ }
138
+ interface McpxRequestContext {
139
+ apiKeyId: number | string;
140
+ capabilities: McpxResolvedCapabilities;
141
+ }
142
+ //#endregion
143
+ export { McpxAuthResult, McpxCollectionCapabilities, McpxCollectionOptions, McpxGlobalOptions, McpxPluginOptions, McpxRequestContext, McpxResolvedCapabilities, McpxTool, McpxToolExtra, defineMcpxTool };
package/dist/types.mjs ADDED
@@ -0,0 +1,7 @@
1
+ //#region src/types.ts
2
+ /**
3
+ * Identity helper that infers the argument type of a custom tool's handler
4
+ * from its input schema.
5
+ */ const defineMcpxTool = (tool) => tool;
6
+ //#endregion
7
+ export { defineMcpxTool };
@@ -0,0 +1,6 @@
1
+ //#region src/version.ts
2
+ /**
3
+ * Package version injected at build time; sources under Vitest report "dev".
4
+ */ const MCPX_VERSION = "0.0.0";
5
+ //#endregion
6
+ export { MCPX_VERSION };
@@ -0,0 +1,10 @@
1
+ import { CollectionConfig, PayloadRequest } from "payload";
2
+ //#region src/write/draft-guard.d.ts
3
+ /**
4
+ * Whether a request originated from the MCP endpoint. The endpoint stamps
5
+ * `req.context.mcpx`, which travels into every local API call made with the
6
+ * same `req`, including those made by custom tools.
7
+ */
8
+ declare const isMcpxRequest: (req: PayloadRequest) => boolean;
9
+ //#endregion
10
+ export { isMcpxRequest };