payload-mcp-toolkit 0.3.4 → 0.7.4

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 (116) hide show
  1. package/README.md +253 -151
  2. package/dist/api-keys.d.ts +46 -0
  3. package/dist/api-keys.js +308 -0
  4. package/dist/api-keys.js.map +1 -0
  5. package/dist/auth-strategy.d.ts +96 -0
  6. package/dist/auth-strategy.js +261 -0
  7. package/dist/auth-strategy.js.map +1 -0
  8. package/dist/components/CollectionScopesMatrix.d.ts +8 -0
  9. package/dist/components/CollectionScopesMatrix.js +32 -0
  10. package/dist/components/CollectionScopesMatrix.js.map +1 -0
  11. package/dist/components/GlobalScopesMatrix.d.ts +8 -0
  12. package/dist/components/GlobalScopesMatrix.js +28 -0
  13. package/dist/components/GlobalScopesMatrix.js.map +1 -0
  14. package/dist/components/ScopesTable.d.ts +19 -0
  15. package/dist/components/ScopesTable.js +285 -0
  16. package/dist/components/ScopesTable.js.map +1 -0
  17. package/dist/components/index.d.ts +2 -0
  18. package/dist/components/index.js +4 -0
  19. package/dist/components/index.js.map +1 -0
  20. package/dist/conflict-detection.d.ts +13 -0
  21. package/dist/conflict-detection.js +41 -0
  22. package/dist/conflict-detection.js.map +1 -0
  23. package/dist/draft-workflow.d.ts +46 -48
  24. package/dist/draft-workflow.js +53 -135
  25. package/dist/draft-workflow.js.map +1 -1
  26. package/dist/endpoint.d.ts +35 -0
  27. package/dist/endpoint.js +105 -0
  28. package/dist/endpoint.js.map +1 -0
  29. package/dist/hash.d.ts +21 -0
  30. package/dist/hash.js +36 -0
  31. package/dist/hash.js.map +1 -0
  32. package/dist/index.d.ts +9 -9
  33. package/dist/index.js +167 -69
  34. package/dist/index.js.map +1 -1
  35. package/dist/introspection.d.ts +17 -3
  36. package/dist/introspection.js +95 -36
  37. package/dist/introspection.js.map +1 -1
  38. package/dist/prompts.js +5 -5
  39. package/dist/prompts.js.map +1 -1
  40. package/dist/registry.d.ts +50 -0
  41. package/dist/registry.js +169 -0
  42. package/dist/registry.js.map +1 -0
  43. package/dist/resources.d.ts +5 -3
  44. package/dist/resources.js +23 -11
  45. package/dist/resources.js.map +1 -1
  46. package/dist/scope/audit-log.d.ts +18 -0
  47. package/dist/scope/audit-log.js +50 -0
  48. package/dist/scope/audit-log.js.map +1 -0
  49. package/dist/scope/policy.d.ts +73 -0
  50. package/dist/scope/policy.js +218 -0
  51. package/dist/scope/policy.js.map +1 -0
  52. package/dist/tools/_helpers.d.ts +62 -1
  53. package/dist/tools/_helpers.js +181 -0
  54. package/dist/tools/_helpers.js.map +1 -1
  55. package/dist/tools/_layout-helpers.d.ts +43 -0
  56. package/dist/tools/_layout-helpers.js +159 -0
  57. package/dist/tools/_layout-helpers.js.map +1 -0
  58. package/dist/tools/create-document.d.ts +5 -5
  59. package/dist/tools/create-document.js +25 -21
  60. package/dist/tools/create-document.js.map +1 -1
  61. package/dist/tools/delete-document.d.ts +25 -0
  62. package/dist/tools/delete-document.js +49 -0
  63. package/dist/tools/delete-document.js.map +1 -0
  64. package/dist/tools/find-document.d.ts +33 -0
  65. package/dist/tools/find-document.js +97 -0
  66. package/dist/tools/find-document.js.map +1 -0
  67. package/dist/tools/find-global.d.ts +26 -0
  68. package/dist/tools/find-global.js +122 -0
  69. package/dist/tools/find-global.js.map +1 -0
  70. package/dist/tools/global-versions.d.ts +39 -0
  71. package/dist/tools/global-versions.js +132 -0
  72. package/dist/tools/global-versions.js.map +1 -0
  73. package/dist/tools/patch-global-layout.d.ts +31 -0
  74. package/dist/tools/patch-global-layout.js +127 -0
  75. package/dist/tools/patch-global-layout.js.map +1 -0
  76. package/dist/tools/patch-layout.d.ts +5 -8
  77. package/dist/tools/patch-layout.js +18 -100
  78. package/dist/tools/patch-layout.js.map +1 -1
  79. package/dist/tools/publish-draft.d.ts +5 -4
  80. package/dist/tools/publish-draft.js +39 -2
  81. package/dist/tools/publish-draft.js.map +1 -1
  82. package/dist/tools/publish-global-draft.d.ts +20 -0
  83. package/dist/tools/publish-global-draft.js +79 -0
  84. package/dist/tools/publish-global-draft.js.map +1 -0
  85. package/dist/tools/resolve-reference.d.ts +5 -4
  86. package/dist/tools/resolve-reference.js +4 -0
  87. package/dist/tools/resolve-reference.js.map +1 -1
  88. package/dist/tools/safe-delete.d.ts +5 -5
  89. package/dist/tools/safe-delete.js +20 -15
  90. package/dist/tools/safe-delete.js.map +1 -1
  91. package/dist/tools/schedule-publish.d.ts +5 -5
  92. package/dist/tools/schedule-publish.js +23 -19
  93. package/dist/tools/schedule-publish.js.map +1 -1
  94. package/dist/tools/search-content.d.ts +5 -9
  95. package/dist/tools/search-content.js +16 -12
  96. package/dist/tools/search-content.js.map +1 -1
  97. package/dist/tools/update-document.d.ts +5 -5
  98. package/dist/tools/update-document.js +10 -5
  99. package/dist/tools/update-document.js.map +1 -1
  100. package/dist/tools/update-global.d.ts +27 -0
  101. package/dist/tools/update-global.js +72 -0
  102. package/dist/tools/update-global.js.map +1 -0
  103. package/dist/tools/upload-media.d.ts +5 -4
  104. package/dist/tools/upload-media.js +6 -1
  105. package/dist/tools/upload-media.js.map +1 -1
  106. package/dist/tools/versions.d.ts +10 -9
  107. package/dist/tools/versions.js +15 -7
  108. package/dist/tools/versions.js.map +1 -1
  109. package/dist/types.d.ts +56 -3
  110. package/dist/types.js +13 -6
  111. package/dist/types.js.map +1 -1
  112. package/package.json +39 -18
  113. package/dist/__tests__/introspection.test.js +0 -459
  114. package/dist/__tests__/introspection.test.js.map +0 -1
  115. package/dist/__tests__/url-validator.test.js +0 -326
  116. package/dist/__tests__/url-validator.test.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/scope/audit-log.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\r\n\r\n// ─── Audit logging helpers ───────────────────────────────────────────\r\n\r\nconst MAX_LOGGED_STRING = 200\r\n\r\n/**\r\n * Returns the top-level keys of a JSON-string `data` arg, sanitized.\r\n * Per the Codex post-planning finding: logging key names (not values) lets us\r\n * later analyze whether the prose-only input shape is causing AI mistakes.\r\n */\r\nexport function extractDataKeys(args: Record<string, unknown>): string[] | undefined {\r\n const data = args.data\r\n if (typeof data !== 'string') return undefined\r\n try {\r\n const parsed = JSON.parse(data) as unknown\r\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\r\n return Object.keys(parsed as Record<string, unknown>)\r\n }\r\n } catch {\r\n return undefined\r\n }\r\n return undefined\r\n}\r\n\r\nexport function summariseArgs(args: Record<string, unknown>): Record<string, unknown> {\r\n const out: Record<string, unknown> = {}\r\n for (const [k, v] of Object.entries(args)) {\r\n if (typeof v === 'string' && v.length > MAX_LOGGED_STRING) {\r\n out[k] = `<truncated:${v.length}>`\r\n } else {\r\n out[k] = v\r\n }\r\n }\r\n return out\r\n}\r\n\r\nexport function getRequestId(req: PayloadRequest): string | undefined {\r\n const headers = req.headers as Headers | undefined\r\n return headers?.get?.('x-request-id') ?? undefined\r\n}\r\n\r\ntype LoggerLike = Partial<Record<'info' | 'warn' | 'error', (...args: unknown[]) => unknown>>\r\n\r\n/**\r\n * Returns a logger-invoker that swallows transport failures. Audit-log\r\n * writes must never break the tool dispatch path — a throwing logger\r\n * transport (closed stream during HMR, a custom pino dest) would otherwise\r\n * flip a success to isError or mask the real tool error.\r\n */\r\nexport function makeSafeLog(logger: LoggerLike | undefined) {\r\n return (\r\n level: 'info' | 'warn' | 'error',\r\n payload: Record<string, unknown>,\r\n message: string,\r\n ) => {\r\n try {\r\n logger?.[level]?.(payload, message)\r\n } catch {\r\n // Logger transport failure must not break dispatch.\r\n }\r\n }\r\n}\r\n"],"names":["MAX_LOGGED_STRING","extractDataKeys","args","data","undefined","parsed","JSON","parse","Array","isArray","Object","keys","summariseArgs","out","k","v","entries","length","getRequestId","req","headers","get","makeSafeLog","logger","level","payload","message"],"mappings":"AAEA,wEAAwE;AAExE,MAAMA,oBAAoB;AAE1B;;;;CAIC,GACD,OAAO,SAASC,gBAAgBC,IAA6B;IAC3D,MAAMC,OAAOD,KAAKC,IAAI;IACtB,IAAI,OAAOA,SAAS,UAAU,OAAOC;IACrC,IAAI;QACF,MAAMC,SAASC,KAAKC,KAAK,CAACJ;QAC1B,IAAIE,UAAU,OAAOA,WAAW,YAAY,CAACG,MAAMC,OAAO,CAACJ,SAAS;YAClE,OAAOK,OAAOC,IAAI,CAACN;QACrB;IACF,EAAE,OAAM;QACN,OAAOD;IACT;IACA,OAAOA;AACT;AAEA,OAAO,SAASQ,cAAcV,IAA6B;IACzD,MAAMW,MAA+B,CAAC;IACtC,KAAK,MAAM,CAACC,GAAGC,EAAE,IAAIL,OAAOM,OAAO,CAACd,MAAO;QACzC,IAAI,OAAOa,MAAM,YAAYA,EAAEE,MAAM,GAAGjB,mBAAmB;YACzDa,GAAG,CAACC,EAAE,GAAG,CAAC,WAAW,EAAEC,EAAEE,MAAM,CAAC,CAAC,CAAC;QACpC,OAAO;YACLJ,GAAG,CAACC,EAAE,GAAGC;QACX;IACF;IACA,OAAOF;AACT;AAEA,OAAO,SAASK,aAAaC,GAAmB;IAC9C,MAAMC,UAAUD,IAAIC,OAAO;IAC3B,OAAOA,SAASC,MAAM,mBAAmBjB;AAC3C;AAIA;;;;;CAKC,GACD,OAAO,SAASkB,YAAYC,MAA8B;IACxD,OAAO,CACLC,OACAC,SACAC;QAEA,IAAI;YACFH,QAAQ,CAACC,MAAM,GAAGC,SAASC;QAC7B,EAAE,OAAM;QACN,oDAAoD;QACtD;IACF;AACF"}
