payload-mcp-toolkit 0.7.0 → 0.7.5

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 (56) hide show
  1. package/README.md +30 -9
  2. package/dist/api-keys.js +57 -21
  3. package/dist/api-keys.js.map +1 -1
  4. package/dist/auth-strategy.d.ts +18 -7
  5. package/dist/auth-strategy.js +54 -12
  6. package/dist/auth-strategy.js.map +1 -1
  7. package/dist/tools/_helpers.d.ts +34 -0
  8. package/dist/tools/_helpers.js +98 -0
  9. package/dist/tools/_helpers.js.map +1 -1
  10. package/dist/tools/create-document.js +8 -0
  11. package/dist/tools/create-document.js.map +1 -1
  12. package/dist/tools/delete-document.d.ts +1 -1
  13. package/dist/tools/delete-document.js +6 -6
  14. package/dist/tools/delete-document.js.map +1 -1
  15. package/dist/tools/find-document.d.ts +3 -3
  16. package/dist/tools/find-document.js +8 -8
  17. package/dist/tools/find-document.js.map +1 -1
  18. package/dist/tools/publish-draft.js +33 -1
  19. package/dist/tools/publish-draft.js.map +1 -1
  20. package/dist/tools/publish-global-draft.js +30 -1
  21. package/dist/tools/publish-global-draft.js.map +1 -1
  22. package/package.json +29 -15
  23. package/dist/__tests__/api-keys.test.js +0 -292
  24. package/dist/__tests__/api-keys.test.js.map +0 -1
  25. package/dist/__tests__/auth-strategy.test.js +0 -681
  26. package/dist/__tests__/auth-strategy.test.js.map +0 -1
  27. package/dist/__tests__/conflict-detection.test.js +0 -69
  28. package/dist/__tests__/conflict-detection.test.js.map +0 -1
  29. package/dist/__tests__/delete-document.test.js +0 -70
  30. package/dist/__tests__/delete-document.test.js.map +0 -1
  31. package/dist/__tests__/endpoint.test.js +0 -143
  32. package/dist/__tests__/endpoint.test.js.map +0 -1
  33. package/dist/__tests__/find-document.test.js +0 -178
  34. package/dist/__tests__/find-document.test.js.map +0 -1
  35. package/dist/__tests__/find-global.test.js +0 -173
  36. package/dist/__tests__/find-global.test.js.map +0 -1
  37. package/dist/__tests__/global-versions.test.js +0 -183
  38. package/dist/__tests__/global-versions.test.js.map +0 -1
  39. package/dist/__tests__/hash.test.js +0 -58
  40. package/dist/__tests__/hash.test.js.map +0 -1
  41. package/dist/__tests__/index-integration.test.js +0 -191
  42. package/dist/__tests__/index-integration.test.js.map +0 -1
  43. package/dist/__tests__/introspection.test.js +0 -659
  44. package/dist/__tests__/introspection.test.js.map +0 -1
  45. package/dist/__tests__/patch-global-layout.test.js +0 -474
  46. package/dist/__tests__/patch-global-layout.test.js.map +0 -1
  47. package/dist/__tests__/patch-layout.test.js +0 -171
  48. package/dist/__tests__/patch-layout.test.js.map +0 -1
  49. package/dist/__tests__/registry.test.js +0 -795
  50. package/dist/__tests__/registry.test.js.map +0 -1
  51. package/dist/__tests__/resources.test.js +0 -139
  52. package/dist/__tests__/resources.test.js.map +0 -1
  53. package/dist/__tests__/update-global.test.js +0 -157
  54. package/dist/__tests__/update-global.test.js.map +0 -1
  55. package/dist/__tests__/url-validator.test.js +0 -326
  56. package/dist/__tests__/url-validator.test.js.map +0 -1
