payload-mcp-toolkit 0.3.3 → 0.7.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 (144) hide show
  1. package/README.md +232 -150
  2. package/dist/__tests__/api-keys.test.js +292 -0
  3. package/dist/__tests__/api-keys.test.js.map +1 -0
  4. package/dist/__tests__/auth-strategy.test.js +681 -0
  5. package/dist/__tests__/auth-strategy.test.js.map +1 -0
  6. package/dist/__tests__/conflict-detection.test.js +69 -0
  7. package/dist/__tests__/conflict-detection.test.js.map +1 -0
  8. package/dist/__tests__/delete-document.test.js +70 -0
  9. package/dist/__tests__/delete-document.test.js.map +1 -0
  10. package/dist/__tests__/endpoint.test.js +143 -0
  11. package/dist/__tests__/endpoint.test.js.map +1 -0
  12. package/dist/__tests__/find-document.test.js +178 -0
  13. package/dist/__tests__/find-document.test.js.map +1 -0
  14. package/dist/__tests__/find-global.test.js +173 -0
  15. package/dist/__tests__/find-global.test.js.map +1 -0
  16. package/dist/__tests__/global-versions.test.js +183 -0
  17. package/dist/__tests__/global-versions.test.js.map +1 -0
  18. package/dist/__tests__/hash.test.js +58 -0
  19. package/dist/__tests__/hash.test.js.map +1 -0
  20. package/dist/__tests__/index-integration.test.js +191 -0
  21. package/dist/__tests__/index-integration.test.js.map +1 -0
  22. package/dist/__tests__/introspection.test.js +201 -1
  23. package/dist/__tests__/introspection.test.js.map +1 -1
  24. package/dist/__tests__/patch-global-layout.test.js +474 -0
  25. package/dist/__tests__/patch-global-layout.test.js.map +1 -0
  26. package/dist/__tests__/patch-layout.test.js +171 -0
  27. package/dist/__tests__/patch-layout.test.js.map +1 -0
  28. package/dist/__tests__/registry.test.js +795 -0
  29. package/dist/__tests__/registry.test.js.map +1 -0
  30. package/dist/__tests__/resources.test.js +139 -0
  31. package/dist/__tests__/resources.test.js.map +1 -0
  32. package/dist/__tests__/update-global.test.js +157 -0
  33. package/dist/__tests__/update-global.test.js.map +1 -0
  34. package/dist/api-keys.d.ts +46 -0
  35. package/dist/api-keys.js +272 -0
  36. package/dist/api-keys.js.map +1 -0
  37. package/dist/auth-strategy.d.ts +85 -0
  38. package/dist/auth-strategy.js +219 -0
  39. package/dist/auth-strategy.js.map +1 -0
  40. package/dist/components/CollectionScopesMatrix.d.ts +8 -0
  41. package/dist/components/CollectionScopesMatrix.js +32 -0
  42. package/dist/components/CollectionScopesMatrix.js.map +1 -0
  43. package/dist/components/GlobalScopesMatrix.d.ts +8 -0
  44. package/dist/components/GlobalScopesMatrix.js +28 -0
  45. package/dist/components/GlobalScopesMatrix.js.map +1 -0
  46. package/dist/components/ScopesTable.d.ts +19 -0
  47. package/dist/components/ScopesTable.js +285 -0
  48. package/dist/components/ScopesTable.js.map +1 -0
  49. package/dist/components/index.d.ts +2 -0
  50. package/dist/components/index.js +4 -0
  51. package/dist/components/index.js.map +1 -0
  52. package/dist/conflict-detection.d.ts +13 -0
  53. package/dist/conflict-detection.js +41 -0
  54. package/dist/conflict-detection.js.map +1 -0
  55. package/dist/draft-workflow.d.ts +46 -47
  56. package/dist/draft-workflow.js +53 -130
  57. package/dist/draft-workflow.js.map +1 -1
  58. package/dist/endpoint.d.ts +35 -0
  59. package/dist/endpoint.js +105 -0
  60. package/dist/endpoint.js.map +1 -0
  61. package/dist/hash.d.ts +21 -0
  62. package/dist/hash.js +36 -0
  63. package/dist/hash.js.map +1 -0
  64. package/dist/index.d.ts +9 -9
  65. package/dist/index.js +168 -68
  66. package/dist/index.js.map +1 -1
  67. package/dist/introspection.d.ts +17 -3
  68. package/dist/introspection.js +95 -36
  69. package/dist/introspection.js.map +1 -1
  70. package/dist/prompts.js +5 -5
  71. package/dist/prompts.js.map +1 -1
  72. package/dist/registry.d.ts +50 -0
  73. package/dist/registry.js +169 -0
  74. package/dist/registry.js.map +1 -0
  75. package/dist/resources.d.ts +5 -3
  76. package/dist/resources.js +23 -11
  77. package/dist/resources.js.map +1 -1
  78. package/dist/scope/audit-log.d.ts +18 -0
  79. package/dist/scope/audit-log.js +50 -0
  80. package/dist/scope/audit-log.js.map +1 -0
  81. package/dist/scope/policy.d.ts +73 -0
  82. package/dist/scope/policy.js +218 -0
  83. package/dist/scope/policy.js.map +1 -0
  84. package/dist/tools/_helpers.d.ts +28 -1
  85. package/dist/tools/_helpers.js +83 -0
  86. package/dist/tools/_helpers.js.map +1 -1
  87. package/dist/tools/_layout-helpers.d.ts +43 -0
  88. package/dist/tools/_layout-helpers.js +159 -0
  89. package/dist/tools/_layout-helpers.js.map +1 -0
  90. package/dist/tools/create-document.d.ts +36 -0
  91. package/dist/tools/create-document.js +83 -0
  92. package/dist/tools/create-document.js.map +1 -0
  93. package/dist/tools/delete-document.d.ts +25 -0
  94. package/dist/tools/delete-document.js +49 -0
  95. package/dist/tools/delete-document.js.map +1 -0
  96. package/dist/tools/find-document.d.ts +33 -0
  97. package/dist/tools/find-document.js +97 -0
  98. package/dist/tools/find-document.js.map +1 -0
  99. package/dist/tools/find-global.d.ts +26 -0
  100. package/dist/tools/find-global.js +122 -0
  101. package/dist/tools/find-global.js.map +1 -0
  102. package/dist/tools/global-versions.d.ts +39 -0
  103. package/dist/tools/global-versions.js +132 -0
  104. package/dist/tools/global-versions.js.map +1 -0
  105. package/dist/tools/patch-global-layout.d.ts +31 -0
  106. package/dist/tools/patch-global-layout.js +127 -0
  107. package/dist/tools/patch-global-layout.js.map +1 -0
  108. package/dist/tools/patch-layout.d.ts +5 -8
  109. package/dist/tools/patch-layout.js +18 -100
  110. package/dist/tools/patch-layout.js.map +1 -1
  111. package/dist/tools/publish-draft.d.ts +5 -4
  112. package/dist/tools/publish-draft.js +6 -1
  113. package/dist/tools/publish-draft.js.map +1 -1
  114. package/dist/tools/publish-global-draft.d.ts +20 -0
  115. package/dist/tools/publish-global-draft.js +50 -0
  116. package/dist/tools/publish-global-draft.js.map +1 -0
  117. package/dist/tools/resolve-reference.d.ts +5 -4
  118. package/dist/tools/resolve-reference.js +4 -0
  119. package/dist/tools/resolve-reference.js.map +1 -1
  120. package/dist/tools/safe-delete.d.ts +5 -5
  121. package/dist/tools/safe-delete.js +20 -15
  122. package/dist/tools/safe-delete.js.map +1 -1
  123. package/dist/tools/schedule-publish.d.ts +5 -5
  124. package/dist/tools/schedule-publish.js +23 -19
  125. package/dist/tools/schedule-publish.js.map +1 -1
  126. package/dist/tools/search-content.d.ts +5 -9
  127. package/dist/tools/search-content.js +16 -12
  128. package/dist/tools/search-content.js.map +1 -1
  129. package/dist/tools/update-document.d.ts +5 -5
  130. package/dist/tools/update-document.js +10 -5
  131. package/dist/tools/update-document.js.map +1 -1
  132. package/dist/tools/update-global.d.ts +27 -0
  133. package/dist/tools/update-global.js +72 -0
  134. package/dist/tools/update-global.js.map +1 -0
  135. package/dist/tools/upload-media.d.ts +5 -4
  136. package/dist/tools/upload-media.js +6 -1
  137. package/dist/tools/upload-media.js.map +1 -1
  138. package/dist/tools/versions.d.ts +10 -9
  139. package/dist/tools/versions.js +15 -7
  140. package/dist/tools/versions.js.map +1 -1
  141. package/dist/types.d.ts +56 -3
  142. package/dist/types.js +13 -6
  143. package/dist/types.js.map +1 -1
  144. package/package.json +11 -4
@@ -1,18 +1,19 @@
1
1
  import { z } from 'zod';
2
2
  import { DRAFT_NOTE, errorMessage, getDocDisplayName, jsonResponse, stampMcpContext } from './_helpers';
