@abinnovision/payloadcms-mcpx 2.0.0-beta.9 → 2.0.0

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 (76) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +56 -36
  3. package/dist/admin/capability-matrix.d.mts +5 -3
  4. package/dist/admin/capability-matrix.mjs +374 -272
  5. package/dist/admin/capability-toggles.mjs +79 -42
  6. package/dist/admin/confirmations-panel.mjs +232 -0
  7. package/dist/admin/confirmations.d.mts +15 -0
  8. package/dist/admin/confirmations.mjs +71 -0
  9. package/dist/admin/endpoint-url.mjs +7 -0
  10. package/dist/admin/index.d.mts +2 -1
  11. package/dist/admin/index.mjs +2 -1
  12. package/dist/admin/setup-guide.d.mts +3 -3
  13. package/dist/admin/setup-guide.mjs +51 -33
  14. package/dist/api-keys/capability-matrix.d.mts +7 -9
  15. package/dist/api-keys/capability-matrix.mjs +15 -37
  16. package/dist/api-keys/collection.mjs +16 -18
  17. package/dist/api-keys/confirmation-view.mjs +7 -0
  18. package/dist/api-keys/fields.mjs +50 -57
  19. package/dist/auth/resolve.mjs +69 -40
  20. package/dist/capabilities.d.mts +2 -0
  21. package/dist/capabilities.mjs +64 -16
  22. package/dist/define-tool.d.mts +4 -4
  23. package/dist/define-tool.mjs +8 -1
  24. package/dist/endpoint/confirmations.mjs +124 -0
  25. package/dist/endpoint/handler.mjs +25 -27
  26. package/dist/endpoint/http.mjs +27 -0
  27. package/dist/endpoint/server.mjs +48 -29
  28. package/dist/endpoint/upload.mjs +104 -0
  29. package/dist/grants/confirmation.mjs +75 -0
  30. package/dist/grants/grant.mjs +137 -0
  31. package/dist/guards.mjs +8 -1
  32. package/dist/index.d.mts +2 -2
  33. package/dist/index.mjs +1 -1
  34. package/dist/options.mjs +68 -61
  35. package/dist/plugin.d.mts +3 -2
  36. package/dist/plugin.mjs +19 -4
  37. package/dist/request.mjs +5 -1
  38. package/dist/result.mjs +11 -1
  39. package/dist/schema/admin-hidden.mjs +4 -4
  40. package/dist/schema/describe.mjs +12 -18
  41. package/dist/schema/index.mjs +5 -6
  42. package/dist/schema/lexical-pointer.mjs +8 -8
  43. package/dist/schema/lexical.mjs +15 -13
  44. package/dist/schema/outline.mjs +4 -5
  45. package/dist/schema/path.mjs +4 -1
  46. package/dist/schema/pointer.mjs +11 -17
  47. package/dist/schema/shape.mjs +38 -50
  48. package/dist/schema/walk.mjs +57 -18
  49. package/dist/tools/builtin.mjs +7 -1
  50. package/dist/tools/create-document.mjs +38 -26
  51. package/dist/tools/delete-document.mjs +154 -0
  52. package/dist/tools/describe-schema.mjs +7 -5
  53. package/dist/tools/document.mjs +131 -45
  54. package/dist/tools/duplicate-document.mjs +50 -0
  55. package/dist/tools/find-documents.mjs +15 -20
  56. package/dist/tools/find-versions.mjs +11 -15
  57. package/dist/tools/get-document.mjs +31 -15
  58. package/dist/tools/links.mjs +53 -0
  59. package/dist/tools/list-capabilities.mjs +17 -7
  60. package/dist/tools/patch-document.mjs +57 -53
  61. package/dist/tools/publish-document.mjs +17 -29
  62. package/dist/tools/query-paths.mjs +1 -1
  63. package/dist/tools/run-confirmed.mjs +114 -0
  64. package/dist/tools/shared.mjs +53 -46
  65. package/dist/tools/validate-document.mjs +6 -8
  66. package/dist/tools/versions.mjs +11 -6
  67. package/dist/types.d.mts +158 -43
  68. package/dist/upload/file.mjs +177 -0
  69. package/dist/version.mjs +4 -2
  70. package/dist/write/draft-guard.mjs +35 -28
  71. package/dist/write/patch.mjs +4 -4
  72. package/dist/write/publish-intent.mjs +23 -11
  73. package/dist/write/row-ids.mjs +3 -1
  74. package/dist/write/write-data.mjs +18 -31
  75. package/package.json +14 -14
  76. package/dist/builtin-tool-names.mjs +0 -16
