Skip to content

plugin-mcp: schema eval returns "use strict" → convertedFields.partial is not a function when registering update tools #16339

@bruno3du

Description

@bruno3du

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):

  1. json-schema-to-zod produces a small snippet of Zod code as a string.
  2. That string is passed through typescript.transpileModule with module: ts.ModuleKind.CommonJS.
  3. The transpiled output is prefixed with a prologue, e.g. "use strict";\n.
  4. The code then does new Function('z', \return ${transpileResult.outputText})(z).
  5. 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

  1. Enable @payloadcms/plugin-mcp for a collection with update: true on the MCP API key.
  2. Hit the MCP endpoint so tools register.
  3. 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().

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions