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
- Configure
next-devtools-mcp as an MCP server in a client that serializes object params as strings (e.g., OpenCode)
- Start a Next.js 16+ dev server
- Call
nextjs_index — works fine, discovers servers and lists tools
- 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:
-
Notion MCP — makenotion/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().
-
MCP TypeScript SDK — modelcontextprotocol/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.
Bug Description
The
nextjs_calltool fails with a Zod validation error when MCP clients pass theargsparameter 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:
The
nextjs_index(action:discover_serversandlist_tools) works correctly. Onlynextjs_call(action:call_tool) fails because it's the only action that accepts theargsparameter typed asz.record(z.string(), z.unknown()).Steps to Reproduce
next-devtools-mcpas an MCP server in a client that serializes object params as strings (e.g., OpenCode)nextjs_index— works fine, discovers servers and lists toolsnextjs_callwith any tool that takes arguments (or even without args) — fails withExpected object, received stringExample call that fails:
{ "action": "call_tool", "port": 3000, "toolName": "get_errors", "args": {} }The MCP client sends
argsas"{}"(a string) instead of{}(an object), and Zod rejects it.Root Cause
In
src/tools/nextjs-runtime.ts, theargsparameter is defined as:And in
src/index.ts, theparseToolArgsfunction validates arguments withzodSchema.safeParse(args[key])but does not preprocess string values that should be objects:When an MCP client serializes
argsas a JSON string ('{"key": "value"}'instead of{"key": "value"}),safeParsereceives 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:
Notion MCP — makenotion/notion-mcp-server#208: Identical
Expected object, received stringerror ondata,new_parent, andparentparameters. Affected Claude Desktop (Cowork mode), Ampcode, and other clients. Fixed withz.preprocess().MCP TypeScript SDK — modelcontextprotocol/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 theargsparameter that defensively parses JSON strings into objects:Or alternatively, add preprocessing in the
parseToolArgsfunction for any schema expecting an object:This makes the server resilient to client-side serialization inconsistencies without breaking existing functionality — the same approach Notion MCP used successfully.
Environment
next-devtools-mcpv0.3.10nextjs_index)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.