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.
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/dist/__tests__/introspection.test.js +364 -0
- package/dist/__tests__/introspection.test.js.map +1 -0
- package/dist/__tests__/url-validator.test.js +326 -0
- package/dist/__tests__/url-validator.test.js.map +1 -0
- package/dist/draft-workflow.d.ts +60 -0
- package/dist/draft-workflow.js +93 -0
- package/dist/draft-workflow.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -0
- package/dist/introspection.d.ts +23 -0
- package/dist/introspection.js +238 -0
- package/dist/introspection.js.map +1 -0
- package/dist/prompts.d.ts +21 -0
- package/dist/prompts.js +215 -0
- package/dist/prompts.js.map +1 -0
- package/dist/rate-limiter.d.ts +25 -0
- package/dist/rate-limiter.js +51 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/resources.d.ts +18 -0
- package/dist/resources.js +77 -0
- package/dist/resources.js.map +1 -0
- package/dist/tools/compose-helpers.d.ts +117 -0
- package/dist/tools/compose-helpers.js +236 -0
- package/dist/tools/compose-helpers.js.map +1 -0
- package/dist/tools/compose-layout.d.ts +139 -0
- package/dist/tools/compose-layout.js +61 -0
- package/dist/tools/compose-layout.js.map +1 -0
- package/dist/tools/patch-layout.d.ts +107 -0
- package/dist/tools/patch-layout.js +123 -0
- package/dist/tools/patch-layout.js.map +1 -0
- package/dist/tools/publish-draft.d.ts +24 -0
- package/dist/tools/publish-draft.js +69 -0
- package/dist/tools/publish-draft.js.map +1 -0
- package/dist/tools/resolve-reference.d.ts +31 -0
- package/dist/tools/resolve-reference.js +169 -0
- package/dist/tools/resolve-reference.js.map +1 -0
- package/dist/tools/safe-delete.d.ts +37 -0
- package/dist/tools/safe-delete.js +161 -0
- package/dist/tools/safe-delete.js.map +1 -0
- package/dist/tools/schedule-publish.d.ts +49 -0
- package/dist/tools/schedule-publish.js +120 -0
- package/dist/tools/schedule-publish.js.map +1 -0
- package/dist/tools/search-content.d.ts +43 -0
- package/dist/tools/search-content.js +210 -0
- package/dist/tools/search-content.js.map +1 -0
- package/dist/tools/update-document.d.ts +32 -0
- package/dist/tools/update-document.js +114 -0
- package/dist/tools/update-document.js.map +1 -0
- package/dist/tools/upload-media.d.ts +26 -0
- package/dist/tools/upload-media.js +115 -0
- package/dist/tools/upload-media.js.map +1 -0
- package/dist/tools/versions.d.ts +50 -0
- package/dist/tools/versions.js +159 -0
- package/dist/tools/versions.js.map +1 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/url-validator.d.ts +36 -0
- package/dist/url-validator.js +222 -0
- package/dist/url-validator.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { applyOperation, buildHint, composeSections, sectionSchema } from './compose-helpers';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the patchLayout MCP tool — a surgical wrapper around composePageLayout
|
|
5
|
+
* that mutates a single document's layout-style field directly, never round-tripping
|
|
6
|
+
* the entire array through updateDocument.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: prompting an LLM to "add a CTA at the bottom of the home page"
|
|
9
|
+
* via updateDocument forces it to send the entire layout array, which one bad token
|
|
10
|
+
* can wipe out. patchLayout fetches the current layout itself, applies a scoped
|
|
11
|
+
* operation, and writes back — the LLM only ever describes the delta.
|
|
12
|
+
*
|
|
13
|
+
* Defaults to layoutField "layout" but accepts any block-array field name.
|
|
14
|
+
*/ export function createPatchLayoutTool(catalog, draftCollections) {
|
|
15
|
+
const sectionSlugs = catalog.sections.map((s)=>s.slug);
|
|
16
|
+
const leafSlugs = catalog.leaves.map((l)=>l.slug);
|
|
17
|
+
return {
|
|
18
|
+
name: 'patchLayout',
|
|
19
|
+
description: 'Surgically modify a document\'s block-array field (e.g. "layout") without ' + 'sending the whole array. Pass only the sections to add or replace plus an operation ' + '(append, prepend, insertAt, replaceAt). The current layout is fetched server-side ' + 'and the operation is applied atomically. Safer than updateDocument for incremental edits. ' + `Available sections: ${sectionSlugs.join(', ')}. Available leaves: ${leafSlugs.join(', ')}.`,
|
|
20
|
+
parameters: {
|
|
21
|
+
collection: z.string().describe('The collection slug containing the document'),
|
|
22
|
+
documentId: z.string().describe('The ID of the document to patch'),
|
|
23
|
+
layoutField: z.string().optional().default('layout').describe('Name of the block-array field to patch (default "layout")'),
|
|
24
|
+
sections: z.array(sectionSchema).describe('Sections to compose. Same shape as composePageLayout.'),
|
|
25
|
+
operation: z.enum([
|
|
26
|
+
'append',
|
|
27
|
+
'prepend',
|
|
28
|
+
'insertAt',
|
|
29
|
+
'replaceAt',
|
|
30
|
+
'full'
|
|
31
|
+
]).describe('How to apply the sections: append (end), prepend (start), insertAt (at index), ' + 'replaceAt (overwrite N starting at index), full (replace entire array — use with care).'),
|
|
32
|
+
insertIndex: z.number().optional().describe('Index for insertAt/replaceAt operations')
|
|
33
|
+
},
|
|
34
|
+
handler: async (args, req, _extra)=>{
|
|
35
|
+
const { collection, documentId, layoutField = 'layout', sections, operation, insertIndex } = args;
|
|
36
|
+
// Compose the new sections first — fail fast on validation errors before touching the doc
|
|
37
|
+
const { blocks, errors } = composeSections(sections, catalog);
|
|
38
|
+
if (errors.length > 0) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: JSON.stringify({
|
|
44
|
+
success: false,
|
|
45
|
+
errors,
|
|
46
|
+
hint: buildHint(catalog)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
req.context = {
|
|
53
|
+
...req.context,
|
|
54
|
+
source: 'mcp'
|
|
55
|
+
};
|
|
56
|
+
// Fetch current document so we can read the existing layout array
|
|
57
|
+
let existing;
|
|
58
|
+
try {
|
|
59
|
+
existing = await req.payload.findByID({
|
|
60
|
+
collection: collection,
|
|
61
|
+
id: documentId,
|
|
62
|
+
draft: true,
|
|
63
|
+
req,
|
|
64
|
+
overrideAccess: false,
|
|
65
|
+
user: req.user
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: 'text',
|
|
73
|
+
text: `Error fetching ${collection}#${documentId}: ${message}`
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const currentLayout = Array.isArray(existing?.[layoutField]) ? existing[layoutField] : [];
|
|
79
|
+
const finalLayout = applyOperation(blocks, operation, insertIndex, currentLayout);
|
|
80
|
+
const isDraftCollection = draftCollections.has(collection);
|
|
81
|
+
try {
|
|
82
|
+
const updated = await req.payload.update({
|
|
83
|
+
collection: collection,
|
|
84
|
+
id: documentId,
|
|
85
|
+
data: {
|
|
86
|
+
[layoutField]: finalLayout
|
|
87
|
+
},
|
|
88
|
+
draft: isDraftCollection,
|
|
89
|
+
req,
|
|
90
|
+
overrideAccess: false,
|
|
91
|
+
user: req.user
|
|
92
|
+
});
|
|
93
|
+
const displayName = updated.name || updated.title || updated.slug || documentId;
|
|
94
|
+
const draftNote = isDraftCollection ? ' Document is in draft status — use publishDraft to make it live.' : '';
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: 'text',
|
|
99
|
+
text: JSON.stringify({
|
|
100
|
+
success: true,
|
|
101
|
+
message: `Patched ${layoutField} on "${displayName}" (${collection}#${documentId}). ` + `Operation: ${operation}. Block count: ${finalLayout.length}.` + draftNote,
|
|
102
|
+
blockCount: finalLayout.length,
|
|
103
|
+
operation
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: `Error patching ${collection}#${documentId}: ${message}`
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=patch-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tools/patch-layout.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { BlockCatalog } from '../types'\nimport {\n applyOperation,\n buildHint,\n composeSections,\n sectionSchema,\n type SectionInput,\n} from './compose-helpers'\n\n/**\n * Creates the patchLayout MCP tool — a surgical wrapper around composePageLayout\n * that mutates a single document's layout-style field directly, never round-tripping\n * the entire array through updateDocument.\n *\n * Why this exists: prompting an LLM to \"add a CTA at the bottom of the home page\"\n * via updateDocument forces it to send the entire layout array, which one bad token\n * can wipe out. patchLayout fetches the current layout itself, applies a scoped\n * operation, and writes back — the LLM only ever describes the delta.\n *\n * Defaults to layoutField \"layout\" but accepts any block-array field name.\n */\nexport function createPatchLayoutTool(\n catalog: BlockCatalog,\n draftCollections: Set<string>,\n) {\n const sectionSlugs = catalog.sections.map((s) => s.slug)\n const leafSlugs = catalog.leaves.map((l) => l.slug)\n\n return {\n name: 'patchLayout',\n description:\n 'Surgically modify a document\\'s block-array field (e.g. \"layout\") without ' +\n 'sending the whole array. Pass only the sections to add or replace plus an operation ' +\n '(append, prepend, insertAt, replaceAt). The current layout is fetched server-side ' +\n 'and the operation is applied atomically. Safer than updateDocument for incremental edits. ' +\n `Available sections: ${sectionSlugs.join(', ')}. Available leaves: ${leafSlugs.join(', ')}.`,\n parameters: {\n collection: z.string().describe('The collection slug containing the document'),\n documentId: z.string().describe('The ID of the document to patch'),\n layoutField: z\n .string()\n .optional()\n .default('layout')\n .describe('Name of the block-array field to patch (default \"layout\")'),\n sections: z\n .array(sectionSchema)\n .describe('Sections to compose. Same shape as composePageLayout.'),\n operation: z\n .enum(['append', 'prepend', 'insertAt', 'replaceAt', 'full'])\n .describe(\n 'How to apply the sections: append (end), prepend (start), insertAt (at index), ' +\n 'replaceAt (overwrite N starting at index), full (replace entire array — use with care).',\n ),\n insertIndex: z\n .number()\n .optional()\n .describe('Index for insertAt/replaceAt operations'),\n },\n handler: async (\n args: {\n collection: string\n documentId: string\n layoutField?: string\n sections: SectionInput[]\n operation: 'append' | 'prepend' | 'insertAt' | 'replaceAt' | 'full'\n insertIndex?: number\n },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const {\n collection,\n documentId,\n layoutField = 'layout',\n sections,\n operation,\n insertIndex,\n } = args\n\n // Compose the new sections first — fail fast on validation errors before touching the doc\n const { blocks, errors } = composeSections(sections, catalog)\n\n if (errors.length > 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n success: false,\n errors,\n hint: buildHint(catalog),\n }),\n },\n ],\n }\n }\n\n req.context = { ...req.context, source: 'mcp' }\n\n // Fetch current document so we can read the existing layout array\n let existing: any\n try {\n existing = await req.payload.findByID({\n collection: collection as any,\n id: documentId,\n draft: true,\n req,\n overrideAccess: false,\n user: req.user,\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 fetching ${collection}#${documentId}: ${message}`,\n },\n ],\n }\n }\n\n const currentLayout = Array.isArray(existing?.[layoutField]) ? existing[layoutField] : []\n const finalLayout = applyOperation(blocks, operation, insertIndex, currentLayout)\n\n const isDraftCollection = draftCollections.has(collection)\n\n try {\n const updated = await req.payload.update({\n collection: collection as any,\n id: documentId,\n data: { [layoutField]: finalLayout } as any,\n draft: isDraftCollection,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const displayName =\n (updated as any).name ||\n (updated as any).title ||\n (updated as any).slug ||\n documentId\n\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: JSON.stringify({\n success: true,\n message:\n `Patched ${layoutField} on \"${displayName}\" (${collection}#${documentId}). ` +\n `Operation: ${operation}. Block count: ${finalLayout.length}.` +\n draftNote,\n blockCount: finalLayout.length,\n operation,\n }),\n },\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 patching ${collection}#${documentId}: ${message}`,\n },\n ],\n }\n }\n },\n }\n}\n"],"names":["z","applyOperation","buildHint","composeSections","sectionSchema","createPatchLayoutTool","catalog","draftCollections","sectionSlugs","sections","map","s","slug","leafSlugs","leaves","l","name","description","join","parameters","collection","string","describe","documentId","layoutField","optional","default","array","operation","enum","insertIndex","number","handler","args","req","_extra","blocks","errors","length","content","type","text","JSON","stringify","success","hint","context","source","existing","payload","findByID","id","draft","overrideAccess","user","error","message","Error","String","currentLayout","Array","isArray","finalLayout","isDraftCollection","has","updated","update","data","displayName","title","draftNote","blockCount"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PayloadRequest } from 'payload';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the publishDraft MCP tool that transitions a document from draft to published status.
|
|
5
|
+
*
|
|
6
|
+
* @param draftCollections - Set of collection slugs that support drafts
|
|
7
|
+
*/
|
|
8
|
+
export declare function createPublishDraftTool(draftCollections: Set<string>): {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
parameters: {
|
|
12
|
+
collection: z.ZodString;
|
|
13
|
+
documentId: z.ZodString;
|
|
14
|
+
};
|
|
15
|
+
handler: (args: {
|
|
16
|
+
collection: string;
|
|
17
|
+
documentId: string;
|
|
18
|
+
}, req: PayloadRequest, _extra: unknown) => Promise<{
|
|
19
|
+
content: {
|
|
20
|
+
type: "text";
|
|
21
|
+
text: string;
|
|
22
|
+
}[];
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Creates the publishDraft MCP tool that transitions a document from draft to published status.
|
|
4
|
+
*
|
|
5
|
+
* @param draftCollections - Set of collection slugs that support drafts
|
|
6
|
+
*/ export function createPublishDraftTool(draftCollections) {
|
|
7
|
+
return {
|
|
8
|
+
name: 'publishDraft',
|
|
9
|
+
description: 'Publish a draft document by transitioning its _status from "draft" to "published". ' + 'Only works on collections that support drafts. Use after creating or editing content ' + 'to make it live on the site.',
|
|
10
|
+
parameters: {
|
|
11
|
+
collection: z.string().describe(`The collection slug. Draft-enabled collections: ${[
|
|
12
|
+
...draftCollections
|
|
13
|
+
].join(', ') || 'none'}`),
|
|
14
|
+
documentId: z.string().describe('The ID of the document to publish')
|
|
15
|
+
},
|
|
16
|
+
handler: async (args, req, _extra)=>{
|
|
17
|
+
const { collection, documentId } = args;
|
|
18
|
+
if (!draftCollections.has(collection)) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: `Error: Collection "${collection}" does not support drafts. ` + `Draft-enabled collections: ${[
|
|
24
|
+
...draftCollections
|
|
25
|
+
].join(', ') || 'none'}`
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
req.context = {
|
|
31
|
+
...req.context,
|
|
32
|
+
source: 'mcp'
|
|
33
|
+
};
|
|
34
|
+
try {
|
|
35
|
+
const doc = await req.payload.update({
|
|
36
|
+
collection: collection,
|
|
37
|
+
id: documentId,
|
|
38
|
+
data: {
|
|
39
|
+
_status: 'published'
|
|
40
|
+
},
|
|
41
|
+
req,
|
|
42
|
+
overrideAccess: false,
|
|
43
|
+
user: req.user
|
|
44
|
+
});
|
|
45
|
+
const displayName = doc.name || doc.title || doc.slug || documentId;
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: `Successfully published "${displayName}" in ${collection} (ID: ${documentId}).`
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: `Error publishing document ${documentId} in ${collection}: ${message}`
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=publish-draft.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tools/publish-draft.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\n\n/**\n * Creates the publishDraft MCP tool that transitions a document from draft to published status.\n *\n * @param draftCollections - Set of collection slugs that support drafts\n */\nexport function createPublishDraftTool(draftCollections: Set<string>) {\n return {\n name: 'publishDraft',\n description:\n 'Publish a draft document by transitioning its _status from \"draft\" to \"published\". ' +\n 'Only works on collections that support drafts. Use after creating or editing content ' +\n 'to make it live on the site.',\n parameters: {\n collection: z\n .string()\n .describe(\n `The collection slug. Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\n ),\n documentId: z.string().describe('The ID of the document to publish'),\n },\n handler: async (\n args: { collection: string; documentId: string },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const { collection, documentId } = args\n\n if (!draftCollections.has(collection)) {\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error: Collection \"${collection}\" does not support drafts. ` +\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\n },\n ],\n }\n }\n\n req.context = { ...req.context, source: 'mcp' }\n\n try {\n const doc = await req.payload.update({\n collection: collection as any,\n id: documentId,\n data: { _status: 'published' } as any,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const displayName =\n (doc as any).name ||\n (doc as any).title ||\n (doc as any).slug ||\n documentId\n\n return {\n content: [\n {\n type: 'text' as const,\n text: `Successfully published \"${displayName}\" in ${collection} (ID: ${documentId}).`,\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 publishing document ${documentId} in ${collection}: ${message}`,\n },\n ],\n }\n }\n },\n }\n}\n"],"names":["z","createPublishDraftTool","draftCollections","name","description","parameters","collection","string","describe","join","documentId","handler","args","req","_extra","has","content","type","text","context","source","doc","payload","update","id","data","_status","overrideAccess","user","displayName","title","slug","error","message","Error","String"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAGvB;;;;CAIC,GACD,OAAO,SAASC,uBAAuBC,gBAA6B;IAClE,OAAO;QACLC,MAAM;QACNC,aACE,wFACA,0FACA;QACFC,YAAY;YACVC,YAAYN,EACTO,MAAM,GACNC,QAAQ,CACP,CAAC,gDAAgD,EAAE;mBAAIN;aAAiB,CAACO,IAAI,CAAC,SAAS,QAAQ;YAEnGC,YAAYV,EAAEO,MAAM,GAAGC,QAAQ,CAAC;QAClC;QACAG,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAER,UAAU,EAAEI,UAAU,EAAE,GAAGE;YAEnC,IAAI,CAACV,iBAAiBa,GAAG,CAACT,aAAa;gBACrC,OAAO;oBACLU,SAAS;wBACP;4BACEC,MAAM;4BACNC,MAAM,CAAC,mBAAmB,EAAEZ,WAAW,2BAA2B,CAAC,GACjE,CAAC,2BAA2B,EAAE;mCAAIJ;6BAAiB,CAACO,IAAI,CAAC,SAAS,QAAQ;wBAC9E;qBACD;gBACH;YACF;YAEAI,IAAIM,OAAO,GAAG;gBAAE,GAAGN,IAAIM,OAAO;gBAAEC,QAAQ;YAAM;YAE9C,IAAI;gBACF,MAAMC,MAAM,MAAMR,IAAIS,OAAO,CAACC,MAAM,CAAC;oBACnCjB,YAAYA;oBACZkB,IAAId;oBACJe,MAAM;wBAAEC,SAAS;oBAAY;oBAC7Bb;oBACAc,gBAAgB;oBAChBC,MAAMf,IAAIe,IAAI;gBAChB;gBAEA,MAAMC,cACJ,AAACR,IAAYlB,IAAI,IACjB,AAACkB,IAAYS,KAAK,IAClB,AAACT,IAAYU,IAAI,IACjBrB;gBAEF,OAAO;oBACLM,SAAS;wBACP;4BACEC,MAAM;4BACNC,MAAM,CAAC,wBAAwB,EAAEW,YAAY,KAAK,EAAEvB,WAAW,MAAM,EAAEI,WAAW,EAAE,CAAC;wBACvF;qBACD;gBACH;YACF,EAAE,OAAOsB,OAAO;gBACd,MAAMC,UAAUD,iBAAiBE,QAAQF,MAAMC,OAAO,GAAGE,OAAOH;gBAChE,OAAO;oBACLhB,SAAS;wBACP;4BACEC,MAAM;4BACNC,MAAM,CAAC,0BAA0B,EAAER,WAAW,IAAI,EAAEJ,WAAW,EAAE,EAAE2B,SAAS;wBAC9E;qBACD;gBACH;YACF;QACF;IACF;AACF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PayloadRequest } from 'payload';
|
|
3
|
+
/**
|
|
4
|
+
* Creates the resolveReference MCP tool that searches collections by
|
|
5
|
+
* natural language terms and returns ranked document ID candidates.
|
|
6
|
+
*
|
|
7
|
+
* @param searchableCollections - Map of collection slug → searchable field names
|
|
8
|
+
*/
|
|
9
|
+
export declare function createResolveReferenceTool(searchableCollections: Map<string, string[]>): {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
parameters: z.ZodObject<{
|
|
13
|
+
query: z.ZodString;
|
|
14
|
+
collection: z.ZodOptional<z.ZodString>;
|
|
15
|
+
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
query: string;
|
|
17
|
+
collection?: string | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
query: string;
|
|
20
|
+
collection?: string | undefined;
|
|
21
|
+
}>;
|
|
22
|
+
handler: (args: {
|
|
23
|
+
query: string;
|
|
24
|
+
collection?: string;
|
|
25
|
+
}, req: PayloadRequest) => Promise<{
|
|
26
|
+
content: {
|
|
27
|
+
type: "text";
|
|
28
|
+
text: string;
|
|
29
|
+
}[];
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Creates the resolveReference MCP tool that searches collections by
|
|
4
|
+
* natural language terms and returns ranked document ID candidates.
|
|
5
|
+
*
|
|
6
|
+
* @param searchableCollections - Map of collection slug → searchable field names
|
|
7
|
+
*/ export function createResolveReferenceTool(searchableCollections) {
|
|
8
|
+
return {
|
|
9
|
+
name: 'resolveReference',
|
|
10
|
+
description: 'Search for documents across collections by name, title, or slug. ' + 'Returns ranked candidates with IDs for use in relationship fields. ' + 'Optionally filter to a specific collection.',
|
|
11
|
+
parameters: z.object({
|
|
12
|
+
query: z.string().describe('Search term to match against name, title, or slug fields'),
|
|
13
|
+
collection: z.string().optional().describe('Optional collection slug to restrict search to a single collection')
|
|
14
|
+
}),
|
|
15
|
+
handler: async (args, req)=>{
|
|
16
|
+
req.context = {
|
|
17
|
+
...req.context,
|
|
18
|
+
source: 'mcp'
|
|
19
|
+
};
|
|
20
|
+
const { query, collection } = args;
|
|
21
|
+
const collectionsToSearch = collection ? new Map(searchableCollections.has(collection) ? [
|
|
22
|
+
[
|
|
23
|
+
collection,
|
|
24
|
+
searchableCollections.get(collection)
|
|
25
|
+
]
|
|
26
|
+
] : []) : searchableCollections;
|
|
27
|
+
if (collectionsToSearch.size === 0) {
|
|
28
|
+
const available = Array.from(searchableCollections.keys()).join(', ');
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: JSON.stringify({
|
|
34
|
+
candidates: [],
|
|
35
|
+
message: collection ? `Collection "${collection}" has no searchable fields or does not exist. Available searchable collections: ${available}` : 'No searchable collections found.'
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const allCandidates = [];
|
|
42
|
+
for (const [slug, fields] of collectionsToSearch){
|
|
43
|
+
const orConditions = fields.map((field)=>({
|
|
44
|
+
[field]: {
|
|
45
|
+
like: query
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
if (orConditions.length === 0) continue;
|
|
49
|
+
const selectFields = {};
|
|
50
|
+
for (const field of fields){
|
|
51
|
+
selectFields[field] = true;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const result = await req.payload.find({
|
|
55
|
+
collection: slug,
|
|
56
|
+
where: {
|
|
57
|
+
or: orConditions
|
|
58
|
+
},
|
|
59
|
+
limit: 5,
|
|
60
|
+
select: selectFields,
|
|
61
|
+
req,
|
|
62
|
+
overrideAccess: false,
|
|
63
|
+
user: req.user
|
|
64
|
+
});
|
|
65
|
+
for (const doc of result.docs){
|
|
66
|
+
const candidates = rankDocument(doc, slug, fields, query);
|
|
67
|
+
allCandidates.push(...candidates);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Sort: exact-slug > exact > partial
|
|
74
|
+
allCandidates.sort((a, b)=>{
|
|
75
|
+
const order = {
|
|
76
|
+
'exact-slug': 0,
|
|
77
|
+
exact: 1,
|
|
78
|
+
partial: 2
|
|
79
|
+
};
|
|
80
|
+
return order[a.matchType] - order[b.matchType];
|
|
81
|
+
});
|
|
82
|
+
const grouped = {};
|
|
83
|
+
for (const candidate of allCandidates){
|
|
84
|
+
const { collection: col, ...rest } = candidate;
|
|
85
|
+
if (!grouped[col]) grouped[col] = [];
|
|
86
|
+
grouped[col].push(rest);
|
|
87
|
+
}
|
|
88
|
+
if (allCandidates.length === 0) {
|
|
89
|
+
const searched = Array.from(collectionsToSearch.keys()).join(', ');
|
|
90
|
+
const available = Array.from(searchableCollections.keys()).join(', ');
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: 'text',
|
|
95
|
+
text: JSON.stringify({
|
|
96
|
+
candidates: {},
|
|
97
|
+
message: `No results found for "${query}" in: ${searched}. Try a different spelling or search term. All searchable collections: ${available}`
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: JSON.stringify({
|
|
108
|
+
candidates: grouped,
|
|
109
|
+
total: allCandidates.length
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function rankDocument(doc, collection, fields, query) {
|
|
118
|
+
const queryLower = query.toLowerCase();
|
|
119
|
+
let bestMatch = null;
|
|
120
|
+
for (const field of fields){
|
|
121
|
+
const value = doc[field];
|
|
122
|
+
if (typeof value !== 'string') continue;
|
|
123
|
+
const valueLower = value.toLowerCase();
|
|
124
|
+
let matchType;
|
|
125
|
+
if (field === 'slug' && valueLower === queryLower) {
|
|
126
|
+
matchType = 'exact-slug';
|
|
127
|
+
} else if (valueLower === queryLower) {
|
|
128
|
+
matchType = 'exact';
|
|
129
|
+
} else {
|
|
130
|
+
matchType = 'partial';
|
|
131
|
+
}
|
|
132
|
+
if (!bestMatch || matchTypePriority(matchType) < matchTypePriority(bestMatch.matchType)) {
|
|
133
|
+
bestMatch = {
|
|
134
|
+
collection,
|
|
135
|
+
id: doc.id,
|
|
136
|
+
displayName: getDisplayName(doc, fields),
|
|
137
|
+
matchedField: field,
|
|
138
|
+
matchType
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return bestMatch ? [
|
|
143
|
+
bestMatch
|
|
144
|
+
] : [];
|
|
145
|
+
}
|
|
146
|
+
function matchTypePriority(type) {
|
|
147
|
+
switch(type){
|
|
148
|
+
case 'exact-slug':
|
|
149
|
+
return 0;
|
|
150
|
+
case 'exact':
|
|
151
|
+
return 1;
|
|
152
|
+
case 'partial':
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function getDisplayName(doc, fields) {
|
|
157
|
+
for (const preferred of [
|
|
158
|
+
'name',
|
|
159
|
+
'title',
|
|
160
|
+
'slug'
|
|
161
|
+
]){
|
|
162
|
+
if (fields.includes(preferred) && typeof doc[preferred] === 'string') {
|
|
163
|
+
return doc[preferred];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return String(doc.id);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
//# sourceMappingURL=resolve-reference.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tools/resolve-reference.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\n\ninterface MatchCandidate {\n collection: string\n id: string | number\n displayName: string\n matchedField: string\n matchType: 'exact-slug' | 'exact' | 'partial'\n}\n\n/**\n * Creates the resolveReference MCP tool that searches collections by\n * natural language terms and returns ranked document ID candidates.\n *\n * @param searchableCollections - Map of collection slug → searchable field names\n */\nexport function createResolveReferenceTool(\n searchableCollections: Map<string, string[]>,\n) {\n return {\n name: 'resolveReference',\n description:\n 'Search for documents across collections by name, title, or slug. ' +\n 'Returns ranked candidates with IDs for use in relationship fields. ' +\n 'Optionally filter to a specific collection.',\n parameters: z.object({\n query: z.string().describe('Search term to match against name, title, or slug fields'),\n collection: z\n .string()\n .optional()\n .describe('Optional collection slug to restrict search to a single collection'),\n }),\n handler: async (args: { query: string; collection?: string }, req: PayloadRequest) => {\n req.context = { ...req.context, source: 'mcp' }\n\n const { query, collection } = args\n\n const collectionsToSearch = collection\n ? new Map(\n searchableCollections.has(collection)\n ? [[collection, searchableCollections.get(collection)!]]\n : [],\n )\n : searchableCollections\n\n if (collectionsToSearch.size === 0) {\n const available = Array.from(searchableCollections.keys()).join(', ')\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n candidates: [],\n message: collection\n ? `Collection \"${collection}\" has no searchable fields or does not exist. Available searchable collections: ${available}`\n : 'No searchable collections found.',\n }),\n },\n ],\n }\n }\n\n const allCandidates: MatchCandidate[] = []\n\n for (const [slug, fields] of collectionsToSearch) {\n const orConditions = fields.map((field) => ({\n [field]: { like: query },\n }))\n\n if (orConditions.length === 0) continue\n\n const selectFields: Record<string, true> = {}\n for (const field of fields) {\n selectFields[field] = true\n }\n\n try {\n const result = await req.payload.find({\n collection: slug as any,\n where: { or: orConditions },\n limit: 5,\n select: selectFields,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n for (const doc of result.docs) {\n const candidates = rankDocument(doc, slug, fields, query)\n allCandidates.push(...candidates)\n }\n } catch {\n // Skip collections that fail (e.g. permission errors)\n continue\n }\n }\n\n // Sort: exact-slug > exact > partial\n allCandidates.sort((a, b) => {\n const order: Record<string, number> = {\n 'exact-slug': 0,\n exact: 1,\n partial: 2,\n }\n return order[a.matchType] - order[b.matchType]\n })\n\n const grouped: Record<string, Omit<MatchCandidate, 'collection'>[]> = {}\n for (const candidate of allCandidates) {\n const { collection: col, ...rest } = candidate\n if (!grouped[col]) grouped[col] = []\n grouped[col].push(rest)\n }\n\n if (allCandidates.length === 0) {\n const searched = Array.from(collectionsToSearch.keys()).join(', ')\n const available = Array.from(searchableCollections.keys()).join(', ')\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n candidates: {},\n message: `No results found for \"${query}\" in: ${searched}. Try a different spelling or search term. All searchable collections: ${available}`,\n }),\n },\n ],\n }\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n candidates: grouped,\n total: allCandidates.length,\n }),\n },\n ],\n }\n },\n }\n}\n\nfunction rankDocument(\n doc: Record<string, any>,\n collection: string,\n fields: string[],\n query: string,\n): MatchCandidate[] {\n const queryLower = query.toLowerCase()\n let bestMatch: MatchCandidate | null = null\n\n for (const field of fields) {\n const value = doc[field]\n if (typeof value !== 'string') continue\n\n const valueLower = value.toLowerCase()\n let matchType: MatchCandidate['matchType']\n\n if (field === 'slug' && valueLower === queryLower) {\n matchType = 'exact-slug'\n } else if (valueLower === queryLower) {\n matchType = 'exact'\n } else {\n matchType = 'partial'\n }\n\n if (\n !bestMatch ||\n matchTypePriority(matchType) < matchTypePriority(bestMatch.matchType)\n ) {\n bestMatch = {\n collection,\n id: doc.id,\n displayName: getDisplayName(doc, fields),\n matchedField: field,\n matchType,\n }\n }\n }\n\n return bestMatch ? [bestMatch] : []\n}\n\nfunction matchTypePriority(type: MatchCandidate['matchType']): number {\n switch (type) {\n case 'exact-slug':\n return 0\n case 'exact':\n return 1\n case 'partial':\n return 2\n }\n}\n\nfunction getDisplayName(doc: Record<string, any>, fields: string[]): string {\n for (const preferred of ['name', 'title', 'slug']) {\n if (fields.includes(preferred) && typeof doc[preferred] === 'string') {\n return doc[preferred]\n }\n }\n return String(doc.id)\n}\n"],"names":["z","createResolveReferenceTool","searchableCollections","name","description","parameters","object","query","string","describe","collection","optional","handler","args","req","context","source","collectionsToSearch","Map","has","get","size","available","Array","from","keys","join","content","type","text","JSON","stringify","candidates","message","allCandidates","slug","fields","orConditions","map","field","like","length","selectFields","result","payload","find","where","or","limit","select","overrideAccess","user","doc","docs","rankDocument","push","sort","a","b","order","exact","partial","matchType","grouped","candidate","col","rest","searched","total","queryLower","toLowerCase","bestMatch","value","valueLower","matchTypePriority","id","displayName","getDisplayName","matchedField","preferred","includes","String"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PayloadRequest } from 'payload';
|
|
3
|
+
import type { RelationshipEdge } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* Creates the safeDelete MCP tool that wraps the official delete operation
|
|
6
|
+
* with a relationship-graph pre-check.
|
|
7
|
+
*
|
|
8
|
+
* Workflow:
|
|
9
|
+
* 1. Use the introspected relationshipGraph to find every (collection, field)
|
|
10
|
+
* pair that points TO the target collection.
|
|
11
|
+
* 2. For each, query for documents that reference the target ID. Aggregate counts
|
|
12
|
+
* and a small sample of inbound document IDs.
|
|
13
|
+
* 3. If any inbound references exist and `confirm` is false, refuse and return
|
|
14
|
+
* the impact summary so the caller (or the LLM driving it) can decide.
|
|
15
|
+
* 4. If `confirm` is true OR there are no inbound references, perform the delete.
|
|
16
|
+
*
|
|
17
|
+
* Excludes built-in Payload bookkeeping collections from the inbound search.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createSafeDeleteTool(relationships: RelationshipEdge[]): {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
parameters: {
|
|
23
|
+
collection: z.ZodString;
|
|
24
|
+
documentId: z.ZodString;
|
|
25
|
+
confirm: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
26
|
+
};
|
|
27
|
+
handler: (args: {
|
|
28
|
+
collection: string;
|
|
29
|
+
documentId: string;
|
|
30
|
+
confirm?: boolean;
|
|
31
|
+
}, req: PayloadRequest, _extra: unknown) => Promise<{
|
|
32
|
+
content: {
|
|
33
|
+
type: "text";
|
|
34
|
+
text: string;
|
|
35
|
+
}[];
|
|
36
|
+
}>;
|
|
37
|
+
};
|