Skip to content

nextjs_call fails with 'Expected object, received string' when MCP clients serialize args as JSON string #132

@carlesandres

Description

@carlesandres

Bug Description

The nextjs_call tool fails with a Zod validation error when MCP clients pass the args parameter as a serialized JSON string instead of a parsed object. This is a known pattern across the MCP ecosystem where certain clients serialize object-typed parameters before sending them to the server.

Error:

Expected object, received string

The nextjs_index (action: discover_servers and list_tools) works correctly. Only nextjs_call (action: call_tool) fails because it's the only action that accepts the args parameter typed as z.record(z.string(), z.unknown()).

Steps to Reproduce

  1. Configure next-devtools-mcp as an MCP server in a client that serializes object params as strings (e.g., OpenCode)
  2. Start a Next.js 16+ dev server
  3. Call nextjs_index — works fine, discovers servers and lists tools
  4. Call nextjs_call with any tool that takes arguments (or even without args) — fails with Expected object, received string

Example call that fails:

{
  "action": "call_tool",
  "port": 3000,
  "toolName": "get_errors",
  "args": {}
}

The MCP client sends args as "{}" (a string) instead of {} (an object), and Zod rejects it.

Root Cause

In src/tools/nextjs-runtime.ts, the args parameter is defined as:

args: z
  .record(z.string(), z.unknown())
  .optional()

And in src/index.ts, the parseToolArgs function validates arguments with zodSchema.safeParse(args[key]) but does not preprocess string values that should be objects:

function parseToolArgs(
  schema: Record<string, z.ZodTypeAny>,
  args: Record<string, unknown>
): Record<string, unknown> {
  const result: Record<string, unknown> = {}
  for (const [key, zodSchema] of Object.entries(schema)) {
    if (args[key] !== undefined) {
      const parsed = zodSchema.safeParse(args[key])
      if (parsed.success) {
        result[key] = parsed.data
      } else {
        throw new Error(`Invalid argument '${key}': ${parsed.error.message}`)
      }
    }
    // ...
  }
  return result
}

When an MCP client serializes args as a JSON string ('{"key": "value"}' instead of {"key": "value"}), safeParse receives a string where it expects an object, and Zod correctly rejects it.

Cross-Ecosystem Evidence

This is the same class of bug that has been reported and fixed in other MCP servers:

  1. Notion MCPmakenotion/notion-mcp-server#208: Identical Expected object, received string error on data, new_parent, and parent parameters. Affected Claude Desktop (Cowork mode), Ampcode, and other clients. Fixed with z.preprocess().

  2. MCP TypeScript SDKmodelcontextprotocol/typescript-sdk#400: Related issue where undefined/string arguments caused validation failures for tools with optional parameters. Fixed in Jan 2026 (PR #1404).

Suggested Fix

Add a z.preprocess() step for the args parameter that defensively parses JSON strings into objects:

args: z.preprocess(
  (val) => {
    if (typeof val === 'string') {
      try { return JSON.parse(val); } catch { return val; }
    }
    return val;
  },
  z.record(z.string(), z.unknown())
).optional()

Or alternatively, add preprocessing in the parseToolArgs function for any schema expecting an object:

function parseToolArgs(
  schema: Record<string, z.ZodTypeAny>,
  args: Record<string, unknown>
): Record<string, unknown> {
  const result: Record<string, unknown> = {}
  for (const [key, zodSchema] of Object.entries(schema)) {
    let value = args[key]
    // Defensively parse JSON strings for object-typed params
    if (typeof value === 'string' && zodSchema._def?.typeName === 'ZodRecord') {
      try { value = JSON.parse(value) } catch { /* leave as-is */ }
    }
    if (value !== undefined) {
      const parsed = zodSchema.safeParse(value)
      // ...
    }
  }
  return result
}

This makes the server resilient to client-side serialization inconsistencies without breaking existing functionality — the same approach Notion MCP used successfully.

Environment

  • MCP Server: next-devtools-mcp v0.3.10
  • MCP Client: OpenCode v1.4.0 and v1.4.3
  • Next.js: 16.x (dev server running, MCP endpoint verified working via nextjs_index)
  • OS: macOS

Notes

The tool description already includes the warning "MUST be an object (e.g., {param: 'value'}), NOT a string", which suggests this has been observed before. A defensive preprocess would eliminate the issue regardless of client behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions