payload-mcp-toolkit 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/dist/__tests__/introspection.test.js +364 -0
  4. package/dist/__tests__/introspection.test.js.map +1 -0
  5. package/dist/__tests__/url-validator.test.js +326 -0
  6. package/dist/__tests__/url-validator.test.js.map +1 -0
  7. package/dist/draft-workflow.d.ts +60 -0
  8. package/dist/draft-workflow.js +93 -0
  9. package/dist/draft-workflow.js.map +1 -0
  10. package/dist/index.d.ts +24 -0
  11. package/dist/index.js +142 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/introspection.d.ts +23 -0
  14. package/dist/introspection.js +238 -0
  15. package/dist/introspection.js.map +1 -0
  16. package/dist/prompts.d.ts +21 -0
  17. package/dist/prompts.js +215 -0
  18. package/dist/prompts.js.map +1 -0
  19. package/dist/rate-limiter.d.ts +25 -0
  20. package/dist/rate-limiter.js +51 -0
  21. package/dist/rate-limiter.js.map +1 -0
  22. package/dist/resources.d.ts +18 -0
  23. package/dist/resources.js +77 -0
  24. package/dist/resources.js.map +1 -0
  25. package/dist/tools/compose-helpers.d.ts +117 -0
  26. package/dist/tools/compose-helpers.js +236 -0
  27. package/dist/tools/compose-helpers.js.map +1 -0
  28. package/dist/tools/compose-layout.d.ts +139 -0
  29. package/dist/tools/compose-layout.js +61 -0
  30. package/dist/tools/compose-layout.js.map +1 -0
  31. package/dist/tools/patch-layout.d.ts +107 -0
  32. package/dist/tools/patch-layout.js +123 -0
  33. package/dist/tools/patch-layout.js.map +1 -0
  34. package/dist/tools/publish-draft.d.ts +24 -0
  35. package/dist/tools/publish-draft.js +69 -0
  36. package/dist/tools/publish-draft.js.map +1 -0
  37. package/dist/tools/resolve-reference.d.ts +31 -0
  38. package/dist/tools/resolve-reference.js +169 -0
  39. package/dist/tools/resolve-reference.js.map +1 -0
  40. package/dist/tools/safe-delete.d.ts +37 -0
  41. package/dist/tools/safe-delete.js +161 -0
  42. package/dist/tools/safe-delete.js.map +1 -0
  43. package/dist/tools/schedule-publish.d.ts +49 -0
  44. package/dist/tools/schedule-publish.js +120 -0
  45. package/dist/tools/schedule-publish.js.map +1 -0
  46. package/dist/tools/search-content.d.ts +43 -0
  47. package/dist/tools/search-content.js +210 -0
  48. package/dist/tools/search-content.js.map +1 -0
  49. package/dist/tools/update-document.d.ts +32 -0
  50. package/dist/tools/update-document.js +114 -0
  51. package/dist/tools/update-document.js.map +1 -0
  52. package/dist/tools/upload-media.d.ts +26 -0
  53. package/dist/tools/upload-media.js +115 -0
  54. package/dist/tools/upload-media.js.map +1 -0
  55. package/dist/tools/versions.d.ts +50 -0
  56. package/dist/tools/versions.js +159 -0
  57. package/dist/tools/versions.js.map +1 -0
  58. package/dist/types.d.ts +118 -0
  59. package/dist/types.js +3 -0
  60. package/dist/types.js.map +1 -0
  61. package/dist/url-validator.d.ts +36 -0
  62. package/dist/url-validator.js +222 -0
  63. package/dist/url-validator.js.map +1 -0
  64. package/package.json +85 -0
