Skip to content

Commit 7612b00

Browse files
committed
fix(mcp): handle JSON-serialized object parameters from MCP clients
Some MCP clients serialize complex object parameters (filter, sort) as JSON strings rather than native objects. Add a z.preprocess wrapper that transparently parses JSON strings before Zod validation, keeping all existing schema validation intact. Closes #5610
1 parent d7d8b4b commit 7612b00

File tree

1 file changed

+18
-3
lines changed

1 file changed

+18
-3
lines changed

helicone-mcp/src/index.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ import { z } from "zod";
55
import { fetchRequests, fetchSessions, useAiGateway } from "./lib/helicone-client.js";
66
import { requestFilterNodeSchema, sortLeafRequestSchema, sessionFilterNodeSchema } from "./types/generated-zod.js";
77

8+
/**
9+
* Wraps a Zod schema with a preprocessing step that parses JSON strings.
10+
* Some MCP clients (e.g. Claude Code) serialize complex object parameters
11+
* as JSON strings rather than native objects. This ensures validation
12+
* still works regardless of how the client sends the data.
13+
*/
14+
function jsonPreprocess<T extends z.ZodTypeAny>(schema: T) {
15+
return z.preprocess((val) => {
16+
if (typeof val === "string" && val !== "all") {
17+
try { return JSON.parse(val); } catch { return val; }
18+
}
19+
return val;
20+
}, schema);
21+
}
22+
823
const HELICONE_API_KEY = process.env.HELICONE_API_KEY;
924

1025
if (!HELICONE_API_KEY) {
@@ -21,10 +36,10 @@ server.tool(
2136
"query_requests",
2237
"Query Helicone requests with filters, pagination, sorting, and optional body content",
2338
{
24-
filter: requestFilterNodeSchema.optional().describe("Filter criteria for requests"),
39+
filter: jsonPreprocess(requestFilterNodeSchema).optional().describe("Filter criteria for requests"),
2540
offset: z.number().optional().describe("Pagination offset (default: 0)"),
2641
limit: z.number().optional().describe("Maximum number of results to return (default: 100)"),
27-
sort: sortLeafRequestSchema.optional().describe("Sort criteria"),
42+
sort: jsonPreprocess(sortLeafRequestSchema).optional().describe("Sort criteria"),
2843
includeBodies: z.boolean().optional().describe("Fetch and include request/response bodies (default: false). If true, fetches content from signed URLs."),
2944
},
3045
async (params: any) => {
@@ -70,7 +85,7 @@ server.tool(
7085
endTimeUnixMs: z.number().describe("End time for session query (Unix timestamp in milliseconds)"),
7186
nameEquals: z.string().optional().describe("Filter sessions by exact name match"),
7287
timezoneDifference: z.number().describe("Timezone difference in hours (e.g., -5 for EST)"),
73-
filter: sessionFilterNodeSchema.optional().describe("Advanced filter criteria"),
88+
filter: jsonPreprocess(sessionFilterNodeSchema).optional().describe("Advanced filter criteria"),
7489
offset: z.number().optional().describe("Pagination offset (default: 0)"),
7590
limit: z.number().optional().describe("Maximum number of results to return (default: 100)"),
7691
},

0 commit comments

Comments
 (0)