@@ -1,10 +1,10 @@
1
- import { canCreate, isLiveWrite } from "../capabilities.mjs";
1
+ import { acceptsFiles } from "../upload/file.mjs";
2
2
  import { z } from "zod";
3
3
  //#region src/tools/shared.ts
4
4
  /**
5
- * An out-of-scope slug fails schema validation before a handler runs, so a
5
+ * A value outside the list fails schema validation before a handler runs, so a
6
6
  * client only ever sees what its key may touch.
7
- */ const slugEnum = (slugs) => z.enum(slugs);
7
+ */ const stringEnum = (values) => z.enum(values);
8
8
  /**
9
9
  * Payload's id type follows the adapter, so both forms are handed on as read.
10
10
  */ const idSchema = z.union([z.string(), z.number()]);
@@ -14,62 +14,50 @@ const slugsOf = (scope, key) => ({
14
14
  });
15
15
  const slugsWhere = (scope, predicate, allowed) => {
16
16
  const pick = (entities, slugs) => entities.filter((entity) => slugs.includes(entity.slug) && predicate(entity)).map((entity) => entity.slug);
17
- return [...pick(scope.exposure.collections, allowed.collections), ...pick(scope.exposure.globals, allowed.globals)];
17
+ return {
18
+ collections: pick(scope.exposure.collections, allowed.collections),
19
+ globals: pick(scope.exposure.globals, allowed.globals)
20
+ };
18
21
  };
19
22
  /**
20
- * Slugs this key may write but never create in, because their documents are
21
- * files. Collection-only, since nothing creates a global either way.
22
- */ const patchOnlySlugs = (scope) => slugsWhere(scope, (entity) => !canCreate(entity), {
23
- collections: scope.collections.writable,
24
- globals: []
25
- });
23
+ * Collections this key may write but not create in: upload collections whose
24
+ * files MCP does not accept. Nothing creates a global either way.
25
+ */ const patchOnlySlugs = (scope) => {
26
+ const creatable = slugsFor(scope, "create").collections;
27
+ return scope.collections.writable.filter((slug) => !creatable.includes(slug));
28
+ };
26
29
  const widen = (branch) => branch;