@@ -0,0 +1,73 @@
1
+ import type { CollectionAction, GlobalAction, KeyScopes, ScopePreset } from '../types';
2
+ export type ResourceKind = 'collection' | 'global' | 'account';
3
+ /**
4
+ * Discriminated routing tag attached to every tool factory output.
5
+ *
6
+ * Collocates the scope-routing decision with the tool definition itself —
7
+ * the registry derives the collection/global/account lookups from `tools`
8
+ * at boot. Adding a new tool can no longer drift the routing maps out of
9
+ * sync because TS requires `routing` on every factory return.
10
+ */
11
+ export type ToolRouting = {
12
+ kind: 'collection';
13
+ action: CollectionAction;
14
+ } | {
15
+ kind: 'global';
16
+ action: GlobalAction;
17
+ } | {
18
+ kind: 'account';
19
+ action: CollectionAction;
20
+ };
21
+ /**
22
+ * Minimal "routable tool" interface used by the policy module. The full
23
+ * `ToolFactoryOutput` shape (handler, parameters, description) is irrelevant
24
+ * here; we only need `name` + `routing` to build the lookup tables.
25
+ */
26
+ export interface RoutableTool {
27
+ name: string;
28
+ routing: ToolRouting;
29
+ }
30
+ export declare const PRESET_ACTIONS: Record<ScopePreset, CollectionAction[]>;
31
+ /**
32
+ * Asymmetric per-preset action map for globals. `editor` is intentionally
33
+ * read-only on globals — a single bad write on a singleton broadcasts
34
+ * site-wide with no per-document containment. Operators who want global
35
+ * writes promote the key to `admin` or use a Custom key with explicit
36
+ * `globalScopes`. README and CHANGELOG call out the asymmetry.
37
+ */
38
+ export declare const PRESET_GLOBAL_ACTIONS: Record<ScopePreset, GlobalAction[]>;
39
+ export declare const PRESET_TOOL_DENY: Record<ScopePreset, string[]>;
40
+ export interface ScopeDecision {
41
+ allowed: boolean;
42
+ reason?: string;
43
+ }
44
+ export interface RoutingTables {
45
+ collectionToolAction: ReadonlyMap<string, CollectionAction>;
46
+ globalToolAction: ReadonlyMap<string, GlobalAction>;
47
+ accountToolAction: ReadonlyMap<string, CollectionAction>;
48
+ toolKind: ReadonlyMap<string, ResourceKind>;
49
+ }
50
+ export declare function buildRoutingTables(tools: RoutableTool[]): RoutingTables;
51
+ export type ScopeChecker = (scopes: KeyScopes | null | undefined, toolName: string, resource: string | undefined) => ScopeDecision;
52
+ /**
53
+ * Build a scope checker bound to a concrete tool list. The checker is a pure
54
+ * function over (scopes, toolName, resource) — the routing tables are closed
55
+ * over once at construction time.
56
+ *
57
+ * Fail-closed semantics:
58
+ * - Null/undefined scopes grant full access (back-compat).
59
+ * - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*
60
+ * for that resource kind — unlisted resources are denied.
61
+ * - When a tool resolves to a collection or global kind but the corresponding
62
+ * scope map is undefined and `scopes.preset` is undefined, the call is
63
+ * denied (closes the `tools.allow`-only latent fail-open).
64
+ * - Account-level tools are gated by the preset's action list, if a preset
65
+ * is set. Without a preset, a key scoped to specific collections/globals
66
+ * cannot use account-level tools — they'd broaden the surface.
67
+ */
68
+ export declare function buildScopeChecker(tools: RoutableTool[]): ScopeChecker;
69
+ /**
70
+ * Internal pure checker. Exposed for the per-request wrapper in the registry
71
+ * so it can re-use the same `RoutingTables` it built once at startup.
72
+ */
73
+ export declare function assertScopeAllows(scopes: KeyScopes | null | undefined, toolName: string, resource: string | undefined, tables: RoutingTables): ScopeDecision;
@@ -0,0 +1,218 @@
1
+ // ─── Per-preset action tables ────────────────────────────────────────
2
+ const ALL_ACTIONS = [
3
+ 'read',
4
+ 'create',
5
+ 'update',
6
+ 'delete'
7
+ ];
8
+ export const PRESET_ACTIONS = {
9
+ 'read-only': [
10
+ 'read'
11
+ ],
12
+ editor: [
13
+ 'read',
14
+ 'create',
15
+ 'update'
16
+ ],
17
+ admin: ALL_ACTIONS
18
+ };
19
+ /**
20
+ * Asymmetric per-preset action map for globals. `editor` is intentionally
21
+ * read-only on globals — a single bad write on a singleton broadcasts
22
+ * site-wide with no per-document containment. Operators who want global
23
+ * writes promote the key to `admin` or use a Custom key with explicit
24
+ * `globalScopes`. README and CHANGELOG call out the asymmetry.
25
+ */ export const PRESET_GLOBAL_ACTIONS = {
26
+ 'read-only': [
27
+ 'read'
28
+ ],
29
+ editor: [
30
+ 'read'
31
+ ],
32
+ admin: [
33
+ 'read',
34
+ 'update'
35
+ ]
36
+ };
37
+ export const PRESET_TOOL_DENY = {
38
+ 'read-only': [],
39
+ editor: [
40
+ 'safeDelete',
41
+ 'deleteDocument'
42
+ ],
43
+ admin: []
44
+ };
45
+ export function buildRoutingTables(tools) {
46
+ const collectionToolAction = new Map();
47
+ const globalToolAction = new Map();
48
+ const accountToolAction = new Map();
49
+ const toolKind = new Map();
50
+ for (const t of tools){
51
+ toolKind.set(t.name, t.routing.kind);
52
+ if (t.routing.kind === 'collection') collectionToolAction.set(t.name, t.routing.action);
53
+ else if (t.routing.kind === 'global') globalToolAction.set(t.name, t.routing.action);
54
+ else accountToolAction.set(t.name, t.routing.action);
55
+ }
56
+ return {
57
+ collectionToolAction,
58
+ globalToolAction,
59
+ accountToolAction,
60
+ toolKind
61
+ };
62
+ }
63
+ /**
64
+ * Build a scope checker bound to a concrete tool list. The checker is a pure
65
+ * function over (scopes, toolName, resource) — the routing tables are closed
66
+ * over once at construction time.
67
+ *
68
+ * Fail-closed semantics:
69
+ * - Null/undefined scopes grant full access (back-compat).
70
+ * - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*
71
+ * for that resource kind — unlisted resources are denied.
72
+ * - When a tool resolves to a collection or global kind but the corresponding
73
+ * scope map is undefined and `scopes.preset` is undefined, the call is
74
+ * denied (closes the `tools.allow`-only latent fail-open).
75
+ * - Account-level tools are gated by the preset's action list, if a preset
76
+ * is set. Without a preset, a key scoped to specific collections/globals
77
+ * cannot use account-level tools — they'd broaden the surface.
78
+ */ export function buildScopeChecker(tools) {
79
+ const tables = buildRoutingTables(tools);
80
+ return (scopes, toolName, resource)=>assertScopeAllows(scopes, toolName, resource, tables);
81
+ }
82
+ /**
83
+ * Internal pure checker. Exposed for the per-request wrapper in the registry
84
+ * so it can re-use the same `RoutingTables` it built once at startup.
85
+ */ export function assertScopeAllows(scopes, toolName, resource, tables) {
86
+ const resourceKind = tables.toolKind.get(toolName) ?? null;
87
+ // Unregistered tool — fail-closed at request time. Adding a tool without a
88
+ // routing field is a TS error at the factory return site, so this branch
89
+ // only fires for typo'd tool names sent by the client.
90
+ if (resourceKind === null) {
91
+ return {
92
+ allowed: false,
93
+ reason: `Tool "${toolName}" has no registered scope mapping.`
94
+ };
95
+ }
96
+ if (!scopes || scopes.preset === undefined && !scopes.collections && !scopes.globals && !scopes.tools) {
97
+ return {
98
+ allowed: true
99
+ };
100
+ }
101
+ if (scopes.tools?.deny?.includes(toolName)) {
102
+ return {
103
+ allowed: false,
104
+ reason: `Tool "${toolName}" is denied for this API key.`
105
+ };
106
+ }
107
+ if (scopes.tools?.allow && !scopes.tools.allow.includes(toolName)) {
108
+ return {
109
+ allowed: false,
110
+ reason: `Tool "${toolName}" is not in the allow-list for this API key.`
111
+ };
112
+ }
113
+ if (scopes.preset && PRESET_TOOL_DENY[scopes.preset]?.includes(toolName)) {
114
+ return {
115
+ allowed: false,
116
+ reason: `Tool "${toolName}" is not allowed by the "${scopes.preset}" preset.`
117
+ };
118
+ }
119
+ if (resourceKind === 'account') {
120
+ return checkAccount(scopes, toolName, tables.accountToolAction);
121
+ }
122
+ const policy = resourceKind === 'collection' ? COLLECTION_POLICY : GLOBAL_POLICY;
123
+ const toolAction = resourceKind === 'collection' ? tables.collectionToolAction : tables.globalToolAction;
124
+ return checkResource(scopes, toolName, resource, toolAction, policy);
125
+ }
126
+ const COLLECTION_POLICY = {
127
+ presetActions: PRESET_ACTIONS,
128
+ scopeAxis: 'collections',
129
+ label: 'collection',
130
+ Label: 'Collection'
131
+ };
132
+ const GLOBAL_POLICY = {
133
+ presetActions: PRESET_GLOBAL_ACTIONS,
134
+ scopeAxis: 'globals',
135
+ label: 'global',
136
+ Label: 'Global'
137
+ };
138
+ function checkResource(scopes, toolName, resource, toolAction, policy) {
139
+ const action = toolAction.get(toolName);
140
+ const presetActions = scopes.preset ? policy.presetActions[scopes.preset] : undefined;
141
+ const resourceScope = scopes[policy.scopeAxis];
142
+ if (!resource) {
143
+ // Resource-keyed tool called without a slug; defer to schema validation.
144
+ return {
145
+ allowed: true
146
+ };
147
+ }
148
+ if (!action) return {
149
+ allowed: true
150
+ };
151
+ if (resourceScope) {
152
+ const override = resourceScope[resource];
153
+ if (!override) {
154
+ return {
155
+ allowed: false,
156
+ reason: `${policy.Label} "${resource}" is not in this API key's allowed ${policy.scopeAxis}.`
157
+ };
158
+ }
159
+ if (!override.includes(action)) {
160
+ return {
161
+ allowed: false,
162
+ reason: `Action "${action}" on ${policy.label} "${resource}" is not permitted by this API key's scope.`
163
+ };
164
+ }
165
+ return {
166
+ allowed: true
167
+ };
168
+ }
169
+ if (!presetActions) {
170
+ // Fail-closed: `tools.allow` without a resource map or preset would
171
+ // otherwise broadcast the tool across every resource. Require explicit
172
+ // intent.
173
+ return {
174
+ allowed: false,
175
+ reason: `Tool "${toolName}" requires an explicit ${policy.label} scope or preset on this API key.`
176
+ };
177
+ }
178
+ if (!presetActions.includes(action)) {
179
+ return {
180
+ allowed: false,
181
+ reason: `Action "${action}" on ${policy.label} "${resource}" is not permitted by this API key's preset.`
182
+ };
183
+ }
184
+ return {
185
+ allowed: true
186
+ };
187
+ }
188
+ function checkAccount(scopes, toolName, toolAction) {
189
+ const action = toolAction.get(toolName);
190
+ const presetActions = scopes.preset ? PRESET_ACTIONS[scopes.preset] : undefined;
191
+ // Explicit resource override is the tightest signal: an account-level tool
192
+ // operates across the whole site (searchContent across every collection,
193
+ // uploadMedia into any media coll, etc.) and would broaden the key beyond
194
+ // the resource whitelist regardless of which preset is set. Deny account
195
+ // tools whenever the key carries explicit collection/global scopes.
196
+ if (scopes.collections || scopes.globals) {
197
+ return {
198
+ allowed: false,
199
+ reason: `Tool "${toolName}" is denied for keys with explicit collection or global scopes — account-level tools would broaden access beyond the whitelist.`
200
+ };
201
+ }
202
+ if (presetActions) {
203
+ if (action && !presetActions.includes(action)) {
204
+ return {
205
+ allowed: false,
206
+ reason: `Action "${action}" is not permitted by this API key's preset.`
207
+ };
208
+ }
209
+ return {
210
+ allowed: true
211
+ };
212
+ }
213
+ return {
214
+ allowed: true
215
+ };
216
+ }
217
+
218
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/scope/policy.ts"],"sourcesContent":["import type { CollectionAction, GlobalAction, KeyScopes, ScopePreset } from '../types'\r\n\r\n// ─── Routing primitives ──────────────────────────────────────────────\r\n\r\nexport type ResourceKind = 'collection' | 'global' | 'account'\r\n\r\n/**\r\n * Discriminated routing tag attached to every tool factory output.\r\n *\r\n * Collocates the scope-routing decision with the tool definition itself —\r\n * the registry derives the collection/global/account lookups from `tools`\r\n * at boot. Adding a new tool can no longer drift the routing maps out of\r\n * sync because TS requires `routing` on every factory return.\r\n */\r\nexport type ToolRouting =\r\n | { kind: 'collection'; action: CollectionAction }\r\n | { kind: 'global'; action: GlobalAction }\r\n | { kind: 'account'; action: CollectionAction }\r\n\r\n/**\r\n * Minimal \"routable tool\" interface used by the policy module. The full\r\n * `ToolFactoryOutput` shape (handler, parameters, description) is irrelevant\r\n * here; we only need `name` + `routing` to build the lookup tables.\r\n */\r\nexport interface RoutableTool {\r\n name: string\r\n routing: ToolRouting\r\n}\r\n\r\n// ─── Per-preset action tables ────────────────────────────────────────\r\n\r\nconst ALL_ACTIONS: CollectionAction[] = ['read', 'create', 'update', 'delete']\r\n\r\nexport const PRESET_ACTIONS: Record<ScopePreset, CollectionAction[]> = {\r\n 'read-only': ['read'],\r\n editor: ['read', 'create', 'update'],\r\n admin: ALL_ACTIONS,\r\n}\r\n\r\n/**\r\n * Asymmetric per-preset action map for globals. `editor` is intentionally\r\n * read-only on globals — a single bad write on a singleton broadcasts\r\n * site-wide with no per-document containment. Operators who want global\r\n * writes promote the key to `admin` or use a Custom key with explicit\r\n * `globalScopes`. README and CHANGELOG call out the asymmetry.\r\n */\r\nexport const PRESET_GLOBAL_ACTIONS: Record<ScopePreset, GlobalAction[]> = {\r\n 'read-only': ['read'],\r\n editor: ['read'],\r\n admin: ['read', 'update'],\r\n}\r\n\r\nexport const PRESET_TOOL_DENY: Record<ScopePreset, string[]> = {\r\n 'read-only': [],\r\n editor: ['safeDelete', 'deleteDocument'],\r\n admin: [],\r\n}\r\n\r\n// ─── Routing tables built from the tool list ─────────────────────────\r\n\r\nexport interface ScopeDecision {\r\n allowed: boolean\r\n reason?: string\r\n}\r\n\r\nexport interface RoutingTables {\r\n collectionToolAction: ReadonlyMap<string, CollectionAction>\r\n globalToolAction: ReadonlyMap<string, GlobalAction>\r\n accountToolAction: ReadonlyMap<string, CollectionAction>\r\n toolKind: ReadonlyMap<string, ResourceKind>\r\n}\r\n\r\nexport function buildRoutingTables(tools: RoutableTool[]): RoutingTables {\r\n const collectionToolAction = new Map<string, CollectionAction>()\r\n const globalToolAction = new Map<string, GlobalAction>()\r\n const accountToolAction = new Map<string, CollectionAction>()\r\n const toolKind = new Map<string, ResourceKind>()\r\n for (const t of tools) {\r\n toolKind.set(t.name, t.routing.kind)\r\n if (t.routing.kind === 'collection') collectionToolAction.set(t.name, t.routing.action)\r\n else if (t.routing.kind === 'global') globalToolAction.set(t.name, t.routing.action)\r\n else accountToolAction.set(t.name, t.routing.action)\r\n }\r\n return { collectionToolAction, globalToolAction, accountToolAction, toolKind }\r\n}\r\n\r\n// ─── Scope evaluation ────────────────────────────────────────────────\r\n\r\nexport type ScopeChecker = (\r\n scopes: KeyScopes | null | undefined,\r\n toolName: string,\r\n resource: string | undefined,\r\n) => ScopeDecision\r\n\r\n/**\r\n * Build a scope checker bound to a concrete tool list. The checker is a pure\r\n * function over (scopes, toolName, resource) — the routing tables are closed\r\n * over once at construction time.\r\n *\r\n * Fail-closed semantics:\r\n * - Null/undefined scopes grant full access (back-compat).\r\n * - When `scopes.collections` / `scopes.globals` is set, it is a *whitelist*\r\n * for that resource kind — unlisted resources are denied.\r\n * - When a tool resolves to a collection or global kind but the corresponding\r\n * scope map is undefined and `scopes.preset` is undefined, the call is\r\n * denied (closes the `tools.allow`-only latent fail-open).\r\n * - Account-level tools are gated by the preset's action list, if a preset\r\n * is set. Without a preset, a key scoped to specific collections/globals\r\n * cannot use account-level tools — they'd broaden the surface.\r\n */\r\nexport function buildScopeChecker(tools: RoutableTool[]): ScopeChecker {\r\n const tables = buildRoutingTables(tools)\r\n return (scopes, toolName, resource) => assertScopeAllows(scopes, toolName, resource, tables)\r\n}\r\n\r\n/**\r\n * Internal pure checker. Exposed for the per-request wrapper in the registry\r\n * so it can re-use the same `RoutingTables` it built once at startup.\r\n */\r\nexport function assertScopeAllows(\r\n scopes: KeyScopes | null | undefined,\r\n toolName: string,\r\n resource: string | undefined,\r\n tables: RoutingTables,\r\n): ScopeDecision {\r\n const resourceKind = tables.toolKind.get(toolName) ?? null\r\n // Unregistered tool — fail-closed at request time. Adding a tool without a\r\n // routing field is a TS error at the factory return site, so this branch\r\n // only fires for typo'd tool names sent by the client.\r\n if (resourceKind === null) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" has no registered scope mapping.`,\r\n }\r\n }\r\n\r\n if (!scopes || (scopes.preset === undefined && !scopes.collections && !scopes.globals && !scopes.tools)) {\r\n return { allowed: true }\r\n }\r\n\r\n if (scopes.tools?.deny?.includes(toolName)) {\r\n return { allowed: false, reason: `Tool \"${toolName}\" is denied for this API key.` }\r\n }\r\n if (scopes.tools?.allow && !scopes.tools.allow.includes(toolName)) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" is not in the allow-list for this API key.`,\r\n }\r\n }\r\n\r\n if (scopes.preset && PRESET_TOOL_DENY[scopes.preset]?.includes(toolName)) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" is not allowed by the \"${scopes.preset}\" preset.`,\r\n }\r\n }\r\n\r\n if (resourceKind === 'account') {\r\n return checkAccount(scopes, toolName, tables.accountToolAction)\r\n }\r\n const policy = resourceKind === 'collection' ? COLLECTION_POLICY : GLOBAL_POLICY\r\n const toolAction =\r\n resourceKind === 'collection' ? tables.collectionToolAction : tables.globalToolAction\r\n return checkResource(scopes, toolName, resource, toolAction, policy)\r\n}\r\n\r\n/**\r\n * Per-resource-kind policy. Collapses what used to be two near-identical\r\n * `checkCollection` / `checkGlobal` helpers — the only differences are\r\n * the preset-actions table, the label, and which axis of `KeyScopes` to\r\n * read for explicit overrides.\r\n */\r\ninterface ResourcePolicy {\r\n presetActions: Record<ScopePreset, readonly string[]>\r\n scopeAxis: 'collections' | 'globals'\r\n label: 'collection' | 'global'\r\n Label: 'Collection' | 'Global'\r\n}\r\n\r\nconst COLLECTION_POLICY: ResourcePolicy = {\r\n presetActions: PRESET_ACTIONS,\r\n scopeAxis: 'collections',\r\n label: 'collection',\r\n Label: 'Collection',\r\n}\r\n\r\nconst GLOBAL_POLICY: ResourcePolicy = {\r\n presetActions: PRESET_GLOBAL_ACTIONS,\r\n scopeAxis: 'globals',\r\n label: 'global',\r\n Label: 'Global',\r\n}\r\n\r\nfunction checkResource(\r\n scopes: KeyScopes,\r\n toolName: string,\r\n resource: string | undefined,\r\n toolAction: ReadonlyMap<string, string>,\r\n policy: ResourcePolicy,\r\n): ScopeDecision {\r\n const action = toolAction.get(toolName)\r\n const presetActions = scopes.preset ? policy.presetActions[scopes.preset] : undefined\r\n const resourceScope = scopes[policy.scopeAxis]\r\n\r\n if (!resource) {\r\n // Resource-keyed tool called without a slug; defer to schema validation.\r\n return { allowed: true }\r\n }\r\n if (!action) return { allowed: true }\r\n\r\n if (resourceScope) {\r\n const override = resourceScope[resource]\r\n if (!override) {\r\n return {\r\n allowed: false,\r\n reason: `${policy.Label} \"${resource}\" is not in this API key's allowed ${policy.scopeAxis}.`,\r\n }\r\n }\r\n if (!override.includes(action as never)) {\r\n return {\r\n allowed: false,\r\n reason: `Action \"${action}\" on ${policy.label} \"${resource}\" is not permitted by this API key's scope.`,\r\n }\r\n }\r\n return { allowed: true }\r\n }\r\n\r\n if (!presetActions) {\r\n // Fail-closed: `tools.allow` without a resource map or preset would\r\n // otherwise broadcast the tool across every resource. Require explicit\r\n // intent.\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" requires an explicit ${policy.label} scope or preset on this API key.`,\r\n }\r\n }\r\n\r\n if (!presetActions.includes(action)) {\r\n return {\r\n allowed: false,\r\n reason: `Action \"${action}\" on ${policy.label} \"${resource}\" is not permitted by this API key's preset.`,\r\n }\r\n }\r\n return { allowed: true }\r\n}\r\n\r\nfunction checkAccount(\r\n scopes: KeyScopes,\r\n toolName: string,\r\n toolAction: ReadonlyMap<string, CollectionAction>,\r\n): ScopeDecision {\r\n const action = toolAction.get(toolName)\r\n const presetActions = scopes.preset ? PRESET_ACTIONS[scopes.preset] : undefined\r\n\r\n // Explicit resource override is the tightest signal: an account-level tool\r\n // operates across the whole site (searchContent across every collection,\r\n // uploadMedia into any media coll, etc.) and would broaden the key beyond\r\n // the resource whitelist regardless of which preset is set. Deny account\r\n // tools whenever the key carries explicit collection/global scopes.\r\n if (scopes.collections || scopes.globals) {\r\n return {\r\n allowed: false,\r\n reason: `Tool \"${toolName}\" is denied for keys with explicit collection or global scopes — account-level tools would broaden access beyond the whitelist.`,\r\n }\r\n }\r\n\r\n if (presetActions) {\r\n if (action && !presetActions.includes(action)) {\r\n return {\r\n allowed: false,\r\n reason: `Action \"${action}\" is not permitted by this API key's preset.`,\r\n }\r\n }\r\n return { allowed: true }\r\n }\r\n\r\n return { allowed: true }\r\n}\r\n"],"names":["ALL_ACTIONS","PRESET_ACTIONS","editor","admin","PRESET_GLOBAL_ACTIONS","PRESET_TOOL_DENY","buildRoutingTables","tools","collectionToolAction","Map","globalToolAction","accountToolAction","toolKind","t","set","name","routing","kind","action","buildScopeChecker","tables","scopes","toolName","resource","assertScopeAllows","resourceKind","get","allowed","reason","preset","undefined","collections","globals","deny","includes","allow","checkAccount","policy","COLLECTION_POLICY","GLOBAL_POLICY","toolAction","checkResource","presetActions","scopeAxis","label","Label","resourceScope","override"],"mappings":"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"}
@@ -1,4 +1,15 @@
1
- import type { PayloadRequest } from 'payload';
1
+ import { z } from 'zod';
2
+ import type { CollectionConfig, PayloadRequest } from 'payload';
3
+ /**
4
+ * Build a `z.enum` over a list of valid resource slugs with a friendly
5
+ * error message that names the valid set and clarifies why an unknown
6
+ * slug (e.g. one removed via `options.exclude.globals`) is rejected.
7
+ *
8
+ * Default Zod enum errors are "Invalid enum value. …" — accurate but
9
+ * unhelpful when the slug looks plausible to a caller who isn't aware
10
+ * the host config excluded it.
11
+ */
12
+ export declare function slugEnum(slugs: string[], kind: 'global' | 'collection'): z.ZodEnum<[string, ...string[]]>;
2
13
  export interface McpTextResponse {
3
14
  content: Array<{
4
15
  type: 'text';
@@ -9,6 +20,56 @@ export declare const DRAFT_NOTE = " Document is in draft status \u2014 use publi
9
20
  export declare function textResponse(text: string): McpTextResponse;
10
21
  export declare function jsonResponse(payload: unknown): McpTextResponse;
11
22
  export declare function errorMessage(error: unknown): string;
23
+ export type PublishVerifyTarget = {
24
+ kind: 'collection';
25
+ slug: string;
26
+ id: string;
27
+ } | {
28
+ kind: 'global';
29
+ slug: string;
30
+ locale?: string;
31
+ };
32
+ /**
33
+ * Capture the document's current `updatedAt` BEFORE a publish attempt so
34
+ * the recovery branch can tell "this attempt landed despite a post-write
35
+ * validator throw" from "an older publish was successful and this attempt
36
+ * did nothing". A missing snapshot is non-fatal — the recovery branch
37
+ * conservatively falls through to the original error in that case.
38
+ */
39
+ export declare function snapshotPublishMarker(req: PayloadRequest, target: PublishVerifyTarget): Promise<string | undefined>;
40
+ /**
41
+ * After a Payload update throws on a publish call, determine whether the
42
+ * publish actually landed despite the error. Returns the live document
43
+ * only when (a) the live `_status` is 'published' AND (b) `updatedAt`
44
+ * strictly advanced past the pre-update snapshot — i.e. the current
45
+ * attempt produced the published row. Without the strictly-newer check,
46
+ * a pre-existing published version from an earlier successful publish
47
+ * would mask a real failure of the current attempt.
48
+ *
49
+ * Returns null on:
50
+ * - missing pre-snapshot (cannot disambiguate; conservative)
51
+ * - verify read failure (do not mask the original error with a
52
+ * secondary read error)
53
+ * - live `_status` not 'published'
54
+ * - live `updatedAt` not strictly newer than the pre-snapshot
55
+ */
56
+ export declare function verifyPublishSucceededDespiteError(req: PayloadRequest, target: PublishVerifyTarget, preUpdatedAt: string | undefined): Promise<Record<string, unknown> | null>;
12
57
  export declare function stampMcpContext(req: PayloadRequest): void;
13
58
  export declare function getDocDisplayName(doc: unknown, fallback: string): string;
14
59
  export declare function requireDraftCollection(collection: string, draftCollections: Set<string>, noun?: string): McpTextResponse | null;
60
+ /**
61
+ * Resolves the preview URL for a draft document by delegating to the
62
+ * collection's own configured preview function (`admin.livePreview.url`
63
+ * preferred, then `admin.preview`). Returns null when no function is
64
+ * configured, when it fails, or when it returns a relative path with no
65
+ * absolute `siteUrl` to anchor it.
66
+ */
67
+ export declare function resolvePreviewUrl(collection: CollectionConfig, doc: Record<string, unknown>, req: PayloadRequest, siteUrl: string | undefined): Promise<string | null>;
68
+ /**
69
+ * If `doc` is a draft, appends a preview-URL hint to the MCP response so the
70
+ * AI can present it to the user. Falls back to a generic admin-panel hint
71
+ * when the collection has no preview function configured.
72
+ *
73
+ * Pure with respect to the response: a fresh content array is returned.
74
+ */
75
+ export declare function decorateDraftResponse(response: McpTextResponse, doc: Record<string, unknown> | null | undefined, collection: CollectionConfig | undefined, req: PayloadRequest, siteUrl: string | undefined): Promise<McpTextResponse>;
@@ -1,3 +1,19 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Build a `z.enum` over a list of valid resource slugs with a friendly
4
+ * error message that names the valid set and clarifies why an unknown
5
+ * slug (e.g. one removed via `options.exclude.globals`) is rejected.
6
+ *
7
+ * Default Zod enum errors are "Invalid enum value. …" — accurate but
8
+ * unhelpful when the slug looks plausible to a caller who isn't aware
9
+ * the host config excluded it.
10
+ */ export function slugEnum(slugs, kind) {
11
+ return z.enum(slugs, {
12
+ errorMap: ()=>({
13
+ message: `${kind === 'global' ? 'Global' : 'Collection'} slug must be one of: ${slugs.join(', ')}. Unknown or excluded slugs are rejected.`
14
+ })
15
+ });
16
+ }
1
17
  export const DRAFT_NOTE = ' Document is in draft status — use publishDraft to make it live.';
2
18
  export function textResponse(text) {
3
19
  return {
@@ -15,6 +31,104 @@ export function jsonResponse(payload) {
15
31
  export function errorMessage(error) {
16
32
  return error instanceof Error ? error.message : String(error);
17
33
  }
34
+ function asIsoString(v) {
35
+ if (typeof v === 'string') return v;
36
+ if (v instanceof Date) return v.toISOString();
37
+ return undefined;
38
+ }
39
+ /**
40
+ * Capture the document's current `updatedAt` BEFORE a publish attempt so
41
+ * the recovery branch can tell "this attempt landed despite a post-write
42
+ * validator throw" from "an older publish was successful and this attempt
43
+ * did nothing". A missing snapshot is non-fatal — the recovery branch
44
+ * conservatively falls through to the original error in that case.
45
+ */ export async function snapshotPublishMarker(req, target) {
46
+ try {
47
+ const pre = target.kind === 'collection' ? await req.payload.findByID({
48
+ collection: target.slug,
49
+ id: target.id,
50
+ draft: true,
51
+ depth: 0,
52
+ req,
53
+ overrideAccess: false,
54
+ user: req.user
55
+ }) : await req.payload.findGlobal({
56
+ slug: target.slug,
57
+ draft: true,
58
+ depth: 0,
59
+ // `fallbackLocale: false` disables Payload's locale-fallback so
60
+ // the read returns the literal state of the requested locale.
61
+ // Without this, a localized global with fallbackLocale='en'
62
+ // could report the 'en' updatedAt while the caller is
63
+ // publishing 'de', producing a false-positive in
64
+ // verifyPublishSucceededDespiteError.
65
+ ...target.locale ? {
66
+ locale: target.locale,
67
+ fallbackLocale: false
68
+ } : {},
69
+ req,
70
+ overrideAccess: false,
71
+ user: req.user
72
+ });
73
+ return asIsoString(pre?.updatedAt);
74
+ } catch {
75
+ return undefined;
76
+ }
77
+ }
78
+ /**
79
+ * After a Payload update throws on a publish call, determine whether the
80
+ * publish actually landed despite the error. Returns the live document
81
+ * only when (a) the live `_status` is 'published' AND (b) `updatedAt`
82
+ * strictly advanced past the pre-update snapshot — i.e. the current
83
+ * attempt produced the published row. Without the strictly-newer check,
84
+ * a pre-existing published version from an earlier successful publish
85
+ * would mask a real failure of the current attempt.
86
+ *
87
+ * Returns null on:
88
+ * - missing pre-snapshot (cannot disambiguate; conservative)
89
+ * - verify read failure (do not mask the original error with a
90
+ * secondary read error)
91
+ * - live `_status` not 'published'
92
+ * - live `updatedAt` not strictly newer than the pre-snapshot
93
+ */ export async function verifyPublishSucceededDespiteError(req, target, preUpdatedAt) {
94
+ if (!preUpdatedAt) return null;
95
+ try {
96
+ const live = target.kind === 'collection' ? await req.payload.findByID({
97
+ collection: target.slug,
98
+ id: target.id,
99
+ draft: false,
100
+ depth: 0,
101
+ req,
102
+ overrideAccess: false,
103
+ user: req.user
104
+ }) : await req.payload.findGlobal({
105
+ slug: target.slug,
106
+ draft: false,
107
+ depth: 0,
108
+ // Disable Payload locale-fallback (see snapshotPublishMarker
109
+ // note) so verify reads the literal state of the locale that
110
+ // updateGlobal was called against.
111
+ ...target.locale ? {
112
+ locale: target.locale,
113
+ fallbackLocale: false
114
+ } : {},
115
+ req,
116
+ overrideAccess: false,
117
+ user: req.user
118
+ });
119
+ const d = live;
120
+ if (!d || d._status !== 'published') return null;
121
+ const liveUpdatedAt = asIsoString(d.updatedAt);
122
+ if (!liveUpdatedAt || liveUpdatedAt <= preUpdatedAt) return null;
123
+ return d;
124
+ } catch (verifyError) {
125
+ req.payload.logger?.debug?.({
126
+ event: 'mcp.publish.verify_read_failed',
127
+ err: verifyError
128
+ }, '[payload-mcp-toolkit] publish-recovery verify-read failed; surfacing original error');
129
+ return null;
130
+ }
131
+ }
18
132
  export function stampMcpContext(req) {
19
133
  req.context = {
20
134
  ...req.context,
@@ -31,5 +145,72 @@ export function requireDraftCollection(collection, draftCollections, noun = 'dra
31
145
  ...draftCollections
32
146
  ].join(', ') || 'none'}`);
33
147
  }
148
+ /**
149
+ * Resolves the preview URL for a draft document by delegating to the
150
+ * collection's own configured preview function (`admin.livePreview.url`
151
+ * preferred, then `admin.preview`). Returns null when no function is
152
+ * configured, when it fails, or when it returns a relative path with no
153
+ * absolute `siteUrl` to anchor it.
154
+ */ export async function resolvePreviewUrl(collection, doc, req, siteUrl) {
155
+ const admin = collection.admin ?? {};
156
+ const locale = req.locale ?? 'en';
157
+ let raw;
158
+ const livePreviewUrl = admin.livePreview?.url;
159
+ if (typeof livePreviewUrl === 'function') {
160
+ try {
161
+ raw = await livePreviewUrl({
162
+ data: doc,
163
+ locale: {
164
+ code: locale,
165
+ label: locale
166
+ },
167
+ req,
168
+ payload: req.payload,
169
+ collectionConfig: collection
170
+ });
171
+ } catch {
172
+ raw = null;
173
+ }
174
+ } else if (typeof livePreviewUrl === 'string') {
175
+ raw = livePreviewUrl;
176
+ }
177
+ if (!raw && typeof admin.preview === 'function') {
178
+ try {
179
+ raw = await admin.preview(doc, {
180
+ locale,
181
+ req,
182
+ token: null
183
+ });
184
+ } catch {
185
+ raw = null;
186
+ }
187
+ }
188
+ if (!raw || typeof raw !== 'string') return null;
189
+ if (raw.startsWith('http://') || raw.startsWith('https://')) return raw;
190
+ if (!siteUrl) return null;
191
+ const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;
192
+ const path = raw.startsWith('/') ? raw : `/${raw}`;
193
+ return `${base}${path}`;
194
+ }
195
+ /**
196
+ * If `doc` is a draft, appends a preview-URL hint to the MCP response so the
197
+ * AI can present it to the user. Falls back to a generic admin-panel hint
198
+ * when the collection has no preview function configured.
199
+ *
200
+ * Pure with respect to the response: a fresh content array is returned.
201
+ */ export async function decorateDraftResponse(response, doc, collection, req, siteUrl) {
202
+ if (!doc || doc._status !== 'draft' || !collection) return response;
203
+ const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl);
204
+ const hint = previewUrl ? `\nšŸ“‹ This document is a draft. Preview it here: ${previewUrl}` : '\nšŸ“‹ This document is a draft. Use the admin panel to preview it.';
205
+ return {
206
+ content: [
207
+ ...response.content,
208
+ {
209
+ type: 'text',
210
+ text: hint
211
+ }
212
+ ]
213
+ };
214
+ }
34
215
 
35
216
  //# sourceMappingURL=_helpers.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/_helpers.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nexport interface McpTextResponse {\n content: Array<{ type: 'text'; text: string }>\n}\n\nexport const DRAFT_NOTE = ' Document is in draft status — use publishDraft to make it live.'\n\nexport function textResponse(text: string): McpTextResponse {\n return { content: [{ type: 'text', text }] }\n}\n\nexport function jsonResponse(payload: unknown): McpTextResponse {\n return textResponse(JSON.stringify(payload))\n}\n\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nexport function stampMcpContext(req: PayloadRequest): void {\n req.context = { ...req.context, source: 'mcp' }\n}\n\nexport function getDocDisplayName(doc: unknown, fallback: string): string {\n const d = doc as Record<string, unknown> | null | undefined\n return (\n (typeof d?.name === 'string' && d.name) ||\n (typeof d?.title === 'string' && d.title) ||\n (typeof d?.slug === 'string' && d.slug) ||\n fallback\n )\n}\n\nexport function requireDraftCollection(\n collection: string,\n draftCollections: Set<string>,\n noun = 'drafts',\n): McpTextResponse | null {\n if (draftCollections.has(collection)) return null\n return textResponse(\n `Error: Collection \"${collection}\" does not support ${noun}. ` +\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\n )\n}\n"],"names":["DRAFT_NOTE","textResponse","text","content","type","jsonResponse","payload","JSON","stringify","errorMessage","error","Error","message","String","stampMcpContext","req","context","source","getDocDisplayName","doc","fallback","d","name","title","slug","requireDraftCollection","collection","draftCollections","noun","has","join"],"mappings":"AAMA,OAAO,MAAMA,aAAa,mEAAkE;AAE5F,OAAO,SAASC,aAAaC,IAAY;IACvC,OAAO;QAAEC,SAAS;YAAC;gBAAEC,MAAM;gBAAQF;YAAK;SAAE;IAAC;AAC7C;AAEA,OAAO,SAASG,aAAaC,OAAgB;IAC3C,OAAOL,aAAaM,KAAKC,SAAS,CAACF;AACrC;AAEA,OAAO,SAASG,aAAaC,KAAc;IACzC,OAAOA,iBAAiBC,QAAQD,MAAME,OAAO,GAAGC,OAAOH;AACzD;AAEA,OAAO,SAASI,gBAAgBC,GAAmB;IACjDA,IAAIC,OAAO,GAAG;QAAE,GAAGD,IAAIC,OAAO;QAAEC,QAAQ;IAAM;AAChD;AAEA,OAAO,SAASC,kBAAkBC,GAAY,EAAEC,QAAgB;IAC9D,MAAMC,IAAIF;IACV,OACE,AAAC,OAAOE,GAAGC,SAAS,YAAYD,EAAEC,IAAI,IACrC,OAAOD,GAAGE,UAAU,YAAYF,EAAEE,KAAK,IACvC,OAAOF,GAAGG,SAAS,YAAYH,EAAEG,IAAI,IACtCJ;AAEJ;AAEA,OAAO,SAASK,uBACdC,UAAkB,EAClBC,gBAA6B,EAC7BC,OAAO,QAAQ;IAEf,IAAID,iBAAiBE,GAAG,CAACH,aAAa,OAAO;IAC7C,OAAOzB,aACL,CAAC,mBAAmB,EAAEyB,WAAW,mBAAmB,EAAEE,KAAK,EAAE,CAAC,GAC5D,CAAC,2BAA2B,EAAE;WAAID;KAAiB,CAACG,IAAI,CAAC,SAAS,QAAQ;AAEhF"}
1
+ {"version":3,"sources":["../../src/tools/_helpers.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { CollectionConfig, PayloadRequest } from 'payload'\r\n\r\n/**\r\n * Build a `z.enum` over a list of valid resource slugs with a friendly\r\n * error message that names the valid set and clarifies why an unknown\r\n * slug (e.g. one removed via `options.exclude.globals`) is rejected.\r\n *\r\n * Default Zod enum errors are \"Invalid enum value. …\" — accurate but\r\n * unhelpful when the slug looks plausible to a caller who isn't aware\r\n * the host config excluded it.\r\n */\r\nexport function slugEnum(\r\n slugs: string[],\r\n kind: 'global' | 'collection',\r\n): z.ZodEnum<[string, ...string[]]> {\r\n return z.enum(slugs as [string, ...string[]], {\r\n errorMap: () => ({\r\n message: `${kind === 'global' ? 'Global' : 'Collection'} slug must be one of: ${slugs.join(', ')}. Unknown or excluded slugs are rejected.`,\r\n }),\r\n })\r\n}\r\n\r\nexport interface McpTextResponse {\r\n content: Array<{ type: 'text'; text: string }>\r\n}\r\n\r\nexport const DRAFT_NOTE = ' Document is in draft status — use publishDraft to make it live.'\r\n\r\nexport function textResponse(text: string): McpTextResponse {\r\n return { content: [{ type: 'text', text }] }\r\n}\r\n\r\nexport function jsonResponse(payload: unknown): McpTextResponse {\r\n return textResponse(JSON.stringify(payload))\r\n}\r\n\r\nexport function errorMessage(error: unknown): string {\r\n return error instanceof Error ? error.message : String(error)\r\n}\r\n\r\nfunction asIsoString(v: unknown): string | undefined {\r\n if (typeof v === 'string') return v\r\n if (v instanceof Date) return v.toISOString()\r\n return undefined\r\n}\r\n\r\nexport type PublishVerifyTarget =\r\n | { kind: 'collection'; slug: string; id: string }\r\n | { kind: 'global'; slug: string; locale?: string }\r\n\r\n/**\r\n * Capture the document's current `updatedAt` BEFORE a publish attempt so\r\n * the recovery branch can tell \"this attempt landed despite a post-write\r\n * validator throw\" from \"an older publish was successful and this attempt\r\n * did nothing\". A missing snapshot is non-fatal — the recovery branch\r\n * conservatively falls through to the original error in that case.\r\n */\r\nexport async function snapshotPublishMarker(\r\n req: PayloadRequest,\r\n target: PublishVerifyTarget,\r\n): Promise<string | undefined> {\r\n try {\r\n const pre =\r\n target.kind === 'collection'\r\n ? await req.payload.findByID({\r\n collection: target.slug as any,\r\n id: target.id,\r\n draft: true,\r\n depth: 0,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n : await req.payload.findGlobal({\r\n slug: target.slug as never,\r\n draft: true,\r\n depth: 0,\r\n // `fallbackLocale: false` disables Payload's locale-fallback so\r\n // the read returns the literal state of the requested locale.\r\n // Without this, a localized global with fallbackLocale='en'\r\n // could report the 'en' updatedAt while the caller is\r\n // publishing 'de', producing a false-positive in\r\n // verifyPublishSucceededDespiteError.\r\n ...(target.locale\r\n ? { locale: target.locale as never, fallbackLocale: false as never }\r\n : {}),\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n return asIsoString((pre as { updatedAt?: unknown } | null | undefined)?.updatedAt)\r\n } catch {\r\n return undefined\r\n }\r\n}\r\n\r\n/**\r\n * After a Payload update throws on a publish call, determine whether the\r\n * publish actually landed despite the error. Returns the live document\r\n * only when (a) the live `_status` is 'published' AND (b) `updatedAt`\r\n * strictly advanced past the pre-update snapshot — i.e. the current\r\n * attempt produced the published row. Without the strictly-newer check,\r\n * a pre-existing published version from an earlier successful publish\r\n * would mask a real failure of the current attempt.\r\n *\r\n * Returns null on:\r\n * - missing pre-snapshot (cannot disambiguate; conservative)\r\n * - verify read failure (do not mask the original error with a\r\n * secondary read error)\r\n * - live `_status` not 'published'\r\n * - live `updatedAt` not strictly newer than the pre-snapshot\r\n */\r\nexport async function verifyPublishSucceededDespiteError(\r\n req: PayloadRequest,\r\n target: PublishVerifyTarget,\r\n preUpdatedAt: string | undefined,\r\n): Promise<Record<string, unknown> | null> {\r\n if (!preUpdatedAt) return null\r\n try {\r\n const live =\r\n target.kind === 'collection'\r\n ? await req.payload.findByID({\r\n collection: target.slug as any,\r\n id: target.id,\r\n draft: false,\r\n depth: 0,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n : await req.payload.findGlobal({\r\n slug: target.slug as never,\r\n draft: false,\r\n depth: 0,\r\n // Disable Payload locale-fallback (see snapshotPublishMarker\r\n // note) so verify reads the literal state of the locale that\r\n // updateGlobal was called against.\r\n ...(target.locale\r\n ? { locale: target.locale as never, fallbackLocale: false as never }\r\n : {}),\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n const d = live as Record<string, unknown> | null | undefined\r\n if (!d || d._status !== 'published') return null\r\n const liveUpdatedAt = asIsoString(d.updatedAt)\r\n if (!liveUpdatedAt || liveUpdatedAt <= preUpdatedAt) return null\r\n return d\r\n } catch (verifyError) {\r\n req.payload.logger?.debug?.(\r\n { event: 'mcp.publish.verify_read_failed', err: verifyError },\r\n '[payload-mcp-toolkit] publish-recovery verify-read failed; surfacing original error',\r\n )\r\n return null\r\n }\r\n}\r\n\r\nexport function stampMcpContext(req: PayloadRequest): void {\r\n req.context = { ...req.context, source: 'mcp' }\r\n}\r\n\r\nexport function getDocDisplayName(doc: unknown, fallback: string): string {\r\n const d = doc as Record<string, unknown> | null | undefined\r\n return (\r\n (typeof d?.name === 'string' && d.name) ||\r\n (typeof d?.title === 'string' && d.title) ||\r\n (typeof d?.slug === 'string' && d.slug) ||\r\n fallback\r\n )\r\n}\r\n\r\nexport function requireDraftCollection(\r\n collection: string,\r\n draftCollections: Set<string>,\r\n noun = 'drafts',\r\n): McpTextResponse | null {\r\n if (draftCollections.has(collection)) return null\r\n return textResponse(\r\n `Error: Collection \"${collection}\" does not support ${noun}. ` +\r\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\r\n )\r\n}\r\n\r\n/**\r\n * Resolves the preview URL for a draft document by delegating to the\r\n * collection's own configured preview function (`admin.livePreview.url`\r\n * preferred, then `admin.preview`). Returns null when no function is\r\n * configured, when it fails, or when it returns a relative path with no\r\n * absolute `siteUrl` to anchor it.\r\n */\r\nexport async function resolvePreviewUrl(\r\n collection: CollectionConfig,\r\n doc: Record<string, unknown>,\r\n req: PayloadRequest,\r\n siteUrl: string | undefined,\r\n): Promise<string | null> {\r\n const admin = (collection.admin ?? {}) as Record<string, any>\r\n const locale = (req as unknown as { locale?: string }).locale ?? 'en'\r\n\r\n let raw: string | null | undefined\r\n\r\n const livePreviewUrl = admin.livePreview?.url\r\n if (typeof livePreviewUrl === 'function') {\r\n try {\r\n raw = await livePreviewUrl({\r\n data: doc,\r\n locale: { code: locale, label: locale },\r\n req,\r\n payload: req.payload,\r\n collectionConfig: collection,\r\n })\r\n } catch {\r\n raw = null\r\n }\r\n } else if (typeof livePreviewUrl === 'string') {\r\n raw = livePreviewUrl\r\n }\r\n\r\n if (!raw && typeof admin.preview === 'function') {\r\n try {\r\n raw = await admin.preview(doc, { locale, req, token: null })\r\n } catch {\r\n raw = null\r\n }\r\n }\r\n\r\n if (!raw || typeof raw !== 'string') return null\r\n\r\n if (raw.startsWith('http://') || raw.startsWith('https://')) return raw\r\n if (!siteUrl) return null\r\n\r\n const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl\r\n const path = raw.startsWith('/') ? raw : `/${raw}`\r\n return `${base}${path}`\r\n}\r\n\r\n/**\r\n * If `doc` is a draft, appends a preview-URL hint to the MCP response so the\r\n * AI can present it to the user. Falls back to a generic admin-panel hint\r\n * when the collection has no preview function configured.\r\n *\r\n * Pure with respect to the response: a fresh content array is returned.\r\n */\r\nexport async function decorateDraftResponse(\r\n response: McpTextResponse,\r\n doc: Record<string, unknown> | null | undefined,\r\n collection: CollectionConfig | undefined,\r\n req: PayloadRequest,\r\n siteUrl: string | undefined,\r\n): Promise<McpTextResponse> {\r\n if (!doc || doc._status !== 'draft' || !collection) return response\r\n\r\n const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl)\r\n const hint = previewUrl\r\n ? `\\nšŸ“‹ This document is a draft. Preview it here: ${previewUrl}`\r\n : '\\nšŸ“‹ This document is a draft. Use the admin panel to preview it.'\r\n\r\n return { content: [...response.content, { type: 'text', text: hint }] }\r\n}\r\n"],"names":["z","slugEnum","slugs","kind","enum","errorMap","message","join","DRAFT_NOTE","textResponse","text","content","type","jsonResponse","payload","JSON","stringify","errorMessage","error","Error","String","asIsoString","v","Date","toISOString","undefined","snapshotPublishMarker","req","target","pre","findByID","collection","slug","id","draft","depth","overrideAccess","user","findGlobal","locale","fallbackLocale","updatedAt","verifyPublishSucceededDespiteError","preUpdatedAt","live","d","_status","liveUpdatedAt","verifyError","logger","debug","event","err","stampMcpContext","context","source","getDocDisplayName","doc","fallback","name","title","requireDraftCollection","draftCollections","noun","has","resolvePreviewUrl","siteUrl","admin","raw","livePreviewUrl","livePreview","url","data","code","label","collectionConfig","preview","token","startsWith","base","endsWith","slice","path","decorateDraftResponse","response","previewUrl","hint"],"mappings":"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"}