MCP client stringifies object parameters for tools with complex schemas
Description
When calling MCP tools that have parameters defined with $ref/oneOf schemas, Claude Code's MCP client incorrectly serializes object parameters as JSON strings instead of passing them as proper objects in the JSON-RPC message.
Reproduction
- Configure the Notion MCP server (
@notionhq/notion-mcp-server) - Use
API-post-searchwith afilterobject parameter → works - Use
API-post-pagewithparentandpropertiesobject parameters → fails
Error received:
body failed validation: body.parent should be an object or `undefined`,
instead was `"{\"database_id\": \"...\"}"`
Proof that the bug is in Claude Code (not the MCP server)
I wrote a test script that sends JSON-RPC messages directly to the Notion MCP server:
// Test 1: Object parameter (works)
arguments: { parent: { database_id: "..." } } // ✅ Page created
// Test 2: Stringified parameter (fails with same error)
arguments: { parent: "{\"database_id\": \"...\"}" } // ❌ Validation error
The MCP server handles object parameters correctly. Claude Code appears to stringify them before sending.
Schema difference
| Tool | Parameter Schema | Result |
|------|------------------|--------|
| API-post-search | filter: {type: "object"} | ✅ Works |
| API-post-page | parent: {$ref: "#/$defs/parentRequest"} (oneOf) | ❌ Stringified |
The issue appears specific to parameters using $ref with oneOf schemas.
Environment
- Claude Code CLI (latest)
- Windows 11
@notionhq/notion-mcp-server@2.0.0
Test script
<details>
<summary>Direct MCP server test (test-notion-mcp.js)</summary>
const { spawn } = require('child_process');
const server = spawn('npx', ['-y', '@notionhq/notion-mcp-server'], {
env: {
...process.env,
OPENAPI_MCP_HEADERS: JSON.stringify({
"Authorization": "Bearer <token>",
"Notion-Version": "2022-06-28"
})
},
shell: true
});
let buffer = '';
server.stdout.on('data', (data) => {
buffer += data.toString();
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (line.trim()) {
try {
console.log('\n[RESPONSE]', JSON.stringify(JSON.parse(line), null, 2));
} catch (e) {
console.log('[RAW]', line);
}
}
}
});
server.stderr.on('data', (data) => console.error('[STDERR]', data.toString()));
setTimeout(() => {
server.stdin.write(JSON.stringify({
jsonrpc: "2.0", id: 1, method: "initialize",
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "test", version: "1.0" } }
}) + '\n');
}, 2000);
setTimeout(() => {
server.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + '\n');
}, 3000);
// Object params - WORKS
setTimeout(() => {
server.stdin.write(JSON.stringify({
jsonrpc: "2.0", id: 2, method: "tools/call",
params: {
name: "API-post-page",
arguments: {
parent: { database_id: "<db-id>" },
properties: { title: [{ text: { content: "Test" } }] }
}
}
}) + '\n');
}, 4000);
// String params - FAILS (simulates Claude Code behavior)
setTimeout(() => {
server.stdin.write(JSON.stringify({
jsonrpc: "2.0", id: 3, method: "tools/call",
params: {
name: "API-post-page",
arguments: {
parent: JSON.stringify({ database_id: "<db-id>" }),
properties: JSON.stringify({ title: [{ text: { content: "Test" } }] })
}
}
}) + '\n');
}, 6000);
setTimeout(() => { server.kill(); process.exit(0); }, 10000);
</details>
Expected behavior
Object parameters should be passed as objects in the JSON-RPC arguments field, not stringified.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