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
|
@@ -2,9 +2,8 @@ import { z } from 'zod';
|
|
|
2
2
|
import type { PayloadRequest } from 'payload';
|
|
3
3
|
import type { BlockCatalog, BlockNestingMap } from '../types';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* array through `updateDocument`.
|
|
5
|
+
* patchLayout — surgical wrapper that mutates a single document's blocks-typed
|
|
6
|
+
* field directly without round-tripping the entire array through updateDocument.
|
|
8
7
|
*
|
|
9
8
|
* Why this exists: prompting an LLM to "add a CTA at the bottom of the home
|
|
10
9
|
* page" via updateDocument forces it to send the whole layout array, which one
|
|
@@ -34,10 +33,5 @@ export declare function createPatchLayoutTool(catalog: BlockCatalog, nesting: Bl
|
|
|
34
33
|
blocks: Array<Record<string, unknown>>;
|
|
35
34
|
operation: "append" | "prepend" | "insertAt" | "replaceAt" | "full";
|
|
36
35
|
insertIndex?: number;
|
|
37
|
-
}, req: PayloadRequest, _extra: unknown) => Promise<
|
|
38
|
-
content: {
|
|
39
|
-
type: "text";
|
|
40
|
-
text: string;
|
|
41
|
-
}[];
|
|
42
|
-
}>;
|
|
36
|
+
}, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
|
|
43
37
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { DRAFT_NOTE, errorMessage, getDocDisplayName, jsonResponse, stampMcpContext } from './_helpers';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* array through `updateDocument`.
|
|
4
|
+
* patchLayout — surgical wrapper that mutates a single document's blocks-typed
|
|
5
|
+
* field directly without round-tripping the entire array through updateDocument.
|
|
6
6
|
*
|
|
7
7
|
* Why this exists: prompting an LLM to "add a CTA at the bottom of the home
|
|
8
8
|
* page" via updateDocument forces it to send the whole layout array, which one
|
|
@@ -14,9 +14,8 @@ import { z } from 'zod';
|
|
|
14
14
|
* BlockNestingMap, so arbitrarily-nested layouts work as long as each
|
|
15
15
|
* `blocks`-typed field's content matches that field's allow list.
|
|
16
16
|
*/ export function createPatchLayoutTool(catalog, nesting, draftCollections) {
|
|
17
|
-
const allBlockSlugs = catalog.blocks.map((b)=>b.slug);
|
|
18
|
-
//
|
|
19
|
-
// recursive validation.
|
|
17
|
+
const allBlockSlugs = new Set(catalog.blocks.map((b)=>b.slug));
|
|
18
|
+
// Lookups keyed by `<owner>:<fieldPath>` for O(1) access during recursive validation.
|
|
20
19
|
const nestingByCollectionField = new Map();
|
|
21
20
|
const nestingByBlockField = new Map();
|
|
22
21
|
for (const edge of nesting){
|
|
@@ -46,8 +45,6 @@ import { z } from 'zod';
|
|
|
46
45
|
},
|
|
47
46
|
handler: async (args, req, _extra)=>{
|
|
48
47
|
const { collection, documentId, layoutField = 'layout', blocks, operation, insertIndex } = args;
|
|
49
|
-
// Validate the incoming blocks against the nesting map for the target
|
|
50
|
-
// field before touching the database.
|
|
51
48
|
const rootKey = `${collection}:${layoutField}`;
|
|
52
49
|
const rootAllowed = nestingByCollectionField.get(rootKey);
|
|
53
50
|
if (!rootAllowed) {
|
|
@@ -64,23 +61,20 @@ import { z } from 'zod';
|
|
|
64
61
|
errors
|
|
65
62
|
});
|
|
66
63
|
}
|
|
67
|
-
req
|
|
68
|
-
...req.context,
|
|
69
|
-
source: 'mcp'
|
|
70
|
-
};
|
|
64
|
+
stampMcpContext(req);
|
|
71
65
|
let existing;
|
|
72
66
|
try {
|
|
73
67
|
existing = await req.payload.findByID({
|
|
74
68
|
collection: collection,
|
|
75
69
|
id: documentId,
|
|
70
|
+
depth: 0,
|
|
76
71
|
draft: true,
|
|
77
72
|
req,
|
|
78
73
|
overrideAccess: false,
|
|
79
74
|
user: req.user
|
|
80
75
|
});
|
|
81
76
|
} catch (error) {
|
|
82
|
-
|
|
83
|
-
return errorResponse(`Error fetching ${collection}#${documentId}: ${message}`);
|
|
77
|
+
return errorResponse(`Error fetching ${collection}#${documentId}: ${errorMessage(error)}`);
|
|
84
78
|
}
|
|
85
79
|
const currentLayout = Array.isArray(existing?.[layoutField]) ? existing[layoutField] : [];
|
|
86
80
|
const finalLayout = applyOperation(blocks, operation, insertIndex, currentLayout);
|
|
@@ -97,41 +91,26 @@ import { z } from 'zod';
|
|
|
97
91
|
overrideAccess: false,
|
|
98
92
|
user: req.user
|
|
99
93
|
});
|
|
100
|
-
const displayName = updated
|
|
101
|
-
const draftNote = isDraftCollection ?
|
|
102
|
-
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
message: `Patched ${layoutField} on "${displayName}" (${collection}#${documentId}). ` + `Operation: ${operation}. Block count: ${finalLayout.length}.` + draftNote,
|
|
109
|
-
blockCount: finalLayout.length,
|
|
110
|
-
operation
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
]
|
|
114
|
-
};
|
|
94
|
+
const displayName = getDocDisplayName(updated, documentId);
|
|
95
|
+
const draftNote = isDraftCollection ? DRAFT_NOTE : '';
|
|
96
|
+
return jsonResponse({
|
|
97
|
+
success: true,
|
|
98
|
+
message: `Patched ${layoutField} on "${displayName}" (${collection}#${documentId}). ` + `Operation: ${operation}. Block count: ${finalLayout.length}.` + draftNote,
|
|
99
|
+
blockCount: finalLayout.length,
|
|
100
|
+
operation
|
|
101
|
+
});
|
|
115
102
|
} catch (error) {
|
|
116
|
-
|
|
117
|
-
return errorResponse(`Error patching ${collection}#${documentId}: ${message}`);
|
|
103
|
+
return errorResponse(`Error patching ${collection}#${documentId}: ${errorMessage(error)}`);
|
|
118
104
|
}
|
|
119
105
|
}
|
|
120
106
|
};
|
|
121
107
|
}
|
|
122
108
|
function errorResponse(message, extra) {
|
|
123
|
-
return {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
success: false,
|
|
129
|
-
error: message,
|
|
130
|
-
...extra ?? {}
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
]
|
|
134
|
-
};
|
|
109
|
+
return jsonResponse({
|
|
110
|
+
success: false,
|
|
111
|
+
error: message,
|
|
112
|
+
...extra ?? {}
|
|
113
|
+
});
|
|
135
114
|
}
|
|
136
115
|
/**
|
|
137
116
|
* Recursively validate a block array against an allow list, descending into
|
|
@@ -149,17 +128,19 @@ function errorResponse(message, extra) {
|
|
|
149
128
|
errors.push(`${here}: missing string \`blockType\``);
|
|
150
129
|
continue;
|
|
151
130
|
}
|
|
152
|
-
if (!allBlockSlugs.
|
|
153
|
-
errors.push(`${here}: unknown blockType "${slug}". Known: ${
|
|
131
|
+
if (!allBlockSlugs.has(slug)) {
|
|
132
|
+
errors.push(`${here}: unknown blockType "${slug}". Known: ${[
|
|
133
|
+
...allBlockSlugs
|
|
134
|
+
].join(', ')}`);
|
|
154
135
|
continue;
|
|
155
136
|
}
|
|
156
137
|
if (!allowedSlugs.includes(slug)) {
|
|
157
138
|
errors.push(`${here}: blockType "${slug}" not allowed here. Allowed at this position: ${allowedSlugs.join(', ') || '(none)'}`);
|
|
158
139
|
continue;
|
|
159
140
|
}
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
141
|
+
// Any value that is itself an array of objects with `blockType` is treated
|
|
142
|
+
// as a nested blocks field. The field name is cross-checked against the
|
|
143
|
+
// nesting map to find the next-level allow list.
|
|
163
144
|
for (const [fieldName, value] of Object.entries(block)){
|
|
164
145
|
if (!Array.isArray(value)) continue;
|
|
165
146
|
if (value.length === 0) continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tools/patch-layout.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { BlockCatalog, BlockNestingMap } from '../types'\n\n/**\n * Creates the patchLayout MCP tool — a surgical wrapper that mutates a single\n * document's blocks-typed field directly without round-tripping the entire\n * array through `updateDocument`.\n *\n * Why this exists: prompting an LLM to \"add a CTA at the bottom of the home\n * page\" via updateDocument forces it to send the whole layout array, which one\n * bad token can wipe out. patchLayout fetches the current array itself,\n * applies a scoped operation, and writes back — the LLM only describes the\n * delta.\n *\n * Validation walks every block recursively against the introspected\n * BlockNestingMap, so arbitrarily-nested layouts work as long as each\n * `blocks`-typed field's content matches that field's allow list.\n */\nexport function createPatchLayoutTool(\n catalog: BlockCatalog,\n nesting: BlockNestingMap,\n draftCollections: Set<string>,\n) {\n const allBlockSlugs = catalog.blocks.map((b) => b.slug)\n\n // Pre-build lookups keyed by `<owner>:<fieldPath>` for O(1) access during\n // recursive validation.\n const nestingByCollectionField = new Map<string, string[]>()\n const nestingByBlockField = new Map<string, string[]>()\n for (const edge of nesting) {\n const key = `${edge.owner}:${edge.fieldPath}`\n if (edge.ownerType === 'collection') {\n nestingByCollectionField.set(key, edge.acceptedBlockSlugs)\n } else {\n nestingByBlockField.set(key, edge.acceptedBlockSlugs)\n }\n }\n\n return {\n name: 'patchLayout',\n description:\n 'Surgically modify a document\\'s blocks-typed field (e.g. \"layout\") without sending the whole array. Pass the blocks to add/replace plus an operation (append, prepend, insertAt, replaceAt, full). The current array is fetched server-side and the operation is applied atomically. Each block must include a `blockType` plus its fields; nested `blocks`-typed fields can contain arbitrarily-deep block arrays as long as each level matches the schema. Use the `blockNesting` resource to see which slugs each field accepts.',\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 blocks-typed field to patch (default \"layout\")'),\n blocks: z\n .array(z.record(z.string(), z.unknown()))\n .describe(\n 'Blocks to compose. Each must have a `blockType` discriminator plus any block-specific fields. Nested blocks fields hold their own `blocks` arrays at any depth.',\n ),\n operation: z\n .enum(['append', 'prepend', 'insertAt', 'replaceAt', 'full'])\n .describe(\n 'How to apply the blocks: append (end), prepend (start), insertAt (at index), 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 blocks: Array<Record<string, unknown>>\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 blocks,\n operation,\n insertIndex,\n } = args\n\n // Validate the incoming blocks against the nesting map for the target\n // field before touching the database.\n const rootKey = `${collection}:${layoutField}`\n const rootAllowed = nestingByCollectionField.get(rootKey)\n if (!rootAllowed) {\n return errorResponse(\n `Field \"${layoutField}\" on collection \"${collection}\" is not a blocks-typed field, or no nesting map entry exists for it.`,\n { availableFields: [...nestingByCollectionField.keys()] },\n )\n }\n\n const errors: string[] = []\n validateBlockList(blocks, rootAllowed, layoutField, allBlockSlugs, nestingByBlockField, errors)\n\n if (errors.length > 0) {\n return errorResponse('Block validation failed.', { errors })\n }\n\n req.context = { ...req.context, source: 'mcp' }\n\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 errorResponse(`Error fetching ${collection}#${documentId}: ${message}`)\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 errorResponse(`Error patching ${collection}#${documentId}: ${message}`)\n }\n },\n }\n}\n\nfunction errorResponse(message: string, extra?: Record<string, unknown>) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ success: false, error: message, ...(extra ?? {}) }),\n },\n ],\n }\n}\n\n/**\n * Recursively validate a block array against an allow list, descending into\n * each block's own `blocks`-typed fields when present.\n */\nfunction validateBlockList(\n blocks: Array<Record<string, unknown>>,\n allowedSlugs: string[],\n pathLabel: string,\n allBlockSlugs: string[],\n nestingByBlockField: Map<string, string[]>,\n errors: string[],\n) {\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i]\n const here = `${pathLabel}[${i}]`\n\n if (!block || typeof block !== 'object') {\n errors.push(`${here}: not an object`)\n continue\n }\n\n const slug = block.blockType\n if (typeof slug !== 'string' || !slug) {\n errors.push(`${here}: missing string \\`blockType\\``)\n continue\n }\n\n if (!allBlockSlugs.includes(slug)) {\n errors.push(`${here}: unknown blockType \"${slug}\". Known: ${allBlockSlugs.join(', ')}`)\n continue\n }\n\n if (!allowedSlugs.includes(slug)) {\n errors.push(\n `${here}: blockType \"${slug}\" not allowed here. Allowed at this position: ${allowedSlugs.join(', ') || '(none)'}`,\n )\n continue\n }\n\n // Recurse: any value on this block that is itself an array of objects with\n // `blockType` is treated as a nested blocks field. Cross-check the field\n // name against the nesting map so we know the allow list for the next level.\n for (const [fieldName, value] of Object.entries(block)) {\n if (!Array.isArray(value)) continue\n if (value.length === 0) continue\n if (!value.every((v) => v && typeof v === 'object' && 'blockType' in v)) continue\n\n const nextKey = `${slug}:${fieldName}`\n const nextAllowed = nestingByBlockField.get(nextKey)\n if (!nextAllowed) {\n errors.push(\n `${here}.${fieldName}: block \"${slug}\" has no blocks field named \"${fieldName}\" in the schema`,\n )\n continue\n }\n\n validateBlockList(\n value as Array<Record<string, unknown>>,\n nextAllowed,\n `${here}.${fieldName}`,\n allBlockSlugs,\n nestingByBlockField,\n errors,\n )\n }\n }\n}\n\n/**\n * Apply a list operation against an existing array of blocks.\n * `full` always replaces; the rest preserve the existing array.\n */\nfunction applyOperation(\n newBlocks: Record<string, unknown>[],\n operation: 'full' | 'append' | 'prepend' | 'insertAt' | 'replaceAt',\n insertIndex: number | undefined,\n existingLayout: Record<string, unknown>[] | undefined,\n): Record<string, unknown>[] {\n if (operation === 'full' || !existingLayout) {\n return newBlocks\n }\n\n const existing = [...existingLayout]\n\n if (operation === 'append') return [...existing, ...newBlocks]\n if (operation === 'prepend') return [...newBlocks, ...existing]\n\n if (operation === 'insertAt') {\n if (insertIndex === undefined || insertIndex < 0 || insertIndex > existing.length) {\n return [...existing, ...newBlocks]\n }\n existing.splice(insertIndex, 0, ...newBlocks)\n return existing\n }\n\n if (operation === 'replaceAt') {\n if (insertIndex === undefined || insertIndex < 0 || insertIndex >= existing.length) {\n return existing\n }\n existing.splice(insertIndex, newBlocks.length, ...newBlocks)\n return existing\n }\n\n return newBlocks\n}\n"],"names":["z","createPatchLayoutTool","catalog","nesting","draftCollections","allBlockSlugs","blocks","map","b","slug","nestingByCollectionField","Map","nestingByBlockField","edge","key","owner","fieldPath","ownerType","set","acceptedBlockSlugs","name","description","parameters","collection","string","describe","documentId","layoutField","optional","default","array","record","unknown","operation","enum","insertIndex","number","handler","args","req","_extra","rootKey","rootAllowed","get","errorResponse","availableFields","keys","errors","validateBlockList","length","context","source","existing","payload","findByID","id","draft","overrideAccess","user","error","message","Error","String","currentLayout","Array","isArray","finalLayout","applyOperation","isDraftCollection","has","updated","update","data","displayName","title","draftNote","content","type","text","JSON","stringify","success","blockCount","extra","allowedSlugs","pathLabel","i","block","here","push","blockType","includes","join","fieldName","value","Object","entries","every","v","nextKey","nextAllowed","newBlocks","existingLayout","undefined","splice"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/tools/patch-layout.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { BlockCatalog, BlockNestingMap } from '../types'\nimport {\n DRAFT_NOTE,\n errorMessage,\n getDocDisplayName,\n jsonResponse,\n stampMcpContext,\n} from './_helpers'\n\n/**\n * patchLayout — surgical wrapper that mutates a single document's blocks-typed\n * field directly without round-tripping the entire array through updateDocument.\n *\n * Why this exists: prompting an LLM to \"add a CTA at the bottom of the home\n * page\" via updateDocument forces it to send the whole layout array, which one\n * bad token can wipe out. patchLayout fetches the current array itself,\n * applies a scoped operation, and writes back — the LLM only describes the\n * delta.\n *\n * Validation walks every block recursively against the introspected\n * BlockNestingMap, so arbitrarily-nested layouts work as long as each\n * `blocks`-typed field's content matches that field's allow list.\n */\nexport function createPatchLayoutTool(\n catalog: BlockCatalog,\n nesting: BlockNestingMap,\n draftCollections: Set<string>,\n) {\n const allBlockSlugs = new Set(catalog.blocks.map((b) => b.slug))\n\n // Lookups keyed by `<owner>:<fieldPath>` for O(1) access during recursive validation.\n const nestingByCollectionField = new Map<string, string[]>()\n const nestingByBlockField = new Map<string, string[]>()\n for (const edge of nesting) {\n const key = `${edge.owner}:${edge.fieldPath}`\n if (edge.ownerType === 'collection') {\n nestingByCollectionField.set(key, edge.acceptedBlockSlugs)\n } else {\n nestingByBlockField.set(key, edge.acceptedBlockSlugs)\n }\n }\n\n return {\n name: 'patchLayout',\n description:\n 'Surgically modify a document\\'s blocks-typed field (e.g. \"layout\") without sending the whole array. Pass the blocks to add/replace plus an operation (append, prepend, insertAt, replaceAt, full). The current array is fetched server-side and the operation is applied atomically. Each block must include a `blockType` plus its fields; nested `blocks`-typed fields can contain arbitrarily-deep block arrays as long as each level matches the schema. Use the `blockNesting` resource to see which slugs each field accepts.',\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 blocks-typed field to patch (default \"layout\")'),\n blocks: z\n .array(z.record(z.string(), z.unknown()))\n .describe(\n 'Blocks to compose. Each must have a `blockType` discriminator plus any block-specific fields. Nested blocks fields hold their own `blocks` arrays at any depth.',\n ),\n operation: z\n .enum(['append', 'prepend', 'insertAt', 'replaceAt', 'full'])\n .describe(\n 'How to apply the blocks: append (end), prepend (start), insertAt (at index), 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 blocks: Array<Record<string, unknown>>\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 blocks,\n operation,\n insertIndex,\n } = args\n\n const rootKey = `${collection}:${layoutField}`\n const rootAllowed = nestingByCollectionField.get(rootKey)\n if (!rootAllowed) {\n return errorResponse(\n `Field \"${layoutField}\" on collection \"${collection}\" is not a blocks-typed field, or no nesting map entry exists for it.`,\n { availableFields: [...nestingByCollectionField.keys()] },\n )\n }\n\n const errors: string[] = []\n validateBlockList(blocks, rootAllowed, layoutField, allBlockSlugs, nestingByBlockField, errors)\n\n if (errors.length > 0) {\n return errorResponse('Block validation failed.', { errors })\n }\n\n stampMcpContext(req)\n\n let existing: any\n try {\n existing = await req.payload.findByID({\n collection: collection as any,\n id: documentId,\n depth: 0,\n draft: true,\n req,\n overrideAccess: false,\n user: req.user,\n })\n } catch (error) {\n return errorResponse(`Error fetching ${collection}#${documentId}: ${errorMessage(error)}`)\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 = getDocDisplayName(updated, documentId)\n const draftNote = isDraftCollection ? DRAFT_NOTE : ''\n\n return jsonResponse({\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 } catch (error) {\n return errorResponse(`Error patching ${collection}#${documentId}: ${errorMessage(error)}`)\n }\n },\n }\n}\n\nfunction errorResponse(message: string, extra?: Record<string, unknown>) {\n return jsonResponse({ success: false, error: message, ...(extra ?? {}) })\n}\n\n/**\n * Recursively validate a block array against an allow list, descending into\n * each block's own `blocks`-typed fields when present.\n */\nfunction validateBlockList(\n blocks: Array<Record<string, unknown>>,\n allowedSlugs: string[],\n pathLabel: string,\n allBlockSlugs: Set<string>,\n nestingByBlockField: Map<string, string[]>,\n errors: string[],\n) {\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i]\n const here = `${pathLabel}[${i}]`\n\n if (!block || typeof block !== 'object') {\n errors.push(`${here}: not an object`)\n continue\n }\n\n const slug = block.blockType\n if (typeof slug !== 'string' || !slug) {\n errors.push(`${here}: missing string \\`blockType\\``)\n continue\n }\n\n if (!allBlockSlugs.has(slug)) {\n errors.push(`${here}: unknown blockType \"${slug}\". Known: ${[...allBlockSlugs].join(', ')}`)\n continue\n }\n\n if (!allowedSlugs.includes(slug)) {\n errors.push(\n `${here}: blockType \"${slug}\" not allowed here. Allowed at this position: ${allowedSlugs.join(', ') || '(none)'}`,\n )\n continue\n }\n\n // Any value that is itself an array of objects with `blockType` is treated\n // as a nested blocks field. The field name is cross-checked against the\n // nesting map to find the next-level allow list.\n for (const [fieldName, value] of Object.entries(block)) {\n if (!Array.isArray(value)) continue\n if (value.length === 0) continue\n if (!value.every((v) => v && typeof v === 'object' && 'blockType' in v)) continue\n\n const nextKey = `${slug}:${fieldName}`\n const nextAllowed = nestingByBlockField.get(nextKey)\n if (!nextAllowed) {\n errors.push(\n `${here}.${fieldName}: block \"${slug}\" has no blocks field named \"${fieldName}\" in the schema`,\n )\n continue\n }\n\n validateBlockList(\n value as Array<Record<string, unknown>>,\n nextAllowed,\n `${here}.${fieldName}`,\n allBlockSlugs,\n nestingByBlockField,\n errors,\n )\n }\n }\n}\n\n/**\n * Apply a list operation against an existing array of blocks.\n * `full` always replaces; the rest preserve the existing array.\n */\nfunction applyOperation(\n newBlocks: Record<string, unknown>[],\n operation: 'full' | 'append' | 'prepend' | 'insertAt' | 'replaceAt',\n insertIndex: number | undefined,\n existingLayout: Record<string, unknown>[] | undefined,\n): Record<string, unknown>[] {\n if (operation === 'full' || !existingLayout) {\n return newBlocks\n }\n\n const existing = [...existingLayout]\n\n if (operation === 'append') return [...existing, ...newBlocks]\n if (operation === 'prepend') return [...newBlocks, ...existing]\n\n if (operation === 'insertAt') {\n if (insertIndex === undefined || insertIndex < 0 || insertIndex > existing.length) {\n return [...existing, ...newBlocks]\n }\n existing.splice(insertIndex, 0, ...newBlocks)\n return existing\n }\n\n if (operation === 'replaceAt') {\n if (insertIndex === undefined || insertIndex < 0 || insertIndex >= existing.length) {\n return existing\n }\n existing.splice(insertIndex, newBlocks.length, ...newBlocks)\n return existing\n }\n\n return newBlocks\n}\n"],"names":["z","DRAFT_NOTE","errorMessage","getDocDisplayName","jsonResponse","stampMcpContext","createPatchLayoutTool","catalog","nesting","draftCollections","allBlockSlugs","Set","blocks","map","b","slug","nestingByCollectionField","Map","nestingByBlockField","edge","key","owner","fieldPath","ownerType","set","acceptedBlockSlugs","name","description","parameters","collection","string","describe","documentId","layoutField","optional","default","array","record","unknown","operation","enum","insertIndex","number","handler","args","req","_extra","rootKey","rootAllowed","get","errorResponse","availableFields","keys","errors","validateBlockList","length","existing","payload","findByID","id","depth","draft","overrideAccess","user","error","currentLayout","Array","isArray","finalLayout","applyOperation","isDraftCollection","has","updated","update","data","displayName","draftNote","success","message","blockCount","extra","allowedSlugs","pathLabel","i","block","here","push","blockType","join","includes","fieldName","value","Object","entries","every","v","nextKey","nextAllowed","newBlocks","existingLayout","undefined","splice"],"mappings":"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"}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
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
3
|
export declare function createPublishDraftTool(draftCollections: Set<string>): {
|
|
9
4
|
name: string;
|
|
10
5
|
description: string;
|
|
@@ -15,10 +10,5 @@ export declare function createPublishDraftTool(draftCollections: Set<string>): {
|
|
|
15
10
|
handler: (args: {
|
|
16
11
|
collection: string;
|
|
17
12
|
documentId: string;
|
|
18
|
-
}, req: PayloadRequest, _extra: unknown) => Promise<
|
|
19
|
-
content: {
|
|
20
|
-
type: "text";
|
|
21
|
-
text: string;
|
|
22
|
-
}[];
|
|
23
|
-
}>;
|
|
13
|
+
}, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
|
|
24
14
|
};
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* @param draftCollections - Set of collection slugs that support drafts
|
|
6
|
-
*/ export function createPublishDraftTool(draftCollections) {
|
|
2
|
+
import { errorMessage, getDocDisplayName, requireDraftCollection, stampMcpContext, textResponse } from './_helpers';
|
|
3
|
+
export function createPublishDraftTool(draftCollections) {
|
|
7
4
|
return {
|
|
8
5
|
name: 'publishDraft',
|
|
9
6
|
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.',
|
|
@@ -15,22 +12,9 @@ import { z } from 'zod';
|
|
|
15
12
|
},
|
|
16
13
|
handler: async (args, req, _extra)=>{
|
|
17
14
|
const { collection, documentId } = args;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
};
|
|
15
|
+
const guard = requireDraftCollection(collection, draftCollections);
|
|
16
|
+
if (guard) return guard;
|
|
17
|
+
stampMcpContext(req);
|
|
34
18
|
try {
|
|
35
19
|
const doc = await req.payload.update({
|
|
36
20
|
collection: collection,
|
|
@@ -42,25 +26,10 @@ import { z } from 'zod';
|
|
|
42
26
|
overrideAccess: false,
|
|
43
27
|
user: req.user
|
|
44
28
|
});
|
|
45
|
-
const displayName = doc
|
|
46
|
-
return {
|
|
47
|
-
content: [
|
|
48
|
-
{
|
|
49
|
-
type: 'text',
|
|
50
|
-
text: `Successfully published "${displayName}" in ${collection} (ID: ${documentId}).`
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
};
|
|
29
|
+
const displayName = getDocDisplayName(doc, documentId);
|
|
30
|
+
return textResponse(`Successfully published "${displayName}" in ${collection} (ID: ${documentId}).`);
|
|
54
31
|
} catch (error) {
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
content: [
|
|
58
|
-
{
|
|
59
|
-
type: 'text',
|
|
60
|
-
text: `Error publishing document ${documentId} in ${collection}: ${message}`
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
};
|
|
32
|
+
return textResponse(`Error publishing document ${documentId} in ${collection}: ${errorMessage(error)}`);
|
|
64
33
|
}
|
|
65
34
|
}
|
|
66
35
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/tools/publish-draft.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\
|
|
1
|
+
{"version":3,"sources":["../../src/tools/publish-draft.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport {\n errorMessage,\n getDocDisplayName,\n requireDraftCollection,\n stampMcpContext,\n textResponse,\n} from './_helpers'\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 const guard = requireDraftCollection(collection, draftCollections)\n if (guard) return guard\n\n stampMcpContext(req)\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 = getDocDisplayName(doc, documentId)\n return textResponse(\n `Successfully published \"${displayName}\" in ${collection} (ID: ${documentId}).`,\n )\n } catch (error) {\n return textResponse(\n `Error publishing document ${documentId} in ${collection}: ${errorMessage(error)}`,\n )\n }\n },\n }\n}\n"],"names":["z","errorMessage","getDocDisplayName","requireDraftCollection","stampMcpContext","textResponse","createPublishDraftTool","draftCollections","name","description","parameters","collection","string","describe","join","documentId","handler","args","req","_extra","guard","doc","payload","update","id","data","_status","overrideAccess","user","displayName","error"],"mappings":"AAAA,SAASA,CAAC,QAAQ,MAAK;AAEvB,SACEC,YAAY,EACZC,iBAAiB,EACjBC,sBAAsB,EACtBC,eAAe,EACfC,YAAY,QACP,aAAY;AAEnB,OAAO,SAASC,uBAAuBC,gBAA6B;IAClE,OAAO;QACLC,MAAM;QACNC,aACE,wFACA,0FACA;QACFC,YAAY;YACVC,YAAYX,EACTY,MAAM,GACNC,QAAQ,CACP,CAAC,gDAAgD,EAAE;mBAAIN;aAAiB,CAACO,IAAI,CAAC,SAAS,QAAQ;YAEnGC,YAAYf,EAAEY,MAAM,GAAGC,QAAQ,CAAC;QAClC;QACAG,SAAS,OACPC,MACAC,KACAC;YAEA,MAAM,EAAER,UAAU,EAAEI,UAAU,EAAE,GAAGE;YAEnC,MAAMG,QAAQjB,uBAAuBQ,YAAYJ;YACjD,IAAIa,OAAO,OAAOA;YAElBhB,gBAAgBc;YAEhB,IAAI;gBACF,MAAMG,MAAM,MAAMH,IAAII,OAAO,CAACC,MAAM,CAAC;oBACnCZ,YAAYA;oBACZa,IAAIT;oBACJU,MAAM;wBAAEC,SAAS;oBAAY;oBAC7BR;oBACAS,gBAAgB;oBAChBC,MAAMV,IAAIU,IAAI;gBAChB;gBAEA,MAAMC,cAAc3B,kBAAkBmB,KAAKN;gBAC3C,OAAOV,aACL,CAAC,wBAAwB,EAAEwB,YAAY,KAAK,EAAElB,WAAW,MAAM,EAAEI,WAAW,EAAE,CAAC;YAEnF,EAAE,OAAOe,OAAO;gBACd,OAAOzB,aACL,CAAC,0BAA0B,EAAEU,WAAW,IAAI,EAAEJ,WAAW,EAAE,EAAEV,aAAa6B,QAAQ;YAEtF;QACF;IACF;AACF"}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
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
3
|
export declare function createResolveReferenceTool(searchableCollections: Map<string, string[]>): {
|
|
10
4
|
name: string;
|
|
11
5
|
description: string;
|
|
@@ -22,10 +16,5 @@ export declare function createResolveReferenceTool(searchableCollections: Map<st
|
|
|
22
16
|
handler: (args: {
|
|
23
17
|
query: string;
|
|
24
18
|
collection?: string;
|
|
25
|
-
}, req: PayloadRequest) => Promise<
|
|
26
|
-
content: {
|
|
27
|
-
type: "text";
|
|
28
|
-
text: string;
|
|
29
|
-
}[];
|
|
30
|
-
}>;
|
|
19
|
+
}, req: PayloadRequest) => Promise<import("./_helpers").McpTextResponse>;
|
|
31
20
|
};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
|
|
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) {
|
|
2
|
+
import { jsonResponse, stampMcpContext } from './_helpers';
|
|
3
|
+
export function createResolveReferenceTool(searchableCollections) {
|
|
8
4
|
return {
|
|
9
5
|
name: 'resolveReference',
|
|
10
6
|
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.',
|
|
@@ -13,10 +9,7 @@ import { z } from 'zod';
|
|
|
13
9
|
collection: z.string().optional().describe('Optional collection slug to restrict search to a single collection')
|
|
14
10
|
}),
|
|
15
11
|
handler: async (args, req)=>{
|
|
16
|
-
req
|
|
17
|
-
...req.context,
|
|
18
|
-
source: 'mcp'
|
|
19
|
-
};
|
|
12
|
+
stampMcpContext(req);
|
|
20
13
|
const { query, collection } = args;
|
|
21
14
|
const collectionsToSearch = collection ? new Map(searchableCollections.has(collection) ? [
|
|
22
15
|
[
|
|
@@ -26,91 +19,58 @@ import { z } from 'zod';
|
|
|
26
19
|
] : []) : searchableCollections;
|
|
27
20
|
if (collectionsToSearch.size === 0) {
|
|
28
21
|
const available = Array.from(searchableCollections.keys()).join(', ');
|
|
29
|
-
return {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
};
|
|
22
|
+
return jsonResponse({
|
|
23
|
+
candidates: [],
|
|
24
|
+
message: collection ? `Collection "${collection}" has no searchable fields or does not exist. Available searchable collections: ${available}` : 'No searchable collections found.'
|
|
25
|
+
});
|
|
40
26
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const orConditions = fields.map((field)=>({
|
|
44
|
-
[field]: {
|
|
45
|
-
like: query
|
|
46
|
-
}
|
|
47
|
-
}));
|
|
48
|
-
if (orConditions.length === 0) continue;
|
|
27
|
+
const targets = Array.from(collectionsToSearch.entries()).filter(([, fields])=>fields.length > 0);
|
|
28
|
+
const settled = await Promise.allSettled(targets.map(([slug, fields])=>{
|
|
49
29
|
const selectFields = {};
|
|
50
|
-
for (const field of fields)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
30
|
+
for (const field of fields)selectFields[field] = true;
|
|
31
|
+
return req.payload.find({
|
|
32
|
+
collection: slug,
|
|
33
|
+
where: {
|
|
34
|
+
or: fields.map((field)=>({
|
|
35
|
+
[field]: {
|
|
36
|
+
like: query
|
|
37
|
+
}
|
|
38
|
+
}))
|
|
39
|
+
},
|
|
40
|
+
limit: 5,
|
|
41
|
+
select: selectFields,
|
|
42
|
+
req,
|
|
43
|
+
overrideAccess: false,
|
|
44
|
+
user: req.user
|
|
45
|
+
});
|
|
46
|
+
}));
|
|
47
|
+
const allCandidates = [];
|
|
48
|
+
settled.forEach((outcome, i)=>{
|
|
49
|
+
if (outcome.status !== 'fulfilled') return;
|
|
50
|
+
const [slug, fields] = targets[i];
|
|
51
|
+
for (const doc of outcome.value.docs){
|
|
52
|
+
allCandidates.push(...rankDocument(doc, slug, fields, query));
|
|
71
53
|
}
|
|
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
54
|
});
|
|
55
|
+
allCandidates.sort((a, b)=>matchTypePriority(a.matchType) - matchTypePriority(b.matchType));
|
|
56
|
+
if (allCandidates.length === 0) {
|
|
57
|
+
const searched = Array.from(collectionsToSearch.keys()).join(', ');
|
|
58
|
+
const available = Array.from(searchableCollections.keys()).join(', ');
|
|
59
|
+
return jsonResponse({
|
|
60
|
+
candidates: {},
|
|
61
|
+
message: `No results found for "${query}" in: ${searched}. Try a different spelling or search term. All searchable collections: ${available}`
|
|
62
|
+
});
|
|
63
|
+
}
|
|
82
64
|
const grouped = {};
|
|
83
65
|
for (const candidate of allCandidates){
|
|
84
66
|
const { collection: col, ...rest } = candidate;
|
|
85
67
|
if (!grouped[col]) grouped[col] = [];
|
|
86
68
|
grouped[col].push(rest);
|
|
87
69
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
};
|
|
70
|
+
return jsonResponse({
|
|
71
|
+
candidates: grouped,
|
|
72
|
+
total: allCandidates.length
|
|
73
|
+
});
|
|
114
74
|
}
|
|
115
75
|
};
|
|
116
76
|
}
|
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"sources":["../../src/tools/resolve-reference.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport { jsonResponse, stampMcpContext } from './_helpers'\n\ninterface MatchCandidate {\n collection: string\n id: string | number\n displayName: string\n matchedField: string\n matchType: 'exact-slug' | 'exact' | 'partial'\n}\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 stampMcpContext(req)\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 jsonResponse({\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 const targets = Array.from(collectionsToSearch.entries()).filter(\n ([, fields]) => fields.length > 0,\n )\n\n const settled = await Promise.allSettled(\n targets.map(([slug, fields]) => {\n const selectFields: Record<string, true> = {}\n for (const field of fields) selectFields[field] = true\n return req.payload.find({\n collection: slug as any,\n where: { or: fields.map((field) => ({ [field]: { like: query } })) },\n limit: 5,\n select: selectFields,\n req,\n overrideAccess: false,\n user: req.user,\n })\n }),\n )\n\n const allCandidates: MatchCandidate[] = []\n settled.forEach((outcome, i) => {\n if (outcome.status !== 'fulfilled') return\n const [slug, fields] = targets[i]\n for (const doc of outcome.value.docs) {\n allCandidates.push(...rankDocument(doc, slug, fields, query))\n }\n })\n\n allCandidates.sort(\n (a, b) => matchTypePriority(a.matchType) - matchTypePriority(b.matchType),\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 jsonResponse({\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 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 return jsonResponse({\n candidates: grouped,\n total: allCandidates.length,\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","jsonResponse","stampMcpContext","createResolveReferenceTool","searchableCollections","name","description","parameters","object","query","string","describe","collection","optional","handler","args","req","collectionsToSearch","Map","has","get","size","available","Array","from","keys","join","candidates","message","targets","entries","filter","fields","length","settled","Promise","allSettled","map","slug","selectFields","field","payload","find","where","or","like","limit","select","overrideAccess","user","allCandidates","forEach","outcome","i","status","doc","value","docs","push","rankDocument","sort","a","b","matchTypePriority","matchType","searched","grouped","candidate","col","rest","total","queryLower","toLowerCase","bestMatch","valueLower","id","displayName","getDisplayName","matchedField","type","preferred","includes","String"],"mappings":"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"}
|