@payloadcms/plugin-mcp 4.0.0-internal.532221d → 4.0.0-internal.567a487

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/mcp/builtin/collections/createTool.js +2 -1
  2. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  3. package/dist/mcp/builtin/collections/duplicateTool.js +2 -1
  4. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
  5. package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +1 -1
  6. package/dist/mcp/builtin/collections/formatCollectionError.js +1 -1
  7. package/dist/mcp/builtin/collections/formatCollectionError.js.map +1 -1
  8. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -1
  9. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +2 -1
  10. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -1
  11. package/dist/mcp/builtin/collections/updateTool.js +3 -1
  12. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  13. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -1
  14. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +2 -1
  15. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -1
  16. package/dist/mcp/builtin/globals/updateTool.js +2 -1
  17. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  18. package/dist/mcp/builtin/validateEntityData.d.ts.map +1 -1
  19. package/dist/mcp/builtin/validateEntityData.js +1 -1
  20. package/dist/mcp/builtin/validateEntityData.js.map +1 -1
  21. package/dist/utils/getVirtualFieldNames.d.ts +14 -0
  22. package/dist/utils/getVirtualFieldNames.d.ts.map +1 -0
  23. package/dist/utils/getVirtualFieldNames.js +35 -0
  24. package/dist/utils/getVirtualFieldNames.js.map +1 -0
  25. package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts +28 -0
  26. package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts.map +1 -0
  27. package/dist/utils/schemaConversion/filterFieldsByAccess.js +71 -0
  28. package/dist/utils/schemaConversion/filterFieldsByAccess.js.map +1 -0
  29. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts +13 -0
  30. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts.map +1 -0
  31. package/dist/utils/schemaConversion/getEntityInputSchema.js +66 -0
  32. package/dist/utils/schemaConversion/getEntityInputSchema.js.map +1 -0
  33. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +10 -0
  34. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +1 -0
  35. package/dist/utils/schemaConversion/sanitizeEntitySchema.js +384 -0
  36. package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +1 -0
  37. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +133 -0
  38. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +1 -0
  39. package/dist/utils/transformPointDataToPayload.d.ts +7 -0
  40. package/dist/utils/transformPointDataToPayload.d.ts.map +1 -0
  41. package/dist/utils/transformPointDataToPayload.js +28 -0
  42. package/dist/utils/transformPointDataToPayload.js.map +1 -0
  43. package/package.json +3 -3
  44. package/src/mcp/builtin/collections/createTool.ts +5 -5
  45. package/src/mcp/builtin/collections/duplicateTool.ts +5 -5
  46. package/src/mcp/builtin/collections/formatCollectionError.ts +2 -2
  47. package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +2 -1
  48. package/src/mcp/builtin/collections/updateTool.ts +6 -6
  49. package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +2 -1
  50. package/src/mcp/builtin/globals/updateTool.ts +5 -5
  51. package/src/mcp/builtin/validateEntityData.ts +7 -3
  52. package/src/utils/getVirtualFieldNames.ts +53 -0
  53. package/src/utils/schemaConversion/filterFieldsByAccess.ts +123 -0
  54. package/src/utils/schemaConversion/getEntityInputSchema.ts +96 -0
  55. package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +96 -0
  56. package/src/utils/schemaConversion/sanitizeEntitySchema.ts +432 -0
  57. package/src/utils/transformPointDataToPayload.ts +40 -0