27
30
  /**
28
31
  * The one list the shape helpers and {@link resolveEntity} both read.
29
32
  */ const slugsFor = (scope, operation) => {
30
33
  switch (operation) {
31
- case "create": return {
32
- collections: slugsWhere(scope, canCreate, {
33
- collections: scope.collections.writable,
34
- globals: []
35
- }),
34
+ case "create": return slugsWhere(scope, (entity) => entity.write && (!entity.isUpload || acceptsFiles(scope, entity.slug)), {
35
+ collections: scope.collections.writable,
36
+ globals: []
37
+ });
38
+ case "delete": return {
39
+ collections: scope.collections.deletable,
36
40
  globals: []
37
41
  };
38
42
  case "publish": return slugsOf(scope, "publishable");
39
43
  case "read": return slugsOf(scope, "readable");
40
- case "versions": return {
41
- collections: slugsWhere(scope, (entity) => entity.hasVersions, {
42
- collections: scope.collections.readable,
43
- globals: []
44
- }),
45
- globals: slugsWhere(scope, (entity) => entity.hasVersions, {
46
- collections: [],
47
- globals: scope.globals.readable
48
- })
49
- };
44
+ case "versions": return slugsWhere(scope, (entity) => entity.hasVersions, slugsOf(scope, "readable"));
50
45
  case "write": return slugsOf(scope, "writable");
51
46
  }
52
47
  };
53
48
  /**
54
- * The slugs of {@link slugsFor} `"versions"` that also have drafts. Only they
55
- * have `_status` on their versions, so only they can be queried by status.
56
- */ const draftVersionSlugs = (scope) => {
57
- const { collections, globals } = slugsFor(scope, "versions");
58
- return {
59
- collections: slugsWhere(scope, (entity) => entity.hasDrafts, {
60
- collections,
61
- globals: []
62
- }),
63
- globals: slugsWhere(scope, (entity) => entity.hasDrafts, {
64
- collections: [],
65
- globals
66
- })
67
- };
49
+ * Whether `operation` reaches any collection or global.
50
+ */ const reaches = (scope, operation) => {
51
+ const { collections, globals } = slugsFor(scope, operation);
52
+ return collections.length + globals.length > 0;
68
53
  };
69
54
  /**
70
55
  * Slugs reachable by `operation` whose writes go live, because they have no
71
56
  * draft to write.
72
- */ const liveWriteSlugs = (scope, operation) => slugsWhere(scope, isLiveWrite, slugsFor(scope, operation));
57
+ */ const liveWriteSlugs = (scope, operation) => {
58
+ const { collections, globals } = slugsWhere(scope, (entity) => entity.liveWrite, slugsFor(scope, operation));
59
+ return [...collections, ...globals];
60
+ };
73
61
  /**
74
62
  * Stated on the tools that write, since a client calling them must know before
75
63
  * the call which writes are public.
@@ -84,11 +72,11 @@ const widen = (branch) => branch;
84
72
  * `globalRule` states that rule on `global` for a tool without an `id`.
85
73
  */ const entityShape = (scope, operation, globalRule) => {
86
74
  const { collections, globals } = slugsFor(scope, operation);
87
- if (globals.length === 0) return widen({ collection: slugEnum(collections) });
88
- if (collections.length === 0) return widen({ global: slugEnum(globals) });
89
- const global = slugEnum(globals).optional();
75
+ if (globals.length === 0) return widen({ collection: stringEnum(collections) });
76
+ if (collections.length === 0) return widen({ global: stringEnum(globals) });
77
+ const global = stringEnum(globals).optional();
90
78
  return widen({
91
- collection: slugEnum(collections).optional(),
79
+ collection: stringEnum(collections).optional(),
92
80
  global: globalRule === void 0 ? global : global.describe(globalRule)
93
81
  });
94
82
  };
@@ -106,7 +94,7 @@ const widen = (branch) => branch;
106
94
  */ const READ_LOCALE_DESCRIPTION = "Default: the default locale. A value missing in this locale is shown from the default locale.";
107
95
  const localeShape = (scope, options) => {
108
96
  if (!scope.localization) return widen({});
109
- const enumerated = z.enum(scope.localization.locales);
97
+ const enumerated = stringEnum(scope.localization.locales);
110
98
  const locale = options.required ? enumerated : enumerated.optional();
111
99
  return widen({ locale: options.description === void 0 ? locale : locale.describe(options.description) });
112
100
  };
@@ -115,6 +103,25 @@ const localeShape = (scope, options) => {
115
103
  * it can write back, and populating a relation costs a query.
116
104
  */ const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.limits.maxDepth).optional().describe("Default 0. A relationship into a collection this key cannot read stays an id.") });