3
- /**
4
- * patchLayout — surgical wrapper that mutates a single document's blocks-typed
5
- * field directly without round-tripping the entire array through updateDocument.
6
- *
7
- * Why this exists: prompting an LLM to "add a CTA at the bottom of the home
8
- * page" via updateDocument forces it to send the whole layout array, which one
9
- * bad token can wipe out. patchLayout fetches the current array itself,
10
- * applies a scoped operation, and writes back the LLM only describes the
11
- * delta.
12
- *
13
- * Validation walks every block recursively against the introspected
14
- * BlockNestingMap, so arbitrarily-nested layouts work as long as each
15
- * `blocks`-typed field's content matches that field's allow list.
3
+ import { applyOperation, errorResponse, validateBlockList } from './_layout-helpers';
4
+ /**
5
+ * patchLayout surgical wrapper that mutates a single document's blocks-typed
6
+ * field directly without round-tripping the entire array through updateDocument.
7
+ *
8
+ * Why this exists: prompting an LLM to "add a CTA at the bottom of the home
9
+ * page" via updateDocument forces it to send the whole layout array, which one
10
+ * bad token can wipe out. patchLayout fetches the current array itself,
11
+ * applies a scoped operation, and writes back — the LLM only describes the
12
+ * delta.
13
+ *
14
+ * Validation walks every block recursively against the introspected
15
+ * BlockNestingMap, so arbitrarily-nested layouts work as long as each
16
+ * `blocks`-typed field's content matches that field's allow list.
16
17
  */ export function createPatchLayoutTool(catalog, nesting, draftCollections) {
17
18
  const allBlockSlugs = new Set(catalog.blocks.map((b)=>b.slug));
18
19
  // Lookups keyed by `<owner>:<fieldPath>` for O(1) access during recursive validation.
@@ -28,6 +29,10 @@ import { DRAFT_NOTE, errorMessage, getDocDisplayName, jsonResponse, stampMcpCont
28
29
  }
29
30
  return {
30
31
  name: 'patchLayout',
32
+ routing: {
33
+ kind: 'collection',
34
+ action: 'update'
35
+ },
31
36
  description: 'Surgically modify a document\'s blocks-typed field (e.g. "layout") without sending the whole array. Pass the blocks to add/replace plus an operation (append, prepend, insertAt, replaceAt, full). The current array is fetched server-side and the operation is applied atomically. Each block must include a `blockType` plus its fields; nested `blocks`-typed fields can contain arbitrarily-deep block arrays as long as each level matches the schema. Use the `blockNesting` resource to see which slugs each field accepts.',
32
37
  parameters: {
33
38
  collection: z.string().describe('The collection slug containing the document'),
@@ -105,92 +110,5 @@ import { DRAFT_NOTE, errorMessage, getDocDisplayName, jsonResponse, stampMcpCont
105
110
  }
106
111
  };
107
112
  }
108
- function errorResponse(message, extra) {
109
- return jsonResponse({
110
- success: false,
111
- error: message,
112
- ...extra ?? {}
113
- });
114
- }
115
- /**
116
- * Recursively validate a block array against an allow list, descending into
117
- * each block's own `blocks`-typed fields when present.
118
- */ function validateBlockList(blocks, allowedSlugs, pathLabel, allBlockSlugs, nestingByBlockField, errors) {
119
- for(let i = 0; i < blocks.length; i++){
120
- const block = blocks[i];
121
- const here = `${pathLabel}[${i}]`;
122
- if (!block || typeof block !== 'object') {
123
- errors.push(`${here}: not an object`);
124
- continue;
125
- }
126
- const slug = block.blockType;
127
- if (typeof slug !== 'string' || !slug) {
128
- errors.push(`${here}: missing string \`blockType\``);
129
- continue;
130
- }
131
- if (!allBlockSlugs.has(slug)) {
132
- errors.push(`${here}: unknown blockType "${slug}". Known: ${[
133
- ...allBlockSlugs
134
- ].join(', ')}`);
135
- continue;
136
- }
137
- if (!allowedSlugs.includes(slug)) {
138
- errors.push(`${here}: blockType "${slug}" not allowed here. Allowed at this position: ${allowedSlugs.join(', ') || '(none)'}`);
139
- continue;
140
- }
141
- // Any value that is itself an array of objects with `blockType` is treated
142
- // as a nested blocks field. The field name is cross-checked against the
143
- // nesting map to find the next-level allow list.
144
- for (const [fieldName, value] of Object.entries(block)){
145
- if (!Array.isArray(value)) continue;
146
- if (value.length === 0) continue;
147
- if (!value.every((v)=>v && typeof v === 'object' && 'blockType' in v)) continue;
148
- const nextKey = `${slug}:${fieldName}`;
149
- const nextAllowed = nestingByBlockField.get(nextKey);
150
- if (!nextAllowed) {
151
- errors.push(`${here}.${fieldName}: block "${slug}" has no blocks field named "${fieldName}" in the schema`);
152
- continue;
153
- }
154
- validateBlockList(value, nextAllowed, `${here}.${fieldName}`, allBlockSlugs, nestingByBlockField, errors);
155
- }
156
- }
157
- }
158
- /**
159
- * Apply a list operation against an existing array of blocks.
160
- * `full` always replaces; the rest preserve the existing array.
161
- */ function applyOperation(newBlocks, operation, insertIndex, existingLayout) {
162
- if (operation === 'full' || !existingLayout) {
163
- return newBlocks;
164
- }
165
- const existing = [
166
- ...existingLayout
167
- ];
168
- if (operation === 'append') return [
169
- ...existing,
170
- ...newBlocks
171
- ];
172
- if (operation === 'prepend') return [
173
- ...newBlocks,
174
- ...existing
175
- ];
176
- if (operation === 'insertAt') {
177
- if (insertIndex === undefined || insertIndex < 0 || insertIndex > existing.length) {
178
- return [
179
- ...existing,
180
- ...newBlocks
181
- ];
182
- }
183
- existing.splice(insertIndex, 0, ...newBlocks);
184
- return existing;
185
- }
186
- if (operation === 'replaceAt') {
187
- if (insertIndex === undefined || insertIndex < 0 || insertIndex >= existing.length) {
188
- return existing;
189
- }
190
- existing.splice(insertIndex, newBlocks.length, ...newBlocks);
191
- return existing;
192
- }
193
- return newBlocks;
194
- }
195
113
 
196
114
  //# sourceMappingURL=patch-layout.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/patch-layout.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { BlockCatalog, BlockNestingMap } from '../types'\nimport {\n DRAFT_NOTE,\n errorMessage,\n getDocDisplayName,\n jsonResponse,\n stampMcpContext,\n} from './_helpers'\n\n/**\n * patchLayout — surgical wrapper that mutates a single document's blocks-typed\n * field directly without round-tripping the entire array through updateDocument.\n *\n * Why this exists: prompting an LLM to \"add a CTA at the bottom of the home\n * page\" via updateDocument forces it to send the whole layout array, which one\n * bad token can wipe out. patchLayout fetches the current array itself,\n * applies a scoped operation, and writes back — the LLM only describes the\n * delta.\n *\n * Validation walks every block recursively against the introspected\n * BlockNestingMap, so arbitrarily-nested layouts work as long as each\n * `blocks`-typed field's content matches that field's allow list.\n */\nexport function createPatchLayoutTool(\n catalog: BlockCatalog,\n nesting: BlockNestingMap,\n draftCollections: Set<string>,\n) {\n const allBlockSlugs = new Set(catalog.blocks.map((b) => b.slug))\n\n // Lookups keyed by `<owner>:<fieldPath>` for O(1) access during recursive validation.\n const nestingByCollectionField = new Map<string, string[]>()\n const nestingByBlockField = new Map<string, string[]>()\n for (const edge of nesting) {\n const key = `${edge.owner}:${edge.fieldPath}`\n if (edge.ownerType === 'collection') {\n nestingByCollectionField.set(key, edge.acceptedBlockSlugs)\n } else {\n nestingByBlockField.set(key, edge.acceptedBlockSlugs)\n }\n }\n\n return {\n name: 'patchLayout',\n description:\n 'Surgically modify a document\\'s blocks-typed field (e.g. \"layout\") without sending the whole array. Pass the blocks to add/replace plus an operation (append, prepend, insertAt, replaceAt, full). The current array is fetched server-side and the operation is applied atomically. Each block must include a `blockType` plus its fields; nested `blocks`-typed fields can contain arbitrarily-deep block arrays as long as each level matches the schema. Use the `blockNesting` resource to see which slugs each field accepts.',\n parameters: {\n collection: z.string().describe('The collection slug containing the document'),\n documentId: z.string().describe('The ID of the document to patch'),\n layoutField: z\n .string()\n .optional()\n .default('layout')\n .describe('Name of the blocks-typed field to patch (default \"layout\")'),\n blocks: z\n .array(z.record(z.string(), z.unknown()))\n .describe(\n 'Blocks to compose. Each must have a `blockType` discriminator plus any block-specific fields. Nested blocks fields hold their own `blocks` arrays at any depth.',\n ),\n operation: z\n .enum(['append', 'prepend', 'insertAt', 'replaceAt', 'full'])\n .describe(\n 'How to apply the blocks: append (end), prepend (start), insertAt (at index), replaceAt (overwrite N starting at index), full (replace entire array — use with care).',\n ),\n insertIndex: z\n .number()\n .optional()\n .describe('Index for insertAt/replaceAt operations'),\n },\n handler: async (\n args: {\n collection: string\n documentId: string\n layoutField?: string\n blocks: Array<Record<string, unknown>>\n operation: 'append' | 'prepend' | 'insertAt' | 'replaceAt' | 'full'\n insertIndex?: number\n },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const {\n collection,\n documentId,\n layoutField = 'layout',\n blocks,\n operation,\n insertIndex,\n } = args\n\n const rootKey = `${collection}:${layoutField}`\n const rootAllowed = nestingByCollectionField.get(rootKey)\n if (!rootAllowed) {\n return errorResponse(\n `Field \"${layoutField}\" on collection \"${collection}\" is not a blocks-typed field, or no nesting map entry exists for it.`,\n { availableFields: [...nestingByCollectionField.keys()] },\n )\n }\n\n const errors: string[] = []\n validateBlockList(blocks, rootAllowed, layoutField, allBlockSlugs, nestingByBlockField, errors)\n\n if (errors.length > 0) {\n return errorResponse('Block validation failed.', { errors })\n }\n\n stampMcpContext(req)\n\n let existing: any\n try {\n existing = await req.payload.findByID({\n collection: collection as any,\n id: documentId,\n depth: 0,\n draft: true,\n req,\n overrideAccess: false,\n user: req.user,\n })\n } catch (error) {\n return errorResponse(`Error fetching ${collection}#${documentId}: ${errorMessage(error)}`)\n }\n\n const currentLayout = Array.isArray(existing?.[layoutField]) ? existing[layoutField] : []\n const finalLayout = applyOperation(blocks, operation, insertIndex, currentLayout)\n\n const isDraftCollection = draftCollections.has(collection)\n\n try {\n const updated = await req.payload.update({\n collection: collection as any,\n id: documentId,\n data: { [layoutField]: finalLayout } as any,\n draft: isDraftCollection,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const displayName = getDocDisplayName(updated, documentId)\n const draftNote = isDraftCollection ? DRAFT_NOTE : ''\n\n return jsonResponse({\n success: true,\n message:\n `Patched ${layoutField} on \"${displayName}\" (${collection}#${documentId}). ` +\n `Operation: ${operation}. Block count: ${finalLayout.length}.` +\n draftNote,\n blockCount: finalLayout.length,\n operation,\n })\n } catch (error) {\n return errorResponse(`Error patching ${collection}#${documentId}: ${errorMessage(error)}`)\n }\n },\n }\n}\n\nfunction errorResponse(message: string, extra?: Record<string, unknown>) {\n return jsonResponse({ success: false, error: message, ...(extra ?? {}) })\n}\n\n/**\n * Recursively validate a block array against an allow list, descending into\n * each block's own `blocks`-typed fields when present.\n */\nfunction validateBlockList(\n blocks: Array<Record<string, unknown>>,\n allowedSlugs: string[],\n pathLabel: string,\n allBlockSlugs: Set<string>,\n nestingByBlockField: Map<string, string[]>,\n errors: string[],\n) {\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i]\n const here = `${pathLabel}[${i}]`\n\n if (!block || typeof block !== 'object') {\n errors.push(`${here}: not an object`)\n continue\n }\n\n const slug = block.blockType\n if (typeof slug !== 'string' || !slug) {\n errors.push(`${here}: missing string \\`blockType\\``)\n continue\n }\n\n if (!allBlockSlugs.has(slug)) {\n errors.push(`${here}: unknown blockType \"${slug}\". Known: ${[...allBlockSlugs].join(', ')}`)\n continue\n }\n\n if (!allowedSlugs.includes(slug)) {\n errors.push(\n `${here}: blockType \"${slug}\" not allowed here. Allowed at this position: ${allowedSlugs.join(', ') || '(none)'}`,\n )\n continue\n }\n\n // Any value that is itself an array of objects with `blockType` is treated\n // as a nested blocks field. The field name is cross-checked against the\n // nesting map to find the next-level allow list.\n for (const [fieldName, value] of Object.entries(block)) {\n if (!Array.isArray(value)) continue\n if (value.length === 0) continue\n if (!value.every((v) => v && typeof v === 'object' && 'blockType' in v)) continue\n\n const nextKey = `${slug}:${fieldName}`\n const nextAllowed = nestingByBlockField.get(nextKey)\n if (!nextAllowed) {\n errors.push(\n `${here}.${fieldName}: block \"${slug}\" has no blocks field named \"${fieldName}\" in the schema`,\n )\n continue\n }\n\n validateBlockList(\n value as Array<Record<string, unknown>>,\n nextAllowed,\n `${here}.${fieldName}`,\n allBlockSlugs,\n nestingByBlockField,\n errors,\n )\n }\n }\n}\n\n/**\n * Apply a list operation against an existing array of blocks.\n * `full` always replaces; the rest preserve the existing array.\n */\nfunction applyOperation(\n newBlocks: Record<string, unknown>[],\n operation: 'full' | 'append' | 'prepend' | 'insertAt' | 'replaceAt',\n insertIndex: number | undefined,\n existingLayout: Record<string, unknown>[] | undefined,\n): Record<string, unknown>[] {\n if (operation === 'full' || !existingLayout) {\n return newBlocks\n }\n\n const existing = [...existingLayout]\n\n if (operation === 'append') return [...existing, ...newBlocks]\n if (operation === 'prepend') return [...newBlocks, ...existing]\n\n if (operation === 'insertAt') {\n if (insertIndex === undefined || insertIndex < 0 || insertIndex > existing.length) {\n return [...existing, ...newBlocks]\n }\n existing.splice(insertIndex, 0, ...newBlocks)\n return existing\n }\n\n if (operation === 'replaceAt') {\n if (insertIndex === undefined || insertIndex < 0 || insertIndex >= existing.length) {\n return existing\n }\n existing.splice(insertIndex, newBlocks.length, ...newBlocks)\n return existing\n }\n\n return newBlocks\n}\n"],"names":["z","DRAFT_NOTE","errorMessage","getDocDisplayName","jsonResponse","stampMcpContext","createPatchLayoutTool","catalog","nesting","draftCollections","allBlockSlugs","Set","blocks","map","b","slug","nestingByCollectionField","Map","nestingByBlockField","edge","key","owner","fieldPath","ownerType","set","acceptedBlockSlugs","name","description","parameters","collection","string","describe","documentId","layoutField","optional","default","array","record","unknown","operation","enum","insertIndex","number","handler","args","req","_extra","rootKey","rootAllowed","get","errorResponse","availableFields","keys","errors","validateBlockList","length","existing","payload","findByID","id","depth","draft","overrideAccess","user","error","currentLayout","Array","isArray","finalLayout","applyOperation","isDraftCollection","has","updated","update","data","displayName","draftNote","success","message","blockCount","extra","allowedSlugs","pathLabel","i","block","here","push","blockType","join","includes","fieldName","value","Object","entries","every","v","nextKey","nextAllowed","newBlocks","existingLayout","undefined","splice"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,UAAU,EACVC,YAAY,EACZC,iBAAiB,EACjBC,YAAY,EACZC,eAAe,QACV,aAAY;AAEnB;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,sBACdC,OAAqB,EACrBC,OAAwB,EACxBC,gBAA6B;IAE7B,MAAMC,gBAAgB,IAAIC,IAAIJ,QAAQK,MAAM,CAACC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI;IAE9D,sFAAsF;IACtF,MAAMC,2BAA2B,IAAIC;IACrC,MAAMC,sBAAsB,IAAID;IAChC,KAAK,MAAME,QAAQX,QAAS;QAC1B,MAAMY,MAAM,GAAGD,KAAKE,KAAK,CAAC,CAAC,EAAEF,KAAKG,SAAS,EAAE;QAC7C,IAAIH,KAAKI,SAAS,KAAK,cAAc;YACnCP,yBAAyBQ,GAAG,CAACJ,KAAKD,KAAKM,kBAAkB;QAC3D,OAAO;YACLP,oBAAoBM,GAAG,CAACJ,KAAKD,KAAKM,kBAAkB;QACtD;IACF;IAEA,OAAO;QACLC,MAAM;QACNC,aACE;QACFC,YAAY;YACVC,YAAY7B,EAAE8B,MAAM,GAAGC,QAAQ,CAAC;YAChCC,YAAYhC,EAAE8B,MAAM,GAAGC,QAAQ,CAAC;YAChCE,aAAajC,EACV8B,MAAM,GACNI,QAAQ,GACRC,OAAO,CAAC,UACRJ,QAAQ,CAAC;YACZnB,QAAQZ,EACLoC,KAAK,CAACpC,EAAEqC,MAAM,CAACrC,EAAE8B,MAAM,IAAI9B,EAAEsC,OAAO,KACpCP,QAAQ,CACP;YAEJQ,WAAWvC,EACRwC,IAAI,CAAC;gBAAC;gBAAU;gBAAW;gBAAY;gBAAa;aAAO,EAC3DT,QAAQ,CACP;YAEJU,aAAazC,EACV0C,MAAM,GACNR,QAAQ,GACRH,QAAQ,CAAC;QACd;QACAY,SAAS,OACPC,MAQAC,KACAC;YAEA,MAAM,EACJjB,UAAU,EACVG,UAAU,EACVC,cAAc,QAAQ,EACtBrB,MAAM,EACN2B,SAAS,EACTE,WAAW,EACZ,GAAGG;YAEJ,MAAMG,UAAU,GAAGlB,WAAW,CAAC,EAAEI,aAAa;YAC9C,MAAMe,cAAchC,yBAAyBiC,GAAG,CAACF;YACjD,IAAI,CAACC,aAAa;gBAChB,OAAOE,cACL,CAAC,OAAO,EAAEjB,YAAY,iBAAiB,EAAEJ,WAAW,qEAAqE,CAAC,EAC1H;oBAAEsB,iBAAiB;2BAAInC,yBAAyBoC,IAAI;qBAAG;gBAAC;YAE5D;YAEA,MAAMC,SAAmB,EAAE;YAC3BC,kBAAkB1C,QAAQoC,aAAaf,aAAavB,eAAeQ,qBAAqBmC;YAExF,IAAIA,OAAOE,MAAM,GAAG,GAAG;gBACrB,OAAOL,cAAc,4BAA4B;oBAAEG;gBAAO;YAC5D;YAEAhD,gBAAgBwC;YAEhB,IAAIW;YACJ,IAAI;gBACFA,WAAW,MAAMX,IAAIY,OAAO,CAACC,QAAQ,CAAC;oBACpC7B,YAAYA;oBACZ8B,IAAI3B;oBACJ4B,OAAO;oBACPC,OAAO;oBACPhB;oBACAiB,gBAAgB;oBAChBC,MAAMlB,IAAIkB,IAAI;gBAChB;YACF,EAAE,OAAOC,OAAO;gBACd,OAAOd,cAAc,CAAC,eAAe,EAAErB,WAAW,CAAC,EAAEG,WAAW,EAAE,EAAE9B,aAAa8D,QAAQ;YAC3F;YAEA,MAAMC,gBAAgBC,MAAMC,OAAO,CAACX,UAAU,CAACvB,YAAY,IAAIuB,QAAQ,CAACvB,YAAY,GAAG,EAAE;YACzF,MAAMmC,cAAcC,eAAezD,QAAQ2B,WAAWE,aAAawB;YAEnE,MAAMK,oBAAoB7D,iBAAiB8D,GAAG,CAAC1C;YAE/C,IAAI;gBACF,MAAM2C,UAAU,MAAM3B,IAAIY,OAAO,CAACgB,MAAM,CAAC;oBACvC5C,YAAYA;oBACZ8B,IAAI3B;oBACJ0C,MAAM;wBAAE,CAACzC,YAAY,EAAEmC;oBAAY;oBACnCP,OAAOS;oBACPzB;oBACAiB,gBAAgB;oBAChBC,MAAMlB,IAAIkB,IAAI;gBAChB;gBAEA,MAAMY,cAAcxE,kBAAkBqE,SAASxC;gBAC/C,MAAM4C,YAAYN,oBAAoBrE,aAAa;gBAEnD,OAAOG,aAAa;oBAClByE,SAAS;oBACTC,SACE,CAAC,QAAQ,EAAE7C,YAAY,KAAK,EAAE0C,YAAY,GAAG,EAAE9C,WAAW,CAAC,EAAEG,WAAW,GAAG,CAAC,GAC5E,CAAC,WAAW,EAAEO,UAAU,eAAe,EAAE6B,YAAYb,MAAM,CAAC,CAAC,CAAC,GAC9DqB;oBACFG,YAAYX,YAAYb,MAAM;oBAC9BhB;gBACF;YACF,EAAE,OAAOyB,OAAO;gBACd,OAAOd,cAAc,CAAC,eAAe,EAAErB,WAAW,CAAC,EAAEG,WAAW,EAAE,EAAE9B,aAAa8D,QAAQ;YAC3F;QACF;IACF;AACF;AAEA,SAASd,cAAc4B,OAAe,EAAEE,KAA+B;IACrE,OAAO5E,aAAa;QAAEyE,SAAS;QAAOb,OAAOc;QAAS,GAAIE,SAAS,CAAC,CAAC;IAAE;AACzE;AAEA;;;CAGC,GACD,SAAS1B,kBACP1C,MAAsC,EACtCqE,YAAsB,EACtBC,SAAiB,EACjBxE,aAA0B,EAC1BQ,mBAA0C,EAC1CmC,MAAgB;IAEhB,IAAK,IAAI8B,IAAI,GAAGA,IAAIvE,OAAO2C,MAAM,EAAE4B,IAAK;QACtC,MAAMC,QAAQxE,MAAM,CAACuE,EAAE;QACvB,MAAME,OAAO,GAAGH,UAAU,CAAC,EAAEC,EAAE,CAAC,CAAC;QAEjC,IAAI,CAACC,SAAS,OAAOA,UAAU,UAAU;YACvC/B,OAAOiC,IAAI,CAAC,GAAGD,KAAK,eAAe,CAAC;YACpC;QACF;QAEA,MAAMtE,OAAOqE,MAAMG,SAAS;QAC5B,IAAI,OAAOxE,SAAS,YAAY,CAACA,MAAM;YACrCsC,OAAOiC,IAAI,CAAC,GAAGD,KAAK,8BAA8B,CAAC;YACnD;QACF;QAEA,IAAI,CAAC3E,cAAc6D,GAAG,CAACxD,OAAO;YAC5BsC,OAAOiC,IAAI,CAAC,GAAGD,KAAK,qBAAqB,EAAEtE,KAAK,UAAU,EAAE;mBAAIL;aAAc,CAAC8E,IAAI,CAAC,OAAO;YAC3F;QACF;QAEA,IAAI,CAACP,aAAaQ,QAAQ,CAAC1E,OAAO;YAChCsC,OAAOiC,IAAI,CACT,GAAGD,KAAK,aAAa,EAAEtE,KAAK,8CAA8C,EAAEkE,aAAaO,IAAI,CAAC,SAAS,UAAU;YAEnH;QACF;QAEA,2EAA2E;QAC3E,wEAAwE;QACxE,iDAAiD;QACjD,KAAK,MAAM,CAACE,WAAWC,MAAM,IAAIC,OAAOC,OAAO,CAACT,OAAQ;YACtD,IAAI,CAAClB,MAAMC,OAAO,CAACwB,QAAQ;YAC3B,IAAIA,MAAMpC,MAAM,KAAK,GAAG;YACxB,IAAI,CAACoC,MAAMG,KAAK,CAAC,CAACC,IAAMA,KAAK,OAAOA,MAAM,YAAY,eAAeA,IAAI;YAEzE,MAAMC,UAAU,GAAGjF,KAAK,CAAC,EAAE2E,WAAW;YACtC,MAAMO,cAAc/E,oBAAoB+B,GAAG,CAAC+C;YAC5C,IAAI,CAACC,aAAa;gBAChB5C,OAAOiC,IAAI,CACT,GAAGD,KAAK,CAAC,EAAEK,UAAU,SAAS,EAAE3E,KAAK,6BAA6B,EAAE2E,UAAU,eAAe,CAAC;gBAEhG;YACF;YAEApC,kBACEqC,OACAM,aACA,GAAGZ,KAAK,CAAC,EAAEK,WAAW,EACtBhF,eACAQ,qBACAmC;QAEJ;IACF;AACF;AAEA;;;CAGC,GACD,SAASgB,eACP6B,SAAoC,EACpC3D,SAAmE,EACnEE,WAA+B,EAC/B0D,cAAqD;IAErD,IAAI5D,cAAc,UAAU,CAAC4D,gBAAgB;QAC3C,OAAOD;IACT;IAEA,MAAM1C,WAAW;WAAI2C;KAAe;IAEpC,IAAI5D,cAAc,UAAU,OAAO;WAAIiB;WAAa0C;KAAU;IAC9D,IAAI3D,cAAc,WAAW,OAAO;WAAI2D;WAAc1C;KAAS;IAE/D,IAAIjB,cAAc,YAAY;QAC5B,IAAIE,gBAAgB2D,aAAa3D,cAAc,KAAKA,cAAce,SAASD,MAAM,EAAE;YACjF,OAAO;mBAAIC;mBAAa0C;aAAU;QACpC;QACA1C,SAAS6C,MAAM,CAAC5D,aAAa,MAAMyD;QACnC,OAAO1C;IACT;IAEA,IAAIjB,cAAc,aAAa;QAC7B,IAAIE,gBAAgB2D,aAAa3D,cAAc,KAAKA,eAAee,SAASD,MAAM,EAAE;YAClF,OAAOC;QACT;QACAA,SAAS6C,MAAM,CAAC5D,aAAayD,UAAU3C,MAAM,KAAK2C;QAClD,OAAO1C;IACT;IAEA,OAAO0C;AACT"}
1
+ {"version":3,"sources":["../../src/tools/patch-layout.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport type { BlockCatalog, BlockNestingMap } from '../types'\r\nimport {\r\n DRAFT_NOTE,\r\n errorMessage,\r\n getDocDisplayName,\r\n jsonResponse,\r\n stampMcpContext,\r\n} from './_helpers'\r\nimport { applyOperation, errorResponse, validateBlockList } from './_layout-helpers'\r\n\r\n/**\r\n * patchLayout — surgical wrapper that mutates a single document's blocks-typed\r\n * field directly without round-tripping the entire array through updateDocument.\r\n *\r\n * Why this exists: prompting an LLM to \"add a CTA at the bottom of the home\r\n * page\" via updateDocument forces it to send the whole layout array, which one\r\n * bad token can wipe out. patchLayout fetches the current array itself,\r\n * applies a scoped operation, and writes back — the LLM only describes the\r\n * delta.\r\n *\r\n * Validation walks every block recursively against the introspected\r\n * BlockNestingMap, so arbitrarily-nested layouts work as long as each\r\n * `blocks`-typed field's content matches that field's allow list.\r\n */\r\nexport function createPatchLayoutTool(\r\n catalog: BlockCatalog,\r\n nesting: BlockNestingMap,\r\n draftCollections: Set<string>,\r\n) {\r\n const allBlockSlugs = new Set(catalog.blocks.map((b) => b.slug))\r\n\r\n // Lookups keyed by `<owner>:<fieldPath>` for O(1) access during recursive validation.\r\n const nestingByCollectionField = new Map<string, string[]>()\r\n const nestingByBlockField = new Map<string, string[]>()\r\n for (const edge of nesting) {\r\n const key = `${edge.owner}:${edge.fieldPath}`\r\n if (edge.ownerType === 'collection') {\r\n nestingByCollectionField.set(key, edge.acceptedBlockSlugs)\r\n } else {\r\n nestingByBlockField.set(key, edge.acceptedBlockSlugs)\r\n }\r\n }\r\n\r\n return {\r\n name: 'patchLayout',\r\n routing: { kind: 'collection', action: 'update' } as const,\r\n description:\r\n 'Surgically modify a document\\'s blocks-typed field (e.g. \"layout\") without sending the whole array. Pass the blocks to add/replace plus an operation (append, prepend, insertAt, replaceAt, full). The current array is fetched server-side and the operation is applied atomically. Each block must include a `blockType` plus its fields; nested `blocks`-typed fields can contain arbitrarily-deep block arrays as long as each level matches the schema. Use the `blockNesting` resource to see which slugs each field accepts.',\r\n parameters: {\r\n collection: z.string().describe('The collection slug containing the document'),\r\n documentId: z.string().describe('The ID of the document to patch'),\r\n layoutField: z\r\n .string()\r\n .optional()\r\n .default('layout')\r\n .describe('Name of the blocks-typed field to patch (default \"layout\")'),\r\n blocks: z\r\n .array(z.record(z.string(), z.unknown()))\r\n .describe(\r\n 'Blocks to compose. Each must have a `blockType` discriminator plus any block-specific fields. Nested blocks fields hold their own `blocks` arrays at any depth.',\r\n ),\r\n operation: z\r\n .enum(['append', 'prepend', 'insertAt', 'replaceAt', 'full'])\r\n .describe(\r\n 'How to apply the blocks: append (end), prepend (start), insertAt (at index), replaceAt (overwrite N starting at index), full (replace entire array — use with care).',\r\n ),\r\n insertIndex: z\r\n .number()\r\n .optional()\r\n .describe('Index for insertAt/replaceAt operations'),\r\n },\r\n handler: async (\r\n args: Record<string, unknown>,\r\n req: PayloadRequest,\r\n _extra: unknown,\r\n ) => {\r\n const {\r\n collection,\r\n documentId,\r\n layoutField = 'layout',\r\n blocks,\r\n operation,\r\n insertIndex,\r\n } = args as {\r\n collection: string\r\n documentId: string\r\n layoutField?: string\r\n blocks: Array<Record<string, unknown>>\r\n operation: 'append' | 'prepend' | 'insertAt' | 'replaceAt' | 'full'\r\n insertIndex?: number\r\n }\r\n\r\n const rootKey = `${collection}:${layoutField}`\r\n const rootAllowed = nestingByCollectionField.get(rootKey)\r\n if (!rootAllowed) {\r\n return errorResponse(\r\n `Field \"${layoutField}\" on collection \"${collection}\" is not a blocks-typed field, or no nesting map entry exists for it.`,\r\n { availableFields: [...nestingByCollectionField.keys()] },\r\n )\r\n }\r\n\r\n const errors: string[] = []\r\n validateBlockList(blocks, rootAllowed, layoutField, allBlockSlugs, nestingByBlockField, errors)\r\n\r\n if (errors.length > 0) {\r\n return errorResponse('Block validation failed.', { errors })\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n let existing: any\r\n try {\r\n existing = await req.payload.findByID({\r\n collection: collection as any,\r\n id: documentId,\r\n depth: 0,\r\n draft: true,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n } catch (error) {\r\n return errorResponse(`Error fetching ${collection}#${documentId}: ${errorMessage(error)}`)\r\n }\r\n\r\n const currentLayout = Array.isArray(existing?.[layoutField]) ? existing[layoutField] : []\r\n const finalLayout = applyOperation(blocks, operation, insertIndex, currentLayout)\r\n\r\n const isDraftCollection = draftCollections.has(collection)\r\n\r\n try {\r\n const updated = await req.payload.update({\r\n collection: collection as any,\r\n id: documentId,\r\n data: { [layoutField]: finalLayout } as any,\r\n draft: isDraftCollection,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n\r\n const displayName = getDocDisplayName(updated, documentId)\r\n const draftNote = isDraftCollection ? DRAFT_NOTE : ''\r\n\r\n return jsonResponse({\r\n success: true,\r\n message:\r\n `Patched ${layoutField} on \"${displayName}\" (${collection}#${documentId}). ` +\r\n `Operation: ${operation}. Block count: ${finalLayout.length}.` +\r\n draftNote,\r\n blockCount: finalLayout.length,\r\n operation,\r\n })\r\n } catch (error) {\r\n return errorResponse(`Error patching ${collection}#${documentId}: ${errorMessage(error)}`)\r\n }\r\n },\r\n }\r\n}\r\n\r\n"],"names":["z","DRAFT_NOTE","errorMessage","getDocDisplayName","jsonResponse","stampMcpContext","applyOperation","errorResponse","validateBlockList","createPatchLayoutTool","catalog","nesting","draftCollections","allBlockSlugs","Set","blocks","map","b","slug","nestingByCollectionField","Map","nestingByBlockField","edge","key","owner","fieldPath","ownerType","set","acceptedBlockSlugs","name","routing","kind","action","description","parameters","collection","string","describe","documentId","layoutField","optional","default","array","record","unknown","operation","enum","insertIndex","number","handler","args","req","_extra","rootKey","rootAllowed","get","availableFields","keys","errors","length","existing","payload","findByID","id","depth","draft","overrideAccess","user","error","currentLayout","Array","isArray","finalLayout","isDraftCollection","has","updated","update","data","displayName","draftNote","success","message","blockCount"],"mappings":"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"}
@@ -2,13 +2,14 @@ import { z } from 'zod';
2
2
  import type { PayloadRequest } from 'payload';
3
3
  export declare function createPublishDraftTool(draftCollections: Set<string>): {
4
4
  name: string;
5
+ routing: {
6
+ readonly kind: "collection";
7
+ readonly action: "update";
8
+ };
5
9
  description: string;
6
10
  parameters: {
7
11
  collection: z.ZodString;
8
12
  documentId: z.ZodString;
9
13
  };
10
- handler: (args: {
11
- collection: string;
12
- documentId: string;
13
- }, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
14
+ handler: (rawArgs: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
14
15
  };
@@ -3,6 +3,10 @@ import { errorMessage, getDocDisplayName, requireDraftCollection, stampMcpContex
3
3
  export function createPublishDraftTool(draftCollections) {
4
4
  return {
5
5
  name: 'publishDraft',
6
+ routing: {
7
+ kind: 'collection',
8
+ action: 'update'
9
+ },
6
10
  description: 'Publish a draft document by transitioning its _status from "draft" to "published". ' + 'Only works on collections that support drafts. Use after creating or editing content ' + 'to make it live on the site.',
7
11
  parameters: {
8
12
  collection: z.string().describe(`The collection slug. Draft-enabled collections: ${[
@@ -10,7 +14,8 @@ export function createPublishDraftTool(draftCollections) {
10
14
  ].join(', ') || 'none'}`),
11
15
  documentId: z.string().describe('The ID of the document to publish')
12
16
  },
13
- handler: async (args, req, _extra)=>{
17
+ handler: async (rawArgs, req, _extra)=>{
18
+ const args = rawArgs;
14
19
  const { collection, documentId } = args;
15
20
  const guard = requireDraftCollection(collection, draftCollections);
16
21
  if (guard) return guard;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/publish-draft.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport {\n errorMessage,\n getDocDisplayName,\n requireDraftCollection,\n stampMcpContext,\n textResponse,\n} from './_helpers'\n\nexport function createPublishDraftTool(draftCollections: Set<string>) {\n return {\n name: 'publishDraft',\n description:\n 'Publish a draft document by transitioning its _status from \"draft\" to \"published\". ' +\n 'Only works on collections that support drafts. Use after creating or editing content ' +\n 'to make it live on the site.',\n parameters: {\n collection: z\n .string()\n .describe(\n `The collection slug. Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\n ),\n documentId: z.string().describe('The ID of the document to publish'),\n },\n handler: async (\n args: { collection: string; documentId: string },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const { collection, documentId } = args\n\n const guard = requireDraftCollection(collection, draftCollections)\n if (guard) return guard\n\n stampMcpContext(req)\n\n try {\n const doc = await req.payload.update({\n collection: collection as any,\n id: documentId,\n data: { _status: 'published' } as any,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const displayName = getDocDisplayName(doc, documentId)\n return textResponse(\n `Successfully published \"${displayName}\" in ${collection} (ID: ${documentId}).`,\n )\n } catch (error) {\n return textResponse(\n `Error publishing document ${documentId} in ${collection}: ${errorMessage(error)}`,\n )\n }\n },\n }\n}\n"],"names":["z","errorMessage","getDocDisplayName","requireDraftCollection","stampMcpContext","textResponse","createPublishDraftTool","draftCollections","name","description","parameters","collection","string","describe","join","documentId","handler","args","req","_extra","guard","doc","payload","update","id","data","_status","overrideAccess","user","displayName","error"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAEvB,SACEC,YAAY,EACZC,iBAAiB,EACjBC,sBAAsB,EACtBC,eAAe,EACfC,YAAY,QACP,aAAY;AAEnB,OAAO,SAASC,uBAAuBC,gBAA6B;IAClE,OAAO;QACLC,MAAM;QACNC,aACE,wFACA,0FACA;QACFC,YAAY;YACVC,YAAYX,EACTY,MAAM,GACNC,QAAQ,CACP,CAAC,gDAAgD,EAAE;mBAAIN;aAAiB,CAACO,IAAI,CAAC,SAAS,QAAQ;YAEnGC,YAAYf,EAAEY,MAAM,GAAGC,QAAQ,CAAC;QAClC;QACAG,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAER,UAAU,EAAEI,UAAU,EAAE,GAAGE;YAEnC,MAAMG,QAAQjB,uBAAuBQ,YAAYJ;YACjD,IAAIa,OAAO,OAAOA;YAElBhB,gBAAgBc;YAEhB,IAAI;gBACF,MAAMG,MAAM,MAAMH,IAAII,OAAO,CAACC,MAAM,CAAC;oBACnCZ,YAAYA;oBACZa,IAAIT;oBACJU,MAAM;wBAAEC,SAAS;oBAAY;oBAC7BR;oBACAS,gBAAgB;oBAChBC,MAAMV,IAAIU,IAAI;gBAChB;gBAEA,MAAMC,cAAc3B,kBAAkBmB,KAAKN;gBAC3C,OAAOV,aACL,CAAC,wBAAwB,EAAEwB,YAAY,KAAK,EAAElB,WAAW,MAAM,EAAEI,WAAW,EAAE,CAAC;YAEnF,EAAE,OAAOe,OAAO;gBACd,OAAOzB,aACL,CAAC,0BAA0B,EAAEU,WAAW,IAAI,EAAEJ,WAAW,EAAE,EAAEV,aAAa6B,QAAQ;YAEtF;QACF;IACF;AACF"}
1
+ {"version":3,"sources":["../../src/tools/publish-draft.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport {\r\n errorMessage,\r\n getDocDisplayName,\r\n requireDraftCollection,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\nexport function createPublishDraftTool(draftCollections: Set<string>) {\r\n return {\r\n name: 'publishDraft',\r\n routing: { kind: 'collection', action: 'update' } as const,\r\n description:\r\n 'Publish a draft document by transitioning its _status from \"draft\" to \"published\". ' +\r\n 'Only works on collections that support drafts. Use after creating or editing content ' +\r\n 'to make it live on the site.',\r\n parameters: {\r\n collection: z\r\n .string()\r\n .describe(\r\n `The collection slug. Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\r\n ),\r\n documentId: z.string().describe('The ID of the document to publish'),\r\n },\r\n handler: async (\r\n rawArgs: Record<string, unknown>,\r\n req: PayloadRequest,\r\n _extra: unknown,\r\n ) => {\r\n const args = rawArgs as { collection: string; documentId: string }\r\n const { collection, documentId } = args\r\n\r\n const guard = requireDraftCollection(collection, draftCollections)\r\n if (guard) return guard\r\n\r\n stampMcpContext(req)\r\n\r\n try {\r\n const doc = await req.payload.update({\r\n collection: collection as any,\r\n id: documentId,\r\n data: { _status: 'published' } as any,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n\r\n const displayName = getDocDisplayName(doc, documentId)\r\n return textResponse(\r\n `Successfully published \"${displayName}\" in ${collection} (ID: ${documentId}).`,\r\n )\r\n } catch (error) {\r\n return textResponse(\r\n `Error publishing document ${documentId} in ${collection}: ${errorMessage(error)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","errorMessage","getDocDisplayName","requireDraftCollection","stampMcpContext","textResponse","createPublishDraftTool","draftCollections","name","routing","kind","action","description","parameters","collection","string","describe","join","documentId","handler","rawArgs","req","_extra","args","guard","doc","payload","update","id","data","_status","overrideAccess","user","displayName","error"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAEvB,SACEC,YAAY,EACZC,iBAAiB,EACjBC,sBAAsB,EACtBC,eAAe,EACfC,YAAY,QACP,aAAY;AAEnB,OAAO,SAASC,uBAAuBC,gBAA6B;IAClE,OAAO;QACLC,MAAM;QACNC,SAAS;YAAEC,MAAM;YAAcC,QAAQ;QAAS;QAChDC,aACE,wFACA,0FACA;QACFC,YAAY;YACVC,YAAYd,EACTe,MAAM,GACNC,QAAQ,CACP,CAAC,gDAAgD,EAAE;mBAAIT;aAAiB,CAACU,IAAI,CAAC,SAAS,QAAQ;YAEnGC,YAAYlB,EAAEe,MAAM,GAAGC,QAAQ,CAAC;QAClC;QACAG,SAAS,OACPC,SACAC,KACAC;YAEA,MAAMC,OAAOH;YACb,MAAM,EAAEN,UAAU,EAAEI,UAAU,EAAE,GAAGK;YAEnC,MAAMC,QAAQrB,uBAAuBW,YAAYP;YACjD,IAAIiB,OAAO,OAAOA;YAElBpB,gBAAgBiB;YAEhB,IAAI;gBACF,MAAMI,MAAM,MAAMJ,IAAIK,OAAO,CAACC,MAAM,CAAC;oBACnCb,YAAYA;oBACZc,IAAIV;oBACJW,MAAM;wBAAEC,SAAS;oBAAY;oBAC7BT;oBACAU,gBAAgB;oBAChBC,MAAMX,IAAIW,IAAI;gBAChB;gBAEA,MAAMC,cAAc/B,kBAAkBuB,KAAKP;gBAC3C,OAAOb,aACL,CAAC,wBAAwB,EAAE4B,YAAY,KAAK,EAAEnB,WAAW,MAAM,EAAEI,WAAW,EAAE,CAAC;YAEnF,EAAE,OAAOgB,OAAO;gBACd,OAAO7B,aACL,CAAC,0BAA0B,EAAEa,WAAW,IAAI,EAAEJ,WAAW,EAAE,EAAEb,aAAaiC,QAAQ;YAEtF;QACF;IACF;AACF"}
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import type { PayloadRequest } from 'payload';
3
+ /**
4
+ * Promote a draft-enabled global's pending draft to published. Mirrors
5
+ * publishDraft for collections. Returns null when no global has drafts
6
+ * enabled so the plugin entry can skip registration.
7
+ */
8
+ export declare function createPublishGlobalDraftTool(draftGlobals: Set<string>): {
9
+ name: string;
10
+ routing: {
11
+ readonly kind: "global";
12
+ readonly action: "update";
13
+ };
14
+ description: string;
15
+ parameters: {
16
+ slug: z.ZodEnum<[string, ...string[]]>;
17
+ locale: z.ZodOptional<z.ZodString>;
18
+ };
19
+ handler: (args: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
20
+ } | null;
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod';
2
+ import { errorMessage, slugEnum, stampMcpContext, textResponse } from './_helpers';
3
+ /**
4
+ * Promote a draft-enabled global's pending draft to published. Mirrors
5
+ * publishDraft for collections. Returns null when no global has drafts
6
+ * enabled so the plugin entry can skip registration.
7
+ */ export function createPublishGlobalDraftTool(draftGlobals) {
8
+ if (draftGlobals.size === 0) return null;
9
+ const slugs = [
10
+ ...draftGlobals
11
+ ];
12
+ return {
13
+ name: 'publishGlobalDraft',
14
+ routing: {
15
+ kind: 'global',
16
+ action: 'update'
17
+ },
18
+ description: 'Publish a draft-enabled global by transitioning its _status from "draft" to "published". ' + `Draft-enabled globals: ${slugs.join(', ')}`,
19
+ parameters: {
20
+ slug: slugEnum(slugs, 'global').describe(`The global slug. One of: ${slugs.join(', ')}`),
21
+ locale: z.string().optional().describe('Optional locale code (e.g. "en", "fr") for the publish operation.')
22
+ },
23
+ handler: async (args, req, _extra)=>{
24
+ const { slug, locale } = args;
25
+ if (!draftGlobals.has(slug)) {
26
+ return textResponse(`Error: Global "${slug}" does not support drafts. Draft-enabled globals: ${slugs.join(', ') || 'none'}`);
27
+ }
28
+ stampMcpContext(req);
29
+ try {
30
+ await req.payload.updateGlobal({
31
+ slug: slug,
32
+ data: {
33
+ _status: 'published'
34
+ },
35
+ ...locale ? {
36
+ locale: locale
37
+ } : {},
38
+ req,
39
+ overrideAccess: false,
40
+ user: req.user
41
+ });
42
+ return textResponse(`Successfully published global "${slug}".`);
43
+ } catch (err) {
44
+ return textResponse(`Error publishing global "${slug}": ${errorMessage(err)}`);
45
+ }
46
+ }
47
+ };
48
+ }
49
+
50
+ //# sourceMappingURL=publish-global-draft.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tools/publish-global-draft.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport { errorMessage, slugEnum, stampMcpContext, textResponse } from './_helpers'\r\n\r\n/**\r\n * Promote a draft-enabled global's pending draft to published. Mirrors\r\n * publishDraft for collections. Returns null when no global has drafts\r\n * enabled so the plugin entry can skip registration.\r\n */\r\nexport function createPublishGlobalDraftTool(draftGlobals: Set<string>) {\r\n if (draftGlobals.size === 0) return null\r\n\r\n const slugs = [...draftGlobals]\r\n return {\r\n name: 'publishGlobalDraft',\r\n routing: { kind: 'global', action: 'update' } as const,\r\n description:\r\n 'Publish a draft-enabled global by transitioning its _status from \"draft\" to \"published\". ' +\r\n `Draft-enabled globals: ${slugs.join(', ')}`,\r\n parameters: {\r\n slug: slugEnum(slugs, 'global').describe(`The global slug. One of: ${slugs.join(', ')}`),\r\n locale: z\r\n .string()\r\n .optional()\r\n .describe('Optional locale code (e.g. \"en\", \"fr\") for the publish operation.'),\r\n },\r\n handler: async (args: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => {\r\n const { slug, locale } = args as { slug: string; locale?: string }\r\n\r\n if (!draftGlobals.has(slug)) {\r\n return textResponse(\r\n `Error: Global \"${slug}\" does not support drafts. Draft-enabled globals: ${slugs.join(', ') || 'none'}`,\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n try {\r\n await req.payload.updateGlobal({\r\n slug: slug as never,\r\n data: { _status: 'published' } as never,\r\n ...(locale ? { locale: locale as never } : {}),\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n return textResponse(`Successfully published global \"${slug}\".`)\r\n } catch (err) {\r\n return textResponse(`Error publishing global \"${slug}\": ${errorMessage(err)}`)\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","errorMessage","slugEnum","stampMcpContext","textResponse","createPublishGlobalDraftTool","draftGlobals","size","slugs","name","routing","kind","action","description","join","parameters","slug","describe","locale","string","optional","handler","args","req","_extra","has","payload","updateGlobal","data","_status","overrideAccess","user","err"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAEvB,SAASC,YAAY,EAAEC,QAAQ,EAAEC,eAAe,EAAEC,YAAY,QAAQ,aAAY;AAElF;;;;CAIC,GACD,OAAO,SAASC,6BAA6BC,YAAyB;IACpE,IAAIA,aAAaC,IAAI,KAAK,GAAG,OAAO;IAEpC,MAAMC,QAAQ;WAAIF;KAAa;IAC/B,OAAO;QACLG,MAAM;QACNC,SAAS;YAAEC,MAAM;YAAUC,QAAQ;QAAS;QAC5CC,aACE,8FACA,CAAC,uBAAuB,EAAEL,MAAMM,IAAI,CAAC,OAAO;QAC9CC,YAAY;YACVC,MAAMd,SAASM,OAAO,UAAUS,QAAQ,CAAC,CAAC,yBAAyB,EAAET,MAAMM,IAAI,CAAC,OAAO;YACvFI,QAAQlB,EACLmB,MAAM,GACNC,QAAQ,GACRH,QAAQ,CAAC;QACd;QACAI,SAAS,OAAOC,MAA+BC,KAAqBC;YAClE,MAAM,EAAER,IAAI,EAAEE,MAAM,EAAE,GAAGI;YAEzB,IAAI,CAAChB,aAAamB,GAAG,CAACT,OAAO;gBAC3B,OAAOZ,aACL,CAAC,eAAe,EAAEY,KAAK,kDAAkD,EAAER,MAAMM,IAAI,CAAC,SAAS,QAAQ;YAE3G;YAEAX,gBAAgBoB;YAEhB,IAAI;gBACF,MAAMA,IAAIG,OAAO,CAACC,YAAY,CAAC;oBAC7BX,MAAMA;oBACNY,MAAM;wBAAEC,SAAS;oBAAY;oBAC7B,GAAIX,SAAS;wBAAEA,QAAQA;oBAAgB,IAAI,CAAC,CAAC;oBAC7CK;oBACAO,gBAAgB;oBAChBC,MAAMR,IAAIQ,IAAI;gBAChB;gBACA,OAAO3B,aAAa,CAAC,+BAA+B,EAAEY,KAAK,EAAE,CAAC;YAChE,EAAE,OAAOgB,KAAK;gBACZ,OAAO5B,aAAa,CAAC,yBAAyB,EAAEY,KAAK,GAAG,EAAEf,aAAa+B,MAAM;YAC/E;QACF;IACF;AACF"}
@@ -2,6 +2,10 @@ import { z } from 'zod';
2
2
  import type { PayloadRequest } from 'payload';
3
3
  export declare function createResolveReferenceTool(searchableCollections: Map<string, string[]>): {
4
4
  name: string;
5
+ routing: {
6
+ readonly kind: "account";
7
+ readonly action: "read";
8
+ };
5
9
  description: string;
6
10
  parameters: z.ZodObject<{
7
11
  query: z.ZodString;
@@ -13,8 +17,5 @@ export declare function createResolveReferenceTool(searchableCollections: Map<st
13
17
  query: string;
14
18
  collection?: string | undefined;
15
19
  }>;
16
- handler: (args: {
17
- query: string;
18
- collection?: string;
19
- }, req: PayloadRequest) => Promise<import("./_helpers").McpTextResponse>;
20
+ handler: (args: Record<string, unknown>, req: PayloadRequest) => Promise<import("./_helpers").McpTextResponse>;
20
21
  };
@@ -3,6 +3,10 @@ import { jsonResponse, stampMcpContext } from './_helpers';
3
3
  export function createResolveReferenceTool(searchableCollections) {
4
4
  return {
5
5
  name: 'resolveReference',
6
+ routing: {
7
+ kind: 'account',
8
+ action: 'read'
9
+ },
6
10
  description: 'Search for documents across collections by name, title, or slug. ' + 'Returns ranked candidates with IDs for use in relationship fields. ' + 'Optionally filter to a specific collection.',
7
11
  parameters: z.object({
8
12
  query: z.string().describe('Search term to match against name, title, or slug fields'),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/resolve-reference.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport { jsonResponse, stampMcpContext } from './_helpers'\n\ninterface MatchCandidate {\n collection: string\n id: string | number\n displayName: string\n matchedField: string\n matchType: 'exact-slug' | 'exact' | 'partial'\n}\n\nexport function createResolveReferenceTool(\n searchableCollections: Map<string, string[]>,\n) {\n return {\n name: 'resolveReference',\n description:\n 'Search for documents across collections by name, title, or slug. ' +\n 'Returns ranked candidates with IDs for use in relationship fields. ' +\n 'Optionally filter to a specific collection.',\n parameters: z.object({\n query: z.string().describe('Search term to match against name, title, or slug fields'),\n collection: z\n .string()\n .optional()\n .describe('Optional collection slug to restrict search to a single collection'),\n }),\n handler: async (args: { query: string; collection?: string }, req: PayloadRequest) => {\n stampMcpContext(req)\n\n const { query, collection } = args\n\n const collectionsToSearch = collection\n ? new Map(\n searchableCollections.has(collection)\n ? [[collection, searchableCollections.get(collection)!]]\n : [],\n )\n : searchableCollections\n\n if (collectionsToSearch.size === 0) {\n const available = Array.from(searchableCollections.keys()).join(', ')\n return jsonResponse({\n candidates: [],\n message: collection\n ? `Collection \"${collection}\" has no searchable fields or does not exist. Available searchable collections: ${available}`\n : 'No searchable collections found.',\n })\n }\n\n const targets = Array.from(collectionsToSearch.entries()).filter(\n ([, fields]) => fields.length > 0,\n )\n\n const settled = await Promise.allSettled(\n targets.map(([slug, fields]) => {\n const selectFields: Record<string, true> = {}\n for (const field of fields) selectFields[field] = true\n return req.payload.find({\n collection: slug as any,\n where: { or: fields.map((field) => ({ [field]: { like: query } })) },\n limit: 5,\n select: selectFields,\n req,\n overrideAccess: false,\n user: req.user,\n })\n }),\n )\n\n const allCandidates: MatchCandidate[] = []\n settled.forEach((outcome, i) => {\n if (outcome.status !== 'fulfilled') return\n const [slug, fields] = targets[i]\n for (const doc of outcome.value.docs) {\n allCandidates.push(...rankDocument(doc, slug, fields, query))\n }\n })\n\n allCandidates.sort(\n (a, b) => matchTypePriority(a.matchType) - matchTypePriority(b.matchType),\n )\n\n if (allCandidates.length === 0) {\n const searched = Array.from(collectionsToSearch.keys()).join(', ')\n const available = Array.from(searchableCollections.keys()).join(', ')\n return jsonResponse({\n candidates: {},\n message: `No results found for \"${query}\" in: ${searched}. Try a different spelling or search term. All searchable collections: ${available}`,\n })\n }\n\n const grouped: Record<string, Omit<MatchCandidate, 'collection'>[]> = {}\n for (const candidate of allCandidates) {\n const { collection: col, ...rest } = candidate\n if (!grouped[col]) grouped[col] = []\n grouped[col].push(rest)\n }\n\n return jsonResponse({\n candidates: grouped,\n total: allCandidates.length,\n })\n },\n }\n}\n\nfunction rankDocument(\n doc: Record<string, any>,\n collection: string,\n fields: string[],\n query: string,\n): MatchCandidate[] {\n const queryLower = query.toLowerCase()\n let bestMatch: MatchCandidate | null = null\n\n for (const field of fields) {\n const value = doc[field]\n if (typeof value !== 'string') continue\n\n const valueLower = value.toLowerCase()\n let matchType: MatchCandidate['matchType']\n\n if (field === 'slug' && valueLower === queryLower) {\n matchType = 'exact-slug'\n } else if (valueLower === queryLower) {\n matchType = 'exact'\n } else {\n matchType = 'partial'\n }\n\n if (\n !bestMatch ||\n matchTypePriority(matchType) < matchTypePriority(bestMatch.matchType)\n ) {\n bestMatch = {\n collection,\n id: doc.id,\n displayName: getDisplayName(doc, fields),\n matchedField: field,\n matchType,\n }\n }\n }\n\n return bestMatch ? [bestMatch] : []\n}\n\nfunction matchTypePriority(type: MatchCandidate['matchType']): number {\n switch (type) {\n case 'exact-slug':\n return 0\n case 'exact':\n return 1\n case 'partial':\n return 2\n }\n}\n\nfunction getDisplayName(doc: Record<string, any>, fields: string[]): string {\n for (const preferred of ['name', 'title', 'slug']) {\n if (fields.includes(preferred) && typeof doc[preferred] === 'string') {\n return doc[preferred]\n }\n }\n return String(doc.id)\n}\n"],"names":["z","jsonResponse","stampMcpContext","createResolveReferenceTool","searchableCollections","name","description","parameters","object","query","string","describe","collection","optional","handler","args","req","collectionsToSearch","Map","has","get","size","available","Array","from","keys","join","candidates","message","targets","entries","filter","fields","length","settled","Promise","allSettled","map","slug","selectFields","field","payload","find","where","or","like","limit","select","overrideAccess","user","allCandidates","forEach","outcome","i","status","doc","value","docs","push","rankDocument","sort","a","b","matchTypePriority","matchType","searched","grouped","candidate","col","rest","total","queryLower","toLowerCase","bestMatch","valueLower","id","displayName","getDisplayName","matchedField","type","preferred","includes","String"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/tools/resolve-reference.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport { jsonResponse, stampMcpContext } from './_helpers'\r\n\r\ninterface MatchCandidate {\r\n collection: string\r\n id: string | number\r\n displayName: string\r\n matchedField: string\r\n matchType: 'exact-slug' | 'exact' | 'partial'\r\n}\r\n\r\nexport function createResolveReferenceTool(\r\n searchableCollections: Map<string, string[]>,\r\n) {\r\n return {\r\n name: 'resolveReference',\r\n routing: { kind: 'account', action: 'read' } as const,\r\n description:\r\n 'Search for documents across collections by name, title, or slug. ' +\r\n 'Returns ranked candidates with IDs for use in relationship fields. ' +\r\n 'Optionally filter to a specific collection.',\r\n parameters: z.object({\r\n query: z.string().describe('Search term to match against name, title, or slug fields'),\r\n collection: z\r\n .string()\r\n .optional()\r\n .describe('Optional collection slug to restrict search to a single collection'),\r\n }),\r\n handler: async (args: Record<string, unknown>, req: PayloadRequest) => {\r\n stampMcpContext(req)\r\n\r\n const { query, collection } = args as { query: string; collection?: string }\r\n\r\n const collectionsToSearch = collection\r\n ? new Map(\r\n searchableCollections.has(collection)\r\n ? [[collection, searchableCollections.get(collection)!]]\r\n : [],\r\n )\r\n : searchableCollections\r\n\r\n if (collectionsToSearch.size === 0) {\r\n const available = Array.from(searchableCollections.keys()).join(', ')\r\n return jsonResponse({\r\n candidates: [],\r\n message: collection\r\n ? `Collection \"${collection}\" has no searchable fields or does not exist. Available searchable collections: ${available}`\r\n : 'No searchable collections found.',\r\n })\r\n }\r\n\r\n const targets = Array.from(collectionsToSearch.entries()).filter(\r\n ([, fields]) => fields.length > 0,\r\n )\r\n\r\n const settled = await Promise.allSettled(\r\n targets.map(([slug, fields]) => {\r\n const selectFields: Record<string, true> = {}\r\n for (const field of fields) selectFields[field] = true\r\n return req.payload.find({\r\n collection: slug as any,\r\n where: { or: fields.map((field) => ({ [field]: { like: query } })) },\r\n limit: 5,\r\n select: selectFields,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n }),\r\n )\r\n\r\n const allCandidates: MatchCandidate[] = []\r\n settled.forEach((outcome, i) => {\r\n if (outcome.status !== 'fulfilled') return\r\n const [slug, fields] = targets[i]\r\n for (const doc of outcome.value.docs) {\r\n allCandidates.push(...rankDocument(doc, slug, fields, query))\r\n }\r\n })\r\n\r\n allCandidates.sort(\r\n (a, b) => matchTypePriority(a.matchType) - matchTypePriority(b.matchType),\r\n )\r\n\r\n if (allCandidates.length === 0) {\r\n const searched = Array.from(collectionsToSearch.keys()).join(', ')\r\n const available = Array.from(searchableCollections.keys()).join(', ')\r\n return jsonResponse({\r\n candidates: {},\r\n message: `No results found for \"${query}\" in: ${searched}. Try a different spelling or search term. All searchable collections: ${available}`,\r\n })\r\n }\r\n\r\n const grouped: Record<string, Omit<MatchCandidate, 'collection'>[]> = {}\r\n for (const candidate of allCandidates) {\r\n const { collection: col, ...rest } = candidate\r\n if (!grouped[col]) grouped[col] = []\r\n grouped[col].push(rest)\r\n }\r\n\r\n return jsonResponse({\r\n candidates: grouped,\r\n total: allCandidates.length,\r\n })\r\n },\r\n }\r\n}\r\n\r\nfunction rankDocument(\r\n doc: Record<string, any>,\r\n collection: string,\r\n fields: string[],\r\n query: string,\r\n): MatchCandidate[] {\r\n const queryLower = query.toLowerCase()\r\n let bestMatch: MatchCandidate | null = null\r\n\r\n for (const field of fields) {\r\n const value = doc[field]\r\n if (typeof value !== 'string') continue\r\n\r\n const valueLower = value.toLowerCase()\r\n let matchType: MatchCandidate['matchType']\r\n\r\n if (field === 'slug' && valueLower === queryLower) {\r\n matchType = 'exact-slug'\r\n } else if (valueLower === queryLower) {\r\n matchType = 'exact'\r\n } else {\r\n matchType = 'partial'\r\n }\r\n\r\n if (\r\n !bestMatch ||\r\n matchTypePriority(matchType) < matchTypePriority(bestMatch.matchType)\r\n ) {\r\n bestMatch = {\r\n collection,\r\n id: doc.id,\r\n displayName: getDisplayName(doc, fields),\r\n matchedField: field,\r\n matchType,\r\n }\r\n }\r\n }\r\n\r\n return bestMatch ? [bestMatch] : []\r\n}\r\n\r\nfunction matchTypePriority(type: MatchCandidate['matchType']): number {\r\n switch (type) {\r\n case 'exact-slug':\r\n return 0\r\n case 'exact':\r\n return 1\r\n case 'partial':\r\n return 2\r\n }\r\n}\r\n\r\nfunction getDisplayName(doc: Record<string, any>, fields: string[]): string {\r\n for (const preferred of ['name', 'title', 'slug']) {\r\n if (fields.includes(preferred) && typeof doc[preferred] === 'string') {\r\n return doc[preferred]\r\n }\r\n }\r\n return String(doc.id)\r\n}\r\n"],"names":["z","jsonResponse","stampMcpContext","createResolveReferenceTool","searchableCollections","name","routing","kind","action","description","parameters","object","query","string","describe","collection","optional","handler","args","req","collectionsToSearch","Map","has","get","size","available","Array","from","keys","join","candidates","message","targets","entries","filter","fields","length","settled","Promise","allSettled","map","slug","selectFields","field","payload","find","where","or","like","limit","select","overrideAccess","user","allCandidates","forEach","outcome","i","status","doc","value","docs","push","rankDocument","sort","a","b","matchTypePriority","matchType","searched","grouped","candidate","col","rest","total","queryLower","toLowerCase","bestMatch","valueLower","id","displayName","getDisplayName","matchedField","type","preferred","includes","String"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAEvB,SAASC,YAAY,EAAEC,eAAe,QAAQ,aAAY;AAU1D,OAAO,SAASC,2BACdC,qBAA4C;IAE5C,OAAO;QACLC,MAAM;QACNC,SAAS;YAAEC,MAAM;YAAWC,QAAQ;QAAO;QAC3CC,aACE,sEACA,wEACA;QACFC,YAAYV,EAAEW,MAAM,CAAC;YACnBC,OAAOZ,EAAEa,MAAM,GAAGC,QAAQ,CAAC;YAC3BC,YAAYf,EACTa,MAAM,GACNG,QAAQ,GACRF,QAAQ,CAAC;QACd;QACAG,SAAS,OAAOC,MAA+BC;YAC7CjB,gBAAgBiB;YAEhB,MAAM,EAAEP,KAAK,EAAEG,UAAU,EAAE,GAAGG;YAE9B,MAAME,sBAAsBL,aACxB,IAAIM,IACFjB,sBAAsBkB,GAAG,CAACP,cACtB;gBAAC;oBAACA;oBAAYX,sBAAsBmB,GAAG,CAACR;iBAAa;aAAC,GACtD,EAAE,IAERX;YAEJ,IAAIgB,oBAAoBI,IAAI,KAAK,GAAG;gBAClC,MAAMC,YAAYC,MAAMC,IAAI,CAACvB,sBAAsBwB,IAAI,IAAIC,IAAI,CAAC;gBAChE,OAAO5B,aAAa;oBAClB6B,YAAY,EAAE;oBACdC,SAAShB,aACL,CAAC,YAAY,EAAEA,WAAW,gFAAgF,EAAEU,WAAW,GACvH;gBACN;YACF;YAEA,MAAMO,UAAUN,MAAMC,IAAI,CAACP,oBAAoBa,OAAO,IAAIC,MAAM,CAC9D,CAAC,GAAGC,OAAO,GAAKA,OAAOC,MAAM,GAAG;YAGlC,MAAMC,UAAU,MAAMC,QAAQC,UAAU,CACtCP,QAAQQ,GAAG,CAAC,CAAC,CAACC,MAAMN,OAAO;gBACzB,MAAMO,eAAqC,CAAC;gBAC5C,KAAK,MAAMC,SAASR,OAAQO,YAAY,CAACC,MAAM,GAAG;gBAClD,OAAOxB,IAAIyB,OAAO,CAACC,IAAI,CAAC;oBACtB9B,YAAY0B;oBACZK,OAAO;wBAAEC,IAAIZ,OAAOK,GAAG,CAAC,CAACG,QAAW,CAAA;gCAAE,CAACA,MAAM,EAAE;oCAAEK,MAAMpC;gCAAM;4BAAE,CAAA;oBAAI;oBACnEqC,OAAO;oBACPC,QAAQR;oBACRvB;oBACAgC,gBAAgB;oBAChBC,MAAMjC,IAAIiC,IAAI;gBAChB;YACF;YAGF,MAAMC,gBAAkC,EAAE;YAC1ChB,QAAQiB,OAAO,CAAC,CAACC,SAASC;gBACxB,IAAID,QAAQE,MAAM,KAAK,aAAa;gBACpC,MAAM,CAAChB,MAAMN,OAAO,GAAGH,OAAO,CAACwB,EAAE;gBACjC,KAAK,MAAME,OAAOH,QAAQI,KAAK,CAACC,IAAI,CAAE;oBACpCP,cAAcQ,IAAI,IAAIC,aAAaJ,KAAKjB,MAAMN,QAAQvB;gBACxD;YACF;YAEAyC,cAAcU,IAAI,CAChB,CAACC,GAAGC,IAAMC,kBAAkBF,EAAEG,SAAS,IAAID,kBAAkBD,EAAEE,SAAS;YAG1E,IAAId,cAAcjB,MAAM,KAAK,GAAG;gBAC9B,MAAMgC,WAAW1C,MAAMC,IAAI,CAACP,oBAAoBQ,IAAI,IAAIC,IAAI,CAAC;gBAC7D,MAAMJ,YAAYC,MAAMC,IAAI,CAACvB,sBAAsBwB,IAAI,IAAIC,IAAI,CAAC;gBAChE,OAAO5B,aAAa;oBAClB6B,YAAY,CAAC;oBACbC,SAAS,CAAC,sBAAsB,EAAEnB,MAAM,MAAM,EAAEwD,SAAS,uEAAuE,EAAE3C,WAAW;gBAC/I;YACF;YAEA,MAAM4C,UAAgE,CAAC;YACvE,KAAK,MAAMC,aAAajB,cAAe;gBACrC,MAAM,EAAEtC,YAAYwD,GAAG,EAAE,GAAGC,MAAM,GAAGF;gBACrC,IAAI,CAACD,OAAO,CAACE,IAAI,EAAEF,OAAO,CAACE,IAAI,GAAG,EAAE;gBACpCF,OAAO,CAACE,IAAI,CAACV,IAAI,CAACW;YACpB;YAEA,OAAOvE,aAAa;gBAClB6B,YAAYuC;gBACZI,OAAOpB,cAAcjB,MAAM;YAC7B;QACF;IACF;AACF;AAEA,SAAS0B,aACPJ,GAAwB,EACxB3C,UAAkB,EAClBoB,MAAgB,EAChBvB,KAAa;IAEb,MAAM8D,aAAa9D,MAAM+D,WAAW;IACpC,IAAIC,YAAmC;IAEvC,KAAK,MAAMjC,SAASR,OAAQ;QAC1B,MAAMwB,QAAQD,GAAG,CAACf,MAAM;QACxB,IAAI,OAAOgB,UAAU,UAAU;QAE/B,MAAMkB,aAAalB,MAAMgB,WAAW;QACpC,IAAIR;QAEJ,IAAIxB,UAAU,UAAUkC,eAAeH,YAAY;YACjDP,YAAY;QACd,OAAO,IAAIU,eAAeH,YAAY;YACpCP,YAAY;QACd,OAAO;YACLA,YAAY;QACd;QAEA,IACE,CAACS,aACDV,kBAAkBC,aAAaD,kBAAkBU,UAAUT,SAAS,GACpE;YACAS,YAAY;gBACV7D;gBACA+D,IAAIpB,IAAIoB,EAAE;gBACVC,aAAaC,eAAetB,KAAKvB;gBACjC8C,cAActC;gBACdwB;YACF;QACF;IACF;IAEA,OAAOS,YAAY;QAACA;KAAU,GAAG,EAAE;AACrC;AAEA,SAASV,kBAAkBgB,IAAiC;IAC1D,OAAQA;QACN,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;IACX;AACF;AAEA,SAASF,eAAetB,GAAwB,EAAEvB,MAAgB;IAChE,KAAK,MAAMgD,aAAa;QAAC;QAAQ;QAAS;KAAO,CAAE;QACjD,IAAIhD,OAAOiD,QAAQ,CAACD,cAAc,OAAOzB,GAAG,CAACyB,UAAU,KAAK,UAAU;YACpE,OAAOzB,GAAG,CAACyB,UAAU;QACvB;IACF;IACA,OAAOE,OAAO3B,IAAIoB,EAAE;AACtB"}
@@ -18,15 +18,15 @@ import type { RelationshipEdge } from '../types';
18
18
  */
19
19
  export declare function createSafeDeleteTool(relationships: RelationshipEdge[]): {
20
20
  name: string;
21
+ routing: {
22
+ readonly kind: "collection";
23
+ readonly action: "delete";
24
+ };
21
25
  description: string;
22
26
  parameters: {
23
27
  collection: z.ZodString;
24
28
  documentId: z.ZodString;
25
29
  confirm: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
26
30
  };
27
- handler: (args: {
28
- collection: string;
29
- documentId: string;
30
- confirm?: boolean;
31
- }, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
31
+ handler: (rawArgs: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
32
32
  };
@@ -1,20 +1,20 @@
1
1
  import { z } from 'zod';
2
2
  import { errorMessage, jsonResponse, stampMcpContext, textResponse } from './_helpers';
3
3
  const SAMPLE_LIMIT = 5;
4
- /**
5
- * safeDelete wraps the official delete operation with a relationship-graph
6
- * pre-check.
7
- *
8
- * Workflow:
9
- * 1. Use the introspected relationshipGraph to find every (collection, field)
10
- * pair that points TO the target collection.
11
- * 2. For each, query for documents that reference the target ID. Aggregate
12
- * counts and a small sample of inbound document IDs.
13
- * 3. If any inbound references exist and `confirm` is false, refuse and
14
- * return the impact summary so the caller can decide.
15
- * 4. Otherwise, perform the delete.
16
- *
17
- * Excludes built-in Payload bookkeeping collections from the inbound search.
4
+ /**
5
+ * safeDelete wraps the official delete operation with a relationship-graph
6
+ * pre-check.
7
+ *
8
+ * Workflow:
9
+ * 1. Use the introspected relationshipGraph to find every (collection, field)
10
+ * pair that points TO the target collection.
11
+ * 2. For each, query for documents that reference the target ID. Aggregate
12
+ * counts and a small sample of inbound document IDs.
13
+ * 3. If any inbound references exist and `confirm` is false, refuse and
14
+ * return the impact summary so the caller can decide.
15
+ * 4. Otherwise, perform the delete.
16
+ *
17
+ * Excludes built-in Payload bookkeeping collections from the inbound search.
18
18
  */ export function createSafeDeleteTool(relationships) {
19
19
  // reverseIndex: target collection slug → edges that reference it
20
20
  const reverseIndex = new Map();
@@ -33,13 +33,18 @@ const SAMPLE_LIMIT = 5;
33
33
  }
34
34
  return {
35
35
  name: 'safeDelete',
36
+ routing: {
37
+ kind: 'collection',
38
+ action: 'delete'
39
+ },
36
40
  description: 'Delete a document only after checking for inbound relationships. ' + 'If other documents reference the target, the delete is refused unless `confirm` is true. ' + 'Use this in preference to the raw delete tools when removing entities that other content might depend on ' + '(authors, categories, media, locations, etc.). Returns the impact summary either way.',
37
41
  parameters: {
38
42
  collection: z.string().describe('Slug of the collection containing the document to delete'),
39
43
  documentId: z.string().describe('ID of the document to delete'),
40
44
  confirm: z.boolean().optional().default(false).describe('If false (default), refuse to delete when inbound references exist and return the impact summary. ' + 'Pass true to delete anyway after reviewing the impact.')
41
45
  },
42
- handler: async (args, req, _extra)=>{
46
+ handler: async (rawArgs, req, _extra)=>{
47
+ const args = rawArgs;
43
48
  const { collection, documentId, confirm = false } = args;
44
49
  stampMcpContext(req);
45
50
  const inboundEdges = (reverseIndex.get(collection) ?? []).filter((edge)=>!edge.fromCollection.startsWith('payload-'));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/safe-delete.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { RelationshipEdge } from '../types'\nimport { errorMessage, jsonResponse, stampMcpContext, textResponse } from './_helpers'\n\nconst SAMPLE_LIMIT = 5\n\ninterface InboundReference {\n fromCollection: string\n fieldName: string\n count: number\n sampleIds: (string | number)[]\n}\n\ninterface ReverseEdge {\n fromCollection: string\n fieldName: string\n hasMany: boolean\n}\n\n/**\n * safeDelete wraps the official delete operation with a relationship-graph\n * pre-check.\n *\n * Workflow:\n * 1. Use the introspected relationshipGraph to find every (collection, field)\n * pair that points TO the target collection.\n * 2. For each, query for documents that reference the target ID. Aggregate\n * counts and a small sample of inbound document IDs.\n * 3. If any inbound references exist and `confirm` is false, refuse and\n * return the impact summary so the caller can decide.\n * 4. Otherwise, perform the delete.\n *\n * Excludes built-in Payload bookkeeping collections from the inbound search.\n */\nexport function createSafeDeleteTool(relationships: RelationshipEdge[]) {\n // reverseIndex: target collection slug → edges that reference it\n const reverseIndex = new Map<string, ReverseEdge[]>()\n for (const edge of relationships) {\n const targets = Array.isArray(edge.toCollection) ? edge.toCollection : [edge.toCollection]\n for (const target of targets) {\n if (!reverseIndex.has(target)) reverseIndex.set(target, [])\n reverseIndex.get(target)!.push({\n fromCollection: edge.fromCollection,\n fieldName: edge.fieldName,\n hasMany: edge.hasMany,\n })\n }\n }\n\n return {\n name: 'safeDelete',\n description:\n 'Delete a document only after checking for inbound relationships. ' +\n 'If other documents reference the target, the delete is refused unless `confirm` is true. ' +\n 'Use this in preference to the raw delete tools when removing entities that other content might depend on ' +\n '(authors, categories, media, locations, etc.). Returns the impact summary either way.',\n parameters: {\n collection: z.string().describe('Slug of the collection containing the document to delete'),\n documentId: z.string().describe('ID of the document to delete'),\n confirm: z\n .boolean()\n .optional()\n .default(false)\n .describe(\n 'If false (default), refuse to delete when inbound references exist and return the impact summary. ' +\n 'Pass true to delete anyway after reviewing the impact.',\n ),\n },\n handler: async (\n args: { collection: string; documentId: string; confirm?: boolean },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const { collection, documentId, confirm = false } = args\n\n stampMcpContext(req)\n\n const inboundEdges = (reverseIndex.get(collection) ?? []).filter(\n (edge) => !edge.fromCollection.startsWith('payload-'),\n )\n\n const settled = await Promise.allSettled(\n inboundEdges.map((edge) =>\n req.payload.find({\n collection: edge.fromCollection as any,\n where: { [edge.fieldName]: { equals: documentId } } as any,\n limit: SAMPLE_LIMIT,\n select: { id: true } as any,\n // CRITICAL: include drafts. Without this, a draft document\n // referencing the target is invisible to the safety check and\n // the delete is allowed through — exactly the failure mode this\n // tool is supposed to prevent.\n draft: true,\n req,\n overrideAccess: false,\n user: req.user,\n }),\n ),\n )\n\n const references: InboundReference[] = []\n const failedEdges: { fromCollection: string; fieldName: string; error: string }[] = []\n\n settled.forEach((outcome, i) => {\n const edge = inboundEdges[i]\n if (outcome.status === 'rejected') {\n // Fail closed: an unverified edge means we don't know whether the\n // target is referenced. Refuse the delete unless the caller opts in.\n failedEdges.push({\n fromCollection: edge.fromCollection,\n fieldName: edge.fieldName,\n error: errorMessage(outcome.reason),\n })\n return\n }\n const result = outcome.value\n if (result.totalDocs > 0) {\n references.push({\n fromCollection: edge.fromCollection,\n fieldName: edge.fieldName,\n count: result.totalDocs,\n sampleIds: result.docs.map((d: any) => d.id),\n })\n }\n })\n\n if (failedEdges.length > 0 && !confirm) {\n return jsonResponse({\n success: false,\n refused: true,\n message:\n `Refusing to delete ${collection}#${documentId}: could not verify ` +\n `${failedEdges.length} inbound reference path(s). Pass confirm=true to delete anyway.`,\n unverifiedEdges: failedEdges,\n })\n }\n\n const totalReferences = references.reduce((sum, r) => sum + r.count, 0)\n\n if (totalReferences > 0 && !confirm) {\n return jsonResponse({\n success: false,\n refused: true,\n message:\n `Refusing to delete ${collection}#${documentId}: ${totalReferences} inbound reference(s) ` +\n `found across ${references.length} field path(s). Pass confirm=true to delete anyway.`,\n totalReferences,\n references,\n })\n }\n\n try {\n await req.payload.delete({\n collection: collection as any,\n id: documentId,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n return jsonResponse({\n success: true,\n message:\n totalReferences > 0\n ? `Deleted ${collection}#${documentId} despite ${totalReferences} inbound reference(s) (confirm=true).`\n : `Deleted ${collection}#${documentId}. No inbound references were found.`,\n totalReferences,\n references,\n })\n } catch (error) {\n return textResponse(\n `Error deleting ${collection}#${documentId}: ${errorMessage(error)}`,\n )\n }\n },\n }\n}\n"],"names":["z","errorMessage","jsonResponse","stampMcpContext","textResponse","SAMPLE_LIMIT","createSafeDeleteTool","relationships","reverseIndex","Map","edge","targets","Array","isArray","toCollection","target","has","set","get","push","fromCollection","fieldName","hasMany","name","description","parameters","collection","string","describe","documentId","confirm","boolean","optional","default","handler","args","req","_extra","inboundEdges","filter","startsWith","settled","Promise","allSettled","map","payload","find","where","equals","limit","select","id","draft","overrideAccess","user","references","failedEdges","forEach","outcome","i","status","error","reason","result","value","totalDocs","count","sampleIds","docs","d","length","success","refused","message","unverifiedEdges","totalReferences","reduce","sum","r","delete"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/tools/safe-delete.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport type { RelationshipEdge } from '../types'\r\nimport { errorMessage, jsonResponse, stampMcpContext, textResponse } from './_helpers'\r\n\r\nconst SAMPLE_LIMIT = 5\r\n\r\ninterface InboundReference {\r\n fromCollection: string\r\n fieldName: string\r\n count: number\r\n sampleIds: (string | number)[]\r\n}\r\n\r\ninterface ReverseEdge {\r\n fromCollection: string\r\n fieldName: string\r\n hasMany: boolean\r\n}\r\n\r\n/**\r\n * safeDelete wraps the official delete operation with a relationship-graph\r\n * pre-check.\r\n *\r\n * Workflow:\r\n * 1. Use the introspected relationshipGraph to find every (collection, field)\r\n * pair that points TO the target collection.\r\n * 2. For each, query for documents that reference the target ID. Aggregate\r\n * counts and a small sample of inbound document IDs.\r\n * 3. If any inbound references exist and `confirm` is false, refuse and\r\n * return the impact summary so the caller can decide.\r\n * 4. Otherwise, perform the delete.\r\n *\r\n * Excludes built-in Payload bookkeeping collections from the inbound search.\r\n */\r\nexport function createSafeDeleteTool(relationships: RelationshipEdge[]) {\r\n // reverseIndex: target collection slug → edges that reference it\r\n const reverseIndex = new Map<string, ReverseEdge[]>()\r\n for (const edge of relationships) {\r\n const targets = Array.isArray(edge.toCollection) ? edge.toCollection : [edge.toCollection]\r\n for (const target of targets) {\r\n if (!reverseIndex.has(target)) reverseIndex.set(target, [])\r\n reverseIndex.get(target)!.push({\r\n fromCollection: edge.fromCollection,\r\n fieldName: edge.fieldName,\r\n hasMany: edge.hasMany,\r\n })\r\n }\r\n }\r\n\r\n return {\r\n name: 'safeDelete',\r\n routing: { kind: 'collection', action: 'delete' } as const,\r\n description:\r\n 'Delete a document only after checking for inbound relationships. ' +\r\n 'If other documents reference the target, the delete is refused unless `confirm` is true. ' +\r\n 'Use this in preference to the raw delete tools when removing entities that other content might depend on ' +\r\n '(authors, categories, media, locations, etc.). Returns the impact summary either way.',\r\n parameters: {\r\n collection: z.string().describe('Slug of the collection containing the document to delete'),\r\n documentId: z.string().describe('ID of the document to delete'),\r\n confirm: z\r\n .boolean()\r\n .optional()\r\n .default(false)\r\n .describe(\r\n 'If false (default), refuse to delete when inbound references exist and return the impact summary. ' +\r\n 'Pass true to delete anyway after reviewing the impact.',\r\n ),\r\n },\r\n handler: async (\r\n rawArgs: Record<string, unknown>,\r\n req: PayloadRequest,\r\n _extra: unknown,\r\n ) => {\r\n const args = rawArgs as { collection: string; documentId: string; confirm?: boolean }\r\n const { collection, documentId, confirm = false } = args\r\n\r\n stampMcpContext(req)\r\n\r\n const inboundEdges = (reverseIndex.get(collection) ?? []).filter(\r\n (edge) => !edge.fromCollection.startsWith('payload-'),\r\n )\r\n\r\n const settled = await Promise.allSettled(\r\n inboundEdges.map((edge) =>\r\n req.payload.find({\r\n collection: edge.fromCollection as any,\r\n where: { [edge.fieldName]: { equals: documentId } } as any,\r\n limit: SAMPLE_LIMIT,\r\n select: { id: true } as any,\r\n // CRITICAL: include drafts. Without this, a draft document\r\n // referencing the target is invisible to the safety check and\r\n // the delete is allowed through — exactly the failure mode this\r\n // tool is supposed to prevent.\r\n draft: true,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n }),\r\n ),\r\n )\r\n\r\n const references: InboundReference[] = []\r\n const failedEdges: { fromCollection: string; fieldName: string; error: string }[] = []\r\n\r\n settled.forEach((outcome, i) => {\r\n const edge = inboundEdges[i]\r\n if (outcome.status === 'rejected') {\r\n // Fail closed: an unverified edge means we don't know whether the\r\n // target is referenced. Refuse the delete unless the caller opts in.\r\n failedEdges.push({\r\n fromCollection: edge.fromCollection,\r\n fieldName: edge.fieldName,\r\n error: errorMessage(outcome.reason),\r\n })\r\n return\r\n }\r\n const result = outcome.value\r\n if (result.totalDocs > 0) {\r\n references.push({\r\n fromCollection: edge.fromCollection,\r\n fieldName: edge.fieldName,\r\n count: result.totalDocs,\r\n sampleIds: result.docs.map((d: any) => d.id),\r\n })\r\n }\r\n })\r\n\r\n if (failedEdges.length > 0 && !confirm) {\r\n return jsonResponse({\r\n success: false,\r\n refused: true,\r\n message:\r\n `Refusing to delete ${collection}#${documentId}: could not verify ` +\r\n `${failedEdges.length} inbound reference path(s). Pass confirm=true to delete anyway.`,\r\n unverifiedEdges: failedEdges,\r\n })\r\n }\r\n\r\n const totalReferences = references.reduce((sum, r) => sum + r.count, 0)\r\n\r\n if (totalReferences > 0 && !confirm) {\r\n return jsonResponse({\r\n success: false,\r\n refused: true,\r\n message:\r\n `Refusing to delete ${collection}#${documentId}: ${totalReferences} inbound reference(s) ` +\r\n `found across ${references.length} field path(s). Pass confirm=true to delete anyway.`,\r\n totalReferences,\r\n references,\r\n })\r\n }\r\n\r\n try {\r\n await req.payload.delete({\r\n collection: collection as any,\r\n id: documentId,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n\r\n return jsonResponse({\r\n success: true,\r\n message:\r\n totalReferences > 0\r\n ? `Deleted ${collection}#${documentId} despite ${totalReferences} inbound reference(s) (confirm=true).`\r\n : `Deleted ${collection}#${documentId}. No inbound references were found.`,\r\n totalReferences,\r\n references,\r\n })\r\n } catch (error) {\r\n return textResponse(\r\n `Error deleting ${collection}#${documentId}: ${errorMessage(error)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","errorMessage","jsonResponse","stampMcpContext","textResponse","SAMPLE_LIMIT","createSafeDeleteTool","relationships","reverseIndex","Map","edge","targets","Array","isArray","toCollection","target","has","set","get","push","fromCollection","fieldName","hasMany","name","routing","kind","action","description","parameters","collection","string","describe","documentId","confirm","boolean","optional","default","handler","rawArgs","req","_extra","args","inboundEdges","filter","startsWith","settled","Promise","allSettled","map","payload","find","where","equals","limit","select","id","draft","overrideAccess","user","references","failedEdges","forEach","outcome","i","status","error","reason","result","value","totalDocs","count","sampleIds","docs","d","length","success","refused","message","unverifiedEdges","totalReferences","reduce","sum","r","delete"],"mappings":"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"}
@@ -24,16 +24,16 @@ import type { CollectionSchema } from '../types';
24
24
  export declare function createSchedulePublishTool(collectionSchemas: Map<string, CollectionSchema>, draftCollections: Set<string>): ReturnType<typeof buildTool> | null;
25
25
  declare function buildTool(schedulableSlugs: string[]): {
26
26
  name: string;
27
+ routing: {
28
+ readonly kind: "collection";
29
+ readonly action: "update";
30
+ };
27
31
  description: string;
28
32
  parameters: {
29
33
  collection: z.ZodString;
30
34
  documentId: z.ZodString;
31
35
  publishAt: z.ZodString;
32
36
  };
33
- handler: (args: {
34
- collection: string;
35
- documentId: string;
36
- publishAt: string;
37
- }, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
37
+ handler: (args: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
38
38
  };
39
39
  export {};