@@ -0,0 +1,133 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { sanitizeEntitySchema } from './sanitizeEntitySchema.js';
3
+ describe('sanitizeEntitySchema', ()=>{
4
+ it('renames the Lexical node-union definition to a short name and keeps it a strict oneOf', ()=>{
5
+ // Shaped like the schema the MCP server prepares for a collection with a Lexical richText field
6
+ // (Payload's `input` variant): a `$defs` node union (a `oneOf` of node shapes). The input variant
7
+ // already types the relationship node's `value` as a bare ID - there is no populated-doc `$ref` to
8
+ // reduce here (the variant did that upstream), so sanitize leaves the value untouched.
9
+ const standalone = {
10
+ type: 'object',
11
+ $defs: {
12
+ LexicalNodes_ABCDEF12: {
13
+ oneOf: [
14
+ {
15
+ type: 'object',
16
+ additionalProperties: false,
17
+ properties: {
18
+ type: {
19
+ const: 'paragraph'
20
+ }
21
+ },
22
+ required: [
23
+ 'type'
24
+ ]
25
+ },
26
+ {
27
+ type: 'object',
28
+ additionalProperties: false,
29
+ properties: {
30
+ type: {
31
+ const: 'relationship'
32
+ },
33
+ value: {
34
+ type: 'string',
35
+ description: 'The related document ID.'
36
+ }
37
+ },
38
+ required: [
39
+ 'type'
40
+ ]
41
+ }
42
+ ]
43
+ }
44
+ },
45
+ properties: {
46
+ id: {
47
+ type: 'string'
48
+ },
49
+ content: {
50
+ type: 'object',
51
+ properties: {
52
+ root: {
53
+ type: 'object',
54
+ properties: {
55
+ children: {
56
+ type: 'array',
57
+ items: {
58
+ $ref: '#/$defs/LexicalNodes_ABCDEF12'
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ };
67
+ expect(sanitizeEntitySchema(standalone)).toStrictEqual({
68
+ type: 'object',
69
+ $defs: {
70
+ // The node union is renamed to a short, readable `node`, and stays a strict discriminated
71
+ // `oneOf` - not loosened to `anyOf`.
72
+ node: {
73
+ oneOf: [
74
+ {
75
+ type: 'object',
76
+ additionalProperties: false,
77
+ properties: {
78
+ type: {
79
+ const: 'paragraph'
80
+ }
81
+ },
82
+ required: [
83
+ 'type'
84
+ ]
85
+ },
86
+ {
87
+ type: 'object',
88
+ additionalProperties: false,
89
+ properties: {
90
+ type: {
91
+ const: 'relationship'
92
+ },
93
+ // The value is already ID-only from the input variant - left as-is.
94
+ value: {
95
+ type: 'string',
96
+ description: 'The related document ID.'
97
+ }
98
+ },
99
+ required: [
100
+ 'type'
101
+ ]
102
+ }
103
+ ]
104
+ }
105
+ },
106
+ properties: {
107
+ // Managed timestamps are excluded upstream by the `input` variant. `id` stays because it's
108
+ // a valid optional input (a client may supply a custom ID).
109
+ id: {
110
+ type: 'string'
111
+ },
112
+ content: {
113
+ type: 'object',
114
+ properties: {
115
+ root: {
116
+ type: 'object',
117
+ properties: {
118
+ children: {
119
+ type: 'array',
120
+ items: {
121
+ $ref: '#/$defs/node'
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ });
130
+ });
131
+ });
132
+
133
+ //# sourceMappingURL=sanitizeEntitySchema.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/schemaConversion/sanitizeEntitySchema.spec.ts"],"sourcesContent":["import { describe, expect, it } from 'vitest'\n\nimport type { JsonSchemaType } from '../../types.js'\n\nimport { sanitizeEntitySchema } from './sanitizeEntitySchema.js'\n\ndescribe('sanitizeEntitySchema', () => {\n it('renames the Lexical node-union definition to a short name and keeps it a strict oneOf', () => {\n // Shaped like the schema the MCP server prepares for a collection with a Lexical richText field\n // (Payload's `input` variant): a `$defs` node union (a `oneOf` of node shapes). The input variant\n // already types the relationship node's `value` as a bare ID - there is no populated-doc `$ref` to\n // reduce here (the variant did that upstream), so sanitize leaves the value untouched.\n const standalone: JsonSchemaType = {\n type: 'object',\n $defs: {\n LexicalNodes_ABCDEF12: {\n oneOf: [\n {\n type: 'object',\n additionalProperties: false,\n properties: { type: { const: 'paragraph' } },\n required: ['type'],\n },\n {\n type: 'object',\n additionalProperties: false,\n properties: {\n type: { const: 'relationship' },\n value: { type: 'string', description: 'The related document ID.' },\n },\n required: ['type'],\n },\n ],\n },\n },\n properties: {\n id: { type: 'string' },\n content: {\n type: 'object',\n properties: {\n root: {\n type: 'object',\n properties: {\n children: { type: 'array', items: { $ref: '#/$defs/LexicalNodes_ABCDEF12' } },\n },\n },\n },\n },\n },\n }\n\n expect(sanitizeEntitySchema(standalone)).toStrictEqual({\n type: 'object',\n $defs: {\n // The node union is renamed to a short, readable `node`, and stays a strict discriminated\n // `oneOf` - not loosened to `anyOf`.\n node: {\n oneOf: [\n {\n type: 'object',\n additionalProperties: false,\n properties: { type: { const: 'paragraph' } },\n required: ['type'],\n },\n {\n type: 'object',\n additionalProperties: false,\n properties: {\n type: { const: 'relationship' },\n // The value is already ID-only from the input variant - left as-is.\n value: { type: 'string', description: 'The related document ID.' },\n },\n required: ['type'],\n },\n ],\n },\n },\n properties: {\n // Managed timestamps are excluded upstream by the `input` variant. `id` stays because it's\n // a valid optional input (a client may supply a custom ID).\n id: { type: 'string' },\n content: {\n type: 'object',\n properties: {\n root: {\n type: 'object',\n properties: {\n children: { type: 'array', items: { $ref: '#/$defs/node' } },\n },\n },\n },\n },\n },\n })\n })\n})\n"],"names":["describe","expect","it","sanitizeEntitySchema","standalone","type","$defs","LexicalNodes_ABCDEF12","oneOf","additionalProperties","properties","const","required","value","description","id","content","root","children","items","$ref","toStrictEqual","node"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAQ;AAI7C,SAASC,oBAAoB,QAAQ,4BAA2B;AAEhEH,SAAS,wBAAwB;IAC/BE,GAAG,yFAAyF;QAC1F,gGAAgG;QAChG,kGAAkG;QAClG,mGAAmG;QACnG,uFAAuF;QACvF,MAAME,aAA6B;YACjCC,MAAM;YACNC,OAAO;gBACLC,uBAAuB;oBACrBC,OAAO;wBACL;4BACEH,MAAM;4BACNI,sBAAsB;4BACtBC,YAAY;gCAAEL,MAAM;oCAAEM,OAAO;gCAAY;4BAAE;4BAC3CC,UAAU;gCAAC;6BAAO;wBACpB;wBACA;4BACEP,MAAM;4BACNI,sBAAsB;4BACtBC,YAAY;gCACVL,MAAM;oCAAEM,OAAO;gCAAe;gCAC9BE,OAAO;oCAAER,MAAM;oCAAUS,aAAa;gCAA2B;4BACnE;4BACAF,UAAU;gCAAC;6BAAO;wBACpB;qBACD;gBACH;YACF;YACAF,YAAY;gBACVK,IAAI;oBAAEV,MAAM;gBAAS;gBACrBW,SAAS;oBACPX,MAAM;oBACNK,YAAY;wBACVO,MAAM;4BACJZ,MAAM;4BACNK,YAAY;gCACVQ,UAAU;oCAAEb,MAAM;oCAASc,OAAO;wCAAEC,MAAM;oCAAgC;gCAAE;4BAC9E;wBACF;oBACF;gBACF;YACF;QACF;QAEAnB,OAAOE,qBAAqBC,aAAaiB,aAAa,CAAC;YACrDhB,MAAM;YACNC,OAAO;gBACL,0FAA0F;gBAC1F,qCAAqC;gBACrCgB,MAAM;oBACJd,OAAO;wBACL;4BACEH,MAAM;4BACNI,sBAAsB;4BACtBC,YAAY;gCAAEL,MAAM;oCAAEM,OAAO;gCAAY;4BAAE;4BAC3CC,UAAU;gCAAC;6BAAO;wBACpB;wBACA;4BACEP,MAAM;4BACNI,sBAAsB;4BACtBC,YAAY;gCACVL,MAAM;oCAAEM,OAAO;gCAAe;gCAC9B,oEAAoE;gCACpEE,OAAO;oCAAER,MAAM;oCAAUS,aAAa;gCAA2B;4BACnE;4BACAF,UAAU;gCAAC;6BAAO;wBACpB;qBACD;gBACH;YACF;YACAF,YAAY;gBACV,2FAA2F;gBAC3F,4DAA4D;gBAC5DK,IAAI;oBAAEV,MAAM;gBAAS;gBACrBW,SAAS;oBACPX,MAAM;oBACNK,YAAY;wBACVO,MAAM;4BACJZ,MAAM;4BACNK,YAAY;gCACVQ,UAAU;oCAAEb,MAAM;oCAASc,OAAO;wCAAEC,MAAM;oCAAe;gCAAE;4BAC7D;wBACF;oBACF;gBACF;YACF;QACF;IACF;AACF"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Transforms incoming MCP tool data from object format to tuple array format.
3
+ * Converts { longitude: number, latitude: number } back to [longitude, latitude]
4
+ * for Payload's internal point field representation.
5
+ */
6
+ export declare function transformPointDataToPayload(data: Record<string, unknown>): Record<string, unknown>;
7
+ //# sourceMappingURL=transformPointDataToPayload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformPointDataToPayload.d.ts","sourceRoot":"","sources":["../../src/utils/transformPointDataToPayload.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgCzB"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Transforms incoming MCP tool data from object format to tuple array format.
3
+ * Converts { longitude: number, latitude: number } back to [longitude, latitude]
4
+ * for Payload's internal point field representation.
5
+ */ export function transformPointDataToPayload(data) {
6
+ if (!data || typeof data !== 'object') {
7
+ return data;
8
+ }
9
+ const transformed = {};
10
+ for (const [key, value] of Object.entries(data)){
11
+ if (value && typeof value === 'object' && 'longitude' in value && 'latitude' in value && typeof value.longitude === 'number' && typeof value.latitude === 'number') {
12
+ // Transform to tuple array [longitude, latitude]
13
+ transformed[key] = [
14
+ value.longitude,
15
+ value.latitude
16
+ ];
17
+ } else if (Array.isArray(value)) {
18
+ transformed[key] = value.map((item)=>typeof item === 'object' ? transformPointDataToPayload(item) : item);
19
+ } else if (value && typeof value === 'object') {
20
+ transformed[key] = transformPointDataToPayload(value);
21
+ } else {
22
+ transformed[key] = value;
23
+ }
24
+ }
25
+ return transformed;
26
+ }
27
+
28
+ //# sourceMappingURL=transformPointDataToPayload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/transformPointDataToPayload.ts"],"sourcesContent":["/**\n * Transforms incoming MCP tool data from object format to tuple array format.\n * Converts { longitude: number, latitude: number } back to [longitude, latitude]\n * for Payload's internal point field representation.\n */\nexport function transformPointDataToPayload(\n data: Record<string, unknown>,\n): Record<string, unknown> {\n if (!data || typeof data !== 'object') {\n return data\n }\n\n const transformed: Record<string, unknown> = {}\n\n for (const [key, value] of Object.entries(data)) {\n if (\n value &&\n typeof value === 'object' &&\n 'longitude' in value &&\n 'latitude' in value &&\n typeof value.longitude === 'number' &&\n typeof value.latitude === 'number'\n ) {\n // Transform to tuple array [longitude, latitude]\n transformed[key] = [value.longitude, value.latitude]\n } else if (Array.isArray(value)) {\n transformed[key] = value.map((item) =>\n typeof item === 'object'\n ? transformPointDataToPayload(item as Record<string, unknown>)\n : item,\n )\n } else if (value && typeof value === 'object') {\n transformed[key] = transformPointDataToPayload(value as Record<string, unknown>)\n } else {\n transformed[key] = value\n }\n }\n\n return transformed\n}\n"],"names":["transformPointDataToPayload","data","transformed","key","value","Object","entries","longitude","latitude","Array","isArray","map","item"],"mappings":"AAAA;;;;CAIC,GACD,OAAO,SAASA,4BACdC,IAA6B;IAE7B,IAAI,CAACA,QAAQ,OAAOA,SAAS,UAAU;QACrC,OAAOA;IACT;IAEA,MAAMC,cAAuC,CAAC;IAE9C,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACL,MAAO;QAC/C,IACEG,SACA,OAAOA,UAAU,YACjB,eAAeA,SACf,cAAcA,SACd,OAAOA,MAAMG,SAAS,KAAK,YAC3B,OAAOH,MAAMI,QAAQ,KAAK,UAC1B;YACA,iDAAiD;YACjDN,WAAW,CAACC,IAAI,GAAG;gBAACC,MAAMG,SAAS;gBAAEH,MAAMI,QAAQ;aAAC;QACtD,OAAO,IAAIC,MAAMC,OAAO,CAACN,QAAQ;YAC/BF,WAAW,CAACC,IAAI,GAAGC,MAAMO,GAAG,CAAC,CAACC,OAC5B,OAAOA,SAAS,WACZZ,4BAA4BY,QAC5BA;QAER,OAAO,IAAIR,SAAS,OAAOA,UAAU,UAAU;YAC7CF,WAAW,CAACC,IAAI,GAAGH,4BAA4BI;QACjD,OAAO;YACLF,WAAW,CAACC,IAAI,GAAGC;QACrB;IACF;IAEA,OAAOF;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payloadcms/plugin-mcp",
3
- "version": "4.0.0-internal.532221d",
3
+ "version": "4.0.0-internal.567a487",
4
4
  "description": "MCP (Model Context Protocol) capabilities with Payload",
5
5
  "keywords": [
6
6
  "plugin",
@@ -61,10 +61,10 @@
61
61
  },
62
62
  "devDependencies": {
63
63
  "@payloadcms/eslint-config": "3.28.0",
64
- "payload": "4.0.0-internal.532221d"
64
+ "payload": "4.0.0-internal.567a487"
65
65
  },
66
66
  "peerDependencies": {
67
- "payload": "4.0.0-internal.532221d"
67
+ "payload": "4.0.0-internal.567a487"
68
68
  },
69
69
  "//deps_notes": {
70
70
  "zod": "zod is a hard dependency of @modelcontextprotocol/server, thus we can safely use it without it impacting bundle size. Make extra sure the zod version here matches exactly what's defined in the dependencies of @modelcontextprotocol/server to avoid duplicate versions being installed.",
@@ -1,15 +1,15 @@
1
1
  import type { SelectType } from 'payload'
2
2
 
3
- import {
4
- getCollectionVirtualFieldNames,
5
- stripVirtualFields,
6
- transformPointDataToPayload,
7
- } from 'payload'
8
3
  import { z } from 'zod'
9
4
 
10
5
  import { defaultAccess } from '../../../defaultAccess.js'
11
6
  import { defineCollectionTool } from '../../../defineTool.js'
12
7
  import { getLogger } from '../../../utils/getLogger.js'
8
+ import {
9
+ getCollectionVirtualFieldNames,
10
+ stripVirtualFields,
11
+ } from '../../../utils/getVirtualFieldNames.js'
12
+ import { transformPointDataToPayload } from '../../../utils/transformPointDataToPayload.js'
13
13
  import { validateCollectionData } from '../validateEntityData.js'
14
14
  import { fileInputSchema, resolveFile } from './fileInput.js'
15
15
  import { formatCollectionError } from './formatCollectionError.js'
@@ -1,15 +1,15 @@
1
1
  import type { PopulateType, SelectType } from 'payload'
2
2
 
3
- import {
4
- getCollectionVirtualFieldNames,
5
- stripVirtualFields,
6
- transformPointDataToPayload,
7
- } from 'payload'
8
3
  import { z } from 'zod'
9
4
 
10
5
  import { defaultAccess } from '../../../defaultAccess.js'
11
6
  import { defineCollectionTool } from '../../../defineTool.js'
12
7
  import { getLogger } from '../../../utils/getLogger.js'
8
+ import {
9
+ getCollectionVirtualFieldNames,
10
+ stripVirtualFields,
11
+ } from '../../../utils/getVirtualFieldNames.js'
12
+ import { transformPointDataToPayload } from '../../../utils/transformPointDataToPayload.js'
13
13
  import { formatCollectionError } from './formatCollectionError.js'
14
14
 
15
15
  const DEFAULT_DESCRIPTION =
@@ -1,9 +1,9 @@
1
1
  import type { CollectionSlug, PayloadRequest } from 'payload'
2
2
 
3
- import { getCollectionInputSchema } from 'payload'
4
-
5
3
  import type { MCPToolResponse } from '../../../types.js'
6
4
 
5
+ import { getCollectionInputSchema } from '../../../utils/schemaConversion/getEntityInputSchema.js'
6
+
7
7
  const getValidationErrors = (error: unknown): undefined | unknown[] => {
8
8
  if (!error || typeof error !== 'object') {
9
9
  return undefined
@@ -1,7 +1,8 @@
1
- import { getAccessResults, getCollectionInputSchema } from 'payload'
1
+ import { getAccessResults } from 'payload'
2
2
 
3
3
  import { defaultAccess } from '../../../defaultAccess.js'
4
4
  import { defineCollectionTool } from '../../../defineTool.js'
5
+ import { getCollectionInputSchema } from '../../../utils/schemaConversion/getEntityInputSchema.js'
5
6
 
6
7
  export const getCollectionSchemaTool = defineCollectionTool({
7
8
  access: (args) => {
@@ -1,16 +1,16 @@
1
1
  import type { SelectType, Where } from 'payload'
2
2
 
3
- import {
4
- getCollectionInputSchema,
5
- getCollectionVirtualFieldNames,
6
- stripVirtualFields,
7
- transformPointDataToPayload,
8
- } from 'payload'
9
3
  import { z } from 'zod'
10
4
 
11
5
  import { defaultAccess } from '../../../defaultAccess.js'
12
6
  import { defineCollectionTool } from '../../../defineTool.js'
13
7
  import { getLogger } from '../../../utils/getLogger.js'
8
+ import {
9
+ getCollectionVirtualFieldNames,
10
+ stripVirtualFields,
11
+ } from '../../../utils/getVirtualFieldNames.js'
12
+ import { getCollectionInputSchema } from '../../../utils/schemaConversion/getEntityInputSchema.js'
13
+ import { transformPointDataToPayload } from '../../../utils/transformPointDataToPayload.js'
14
14
  import { whereSchema } from '../../../utils/whereSchema.js'
15
15
  import { validateCollectionData } from '../validateEntityData.js'
16
16
  import { fileInputSchema, resolveFile } from './fileInput.js'
@@ -1,7 +1,8 @@
1
- import { getAccessResults, getGlobalInputSchema } from 'payload'
1
+ import { getAccessResults } from 'payload'
2
2
 
3
3
  import { defaultAccess } from '../../../defaultAccess.js'
4
4
  import { defineGlobalTool } from '../../../defineTool.js'
5
+ import { getGlobalInputSchema } from '../../../utils/schemaConversion/getEntityInputSchema.js'
5
6
 
6
7
  export const getGlobalSchemaTool = defineGlobalTool({
7
8
  access: (args) => {
@@ -1,15 +1,15 @@
1
1
  import type { SelectType } from 'payload'
2
2
 
3
- import {
4
- getGlobalVirtualFieldNames,
5
- stripVirtualFields,
6
- transformPointDataToPayload,
7
- } from 'payload'
8
3
  import { z } from 'zod'
9
4
 
10
5
  import { defaultAccess } from '../../../defaultAccess.js'
11
6
  import { defineGlobalTool } from '../../../defineTool.js'
12
7
  import { getLogger } from '../../../utils/getLogger.js'
8
+ import {
9
+ getGlobalVirtualFieldNames,
10
+ stripVirtualFields,
11
+ } from '../../../utils/getVirtualFieldNames.js'
12
+ import { transformPointDataToPayload } from '../../../utils/transformPointDataToPayload.js'
13
13
  import { validateGlobalData } from '../validateEntityData.js'
14
14
 
15
15
  const DEFAULT_DESCRIPTION = 'Update any Payload global by passing the global slug and data.'
@@ -1,10 +1,14 @@
1
1
  import type { CollectionSlug, GlobalSlug, PayloadRequest, SanitizedConfig } from 'payload'
2
2
 
3
3
  import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server/validators/cf-worker'
4
- import { getCollectionInputSchema, getGlobalInputSchema } from 'payload'
5
4
 
6
5
  import type { JsonSchemaType, MCPToolResponse } from '../../types.js'
7
6
 
7
+ import {
8
+ getCollectionInputSchema,
9
+ getGlobalInputSchema,
10
+ } from '../../utils/schemaConversion/getEntityInputSchema.js'
11
+
8
12
  /**
9
13
  * Keep the default `shortcircuit: true` - `false` makes validating nested rich text exponentially slow.
10
14
  */
@@ -34,7 +38,7 @@ export const validateCollectionData = ({
34
38
  }): MCPToolResponse | null =>
35
39
  validateData({
36
40
  slug: collectionSlug,
37
- buildSchema: () => getCollectionInputSchema({ collectionSlug, req }) as JsonSchemaType | null,
41
+ buildSchema: () => getCollectionInputSchema({ collectionSlug, req }),
38
42
  data,
39
43
  entity: 'collection',
40
44
  partial,
@@ -52,7 +56,7 @@ export const validateGlobalData = ({
52
56
  }): MCPToolResponse | null =>
53
57
  validateData({
54
58
  slug: globalSlug,
55
- buildSchema: () => getGlobalInputSchema({ globalSlug, req }) as JsonSchemaType | null,
59
+ buildSchema: () => getGlobalInputSchema({ globalSlug, req }),
56
60
  data,
57
61
  entity: 'global',
58
62
  partial: true,
@@ -0,0 +1,53 @@
1
+ import type { SanitizedConfig } from 'payload'
2
+
3
+ import { fieldIsVirtual } from 'payload/shared'
4
+
5
+ /**
6
+ * Returns the names of all top-level virtual fields for a given collection slug.
7
+ */
8
+ export function getCollectionVirtualFieldNames(config: SanitizedConfig, slug: string): string[] {
9
+ const collection = config.collections.find((c) => c.slug === slug)
10
+
11
+ if (!collection) {
12
+ return []
13
+ }
14
+
15
+ return collection.flattenedFields
16
+ .filter((field) => 'name' in field && fieldIsVirtual(field))
17
+ .map((field) => (field as { name: string }).name)
18
+ }
19
+
20
+ /**
21
+ * Returns the names of all top-level virtual fields for a given global slug.
22
+ */
23
+ export function getGlobalVirtualFieldNames(config: SanitizedConfig, slug: string): string[] {
24
+ const global = config.globals.find((g) => g.slug === slug)
25
+
26
+ if (!global) {
27
+ return []
28
+ }
29
+
30
+ return global.flattenedFields
31
+ .filter((field) => 'name' in field && fieldIsVirtual(field))
32
+ .map((field) => (field as { name: string }).name)
33
+ }
34
+
35
+ /**
36
+ * Strips virtual field values from a data object given a list of virtual field names.
37
+ */
38
+ export function stripVirtualFields(
39
+ data: Record<string, unknown>,
40
+ virtualFieldNames: string[],
41
+ ): Record<string, unknown> {
42
+ if (virtualFieldNames.length === 0) {
43
+ return data
44
+ }
45
+
46
+ const stripped = { ...data }
47
+
48
+ for (const name of virtualFieldNames) {
49
+ delete stripped[name]
50
+ }
51
+
52
+ return stripped
53
+ }
@@ -0,0 +1,123 @@
1
+ import type {
2
+ FlattenedBlock,
3
+ FlattenedField,
4
+ Operation,
5
+ SanitizedDocumentPermissions,
6
+ SanitizedFieldsPermissions,
7
+ } from 'payload'
8
+
9
+ type FieldOperation = Exclude<Operation, 'delete'>
10
+ type AllowedFieldOperations = Record<FieldOperation, boolean>
11
+
12
+ /**
13
+ * Filters fields using the access results in `permissions`. `shouldExcludeField` receives the
14
+ * operations allowed by the entity, field, and its parents and decides whether to remove the
15
+ * field.
16
+ *
17
+ * Arrays, groups, named tabs, and blocks are filtered recursively. Rows and unnamed tabs are
18
+ * flattened by Payload, so their children are filtered at the containing level.
19
+ *
20
+ * @example Include fields writable through any collection operation the user can perform.
21
+ * ```ts
22
+ * filterFieldsByAccess({
23
+ * fields,
24
+ * permissions: collectionPermissions,
25
+ * shouldExcludeField: ({ create, update }) => !create && !update,
26
+ * })
27
+ * ```
28
+ */
29
+ export const filterFieldsByAccess = ({
30
+ blocks,
31
+ fields,
32
+ permissions,
33
+ shouldExcludeField,
34
+ }: {
35
+ blocks?: FlattenedBlock[]
36
+ fields: FlattenedField[]
37
+ permissions: SanitizedDocumentPermissions
38
+ shouldExcludeField: (allowedOperations: AllowedFieldOperations) => boolean
39
+ }): FlattenedField[] => {
40
+ const filterFields = (
41
+ nestedFields: FlattenedField[],
42
+ nestedPermissions: SanitizedFieldsPermissions | undefined,
43
+ parentAllowedOperations: AllowedFieldOperations,
44
+ ): FlattenedField[] => {
45
+ const accessibleFields: FlattenedField[] = []
46
+
47
+ for (const field of nestedFields) {
48
+ const fieldPermissions = nestedPermissions === true ? true : nestedPermissions?.[field.name]
49
+ const isOperationAllowed = (operation: FieldOperation): boolean =>
50
+ parentAllowedOperations[operation] &&
51
+ (fieldPermissions === true || fieldPermissions?.[operation] === true)
52
+ const allowedOperations: AllowedFieldOperations = {
53
+ create: isOperationAllowed('create'),
54
+ read: isOperationAllowed('read'),
55
+ update: isOperationAllowed('update'),
56
+ }
57
+
58
+ if (shouldExcludeField(allowedOperations)) {
59
+ continue
60
+ }
61
+
62
+ if (field.type === 'blocks') {
63
+ const accessibleBlocks: (FlattenedBlock | string)[] = []
64
+
65
+ for (const blockOrReference of field.blocks) {
66
+ const block =
67
+ typeof blockOrReference === 'string'
68
+ ? blocks?.find(({ slug }) => slug === blockOrReference)
69
+ : blockOrReference
70
+
71
+ if (!block) {
72
+ continue
73
+ }
74
+
75
+ const blockPermissions =
76
+ fieldPermissions === true
77
+ ? true
78
+ : fieldPermissions?.blocks === true
79
+ ? true
80
+ : fieldPermissions?.blocks?.[block.slug]
81
+
82
+ accessibleBlocks.push({
83
+ ...block,
84
+ flattenedFields: filterFields(
85
+ block.flattenedFields,
86
+ blockPermissions === true ? true : blockPermissions?.fields,
87
+ allowedOperations,
88
+ ),
89
+ })
90
+ }
91
+
92
+ if (accessibleBlocks.length === 0) {
93
+ continue
94
+ }
95
+
96
+ accessibleFields.push({ ...field, blocks: accessibleBlocks })
97
+ continue
98
+ }
99
+
100
+ if (field.type === 'array' || field.type === 'group' || field.type === 'tab') {
101
+ accessibleFields.push({
102
+ ...field,
103
+ flattenedFields: filterFields(
104
+ field.flattenedFields,
105
+ fieldPermissions === true ? true : fieldPermissions?.fields,
106
+ allowedOperations,
107
+ ),
108
+ })
109
+ continue
110
+ }
111
+
112
+ accessibleFields.push(field)
113
+ }
114
+
115
+ return accessibleFields
116
+ }
117
+
118
+ return filterFields(fields, permissions.fields, {
119
+ create: 'create' in permissions && permissions.create === true,
120
+ read: permissions.read === true,
121
+ update: permissions.update === true,
122
+ })
123
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ type CollectionSlug,
3
+ entityToStandaloneJSONSchema,
4
+ type FlattenedField,
5
+ type GlobalSlug,
6
+ type PayloadRequest,
7
+ type SanitizedCollectionConfig,
8
+ type SanitizedCollectionPermission,
9
+ type SanitizedGlobalConfig,
10
+ type SanitizedGlobalPermission,
11
+ } from 'payload'
12
+
13
+ import type { JsonSchemaType } from '../../types.js'
14
+
15
+ import { filterFieldsByAccess } from './filterFieldsByAccess.js'
16
+ import { sanitizeEntitySchema } from './sanitizeEntitySchema.js'
17
+
18
+ export const getCollectionInputSchema = ({
19
+ collectionSlug,
20
+ permissions,
21
+ req,
22
+ }: {
23
+ collectionSlug: CollectionSlug
24
+ permissions?: SanitizedCollectionPermission
25
+ req: PayloadRequest
26
+ }): JsonSchemaType | null => {
27
+ const collection = req.payload.collections[collectionSlug]?.config
28
+
29
+ if (!collection) {
30
+ return null
31
+ }
32
+
33
+ if (!permissions) {
34
+ return buildEntityInputSchema({ entity: collection, req })
35
+ }
36
+
37
+ const fieldsAllowedByAccess = filterFieldsByAccess({
38
+ blocks: req.payload.config.blocks,
39
+ fields: collection.flattenedFields,
40
+ permissions,
41
+ shouldExcludeField: ({ create, update }) => !create && !update,
42
+ })
43
+
44
+ return buildEntityInputSchema({ entity: collection, fields: fieldsAllowedByAccess, req })
45
+ }
46
+
47
+ export const getGlobalInputSchema = ({
48
+ globalSlug,
49
+ permissions,
50
+ req,
51
+ }: {
52
+ globalSlug: GlobalSlug
53
+ permissions?: SanitizedGlobalPermission
54
+ req: PayloadRequest
55
+ }): JsonSchemaType | null => {
56
+ const global = req.payload.config.globals.find((globalConfig) => globalConfig.slug === globalSlug)
57
+
58
+ if (!global) {
59
+ return null
60
+ }
61
+
62
+ if (!permissions) {
63
+ return buildEntityInputSchema({ entity: global, req })
64
+ }
65
+
66
+ const fieldsAllowedByAccess = filterFieldsByAccess({
67
+ blocks: req.payload.config.blocks,
68
+ fields: global.flattenedFields,
69
+ permissions,
70
+ shouldExcludeField: ({ create, update }) => !create && !update,
71
+ })
72
+
73
+ return buildEntityInputSchema({ entity: global, fields: fieldsAllowedByAccess, req })
74
+ }
75
+
76
+ const buildEntityInputSchema = ({
77
+ entity,
78
+ fields = entity.flattenedFields,
79
+ req,
80
+ }: {
81
+ entity: SanitizedCollectionConfig | SanitizedGlobalConfig
82
+ fields?: FlattenedField[]
83
+ req: PayloadRequest
84
+ }): JsonSchemaType => {
85
+ // The core schema generator reads flattenedFields from the entity and has no fields argument.
86
+ const entityForSchema = { ...entity, flattenedFields: fields }
87
+ const schema = entityToStandaloneJSONSchema({
88
+ config: req.payload.config,
89
+ defaultIDType: req.payload.db.defaultIDType,
90
+ entity: entityForSchema,
91
+ i18n: req.i18n,
92
+ variant: 'input',
93
+ }) as unknown as JsonSchemaType
94
+
95
+ return sanitizeEntitySchema(schema)
96
+ }