117
105
  /**
106
+ * For the writes that refuse a document changed since the client read it.
107
+ */ const expectedUpdatedAtShape = { expectedUpdatedAt: z.string().optional().describe("\"updatedAt\" from your last read. Refused if the document changed since.") };
108
+ /**
109
+ * `limit` is bounded by the configured ceiling, so the client sees it instead
110
+ * of being clamped silently.
111
+ */ const pageShape = (scope) => ({
112
+ limit: z.number().int().min(1).max(scope.limits.maxLimit).optional().describe("Default 10."),
113
+ page: z.number().int().min(1).optional()
114
+ });
115
+ /**
116
+ * The paging fields of a result.
117
+ */ const pageFields = (result) => ({
118
+ totalDocs: result.totalDocs,
119
+ page: result.page,
120
+ totalPages: result.totalPages,
121
+ limit: result.limit,
122
+ hasNextPage: result.hasNextPage
123
+ });
124
+ /**
118
125
  * The locale to operate on: the explicit argument, else the request's, else
119
126
  * the default. `undefined` when localization is off.
120
127
  */ const localeOf = (scope, locale) => {
@@ -124,4 +131,4 @@ const localeShape = (scope, options) => {
124
131
  return requested && locales.includes(requested) ? requested : defaultLocale;
125
132
  };
126
133
  //#endregion
127
- export { READ_LOCALE_DESCRIPTION, depthShape, draftVersionSlugs, entityShape, idSchema, idShape, liveWriteSentence, liveWriteSlugs, localeOf, localeShape, patchOnlySlugs, slugEnum, slugsFor, widen };
134
+ export { READ_LOCALE_DESCRIPTION, depthShape, entityShape, expectedUpdatedAtShape, idSchema, idShape, liveWriteSentence, liveWriteSlugs, localeOf, localeShape, pageFields, pageShape, patchOnlySlugs, reaches, slugsFor, slugsWhere, stringEnum, widen };
@@ -1,11 +1,11 @@
1
1
  import { defineMcpxTool } from "../define-tool.mjs";
2
2
  import { jsonResult } from "../result.mjs";
3
- import { entityShape, idShape, localeOf, localeShape } from "./shared.mjs";
4
3
  import { collectPublishBlockers } from "../write/publish-blockers.mjs";
5
- import { collectLocaleBlockers, identityOf, readDraft, resolveDocument } from "./document.mjs";
4
+ import { entityShape, idShape, localeOf, localeShape, reaches } from "./shared.mjs";
5
+ import { collectLocaleBlockers, documentSummary, readDraft, resolveDocument } from "./document.mjs";
6
6
  /**
7
- * Gated on write rather than read, because publish blockers only mean something
8
- * to a caller who can act on them.
7
+ * Gated on write, which implies read, because publish blockers only mean
8
+ * something to a caller who can act on them.
9
9
  *
10
10
  * It reads the document twice on purpose: once under the key's own access to
11
11
  * refuse a caller who may not see it, then privileged, so the check covers every
@@ -15,7 +15,7 @@ import { collectLocaleBlockers, identityOf, readDraft, resolveDocument } from ".
15
15
  name: "validateDocument",
16
16
  description: `Lists what still blocks publishing one document or global, without saving. Checks one locale when "locale" is given, otherwise every locale. Returns "publishBlockers", each with a pointer and a message, and a "locale" when every locale was checked. An empty list means it can be published, unless "publishBlockersUnavailable" is true: then the check itself failed. The check runs the field hooks a save runs.`,
17
17
  annotations: { openWorldHint: false },
18
- isEnabled: (scope) => scope.collections.writable.length + scope.globals.writable.length > 0,
18
+ isEnabled: (scope) => reaches(scope, "write"),
19
19
  inputSchema: (scope) => ({
20
20
  ...entityShape(scope, "write"),
21
21
  ...idShape(scope, "write"),
@@ -39,9 +39,7 @@ import { collectLocaleBlockers, identityOf, readDraft, resolveDocument } from ".
39
39
  entity: target
40
40
  });
41
41
  return jsonResult({
42
- ...identityOf(target, doc["id"]),
43
- status: doc["_status"],
44
- updatedAt: doc["updatedAt"],
42
+ ...documentSummary(target, doc),
45
43
  publishBlockers: validation.blockers,
46
44
  ...validation.unavailable ? { publishBlockersUnavailable: true } : {}
47
45
  });
@@ -1,3 +1,4 @@
1
+ import { definedProps, isPlainObject } from "../guards.mjs";
1
2
  import { stripAdminHidden } from "../schema/admin-hidden.mjs";
2
3
  import "../schema/index.mjs";
3
4
  import { mcpxReadRequest } from "./read-request.mjs";
@@ -21,8 +22,10 @@ const NEWEST_FIRST = ["-updatedAt", "-id"];
21
22
  draft: options.draft,
22
23
  overrideAccess: false,
23
24
  req: mcpxReadRequest(scope),
24
- ...options.select === void 0 ? {} : { select: options.select },
25
- ...read.locale === void 0 ? {} : { locale: read.locale }
25
+ ...definedProps({
26
+ select: options.select,
27
+ locale: read.locale
28
+ })
26
29
  };
27
30
  const doc = await (read.target.kind === "collection" ? scope.req.payload.findByID({
28
31
  ...shared,
@@ -51,7 +54,7 @@ const assertReadable = async (scope, read) => {
51
54
  * `parent` comes back as a raw id or, populated, as the document itself.
52
55
  */ const isVersionOf = (version, id) => {
53
56
  const parent = version["parent"];
54
- const parentId = typeof parent === "object" && parent !== null ? parent["id"] : parent;
57
+ const parentId = isPlainObject(parent) ? parent["id"] : parent;
55
58
  return (typeof parentId === "string" || typeof parentId === "number") && String(parentId) === String(id);
56
59
  };
57
60
  /**
@@ -68,7 +71,7 @@ const assertReadable = async (scope, read) => {
68
71
  disableErrors: true,
69
72
  overrideAccess: false,
70
73
  req: mcpxReadRequest(scope),
71
- ...read.locale === void 0 ? {} : { locale: read.locale }
74
+ ...definedProps({ locale: read.locale })
72
75
  };
73
76
  const version = await (read.target.kind === "collection" ? payload.findVersionByID({
74
77
  ...shared,
@@ -96,8 +99,10 @@ const assertReadable = async (scope, read) => {
96
99
  overrideAccess: false,
97
100
  req: mcpxReadRequest(scope),
98
101
  where,
99
- ...options.page === void 0 ? {} : { page: options.page },
100
- ...read.locale === void 0 ? {} : { locale: read.locale }
102
+ ...definedProps({
103
+ page: options.page,
104
+ locale: read.locale
105
+ })
101
106
  };
102
107
  return await (read.target.kind === "collection" ? scope.req.payload.findVersions({
103
108
  ...shared,
package/dist/types.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DocumentId } from "./entity.mjs";
2
- import { CollectionConfig, CollectionSlug, GlobalSlug, PayloadRequest, TypedUser } from "payload";
3
2
  import { z } from "zod";
3
+ import { CollectionConfig, CollectionSlug, GlobalSlug, PayloadRequest, TypedUser } from "payload";
4
4
  import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
5
5
  import { CallToolResult, ServerNotification, ServerRequest, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
6
6
  //#region src/types.d.ts
@@ -12,17 +12,6 @@ declare module "payload" {
12
12
  "@abinnovision/payloadcms-mcpx": McpxPluginOptions;
13
13
  }
14
14
  }
15
- /**
16
- * How far an exposed entity lets MCP writes reach.
17
- *
18
- * - `false`: no write tool touches it.
19
- * - `"draft"`: writes land as drafts and nothing MCP does changes what the
20
- * public sees. Requires `versions.drafts`.
21
- * - `"live"`: MCP may change live content. On an entity with drafts that
22
- * exposes `publishDocument`. On one without, there is no draft, so the write
23
- * itself is permitted and lands live.
24
- */
25
- export type McpxWriteMode = "draft" | "live" | false;
26
15
  /**
27
16
  * What an entity exposes. The config names the entities that are reachable and
28
17
  * only takes capabilities away; a key's checkboxes decide per key. `true` is
@@ -34,9 +23,10 @@ export interface McpxCollectionOptions {
34
23
  */
35
24
  read?: boolean;
36
25
  /**
37
- * Expose `patchDocument`, `validateDocument` and, unless this is an upload
38
- * collection, `createDocument`. Without drafts a write changes live
39
- * content. Default `true`.
26
+ * Expose `patchDocument`, `validateDocument`, `createDocument` and
27
+ * `duplicateDocument`. An upload collection is created in only where MCP
28
+ * accepts its files, and is never duplicated. Without drafts
29
+ * a write changes live content. Needs `read`. Default `true` while `read` is.
40
30
  */
41
31
  write?: boolean;
42
32
  /**
@@ -45,20 +35,28 @@ export interface McpxCollectionOptions {
45
35
  * `false` keeps writes as drafts and is refused where there are none.
46
36
  */
47
37
  publish?: boolean;
38
+ /**
39
+ * Expose `deleteDocument`, which deletes one document once the key's user
40
+ * approved the call in the admin panel. On a collection with `trash` it
41
+ * moves the document to trash. `"unattended"` also lets a key set to it move
42
+ * documents to trash without approval; it needs `trash`. Needs `read`.
43
+ * Default `false`.
44
+ */
45
+ delete?: boolean | "unattended";
48
46
  }