@@ -0,0 +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'\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 req.context = { ...req.context, source: 'mcp' }\n\n const cappedLimit = Math.min(Math.max(1, limit), HARD_LIMIT)\n\n // Determine target collections.\n // When the user filters by a specific draft status, only consider collections\n // that actually support drafts — other collections are not \"draft\" or\n // \"published\" in any meaningful sense and shouldn't pollute the results.\n const isDraftStatusFilter = status === 'draft' || status === 'published'\n const initialTargets = collection\n ? collectionSchemas.has(collection)\n ? [collection]\n : []\n : allSlugs\n\n const targets = isDraftStatusFilter\n ? initialTargets.filter((slug) => collectionSchemas.get(slug)?.hasDrafts)\n : initialTargets\n\n if (targets.length === 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n hits: {},\n message: collection\n ? `Unknown collection \"${collection}\". Available: ${allSlugs.join(', ')}`\n : 'No searchable collections found.',\n }),\n },\n ],\n }\n }\n\n const grouped: Record<string, SearchHit[]> = {}\n const stats: Record<string, { totalDocs: number; returned: number }> = {}\n\n for (const slug of targets) {\n const schema = collectionSchemas.get(slug)!\n const where = buildWhereClause(schema, {\n query,\n status,\n olderThanDays,\n newerThanDays,\n missingFields,\n })\n\n try {\n const result = await 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' actually works and so missingFields\n // queries against draft-only collections aren't misleadingly empty.\n draft: schema.hasDrafts,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\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 } catch {\n // Skip collections that fail (permissions, missing fields, etc.) — return what we can\n continue\n }\n }\n\n const totalHits = Object.values(grouped).reduce((sum, hits) => sum + hits.length, 0)\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n totalHits,\n stats,\n hits: grouped,\n }),\n },\n ],\n }\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 // Free-text query against searchable fields\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 (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 // Age filters\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","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","context","source","cappedLimit","Math","min","max","isDraftStatusFilter","initialTargets","has","targets","filter","slug","get","hasDrafts","length","content","type","text","JSON","stringify","hits","message","grouped","stats","schema","where","buildWhereClause","result","payload","find","sort","depth","draft","overrideAccess","user","totalDocs","returned","docs","map","doc","buildHit","totalHits","Object","values","reduce","sum","hit","id","displayName","title","String","_status","updatedAt","f","isFieldEmpty","getByPath","path","current","segment","split","undefined","value","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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"}
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import type { PayloadRequest } from 'payload';
3
+ import type { CollectionSchema } from '../types';
4
+ /**
5
+ * Creates the updateDocument MCP tool that updates fields on an existing document.
6
+ *
7
+ * This is a custom replacement for the official plugin's update tools, which
8
+ * fail on collections with upload fields due to a Zod schema generation bug.
9
+ * Uses Payload's Local API directly, bypassing the problematic schema pipeline.
10
+ *
11
+ * @param collectionSchemas - Introspected collection schemas for validation and description
12
+ * @param draftCollections - Set of collection slugs that support drafts
13
+ */
14
+ export declare function createUpdateDocumentTool(collectionSchemas: Map<string, CollectionSchema>, draftCollections: Set<string>): {
15
+ name: string;
16
+ description: string;
17
+ parameters: {
18
+ collection: z.ZodString;
19
+ documentId: z.ZodString;
20
+ data: z.ZodString;
21
+ };
22
+ handler: (args: {
23
+ collection: string;
24
+ documentId: string;
25
+ data: string;
26
+ }, req: PayloadRequest, _extra: unknown) => Promise<{
27
+ content: {
28
+ type: "text";
29
+ text: string;
30
+ }[];
31
+ }>;
32
+ };
@@ -0,0 +1,114 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Creates the updateDocument MCP tool that updates fields on an existing document.
4
+ *
5
+ * This is a custom replacement for the official plugin's update tools, which
6
+ * fail on collections with upload fields due to a Zod schema generation bug.
7
+ * Uses Payload's Local API directly, bypassing the problematic schema pipeline.
8
+ *
9
+ * @param collectionSchemas - Introspected collection schemas for validation and description
10
+ * @param draftCollections - Set of collection slugs that support drafts
11
+ */ export function createUpdateDocumentTool(collectionSchemas, draftCollections) {
12
+ const updatableSlugs = [
13
+ ...collectionSchemas.keys()
14
+ ].filter((slug)=>slug !== 'media');
15
+ const collectionDescriptions = updatableSlugs.map((slug)=>{
16
+ const schema = collectionSchemas.get(slug);
17
+ const fieldNames = schema.fields.map((f)=>f.name).join(', ');
18
+ return ` - "${slug}": ${fieldNames}`;
19
+ }).join('\n');
20
+ return {
21
+ name: 'updateDocument',
22
+ 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,
23
+ parameters: {
24
+ collection: z.string().describe(`The collection slug. One of: ${updatableSlugs.join(', ')}`),
25
+ documentId: z.string().describe('The ID of the document to update'),
26
+ 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"]}\'')
27
+ },
28
+ handler: async (args, req, _extra)=>{
29
+ const { collection, documentId } = args;
30
+ let data;
31
+ try {
32
+ data = JSON.parse(args.data);
33
+ } catch {
34
+ return {
35
+ content: [
36
+ {
37
+ type: 'text',
38
+ text: 'Error: "data" must be a valid JSON string. Example: \'{"title": "New Title"}\''
39
+ }
40
+ ]
41
+ };
42
+ }
43
+ if (!collectionSchemas.has(collection)) {
44
+ return {
45
+ content: [
46
+ {
47
+ type: 'text',
48
+ text: `Error: Unknown collection "${collection}". ` + `Available: ${updatableSlugs.join(', ')}`
49
+ }
50
+ ]
51
+ };
52
+ }
53
+ if (collection === 'media') {
54
+ return {
55
+ content: [
56
+ {
57
+ type: 'text',
58
+ text: 'Error: Use the uploadMedia tool to manage media files.'
59
+ }
60
+ ]
61
+ };
62
+ }
63
+ if (!data || Object.keys(data).length === 0) {
64
+ return {
65
+ content: [
66
+ {
67
+ type: 'text',
68
+ text: 'Error: No fields provided in "data". Pass an object with field names and values to update.'
69
+ }
70
+ ]
71
+ };
72
+ }
73
+ req.context = {
74
+ ...req.context,
75
+ source: 'mcp'
76
+ };
77
+ const isDraftCollection = draftCollections.has(collection);
78
+ try {
79
+ const doc = await req.payload.update({
80
+ collection: collection,
81
+ id: documentId,
82
+ data: data,
83
+ draft: isDraftCollection,
84
+ req,
85
+ overrideAccess: false,
86
+ user: req.user
87
+ });
88
+ const displayName = doc.name || doc.title || doc.slug || documentId;
89
+ const updatedFields = Object.keys(data).join(', ');
90
+ const draftNote = isDraftCollection ? ` Document is in draft status — use publishDraft to make it live.` : '';
91
+ return {
92
+ content: [
93
+ {
94
+ type: 'text',
95
+ text: `Updated "${displayName}" in ${collection} (ID: ${documentId}). ` + `Changed fields: ${updatedFields}.${draftNote}`
96
+ }
97
+ ]
98
+ };
99
+ } catch (error) {
100
+ const message = error instanceof Error ? error.message : String(error);
101
+ return {
102
+ content: [
103
+ {
104
+ type: 'text',
105
+ text: `Error updating document ${documentId} in ${collection}: ${message}`
106
+ }
107
+ ]
108
+ };
109
+ }
110
+ }
111
+ };
112
+ }
113
+
114
+ //# sourceMappingURL=update-document.js.map
@@ -0,0 +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'\n\n/**\n * Creates the updateDocument MCP tool that updates fields on an existing document.\n *\n * This is a custom replacement for the official plugin's update tools, which\n * fail on collections with upload fields due to a Zod schema generation bug.\n * Uses Payload's Local API directly, bypassing the problematic schema pipeline.\n *\n * @param collectionSchemas - Introspected collection schemas for validation and description\n * @param draftCollections - Set of collection slugs that support drafts\n */\nexport function createUpdateDocumentTool(\n collectionSchemas: Map<string, CollectionSchema>,\n draftCollections: Set<string>,\n) {\n const updatableSlugs = [...collectionSchemas.keys()].filter(\n (slug) => slug !== 'media',\n )\n\n const collectionDescriptions = updatableSlugs\n .map((slug) => {\n const schema = collectionSchemas.get(slug)!\n const fieldNames = schema.fields.map((f) => f.name).join(', ')\n return ` - \"${slug}\": ${fieldNames}`\n })\n .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 {\n content: [\n {\n type: 'text' as const,\n text: 'Error: \"data\" must be a valid JSON string. Example: \\'{\"title\": \"New Title\"}\\'',\n },\n ],\n }\n }\n\n if (!collectionSchemas.has(collection)) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: Unknown collection \"${collection}\". ` +\n `Available: ${updatableSlugs.join(', ')}`,\n },\n ],\n }\n }\n\n if (collection === 'media') {\n return {\n content: [\n {\n type: 'text' as const,\n text: 'Error: Use the uploadMedia tool to manage media files.',\n },\n ],\n }\n }\n\n if (!data || Object.keys(data).length === 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: 'Error: No fields provided in \"data\". Pass an object with field names and values to update.',\n },\n ],\n }\n }\n\n req.context = { ...req.context, source: 'mcp' }\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 =\n (doc as any).name ||\n (doc as any).title ||\n (doc as any).slug ||\n documentId\n\n const updatedFields = Object.keys(data).join(', ')\n const draftNote = isDraftCollection\n ? ` Document is in draft status — use publishDraft to make it live.`\n : ''\n\n return {\n content: [\n {\n type: 'text' as const,\n text: `Updated \"${displayName}\" in ${collection} (ID: ${documentId}). ` +\n `Changed fields: ${updatedFields}.${draftNote}`,\n },\n ],\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error updating document ${documentId} in ${collection}: ${message}`,\n },\n ],\n }\n }\n },\n }\n}\n"],"names":["z","createUpdateDocumentTool","collectionSchemas","draftCollections","updatableSlugs","keys","filter","slug","collectionDescriptions","map","schema","get","fieldNames","fields","f","name","join","description","parameters","collection","string","describe","documentId","data","handler","args","req","_extra","JSON","parse","content","type","text","has","Object","length","context","source","isDraftCollection","doc","payload","update","id","draft","overrideAccess","user","displayName","title","updatedFields","draftNote","error","message","Error","String"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ import type { PayloadRequest } from 'payload';
3
+ /**
4
+ * Creates the uploadMedia MCP tool that fetches an image from a public URL,
5
+ * validates it for SSRF safety and content type, then creates a Media document.
6
+ */
7
+ export declare function createUploadMediaTool(options?: {
8
+ maxFileSize?: number;
9
+ collectionSlug?: string;
10
+ }): {
11
+ name: string;
12
+ description: string;
13
+ parameters: {
14
+ url: z.ZodString;
15
+ alt: z.ZodOptional<z.ZodString>;
16
+ };
17
+ handler: (args: {
18
+ url: string;
19
+ alt?: string;
20
+ }, req: PayloadRequest, _extra: unknown) => Promise<{
21
+ content: {
22
+ type: "text";
23
+ text: string;
24
+ }[];
25
+ }>;
26
+ };
@@ -0,0 +1,115 @@
1
+ import { z } from 'zod';
2
+ import { validateAndFetchUrl } from '../url-validator';
3
+ const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
4
+ ;
5
+ const ALLOWED_IMAGE_TYPES = new Set([
6
+ 'image/jpeg',
7
+ 'image/png',
8
+ 'image/webp',
9
+ 'image/gif'
10
+ ]);
11
+ /**
12
+ * Creates the uploadMedia MCP tool that fetches an image from a public URL,
13
+ * validates it for SSRF safety and content type, then creates a Media document.
14
+ */ export function createUploadMediaTool(options) {
15
+ const maxFileSize = options?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
16
+ const mediaSlug = options?.collectionSlug ?? 'media';
17
+ return {
18
+ name: 'uploadMedia',
19
+ 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.',
20
+ parameters: {
21
+ url: z.string().url().describe('Public HTTPS URL of the image to upload'),
22
+ alt: z.string().optional().describe('Alt text for the image. Generates from filename if omitted.')
23
+ },
24
+ handler: async (args, req, _extra)=>{
25
+ const { url } = args;
26
+ let buffer;
27
+ let contentType;
28
+ let filename;
29
+ try {
30
+ const result = await validateAndFetchUrl(url, {
31
+ maxBytes: maxFileSize
32
+ });
33
+ buffer = result.buffer;
34
+ contentType = result.contentType;
35
+ filename = result.filename;
36
+ } catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ return {
39
+ content: [
40
+ {
41
+ type: 'text',
42
+ text: `Error fetching URL: ${message}`
43
+ }
44
+ ]
45
+ };
46
+ }
47
+ if (!ALLOWED_IMAGE_TYPES.has(contentType)) {
48
+ return {
49
+ content: [
50
+ {
51
+ type: 'text',
52
+ text: `Error: Unsupported Content-Type "${contentType}". ` + `Allowed types: ${[
53
+ ...ALLOWED_IMAGE_TYPES
54
+ ].join(', ')}`
55
+ }
56
+ ]
57
+ };
58
+ }
59
+ if (buffer.byteLength > maxFileSize) {
60
+ const sizeMB = (buffer.byteLength / (1024 * 1024)).toFixed(2);
61
+ const limitMB = (maxFileSize / (1024 * 1024)).toFixed(2);
62
+ return {
63
+ content: [
64
+ {
65
+ type: 'text',
66
+ text: `Error: File size ${sizeMB}MB exceeds maximum ${limitMB}MB.`
67
+ }
68
+ ]
69
+ };
70
+ }
71
+ const alt = args.alt || filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim() || 'Uploaded image';
72
+ req.context = {
73
+ ...req.context,
74
+ source: 'mcp'
75
+ };
76
+ try {
77
+ const doc = await req.payload.create({
78
+ collection: mediaSlug,
79
+ data: {
80
+ alt
81
+ },
82
+ file: {
83
+ data: buffer,
84
+ mimetype: contentType,
85
+ name: filename,
86
+ size: buffer.byteLength
87
+ },
88
+ req,
89
+ overrideAccess: false,
90
+ user: req.user
91
+ });
92
+ return {
93
+ content: [
94
+ {
95
+ type: 'text',
96
+ text: `Successfully uploaded "${filename}" to ${mediaSlug}.\n` + `ID: ${doc.id}\n` + `Filename: ${filename}\n` + `Alt: ${alt}`
97
+ }
98
+ ]
99
+ };
100
+ } catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ return {
103
+ content: [
104
+ {
105
+ type: 'text',
106
+ text: `Error creating media document: ${message}`
107
+ }
108
+ ]
109
+ };
110
+ }
111
+ }
112
+ };
113
+ }
114
+
115
+ //# sourceMappingURL=upload-media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tools/upload-media.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport { validateAndFetchUrl } from '../url-validator'\n\nconst DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB\nconst ALLOWED_IMAGE_TYPES = new Set([\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n])\n\n/**\n * Creates the uploadMedia MCP tool that fetches an image from a public URL,\n * validates it for SSRF safety and content type, then creates a Media document.\n */\nexport function createUploadMediaTool(options?: {\n maxFileSize?: number\n collectionSlug?: string\n}) {\n const maxFileSize = options?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE\n const mediaSlug = options?.collectionSlug ?? 'media'\n\n return {\n name: 'uploadMedia',\n description:\n 'Upload an image to the media library from a public HTTPS URL. ' +\n 'Fetches the image with SSRF protection, validates it is an allowed image type ' +\n '(JPEG, PNG, WebP, GIF), and creates a Media document with alt text. ' +\n 'Returns the created document ID, filename, and alt text.',\n parameters: {\n url: z.string().url().describe('Public HTTPS URL of the image to upload'),\n alt: z\n .string()\n .optional()\n .describe('Alt text for the image. Generates from filename if omitted.'),\n },\n handler: async (\n args: { url: string; alt?: string },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const { url } = args\n\n let buffer: Buffer\n let contentType: string\n let filename: string\n try {\n const result = await validateAndFetchUrl(url, { maxBytes: maxFileSize })\n buffer = result.buffer\n contentType = result.contentType\n filename = result.filename\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error fetching URL: ${message}`,\n },\n ],\n }\n }\n\n if (!ALLOWED_IMAGE_TYPES.has(contentType)) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: Unsupported Content-Type \"${contentType}\". ` +\n `Allowed types: ${[...ALLOWED_IMAGE_TYPES].join(', ')}`,\n },\n ],\n }\n }\n\n if (buffer.byteLength > maxFileSize) {\n const sizeMB = (buffer.byteLength / (1024 * 1024)).toFixed(2)\n const limitMB = (maxFileSize / (1024 * 1024)).toFixed(2)\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: File size ${sizeMB}MB exceeds maximum ${limitMB}MB.`,\n },\n ],\n }\n }\n\n const alt =\n args.alt ||\n filename\n .replace(/\\.[^.]+$/, '')\n .replace(/[-_]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim() ||\n 'Uploaded image'\n\n req.context = { ...req.context, source: 'mcp' }\n\n try {\n const doc = await req.payload.create({\n collection: mediaSlug as any,\n data: { alt } as any,\n file: {\n data: buffer,\n mimetype: contentType,\n name: filename,\n size: buffer.byteLength,\n },\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n return {\n content: [\n {\n type: 'text' as const,\n text: `Successfully uploaded \"${filename}\" to ${mediaSlug}.\\n` +\n `ID: ${doc.id}\\n` +\n `Filename: ${filename}\\n` +\n `Alt: ${alt}`,\n },\n ],\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error creating media document: ${message}`,\n },\n ],\n }\n }\n },\n }\n}\n"],"names":["z","validateAndFetchUrl","DEFAULT_MAX_FILE_SIZE","ALLOWED_IMAGE_TYPES","Set","createUploadMediaTool","options","maxFileSize","mediaSlug","collectionSlug","name","description","parameters","url","string","describe","alt","optional","handler","args","req","_extra","buffer","contentType","filename","result","maxBytes","error","message","Error","String","content","type","text","has","join","byteLength","sizeMB","toFixed","limitMB","replace","trim","context","source","doc","payload","create","collection","data","file","mimetype","size","overrideAccess","user","id"],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { z } from 'zod';
2
+ import type { PayloadRequest } from 'payload';
3
+ /**
4
+ * Creates the listVersions MCP tool that returns recent saved versions of a draft document.
5
+ *
6
+ * Only works on collections in `draftCollections`. Returns id, _status, updatedAt, and a
7
+ * compact display name per version so an LLM can pick one to restore.
8
+ */
9
+ export declare function createListVersionsTool(draftCollections: Set<string>): {
10
+ name: string;
11
+ description: string;
12
+ parameters: {
13
+ collection: z.ZodString;
14
+ documentId: z.ZodString;
15
+ limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
16
+ };
17
+ handler: (args: {
18
+ collection: string;
19
+ documentId: string;
20
+ limit?: number;
21
+ }, req: PayloadRequest, _extra: unknown) => Promise<{
22
+ content: {
23
+ type: "text";
24
+ text: string;
25
+ }[];
26
+ }>;
27
+ };
28
+ /**
29
+ * Creates the restoreVersion MCP tool that rolls a document back to a saved version.
30
+ *
31
+ * Restoring a version creates a new version on top — the old current state is preserved
32
+ * so the operation is itself reversible via another restore.
33
+ */
34
+ export declare function createRestoreVersionTool(draftCollections: Set<string>): {
35
+ name: string;
36
+ description: string;
37
+ parameters: {
38
+ collection: z.ZodString;
39
+ versionId: z.ZodString;
40
+ };
41
+ handler: (args: {
42
+ collection: string;
43
+ versionId: string;
44
+ }, req: PayloadRequest, _extra: unknown) => Promise<{
45
+ content: {
46
+ type: "text";
47
+ text: string;
48
+ }[];
49
+ }>;
50
+ };
@@ -0,0 +1,159 @@
1
+ import { z } from 'zod';
2
+ const DEFAULT_LIST_LIMIT = 10;
3
+ /**
4
+ * Creates the listVersions MCP tool that returns recent saved versions of a draft document.
5
+ *
6
+ * Only works on collections in `draftCollections`. Returns id, _status, updatedAt, and a
7
+ * compact display name per version so an LLM can pick one to restore.
8
+ */ export function createListVersionsTool(draftCollections) {
9
+ return {
10
+ name: 'listVersions',
11
+ description: 'List recent saved versions of a document on a draft-enabled collection. ' + 'Use before restoreVersion to pick the right point in time. ' + `Draft-enabled collections: ${[
12
+ ...draftCollections
13
+ ].join(', ') || 'none'}`,
14
+ parameters: {
15
+ collection: z.string().describe(`The collection slug. Must be one of: ${[
16
+ ...draftCollections
17
+ ].join(', ') || 'none'}`),
18
+ documentId: z.string().describe('The ID of the document whose versions you want to list'),
19
+ limit: z.number().optional().default(DEFAULT_LIST_LIMIT).describe(`Maximum number of versions to return (default ${DEFAULT_LIST_LIMIT})`)
20
+ },
21
+ handler: async (args, req, _extra)=>{
22
+ const { collection, documentId, limit = DEFAULT_LIST_LIMIT } = args;
23
+ if (!draftCollections.has(collection)) {
24
+ return {
25
+ content: [
26
+ {
27
+ type: 'text',
28
+ text: `Error: Collection "${collection}" does not support versions. ` + `Draft-enabled collections: ${[
29
+ ...draftCollections
30
+ ].join(', ') || 'none'}`
31
+ }
32
+ ]
33
+ };
34
+ }
35
+ req.context = {
36
+ ...req.context,
37
+ source: 'mcp'
38
+ };
39
+ try {
40
+ const result = await req.payload.findVersions({
41
+ collection: collection,
42
+ where: {
43
+ parent: {
44
+ equals: documentId
45
+ }
46
+ },
47
+ sort: '-updatedAt',
48
+ limit,
49
+ req,
50
+ overrideAccess: false,
51
+ user: req.user
52
+ });
53
+ const versions = result.docs.map((v)=>{
54
+ const snapshot = v.version || {};
55
+ return {
56
+ id: v.id,
57
+ updatedAt: v.updatedAt,
58
+ createdAt: v.createdAt,
59
+ status: snapshot._status ?? 'unknown',
60
+ displayName: snapshot.name || snapshot.title || snapshot.slug || `${collection}#${documentId}`,
61
+ autosave: v.autosave === true
62
+ };
63
+ });
64
+ return {
65
+ content: [
66
+ {
67
+ type: 'text',
68
+ text: JSON.stringify({
69
+ collection,
70
+ documentId,
71
+ totalDocs: result.totalDocs,
72
+ returned: versions.length,
73
+ versions
74
+ })
75
+ }
76
+ ]
77
+ };
78
+ } catch (error) {
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ return {
81
+ content: [
82
+ {
83
+ type: 'text',
84
+ text: `Error listing versions for ${collection}#${documentId}: ${message}`
85
+ }
86
+ ]
87
+ };
88
+ }
89
+ }
90
+ };
91
+ }
92
+ /**
93
+ * Creates the restoreVersion MCP tool that rolls a document back to a saved version.
94
+ *
95
+ * Restoring a version creates a new version on top — the old current state is preserved
96
+ * so the operation is itself reversible via another restore.
97
+ */ export function createRestoreVersionTool(draftCollections) {
98
+ return {
99
+ name: 'restoreVersion',
100
+ description: 'Restore a document to a previously saved version. ' + 'Use listVersions first to find the version ID. ' + 'Restoring creates a new version on top, so the previous current state is also recoverable. ' + `Draft-enabled collections: ${[
101
+ ...draftCollections
102
+ ].join(', ') || 'none'}`,
103
+ parameters: {
104
+ collection: z.string().describe(`The collection slug. Must be one of: ${[
105
+ ...draftCollections
106
+ ].join(', ') || 'none'}`),
107
+ versionId: z.string().describe('The version ID returned by listVersions (NOT the document ID)')
108
+ },
109
+ handler: async (args, req, _extra)=>{
110
+ const { collection, versionId } = args;
111
+ if (!draftCollections.has(collection)) {
112
+ return {
113
+ content: [
114
+ {
115
+ type: 'text',
116
+ text: `Error: Collection "${collection}" does not support versions. ` + `Draft-enabled collections: ${[
117
+ ...draftCollections
118
+ ].join(', ') || 'none'}`
119
+ }
120
+ ]
121
+ };
122
+ }
123
+ req.context = {
124
+ ...req.context,
125
+ source: 'mcp'
126
+ };
127
+ try {
128
+ const restored = await req.payload.restoreVersion({
129
+ collection: collection,
130
+ id: versionId,
131
+ req,
132
+ overrideAccess: false,
133
+ user: req.user
134
+ });
135
+ const displayName = restored.name || restored.title || restored.slug || restored.id;
136
+ return {
137
+ content: [
138
+ {
139
+ type: 'text',
140
+ text: `Restored ${collection} document "${displayName}" (ID: ${restored.id}) ` + `from version ${versionId}. The document is now in draft status — ` + `use publishDraft to make the restored content live.`
141
+ }
142
+ ]
143
+ };
144
+ } catch (error) {
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ return {
147
+ content: [
148
+ {
149
+ type: 'text',
150
+ text: `Error restoring ${collection} from version ${versionId}: ${message}`
151
+ }
152
+ ]
153
+ };
154
+ }
155
+ }
156
+ };
157
+ }
158
+
159
+ //# sourceMappingURL=versions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tools/versions.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\n\nconst DEFAULT_LIST_LIMIT = 10\n\n/**\n * Creates the listVersions MCP tool that returns recent saved versions of a draft document.\n *\n * Only works on collections in `draftCollections`. Returns id, _status, updatedAt, and a\n * compact display name per version so an LLM can pick one to restore.\n */\nexport function createListVersionsTool(draftCollections: Set<string>) {\n return {\n name: 'listVersions',\n description:\n 'List recent saved versions of a document on a draft-enabled collection. ' +\n 'Use before restoreVersion to pick the right point in time. ' +\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\n parameters: {\n collection: z\n .string()\n .describe(\n `The collection slug. Must be one of: ${[...draftCollections].join(', ') || 'none'}`,\n ),\n documentId: z.string().describe('The ID of the document whose versions you want to list'),\n limit: z\n .number()\n .optional()\n .default(DEFAULT_LIST_LIMIT)\n .describe(`Maximum number of versions to return (default ${DEFAULT_LIST_LIMIT})`),\n },\n handler: async (\n args: { collection: string; documentId: string; limit?: number },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const { collection, documentId, limit = DEFAULT_LIST_LIMIT } = args\n\n if (!draftCollections.has(collection)) {\n return {\n content: [\n {\n type: 'text' as const,\n text:\n `Error: Collection \"${collection}\" does not support versions. ` +\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\n },\n ],\n }\n }\n\n req.context = { ...req.context, source: 'mcp' }\n\n try {\n const result = await req.payload.findVersions({\n collection: collection as any,\n where: { parent: { equals: documentId } },\n sort: '-updatedAt',\n limit,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const versions = result.docs.map((v: any) => {\n const snapshot = v.version || {}\n return {\n id: v.id,\n updatedAt: v.updatedAt,\n createdAt: v.createdAt,\n status: snapshot._status ?? 'unknown',\n displayName:\n snapshot.name ||\n snapshot.title ||\n snapshot.slug ||\n `${collection}#${documentId}`,\n autosave: v.autosave === true,\n }\n })\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n collection,\n documentId,\n totalDocs: result.totalDocs,\n returned: versions.length,\n versions,\n }),\n },\n ],\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error listing versions for ${collection}#${documentId}: ${message}`,\n },\n ],\n }\n }\n },\n }\n}\n\n/**\n * Creates the restoreVersion MCP tool that rolls a document back to a saved version.\n *\n * Restoring a version creates a new version on top — the old current state is preserved\n * so the operation is itself reversible via another restore.\n */\nexport function createRestoreVersionTool(draftCollections: Set<string>) {\n return {\n name: 'restoreVersion',\n description:\n 'Restore a document to a previously saved version. ' +\n 'Use listVersions first to find the version ID. ' +\n 'Restoring creates a new version on top, so the previous current state is also recoverable. ' +\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\n parameters: {\n collection: z\n .string()\n .describe(\n `The collection slug. Must be one of: ${[...draftCollections].join(', ') || 'none'}`,\n ),\n versionId: z\n .string()\n .describe('The version ID returned by listVersions (NOT the document ID)'),\n },\n handler: async (\n args: { collection: string; versionId: string },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const { collection, versionId } = args\n\n if (!draftCollections.has(collection)) {\n return {\n content: [\n {\n type: 'text' as const,\n text:\n `Error: Collection \"${collection}\" does not support versions. ` +\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\n },\n ],\n }\n }\n\n req.context = { ...req.context, source: 'mcp' }\n\n try {\n const restored = await req.payload.restoreVersion({\n collection: collection as any,\n id: versionId,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const displayName =\n (restored as any).name ||\n (restored as any).title ||\n (restored as any).slug ||\n (restored as any).id\n\n return {\n content: [\n {\n type: 'text' as const,\n text:\n `Restored ${collection} document \"${displayName}\" (ID: ${(restored as any).id}) ` +\n `from version ${versionId}. The document is now in draft status — ` +\n `use publishDraft to make the restored content live.`,\n },\n ],\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error restoring ${collection} from version ${versionId}: ${message}`,\n },\n ],\n }\n }\n },\n }\n}\n"],"names":["z","DEFAULT_LIST_LIMIT","createListVersionsTool","draftCollections","name","description","join","parameters","collection","string","describe","documentId","limit","number","optional","default","handler","args","req","_extra","has","content","type","text","context","source","result","payload","findVersions","where","parent","equals","sort","overrideAccess","user","versions","docs","map","v","snapshot","version","id","updatedAt","createdAt","status","_status","displayName","title","slug","autosave","JSON","stringify","totalDocs","returned","length","error","message","Error","String","createRestoreVersionTool","versionId","restored","restoreVersion"],"mappings":"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"}