Describe the Bug
Package / area
@payloadcms/plugin-mcp (packages/plugin-mcp in this monorepo)
Summary
When MCP registers collection tools (e.g. Update {collection}), updateResourceTool calls convertCollectionSchemaToZod(schema) and then convertedFields.partial(). In practice convertedFields is sometimes not a ZodObject (e.g. the string "use strict"), so registration throws:
TypeError: convertedFields.partial is not a function
Wrapped as: Error registering tools for collection posts: ...
Root cause
In convertCollectionSchemaToZod (src/utils/schemaConversion/convertCollectionSchemaToZod.ts):
- json-schema-to-zod produces a small snippet of Zod code as a string.
- That string is passed through typescript.transpileModule with module: ts.ModuleKind.CommonJS.
- The transpiled output is prefixed with a prologue, e.g. "use strict";\n.
- The code then does new Function('z', \return ${transpileResult.outputText}
)(z).
- Because the transpiled text starts with "use strict";, the generated function is effectively:
return "use strict";
// … rest is dead code
So the “schema” returned is the string "use strict", not a Zod schema. Strings have no .partial(), hence the error when building updateResourceSchema.
This is easy to reproduce with any minimal object schema and the current transpileModule + return concatenation pattern.
Secondary issue
On failure, the catch path returns z.record(z.any()). ZodRecord does not implement .partial() like ZodObject, so even the intentional fallback would still break updateResourceTool if the try path threw.
Suggested fix
Strip the "use strict" prologue from transpileResult.outputText (and normalize to a single expression) before new Function('z', 'return …').
Optionally change the fallback to something that supports .partial() / .shape as used by callers (e.g. z.object({}).passthrough()), or branch in updateResourceTool / createResourceTool when the converted value is not a ZodObject.
Link to the code that reproduces this issue
https://github.com/payloadcms/payload/blob/main/packages/plugin-mcp/src/utils/schemaConversion/convertCollectionSchemaToZod.ts#L39
Reproduction Steps
- Enable @payloadcms/plugin-mcp for a collection with update: true on the MCP API key.
- Hit the MCP endpoint so tools register.
- Observe failure when registering the update tool for that collection.
Minimal isolated repro (same pipeline as the plugin):
import { jsonSchemaToZod } from 'json-schema-to-zod'
import * as ts from 'typescript'
import { z } from 'zod'
const zodSchemaAsString = jsonSchemaToZod({
type: 'object',
properties: { title: { type: 'string' } },
})
const tr = ts.transpileModule(zodSchemaAsString, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2018,
},
})
const fn = new Function('z', 'return ' + tr.outputText)
const r = fn(z)
// r is the string "use strict", not a ZodObject
Which area(s) are affected?
plugin: mcp
Environment Info
Environment
@payloadcms/plugin-mcp: 3.82.x (and likely same code path on current main)
typescript: 6.x (behavior may match older TS as well for this prologue)
json-schema-to-zod: as declared by the plugin
Workaround used downstream
Strip "use strict"; from transpiled output before eval, and use a ZodObject-compatible fallback instead of z.record for callers that call .partial().
Describe the Bug
Package / area
@payloadcms/plugin-mcp (packages/plugin-mcp in this monorepo)
Summary
When MCP registers collection tools (e.g. Update {collection}), updateResourceTool calls convertCollectionSchemaToZod(schema) and then convertedFields.partial(). In practice convertedFields is sometimes not a ZodObject (e.g. the string "use strict"), so registration throws:
TypeError: convertedFields.partial is not a function
Wrapped as: Error registering tools for collection posts: ...
Root cause
In
convertCollectionSchemaToZod(src/utils/schemaConversion/convertCollectionSchemaToZod.ts):)(z).So the “schema” returned is the string "use strict", not a Zod schema. Strings have no .partial(), hence the error when building updateResourceSchema.
This is easy to reproduce with any minimal object schema and the current transpileModule + return concatenation pattern.
Secondary issue
On failure, the catch path returns z.record(z.any()). ZodRecord does not implement .partial() like ZodObject, so even the intentional fallback would still break updateResourceTool if the try path threw.
Suggested fix
Strip the "use strict" prologue from transpileResult.outputText (and normalize to a single expression) before new Function('z', 'return …').
Optionally change the fallback to something that supports .partial() / .shape as used by callers (e.g. z.object({}).passthrough()), or branch in updateResourceTool / createResourceTool when the converted value is not a ZodObject.
Link to the code that reproduces this issue
https://github.com/payloadcms/payload/blob/main/packages/plugin-mcp/src/utils/schemaConversion/convertCollectionSchemaToZod.ts#L39
Reproduction Steps
Minimal isolated repro (same pipeline as the plugin):
Which area(s) are affected?
plugin: mcp
Environment Info