49
47
  /**
50
- * The same options. A singleton, so neither `findDocuments` nor
51
- * `createDocument` reaches one.
48
+ * The same options without `delete`. A singleton, so neither `findDocuments`,
49
+ * `createDocument` nor `deleteDocument` reaches one.
52
50
  */
53
- export type McpxGlobalOptions = McpxCollectionOptions;
51
+ export type McpxGlobalOptions = Omit<McpxCollectionOptions, "delete">;
54
52
  export type McpxToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;
55
53
  /**
56
- * What the config exposes, before an API key's checkboxes narrow it.
54
+ * What the config exposes, before an API key's checkboxes narrow it. The
55
+ * operations are the config's side of {@link McpxEntityCapabilities}: `write`
56
+ * covers draft and live writes, and `publish` is only true where drafts exist.
57
57
  */
58
- export interface McpxExposedEntity {
58
+ export interface McpxExposedEntity extends McpxEntityCapabilities {
59
59
  slug: string;
60
- read: boolean;
61
- write: McpxWriteMode;
62
60
  hasDrafts: boolean;
63
61
  /**
64
62
  * The entity has Payload `versions`, with or without drafts, and is
@@ -66,7 +64,11 @@ export interface McpxExposedEntity {
66
64
  */
67
65
  hasVersions: boolean;
68
66
  /**
69
- * An upload document is a file, and no tool here can supply one.
67
+ * Writes go live immediately, as the entity keeps no drafts.
68
+ */
69
+ liveWrite: boolean;
70
+ /**
71
+ * An upload document is a file, which only an upload grant can supply.
70
72
  */
71
73
  isUpload: boolean;
72
74
  /**
@@ -75,12 +77,13 @@ export interface McpxExposedEntity {
75
77
  fieldName: string;
76
78
  }
77
79
  /**
78
- * The slugs a key may read, write and publish.
80
+ * The slugs a key may read, write, publish and delete.
79
81
  */
80
82
  export interface McpxScopeSlugs {
81
83
  readable: string[];
82
84
  writable: string[];
83
85
  publishable: string[];
86
+ deletable: string[];
84
87
  }
85
88
  /**
86
89
  * What a tool knows about the current request.
@@ -101,17 +104,27 @@ export interface McpxToolScope {
101
104
  maxLimit: number;
102
105
  maxDepth: number;
103
106
  };
107
+ /**
108
+ * Whether files can be uploaded at all: the KV is database-backed and no
109
+ * custom `auth.resolve` is configured. Confirmations need the same.
110
+ */
111
+ uploads: boolean;
112
+ /**
113
+ * `null` when `diagnostics` is off.
114
+ */
115
+ diagnostics: null | {
116
+ name: string;
117
+ version: string;
118
+ };
104
119
  exposure: {
105
120
  collections: McpxExposedEntity[];
106
121
  globals: McpxExposedEntity[];
107
122
  };
108
123
  }
109
124
  /**
110
- * A tool, builtin or custom. Runs with `req.user` resolved from the key and
111
- * `req.context.mcpx` set. `Args` only needs stating when `inputSchema` is built
112
- * per request, leaving no static shape to infer from.
125
+ * What every tool declares, apart from `confirm` and the handler.
113
126
  */
114
- export interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.infer<z.ZodObject<Shape>>> {
127
+ interface McpxToolBase<Shape extends z.ZodRawShape> {
115
128
  /**
116
129
  * camelCase, unique, not one of the builtin tool names.
117
130
  */
@@ -133,15 +146,103 @@ export interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.
133
146
  * instead of stripped.
134
147
  */
135
148
  inputSchema?: Shape | ((scope: McpxToolScope) => z.ZodRawShape);
136
- handler(ctx: {
137
- args: Args;
138
- scope: McpxToolScope;
139
- /**
140
- * Shorthand for `scope.req`.
141
- */
142
- req: PayloadRequest;
143
- extra: McpxToolExtra;
144
- }): CallToolResult | Promise<CallToolResult>;
149
+ }
150
+ /**
151
+ * What a handler receives.
152
+ */
153
+ export interface McpxToolContext<Args> {
154
+ args: Args;
155
+ scope: McpxToolScope;
156
+ /**
157
+ * Shorthand for `scope.req`.
158
+ */
159
+ req: PayloadRequest;
160
+ extra: McpxToolExtra;
161
+ }
162
+ /**
163
+ * Handed to the handler of a tool with `confirm`. Internal for now.
164
+ */
165
+ export interface McpxConfirmation {
166
+ /**
167
+ * `true` when `runConfirmed` runs an approved call.
168
+ */
169
+ confirmed: boolean;
170
+ /**
171
+ * Stores the call with `args` for the key's user to approve and returns
172
+ * the result that hands its id and URL to the client. Throws in a
173
+ * confirmed run.
174
+ */
175
+ request: (args: Record<string, unknown>) => Promise<CallToolResult>;
176
+ }
177
+ /**
178
+ * A tool as `defineMcpxTool` takes it. `Confirm` is inferred from `confirm`,
179
+ * so only a tool that sets it gets `confirmation` in its handler.
180
+ */
181
+ export type McpxToolDefinition<Shape extends z.ZodRawShape, Args, Confirm extends McpxToolConfirm | undefined> = McpxToolBase<Shape> & {
182
+ /**
183
+ * Internal: only builtin tools are confirmable for now. The handler gets
184
+ * `confirmation`: a call outside a confirmed run stores itself through
185
+ * `confirmation.request`, the key's user approves it in the admin panel,
186
+ * and `runConfirmed` runs the handler again with `confirmation.confirmed`
187
+ * set.
188
+ */
189
+ confirm?: Confirm;
190
+ handler(ctx: McpxToolContext<Args> & (Confirm extends McpxToolConfirm ? {
191
+ confirmation: McpxConfirmation;
192
+ } : unknown)): CallToolResult | Promise<CallToolResult>;
193
+ };
194
+ /**
195
+ * A tool, builtin or custom. Runs with `req.user` resolved from the key and
196
+ * `req.context.mcpx` set. `Args` only needs stating when `inputSchema` is built
197
+ * per request, leaving no static shape to infer from.
198
+ */
199
+ export type McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.infer<z.ZodObject<Shape>>> = McpxToolDefinition<Shape, Args, undefined> | (McpxToolDefinition<Shape, Args, McpxToolConfirm> & {
200
+ confirm: McpxToolConfirm;
201
+ });
202
+ /**
203
+ * How the admin panel shows one stored call of a confirmable tool.
204
+ */
205
+ export interface McpxConfirmationSummary {
206
+ /**
207
+ * What the call acts on, such as a collection's label.
208
+ */
209
+ label: string;
210
+ /**
211
+ * `undefined` when the target cannot be read.
212
+ */
213
+ title?: string;
214
+ /**
215
+ * The target's `_status`, where it has drafts.
216
+ */
217
+ status?: string;
218
+ id?: string;
219
+ /**
220
+ * Admin path that opens the target.
221
+ */
222
+ href?: string;
223
+ /**
224
+ * The call cannot be undone.
225
+ */
226
+ permanent: boolean;
227
+ /**
228
+ * Written by the client, so shown as such.
229
+ */
230
+ reason?: string;
231
+ }
232
+ /**
233
+ * Internal for now; see {@link McpxTool.confirm}.
234
+ */
235
+ export interface McpxToolConfirm {
236
+ /**
237
+ * Heading of the tool's group in the admin panel.
238
+ */
239
+ label: string;
240
+ /**
241
+ * Read at view time with the access of the key's user.
242
+ */
243
+ summarize(call: {
244
+ args: Record<string, unknown>;
245
+ }, req: PayloadRequest): Promise<McpxConfirmationSummary>;
145
246
  }
146
247
  /**
147
248
  * Argument type erased, so a registry can hold tools of differing shapes.
@@ -183,8 +284,9 @@ export type McpxPluginOptions = {
183
284
  */
184
285
  slug?: string;
185
286
  /**
186
- * Add a "Connect a client" tab to saved keys, holding ready-to-paste MCP
187
- * client config. Default `true`. The snippets contain the key in full.
287
+ * Add a "Connect a client" button to saved keys, opening a drawer with
288
+ * ready-to-paste MCP client config. Default `true`. The snippets contain
289
+ * the key in full.
188
290
  */
189
291
  setupGuide?: boolean;
190
292
  /**
@@ -224,24 +326,37 @@ export type McpxPluginOptions = {
224
326
  name?: string;
225
327
  version?: string;
226
328
  };
329
+ /**
330
+ * Expose the server version plus repository and issue links to clients. Default `true`.
331
+ */
332
+ diagnostics?: boolean;
227
333
  };
228
334
  /**
229
- * What a key may do with one entity. Globals reuse this shape.
335
+ * One entity's operations, as the config exposes them or as a key may use
336
+ * them. Globals reuse this shape.
230
337
  */
231
- export interface McpxCollectionCapabilities {
338
+ export interface McpxEntityCapabilities {
232
339
  read: boolean;
233
340
  write: boolean;
234
341
  /**
235
342
  * Only ever true where the config lets writes publish and drafts exist.
236
343
  */
237
344
  publish: boolean;
345
+ /**
346
+ * Always `false` on a global.
347
+ */
348
+ delete: boolean;
349
+ /**
350
+ * Moves to trash without approval. Only ever true where `delete` is.
351
+ */
352
+ deleteUnattended: boolean;
238
353
  }
239
354
  /**
240
355
  * In force for one request: the plugin config and the key checkboxes together.
241
356
  */
242
357
  export interface McpxResolvedCapabilities {
243
- collections: Record<string, McpxCollectionCapabilities>;
244
- globals: Record<string, McpxCollectionCapabilities>;
358
+ collections: Record<string, McpxEntityCapabilities>;
359
+ globals: Record<string, McpxEntityCapabilities>;
245
360
  tools: Record<string, boolean>;
246
361
  }
247
362
  /**