@@ -31,6 +31,104 @@ export function jsonResponse(payload) {
31
31
  export function errorMessage(error) {
32
32
  return error instanceof Error ? error.message : String(error);
33
33
  }
34
+ function asIsoString(v) {
35
+ if (typeof v === 'string') return v;
36
+ if (v instanceof Date) return v.toISOString();
37
+ return undefined;
38
+ }
39
+ /**
40
+ * Capture the document's current `updatedAt` BEFORE a publish attempt so
41
+ * the recovery branch can tell "this attempt landed despite a post-write
42
+ * validator throw" from "an older publish was successful and this attempt
43
+ * did nothing". A missing snapshot is non-fatal — the recovery branch
44
+ * conservatively falls through to the original error in that case.
45
+ */ export async function snapshotPublishMarker(req, target) {
46
+ try {
47
+ const pre = target.kind === 'collection' ? await req.payload.findByID({
48
+ collection: target.slug,
49
+ id: target.id,
50
+ draft: true,
51
+ depth: 0,
52
+ req,
53
+ overrideAccess: false,
54
+ user: req.user
55
+ }) : await req.payload.findGlobal({
56
+ slug: target.slug,
57
+ draft: true,
58
+ depth: 0,
59
+ // `fallbackLocale: false` disables Payload's locale-fallback so
60
+ // the read returns the literal state of the requested locale.
61
+ // Without this, a localized global with fallbackLocale='en'
62
+ // could report the 'en' updatedAt while the caller is
63
+ // publishing 'de', producing a false-positive in
64
+ // verifyPublishSucceededDespiteError.
65
+ ...target.locale ? {
66
+ locale: target.locale,
67
+ fallbackLocale: false
68
+ } : {},
69
+ req,
70
+ overrideAccess: false,
71
+ user: req.user
72
+ });
73
+ return asIsoString(pre?.updatedAt);
74
+ } catch {
75
+ return undefined;
76
+ }
77
+ }
78
+ /**
79
+ * After a Payload update throws on a publish call, determine whether the
80
+ * publish actually landed despite the error. Returns the live document
81
+ * only when (a) the live `_status` is 'published' AND (b) `updatedAt`
82
+ * strictly advanced past the pre-update snapshot — i.e. the current
83
+ * attempt produced the published row. Without the strictly-newer check,
84
+ * a pre-existing published version from an earlier successful publish
85
+ * would mask a real failure of the current attempt.
86
+ *
87
+ * Returns null on:
88
+ * - missing pre-snapshot (cannot disambiguate; conservative)
89
+ * - verify read failure (do not mask the original error with a
90
+ * secondary read error)
91
+ * - live `_status` not 'published'
92
+ * - live `updatedAt` not strictly newer than the pre-snapshot
93
+ */ export async function verifyPublishSucceededDespiteError(req, target, preUpdatedAt) {
94
+ if (!preUpdatedAt) return null;
95
+ try {
96
+ const live = target.kind === 'collection' ? await req.payload.findByID({
97
+ collection: target.slug,
98
+ id: target.id,
99
+ draft: false,
100
+ depth: 0,
101
+ req,
102
+ overrideAccess: false,
103
+ user: req.user
104
+ }) : await req.payload.findGlobal({
105
+ slug: target.slug,
106
+ draft: false,
107
+ depth: 0,
108
+ // Disable Payload locale-fallback (see snapshotPublishMarker
109
+ // note) so verify reads the literal state of the locale that
110
+ // updateGlobal was called against.
111
+ ...target.locale ? {
112
+ locale: target.locale,
113
+ fallbackLocale: false
114
+ } : {},
115
+ req,
116
+ overrideAccess: false,
117
+ user: req.user
118
+ });
119
+ const d = live;
120
+ if (!d || d._status !== 'published') return null;
121
+ const liveUpdatedAt = asIsoString(d.updatedAt);
122
+ if (!liveUpdatedAt || liveUpdatedAt <= preUpdatedAt) return null;
123
+ return d;
124
+ } catch (verifyError) {
125
+ req.payload.logger?.debug?.({
126
+ event: 'mcp.publish.verify_read_failed',
127
+ err: verifyError
128
+ }, '[payload-mcp-toolkit] publish-recovery verify-read failed; surfacing original error');
129
+ return null;
130
+ }
131
+ }
34
132
  export function stampMcpContext(req) {
35
133
  req.context = {
36
134
  ...req.context,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/_helpers.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { CollectionConfig, PayloadRequest } from 'payload'\r\n\r\n/**\r\n * Build a `z.enum` over a list of valid resource slugs with a friendly\r\n * error message that names the valid set and clarifies why an unknown\r\n * slug (e.g. one removed via `options.exclude.globals`) is rejected.\r\n *\r\n * Default Zod enum errors are \"Invalid enum value. …\" — accurate but\r\n * unhelpful when the slug looks plausible to a caller who isn't aware\r\n * the host config excluded it.\r\n */\r\nexport function slugEnum(\r\n slugs: string[],\r\n kind: 'global' | 'collection',\r\n): z.ZodEnum<[string, ...string[]]> {\r\n return z.enum(slugs as [string, ...string[]], {\r\n errorMap: () => ({\r\n message: `${kind === 'global' ? 'Global' : 'Collection'} slug must be one of: ${slugs.join(', ')}. Unknown or excluded slugs are rejected.`,\r\n }),\r\n })\r\n}\r\n\r\nexport interface McpTextResponse {\r\n content: Array<{ type: 'text'; text: string }>\r\n}\r\n\r\nexport const DRAFT_NOTE = ' Document is in draft status — use publishDraft to make it live.'\r\n\r\nexport function textResponse(text: string): McpTextResponse {\r\n return { content: [{ type: 'text', text }] }\r\n}\r\n\r\nexport function jsonResponse(payload: unknown): McpTextResponse {\r\n return textResponse(JSON.stringify(payload))\r\n}\r\n\r\nexport function errorMessage(error: unknown): string {\r\n return error instanceof Error ? error.message : String(error)\r\n}\r\n\r\nexport function stampMcpContext(req: PayloadRequest): void {\r\n req.context = { ...req.context, source: 'mcp' }\r\n}\r\n\r\nexport function getDocDisplayName(doc: unknown, fallback: string): string {\r\n const d = doc as Record<string, unknown> | null | undefined\r\n return (\r\n (typeof d?.name === 'string' && d.name) ||\r\n (typeof d?.title === 'string' && d.title) ||\r\n (typeof d?.slug === 'string' && d.slug) ||\r\n fallback\r\n )\r\n}\r\n\r\nexport function requireDraftCollection(\r\n collection: string,\r\n draftCollections: Set<string>,\r\n noun = 'drafts',\r\n): McpTextResponse | null {\r\n if (draftCollections.has(collection)) return null\r\n return textResponse(\r\n `Error: Collection \"${collection}\" does not support ${noun}. ` +\r\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\r\n )\r\n}\r\n\r\n/**\r\n * Resolves the preview URL for a draft document by delegating to the\r\n * collection's own configured preview function (`admin.livePreview.url`\r\n * preferred, then `admin.preview`). Returns null when no function is\r\n * configured, when it fails, or when it returns a relative path with no\r\n * absolute `siteUrl` to anchor it.\r\n */\r\nexport async function resolvePreviewUrl(\r\n collection: CollectionConfig,\r\n doc: Record<string, unknown>,\r\n req: PayloadRequest,\r\n siteUrl: string | undefined,\r\n): Promise<string | null> {\r\n const admin = (collection.admin ?? {}) as Record<string, any>\r\n const locale = (req as unknown as { locale?: string }).locale ?? 'en'\r\n\r\n let raw: string | null | undefined\r\n\r\n const livePreviewUrl = admin.livePreview?.url\r\n if (typeof livePreviewUrl === 'function') {\r\n try {\r\n raw = await livePreviewUrl({\r\n data: doc,\r\n locale: { code: locale, label: locale },\r\n req,\r\n payload: req.payload,\r\n collectionConfig: collection,\r\n })\r\n } catch {\r\n raw = null\r\n }\r\n } else if (typeof livePreviewUrl === 'string') {\r\n raw = livePreviewUrl\r\n }\r\n\r\n if (!raw && typeof admin.preview === 'function') {\r\n try {\r\n raw = await admin.preview(doc, { locale, req, token: null })\r\n } catch {\r\n raw = null\r\n }\r\n }\r\n\r\n if (!raw || typeof raw !== 'string') return null\r\n\r\n if (raw.startsWith('http://') || raw.startsWith('https://')) return raw\r\n if (!siteUrl) return null\r\n\r\n const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl\r\n const path = raw.startsWith('/') ? raw : `/${raw}`\r\n return `${base}${path}`\r\n}\r\n\r\n/**\r\n * If `doc` is a draft, appends a preview-URL hint to the MCP response so the\r\n * AI can present it to the user. Falls back to a generic admin-panel hint\r\n * when the collection has no preview function configured.\r\n *\r\n * Pure with respect to the response: a fresh content array is returned.\r\n */\r\nexport async function decorateDraftResponse(\r\n response: McpTextResponse,\r\n doc: Record<string, unknown> | null | undefined,\r\n collection: CollectionConfig | undefined,\r\n req: PayloadRequest,\r\n siteUrl: string | undefined,\r\n): Promise<McpTextResponse> {\r\n if (!doc || doc._status !== 'draft' || !collection) return response\r\n\r\n const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl)\r\n const hint = previewUrl\r\n ? `\\n📋 This document is a draft. Preview it here: ${previewUrl}`\r\n : '\\n📋 This document is a draft. Use the admin panel to preview it.'\r\n\r\n return { content: [...response.content, { type: 'text', text: hint }] }\r\n}\r\n"],"names":["z","slugEnum","slugs","kind","enum","errorMap","message","join","DRAFT_NOTE","textResponse","text","content","type","jsonResponse","payload","JSON","stringify","errorMessage","error","Error","String","stampMcpContext","req","context","source","getDocDisplayName","doc","fallback","d","name","title","slug","requireDraftCollection","collection","draftCollections","noun","has","resolvePreviewUrl","siteUrl","admin","locale","raw","livePreviewUrl","livePreview","url","data","code","label","collectionConfig","preview","token","startsWith","base","endsWith","slice","path","decorateDraftResponse","response","_status","previewUrl","hint"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/tools/_helpers.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { CollectionConfig, PayloadRequest } from 'payload'\r\n\r\n/**\r\n * Build a `z.enum` over a list of valid resource slugs with a friendly\r\n * error message that names the valid set and clarifies why an unknown\r\n * slug (e.g. one removed via `options.exclude.globals`) is rejected.\r\n *\r\n * Default Zod enum errors are \"Invalid enum value. …\" — accurate but\r\n * unhelpful when the slug looks plausible to a caller who isn't aware\r\n * the host config excluded it.\r\n */\r\nexport function slugEnum(\r\n slugs: string[],\r\n kind: 'global' | 'collection',\r\n): z.ZodEnum<[string, ...string[]]> {\r\n return z.enum(slugs as [string, ...string[]], {\r\n errorMap: () => ({\r\n message: `${kind === 'global' ? 'Global' : 'Collection'} slug must be one of: ${slugs.join(', ')}. Unknown or excluded slugs are rejected.`,\r\n }),\r\n })\r\n}\r\n\r\nexport interface McpTextResponse {\r\n content: Array<{ type: 'text'; text: string }>\r\n}\r\n\r\nexport const DRAFT_NOTE = ' Document is in draft status — use publishDraft to make it live.'\r\n\r\nexport function textResponse(text: string): McpTextResponse {\r\n return { content: [{ type: 'text', text }] }\r\n}\r\n\r\nexport function jsonResponse(payload: unknown): McpTextResponse {\r\n return textResponse(JSON.stringify(payload))\r\n}\r\n\r\nexport function errorMessage(error: unknown): string {\r\n return error instanceof Error ? error.message : String(error)\r\n}\r\n\r\nfunction asIsoString(v: unknown): string | undefined {\r\n if (typeof v === 'string') return v\r\n if (v instanceof Date) return v.toISOString()\r\n return undefined\r\n}\r\n\r\nexport type PublishVerifyTarget =\r\n | { kind: 'collection'; slug: string; id: string }\r\n | { kind: 'global'; slug: string; locale?: string }\r\n\r\n/**\r\n * Capture the document's current `updatedAt` BEFORE a publish attempt so\r\n * the recovery branch can tell \"this attempt landed despite a post-write\r\n * validator throw\" from \"an older publish was successful and this attempt\r\n * did nothing\". A missing snapshot is non-fatal — the recovery branch\r\n * conservatively falls through to the original error in that case.\r\n */\r\nexport async function snapshotPublishMarker(\r\n req: PayloadRequest,\r\n target: PublishVerifyTarget,\r\n): Promise<string | undefined> {\r\n try {\r\n const pre =\r\n target.kind === 'collection'\r\n ? await req.payload.findByID({\r\n collection: target.slug as any,\r\n id: target.id,\r\n draft: true,\r\n depth: 0,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n : await req.payload.findGlobal({\r\n slug: target.slug as never,\r\n draft: true,\r\n depth: 0,\r\n // `fallbackLocale: false` disables Payload's locale-fallback so\r\n // the read returns the literal state of the requested locale.\r\n // Without this, a localized global with fallbackLocale='en'\r\n // could report the 'en' updatedAt while the caller is\r\n // publishing 'de', producing a false-positive in\r\n // verifyPublishSucceededDespiteError.\r\n ...(target.locale\r\n ? { locale: target.locale as never, fallbackLocale: false as never }\r\n : {}),\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n return asIsoString((pre as { updatedAt?: unknown } | null | undefined)?.updatedAt)\r\n } catch {\r\n return undefined\r\n }\r\n}\r\n\r\n/**\r\n * After a Payload update throws on a publish call, determine whether the\r\n * publish actually landed despite the error. Returns the live document\r\n * only when (a) the live `_status` is 'published' AND (b) `updatedAt`\r\n * strictly advanced past the pre-update snapshot — i.e. the current\r\n * attempt produced the published row. Without the strictly-newer check,\r\n * a pre-existing published version from an earlier successful publish\r\n * would mask a real failure of the current attempt.\r\n *\r\n * Returns null on:\r\n * - missing pre-snapshot (cannot disambiguate; conservative)\r\n * - verify read failure (do not mask the original error with a\r\n * secondary read error)\r\n * - live `_status` not 'published'\r\n * - live `updatedAt` not strictly newer than the pre-snapshot\r\n */\r\nexport async function verifyPublishSucceededDespiteError(\r\n req: PayloadRequest,\r\n target: PublishVerifyTarget,\r\n preUpdatedAt: string | undefined,\r\n): Promise<Record<string, unknown> | null> {\r\n if (!preUpdatedAt) return null\r\n try {\r\n const live =\r\n target.kind === 'collection'\r\n ? await req.payload.findByID({\r\n collection: target.slug as any,\r\n id: target.id,\r\n draft: false,\r\n depth: 0,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n : await req.payload.findGlobal({\r\n slug: target.slug as never,\r\n draft: false,\r\n depth: 0,\r\n // Disable Payload locale-fallback (see snapshotPublishMarker\r\n // note) so verify reads the literal state of the locale that\r\n // updateGlobal was called against.\r\n ...(target.locale\r\n ? { locale: target.locale as never, fallbackLocale: false as never }\r\n : {}),\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n const d = live as Record<string, unknown> | null | undefined\r\n if (!d || d._status !== 'published') return null\r\n const liveUpdatedAt = asIsoString(d.updatedAt)\r\n if (!liveUpdatedAt || liveUpdatedAt <= preUpdatedAt) return null\r\n return d\r\n } catch (verifyError) {\r\n req.payload.logger?.debug?.(\r\n { event: 'mcp.publish.verify_read_failed', err: verifyError },\r\n '[payload-mcp-toolkit] publish-recovery verify-read failed; surfacing original error',\r\n )\r\n return null\r\n }\r\n}\r\n\r\nexport function stampMcpContext(req: PayloadRequest): void {\r\n req.context = { ...req.context, source: 'mcp' }\r\n}\r\n\r\nexport function getDocDisplayName(doc: unknown, fallback: string): string {\r\n const d = doc as Record<string, unknown> | null | undefined\r\n return (\r\n (typeof d?.name === 'string' && d.name) ||\r\n (typeof d?.title === 'string' && d.title) ||\r\n (typeof d?.slug === 'string' && d.slug) ||\r\n fallback\r\n )\r\n}\r\n\r\nexport function requireDraftCollection(\r\n collection: string,\r\n draftCollections: Set<string>,\r\n noun = 'drafts',\r\n): McpTextResponse | null {\r\n if (draftCollections.has(collection)) return null\r\n return textResponse(\r\n `Error: Collection \"${collection}\" does not support ${noun}. ` +\r\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\r\n )\r\n}\r\n\r\n/**\r\n * Resolves the preview URL for a draft document by delegating to the\r\n * collection's own configured preview function (`admin.livePreview.url`\r\n * preferred, then `admin.preview`). Returns null when no function is\r\n * configured, when it fails, or when it returns a relative path with no\r\n * absolute `siteUrl` to anchor it.\r\n */\r\nexport async function resolvePreviewUrl(\r\n collection: CollectionConfig,\r\n doc: Record<string, unknown>,\r\n req: PayloadRequest,\r\n siteUrl: string | undefined,\r\n): Promise<string | null> {\r\n const admin = (collection.admin ?? {}) as Record<string, any>\r\n const locale = (req as unknown as { locale?: string }).locale ?? 'en'\r\n\r\n let raw: string | null | undefined\r\n\r\n const livePreviewUrl = admin.livePreview?.url\r\n if (typeof livePreviewUrl === 'function') {\r\n try {\r\n raw = await livePreviewUrl({\r\n data: doc,\r\n locale: { code: locale, label: locale },\r\n req,\r\n payload: req.payload,\r\n collectionConfig: collection,\r\n })\r\n } catch {\r\n raw = null\r\n }\r\n } else if (typeof livePreviewUrl === 'string') {\r\n raw = livePreviewUrl\r\n }\r\n\r\n if (!raw && typeof admin.preview === 'function') {\r\n try {\r\n raw = await admin.preview(doc, { locale, req, token: null })\r\n } catch {\r\n raw = null\r\n }\r\n }\r\n\r\n if (!raw || typeof raw !== 'string') return null\r\n\r\n if (raw.startsWith('http://') || raw.startsWith('https://')) return raw\r\n if (!siteUrl) return null\r\n\r\n const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl\r\n const path = raw.startsWith('/') ? raw : `/${raw}`\r\n return `${base}${path}`\r\n}\r\n\r\n/**\r\n * If `doc` is a draft, appends a preview-URL hint to the MCP response so the\r\n * AI can present it to the user. Falls back to a generic admin-panel hint\r\n * when the collection has no preview function configured.\r\n *\r\n * Pure with respect to the response: a fresh content array is returned.\r\n */\r\nexport async function decorateDraftResponse(\r\n response: McpTextResponse,\r\n doc: Record<string, unknown> | null | undefined,\r\n collection: CollectionConfig | undefined,\r\n req: PayloadRequest,\r\n siteUrl: string | undefined,\r\n): Promise<McpTextResponse> {\r\n if (!doc || doc._status !== 'draft' || !collection) return response\r\n\r\n const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl)\r\n const hint = previewUrl\r\n ? `\\n📋 This document is a draft. Preview it here: ${previewUrl}`\r\n : '\\n📋 This document is a draft. Use the admin panel to preview it.'\r\n\r\n return { content: [...response.content, { type: 'text', text: hint }] }\r\n}\r\n"],"names":["z","slugEnum","slugs","kind","enum","errorMap","message","join","DRAFT_NOTE","textResponse","text","content","type","jsonResponse","payload","JSON","stringify","errorMessage","error","Error","String","asIsoString","v","Date","toISOString","undefined","snapshotPublishMarker","req","target","pre","findByID","collection","slug","id","draft","depth","overrideAccess","user","findGlobal","locale","fallbackLocale","updatedAt","verifyPublishSucceededDespiteError","preUpdatedAt","live","d","_status","liveUpdatedAt","verifyError","logger","debug","event","err","stampMcpContext","context","source","getDocDisplayName","doc","fallback","name","title","requireDraftCollection","draftCollections","noun","has","resolvePreviewUrl","siteUrl","admin","raw","livePreviewUrl","livePreview","url","data","code","label","collectionConfig","preview","token","startsWith","base","endsWith","slice","path","decorateDraftResponse","response","previewUrl","hint"],"mappings":"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"}
