payload-mcp-toolkit 0.3.0 → 0.3.1
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.
- package/README.md +150 -150
- package/dist/__tests__/introspection.test.js.map +1 -1
- package/dist/draft-workflow.js +29 -29
- package/dist/draft-workflow.js.map +1 -1
- package/dist/index.js +20 -33
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts +4 -0
- package/dist/introspection.js +38 -33
- package/dist/introspection.js.map +1 -1
- package/dist/prompts.js +5 -5
- package/dist/prompts.js.map +1 -1
- package/dist/resources.js +9 -16
- package/dist/resources.js.map +1 -1
- package/dist/tools/_helpers.d.ts +14 -0
- package/dist/tools/_helpers.js +35 -0
- package/dist/tools/_helpers.js.map +1 -0
- package/dist/tools/patch-layout.d.ts +3 -9
- package/dist/tools/patch-layout.js +29 -48
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/tools/publish-draft.d.ts +1 -11
- package/dist/tools/publish-draft.js +8 -39
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/resolve-reference.d.ts +1 -12
- package/dist/tools/resolve-reference.js +45 -85
- package/dist/tools/resolve-reference.js.map +1 -1
- package/dist/tools/safe-delete.d.ts +8 -13
- package/dist/tools/safe-delete.js +68 -100
- package/dist/tools/safe-delete.js.map +1 -1
- package/dist/tools/schedule-publish.d.ts +11 -21
- package/dist/tools/schedule-publish.js +18 -61
- package/dist/tools/schedule-publish.js.map +1 -1
- package/dist/tools/search-content.d.ts +1 -6
- package/dist/tools/search-content.js +52 -64
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/update-document.d.ts +4 -14
- package/dist/tools/update-document.js +23 -72
- package/dist/tools/update-document.js.map +1 -1
- package/dist/tools/upload-media.d.ts +1 -10
- package/dist/tools/upload-media.js +11 -54
- package/dist/tools/upload-media.js.map +1 -1
- package/dist/tools/versions.d.ts +7 -20
- package/dist/tools/versions.js +25 -82
- package/dist/tools/versions.js.map +1 -1
- package/dist/types.js +5 -5
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/rate-limiter.d.ts +0 -25
- package/dist/rate-limiter.js +0 -51
- package/dist/rate-limiter.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { jsonResponse, stampMcpContext } from './_helpers';
|
|
2
3
|
const DEFAULT_LIMIT = 20;
|
|
3
4
|
const HARD_LIMIT = 100;
|
|
4
5
|
/**
|
|
@@ -35,36 +36,29 @@ const HARD_LIMIT = 100;
|
|
|
35
36
|
},
|
|
36
37
|
handler: async (args, req, _extra)=>{
|
|
37
38
|
const { collection, query, status, olderThanDays, newerThanDays, missingFields, limit = DEFAULT_LIMIT } = args;
|
|
38
|
-
req
|
|
39
|
-
...req.context,
|
|
40
|
-
source: 'mcp'
|
|
41
|
-
};
|
|
39
|
+
stampMcpContext(req);
|
|
42
40
|
const cappedLimit = Math.min(Math.max(1, limit), HARD_LIMIT);
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// that actually support drafts — other collections are not "draft" or
|
|
46
|
-
// "published" in any meaningful sense and shouldn't pollute the results.
|
|
41
|
+
// When filtering by draft status, only collections that support drafts
|
|
42
|
+
// are meaningful — others can't be "draft" or "published".
|
|
47
43
|
const isDraftStatusFilter = status === 'draft' || status === 'published';
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
let initialTargets;
|
|
45
|
+
if (!collection) {
|
|
46
|
+
initialTargets = allSlugs;
|
|
47
|
+
} else if (collectionSchemas.has(collection)) {
|
|
48
|
+
initialTargets = [
|
|
49
|
+
collection
|
|
50
|
+
];
|
|
51
|
+
} else {
|
|
52
|
+
initialTargets = [];
|
|
53
|
+
}
|
|
51
54
|
const targets = isDraftStatusFilter ? initialTargets.filter((slug)=>collectionSchemas.get(slug)?.hasDrafts) : initialTargets;
|
|
52
55
|
if (targets.length === 0) {
|
|
53
|
-
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
text: JSON.stringify({
|
|
58
|
-
hits: {},
|
|
59
|
-
message: collection ? `Unknown collection "${collection}". Available: ${allSlugs.join(', ')}` : 'No searchable collections found.'
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
};
|
|
56
|
+
return jsonResponse({
|
|
57
|
+
hits: {},
|
|
58
|
+
message: collection ? `Unknown collection "${collection}". Available: ${allSlugs.join(', ')}` : 'No searchable collections found.'
|
|
59
|
+
});
|
|
64
60
|
}
|
|
65
|
-
const
|
|
66
|
-
const stats = {};
|
|
67
|
-
for (const slug of targets){
|
|
61
|
+
const settled = await Promise.allSettled(targets.map((slug)=>{
|
|
68
62
|
const schema = collectionSchemas.get(slug);
|
|
69
63
|
const where = buildWhereClause(schema, {
|
|
70
64
|
query,
|
|
@@ -73,44 +67,40 @@ const HARD_LIMIT = 100;
|
|
|
73
67
|
newerThanDays,
|
|
74
68
|
missingFields
|
|
75
69
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
70
|
+
return req.payload.find({
|
|
71
|
+
collection: slug,
|
|
72
|
+
where: where,
|
|
73
|
+
limit: cappedLimit,
|
|
74
|
+
sort: '-updatedAt',
|
|
75
|
+
depth: 0,
|
|
76
|
+
// Include drafts so status='draft' works and missingFields queries
|
|
77
|
+
// against draft-only collections aren't misleadingly empty.
|
|
78
|
+
draft: schema.hasDrafts,
|
|
79
|
+
req,
|
|
80
|
+
overrideAccess: false,
|
|
81
|
+
user: req.user
|
|
82
|
+
});
|
|
83
|
+
}));
|
|
84
|
+
const grouped = {};
|
|
85
|
+
const stats = {};
|
|
86
|
+
settled.forEach((outcome, i)=>{
|
|
87
|
+
if (outcome.status !== 'fulfilled') return;
|
|
88
|
+
const slug = targets[i];
|
|
89
|
+
const result = outcome.value;
|
|
90
|
+
if (result.totalDocs > 0) {
|
|
91
|
+
stats[slug] = {
|
|
92
|
+
totalDocs: result.totalDocs,
|
|
93
|
+
returned: result.docs.length
|
|
94
|
+
};
|
|
95
|
+
grouped[slug] = result.docs.map((doc)=>buildHit(doc, missingFields));
|
|
99
96
|
}
|
|
100
|
-
}
|
|
97
|
+
});
|
|
101
98
|
const totalHits = Object.values(grouped).reduce((sum, hits)=>sum + hits.length, 0);
|
|
102
|
-
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
totalHits,
|
|
108
|
-
stats,
|
|
109
|
-
hits: grouped
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
]
|
|
113
|
-
};
|
|
99
|
+
return jsonResponse({
|
|
100
|
+
totalHits,
|
|
101
|
+
stats,
|
|
102
|
+
hits: grouped
|
|
103
|
+
});
|
|
114
104
|
}
|
|
115
105
|
};
|
|
116
106
|
}
|
|
@@ -145,7 +135,6 @@ function isFieldEmpty(value) {
|
|
|
145
135
|
}
|
|
146
136
|
function buildWhereClause(schema, filters) {
|
|
147
137
|
const and = [];
|
|
148
|
-
// Free-text query against searchable fields
|
|
149
138
|
if (filters.query && schema.searchableFields.length > 0) {
|
|
150
139
|
const or = schema.searchableFields.map((field)=>({
|
|
151
140
|
[field]: {
|
|
@@ -156,7 +145,7 @@ function buildWhereClause(schema, filters) {
|
|
|
156
145
|
or
|
|
157
146
|
});
|
|
158
147
|
}
|
|
159
|
-
// Status filter
|
|
148
|
+
// Status filter is only meaningful on draft-enabled collections
|
|
160
149
|
if (filters.status && filters.status !== 'any' && schema.hasDrafts) {
|
|
161
150
|
and.push({
|
|
162
151
|
_status: {
|
|
@@ -164,7 +153,6 @@ function buildWhereClause(schema, filters) {
|
|
|
164
153
|
}
|
|
165
154
|
});
|
|
166
155
|
}
|
|
167
|
-
// Age filters
|
|
168
156
|
if (filters.olderThanDays !== undefined) {
|
|
169
157
|
const cutoff = new Date(Date.now() - filters.olderThanDays * 24 * 60 * 60 * 1000);
|
|
170
158
|
and.push({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tools/search-content.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { CollectionSchema } from '../types'\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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"}
|
|
1
|
+
{"version":3,"sources":["../../src/tools/search-content.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { CollectionSchema } from '../types'\nimport { jsonResponse, stampMcpContext } from './_helpers'\n\nconst DEFAULT_LIMIT = 20\nconst HARD_LIMIT = 100\n\ninterface SearchHit {\n id: string | number\n displayName: string\n status?: string\n updatedAt?: string\n missingFields?: string[]\n}\n\n/**\n * Creates the searchContent MCP tool — natural-language filters across collections,\n * built for editor triage tasks that the official find tools don't express well.\n *\n * Examples the LLM can drive:\n * - \"all posts that are still drafts\"\n * - \"pages missing a meta description\"\n * - \"anything updated more than 30 days ago\"\n * - \"posts by jane in the last quarter\"\n *\n * Returns compact hits per collection — id, displayName, _status, updatedAt, and\n * (when missingFields was requested) which of those fields are blank on each doc.\n */\nexport function createSearchContentTool(\n collectionSchemas: Map<string, CollectionSchema>,\n) {\n const allSlugs = [...collectionSchemas.keys()]\n\n return {\n name: 'searchContent',\n description:\n 'Search and filter content across collections by status, age, missing fields, or free-text query. ' +\n 'Designed for editor triage — finding drafts, stale content, content with missing SEO fields, etc. ' +\n `Searchable collections: ${allSlugs.join(', ')}.`,\n parameters: {\n collection: z\n .string()\n .optional()\n .describe('Restrict search to a single collection slug. Omit to search all.'),\n query: z\n .string()\n .optional()\n .describe('Free-text query matched against name/title/slug fields (case-insensitive).'),\n status: z\n .enum(['draft', 'published', 'any'])\n .optional()\n .describe(\n 'Filter by draft status. \"draft\" or \"published\" only return matching docs; \"any\" or omitted returns all.',\n ),\n olderThanDays: z\n .number()\n .optional()\n .describe('Only docs whose updatedAt is older than this many days.'),\n newerThanDays: z\n .number()\n .optional()\n .describe('Only docs whose updatedAt is newer than this many days.'),\n missingFields: z\n .array(z.string())\n .optional()\n .describe(\n 'Field names that should be empty/null. Useful for finding e.g. posts without a coverImage. ' +\n 'Each hit will include a missingFields array confirming which were actually blank.',\n ),\n limit: z\n .number()\n .optional()\n .default(DEFAULT_LIMIT)\n .describe(`Maximum hits per collection (default ${DEFAULT_LIMIT}, max ${HARD_LIMIT}).`),\n },\n handler: async (\n args: {\n collection?: string\n query?: string\n status?: 'draft' | 'published' | 'any'\n olderThanDays?: number\n newerThanDays?: number\n missingFields?: string[]\n limit?: number\n },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const {\n collection,\n query,\n status,\n olderThanDays,\n newerThanDays,\n missingFields,\n limit = DEFAULT_LIMIT,\n } = args\n\n stampMcpContext(req)\n\n const cappedLimit = Math.min(Math.max(1, limit), HARD_LIMIT)\n\n // When filtering by draft status, only collections that support drafts\n // are meaningful — others can't be \"draft\" or \"published\".\n const isDraftStatusFilter = status === 'draft' || status === 'published'\n\n let initialTargets: string[]\n if (!collection) {\n initialTargets = allSlugs\n } else if (collectionSchemas.has(collection)) {\n initialTargets = [collection]\n } else {\n initialTargets = []\n }\n\n const targets = isDraftStatusFilter\n ? initialTargets.filter((slug) => collectionSchemas.get(slug)?.hasDrafts)\n : initialTargets\n\n if (targets.length === 0) {\n return jsonResponse({\n hits: {},\n message: collection\n ? `Unknown collection \"${collection}\". Available: ${allSlugs.join(', ')}`\n : 'No searchable collections found.',\n })\n }\n\n const settled = await Promise.allSettled(\n targets.map((slug) => {\n const schema = collectionSchemas.get(slug)!\n const where = buildWhereClause(schema, {\n query,\n status,\n olderThanDays,\n newerThanDays,\n missingFields,\n })\n return req.payload.find({\n collection: slug as any,\n where: where as any,\n limit: cappedLimit,\n sort: '-updatedAt',\n depth: 0,\n // Include drafts so status='draft' works and missingFields queries\n // against draft-only collections aren't misleadingly empty.\n draft: schema.hasDrafts,\n req,\n overrideAccess: false,\n user: req.user,\n })\n }),\n )\n\n const grouped: Record<string, SearchHit[]> = {}\n const stats: Record<string, { totalDocs: number; returned: number }> = {}\n\n settled.forEach((outcome, i) => {\n if (outcome.status !== 'fulfilled') return\n const slug = targets[i]\n const result = outcome.value\n if (result.totalDocs > 0) {\n stats[slug] = { totalDocs: result.totalDocs, returned: result.docs.length }\n grouped[slug] = result.docs.map((doc: any) => buildHit(doc, missingFields))\n }\n })\n\n const totalHits = Object.values(grouped).reduce((sum, hits) => sum + hits.length, 0)\n\n return jsonResponse({ totalHits, stats, hits: grouped })\n },\n }\n}\n\nfunction buildHit(doc: Record<string, any>, missingFields?: string[]): SearchHit {\n const hit: SearchHit = {\n id: doc.id,\n displayName: doc.name || doc.title || doc.slug || String(doc.id),\n status: doc._status,\n updatedAt: doc.updatedAt,\n }\n\n if (missingFields?.length) {\n hit.missingFields = missingFields.filter((f) => isFieldEmpty(getByPath(doc, f)))\n }\n\n return hit\n}\n\nfunction getByPath(doc: Record<string, any>, path: string): unknown {\n // Walk dotted paths so `meta.description` reads doc.meta?.description rather\n // than the literal key `\"meta.description\"`. Matches the `where` keys we emit.\n let current: any = doc\n for (const segment of path.split('.')) {\n if (current === null || current === undefined) return undefined\n current = current[segment]\n }\n return current\n}\n\nfunction isFieldEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === 'string') return value.trim() === ''\n if (Array.isArray(value)) return value.length === 0\n if (typeof value === 'object') return Object.keys(value as object).length === 0\n return false\n}\n\ninterface FilterArgs {\n query?: string\n status?: 'draft' | 'published' | 'any'\n olderThanDays?: number\n newerThanDays?: number\n missingFields?: string[]\n}\n\nfunction buildWhereClause(schema: CollectionSchema, filters: FilterArgs): Record<string, unknown> {\n const and: Array<Record<string, unknown>> = []\n\n if (filters.query && schema.searchableFields.length > 0) {\n const or = schema.searchableFields.map((field) => ({\n [field]: { like: filters.query },\n }))\n and.push({ or })\n }\n\n // Status filter is only meaningful on draft-enabled collections\n if (filters.status && filters.status !== 'any' && schema.hasDrafts) {\n and.push({ _status: { equals: filters.status } })\n }\n\n if (filters.olderThanDays !== undefined) {\n const cutoff = new Date(Date.now() - filters.olderThanDays * 24 * 60 * 60 * 1000)\n and.push({ updatedAt: { less_than: cutoff.toISOString() } })\n }\n if (filters.newerThanDays !== undefined) {\n const cutoff = new Date(Date.now() - filters.newerThanDays * 24 * 60 * 60 * 1000)\n and.push({ updatedAt: { greater_than: cutoff.toISOString() } })\n }\n\n // Missing-field filter — express as \"field is null OR field doesn't exist\"\n if (filters.missingFields?.length) {\n for (const field of filters.missingFields) {\n and.push({\n or: [{ [field]: { exists: false } }, { [field]: { equals: null } }],\n })\n }\n }\n\n if (and.length === 0) return {}\n if (and.length === 1) return and[0]\n return { and }\n}\n"],"names":["z","jsonResponse","stampMcpContext","DEFAULT_LIMIT","HARD_LIMIT","createSearchContentTool","collectionSchemas","allSlugs","keys","name","description","join","parameters","collection","string","optional","describe","query","status","enum","olderThanDays","number","newerThanDays","missingFields","array","limit","default","handler","args","req","_extra","cappedLimit","Math","min","max","isDraftStatusFilter","initialTargets","has","targets","filter","slug","get","hasDrafts","length","hits","message","settled","Promise","allSettled","map","schema","where","buildWhereClause","payload","find","sort","depth","draft","overrideAccess","user","grouped","stats","forEach","outcome","i","result","value","totalDocs","returned","docs","doc","buildHit","totalHits","Object","values","reduce","sum","hit","id","displayName","title","String","_status","updatedAt","f","isFieldEmpty","getByPath","path","current","segment","split","undefined","trim","Array","isArray","filters","and","searchableFields","or","field","like","push","equals","cutoff","Date","now","less_than","toISOString","greater_than","exists"],"mappings":"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"}
|
|
@@ -2,14 +2,9 @@ import { z } from 'zod';
|
|
|
2
2
|
import type { PayloadRequest } from 'payload';
|
|
3
3
|
import type { CollectionSchema } from '../types';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
5
|
+
* Custom replacement for the official plugin's update tools, which fail on
|
|
6
|
+
* collections with upload fields due to a Zod schema generation bug. Uses
|
|
7
|
+
* Payload's Local API directly, bypassing the problematic schema pipeline.
|
|
13
8
|
*/
|
|
14
9
|
export declare function createUpdateDocumentTool(collectionSchemas: Map<string, CollectionSchema>, draftCollections: Set<string>): {
|
|
15
10
|
name: string;
|
|
@@ -23,10 +18,5 @@ export declare function createUpdateDocumentTool(collectionSchemas: Map<string,
|
|
|
23
18
|
collection: string;
|
|
24
19
|
documentId: string;
|
|
25
20
|
data: string;
|
|
26
|
-
}, req: PayloadRequest, _extra: unknown) => Promise<
|
|
27
|
-
content: {
|
|
28
|
-
type: "text";
|
|
29
|
-
text: string;
|
|
30
|
-
}[];
|
|
31
|
-
}>;
|
|
21
|
+
}, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
|
|
32
22
|
};
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { DRAFT_NOTE, errorMessage, getDocDisplayName, stampMcpContext, textResponse } from './_helpers';
|
|
3
|
+
const MEDIA_SLUG = 'media';
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
5
|
+
* Custom replacement for the official plugin's update tools, which fail on
|
|
6
|
+
* collections with upload fields due to a Zod schema generation bug. Uses
|
|
7
|
+
* Payload's Local API directly, bypassing the problematic schema pipeline.
|
|
11
8
|
*/ export function createUpdateDocumentTool(collectionSchemas, draftCollections) {
|
|
12
|
-
const updatableSlugs = [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
const updatableSlugs = [];
|
|
10
|
+
const descriptionLines = [];
|
|
11
|
+
for (const [slug, schema] of collectionSchemas){
|
|
12
|
+
if (slug === MEDIA_SLUG) continue;
|
|
13
|
+
updatableSlugs.push(slug);
|
|
14
|
+
descriptionLines.push(` - "${slug}": ${schema.fields.map((f)=>f.name).join(', ')}`);
|
|
15
|
+
}
|
|
16
|
+
const collectionDescriptions = descriptionLines.join('\n');
|
|
20
17
|
return {
|
|
21
18
|
name: 'updateDocument',
|
|
22
19
|
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,
|
|
@@ -31,49 +28,18 @@ import { z } from 'zod';
|
|
|
31
28
|
try {
|
|
32
29
|
data = JSON.parse(args.data);
|
|
33
30
|
} 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
|
-
};
|
|
31
|
+
return textResponse('Error: "data" must be a valid JSON string. Example: \'{"title": "New Title"}\'');
|
|
42
32
|
}
|
|
43
33
|
if (!collectionSchemas.has(collection)) {
|
|
44
|
-
return {
|
|
45
|
-
content: [
|
|
46
|
-
{
|
|
47
|
-
type: 'text',
|
|
48
|
-
text: `Error: Unknown collection "${collection}". ` + `Available: ${updatableSlugs.join(', ')}`
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
};
|
|
34
|
+
return textResponse(`Error: Unknown collection "${collection}". Available: ${updatableSlugs.join(', ')}`);
|
|
52
35
|
}
|
|
53
|
-
if (collection ===
|
|
54
|
-
return
|
|
55
|
-
content: [
|
|
56
|
-
{
|
|
57
|
-
type: 'text',
|
|
58
|
-
text: 'Error: Use the uploadMedia tool to manage media files.'
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
};
|
|
36
|
+
if (collection === MEDIA_SLUG) {
|
|
37
|
+
return textResponse('Error: Use the uploadMedia tool to manage media files.');
|
|
62
38
|
}
|
|
63
39
|
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
|
-
};
|
|
40
|
+
return textResponse('Error: No fields provided in "data". Pass an object with field names and values to update.');
|
|
72
41
|
}
|
|
73
|
-
req
|
|
74
|
-
...req.context,
|
|
75
|
-
source: 'mcp'
|
|
76
|
-
};
|
|
42
|
+
stampMcpContext(req);
|
|
77
43
|
const isDraftCollection = draftCollections.has(collection);
|
|
78
44
|
try {
|
|
79
45
|
const doc = await req.payload.update({
|
|
@@ -85,27 +51,12 @@ import { z } from 'zod';
|
|
|
85
51
|
overrideAccess: false,
|
|
86
52
|
user: req.user
|
|
87
53
|
});
|
|
88
|
-
const displayName = doc
|
|
54
|
+
const displayName = getDocDisplayName(doc, documentId);
|
|
89
55
|
const updatedFields = Object.keys(data).join(', ');
|
|
90
|
-
const draftNote = isDraftCollection ?
|
|
91
|
-
return {
|
|
92
|
-
content: [
|
|
93
|
-
{
|
|
94
|
-
type: 'text',
|
|
95
|
-
text: `Updated "${displayName}" in ${collection} (ID: ${documentId}). ` + `Changed fields: ${updatedFields}.${draftNote}`
|
|
96
|
-
}
|
|
97
|
-
]
|
|
98
|
-
};
|
|
56
|
+
const draftNote = isDraftCollection ? DRAFT_NOTE : '';
|
|
57
|
+
return textResponse(`Updated "${displayName}" in ${collection} (ID: ${documentId}). ` + `Changed fields: ${updatedFields}.${draftNote}`);
|
|
99
58
|
} catch (error) {
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
content: [
|
|
103
|
-
{
|
|
104
|
-
type: 'text',
|
|
105
|
-
text: `Error updating document ${documentId} in ${collection}: ${message}`
|
|
106
|
-
}
|
|
107
|
-
]
|
|
108
|
-
};
|
|
59
|
+
return textResponse(`Error updating document ${documentId} in ${collection}: ${errorMessage(error)}`);
|
|
109
60
|
}
|
|
110
61
|
}
|
|
111
62
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tools/update-document.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { CollectionSchema } from '../types'\
|
|
1
|
+
{"version":3,"sources":["../../src/tools/update-document.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { CollectionSchema } from '../types'\nimport {\n DRAFT_NOTE,\n errorMessage,\n getDocDisplayName,\n stampMcpContext,\n textResponse,\n} from './_helpers'\n\nconst MEDIA_SLUG = 'media'\n\n/**\n * Custom replacement for the official plugin's update tools, which fail on\n * collections with upload fields due to a Zod schema generation bug. Uses\n * Payload's Local API directly, bypassing the problematic schema pipeline.\n */\nexport function createUpdateDocumentTool(\n collectionSchemas: Map<string, CollectionSchema>,\n draftCollections: Set<string>,\n) {\n const updatableSlugs: string[] = []\n const descriptionLines: string[] = []\n for (const [slug, schema] of collectionSchemas) {\n if (slug === MEDIA_SLUG) continue\n updatableSlugs.push(slug)\n descriptionLines.push(` - \"${slug}\": ${schema.fields.map((f) => f.name).join(', ')}`)\n }\n const collectionDescriptions = descriptionLines.join('\\n')\n\n return {\n name: 'updateDocument',\n description:\n 'Update fields on an existing document in any collection. ' +\n 'Pass only the fields you want to change — unspecified fields are left untouched. ' +\n 'For draft-enabled collections, updates create a new draft version (use publishDraft to make it live). ' +\n 'For relationship fields, pass the related document ID (use resolveReference to find IDs). ' +\n 'For upload fields, pass the media document ID (use uploadMedia to create one first).\\n\\n' +\n 'Available collections and their fields:\\n' +\n collectionDescriptions,\n parameters: {\n collection: z\n .string()\n .describe(`The collection slug. One of: ${updatableSlugs.join(', ')}`),\n documentId: z\n .string()\n .describe('The ID of the document to update'),\n data: z\n .string()\n .describe(\n 'JSON string of field names to new values. Only include fields you want to change. ' +\n 'Examples: \\'{\"title\": \"New Title\"}\\', \\'{\"featured\": true, \"category\": \"category-id\"}\\', ' +\n '\\'{\"tags\": [\"news\", \"update\"]}\\'',\n ),\n },\n handler: async (\n args: { collection: string; documentId: string; data: string },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const { collection, documentId } = args\n\n let data: Record<string, unknown>\n try {\n data = JSON.parse(args.data)\n } catch {\n return textResponse(\n 'Error: \"data\" must be a valid JSON string. Example: \\'{\"title\": \"New Title\"}\\'',\n )\n }\n\n if (!collectionSchemas.has(collection)) {\n return textResponse(\n `Error: Unknown collection \"${collection}\". Available: ${updatableSlugs.join(', ')}`,\n )\n }\n\n if (collection === MEDIA_SLUG) {\n return textResponse('Error: Use the uploadMedia tool to manage media files.')\n }\n\n if (!data || Object.keys(data).length === 0) {\n return textResponse(\n 'Error: No fields provided in \"data\". Pass an object with field names and values to update.',\n )\n }\n\n stampMcpContext(req)\n\n const isDraftCollection = draftCollections.has(collection)\n\n try {\n const doc = await req.payload.update({\n collection: collection as any,\n id: documentId,\n data: data as any,\n draft: isDraftCollection,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const displayName = getDocDisplayName(doc, documentId)\n const updatedFields = Object.keys(data).join(', ')\n const draftNote = isDraftCollection ? DRAFT_NOTE : ''\n\n return textResponse(\n `Updated \"${displayName}\" in ${collection} (ID: ${documentId}). ` +\n `Changed fields: ${updatedFields}.${draftNote}`,\n )\n } catch (error) {\n return textResponse(\n `Error updating document ${documentId} in ${collection}: ${errorMessage(error)}`,\n )\n }\n },\n }\n}\n"],"names":["z","DRAFT_NOTE","errorMessage","getDocDisplayName","stampMcpContext","textResponse","MEDIA_SLUG","createUpdateDocumentTool","collectionSchemas","draftCollections","updatableSlugs","descriptionLines","slug","schema","push","fields","map","f","name","join","collectionDescriptions","description","parameters","collection","string","describe","documentId","data","handler","args","req","_extra","JSON","parse","has","Object","keys","length","isDraftCollection","doc","payload","update","id","draft","overrideAccess","user","displayName","updatedFields","draftNote","error"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB,SACEC,UAAU,EACVC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,EACfC,YAAY,QACP,aAAY;AAEnB,MAAMC,aAAa;AAEnB;;;;CAIC,GACD,OAAO,SAASC,yBACdC,iBAAgD,EAChDC,gBAA6B;IAE7B,MAAMC,iBAA2B,EAAE;IACnC,MAAMC,mBAA6B,EAAE;IACrC,KAAK,MAAM,CAACC,MAAMC,OAAO,IAAIL,kBAAmB;QAC9C,IAAII,SAASN,YAAY;QACzBI,eAAeI,IAAI,CAACF;QACpBD,iBAAiBG,IAAI,CAAC,CAAC,KAAK,EAAEF,KAAK,GAAG,EAAEC,OAAOE,MAAM,CAACC,GAAG,CAAC,CAACC,IAAMA,EAAEC,IAAI,EAAEC,IAAI,CAAC,OAAO;IACvF;IACA,MAAMC,yBAAyBT,iBAAiBQ,IAAI,CAAC;IAErD,OAAO;QACLD,MAAM;QACNG,aACE,8DACA,sFACA,2GACA,+FACA,6FACA,8CACAD;QACFE,YAAY;YACVC,YAAYvB,EACTwB,MAAM,GACNC,QAAQ,CAAC,CAAC,6BAA6B,EAAEf,eAAeS,IAAI,CAAC,OAAO;YACvEO,YAAY1B,EACTwB,MAAM,GACNC,QAAQ,CAAC;YACZE,MAAM3B,EACHwB,MAAM,GACNC,QAAQ,CACP,uFACA,8FACA;QAEN;QACAG,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAER,UAAU,EAAEG,UAAU,EAAE,GAAGG;YAEnC,IAAIF;YACJ,IAAI;gBACFA,OAAOK,KAAKC,KAAK,CAACJ,KAAKF,IAAI;YAC7B,EAAE,OAAM;gBACN,OAAOtB,aACL;YAEJ;YAEA,IAAI,CAACG,kBAAkB0B,GAAG,CAACX,aAAa;gBACtC,OAAOlB,aACL,CAAC,2BAA2B,EAAEkB,WAAW,cAAc,EAAEb,eAAeS,IAAI,CAAC,OAAO;YAExF;YAEA,IAAII,eAAejB,YAAY;gBAC7B,OAAOD,aAAa;YACtB;YAEA,IAAI,CAACsB,QAAQQ,OAAOC,IAAI,CAACT,MAAMU,MAAM,KAAK,GAAG;gBAC3C,OAAOhC,aACL;YAEJ;YAEAD,gBAAgB0B;YAEhB,MAAMQ,oBAAoB7B,iBAAiByB,GAAG,CAACX;YAE/C,IAAI;gBACF,MAAMgB,MAAM,MAAMT,IAAIU,OAAO,CAACC,MAAM,CAAC;oBACnClB,YAAYA;oBACZmB,IAAIhB;oBACJC,MAAMA;oBACNgB,OAAOL;oBACPR;oBACAc,gBAAgB;oBAChBC,MAAMf,IAAIe,IAAI;gBAChB;gBAEA,MAAMC,cAAc3C,kBAAkBoC,KAAKb;gBAC3C,MAAMqB,gBAAgBZ,OAAOC,IAAI,CAACT,MAAMR,IAAI,CAAC;gBAC7C,MAAM6B,YAAYV,oBAAoBrC,aAAa;gBAEnD,OAAOI,aACL,CAAC,SAAS,EAAEyC,YAAY,KAAK,EAAEvB,WAAW,MAAM,EAAEG,WAAW,GAAG,CAAC,GAC/D,CAAC,gBAAgB,EAAEqB,cAAc,CAAC,EAAEC,WAAW;YAErD,EAAE,OAAOC,OAAO;gBACd,OAAO5C,aACL,CAAC,wBAAwB,EAAEqB,WAAW,IAAI,EAAEH,WAAW,EAAE,EAAErB,aAAa+C,QAAQ;YAEpF;QACF;IACF;AACF"}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
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
3
|
export declare function createUploadMediaTool(options?: {
|
|
8
4
|
maxFileSize?: number;
|
|
9
5
|
collectionSlug?: string;
|
|
@@ -17,10 +13,5 @@ export declare function createUploadMediaTool(options?: {
|
|
|
17
13
|
handler: (args: {
|
|
18
14
|
url: string;
|
|
19
15
|
alt?: string;
|
|
20
|
-
}, req: PayloadRequest, _extra: unknown) => Promise<
|
|
21
|
-
content: {
|
|
22
|
-
type: "text";
|
|
23
|
-
text: string;
|
|
24
|
-
}[];
|
|
25
|
-
}>;
|
|
16
|
+
}, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
|
|
26
17
|
};
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { validateAndFetchUrl } from '../url-validator';
|
|
3
|
-
|
|
4
|
-
;
|
|
3
|
+
import { errorMessage, stampMcpContext, textResponse } from './_helpers';
|
|
4
|
+
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
5
5
|
const ALLOWED_IMAGE_TYPES = new Set([
|
|
6
6
|
'image/jpeg',
|
|
7
7
|
'image/png',
|
|
8
8
|
'image/webp',
|
|
9
9
|
'image/gif'
|
|
10
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) {
|
|
11
|
+
export function createUploadMediaTool(options) {
|
|
15
12
|
const maxFileSize = options?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
16
13
|
const mediaSlug = options?.collectionSlug ?? 'media';
|
|
17
14
|
return {
|
|
@@ -34,45 +31,20 @@ const ALLOWED_IMAGE_TYPES = new Set([
|
|
|
34
31
|
contentType = result.contentType;
|
|
35
32
|
filename = result.filename;
|
|
36
33
|
} catch (error) {
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
content: [
|
|
40
|
-
{
|
|
41
|
-
type: 'text',
|
|
42
|
-
text: `Error fetching URL: ${message}`
|
|
43
|
-
}
|
|
44
|
-
]
|
|
45
|
-
};
|
|
34
|
+
return textResponse(`Error fetching URL: ${errorMessage(error)}`);
|
|
46
35
|
}
|
|
47
36
|
if (!ALLOWED_IMAGE_TYPES.has(contentType)) {
|
|
48
|
-
return {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
type: 'text',
|
|
52
|
-
text: `Error: Unsupported Content-Type "${contentType}". ` + `Allowed types: ${[
|
|
53
|
-
...ALLOWED_IMAGE_TYPES
|
|
54
|
-
].join(', ')}`
|
|
55
|
-
}
|
|
56
|
-
]
|
|
57
|
-
};
|
|
37
|
+
return textResponse(`Error: Unsupported Content-Type "${contentType}". ` + `Allowed types: ${[
|
|
38
|
+
...ALLOWED_IMAGE_TYPES
|
|
39
|
+
].join(', ')}`);
|
|
58
40
|
}
|
|
59
41
|
if (buffer.byteLength > maxFileSize) {
|
|
60
42
|
const sizeMB = (buffer.byteLength / (1024 * 1024)).toFixed(2);
|
|
61
43
|
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
|
-
};
|
|
44
|
+
return textResponse(`Error: File size ${sizeMB}MB exceeds maximum ${limitMB}MB.`);
|
|
70
45
|
}
|
|
71
46
|
const alt = args.alt || filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim() || 'Uploaded image';
|
|
72
|
-
req
|
|
73
|
-
...req.context,
|
|
74
|
-
source: 'mcp'
|
|
75
|
-
};
|
|
47
|
+
stampMcpContext(req);
|
|
76
48
|
try {
|
|
77
49
|
const doc = await req.payload.create({
|
|
78
50
|
collection: mediaSlug,
|
|
@@ -89,24 +61,9 @@ const ALLOWED_IMAGE_TYPES = new Set([
|
|
|
89
61
|
overrideAccess: false,
|
|
90
62
|
user: req.user
|
|
91
63
|
});
|
|
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
|
-
};
|
|
64
|
+
return textResponse(`Successfully uploaded "${filename}" to ${mediaSlug}.\n` + `ID: ${doc.id}\n` + `Filename: ${filename}\n` + `Alt: ${alt}`);
|
|
100
65
|
} catch (error) {
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
content: [
|
|
104
|
-
{
|
|
105
|
-
type: 'text',
|
|
106
|
-
text: `Error creating media document: ${message}`
|
|
107
|
-
}
|
|
108
|
-
]
|
|
109
|
-
};
|
|
66
|
+
return textResponse(`Error creating media document: ${errorMessage(error)}`);
|
|
110
67
|
}
|
|
111
68
|
}
|
|
112
69
|
};
|
|
@@ -1 +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
|
|
1
|
+
{"version":3,"sources":["../../src/tools/upload-media.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport { validateAndFetchUrl } from '../url-validator'\nimport { errorMessage, stampMcpContext, textResponse } from './_helpers'\n\nconst DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024\nconst ALLOWED_IMAGE_TYPES = new Set([\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n])\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 return textResponse(`Error fetching URL: ${errorMessage(error)}`)\n }\n\n if (!ALLOWED_IMAGE_TYPES.has(contentType)) {\n return textResponse(\n `Error: Unsupported Content-Type \"${contentType}\". ` +\n `Allowed types: ${[...ALLOWED_IMAGE_TYPES].join(', ')}`,\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 textResponse(`Error: File size ${sizeMB}MB exceeds maximum ${limitMB}MB.`)\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 stampMcpContext(req)\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 textResponse(\n `Successfully uploaded \"${filename}\" to ${mediaSlug}.\\n` +\n `ID: ${doc.id}\\n` +\n `Filename: ${filename}\\n` +\n `Alt: ${alt}`,\n )\n } catch (error) {\n return textResponse(`Error creating media document: ${errorMessage(error)}`)\n }\n },\n }\n}\n"],"names":["z","validateAndFetchUrl","errorMessage","stampMcpContext","textResponse","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","has","join","byteLength","sizeMB","toFixed","limitMB","replace","trim","doc","payload","create","collection","data","file","mimetype","size","overrideAccess","user","id"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAEvB,SAASC,mBAAmB,QAAQ,mBAAkB;AACtD,SAASC,YAAY,EAAEC,eAAe,EAAEC,YAAY,QAAQ,aAAY;AAExE,MAAMC,wBAAwB,KAAK,OAAO;AAC1C,MAAMC,sBAAsB,IAAIC,IAAI;IAClC;IACA;IACA;IACA;CACD;AAED,OAAO,SAASC,sBAAsBC,OAGrC;IACC,MAAMC,cAAcD,SAASC,eAAeL;IAC5C,MAAMM,YAAYF,SAASG,kBAAkB;IAE7C,OAAO;QACLC,MAAM;QACNC,aACE,mEACA,mFACA,yEACA;QACFC,YAAY;YACVC,KAAKhB,EAAEiB,MAAM,GAAGD,GAAG,GAAGE,QAAQ,CAAC;YAC/BC,KAAKnB,EACFiB,MAAM,GACNG,QAAQ,GACRF,QAAQ,CAAC;QACd;QACAG,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAER,GAAG,EAAE,GAAGM;YAEhB,IAAIG;YACJ,IAAIC;YACJ,IAAIC;YACJ,IAAI;gBACF,MAAMC,SAAS,MAAM3B,oBAAoBe,KAAK;oBAAEa,UAAUnB;gBAAY;gBACtEe,SAASG,OAAOH,MAAM;gBACtBC,cAAcE,OAAOF,WAAW;gBAChCC,WAAWC,OAAOD,QAAQ;YAC5B,EAAE,OAAOG,OAAO;gBACd,OAAO1B,aAAa,CAAC,oBAAoB,EAAEF,aAAa4B,QAAQ;YAClE;YAEA,IAAI,CAACxB,oBAAoByB,GAAG,CAACL,cAAc;gBACzC,OAAOtB,aACL,CAAC,iCAAiC,EAAEsB,YAAY,GAAG,CAAC,GAClD,CAAC,eAAe,EAAE;uBAAIpB;iBAAoB,CAAC0B,IAAI,CAAC,OAAO;YAE7D;YAEA,IAAIP,OAAOQ,UAAU,GAAGvB,aAAa;gBACnC,MAAMwB,SAAS,AAACT,CAAAA,OAAOQ,UAAU,GAAI,CAAA,OAAO,IAAG,CAAC,EAAGE,OAAO,CAAC;gBAC3D,MAAMC,UAAU,AAAC1B,CAAAA,cAAe,CAAA,OAAO,IAAG,CAAC,EAAGyB,OAAO,CAAC;gBACtD,OAAO/B,aAAa,CAAC,iBAAiB,EAAE8B,OAAO,mBAAmB,EAAEE,QAAQ,GAAG,CAAC;YAClF;YAEA,MAAMjB,MACJG,KAAKH,GAAG,IACRQ,SACGU,OAAO,CAAC,YAAY,IACpBA,OAAO,CAAC,SAAS,KACjBA,OAAO,CAAC,QAAQ,KAChBC,IAAI,MACP;YAEFnC,gBAAgBoB;YAEhB,IAAI;gBACF,MAAMgB,MAAM,MAAMhB,IAAIiB,OAAO,CAACC,MAAM,CAAC;oBACnCC,YAAY/B;oBACZgC,MAAM;wBAAExB;oBAAI;oBACZyB,MAAM;wBACJD,MAAMlB;wBACNoB,UAAUnB;wBACVb,MAAMc;wBACNmB,MAAMrB,OAAOQ,UAAU;oBACzB;oBACAV;oBACAwB,gBAAgB;oBAChBC,MAAMzB,IAAIyB,IAAI;gBAChB;gBAEA,OAAO5C,aACL,CAAC,uBAAuB,EAAEuB,SAAS,KAAK,EAAEhB,UAAU,GAAG,CAAC,GACtD,CAAC,IAAI,EAAE4B,IAAIU,EAAE,CAAC,EAAE,CAAC,GACjB,CAAC,UAAU,EAAEtB,SAAS,EAAE,CAAC,GACzB,CAAC,KAAK,EAAER,KAAK;YAEnB,EAAE,OAAOW,OAAO;gBACd,OAAO1B,aAAa,CAAC,+BAA+B,EAAEF,aAAa4B,QAAQ;YAC7E;QACF;IACF;AACF"}
|