Skip to content

LLM-client compatibility: observations on resource stringification with the hosted MCP #44

@andybywire

Description

@andybywire

What we're seeing

We're using the hosted Sanity MCP at https://mcp.sanity.io from a Claude Code skill that drives content-team workflows. We've been hitting a recurring failure on the first Sanity:* tool call per session: the LLM serializes the resource parameter as a JSON-encoded string rather than an object literal, and the server's validator rejects it.

Wanted to share the observation and a thought on possible mitigations, in case it's useful.

This is probably more of a DX observation rather than a bug — the server's current behavior is correct per its documented schema. But we're seeing it often enough that we thought it was worth flagging.

What the failure looks like

A Claude-driven call to get_schema went out like this:

{
  "workspaceName": "administration",
  "resource": "{\"projectId\": \"5abcdefg\", \"dataset\": \"production\"}",
  "type": "resource"
}

Note resource's value is a string literal, not an object. Server response:

MCP error -32602: Input validation error: Invalid arguments for tool get_schema: [
  {
    "code": "invalid_type",
    "expected": "object",
    "received": "string",
    "path": ["resource"],
    "message": "Expected object, received string"
  }
]

We've seen the same thing anywhere resource: { projectId, dataset } is required. The params argument on query_documents hits the same pattern.

What we've tried

From our side, we've tried mitigating in the prompt:

  • Explicit instructions stating the expected shape ("resource is a JSON object, not a stringified JSON")
  • A dedicated "common mistakes" section in the skill
  • Preloading the tool schema via the Claude harness's ToolSearch so validation fires on the way out

None of these fully eliminate it. Our read is that the stringification is coming from the LLM's internal serialization layer, not from anything the prompt can deterministically control.

A thought on server-side mitigation

One pattern our debugging suggests is a lenient preprocessor that accepts either shape and parses the string form before validation — something like:

const resourceSchema = z.preprocess(
  (val) => {
    if (typeof val === "string") {
      try { return JSON.parse(val); } catch { return val; }
    }
    return val;
  },
  z.object({
    projectId: z.string(),
    dataset: z.string(),
  }),
);

This preserves strict validation for well-behaved clients while being forgiving of the stringification glitch. Just a thought — completely understand there may be reasons not to go this direction.

Error messaging

The error message is technically correct but doesn't give the LLM much to self-correct against. Something like "Expected object, received string. If you intended a JSON object, check that it isn't wrapped in quotes." might shorten the retry loop even if the strict validation stays in place.

Impact

For context on why we noticed this: our users are non-technical clinical content authors, and they hit this failure on the first Sanity:* call in most sessions. Each retry loop costs ~30–60s and surfaces a validation error in the UI. Not a blocker — the skill recovers — but it's consistent enough that we wanted to flag it.

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