@@ -60,6 +60,14 @@ const MEDIA_SLUG = 'media';
60
60
  stampMcpContext(req);
61
61
  const isDraftCollection = draftCollections.has(collection);
62
62
  const asDraft = draft ?? isDraftCollection;
63
+ // Payload's `draft: false` writes a published version but leaves the main
64
+ // row's `_status` at its field default ('draft'), so the admin list shows
65
+ // "draft" even though routes serve the published doc. Set `_status`
66
+ // explicitly to match intent (mirrors publishDraft); respect a caller
67
+ // value if one was passed in `data`.
68
+ if (isDraftCollection && data._status === undefined) {
69
+ data._status = asDraft ? 'draft' : 'published';
70
+ }
63
71
  try {
64
72
  const doc = await req.payload.create({
65
73
  collection: collection,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/create-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 `create<Resource>` tools.\r\n *\r\n * The official plugin builds its create-tool input schema by spreading\r\n * `convertCollectionSchemaToZod(schema).shape`, then validates the request\r\n * with `additionalProperties: false`. For any collection whose JSON schema\r\n * trips `json-schema-to-zod` (richText, upload, blocks, relationship arrays\r\n * — i.e. virtually every real-world content collection), the converter\r\n * falls back to `z.record(z.any())`, `.shape` is undefined, and the spread\r\n * silently produces a metadata-only schema. The MCP SDK then strips every\r\n * content field before it reaches `payload.create()`, so creates end up\r\n * with empty `data` and fail required-field validation.\r\n *\r\n * `createDocument` sidesteps the whole pipeline: take a JSON `data` string,\r\n * call `payload.create()` directly via the local API.\r\n *\r\n * Defaults to `draft: true` for draft-enabled collections so newly created\r\n * documents land in the same draft-first workflow used by `updateDocument`.\r\n */\r\nexport function createCreateDocumentTool(\r\n collectionSchemas: Map<string, CollectionSchema>,\r\n draftCollections: Set<string>,\r\n) {\r\n const creatableSlugs: string[] = []\r\n const descriptionLines: string[] = []\r\n for (const [slug, schema] of collectionSchemas) {\r\n if (slug === MEDIA_SLUG) continue\r\n creatableSlugs.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: 'createDocument',\r\n routing: { kind: 'collection', action: 'create' } as const,\r\n description:\r\n 'Create a new document in any collection. Pass the field values as a JSON string in `data`. ' +\r\n 'For draft-enabled collections, the document is created as a draft by default — use publishDraft to make it live, ' +\r\n 'or pass `draft: false` to publish immediately. ' +\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: ${creatableSlugs.join(', ')}`),\r\n data: z\r\n .string()\r\n .describe(\r\n 'JSON string of field names to values for the new document. ' +\r\n 'Examples: \\'{\"name\": \"Aria\", \"slug\": \"aria\"}\\', ' +\r\n '\\'{\"title\": \"First-Time Clients\", \"heroTitle\": \"Welcome\", \"slug\": \"first-time-clients\"}\\'',\r\n ),\r\n draft: z\r\n .boolean()\r\n .optional()\r\n .describe(\r\n 'Override draft status. Defaults to `true` for draft-enabled collections, `false` otherwise. ' +\r\n 'Set explicitly to `false` on a draft-enabled collection to publish immediately.',\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, data: rawData, draft } = args as {\r\n collection: string\r\n data: string\r\n draft?: boolean\r\n }\r\n\r\n let data: Record<string, unknown>\r\n try {\r\n data = JSON.parse(rawData)\r\n } catch {\r\n return textResponse(\r\n 'Error: \"data\" must be a valid JSON string. Example: \\'{\"name\": \"Aria\", \"slug\": \"aria\"}\\'',\r\n )\r\n }\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Available: ${creatableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n if (collection === MEDIA_SLUG) {\r\n return textResponse('Error: Use the uploadMedia tool to create 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 for the new document.',\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n const isDraftCollection = draftCollections.has(collection)\r\n const asDraft = draft ?? isDraftCollection\r\n\r\n try {\r\n const doc = await req.payload.create({\r\n collection: collection as any,\r\n data: data as any,\r\n draft: asDraft,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n\r\n const displayName = getDocDisplayName(doc, String((doc as { id?: unknown }).id ?? ''))\r\n const newId = String((doc as { id?: unknown }).id ?? '')\r\n const draftNote = isDraftCollection && asDraft ? DRAFT_NOTE : ''\r\n\r\n return textResponse(\r\n `Created \"${displayName}\" in ${collection} (ID: ${newId}).${draftNote}`,\r\n )\r\n } catch (error) {\r\n return textResponse(\r\n `Error creating document 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","createCreateDocumentTool","collectionSchemas","draftCollections","creatableSlugs","descriptionLines","slug","schema","push","fields","map","f","name","join","collectionDescriptions","routing","kind","action","description","parameters","collection","string","describe","data","draft","boolean","optional","handler","args","req","_extra","rawData","JSON","parse","has","Object","keys","length","isDraftCollection","asDraft","doc","payload","create","overrideAccess","user","displayName","String","id","newId","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;;;;;;;;;;;;;;;;;;CAkBC,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,gGACA,sHACA,oDACA,+FACA,6FACA,8CACAJ;QACFK,YAAY;YACVC,YAAY1B,EACT2B,MAAM,GACNC,QAAQ,CAAC,CAAC,6BAA6B,EAAElB,eAAeS,IAAI,CAAC,OAAO;YACvEU,MAAM7B,EACH2B,MAAM,GACNC,QAAQ,CACP,gEACE,qDACA;YAENE,OAAO9B,EACJ+B,OAAO,GACPC,QAAQ,GACRJ,QAAQ,CACP,iGACE;QAER;QACAK,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAEV,UAAU,EAAEG,MAAMQ,OAAO,EAAEP,KAAK,EAAE,GAAGI;YAM7C,IAAIL;YACJ,IAAI;gBACFA,OAAOS,KAAKC,KAAK,CAACF;YACpB,EAAE,OAAM;gBACN,OAAOhC,aACL;YAEJ;YAEA,IAAI,CAACG,kBAAkBgC,GAAG,CAACd,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,CAACwB,QAAQY,OAAOC,IAAI,CAACb,MAAMc,MAAM,KAAK,GAAG;gBAC3C,OAAOtC,aACL;YAEJ;YAEAD,gBAAgB+B;YAEhB,MAAMS,oBAAoBnC,iBAAiB+B,GAAG,CAACd;YAC/C,MAAMmB,UAAUf,SAASc;YAEzB,IAAI;gBACF,MAAME,MAAM,MAAMX,IAAIY,OAAO,CAACC,MAAM,CAAC;oBACnCtB,YAAYA;oBACZG,MAAMA;oBACNC,OAAOe;oBACPV;oBACAc,gBAAgB;oBAChBC,MAAMf,IAAIe,IAAI;gBAChB;gBAEA,MAAMC,cAAchD,kBAAkB2C,KAAKM,OAAO,AAACN,IAAyBO,EAAE,IAAI;gBAClF,MAAMC,QAAQF,OAAO,AAACN,IAAyBO,EAAE,IAAI;gBACrD,MAAME,YAAYX,qBAAqBC,UAAU5C,aAAa;gBAE9D,OAAOI,aACL,CAAC,SAAS,EAAE8C,YAAY,KAAK,EAAEzB,WAAW,MAAM,EAAE4B,MAAM,EAAE,EAAEC,WAAW;YAE3E,EAAE,OAAOC,OAAO;gBACd,OAAOnD,aACL,CAAC,2BAA2B,EAAEqB,WAAW,EAAE,EAAExB,aAAasD,QAAQ;YAEtE;QACF;IACF;AACF"}
1
+ {"version":3,"sources":["../../src/tools/create-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 `create<Resource>` tools.\r\n *\r\n * The official plugin builds its create-tool input schema by spreading\r\n * `convertCollectionSchemaToZod(schema).shape`, then validates the request\r\n * with `additionalProperties: false`. For any collection whose JSON schema\r\n * trips `json-schema-to-zod` (richText, upload, blocks, relationship arrays\r\n * — i.e. virtually every real-world content collection), the converter\r\n * falls back to `z.record(z.any())`, `.shape` is undefined, and the spread\r\n * silently produces a metadata-only schema. The MCP SDK then strips every\r\n * content field before it reaches `payload.create()`, so creates end up\r\n * with empty `data` and fail required-field validation.\r\n *\r\n * `createDocument` sidesteps the whole pipeline: take a JSON `data` string,\r\n * call `payload.create()` directly via the local API.\r\n *\r\n * Defaults to `draft: true` for draft-enabled collections so newly created\r\n * documents land in the same draft-first workflow used by `updateDocument`.\r\n */\r\nexport function createCreateDocumentTool(\r\n collectionSchemas: Map<string, CollectionSchema>,\r\n draftCollections: Set<string>,\r\n) {\r\n const creatableSlugs: string[] = []\r\n const descriptionLines: string[] = []\r\n for (const [slug, schema] of collectionSchemas) {\r\n if (slug === MEDIA_SLUG) continue\r\n creatableSlugs.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: 'createDocument',\r\n routing: { kind: 'collection', action: 'create' } as const,\r\n description:\r\n 'Create a new document in any collection. Pass the field values as a JSON string in `data`. ' +\r\n 'For draft-enabled collections, the document is created as a draft by default — use publishDraft to make it live, ' +\r\n 'or pass `draft: false` to publish immediately. ' +\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: ${creatableSlugs.join(', ')}`),\r\n data: z\r\n .string()\r\n .describe(\r\n 'JSON string of field names to values for the new document. ' +\r\n 'Examples: \\'{\"name\": \"Aria\", \"slug\": \"aria\"}\\', ' +\r\n '\\'{\"title\": \"First-Time Clients\", \"heroTitle\": \"Welcome\", \"slug\": \"first-time-clients\"}\\'',\r\n ),\r\n draft: z\r\n .boolean()\r\n .optional()\r\n .describe(\r\n 'Override draft status. Defaults to `true` for draft-enabled collections, `false` otherwise. ' +\r\n 'Set explicitly to `false` on a draft-enabled collection to publish immediately.',\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, data: rawData, draft } = args as {\r\n collection: string\r\n data: string\r\n draft?: boolean\r\n }\r\n\r\n let data: Record<string, unknown>\r\n try {\r\n data = JSON.parse(rawData)\r\n } catch {\r\n return textResponse(\r\n 'Error: \"data\" must be a valid JSON string. Example: \\'{\"name\": \"Aria\", \"slug\": \"aria\"}\\'',\r\n )\r\n }\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Available: ${creatableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n if (collection === MEDIA_SLUG) {\r\n return textResponse('Error: Use the uploadMedia tool to create 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 for the new document.',\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n const isDraftCollection = draftCollections.has(collection)\r\n const asDraft = draft ?? isDraftCollection\r\n\r\n // Payload's `draft: false` writes a published version but leaves the main\r\n // row's `_status` at its field default ('draft'), so the admin list shows\r\n // \"draft\" even though routes serve the published doc. Set `_status`\r\n // explicitly to match intent (mirrors publishDraft); respect a caller\r\n // value if one was passed in `data`.\r\n if (isDraftCollection && data._status === undefined) {\r\n data._status = asDraft ? 'draft' : 'published'\r\n }\r\n\r\n try {\r\n const doc = await req.payload.create({\r\n collection: collection as any,\r\n data: data as any,\r\n draft: asDraft,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n\r\n const displayName = getDocDisplayName(doc, String((doc as { id?: unknown }).id ?? ''))\r\n const newId = String((doc as { id?: unknown }).id ?? '')\r\n const draftNote = isDraftCollection && asDraft ? DRAFT_NOTE : ''\r\n\r\n return textResponse(\r\n `Created \"${displayName}\" in ${collection} (ID: ${newId}).${draftNote}`,\r\n )\r\n } catch (error) {\r\n return textResponse(\r\n `Error creating document 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","createCreateDocumentTool","collectionSchemas","draftCollections","creatableSlugs","descriptionLines","slug","schema","push","fields","map","f","name","join","collectionDescriptions","routing","kind","action","description","parameters","collection","string","describe","data","draft","boolean","optional","handler","args","req","_extra","rawData","JSON","parse","has","Object","keys","length","isDraftCollection","asDraft","_status","undefined","doc","payload","create","overrideAccess","user","displayName","String","id","newId","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;;;;;;;;;;;;;;;;;;CAkBC,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,gGACA,sHACA,oDACA,+FACA,6FACA,8CACAJ;QACFK,YAAY;YACVC,YAAY1B,EACT2B,MAAM,GACNC,QAAQ,CAAC,CAAC,6BAA6B,EAAElB,eAAeS,IAAI,CAAC,OAAO;YACvEU,MAAM7B,EACH2B,MAAM,GACNC,QAAQ,CACP,gEACE,qDACA;YAENE,OAAO9B,EACJ+B,OAAO,GACPC,QAAQ,GACRJ,QAAQ,CACP,iGACE;QAER;QACAK,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAEV,UAAU,EAAEG,MAAMQ,OAAO,EAAEP,KAAK,EAAE,GAAGI;YAM7C,IAAIL;YACJ,IAAI;gBACFA,OAAOS,KAAKC,KAAK,CAACF;YACpB,EAAE,OAAM;gBACN,OAAOhC,aACL;YAEJ;YAEA,IAAI,CAACG,kBAAkBgC,GAAG,CAACd,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,CAACwB,QAAQY,OAAOC,IAAI,CAACb,MAAMc,MAAM,KAAK,GAAG;gBAC3C,OAAOtC,aACL;YAEJ;YAEAD,gBAAgB+B;YAEhB,MAAMS,oBAAoBnC,iBAAiB+B,GAAG,CAACd;YAC/C,MAAMmB,UAAUf,SAASc;YAEzB,0EAA0E;YAC1E,0EAA0E;YAC1E,oEAAoE;YACpE,sEAAsE;YACtE,qCAAqC;YACrC,IAAIA,qBAAqBf,KAAKiB,OAAO,KAAKC,WAAW;gBACnDlB,KAAKiB,OAAO,GAAGD,UAAU,UAAU;YACrC;YAEA,IAAI;gBACF,MAAMG,MAAM,MAAMb,IAAIc,OAAO,CAACC,MAAM,CAAC;oBACnCxB,YAAYA;oBACZG,MAAMA;oBACNC,OAAOe;oBACPV;oBACAgB,gBAAgB;oBAChBC,MAAMjB,IAAIiB,IAAI;gBAChB;gBAEA,MAAMC,cAAclD,kBAAkB6C,KAAKM,OAAO,AAACN,IAAyBO,EAAE,IAAI;gBAClF,MAAMC,QAAQF,OAAO,AAACN,IAAyBO,EAAE,IAAI;gBACrD,MAAME,YAAYb,qBAAqBC,UAAU5C,aAAa;gBAE9D,OAAOI,aACL,CAAC,SAAS,EAAEgD,YAAY,KAAK,EAAE3B,WAAW,MAAM,EAAE8B,MAAM,EAAE,EAAEC,WAAW;YAE3E,EAAE,OAAOC,OAAO;gBACd,OAAOrD,aACL,CAAC,2BAA2B,EAAEqB,WAAW,EAAE,EAAExB,aAAawD,QAAQ;YAEtE;QACF;IACF;AACF"}
@@ -19,7 +19,7 @@ export declare function createDeleteDocumentTool(collectionSchemas: Map<string,
19
19
  description: string;
20
20
  parameters: {
21
21
  collection: z.ZodString;
22
- id: z.ZodString;
22
+ documentId: z.ZodString;
23
23
  };
24
24
  handler: (args: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
25
25
  };
@@ -21,10 +21,10 @@ import { errorMessage, getDocDisplayName, stampMcpContext, textResponse } from '
21
21
  description: 'Delete a document by ID. Skips the inbound-relationship safety check that `safeDelete` performs — use only when you know the document has no inbound references, or when broken relationships are acceptable. Prefer `safeDelete` for general use.\n\n' + `Collections: ${deletableSlugs.join(', ')}`,
22
22
  parameters: {
23
23
  collection: z.string().describe(`Collection slug. One of: ${deletableSlugs.join(', ')}`),
24
- id: z.string().describe('Document ID to delete.')
24
+ documentId: z.string().describe('Document ID to delete.')
25
25
  },
26
26
  handler: async (args, req, _extra)=>{
27
- const { collection, id } = args;
27
+ const { collection, documentId } = args;
28
28
  if (!collectionSchemas.has(collection)) {
29
29
  return textResponse(`Error: Unknown collection "${collection}". Valid: ${deletableSlugs.join(', ')}`);
30
30
  }
@@ -32,15 +32,15 @@ import { errorMessage, getDocDisplayName, stampMcpContext, textResponse } from '
32
32
  try {
33
33
  const doc = await req.payload.delete({
34
34
  collection: collection,
35
- id,
35
+ id: documentId,
36
36
  req,
37
37
  overrideAccess: false,
38
38
  user: req.user
39
39
  });
40
- const displayName = getDocDisplayName(doc, id);
41
- return textResponse(`Deleted "${displayName}" from ${collection} (ID: ${id}).`);
40
+ const displayName = getDocDisplayName(doc, documentId);
41
+ return textResponse(`Deleted "${displayName}" from ${collection} (ID: ${documentId}).`);
42
42
  } catch (err) {
43
- return textResponse(`Error deleting ${id} from ${collection}: ${errorMessage(err)}`);
43
+ return textResponse(`Error deleting ${documentId} from ${collection}: ${errorMessage(err)}`);
44
44
  }
45
45
  }
46
46
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/delete-document.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\ninterface DeleteDocumentArgs {\r\n collection: string\r\n id: string\r\n}\r\n\r\n/**\r\n * Polymorphic, fast unsafe-delete tool. Mirrors `safeDelete`'s args but skips\r\n * the relationship-walk — use this when the caller knows the doc has no\r\n * inbound references, or when relationship breakage is acceptable.\r\n *\r\n * `safeDelete` remains the recommended default; this exists for surgical\r\n * deletes inside scripts and AI workflows where a relationship walk would\r\n * be wasteful.\r\n */\r\nexport function createDeleteDocumentTool(collectionSchemas: Map<string, CollectionSchema>) {\r\n const deletableSlugs = [...collectionSchemas.keys()]\r\n\r\n return {\r\n name: 'deleteDocument',\r\n routing: { kind: 'collection', action: 'delete' } as const,\r\n description:\r\n 'Delete a document by ID. Skips the inbound-relationship safety check that `safeDelete` performs — use only when you know the document has no inbound references, or when broken relationships are acceptable. Prefer `safeDelete` for general use.\\n\\n' +\r\n `Collections: ${deletableSlugs.join(', ')}`,\r\n parameters: {\r\n collection: z.string().describe(`Collection slug. One of: ${deletableSlugs.join(', ')}`),\r\n id: z.string().describe('Document ID to delete.'),\r\n },\r\n handler: async (args: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => {\r\n const { collection, id } = args as unknown as DeleteDocumentArgs\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Valid: ${deletableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n try {\r\n const doc = await req.payload.delete({\r\n collection: collection as never,\r\n id,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n\r\n const displayName = getDocDisplayName(doc, id)\r\n return textResponse(\r\n `Deleted \"${displayName}\" from ${collection} (ID: ${id}).`,\r\n )\r\n } catch (err) {\r\n return textResponse(\r\n `Error deleting ${id} from ${collection}: ${errorMessage(err)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","errorMessage","getDocDisplayName","stampMcpContext","textResponse","createDeleteDocumentTool","collectionSchemas","deletableSlugs","keys","name","routing","kind","action","description","join","parameters","collection","string","describe","id","handler","args","req","_extra","has","doc","payload","delete","overrideAccess","user","displayName","err"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,EACfC,YAAY,QACP,aAAY;AAOnB;;;;;;;;CAQC,GACD,OAAO,SAASC,yBAAyBC,iBAAgD;IACvF,MAAMC,iBAAiB;WAAID,kBAAkBE,IAAI;KAAG;IAEpD,OAAO;QACLC,MAAM;QACNC,SAAS;YAAEC,MAAM;YAAcC,QAAQ;QAAS;QAChDC,aACE,2PACA,CAAC,aAAa,EAAEN,eAAeO,IAAI,CAAC,OAAO;QAC7CC,YAAY;YACVC,YAAYhB,EAAEiB,MAAM,GAAGC,QAAQ,CAAC,CAAC,yBAAyB,EAAEX,eAAeO,IAAI,CAAC,OAAO;YACvFK,IAAInB,EAAEiB,MAAM,GAAGC,QAAQ,CAAC;QAC1B;QACAE,SAAS,OAAOC,MAA+BC,KAAqBC;YAClE,MAAM,EAAEP,UAAU,EAAEG,EAAE,EAAE,GAAGE;YAE3B,IAAI,CAACf,kBAAkBkB,GAAG,CAACR,aAAa;gBACtC,OAAOZ,aACL,CAAC,2BAA2B,EAAEY,WAAW,UAAU,EAAET,eAAeO,IAAI,CAAC,OAAO;YAEpF;YAEAX,gBAAgBmB;YAEhB,IAAI;gBACF,MAAMG,MAAM,MAAMH,IAAII,OAAO,CAACC,MAAM,CAAC;oBACnCX,YAAYA;oBACZG;oBACAG;oBACAM,gBAAgB;oBAChBC,MAAMP,IAAIO,IAAI;gBAChB;gBAEA,MAAMC,cAAc5B,kBAAkBuB,KAAKN;gBAC3C,OAAOf,aACL,CAAC,SAAS,EAAE0B,YAAY,OAAO,EAAEd,WAAW,MAAM,EAAEG,GAAG,EAAE,CAAC;YAE9D,EAAE,OAAOY,KAAK;gBACZ,OAAO3B,aACL,CAAC,eAAe,EAAEe,GAAG,MAAM,EAAEH,WAAW,EAAE,EAAEf,aAAa8B,MAAM;YAEnE;QACF;IACF;AACF"}
1
+ {"version":3,"sources":["../../src/tools/delete-document.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\ninterface DeleteDocumentArgs {\r\n collection: string\r\n documentId: string\r\n}\r\n\r\n/**\r\n * Polymorphic, fast unsafe-delete tool. Mirrors `safeDelete`'s args but skips\r\n * the relationship-walk — use this when the caller knows the doc has no\r\n * inbound references, or when relationship breakage is acceptable.\r\n *\r\n * `safeDelete` remains the recommended default; this exists for surgical\r\n * deletes inside scripts and AI workflows where a relationship walk would\r\n * be wasteful.\r\n */\r\nexport function createDeleteDocumentTool(collectionSchemas: Map<string, CollectionSchema>) {\r\n const deletableSlugs = [...collectionSchemas.keys()]\r\n\r\n return {\r\n name: 'deleteDocument',\r\n routing: { kind: 'collection', action: 'delete' } as const,\r\n description:\r\n 'Delete a document by ID. Skips the inbound-relationship safety check that `safeDelete` performs — use only when you know the document has no inbound references, or when broken relationships are acceptable. Prefer `safeDelete` for general use.\\n\\n' +\r\n `Collections: ${deletableSlugs.join(', ')}`,\r\n parameters: {\r\n collection: z.string().describe(`Collection slug. One of: ${deletableSlugs.join(', ')}`),\r\n documentId: z.string().describe('Document ID to delete.'),\r\n },\r\n handler: async (args: Record<string, unknown>, req: PayloadRequest, _extra: unknown) => {\r\n const { collection, documentId } = args as unknown as DeleteDocumentArgs\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Valid: ${deletableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n\r\n try {\r\n const doc = await req.payload.delete({\r\n collection: collection as never,\r\n id: documentId,\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 `Deleted \"${displayName}\" from ${collection} (ID: ${documentId}).`,\r\n )\r\n } catch (err) {\r\n return textResponse(\r\n `Error deleting ${documentId} from ${collection}: ${errorMessage(err)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","errorMessage","getDocDisplayName","stampMcpContext","textResponse","createDeleteDocumentTool","collectionSchemas","deletableSlugs","keys","name","routing","kind","action","description","join","parameters","collection","string","describe","documentId","handler","args","req","_extra","has","doc","payload","delete","id","overrideAccess","user","displayName","err"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,EACfC,YAAY,QACP,aAAY;AAOnB;;;;;;;;CAQC,GACD,OAAO,SAASC,yBAAyBC,iBAAgD;IACvF,MAAMC,iBAAiB;WAAID,kBAAkBE,IAAI;KAAG;IAEpD,OAAO;QACLC,MAAM;QACNC,SAAS;YAAEC,MAAM;YAAcC,QAAQ;QAAS;QAChDC,aACE,2PACA,CAAC,aAAa,EAAEN,eAAeO,IAAI,CAAC,OAAO;QAC7CC,YAAY;YACVC,YAAYhB,EAAEiB,MAAM,GAAGC,QAAQ,CAAC,CAAC,yBAAyB,EAAEX,eAAeO,IAAI,CAAC,OAAO;YACvFK,YAAYnB,EAAEiB,MAAM,GAAGC,QAAQ,CAAC;QAClC;QACAE,SAAS,OAAOC,MAA+BC,KAAqBC;YAClE,MAAM,EAAEP,UAAU,EAAEG,UAAU,EAAE,GAAGE;YAEnC,IAAI,CAACf,kBAAkBkB,GAAG,CAACR,aAAa;gBACtC,OAAOZ,aACL,CAAC,2BAA2B,EAAEY,WAAW,UAAU,EAAET,eAAeO,IAAI,CAAC,OAAO;YAEpF;YAEAX,gBAAgBmB;YAEhB,IAAI;gBACF,MAAMG,MAAM,MAAMH,IAAII,OAAO,CAACC,MAAM,CAAC;oBACnCX,YAAYA;oBACZY,IAAIT;oBACJG;oBACAO,gBAAgB;oBAChBC,MAAMR,IAAIQ,IAAI;gBAChB;gBAEA,MAAMC,cAAc7B,kBAAkBuB,KAAKN;gBAC3C,OAAOf,aACL,CAAC,SAAS,EAAE2B,YAAY,OAAO,EAAEf,WAAW,MAAM,EAAEG,WAAW,EAAE,CAAC;YAEtE,EAAE,OAAOa,KAAK;gBACZ,OAAO5B,aACL,CAAC,eAAe,EAAEe,WAAW,MAAM,EAAEH,WAAW,EAAE,EAAEf,aAAa+B,MAAM;YAE3E;QACF;IACF;AACF"}
@@ -8,8 +8,8 @@ import type { CollectionSchema } from '../types';
8
8
  * `createDocument` / `updateDocument`.
9
9
  *
10
10
  * Two modes:
11
- * - `id` set: `payload.findByID` (single doc)
12
- * - `id` unset: `payload.find` with optional JSON-string `where`
11
+ * - `documentId` set: `payload.findByID` (single doc)
12
+ * - `documentId` unset: `payload.find` with optional JSON-string `where`
13
13
  *
14
14
  * Draft-enabled collections get preview URLs appended to draft documents
15
15
  * via `decorateDraftResponse`.
@@ -23,7 +23,7 @@ export declare function createFindDocumentTool(collectionSchemas: Map<string, Co
23
23
  description: string;
24
24
  parameters: {
25
25
  collection: z.ZodString;
26
- id: z.ZodOptional<z.ZodString>;
26
+ documentId: z.ZodOptional<z.ZodString>;
27
27
  where: z.ZodOptional<z.ZodString>;
28
28
  limit: z.ZodOptional<z.ZodNumber>;
29
29
  depth: z.ZodOptional<z.ZodNumber>;
@@ -7,8 +7,8 @@ import { decorateDraftResponse, errorMessage, jsonResponse, stampMcpContext, tex
7
7
  * `createDocument` / `updateDocument`.
8
8
  *
9
9
  * Two modes:
10
- * - `id` set: `payload.findByID` (single doc)
11
- * - `id` unset: `payload.find` with optional JSON-string `where`
10
+ * - `documentId` set: `payload.findByID` (single doc)
11
+ * - `documentId` unset: `payload.find` with optional JSON-string `where`
12
12
  *
13
13
  * Draft-enabled collections get preview URLs appended to draft documents
14
14
  * via `decorateDraftResponse`.
@@ -23,28 +23,28 @@ import { decorateDraftResponse, errorMessage, jsonResponse, stampMcpContext, tex
23
23
  kind: 'collection',
24
24
  action: 'read'
25
25
  },
26
- description: 'Read documents from any collection. Pass `id` for a single document, or omit `id` and pass a Payload `where` filter as a JSON string for a list. ' + 'Draft-enabled collections include a preview URL on draft documents when available.\n\n' + 'Available collections:\n' + descriptionLines.join('\n'),
26
+ description: 'Read documents from any collection. Pass `documentId` for a single document, or omit `documentId` and pass a Payload `where` filter as a JSON string for a list. ' + 'Draft-enabled collections include a preview URL on draft documents when available.\n\n' + 'Available collections:\n' + descriptionLines.join('\n'),
27
27
  parameters: {
28
28
  collection: z.string().describe(`The collection slug. One of: ${findableSlugs.join(', ')}`),
29
- id: z.string().optional().describe('Document ID. When set, returns a single document.'),
30
- where: z.string().optional().describe('JSON-encoded Payload `where` clause. Examples: \'{"status":{"equals":"published"}}\', ' + '\'{"slug":{"equals":"hello-world"}}\'. Ignored if `id` is set.'),
29
+ documentId: z.string().optional().describe('Document ID. When set, returns a single document.'),
30
+ where: z.string().optional().describe('JSON-encoded Payload `where` clause. Examples: \'{"status":{"equals":"published"}}\', ' + '\'{"slug":{"equals":"hello-world"}}\'. Ignored if `documentId` is set.'),
31
31
  limit: z.number().int().min(1).max(100).optional().describe('Max results when listing. Default 25.'),
32
32
  depth: z.number().int().min(0).max(3).optional().describe('Relationship population depth. Default 1.'),
33
33
  draft: z.boolean().optional().describe('When true, returns draft versions of draft-enabled collections. Default false (published only).')
34
34
  },
35
35
  handler: async (rawArgs, req, _extra)=>{
36
36
  const args = rawArgs;
37
- const { collection, id, where, limit, depth, draft } = args;
37
+ const { collection, documentId, where, limit, depth, draft } = args;
38
38
  if (!collectionSchemas.has(collection)) {
39
39
  return textResponse(`Error: Unknown collection "${collection}". Valid: ${findableSlugs.join(', ')}`);
40
40
  }
41
41
  stampMcpContext(req);
42
42
  const collectionConfig = collectionsBySlug.get(collection);
43
43
  try {
44
- if (id) {
44
+ if (documentId) {
45
45
  const doc = await req.payload.findByID({
46
46
  collection: collection,
47
- id,
47
+ id: documentId,
48
48
  depth: depth ?? 1,
49
49
  draft: draft ?? false,
50
50
  req,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/find-document.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { CollectionConfig, PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport {\r\n decorateDraftResponse,\r\n errorMessage,\r\n jsonResponse,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\ninterface FindDocumentArgs {\r\n collection: string\r\n id?: string\r\n where?: string\r\n limit?: number\r\n depth?: number\r\n draft?: boolean\r\n}\r\n\r\n/**\r\n * Polymorphic replacement for the upstream plugin's per-collection\r\n * `find<Resource>` tools. Takes the collection as an arg rather than\r\n * generating one tool per collection — same authoring shape as\r\n * `createDocument` / `updateDocument`.\r\n *\r\n * Two modes:\r\n * - `id` set: `payload.findByID` (single doc)\r\n * - `id` unset: `payload.find` with optional JSON-string `where`\r\n *\r\n * Draft-enabled collections get preview URLs appended to draft documents\r\n * via `decorateDraftResponse`.\r\n */\r\nexport function createFindDocumentTool(\r\n collectionSchemas: Map<string, CollectionSchema>,\r\n draftCollections: Set<string>,\r\n collectionsBySlug: Map<string, CollectionConfig>,\r\n previewSiteUrl: string | undefined,\r\n previewDisabled = false,\r\n) {\r\n const findableSlugs = [...collectionSchemas.keys()]\r\n const descriptionLines = findableSlugs.map(\r\n (slug) =>\r\n ` - \"${slug}\"${draftCollections.has(slug) ? ' (draft-enabled)' : ''}`,\r\n )\r\n\r\n return {\r\n name: 'findDocument',\r\n routing: { kind: 'collection', action: 'read' } as const,\r\n description:\r\n 'Read documents from any collection. Pass `id` for a single document, or omit `id` and pass a Payload `where` filter as a JSON string for a list. ' +\r\n 'Draft-enabled collections include a preview URL on draft documents when available.\\n\\n' +\r\n 'Available collections:\\n' +\r\n descriptionLines.join('\\n'),\r\n parameters: {\r\n collection: z\r\n .string()\r\n .describe(`The collection slug. One of: ${findableSlugs.join(', ')}`),\r\n id: z.string().optional().describe('Document ID. When set, returns a single document.'),\r\n where: z\r\n .string()\r\n .optional()\r\n .describe(\r\n 'JSON-encoded Payload `where` clause. Examples: \\'{\"status\":{\"equals\":\"published\"}}\\', ' +\r\n '\\'{\"slug\":{\"equals\":\"hello-world\"}}\\'. Ignored if `id` is set.',\r\n ),\r\n limit: z\r\n .number()\r\n .int()\r\n .min(1)\r\n .max(100)\r\n .optional()\r\n .describe('Max results when listing. Default 25.'),\r\n depth: z\r\n .number()\r\n .int()\r\n .min(0)\r\n .max(3)\r\n .optional()\r\n .describe('Relationship population depth. Default 1.'),\r\n draft: z\r\n .boolean()\r\n .optional()\r\n .describe(\r\n 'When true, returns draft versions of draft-enabled collections. Default false (published only).',\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 unknown as FindDocumentArgs\r\n const { collection, id, where, limit, depth, draft } = args\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Valid: ${findableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n const collectionConfig = collectionsBySlug.get(collection)\r\n\r\n try {\r\n if (id) {\r\n const doc = await req.payload.findByID({\r\n collection: collection as never,\r\n id,\r\n depth: depth ?? 1,\r\n draft: draft ?? false,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n const base = jsonResponse(doc)\r\n if (previewDisabled) return base\r\n return await decorateDraftResponse(\r\n base,\r\n doc as Record<string, unknown>,\r\n collectionConfig,\r\n req,\r\n previewSiteUrl,\r\n )\r\n }\r\n\r\n let parsedWhere: unknown\r\n if (where && where.trim().length > 0) {\r\n try {\r\n parsedWhere = JSON.parse(where)\r\n } catch (err) {\r\n return textResponse(\r\n `Error: \\`where\\` must be a valid JSON string. ${errorMessage(err)}`,\r\n )\r\n }\r\n }\r\n\r\n const result = await req.payload.find({\r\n collection: collection as never,\r\n where: parsedWhere as never,\r\n depth: depth ?? 1,\r\n limit: limit ?? 25,\r\n draft: draft ?? false,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n pagination: false,\r\n })\r\n\r\n const base = jsonResponse({\r\n totalDocs: (result as { totalDocs?: number }).totalDocs ?? result.docs.length,\r\n docs: result.docs,\r\n })\r\n\r\n if (previewDisabled || !collectionConfig || !draftCollections.has(collection)) return base\r\n\r\n // Decorate any draft docs in the page with preview URLs.\r\n let decorated = base\r\n for (const doc of result.docs as Array<Record<string, unknown>>) {\r\n if (doc._status === 'draft') {\r\n decorated = await decorateDraftResponse(\r\n decorated,\r\n doc,\r\n collectionConfig,\r\n req,\r\n previewSiteUrl,\r\n )\r\n }\r\n }\r\n return decorated\r\n } catch (err) {\r\n return textResponse(\r\n `Error reading from ${collection}: ${errorMessage(err)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","decorateDraftResponse","errorMessage","jsonResponse","stampMcpContext","textResponse","createFindDocumentTool","collectionSchemas","draftCollections","collectionsBySlug","previewSiteUrl","previewDisabled","findableSlugs","keys","descriptionLines","map","slug","has","name","routing","kind","action","description","join","parameters","collection","string","describe","id","optional","where","limit","number","int","min","max","depth","draft","boolean","handler","rawArgs","req","_extra","args","collectionConfig","get","doc","payload","findByID","overrideAccess","user","base","parsedWhere","trim","length","JSON","parse","err","result","find","pagination","totalDocs","docs","decorated","_status"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/tools/find-document.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { CollectionConfig, PayloadRequest } from 'payload'\r\nimport type { CollectionSchema } from '../types'\r\nimport {\r\n decorateDraftResponse,\r\n errorMessage,\r\n jsonResponse,\r\n stampMcpContext,\r\n textResponse,\r\n} from './_helpers'\r\n\r\ninterface FindDocumentArgs {\r\n collection: string\r\n documentId?: string\r\n where?: string\r\n limit?: number\r\n depth?: number\r\n draft?: boolean\r\n}\r\n\r\n/**\r\n * Polymorphic replacement for the upstream plugin's per-collection\r\n * `find<Resource>` tools. Takes the collection as an arg rather than\r\n * generating one tool per collection — same authoring shape as\r\n * `createDocument` / `updateDocument`.\r\n *\r\n * Two modes:\r\n * - `documentId` set: `payload.findByID` (single doc)\r\n * - `documentId` unset: `payload.find` with optional JSON-string `where`\r\n *\r\n * Draft-enabled collections get preview URLs appended to draft documents\r\n * via `decorateDraftResponse`.\r\n */\r\nexport function createFindDocumentTool(\r\n collectionSchemas: Map<string, CollectionSchema>,\r\n draftCollections: Set<string>,\r\n collectionsBySlug: Map<string, CollectionConfig>,\r\n previewSiteUrl: string | undefined,\r\n previewDisabled = false,\r\n) {\r\n const findableSlugs = [...collectionSchemas.keys()]\r\n const descriptionLines = findableSlugs.map(\r\n (slug) =>\r\n ` - \"${slug}\"${draftCollections.has(slug) ? ' (draft-enabled)' : ''}`,\r\n )\r\n\r\n return {\r\n name: 'findDocument',\r\n routing: { kind: 'collection', action: 'read' } as const,\r\n description:\r\n 'Read documents from any collection. Pass `documentId` for a single document, or omit `documentId` and pass a Payload `where` filter as a JSON string for a list. ' +\r\n 'Draft-enabled collections include a preview URL on draft documents when available.\\n\\n' +\r\n 'Available collections:\\n' +\r\n descriptionLines.join('\\n'),\r\n parameters: {\r\n collection: z\r\n .string()\r\n .describe(`The collection slug. One of: ${findableSlugs.join(', ')}`),\r\n documentId: z\r\n .string()\r\n .optional()\r\n .describe('Document ID. When set, returns a single document.'),\r\n where: z\r\n .string()\r\n .optional()\r\n .describe(\r\n 'JSON-encoded Payload `where` clause. Examples: \\'{\"status\":{\"equals\":\"published\"}}\\', ' +\r\n '\\'{\"slug\":{\"equals\":\"hello-world\"}}\\'. Ignored if `documentId` is set.',\r\n ),\r\n limit: z\r\n .number()\r\n .int()\r\n .min(1)\r\n .max(100)\r\n .optional()\r\n .describe('Max results when listing. Default 25.'),\r\n depth: z\r\n .number()\r\n .int()\r\n .min(0)\r\n .max(3)\r\n .optional()\r\n .describe('Relationship population depth. Default 1.'),\r\n draft: z\r\n .boolean()\r\n .optional()\r\n .describe(\r\n 'When true, returns draft versions of draft-enabled collections. Default false (published only).',\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 unknown as FindDocumentArgs\r\n const { collection, documentId, where, limit, depth, draft } = args\r\n\r\n if (!collectionSchemas.has(collection)) {\r\n return textResponse(\r\n `Error: Unknown collection \"${collection}\". Valid: ${findableSlugs.join(', ')}`,\r\n )\r\n }\r\n\r\n stampMcpContext(req)\r\n const collectionConfig = collectionsBySlug.get(collection)\r\n\r\n try {\r\n if (documentId) {\r\n const doc = await req.payload.findByID({\r\n collection: collection as never,\r\n id: documentId,\r\n depth: depth ?? 1,\r\n draft: draft ?? false,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n })\r\n const base = jsonResponse(doc)\r\n if (previewDisabled) return base\r\n return await decorateDraftResponse(\r\n base,\r\n doc as Record<string, unknown>,\r\n collectionConfig,\r\n req,\r\n previewSiteUrl,\r\n )\r\n }\r\n\r\n let parsedWhere: unknown\r\n if (where && where.trim().length > 0) {\r\n try {\r\n parsedWhere = JSON.parse(where)\r\n } catch (err) {\r\n return textResponse(\r\n `Error: \\`where\\` must be a valid JSON string. ${errorMessage(err)}`,\r\n )\r\n }\r\n }\r\n\r\n const result = await req.payload.find({\r\n collection: collection as never,\r\n where: parsedWhere as never,\r\n depth: depth ?? 1,\r\n limit: limit ?? 25,\r\n draft: draft ?? false,\r\n req,\r\n overrideAccess: false,\r\n user: req.user,\r\n pagination: false,\r\n })\r\n\r\n const base = jsonResponse({\r\n totalDocs: (result as { totalDocs?: number }).totalDocs ?? result.docs.length,\r\n docs: result.docs,\r\n })\r\n\r\n if (previewDisabled || !collectionConfig || !draftCollections.has(collection)) return base\r\n\r\n // Decorate any draft docs in the page with preview URLs.\r\n let decorated = base\r\n for (const doc of result.docs as Array<Record<string, unknown>>) {\r\n if (doc._status === 'draft') {\r\n decorated = await decorateDraftResponse(\r\n decorated,\r\n doc,\r\n collectionConfig,\r\n req,\r\n previewSiteUrl,\r\n )\r\n }\r\n }\r\n return decorated\r\n } catch (err) {\r\n return textResponse(\r\n `Error reading from ${collection}: ${errorMessage(err)}`,\r\n )\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","decorateDraftResponse","errorMessage","jsonResponse","stampMcpContext","textResponse","createFindDocumentTool","collectionSchemas","draftCollections","collectionsBySlug","previewSiteUrl","previewDisabled","findableSlugs","keys","descriptionLines","map","slug","has","name","routing","kind","action","description","join","parameters","collection","string","describe","documentId","optional","where","limit","number","int","min","max","depth","draft","boolean","handler","rawArgs","req","_extra","args","collectionConfig","get","doc","payload","findByID","id","overrideAccess","user","base","parsedWhere","trim","length","JSON","parse","err","result","find","pagination","totalDocs","docs","decorated","_status"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { errorMessage, getDocDisplayName, requireDraftCollection, stampMcpContext, textResponse } from './_helpers';
2
+ import { errorMessage, getDocDisplayName, requireDraftCollection, snapshotPublishMarker, stampMcpContext, textResponse, verifyPublishSucceededDespiteError } from './_helpers';
3
3
  export function createPublishDraftTool(draftCollections) {
4
4
  return {
5
5
  name: 'publishDraft',
@@ -20,6 +20,15 @@ export function createPublishDraftTool(draftCollections) {
20
20
  const guard = requireDraftCollection(collection, draftCollections);
21
21
  if (guard) return guard;
22
22
  stampMcpContext(req);
23
+ // Snapshot the doc's pre-update `updatedAt` so the recovery branch
24
+ // below can distinguish "this attempt landed despite a post-write
25
+ // validator throw" from "an older publish was successful and this
26
+ // attempt did nothing" (see verifyPublishSucceededDespiteError).
27
+ const preMarker = await snapshotPublishMarker(req, {
28
+ kind: 'collection',
29
+ slug: collection,
30
+ id: documentId
31
+ });
23
32
  try {
24
33
  const doc = await req.payload.update({
25
34
  collection: collection,
@@ -34,6 +43,29 @@ export function createPublishDraftTool(draftCollections) {
34
43
  const displayName = getDocDisplayName(doc, documentId);
35
44
  return textResponse(`Successfully published "${displayName}" in ${collection} (ID: ${documentId}).`);
36
45
  } catch (error) {
46
+ // Payload's update can throw a field-validation error AFTER a new
47
+ // published version has already been written to the `_<slug>_v`
48
+ // versions table (the validator runs in beforeChange-Fields, which
49
+ // fires after Collection-level beforeChange hooks have already
50
+ // mutated `data` and after the version row has been committed in
51
+ // some draft+versions setups — see the breadcrumb self-reference
52
+ // bug surfaced by `@payloadcms/plugin-nested-docs` on Payload v3).
53
+ // The visible-to-the-user effect is "publish appears to fail but
54
+ // the document is in fact live". Verify against the pre-update
55
+ // marker before downgrading to a warning, so a stale published
56
+ // version from a prior successful publish cannot mask a real
57
+ // failure of the current attempt.
58
+ const liveDoc = await verifyPublishSucceededDespiteError(req, {
59
+ kind: 'collection',
60
+ slug: collection,
61
+ id: documentId
62
+ }, preMarker);
63
+ if (liveDoc) {
64
+ const displayName = getDocDisplayName(liveDoc, documentId);
65
+ // Stable token prefix lets MCP clients branch on the published-
66
+ // with-warning state without regex-matching the prose body.
67
+ return textResponse(`[publishDraft:published_with_warning] ` + `Published "${displayName}" in ${collection} (ID: ${documentId}) — ` + `but Payload reported a post-write validation error: ${errorMessage(error)}. ` + `The document is live; the error did not roll back the published version.`);
68
+ }
37
69
  return textResponse(`Error publishing document ${documentId} in ${collection}: ${errorMessage(error)}`);
38
70
  }
39
71
  }
@@ -1 +1 @@
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"}
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 snapshotPublishMarker,\r\n stampMcpContext,\r\n textResponse,\r\n verifyPublishSucceededDespiteError,\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 // Snapshot the doc's pre-update `updatedAt` so the recovery branch\r\n // below can distinguish \"this attempt landed despite a post-write\r\n // validator throw\" from \"an older publish was successful and this\r\n // attempt did nothing\" (see verifyPublishSucceededDespiteError).\r\n const preMarker = await snapshotPublishMarker(req, {\r\n kind: 'collection',\r\n slug: collection,\r\n id: documentId,\r\n })\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 // Payload's update can throw a field-validation error AFTER a new\r\n // published version has already been written to the `_<slug>_v`\r\n // versions table (the validator runs in beforeChange-Fields, which\r\n // fires after Collection-level beforeChange hooks have already\r\n // mutated `data` and after the version row has been committed in\r\n // some draft+versions setups — see the breadcrumb self-reference\r\n // bug surfaced by `@payloadcms/plugin-nested-docs` on Payload v3).\r\n // The visible-to-the-user effect is \"publish appears to fail but\r\n // the document is in fact live\". Verify against the pre-update\r\n // marker before downgrading to a warning, so a stale published\r\n // version from a prior successful publish cannot mask a real\r\n // failure of the current attempt.\r\n const liveDoc = await verifyPublishSucceededDespiteError(\r\n req,\r\n { kind: 'collection', slug: collection, id: documentId },\r\n preMarker,\r\n )\r\n if (liveDoc) {\r\n const displayName = getDocDisplayName(liveDoc, documentId)\r\n // Stable token prefix lets MCP clients branch on the published-\r\n // with-warning state without regex-matching the prose body.\r\n return textResponse(\r\n `[publishDraft:published_with_warning] ` +\r\n `Published \"${displayName}\" in ${collection} (ID: ${documentId}) — ` +\r\n `but Payload reported a post-write validation error: ${errorMessage(error)}. ` +\r\n `The document is live; the error did not roll back the published version.`,\r\n )\r\n }\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","snapshotPublishMarker","stampMcpContext","textResponse","verifyPublishSucceededDespiteError","createPublishDraftTool","draftCollections","name","routing","kind","action","description","parameters","collection","string","describe","join","documentId","handler","rawArgs","req","_extra","args","guard","preMarker","slug","id","doc","payload","update","data","_status","overrideAccess","user","displayName","error","liveDoc"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAEvB,SACEC,YAAY,EACZC,iBAAiB,EACjBC,sBAAsB,EACtBC,qBAAqB,EACrBC,eAAe,EACfC,YAAY,EACZC,kCAAkC,QAC7B,aAAY;AAEnB,OAAO,SAASC,uBAAuBC,gBAA6B;IAClE,OAAO;QACLC,MAAM;QACNC,SAAS;YAAEC,MAAM;YAAcC,QAAQ;QAAS;QAChDC,aACE,wFACA,0FACA;QACFC,YAAY;YACVC,YAAYhB,EACTiB,MAAM,GACNC,QAAQ,CACP,CAAC,gDAAgD,EAAE;mBAAIT;aAAiB,CAACU,IAAI,CAAC,SAAS,QAAQ;YAEnGC,YAAYpB,EAAEiB,MAAM,GAAGC,QAAQ,CAAC;QAClC;QACAG,SAAS,OACPC,SACAC,KACAC;YAEA,MAAMC,OAAOH;YACb,MAAM,EAAEN,UAAU,EAAEI,UAAU,EAAE,GAAGK;YAEnC,MAAMC,QAAQvB,uBAAuBa,YAAYP;YACjD,IAAIiB,OAAO,OAAOA;YAElBrB,gBAAgBkB;YAEhB,mEAAmE;YACnE,kEAAkE;YAClE,kEAAkE;YAClE,iEAAiE;YACjE,MAAMI,YAAY,MAAMvB,sBAAsBmB,KAAK;gBACjDX,MAAM;gBACNgB,MAAMZ;gBACNa,IAAIT;YACN;YAEA,IAAI;gBACF,MAAMU,MAAM,MAAMP,IAAIQ,OAAO,CAACC,MAAM,CAAC;oBACnChB,YAAYA;oBACZa,IAAIT;oBACJa,MAAM;wBAAEC,SAAS;oBAAY;oBAC7BX;oBACAY,gBAAgB;oBAChBC,MAAMb,IAAIa,IAAI;gBAChB;gBAEA,MAAMC,cAAcnC,kBAAkB4B,KAAKV;gBAC3C,OAAOd,aACL,CAAC,wBAAwB,EAAE+B,YAAY,KAAK,EAAErB,WAAW,MAAM,EAAEI,WAAW,EAAE,CAAC;YAEnF,EAAE,OAAOkB,OAAO;gBACd,kEAAkE;gBAClE,gEAAgE;gBAChE,mEAAmE;gBACnE,+DAA+D;gBAC/D,iEAAiE;gBACjE,iEAAiE;gBACjE,mEAAmE;gBACnE,iEAAiE;gBACjE,+DAA+D;gBAC/D,+DAA+D;gBAC/D,6DAA6D;gBAC7D,kCAAkC;gBAClC,MAAMC,UAAU,MAAMhC,mCACpBgB,KACA;oBAAEX,MAAM;oBAAcgB,MAAMZ;oBAAYa,IAAIT;gBAAW,GACvDO;gBAEF,IAAIY,SAAS;oBACX,MAAMF,cAAcnC,kBAAkBqC,SAASnB;oBAC/C,gEAAgE;oBAChE,4DAA4D;oBAC5D,OAAOd,aACL,CAAC,sCAAsC,CAAC,GACtC,CAAC,WAAW,EAAE+B,YAAY,KAAK,EAAErB,WAAW,MAAM,EAAEI,WAAW,IAAI,CAAC,GACpE,CAAC,oDAAoD,EAAEnB,aAAaqC,OAAO,EAAE,CAAC,GAC9E,CAAC,wEAAwE,CAAC;gBAEhF;gBACA,OAAOhC,aACL,CAAC,0BAA0B,EAAEc,WAAW,IAAI,EAAEJ,WAAW,EAAE,EAAEf,aAAaqC,QAAQ;YAEtF;QACF;IACF;AACF"}
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { errorMessage, slugEnum, stampMcpContext, textResponse } from './_helpers';
2
+ import { errorMessage, slugEnum, snapshotPublishMarker, stampMcpContext, textResponse, verifyPublishSucceededDespiteError } from './_helpers';
3
3
  /**
4
4
  * Promote a draft-enabled global's pending draft to published. Mirrors
5
5
  * publishDraft for collections. Returns null when no global has drafts
@@ -26,7 +26,21 @@ import { errorMessage, slugEnum, stampMcpContext, textResponse } from './_helper
26
26
  return textResponse(`Error: Global "${slug}" does not support drafts. Draft-enabled globals: ${slugs.join(', ') || 'none'}`);
27
27
  }
28
28
  stampMcpContext(req);
29
+ // Capture pre-update marker so the recovery branch below can
30
+ // distinguish a publish that landed despite a post-write throw from
31
+ // a pre-existing published version inherited from an earlier
32
+ // successful publish.
33
+ const preMarker = await snapshotPublishMarker(req, {
34
+ kind: 'global',
35
+ slug,
36
+ ...locale ? {
37
+ locale
38
+ } : {}
39
+ });
29
40
  try {
41
+ // `slug as never` / `locale as never`: Payload's updateGlobal /
42
+ // findGlobal generic narrows the slug to a TConfig-derived literal
43
+ // union we cannot satisfy with a runtime-supplied string.
30
44
  await req.payload.updateGlobal({
31
45
  slug: slug,
32
46
  data: {
@@ -41,6 +55,21 @@ import { errorMessage, slugEnum, stampMcpContext, textResponse } from './_helper
41
55
  });
42
56
  return textResponse(`Successfully published global "${slug}".`);
43
57
  } catch (err) {
58
+ // Mirror publishDraft's recovery (see publish-draft.ts for the
59
+ // longer note on the post-write validator throw). The shared
60
+ // helper enforces the strictly-newer `updatedAt` check, so a
61
+ // pre-existing published row from an earlier publish cannot mask
62
+ // a real failure of the current attempt.
63
+ const liveGlobal = await verifyPublishSucceededDespiteError(req, {
64
+ kind: 'global',
65
+ slug,
66
+ ...locale ? {
67
+ locale
68
+ } : {}
69
+ }, preMarker);
70
+ if (liveGlobal) {
71
+ return textResponse(`[publishGlobalDraft:published_with_warning] ` + `Published global "${slug}" — but Payload reported a post-write validation error: ` + `${errorMessage(err)}. The global is live; the error did not roll back the ` + `published version.`);
72
+ }
44
73
  return textResponse(`Error publishing global "${slug}": ${errorMessage(err)}`);
45
74
  }
46
75
  }
@@ -1 +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"}
1
+ {"version":3,"sources":["../../src/tools/publish-global-draft.ts"],"sourcesContent":["import { z } from 'zod'\r\nimport type { PayloadRequest } from 'payload'\r\nimport {\r\n errorMessage,\r\n slugEnum,\r\n snapshotPublishMarker,\r\n stampMcpContext,\r\n textResponse,\r\n verifyPublishSucceededDespiteError,\r\n} 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 // Capture pre-update marker so the recovery branch below can\r\n // distinguish a publish that landed despite a post-write throw from\r\n // a pre-existing published version inherited from an earlier\r\n // successful publish.\r\n const preMarker = await snapshotPublishMarker(req, {\r\n kind: 'global',\r\n slug,\r\n ...(locale ? { locale } : {}),\r\n })\r\n\r\n try {\r\n // `slug as never` / `locale as never`: Payload's updateGlobal /\r\n // findGlobal generic narrows the slug to a TConfig-derived literal\r\n // union we cannot satisfy with a runtime-supplied string.\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 // Mirror publishDraft's recovery (see publish-draft.ts for the\r\n // longer note on the post-write validator throw). The shared\r\n // helper enforces the strictly-newer `updatedAt` check, so a\r\n // pre-existing published row from an earlier publish cannot mask\r\n // a real failure of the current attempt.\r\n const liveGlobal = await verifyPublishSucceededDespiteError(\r\n req,\r\n { kind: 'global', slug, ...(locale ? { locale } : {}) },\r\n preMarker,\r\n )\r\n if (liveGlobal) {\r\n return textResponse(\r\n `[publishGlobalDraft:published_with_warning] ` +\r\n `Published global \"${slug}\" — but Payload reported a post-write validation error: ` +\r\n `${errorMessage(err)}. The global is live; the error did not roll back the ` +\r\n `published version.`,\r\n )\r\n }\r\n return textResponse(`Error publishing global \"${slug}\": ${errorMessage(err)}`)\r\n }\r\n },\r\n }\r\n}\r\n"],"names":["z","errorMessage","slugEnum","snapshotPublishMarker","stampMcpContext","textResponse","verifyPublishSucceededDespiteError","createPublishGlobalDraftTool","draftGlobals","size","slugs","name","routing","kind","action","description","join","parameters","slug","describe","locale","string","optional","handler","args","req","_extra","has","preMarker","payload","updateGlobal","data","_status","overrideAccess","user","err","liveGlobal"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAEvB,SACEC,YAAY,EACZC,QAAQ,EACRC,qBAAqB,EACrBC,eAAe,EACfC,YAAY,EACZC,kCAAkC,QAC7B,aAAY;AAEnB;;;;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,MAAMhB,SAASQ,OAAO,UAAUS,QAAQ,CAAC,CAAC,yBAAyB,EAAET,MAAMM,IAAI,CAAC,OAAO;YACvFI,QAAQpB,EACLqB,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,OAAOb,aACL,CAAC,eAAe,EAAEa,KAAK,kDAAkD,EAAER,MAAMM,IAAI,CAAC,SAAS,QAAQ;YAE3G;YAEAZ,gBAAgBqB;YAEhB,6DAA6D;YAC7D,oEAAoE;YACpE,6DAA6D;YAC7D,sBAAsB;YACtB,MAAMG,YAAY,MAAMzB,sBAAsBsB,KAAK;gBACjDZ,MAAM;gBACNK;gBACA,GAAIE,SAAS;oBAAEA;gBAAO,IAAI,CAAC,CAAC;YAC9B;YAEA,IAAI;gBACF,gEAAgE;gBAChE,mEAAmE;gBACnE,0DAA0D;gBAC1D,MAAMK,IAAII,OAAO,CAACC,YAAY,CAAC;oBAC7BZ,MAAMA;oBACNa,MAAM;wBAAEC,SAAS;oBAAY;oBAC7B,GAAIZ,SAAS;wBAAEA,QAAQA;oBAAgB,IAAI,CAAC,CAAC;oBAC7CK;oBACAQ,gBAAgB;oBAChBC,MAAMT,IAAIS,IAAI;gBAChB;gBACA,OAAO7B,aAAa,CAAC,+BAA+B,EAAEa,KAAK,EAAE,CAAC;YAChE,EAAE,OAAOiB,KAAK;gBACZ,+DAA+D;gBAC/D,6DAA6D;gBAC7D,6DAA6D;gBAC7D,iEAAiE;gBACjE,yCAAyC;gBACzC,MAAMC,aAAa,MAAM9B,mCACvBmB,KACA;oBAAEZ,MAAM;oBAAUK;oBAAM,GAAIE,SAAS;wBAAEA;oBAAO,IAAI,CAAC,CAAC;gBAAE,GACtDQ;gBAEF,IAAIQ,YAAY;oBACd,OAAO/B,aACL,CAAC,4CAA4C,CAAC,GAC5C,CAAC,kBAAkB,EAAEa,KAAK,wDAAwD,CAAC,GACnF,GAAGjB,aAAakC,KAAK,sDAAsD,CAAC,GAC5E,CAAC,kBAAkB,CAAC;gBAE1B;gBACA,OAAO9B,aAAa,CAAC,yBAAyB,EAAEa,KAAK,GAAG,EAAEjB,aAAakC,MAAM;YAC/E;QACF;IACF;AACF"}