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
@@ -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 {};
@@ -1,24 +1,24 @@
1
1
  import { z } from 'zod';
2
2
  import { errorMessage, getDocDisplayName, stampMcpContext, textResponse } from './_helpers';
3
- /**
4
- * schedulePublish stamps a future publish time on a draft document.
5
- *
6
- * IMPORTANT: this tool ONLY sets `publishedAt` and keeps `_status: 'draft'`.
7
- * It does not flip the status when the time arrives. The consumer's Payload
8
- * config must own the actual flip via:
9
- *
10
- * 1. A Payload Jobs Queue scheduled task that finds drafts whose
11
- * `publishedAt <= now` and updates `_status: 'published'`. This is the
12
- * canonical Payload-3 approach (https://payloadcms.com/docs/jobs-queue/scheduled-jobs).
13
- * 2. An external cron / worker doing the same query + update.
14
- * 3. A `beforeRead` hook that resolves status on the fly.
15
- *
16
- * Without one of those, scheduled drafts remain drafts forever — the tool
17
- * tells the LLM exactly that in its response so the user doesn't get a
18
- * silent surprise.
19
- *
20
- * Skip-detects: only registered for collections that have BOTH drafts AND a
21
- * `publishedAt` date field.
3
+ /**
4
+ * schedulePublish stamps a future publish time on a draft document.
5
+ *
6
+ * IMPORTANT: this tool ONLY sets `publishedAt` and keeps `_status: 'draft'`.
7
+ * It does not flip the status when the time arrives. The consumer's Payload
8
+ * config must own the actual flip via:
9
+ *
10
+ * 1. A Payload Jobs Queue scheduled task that finds drafts whose
11
+ * `publishedAt <= now` and updates `_status: 'published'`. This is the
12
+ * canonical Payload-3 approach (https://payloadcms.com/docs/jobs-queue/scheduled-jobs).
13
+ * 2. An external cron / worker doing the same query + update.
14
+ * 3. A `beforeRead` hook that resolves status on the fly.
15
+ *
16
+ * Without one of those, scheduled drafts remain drafts forever — the tool
17
+ * tells the LLM exactly that in its response so the user doesn't get a
18
+ * silent surprise.
19
+ *
20
+ * Skip-detects: only registered for collections that have BOTH drafts AND a
21
+ * `publishedAt` date field.
22
22
  */ export function createSchedulePublishTool(collectionSchemas, draftCollections) {
23
23
  const schedulableSlugs = [];
24
24
  for (const slug of draftCollections){
@@ -33,6 +33,10 @@ import { errorMessage, getDocDisplayName, stampMcpContext, textResponse } from '
33
33
  function buildTool(schedulableSlugs) {
34
34
  return {
35
35
  name: 'schedulePublish',
36
+ routing: {
37
+ kind: 'collection',
38
+ action: 'update'
39
+ },
36
40
  description: 'Schedule a draft to be published at a future date by stamping its publishedAt field. ' + 'The document stays in draft status until your Payload jobs queue (or an external worker) ' + 'flips it. If your project does not have a scheduled job that publishes drafts whose ' + 'publishedAt has passed, the document will remain a draft indefinitely. ' + `Schedulable collections (have both drafts and a publishedAt date field): ${schedulableSlugs.join(', ')}.`,
37
41
  parameters: {
38
42
  collection: z.string().describe(`The collection slug. One of: ${schedulableSlugs.join(', ')}`),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/schedule-publish.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { CollectionSchema } from '../types'\nimport {\n errorMessage,\n getDocDisplayName,\n stampMcpContext,\n textResponse,\n} from './_helpers'\n\n/**\n * schedulePublish stamps a future publish time on a draft document.\n *\n * IMPORTANT: this tool ONLY sets `publishedAt` and keeps `_status: 'draft'`.\n * It does not flip the status when the time arrives. The consumer's Payload\n * config must own the actual flip via:\n *\n * 1. A Payload Jobs Queue scheduled task that finds drafts whose\n * `publishedAt <= now` and updates `_status: 'published'`. This is the\n * canonical Payload-3 approach (https://payloadcms.com/docs/jobs-queue/scheduled-jobs).\n * 2. An external cron / worker doing the same query + update.\n * 3. A `beforeRead` hook that resolves status on the fly.\n *\n * Without one of those, scheduled drafts remain drafts forever — the tool\n * tells the LLM exactly that in its response so the user doesn't get a\n * silent surprise.\n *\n * Skip-detects: only registered for collections that have BOTH drafts AND a\n * `publishedAt` date field.\n */\nexport function createSchedulePublishTool(\n collectionSchemas: Map<string, CollectionSchema>,\n draftCollections: Set<string>,\n): ReturnType<typeof buildTool> | null {\n const schedulableSlugs: string[] = []\n for (const slug of draftCollections) {\n const schema = collectionSchemas.get(slug)\n if (!schema) continue\n const hasPublishedAt = schema.fields.some(\n (f) => f.name === 'publishedAt' && f.type === 'date',\n )\n if (hasPublishedAt) schedulableSlugs.push(slug)\n }\n\n if (schedulableSlugs.length === 0) return null\n return buildTool(schedulableSlugs)\n}\n\nfunction buildTool(schedulableSlugs: string[]) {\n return {\n name: 'schedulePublish',\n description:\n 'Schedule a draft to be published at a future date by stamping its publishedAt field. ' +\n 'The document stays in draft status until your Payload jobs queue (or an external worker) ' +\n 'flips it. If your project does not have a scheduled job that publishes drafts whose ' +\n 'publishedAt has passed, the document will remain a draft indefinitely. ' +\n `Schedulable collections (have both drafts and a publishedAt date field): ${schedulableSlugs.join(', ')}.`,\n parameters: {\n collection: z\n .string()\n .describe(`The collection slug. One of: ${schedulableSlugs.join(', ')}`),\n documentId: z.string().describe('The ID of the draft document to schedule'),\n publishAt: z\n .string()\n .describe(\n 'ISO 8601 date-time when the document should be published, e.g. \"2026-06-01T09:00:00Z\". ' +\n 'Must be in the future.',\n ),\n },\n handler: async (\n args: { collection: string; documentId: string; publishAt: string },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const { collection, documentId, publishAt } = args\n\n if (!schedulableSlugs.includes(collection)) {\n return textResponse(\n `Error: Collection \"${collection}\" is not schedulable. ` +\n `Schedulable collections: ${schedulableSlugs.join(', ')}. ` +\n 'A collection is schedulable when it has draft support AND a date field named \"publishedAt\".',\n )\n }\n\n const parsed = new Date(publishAt)\n if (isNaN(parsed.getTime())) {\n return textResponse(\n `Error: \"${publishAt}\" is not a valid ISO 8601 date-time. Example: \"2026-06-01T09:00:00Z\"`,\n )\n }\n\n if (parsed.getTime() <= Date.now()) {\n return textResponse(\n `Error: publishAt (${parsed.toISOString()}) is not in the future. ` +\n `Use the publishDraft tool to publish immediately instead.`,\n )\n }\n\n stampMcpContext(req)\n\n try {\n const updated = await req.payload.update({\n collection: collection as any,\n id: documentId,\n data: {\n publishedAt: parsed.toISOString(),\n _status: 'draft',\n } as any,\n draft: true,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const displayName = getDocDisplayName(updated, documentId)\n return textResponse(\n `Scheduled \"${displayName}\" (${collection}#${documentId}) for publish at ${parsed.toISOString()}. ` +\n `The document will remain a draft until a scheduled job (Payload jobs queue or external worker) ` +\n `flips its _status to \"published\". If your project has not configured such a job, the document ` +\n `will stay a draft indefinitely — see the Payload jobs queue docs for the canonical setup.`,\n )\n } catch (error) {\n return textResponse(\n `Error scheduling ${collection}#${documentId}: ${errorMessage(error)}`,\n )\n }\n },\n }\n}\n"],"names":["z","errorMessage","getDocDisplayName","stampMcpContext","textResponse","createSchedulePublishTool","collectionSchemas","draftCollections","schedulableSlugs","slug","schema","get","hasPublishedAt","fields","some","f","name","type","push","length","buildTool","description","join","parameters","collection","string","describe","documentId","publishAt","handler","args","req","_extra","includes","parsed","Date","isNaN","getTime","now","toISOString","updated","payload","update","id","data","publishedAt","_status","draft","overrideAccess","user","displayName","error"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,EACfC,YAAY,QACP,aAAY;AAEnB;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,0BACdC,iBAAgD,EAChDC,gBAA6B;IAE7B,MAAMC,mBAA6B,EAAE;IACrC,KAAK,MAAMC,QAAQF,iBAAkB;QACnC,MAAMG,SAASJ,kBAAkBK,GAAG,CAACF;QACrC,IAAI,CAACC,QAAQ;QACb,MAAME,iBAAiBF,OAAOG,MAAM,CAACC,IAAI,CACvC,CAACC,IAAMA,EAAEC,IAAI,KAAK,iBAAiBD,EAAEE,IAAI,KAAK;QAEhD,IAAIL,gBAAgBJ,iBAAiBU,IAAI,CAACT;IAC5C;IAEA,IAAID,iBAAiBW,MAAM,KAAK,GAAG,OAAO;IAC1C,OAAOC,UAAUZ;AACnB;AAEA,SAASY,UAAUZ,gBAA0B;IAC3C,OAAO;QACLQ,MAAM;QACNK,aACE,0FACA,8FACA,yFACA,4EACA,CAAC,yEAAyE,EAAEb,iBAAiBc,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5GC,YAAY;YACVC,YAAYxB,EACTyB,MAAM,GACNC,QAAQ,CAAC,CAAC,6BAA6B,EAAElB,iBAAiBc,IAAI,CAAC,OAAO;YACzEK,YAAY3B,EAAEyB,MAAM,GAAGC,QAAQ,CAAC;YAChCE,WAAW5B,EACRyB,MAAM,GACNC,QAAQ,CACP,4FACA;QAEN;QACAG,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAER,UAAU,EAAEG,UAAU,EAAEC,SAAS,EAAE,GAAGE;YAE9C,IAAI,CAACtB,iBAAiByB,QAAQ,CAACT,aAAa;gBAC1C,OAAOpB,aACL,CAAC,mBAAmB,EAAEoB,WAAW,sBAAsB,CAAC,GACtD,CAAC,yBAAyB,EAAEhB,iBAAiBc,IAAI,CAAC,MAAM,EAAE,CAAC,GAC3D;YAEN;YAEA,MAAMY,SAAS,IAAIC,KAAKP;YACxB,IAAIQ,MAAMF,OAAOG,OAAO,KAAK;gBAC3B,OAAOjC,aACL,CAAC,QAAQ,EAAEwB,UAAU,oEAAoE,CAAC;YAE9F;YAEA,IAAIM,OAAOG,OAAO,MAAMF,KAAKG,GAAG,IAAI;gBAClC,OAAOlC,aACL,CAAC,kBAAkB,EAAE8B,OAAOK,WAAW,GAAG,wBAAwB,CAAC,GACjE,CAAC,yDAAyD,CAAC;YAEjE;YAEApC,gBAAgB4B;YAEhB,IAAI;gBACF,MAAMS,UAAU,MAAMT,IAAIU,OAAO,CAACC,MAAM,CAAC;oBACvClB,YAAYA;oBACZmB,IAAIhB;oBACJiB,MAAM;wBACJC,aAAaX,OAAOK,WAAW;wBAC/BO,SAAS;oBACX;oBACAC,OAAO;oBACPhB;oBACAiB,gBAAgB;oBAChBC,MAAMlB,IAAIkB,IAAI;gBAChB;gBAEA,MAAMC,cAAchD,kBAAkBsC,SAASb;gBAC/C,OAAOvB,aACL,CAAC,WAAW,EAAE8C,YAAY,GAAG,EAAE1B,WAAW,CAAC,EAAEG,WAAW,iBAAiB,EAAEO,OAAOK,WAAW,GAAG,EAAE,CAAC,GACjG,CAAC,+FAA+F,CAAC,GACjG,CAAC,8FAA8F,CAAC,GAChG,CAAC,yFAAyF,CAAC;YAEjG,EAAE,OAAOY,OAAO;gBACd,OAAO/C,aACL,CAAC,iBAAiB,EAAEoB,WAAW,CAAC,EAAEG,WAAW,EAAE,EAAE1B,aAAakD,QAAQ;YAE1E;QACF;IACF;AACF"}
1
+ {"version":3,"sources":["../../src/tools/schedule-publish.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport {\r\n errorMessage,\r\n getDocDisplayName,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\n/**\r\n * schedulePublish stamps a future publish time on a draft document.\r\n *\r\n * IMPORTANT: this tool ONLY sets `publishedAt` and keeps `_status: 'draft'`.\r\n * It does not flip the status when the time arrives. The consumer's Payload\r\n * config must own the actual flip via:\r\n *\r\n * 1. A Payload Jobs Queue scheduled task that finds drafts whose\r\n * `publishedAt <= now` and updates `_status: 'published'`. This is the\r\n * canonical Payload-3 approach (https://payloadcms.com/docs/jobs-queue/scheduled-jobs).\r\n * 2. An external cron / worker doing the same query + update.\r\n * 3. A `beforeRead` hook that resolves status on the fly.\r\n *\r\n * Without one of those, scheduled drafts remain drafts forever — the tool\r\n * tells the LLM exactly that in its response so the user doesn't get a\r\n * silent surprise.\r\n *\r\n * Skip-detects: only registered for collections that have BOTH drafts AND a\r\n * `publishedAt` date field.\r\n */\r\nexport function createSchedulePublishTool(\r\n collectionSchemas: Map<string, CollectionSchema>,\r\n draftCollections: Set<string>,\r\n): ReturnType<typeof buildTool> | null {\r\n const schedulableSlugs: string[] = []\r\n for (const slug of draftCollections) {\r\n const schema = collectionSchemas.get(slug)\r\n if (!schema) continue\r\n const hasPublishedAt = schema.fields.some(\r\n (f) => f.name === 'publishedAt' && f.type === 'date',\r\n )\r\n if (hasPublishedAt) schedulableSlugs.push(slug)\r\n }\r\n\r\n if (schedulableSlugs.length === 0) return null\r\n return buildTool(schedulableSlugs)\r\n}\r\n\r\nfunction buildTool(schedulableSlugs: string[]) {\r\n return {\r\n name: 'schedulePublish',\r\n routing: { kind: 'collection', action: 'update' } as const,\r\n description:\r\n 'Schedule a draft to be published at a future date by stamping its publishedAt field. ' +\r\n 'The document stays in draft status until your Payload jobs queue (or an external worker) ' +\r\n 'flips it. If your project does not have a scheduled job that publishes drafts whose ' +\r\n 'publishedAt has passed, the document will remain a draft indefinitely. ' +\r\n `Schedulable collections (have both drafts and a publishedAt date field): ${schedulableSlugs.join(', ')}.`,\r\n parameters: {\r\n collection: z\r\n .string()\r\n .describe(`The collection slug. One of: ${schedulableSlugs.join(', ')}`),\r\n documentId: z.string().describe('The ID of the draft document to schedule'),\r\n publishAt: z\r\n .string()\r\n .describe(\r\n 'ISO 8601 date-time when the document should be published, e.g. \"2026-06-01T09:00:00Z\". ' +\r\n 'Must be in the future.',\r\n ),\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 { collection, documentId, publishAt } = args as {\r\n collection: string\r\n documentId: string\r\n publishAt: string\r\n }\r\n\r\n if (!schedulableSlugs.includes(collection)) {\r\n return textResponse(\r\n `Error: Collection \"${collection}\" is not schedulable. ` +\r\n `Schedulable collections: ${schedulableSlugs.join(', ')}. ` +\r\n 'A collection is schedulable when it has draft support AND a date field named \"publishedAt\".',\r\n )\r\n }\r\n\r\n const parsed = new Date(publishAt)\r\n if (isNaN(parsed.getTime())) {\r\n return textResponse(\r\n `Error: \"${publishAt}\" is not a valid ISO 8601 date-time. Example: \"2026-06-01T09:00:00Z\"`,\r\n )\r\n }\r\n\r\n if (parsed.getTime() <= Date.now()) {\r\n return textResponse(\r\n `Error: publishAt (${parsed.toISOString()}) is not in the future. ` +\r\n `Use the publishDraft tool to publish immediately instead.`,\r\n )\r\n }\r\n\r\n stampMcpContext(req)\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: {\r\n publishedAt: parsed.toISOString(),\r\n _status: 'draft',\r\n } as any,\r\n draft: true,\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 return textResponse(\r\n `Scheduled \"${displayName}\" (${collection}#${documentId}) for publish at ${parsed.toISOString()}. ` +\r\n `The document will remain a draft until a scheduled job (Payload jobs queue or external worker) ` +\r\n `flips its _status to \"published\". If your project has not configured such a job, the document ` +\r\n `will stay a draft indefinitely — see the Payload jobs queue docs for the canonical setup.`,\r\n )\r\n } catch (error) {\r\n return textResponse(\r\n `Error scheduling ${collection}#${documentId}: ${errorMessage(error)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","errorMessage","getDocDisplayName","stampMcpContext","textResponse","createSchedulePublishTool","collectionSchemas","draftCollections","schedulableSlugs","slug","schema","get","hasPublishedAt","fields","some","f","name","type","push","length","buildTool","routing","kind","action","description","join","parameters","collection","string","describe","documentId","publishAt","handler","args","req","_extra","includes","parsed","Date","isNaN","getTime","now","toISOString","updated","payload","update","id","data","publishedAt","_status","draft","overrideAccess","user","displayName","error"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,EACfC,YAAY,QACP,aAAY;AAEnB;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,0BACdC,iBAAgD,EAChDC,gBAA6B;IAE7B,MAAMC,mBAA6B,EAAE;IACrC,KAAK,MAAMC,QAAQF,iBAAkB;QACnC,MAAMG,SAASJ,kBAAkBK,GAAG,CAACF;QACrC,IAAI,CAACC,QAAQ;QACb,MAAME,iBAAiBF,OAAOG,MAAM,CAACC,IAAI,CACvC,CAACC,IAAMA,EAAEC,IAAI,KAAK,iBAAiBD,EAAEE,IAAI,KAAK;QAEhD,IAAIL,gBAAgBJ,iBAAiBU,IAAI,CAACT;IAC5C;IAEA,IAAID,iBAAiBW,MAAM,KAAK,GAAG,OAAO;IAC1C,OAAOC,UAAUZ;AACnB;AAEA,SAASY,UAAUZ,gBAA0B;IAC3C,OAAO;QACLQ,MAAM;QACNK,SAAS;YAAEC,MAAM;YAAcC,QAAQ;QAAS;QAChDC,aACE,0FACA,8FACA,yFACA,4EACA,CAAC,yEAAyE,EAAEhB,iBAAiBiB,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5GC,YAAY;YACVC,YAAY3B,EACT4B,MAAM,GACNC,QAAQ,CAAC,CAAC,6BAA6B,EAAErB,iBAAiBiB,IAAI,CAAC,OAAO;YACzEK,YAAY9B,EAAE4B,MAAM,GAAGC,QAAQ,CAAC;YAChCE,WAAW/B,EACR4B,MAAM,GACNC,QAAQ,CACP,4FACA;QAEN;QACAG,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAER,UAAU,EAAEG,UAAU,EAAEC,SAAS,EAAE,GAAGE;YAM9C,IAAI,CAACzB,iBAAiB4B,QAAQ,CAACT,aAAa;gBAC1C,OAAOvB,aACL,CAAC,mBAAmB,EAAEuB,WAAW,sBAAsB,CAAC,GACtD,CAAC,yBAAyB,EAAEnB,iBAAiBiB,IAAI,CAAC,MAAM,EAAE,CAAC,GAC3D;YAEN;YAEA,MAAMY,SAAS,IAAIC,KAAKP;YACxB,IAAIQ,MAAMF,OAAOG,OAAO,KAAK;gBAC3B,OAAOpC,aACL,CAAC,QAAQ,EAAE2B,UAAU,oEAAoE,CAAC;YAE9F;YAEA,IAAIM,OAAOG,OAAO,MAAMF,KAAKG,GAAG,IAAI;gBAClC,OAAOrC,aACL,CAAC,kBAAkB,EAAEiC,OAAOK,WAAW,GAAG,wBAAwB,CAAC,GACjE,CAAC,yDAAyD,CAAC;YAEjE;YAEAvC,gBAAgB+B;YAEhB,IAAI;gBACF,MAAMS,UAAU,MAAMT,IAAIU,OAAO,CAACC,MAAM,CAAC;oBACvClB,YAAYA;oBACZmB,IAAIhB;oBACJiB,MAAM;wBACJC,aAAaX,OAAOK,WAAW;wBAC/BO,SAAS;oBACX;oBACAC,OAAO;oBACPhB;oBACAiB,gBAAgB;oBAChBC,MAAMlB,IAAIkB,IAAI;gBAChB;gBAEA,MAAMC,cAAcnD,kBAAkByC,SAASb;gBAC/C,OAAO1B,aACL,CAAC,WAAW,EAAEiD,YAAY,GAAG,EAAE1B,WAAW,CAAC,EAAEG,WAAW,iBAAiB,EAAEO,OAAOK,WAAW,GAAG,EAAE,CAAC,GACjG,CAAC,+FAA+F,CAAC,GACjG,CAAC,8FAA8F,CAAC,GAChG,CAAC,yFAAyF,CAAC;YAEjG,EAAE,OAAOY,OAAO;gBACd,OAAOlD,aACL,CAAC,iBAAiB,EAAEuB,WAAW,CAAC,EAAEG,WAAW,EAAE,EAAE7B,aAAaqD,QAAQ;YAE1E;QACF;IACF;AACF"}
@@ -16,6 +16,10 @@ import type { CollectionSchema } from '../types';
16
16
  */
17
17
  export declare function createSearchContentTool(collectionSchemas: Map<string, CollectionSchema>): {
18
18
  name: string;
19
+ routing: {
20
+ readonly kind: "account";
21
+ readonly action: "read";
22
+ };
19
23
  description: string;
20
24
  parameters: {
21
25
  collection: z.ZodOptional<z.ZodString>;
@@ -26,13 +30,5 @@ export declare function createSearchContentTool(collectionSchemas: Map<string, C
26
30
  missingFields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
27
31
  limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
28
32
  };
29
- handler: (args: {
30
- collection?: string;
31
- query?: string;
32
- status?: "draft" | "published" | "any";
33
- olderThanDays?: number;
34
- newerThanDays?: number;
35
- missingFields?: string[];
36
- limit?: number;
37
- }, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
33
+ handler: (args: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
38
34
  };
@@ -2,24 +2,28 @@ import { z } from 'zod';
2
2
  import { jsonResponse, stampMcpContext } from './_helpers';
3
3
  const DEFAULT_LIMIT = 20;
4
4
  const HARD_LIMIT = 100;
5
- /**
6
- * Creates the searchContent MCP tool — natural-language filters across collections,
7
- * built for editor triage tasks that the official find tools don't express well.
8
- *
9
- * Examples the LLM can drive:
10
- * - "all posts that are still drafts"
11
- * - "pages missing a meta description"
12
- * - "anything updated more than 30 days ago"
13
- * - "posts by jane in the last quarter"
14
- *
15
- * Returns compact hits per collection — id, displayName, _status, updatedAt, and
16
- * (when missingFields was requested) which of those fields are blank on each doc.
5
+ /**
6
+ * Creates the searchContent MCP tool — natural-language filters across collections,
7
+ * built for editor triage tasks that the official find tools don't express well.
8
+ *
9
+ * Examples the LLM can drive:
10
+ * - "all posts that are still drafts"
11
+ * - "pages missing a meta description"
12
+ * - "anything updated more than 30 days ago"
13
+ * - "posts by jane in the last quarter"
14
+ *
15
+ * Returns compact hits per collection — id, displayName, _status, updatedAt, and
16
+ * (when missingFields was requested) which of those fields are blank on each doc.
17
17
  */ export function createSearchContentTool(collectionSchemas) {
18
18
  const allSlugs = [
19
19
  ...collectionSchemas.keys()
20
20
  ];
21
21
  return {
22
22
  name: 'searchContent',
23
+ routing: {
24
+ kind: 'account',
25
+ action: 'read'
26
+ },
23
27
  description: 'Search and filter content across collections by status, age, missing fields, or free-text query. ' + 'Designed for editor triage — finding drafts, stale content, content with missing SEO fields, etc. ' + `Searchable collections: ${allSlugs.join(', ')}.`,
24
28
  parameters: {
25
29
  collection: z.string().optional().describe('Restrict search to a single collection slug. Omit to search all.'),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/search-content.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { CollectionSchema } from '../types'\nimport { jsonResponse, stampMcpContext } from './_helpers'\n\nconst DEFAULT_LIMIT = 20\nconst HARD_LIMIT = 100\n\ninterface SearchHit {\n id: string | number\n displayName: string\n status?: string\n updatedAt?: string\n missingFields?: string[]\n}\n\n/**\n * Creates the searchContent MCP tool — natural-language filters across collections,\n * built for editor triage tasks that the official find tools don't express well.\n *\n * Examples the LLM can drive:\n * - \"all posts that are still drafts\"\n * - \"pages missing a meta description\"\n * - \"anything updated more than 30 days ago\"\n * - \"posts by jane in the last quarter\"\n *\n * Returns compact hits per collection — id, displayName, _status, updatedAt, and\n * (when missingFields was requested) which of those fields are blank on each doc.\n */\nexport function createSearchContentTool(\n collectionSchemas: Map<string, CollectionSchema>,\n) {\n const allSlugs = [...collectionSchemas.keys()]\n\n return {\n name: 'searchContent',\n description:\n 'Search and filter content across collections by status, age, missing fields, or free-text query. ' +\n 'Designed for editor triage — finding drafts, stale content, content with missing SEO fields, etc. ' +\n `Searchable collections: ${allSlugs.join(', ')}.`,\n parameters: {\n collection: z\n .string()\n .optional()\n .describe('Restrict search to a single collection slug. Omit to search all.'),\n query: z\n .string()\n .optional()\n .describe('Free-text query matched against name/title/slug fields (case-insensitive).'),\n status: z\n .enum(['draft', 'published', 'any'])\n .optional()\n .describe(\n 'Filter by draft status. \"draft\" or \"published\" only return matching docs; \"any\" or omitted returns all.',\n ),\n olderThanDays: z\n .number()\n .optional()\n .describe('Only docs whose updatedAt is older than this many days.'),\n newerThanDays: z\n .number()\n .optional()\n .describe('Only docs whose updatedAt is newer than this many days.'),\n missingFields: z\n .array(z.string())\n .optional()\n .describe(\n 'Field names that should be empty/null. Useful for finding e.g. posts without a coverImage. ' +\n 'Each hit will include a missingFields array confirming which were actually blank.',\n ),\n limit: z\n .number()\n .optional()\n .default(DEFAULT_LIMIT)\n .describe(`Maximum hits per collection (default ${DEFAULT_LIMIT}, max ${HARD_LIMIT}).`),\n },\n handler: async (\n args: {\n collection?: string\n query?: string\n status?: 'draft' | 'published' | 'any'\n olderThanDays?: number\n newerThanDays?: number\n missingFields?: string[]\n limit?: number\n },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const {\n collection,\n query,\n status,\n olderThanDays,\n newerThanDays,\n missingFields,\n limit = DEFAULT_LIMIT,\n } = args\n\n stampMcpContext(req)\n\n const cappedLimit = Math.min(Math.max(1, limit), HARD_LIMIT)\n\n // When filtering by draft status, only collections that support drafts\n // are meaningful — others can't be \"draft\" or \"published\".\n const isDraftStatusFilter = status === 'draft' || status === 'published'\n\n let initialTargets: string[]\n if (!collection) {\n initialTargets = allSlugs\n } else if (collectionSchemas.has(collection)) {\n initialTargets = [collection]\n } else {\n initialTargets = []\n }\n\n const targets = isDraftStatusFilter\n ? initialTargets.filter((slug) => collectionSchemas.get(slug)?.hasDrafts)\n : initialTargets\n\n if (targets.length === 0) {\n return jsonResponse({\n hits: {},\n message: collection\n ? `Unknown collection \"${collection}\". Available: ${allSlugs.join(', ')}`\n : 'No searchable collections found.',\n })\n }\n\n const settled = await Promise.allSettled(\n targets.map((slug) => {\n const schema = collectionSchemas.get(slug)!\n const where = buildWhereClause(schema, {\n query,\n status,\n olderThanDays,\n newerThanDays,\n missingFields,\n })\n return req.payload.find({\n collection: slug as any,\n where: where as any,\n limit: cappedLimit,\n sort: '-updatedAt',\n depth: 0,\n // Include drafts so status='draft' works and missingFields queries\n // against draft-only collections aren't misleadingly empty.\n draft: schema.hasDrafts,\n req,\n overrideAccess: false,\n user: req.user,\n })\n }),\n )\n\n const grouped: Record<string, SearchHit[]> = {}\n const stats: Record<string, { totalDocs: number; returned: number }> = {}\n\n settled.forEach((outcome, i) => {\n if (outcome.status !== 'fulfilled') return\n const slug = targets[i]\n const result = outcome.value\n if (result.totalDocs > 0) {\n stats[slug] = { totalDocs: result.totalDocs, returned: result.docs.length }\n grouped[slug] = result.docs.map((doc: any) => buildHit(doc, missingFields))\n }\n })\n\n const totalHits = Object.values(grouped).reduce((sum, hits) => sum + hits.length, 0)\n\n return jsonResponse({ totalHits, stats, hits: grouped })\n },\n }\n}\n\nfunction buildHit(doc: Record<string, any>, missingFields?: string[]): SearchHit {\n const hit: SearchHit = {\n id: doc.id,\n displayName: doc.name || doc.title || doc.slug || String(doc.id),\n status: doc._status,\n updatedAt: doc.updatedAt,\n }\n\n if (missingFields?.length) {\n hit.missingFields = missingFields.filter((f) => isFieldEmpty(getByPath(doc, f)))\n }\n\n return hit\n}\n\nfunction getByPath(doc: Record<string, any>, path: string): unknown {\n // Walk dotted paths so `meta.description` reads doc.meta?.description rather\n // than the literal key `\"meta.description\"`. Matches the `where` keys we emit.\n let current: any = doc\n for (const segment of path.split('.')) {\n if (current === null || current === undefined) return undefined\n current = current[segment]\n }\n return current\n}\n\nfunction isFieldEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === 'string') return value.trim() === ''\n if (Array.isArray(value)) return value.length === 0\n if (typeof value === 'object') return Object.keys(value as object).length === 0\n return false\n}\n\ninterface FilterArgs {\n query?: string\n status?: 'draft' | 'published' | 'any'\n olderThanDays?: number\n newerThanDays?: number\n missingFields?: string[]\n}\n\nfunction buildWhereClause(schema: CollectionSchema, filters: FilterArgs): Record<string, unknown> {\n const and: Array<Record<string, unknown>> = []\n\n if (filters.query && schema.searchableFields.length > 0) {\n const or = schema.searchableFields.map((field) => ({\n [field]: { like: filters.query },\n }))\n and.push({ or })\n }\n\n // Status filter is only meaningful on draft-enabled collections\n if (filters.status && filters.status !== 'any' && schema.hasDrafts) {\n and.push({ _status: { equals: filters.status } })\n }\n\n if (filters.olderThanDays !== undefined) {\n const cutoff = new Date(Date.now() - filters.olderThanDays * 24 * 60 * 60 * 1000)\n and.push({ updatedAt: { less_than: cutoff.toISOString() } })\n }\n if (filters.newerThanDays !== undefined) {\n const cutoff = new Date(Date.now() - filters.newerThanDays * 24 * 60 * 60 * 1000)\n and.push({ updatedAt: { greater_than: cutoff.toISOString() } })\n }\n\n // Missing-field filter — express as \"field is null OR field doesn't exist\"\n if (filters.missingFields?.length) {\n for (const field of filters.missingFields) {\n and.push({\n or: [{ [field]: { exists: false } }, { [field]: { equals: null } }],\n })\n }\n }\n\n if (and.length === 0) return {}\n if (and.length === 1) return and[0]\n return { and }\n}\n"],"names":["z","jsonResponse","stampMcpContext","DEFAULT_LIMIT","HARD_LIMIT","createSearchContentTool","collectionSchemas","allSlugs","keys","name","description","join","parameters","collection","string","optional","describe","query","status","enum","olderThanDays","number","newerThanDays","missingFields","array","limit","default","handler","args","req","_extra","cappedLimit","Math","min","max","isDraftStatusFilter","initialTargets","has","targets","filter","slug","get","hasDrafts","length","hits","message","settled","Promise","allSettled","map","schema","where","buildWhereClause","payload","find","sort","depth","draft","overrideAccess","user","grouped","stats","forEach","outcome","i","result","value","totalDocs","returned","docs","doc","buildHit","totalHits","Object","values","reduce","sum","hit","id","displayName","title","String","_status","updatedAt","f","isFieldEmpty","getByPath","path","current","segment","split","undefined","trim","Array","isArray","filters","and","searchableFields","or","field","like","push","equals","cutoff","Date","now","less_than","toISOString","greater_than","exists"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/tools/search-content.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport { jsonResponse, stampMcpContext } from './_helpers'\r\n\r\nconst DEFAULT_LIMIT = 20\r\nconst HARD_LIMIT = 100\r\n\r\ninterface SearchHit {\r\n id: string | number\r\n displayName: string\r\n status?: string\r\n updatedAt?: string\r\n missingFields?: string[]\r\n}\r\n\r\n/**\r\n * Creates the searchContent MCP tool — natural-language filters across collections,\r\n * built for editor triage tasks that the official find tools don't express well.\r\n *\r\n * Examples the LLM can drive:\r\n * - \"all posts that are still drafts\"\r\n * - \"pages missing a meta description\"\r\n * - \"anything updated more than 30 days ago\"\r\n * - \"posts by jane in the last quarter\"\r\n *\r\n * Returns compact hits per collection — id, displayName, _status, updatedAt, and\r\n * (when missingFields was requested) which of those fields are blank on each doc.\r\n */\r\nexport function createSearchContentTool(\r\n collectionSchemas: Map<string, CollectionSchema>,\r\n) {\r\n const allSlugs = [...collectionSchemas.keys()]\r\n\r\n return {\r\n name: 'searchContent',\r\n routing: { kind: 'account', action: 'read' } as const,\r\n description:\r\n 'Search and filter content across collections by status, age, missing fields, or free-text query. ' +\r\n 'Designed for editor triage — finding drafts, stale content, content with missing SEO fields, etc. ' +\r\n `Searchable collections: ${allSlugs.join(', ')}.`,\r\n parameters: {\r\n collection: z\r\n .string()\r\n .optional()\r\n .describe('Restrict search to a single collection slug. Omit to search all.'),\r\n query: z\r\n .string()\r\n .optional()\r\n .describe('Free-text query matched against name/title/slug fields (case-insensitive).'),\r\n status: z\r\n .enum(['draft', 'published', 'any'])\r\n .optional()\r\n .describe(\r\n 'Filter by draft status. \"draft\" or \"published\" only return matching docs; \"any\" or omitted returns all.',\r\n ),\r\n olderThanDays: z\r\n .number()\r\n .optional()\r\n .describe('Only docs whose updatedAt is older than this many days.'),\r\n newerThanDays: z\r\n .number()\r\n .optional()\r\n .describe('Only docs whose updatedAt is newer than this many days.'),\r\n missingFields: z\r\n .array(z.string())\r\n .optional()\r\n .describe(\r\n 'Field names that should be empty/null. Useful for finding e.g. posts without a coverImage. ' +\r\n 'Each hit will include a missingFields array confirming which were actually blank.',\r\n ),\r\n limit: z\r\n .number()\r\n .optional()\r\n .default(DEFAULT_LIMIT)\r\n .describe(`Maximum hits per collection (default ${DEFAULT_LIMIT}, max ${HARD_LIMIT}).`),\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 query,\r\n status,\r\n olderThanDays,\r\n newerThanDays,\r\n missingFields,\r\n limit = DEFAULT_LIMIT,\r\n } = args as {\r\n collection?: string\r\n query?: string\r\n status?: 'draft' | 'published' | 'any'\r\n olderThanDays?: number\r\n newerThanDays?: number\r\n missingFields?: string[]\r\n limit?: number\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n const cappedLimit = Math.min(Math.max(1, limit), HARD_LIMIT)\r\n\r\n // When filtering by draft status, only collections that support drafts\r\n // are meaningful — others can't be \"draft\" or \"published\".\r\n const isDraftStatusFilter = status === 'draft' || status === 'published'\r\n\r\n let initialTargets: string[]\r\n if (!collection) {\r\n initialTargets = allSlugs\r\n } else if (collectionSchemas.has(collection)) {\r\n initialTargets = [collection]\r\n } else {\r\n initialTargets = []\r\n }\r\n\r\n const targets = isDraftStatusFilter\r\n ? initialTargets.filter((slug) => collectionSchemas.get(slug)?.hasDrafts)\r\n : initialTargets\r\n\r\n if (targets.length === 0) {\r\n return jsonResponse({\r\n hits: {},\r\n message: collection\r\n ? `Unknown collection \"${collection}\". Available: ${allSlugs.join(', ')}`\r\n : 'No searchable collections found.',\r\n })\r\n }\r\n\r\n const settled = await Promise.allSettled(\r\n targets.map((slug) => {\r\n const schema = collectionSchemas.get(slug)!\r\n const where = buildWhereClause(schema, {\r\n query,\r\n status,\r\n olderThanDays,\r\n newerThanDays,\r\n missingFields,\r\n })\r\n return req.payload.find({\r\n collection: slug as any,\r\n where: where as any,\r\n limit: cappedLimit,\r\n sort: '-updatedAt',\r\n depth: 0,\r\n // Include drafts so status='draft' works and missingFields queries\r\n // against draft-only collections aren't misleadingly empty.\r\n draft: schema.hasDrafts,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n }),\r\n )\r\n\r\n const grouped: Record<string, SearchHit[]> = {}\r\n const stats: Record<string, { totalDocs: number; returned: number }> = {}\r\n\r\n settled.forEach((outcome, i) => {\r\n if (outcome.status !== 'fulfilled') return\r\n const slug = targets[i]\r\n const result = outcome.value\r\n if (result.totalDocs > 0) {\r\n stats[slug] = { totalDocs: result.totalDocs, returned: result.docs.length }\r\n grouped[slug] = result.docs.map((doc: any) => buildHit(doc, missingFields))\r\n }\r\n })\r\n\r\n const totalHits = Object.values(grouped).reduce((sum, hits) => sum + hits.length, 0)\r\n\r\n return jsonResponse({ totalHits, stats, hits: grouped })\r\n },\r\n }\r\n}\r\n\r\nfunction buildHit(doc: Record<string, any>, missingFields?: string[]): SearchHit {\r\n const hit: SearchHit = {\r\n id: doc.id,\r\n displayName: doc.name || doc.title || doc.slug || String(doc.id),\r\n status: doc._status,\r\n updatedAt: doc.updatedAt,\r\n }\r\n\r\n if (missingFields?.length) {\r\n hit.missingFields = missingFields.filter((f) => isFieldEmpty(getByPath(doc, f)))\r\n }\r\n\r\n return hit\r\n}\r\n\r\nfunction getByPath(doc: Record<string, any>, path: string): unknown {\r\n // Walk dotted paths so `meta.description` reads doc.meta?.description rather\r\n // than the literal key `\"meta.description\"`. Matches the `where` keys we emit.\r\n let current: any = doc\r\n for (const segment of path.split('.')) {\r\n if (current === null || current === undefined) return undefined\r\n current = current[segment]\r\n }\r\n return current\r\n}\r\n\r\nfunction isFieldEmpty(value: unknown): boolean {\r\n if (value === null || value === undefined) return true\r\n if (typeof value === 'string') return value.trim() === ''\r\n if (Array.isArray(value)) return value.length === 0\r\n if (typeof value === 'object') return Object.keys(value as object).length === 0\r\n return false\r\n}\r\n\r\ninterface FilterArgs {\r\n query?: string\r\n status?: 'draft' | 'published' | 'any'\r\n olderThanDays?: number\r\n newerThanDays?: number\r\n missingFields?: string[]\r\n}\r\n\r\nfunction buildWhereClause(schema: CollectionSchema, filters: FilterArgs): Record<string, unknown> {\r\n const and: Array<Record<string, unknown>> = []\r\n\r\n if (filters.query && schema.searchableFields.length > 0) {\r\n const or = schema.searchableFields.map((field) => ({\r\n [field]: { like: filters.query },\r\n }))\r\n and.push({ or })\r\n }\r\n\r\n // Status filter is only meaningful on draft-enabled collections\r\n if (filters.status && filters.status !== 'any' && schema.hasDrafts) {\r\n and.push({ _status: { equals: filters.status } })\r\n }\r\n\r\n if (filters.olderThanDays !== undefined) {\r\n const cutoff = new Date(Date.now() - filters.olderThanDays * 24 * 60 * 60 * 1000)\r\n and.push({ updatedAt: { less_than: cutoff.toISOString() } })\r\n }\r\n if (filters.newerThanDays !== undefined) {\r\n const cutoff = new Date(Date.now() - filters.newerThanDays * 24 * 60 * 60 * 1000)\r\n and.push({ updatedAt: { greater_than: cutoff.toISOString() } })\r\n }\r\n\r\n // Missing-field filter — express as \"field is null OR field doesn't exist\"\r\n if (filters.missingFields?.length) {\r\n for (const field of filters.missingFields) {\r\n and.push({\r\n or: [{ [field]: { exists: false } }, { [field]: { equals: null } }],\r\n })\r\n }\r\n }\r\n\r\n if (and.length === 0) return {}\r\n if (and.length === 1) return and[0]\r\n return { and }\r\n}\r\n"],"names":["z","jsonResponse","stampMcpContext","DEFAULT_LIMIT","HARD_LIMIT","createSearchContentTool","collectionSchemas","allSlugs","keys","name","routing","kind","action","description","join","parameters","collection","string","optional","describe","query","status","enum","olderThanDays","number","newerThanDays","missingFields","array","limit","default","handler","args","req","_extra","cappedLimit","Math","min","max","isDraftStatusFilter","initialTargets","has","targets","filter","slug","get","hasDrafts","length","hits","message","settled","Promise","allSettled","map","schema","where","buildWhereClause","payload","find","sort","depth","draft","overrideAccess","user","grouped","stats","forEach","outcome","i","result","value","totalDocs","returned","docs","doc","buildHit","totalHits","Object","values","reduce","sum","hit","id","displayName","title","String","_status","updatedAt","f","isFieldEmpty","getByPath","path","current","segment","split","undefined","trim","Array","isArray","filters","and","searchableFields","or","field","like","push","equals","cutoff","Date","now","less_than","toISOString","greater_than","exists"],"mappings":"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"}
@@ -8,15 +8,15 @@ import type { CollectionSchema } from '../types';
8
8
  */
9
9
  export declare function createUpdateDocumentTool(collectionSchemas: Map<string, CollectionSchema>, draftCollections: Set<string>): {
10
10
  name: string;
11
+ routing: {
12
+ readonly kind: "collection";
13
+ readonly action: "update";
14
+ };
11
15
  description: string;
12
16
  parameters: {
13
17
  collection: z.ZodString;
14
18
  documentId: z.ZodString;
15
19
  data: z.ZodString;
16
20
  };
17
- handler: (args: {
18
- collection: string;
19
- documentId: string;
20
- data: string;
21
- }, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
21
+ handler: (rawArgs: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
22
22
  };
@@ -1,10 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { DRAFT_NOTE, errorMessage, getDocDisplayName, stampMcpContext, textResponse } from './_helpers';
3
3
  const MEDIA_SLUG = 'media';
4
- /**
5
- * Custom replacement for the official plugin's update tools, which fail on
6
- * collections with upload fields due to a Zod schema generation bug. Uses
7
- * Payload's Local API directly, bypassing the problematic schema pipeline.
4
+ /**
5
+ * Custom replacement for the official plugin's update tools, which fail on
6
+ * collections with upload fields due to a Zod schema generation bug. Uses
7
+ * Payload's Local API directly, bypassing the problematic schema pipeline.
8
8
  */ export function createUpdateDocumentTool(collectionSchemas, draftCollections) {
9
9
  const updatableSlugs = [];
10
10
  const descriptionLines = [];
@@ -16,13 +16,18 @@ const MEDIA_SLUG = 'media';
16
16
  const collectionDescriptions = descriptionLines.join('\n');
17
17
  return {
18
18
  name: 'updateDocument',
19
+ routing: {
20
+ kind: 'collection',
21
+ action: 'update'
22
+ },
19
23
  description: 'Update fields on an existing document in any collection. ' + 'Pass only the fields you want to change — unspecified fields are left untouched. ' + 'For draft-enabled collections, updates create a new draft version (use publishDraft to make it live). ' + 'For relationship fields, pass the related document ID (use resolveReference to find IDs). ' + 'For upload fields, pass the media document ID (use uploadMedia to create one first).\n\n' + 'Available collections and their fields:\n' + collectionDescriptions,
20
24
  parameters: {
21
25
  collection: z.string().describe(`The collection slug. One of: ${updatableSlugs.join(', ')}`),
22
26
  documentId: z.string().describe('The ID of the document to update'),
23
27
  data: z.string().describe('JSON string of field names to new values. Only include fields you want to change. ' + 'Examples: \'{"title": "New Title"}\', \'{"featured": true, "category": "category-id"}\', ' + '\'{"tags": ["news", "update"]}\'')
24
28
  },
25
- handler: async (args, req, _extra)=>{
29
+ handler: async (rawArgs, req, _extra)=>{
30
+ const args = rawArgs;
26
31
  const { collection, documentId } = args;
27
32
  let data;
28
33
  try {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/update-document.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { CollectionSchema } from '../types'\nimport {\n DRAFT_NOTE,\n errorMessage,\n getDocDisplayName,\n stampMcpContext,\n textResponse,\n} from './_helpers'\n\nconst MEDIA_SLUG = 'media'\n\n/**\n * Custom replacement for the official plugin's update tools, which fail on\n * collections with upload fields due to a Zod schema generation bug. Uses\n * Payload's Local API directly, bypassing the problematic schema pipeline.\n */\nexport function createUpdateDocumentTool(\n collectionSchemas: Map<string, CollectionSchema>,\n draftCollections: Set<string>,\n) {\n const updatableSlugs: string[] = []\n const descriptionLines: string[] = []\n for (const [slug, schema] of collectionSchemas) {\n if (slug === MEDIA_SLUG) continue\n updatableSlugs.push(slug)\n descriptionLines.push(` - \"${slug}\": ${schema.fields.map((f) => f.name).join(', ')}`)\n }\n const collectionDescriptions = descriptionLines.join('\\n')\n\n return {\n name: 'updateDocument',\n description:\n 'Update fields on an existing document in any collection. ' +\n 'Pass only the fields you want to change — unspecified fields are left untouched. ' +\n 'For draft-enabled collections, updates create a new draft version (use publishDraft to make it live). ' +\n 'For relationship fields, pass the related document ID (use resolveReference to find IDs). ' +\n 'For upload fields, pass the media document ID (use uploadMedia to create one first).\\n\\n' +\n 'Available collections and their fields:\\n' +\n collectionDescriptions,\n parameters: {\n collection: z\n .string()\n .describe(`The collection slug. One of: ${updatableSlugs.join(', ')}`),\n documentId: z\n .string()\n .describe('The ID of the document to update'),\n data: z\n .string()\n .describe(\n 'JSON string of field names to new values. Only include fields you want to change. ' +\n 'Examples: \\'{\"title\": \"New Title\"}\\', \\'{\"featured\": true, \"category\": \"category-id\"}\\', ' +\n '\\'{\"tags\": [\"news\", \"update\"]}\\'',\n ),\n },\n handler: async (\n args: { collection: string; documentId: string; data: string },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const { collection, documentId } = args\n\n let data: Record<string, unknown>\n try {\n data = JSON.parse(args.data)\n } catch {\n return textResponse(\n 'Error: \"data\" must be a valid JSON string. Example: \\'{\"title\": \"New Title\"}\\'',\n )\n }\n\n if (!collectionSchemas.has(collection)) {\n return textResponse(\n `Error: Unknown collection \"${collection}\". Available: ${updatableSlugs.join(', ')}`,\n )\n }\n\n if (collection === MEDIA_SLUG) {\n return textResponse('Error: Use the uploadMedia tool to manage media files.')\n }\n\n if (!data || Object.keys(data).length === 0) {\n return textResponse(\n 'Error: No fields provided in \"data\". Pass an object with field names and values to update.',\n )\n }\n\n stampMcpContext(req)\n\n const isDraftCollection = draftCollections.has(collection)\n\n try {\n const doc = await req.payload.update({\n collection: collection as any,\n id: documentId,\n data: data as any,\n draft: isDraftCollection,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const displayName = getDocDisplayName(doc, documentId)\n const updatedFields = Object.keys(data).join(', ')\n const draftNote = isDraftCollection ? DRAFT_NOTE : ''\n\n return textResponse(\n `Updated \"${displayName}\" in ${collection} (ID: ${documentId}). ` +\n `Changed fields: ${updatedFields}.${draftNote}`,\n )\n } catch (error) {\n return textResponse(\n `Error updating document ${documentId} in ${collection}: ${errorMessage(error)}`,\n )\n }\n },\n }\n}\n"],"names":["z","DRAFT_NOTE","errorMessage","getDocDisplayName","stampMcpContext","textResponse","MEDIA_SLUG","createUpdateDocumentTool","collectionSchemas","draftCollections","updatableSlugs","descriptionLines","slug","schema","push","fields","map","f","name","join","collectionDescriptions","description","parameters","collection","string","describe","documentId","data","handler","args","req","_extra","JSON","parse","has","Object","keys","length","isDraftCollection","doc","payload","update","id","draft","overrideAccess","user","displayName","updatedFields","draftNote","error"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,UAAU,EACVC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,EACfC,YAAY,QACP,aAAY;AAEnB,MAAMC,aAAa;AAEnB;;;;CAIC,GACD,OAAO,SAASC,yBACdC,iBAAgD,EAChDC,gBAA6B;IAE7B,MAAMC,iBAA2B,EAAE;IACnC,MAAMC,mBAA6B,EAAE;IACrC,KAAK,MAAM,CAACC,MAAMC,OAAO,IAAIL,kBAAmB;QAC9C,IAAII,SAASN,YAAY;QACzBI,eAAeI,IAAI,CAACF;QACpBD,iBAAiBG,IAAI,CAAC,CAAC,KAAK,EAAEF,KAAK,GAAG,EAAEC,OAAOE,MAAM,CAACC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,EAAEC,IAAI,CAAC,OAAO;IACvF;IACA,MAAMC,yBAAyBT,iBAAiBQ,IAAI,CAAC;IAErD,OAAO;QACLD,MAAM;QACNG,aACE,8DACA,sFACA,2GACA,+FACA,6FACA,8CACAD;QACFE,YAAY;YACVC,YAAYvB,EACTwB,MAAM,GACNC,QAAQ,CAAC,CAAC,6BAA6B,EAAEf,eAAeS,IAAI,CAAC,OAAO;YACvEO,YAAY1B,EACTwB,MAAM,GACNC,QAAQ,CAAC;YACZE,MAAM3B,EACHwB,MAAM,GACNC,QAAQ,CACP,uFACA,8FACA;QAEN;QACAG,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAER,UAAU,EAAEG,UAAU,EAAE,GAAGG;YAEnC,IAAIF;YACJ,IAAI;gBACFA,OAAOK,KAAKC,KAAK,CAACJ,KAAKF,IAAI;YAC7B,EAAE,OAAM;gBACN,OAAOtB,aACL;YAEJ;YAEA,IAAI,CAACG,kBAAkB0B,GAAG,CAACX,aAAa;gBACtC,OAAOlB,aACL,CAAC,2BAA2B,EAAEkB,WAAW,cAAc,EAAEb,eAAeS,IAAI,CAAC,OAAO;YAExF;YAEA,IAAII,eAAejB,YAAY;gBAC7B,OAAOD,aAAa;YACtB;YAEA,IAAI,CAACsB,QAAQQ,OAAOC,IAAI,CAACT,MAAMU,MAAM,KAAK,GAAG;gBAC3C,OAAOhC,aACL;YAEJ;YAEAD,gBAAgB0B;YAEhB,MAAMQ,oBAAoB7B,iBAAiByB,GAAG,CAACX;YAE/C,IAAI;gBACF,MAAMgB,MAAM,MAAMT,IAAIU,OAAO,CAACC,MAAM,CAAC;oBACnClB,YAAYA;oBACZmB,IAAIhB;oBACJC,MAAMA;oBACNgB,OAAOL;oBACPR;oBACAc,gBAAgB;oBAChBC,MAAMf,IAAIe,IAAI;gBAChB;gBAEA,MAAMC,cAAc3C,kBAAkBoC,KAAKb;gBAC3C,MAAMqB,gBAAgBZ,OAAOC,IAAI,CAACT,MAAMR,IAAI,CAAC;gBAC7C,MAAM6B,YAAYV,oBAAoBrC,aAAa;gBAEnD,OAAOI,aACL,CAAC,SAAS,EAAEyC,YAAY,KAAK,EAAEvB,WAAW,MAAM,EAAEG,WAAW,GAAG,CAAC,GAC/D,CAAC,gBAAgB,EAAEqB,cAAc,CAAC,EAAEC,WAAW;YAErD,EAAE,OAAOC,OAAO;gBACd,OAAO5C,aACL,CAAC,wBAAwB,EAAEqB,WAAW,IAAI,EAAEH,WAAW,EAAE,EAAErB,aAAa+C,QAAQ;YAEpF;QACF;IACF;AACF"}
1
+ {"version":3,"sources":["../../src/tools/update-document.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport {\r\n DRAFT_NOTE,\r\n errorMessage,\r\n getDocDisplayName,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\nconst MEDIA_SLUG = 'media'\r\n\r\n/**\r\n * Custom replacement for the official plugin's update tools, which fail on\r\n * collections with upload fields due to a Zod schema generation bug. Uses\r\n * Payload's Local API directly, bypassing the problematic schema pipeline.\r\n */\r\nexport function createUpdateDocumentTool(\r\n collectionSchemas: Map<string, CollectionSchema>,\r\n draftCollections: Set<string>,\r\n) {\r\n const updatableSlugs: string[] = []\r\n const descriptionLines: string[] = []\r\n for (const [slug, schema] of collectionSchemas) {\r\n if (slug === MEDIA_SLUG) continue\r\n updatableSlugs.push(slug)\r\n descriptionLines.push(` - \"${slug}\": ${schema.fields.map((f) => f.name).join(', ')}`)\r\n }\r\n const collectionDescriptions = descriptionLines.join('\\n')\r\n\r\n return {\r\n name: 'updateDocument',\r\n routing: { kind: 'collection', action: 'update' } as const,\r\n description:\r\n 'Update fields on an existing document in any collection. ' +\r\n 'Pass only the fields you want to change — unspecified fields are left untouched. ' +\r\n 'For draft-enabled collections, updates create a new draft version (use publishDraft to make it live). ' +\r\n 'For relationship fields, pass the related document ID (use resolveReference to find IDs). ' +\r\n 'For upload fields, pass the media document ID (use uploadMedia to create one first).\\n\\n' +\r\n 'Available collections and their fields:\\n' +\r\n collectionDescriptions,\r\n parameters: {\r\n collection: z\r\n .string()\r\n .describe(`The collection slug. One of: ${updatableSlugs.join(', ')}`),\r\n documentId: z\r\n .string()\r\n .describe('The ID of the document to update'),\r\n data: z\r\n .string()\r\n .describe(\r\n 'JSON string of field names to new values. Only include fields you want to change. ' +\r\n 'Examples: \\'{\"title\": \"New Title\"}\\', \\'{\"featured\": true, \"category\": \"category-id\"}\\', ' +\r\n '\\'{\"tags\": [\"news\", \"update\"]}\\'',\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; data: string }\r\n const { collection, documentId } = args\r\n\r\n let data: Record<string, unknown>\r\n try {\r\n data = JSON.parse(args.data)\r\n } catch {\r\n return textResponse(\r\n 'Error: \"data\" must be a valid JSON string. Example: \\'{\"title\": \"New Title\"}\\'',\r\n )\r\n }\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Available: ${updatableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n if (collection === MEDIA_SLUG) {\r\n return textResponse('Error: Use the uploadMedia tool to manage media files.')\r\n }\r\n\r\n if (!data || Object.keys(data).length === 0) {\r\n return textResponse(\r\n 'Error: No fields provided in \"data\". Pass an object with field names and values to update.',\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n const isDraftCollection = draftCollections.has(collection)\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: data 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(doc, documentId)\r\n const updatedFields = Object.keys(data).join(', ')\r\n const draftNote = isDraftCollection ? DRAFT_NOTE : ''\r\n\r\n return textResponse(\r\n `Updated \"${displayName}\" in ${collection} (ID: ${documentId}). ` +\r\n `Changed fields: ${updatedFields}.${draftNote}`,\r\n )\r\n } catch (error) {\r\n return textResponse(\r\n `Error updating document ${documentId} in ${collection}: ${errorMessage(error)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","DRAFT_NOTE","errorMessage","getDocDisplayName","stampMcpContext","textResponse","MEDIA_SLUG","createUpdateDocumentTool","collectionSchemas","draftCollections","updatableSlugs","descriptionLines","slug","schema","push","fields","map","f","name","join","collectionDescriptions","routing","kind","action","description","parameters","collection","string","describe","documentId","data","handler","rawArgs","req","_extra","args","JSON","parse","has","Object","keys","length","isDraftCollection","doc","payload","update","id","draft","overrideAccess","user","displayName","updatedFields","draftNote","error"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,UAAU,EACVC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,EACfC,YAAY,QACP,aAAY;AAEnB,MAAMC,aAAa;AAEnB;;;;CAIC,GACD,OAAO,SAASC,yBACdC,iBAAgD,EAChDC,gBAA6B;IAE7B,MAAMC,iBAA2B,EAAE;IACnC,MAAMC,mBAA6B,EAAE;IACrC,KAAK,MAAM,CAACC,MAAMC,OAAO,IAAIL,kBAAmB;QAC9C,IAAII,SAASN,YAAY;QACzBI,eAAeI,IAAI,CAACF;QACpBD,iBAAiBG,IAAI,CAAC,CAAC,KAAK,EAAEF,KAAK,GAAG,EAAEC,OAAOE,MAAM,CAACC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,EAAEC,IAAI,CAAC,OAAO;IACvF;IACA,MAAMC,yBAAyBT,iBAAiBQ,IAAI,CAAC;IAErD,OAAO;QACLD,MAAM;QACNG,SAAS;YAAEC,MAAM;YAAcC,QAAQ;QAAS;QAChDC,aACE,8DACA,sFACA,2GACA,+FACA,6FACA,8CACAJ;QACFK,YAAY;YACVC,YAAY1B,EACT2B,MAAM,GACNC,QAAQ,CAAC,CAAC,6BAA6B,EAAElB,eAAeS,IAAI,CAAC,OAAO;YACvEU,YAAY7B,EACT2B,MAAM,GACNC,QAAQ,CAAC;YACZE,MAAM9B,EACH2B,MAAM,GACNC,QAAQ,CACP,uFACA,8FACA;QAEN;QACAG,SAAS,OACPC,SACAC,KACAC;YAEA,MAAMC,OAAOH;YACb,MAAM,EAAEN,UAAU,EAAEG,UAAU,EAAE,GAAGM;YAEnC,IAAIL;YACJ,IAAI;gBACFA,OAAOM,KAAKC,KAAK,CAACF,KAAKL,IAAI;YAC7B,EAAE,OAAM;gBACN,OAAOzB,aACL;YAEJ;YAEA,IAAI,CAACG,kBAAkB8B,GAAG,CAACZ,aAAa;gBACtC,OAAOrB,aACL,CAAC,2BAA2B,EAAEqB,WAAW,cAAc,EAAEhB,eAAeS,IAAI,CAAC,OAAO;YAExF;YAEA,IAAIO,eAAepB,YAAY;gBAC7B,OAAOD,aAAa;YACtB;YAEA,IAAI,CAACyB,QAAQS,OAAOC,IAAI,CAACV,MAAMW,MAAM,KAAK,GAAG;gBAC3C,OAAOpC,aACL;YAEJ;YAEAD,gBAAgB6B;YAEhB,MAAMS,oBAAoBjC,iBAAiB6B,GAAG,CAACZ;YAE/C,IAAI;gBACF,MAAMiB,MAAM,MAAMV,IAAIW,OAAO,CAACC,MAAM,CAAC;oBACnCnB,YAAYA;oBACZoB,IAAIjB;oBACJC,MAAMA;oBACNiB,OAAOL;oBACPT;oBACAe,gBAAgB;oBAChBC,MAAMhB,IAAIgB,IAAI;gBAChB;gBAEA,MAAMC,cAAc/C,kBAAkBwC,KAAKd;gBAC3C,MAAMsB,gBAAgBZ,OAAOC,IAAI,CAACV,MAAMX,IAAI,CAAC;gBAC7C,MAAMiC,YAAYV,oBAAoBzC,aAAa;gBAEnD,OAAOI,aACL,CAAC,SAAS,EAAE6C,YAAY,KAAK,EAAExB,WAAW,MAAM,EAAEG,WAAW,GAAG,CAAC,GAC/D,CAAC,gBAAgB,EAAEsB,cAAc,CAAC,EAAEC,WAAW;YAErD,EAAE,OAAOC,OAAO;gBACd,OAAOhD,aACL,CAAC,wBAAwB,EAAEwB,WAAW,IAAI,EAAEH,WAAW,EAAE,EAAExB,aAAamD,QAAQ;YAEpF;QACF;IACF;AACF"}
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ import type { PayloadRequest } from 'payload';
3
+ import type { GlobalSchema } from '../types';
4
+ /**
5
+ * Partial-merge update for any global. Same prose-input contract as
6
+ * `updateDocument`: `data` is a JSON string of field-name → value pairs,
7
+ * unspecified fields are left untouched, and the response message lists
8
+ * the changed top-level field names.
9
+ *
10
+ * For draft-enabled globals, the update lands as a new draft (mirrors the
11
+ * `always-draft` default for draft-enabled collections); use
12
+ * `publishGlobalDraft` to make it live.
13
+ */
14
+ export declare function createUpdateGlobalTool(globalSchemas: Map<string, GlobalSchema>, draftGlobals: Set<string>): {
15
+ name: string;
16
+ routing: {
17
+ readonly kind: "global";
18
+ readonly action: "update";
19
+ };
20
+ description: string;
21
+ parameters: {
22
+ slug: z.ZodEnum<[string, ...string[]]>;
23
+ data: z.ZodString;
24
+ locale: z.ZodOptional<z.ZodString>;
25
+ };
26
+ handler: (rawArgs: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
27
+ };
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import { DRAFT_NOTE, errorMessage, slugEnum, stampMcpContext, textResponse } from './_helpers';
3
+ /**
4
+ * Partial-merge update for any global. Same prose-input contract as
5
+ * `updateDocument`: `data` is a JSON string of field-name → value pairs,
6
+ * unspecified fields are left untouched, and the response message lists
7
+ * the changed top-level field names.
8
+ *
9
+ * For draft-enabled globals, the update lands as a new draft (mirrors the
10
+ * `always-draft` default for draft-enabled collections); use
11
+ * `publishGlobalDraft` to make it live.
12
+ */ export function createUpdateGlobalTool(globalSchemas, draftGlobals) {
13
+ const updatableSlugs = [
14
+ ...globalSchemas.keys()
15
+ ];
16
+ const descriptionLines = [];
17
+ for (const [slug, schema] of globalSchemas){
18
+ descriptionLines.push(` - "${slug}": ${schema.fields.map((f)=>f.name).join(', ')}`);
19
+ }
20
+ const globalDescriptions = descriptionLines.join('\n');
21
+ return {
22
+ name: 'updateGlobal',
23
+ routing: {
24
+ kind: 'global',
25
+ action: 'update'
26
+ },
27
+ description: 'Update fields on a global (site-wide settings singleton). ' + 'Pass only the fields you want to change — unspecified fields are left untouched. ' + 'For draft-enabled globals, updates create a new draft (use publishGlobalDraft to make it live).\n\n' + 'Available globals and their fields:\n' + globalDescriptions,
28
+ parameters: {
29
+ slug: slugEnum(updatableSlugs, 'global').describe(`The global slug. One of: ${updatableSlugs.join(', ')}`),
30
+ data: z.string().describe('JSON string of field names to new values. Only include fields you want to change. ' + 'Examples: \'{"siteName":"Acme"}\', \'{"social":{"twitter":"@acme"}}\'.'),
31
+ locale: z.string().optional().describe('Optional locale code (e.g. "en", "fr") to scope the update to a single locale on localized fields.')
32
+ },
33
+ handler: async (rawArgs, req, _extra)=>{
34
+ const args = rawArgs;
35
+ const { slug, locale } = args;
36
+ let data;
37
+ try {
38
+ data = JSON.parse(args.data);
39
+ } catch {
40
+ return textResponse('Error: "data" must be a valid JSON string. Example: \'{"siteName":"Acme"}\'');
41
+ }
42
+ if (!globalSchemas.has(slug)) {
43
+ return textResponse(`Error: Unknown global "${slug}". Available: ${updatableSlugs.join(', ')}`);
44
+ }
45
+ if (!data || typeof data !== 'object' || Array.isArray(data) || Object.keys(data).length === 0) {
46
+ return textResponse('Error: No fields provided in "data". Pass an object with field names and values to update.');
47
+ }
48
+ stampMcpContext(req);
49
+ const isDraftGlobal = draftGlobals.has(slug);
50
+ try {
51
+ await req.payload.updateGlobal({
52
+ slug: slug,
53
+ data: data,
54
+ draft: isDraftGlobal,
55
+ ...locale ? {
56
+ locale: locale
57
+ } : {},
58
+ req,
59
+ overrideAccess: false,
60
+ user: req.user
61
+ });
62
+ const updatedFields = Object.keys(data).join(', ');
63
+ const draftNote = isDraftGlobal ? DRAFT_NOTE : '';
64
+ return textResponse(`Updated global "${slug}". Changed fields: ${updatedFields}.${draftNote}`);
65
+ } catch (err) {
66
+ return textResponse(`Error updating global "${slug}": ${errorMessage(err)}`);
67
+ }
68
+ }
69
+ };
70
+ }
71
+
72
+ //# sourceMappingURL=update-global.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tools/update-global.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport type { GlobalSchema } from '../types'\r\nimport { DRAFT_NOTE, errorMessage, slugEnum, stampMcpContext, textResponse } from './_helpers'\r\n\r\ninterface UpdateGlobalArgs {\r\n slug: string\r\n data: string\r\n locale?: string\r\n}\r\n\r\n/**\r\n * Partial-merge update for any global. Same prose-input contract as\r\n * `updateDocument`: `data` is a JSON string of field-name → value pairs,\r\n * unspecified fields are left untouched, and the response message lists\r\n * the changed top-level field names.\r\n *\r\n * For draft-enabled globals, the update lands as a new draft (mirrors the\r\n * `always-draft` default for draft-enabled collections); use\r\n * `publishGlobalDraft` to make it live.\r\n */\r\nexport function createUpdateGlobalTool(\r\n globalSchemas: Map<string, GlobalSchema>,\r\n draftGlobals: Set<string>,\r\n) {\r\n const updatableSlugs = [...globalSchemas.keys()]\r\n const descriptionLines: string[] = []\r\n for (const [slug, schema] of globalSchemas) {\r\n descriptionLines.push(` - \"${slug}\": ${schema.fields.map((f) => f.name).join(', ')}`)\r\n }\r\n const globalDescriptions = descriptionLines.join('\\n')\r\n\r\n return {\r\n name: 'updateGlobal',\r\n routing: { kind: 'global', action: 'update' } as const,\r\n description:\r\n 'Update fields on a global (site-wide settings singleton). ' +\r\n 'Pass only the fields you want to change — unspecified fields are left untouched. ' +\r\n 'For draft-enabled globals, updates create a new draft (use publishGlobalDraft to make it live).\\n\\n' +\r\n 'Available globals and their fields:\\n' +\r\n globalDescriptions,\r\n parameters: {\r\n slug: slugEnum(updatableSlugs, 'global').describe(\r\n `The global slug. One of: ${updatableSlugs.join(', ')}`,\r\n ),\r\n data: z\r\n .string()\r\n .describe(\r\n 'JSON string of field names to new values. Only include fields you want to change. ' +\r\n 'Examples: \\'{\"siteName\":\"Acme\"}\\', \\'{\"social\":{\"twitter\":\"@acme\"}}\\'.',\r\n ),\r\n locale: z\r\n .string()\r\n .optional()\r\n .describe(\r\n 'Optional locale code (e.g. \"en\", \"fr\") to scope the update to a single locale on localized fields.',\r\n ),\r\n },\r\n handler: async (rawArgs: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => {\r\n const args = rawArgs as unknown as UpdateGlobalArgs\r\n const { slug, locale } = args\r\n\r\n let data: Record<string, unknown>\r\n try {\r\n data = JSON.parse(args.data)\r\n } catch {\r\n return textResponse(\r\n 'Error: \"data\" must be a valid JSON string. Example: \\'{\"siteName\":\"Acme\"}\\'',\r\n )\r\n }\r\n\r\n if (!globalSchemas.has(slug)) {\r\n return textResponse(\r\n `Error: Unknown global \"${slug}\". Available: ${updatableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n if (!data || typeof data !== 'object' || Array.isArray(data) || Object.keys(data).length === 0) {\r\n return textResponse(\r\n 'Error: No fields provided in \"data\". Pass an object with field names and values to update.',\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n const isDraftGlobal = draftGlobals.has(slug)\r\n\r\n try {\r\n await req.payload.updateGlobal({\r\n slug: slug as never,\r\n data: data as never,\r\n draft: isDraftGlobal,\r\n ...(locale ? { locale: locale as never } : {}),\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n\r\n const updatedFields = Object.keys(data).join(', ')\r\n const draftNote = isDraftGlobal ? DRAFT_NOTE : ''\r\n return textResponse(\r\n `Updated global \"${slug}\". Changed fields: ${updatedFields}.${draftNote}`,\r\n )\r\n } catch (err) {\r\n return textResponse(`Error updating global \"${slug}\": ${errorMessage(err)}`)\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","DRAFT_NOTE","errorMessage","slugEnum","stampMcpContext","textResponse","createUpdateGlobalTool","globalSchemas","draftGlobals","updatableSlugs","keys","descriptionLines","slug","schema","push","fields","map","f","name","join","globalDescriptions","routing","kind","action","description","parameters","describe","data","string","locale","optional","handler","rawArgs","req","_extra","args","JSON","parse","has","Array","isArray","Object","length","isDraftGlobal","payload","updateGlobal","draft","overrideAccess","user","updatedFields","draftNote","err"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SAASC,UAAU,EAAEC,YAAY,EAAEC,QAAQ,EAAEC,eAAe,EAAEC,YAAY,QAAQ,aAAY;AAQ9F;;;;;;;;;CASC,GACD,OAAO,SAASC,uBACdC,aAAwC,EACxCC,YAAyB;IAEzB,MAAMC,iBAAiB;WAAIF,cAAcG,IAAI;KAAG;IAChD,MAAMC,mBAA6B,EAAE;IACrC,KAAK,MAAM,CAACC,MAAMC,OAAO,IAAIN,cAAe;QAC1CI,iBAAiBG,IAAI,CAAC,CAAC,KAAK,EAAEF,KAAK,GAAG,EAAEC,OAAOE,MAAM,CAACC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,EAAEC,IAAI,CAAC,OAAO;IACvF;IACA,MAAMC,qBAAqBT,iBAAiBQ,IAAI,CAAC;IAEjD,OAAO;QACLD,MAAM;QACNG,SAAS;YAAEC,MAAM;YAAUC,QAAQ;QAAS;QAC5CC,aACE,+DACA,sFACA,wGACA,0CACAJ;QACFK,YAAY;YACVb,MAAMT,SAASM,gBAAgB,UAAUiB,QAAQ,CAC/C,CAAC,yBAAyB,EAAEjB,eAAeU,IAAI,CAAC,OAAO;YAEzDQ,MAAM3B,EACH4B,MAAM,GACNF,QAAQ,CACP,uFACE;YAENG,QAAQ7B,EACL4B,MAAM,GACNE,QAAQ,GACRJ,QAAQ,CACP;QAEN;QACAK,SAAS,OAAOC,SAAkCC,KAAqBC;YACrE,MAAMC,OAAOH;YACb,MAAM,EAAEpB,IAAI,EAAEiB,MAAM,EAAE,GAAGM;YAEzB,IAAIR;YACJ,IAAI;gBACFA,OAAOS,KAAKC,KAAK,CAACF,KAAKR,IAAI;YAC7B,EAAE,OAAM;gBACN,OAAOtB,aACL;YAEJ;YAEA,IAAI,CAACE,cAAc+B,GAAG,CAAC1B,OAAO;gBAC5B,OAAOP,aACL,CAAC,uBAAuB,EAAEO,KAAK,cAAc,EAAEH,eAAeU,IAAI,CAAC,OAAO;YAE9E;YAEA,IAAI,CAACQ,QAAQ,OAAOA,SAAS,YAAYY,MAAMC,OAAO,CAACb,SAASc,OAAO/B,IAAI,CAACiB,MAAMe,MAAM,KAAK,GAAG;gBAC9F,OAAOrC,aACL;YAEJ;YAEAD,gBAAgB6B;YAChB,MAAMU,gBAAgBnC,aAAa8B,GAAG,CAAC1B;YAEvC,IAAI;gBACF,MAAMqB,IAAIW,OAAO,CAACC,YAAY,CAAC;oBAC7BjC,MAAMA;oBACNe,MAAMA;oBACNmB,OAAOH;oBACP,GAAId,SAAS;wBAAEA,QAAQA;oBAAgB,IAAI,CAAC,CAAC;oBAC7CI;oBACAc,gBAAgB;oBAChBC,MAAMf,IAAIe,IAAI;gBAChB;gBAEA,MAAMC,gBAAgBR,OAAO/B,IAAI,CAACiB,MAAMR,IAAI,CAAC;gBAC7C,MAAM+B,YAAYP,gBAAgB1C,aAAa;gBAC/C,OAAOI,aACL,CAAC,gBAAgB,EAAEO,KAAK,mBAAmB,EAAEqC,cAAc,CAAC,EAAEC,WAAW;YAE7E,EAAE,OAAOC,KAAK;gBACZ,OAAO9C,aAAa,CAAC,uBAAuB,EAAEO,KAAK,GAAG,EAAEV,aAAaiD,MAAM;YAC7E;QACF;IACF;AACF"}
@@ -5,13 +5,14 @@ export declare function createUploadMediaTool(options?: {
5
5
  collectionSlug?: string;
6
6
  }): {
7
7
  name: string;
8
+ routing: {
9
+ readonly kind: "account";
10
+ readonly action: "create";
11
+ };
8
12
  description: string;
9
13
  parameters: {
10
14
  url: z.ZodString;
11
15
  alt: z.ZodOptional<z.ZodString>;
12
16
  };
13
- handler: (args: {
14
- url: string;
15
- alt?: string;
16
- }, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
17
+ handler: (rawArgs: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
17
18
  };
@@ -13,12 +13,17 @@ export function createUploadMediaTool(options) {
13
13
  const mediaSlug = options?.collectionSlug ?? 'media';
14
14
  return {
15
15
  name: 'uploadMedia',
16
+ routing: {
17
+ kind: 'account',
18
+ action: 'create'
19
+ },
16
20
  description: 'Upload an image to the media library from a public HTTPS URL. ' + 'Fetches the image with SSRF protection, validates it is an allowed image type ' + '(JPEG, PNG, WebP, GIF), and creates a Media document with alt text. ' + 'Returns the created document ID, filename, and alt text.',
17
21
  parameters: {
18
22
  url: z.string().url().describe('Public HTTPS URL of the image to upload'),
19
23
  alt: z.string().optional().describe('Alt text for the image. Generates from filename if omitted.')
20
24
  },
21
- handler: async (args, req, _extra)=>{
25
+ handler: async (rawArgs, req, _extra)=>{
26
+ const args = rawArgs;
22
27
  const { url } = args;
23
28
  let buffer;
24
29
  let contentType;