DEV Community

Cover image for Your MCP server's write tools are silently dropping their body
Mirza Iqbal
Mirza Iqbal

Posted on

Your MCP server's write tools are silently dropping their body

If you have built a Model Context Protocol server with a passthrough tool, the kind that
lets the model call any endpoint, there is a real chance its write operations are quietly
broken in some clients. You may not have noticed, because reads work.

I hit this building an open source MCP server for Hetzner. Listing worked perfectly. Every
create, update, and delete came back from the API with the same complaint.

A valid JSON document is required.
Enter fullscreen mode Exit fullscreen mode

The body was arriving empty. Here is why, and the single line that fixed it.

First, here it is working. This server stands up a load-balanced stack and tears it back
down in plain language, with a cost guard in front of every billed action.

The symptom

A passthrough tool needs a free-form body so the model can send any payload. The obvious
way to type that in Zod looks harmless.

body: z.unknown().optional()
Enter fullscreen mode Exit fullscreen mode

Reads need no body, so every GET sailed through. The moment the model tried a POST, the
body never reached the API.

The root cause

z.unknown() compiles to an empty JSON schema. Not an object, not a string, an empty shape.

Several MCP clients drop a property whose schema is empty. There is nothing to validate and
nothing to send, so the field is stripped before the request leaves the client. Your server
receives the call with the body undefined, forwards an empty payload, and the API rejects it.

The cruel part is that the error comes from the upstream service, not from your server, so
you go hunting in the wrong place for an hour.

The fix

Give the body a real schema. An object is enough, and accepting a JSON string as well makes
it work no matter how a client serializes the payload.

body: z.union([z.record(z.string(), z.unknown()), z.string()]).optional()
Enter fullscreen mode Exit fullscreen mode

Then parse a string body into an object before you use it. The schema is now non-empty, so
clients forward the field, and the string branch covers the clients that send serialized
JSON. That is the whole fix.

How I knew the fix actually held

A schema change you cannot see is a schema change you cannot trust. So I wrote a suite that
drives the compiled server over stdio, the exact artifact a client loads, against the live
API. Reads, the cost and delete guards, and a create then delete for every resource type.
Every billed resource is removed in a finally block, so a failure never leaks a charge.

It caught the bug, proved the fix, and runs on every change now. The number that matters is
not that it compiles, it is that every tool answered the real API and the account was left
clean.

If you build MCP servers

Two things to carry away.

  • Audit any tool that types an input as unknown or leaves it untyped. Give it a real schema.
  • Test write tools through the built server against the real service, not only your unit tests. This failure only appeared at the client boundary.

The server I found this in is open source under MIT. It gives an AI assistant the full
Hetzner platform, Cloud, Storage Box, and Robot dedicated servers.

npx -y hetzner-mcp
Enter fullscreen mode Exit fullscreen mode

What is the most painful silent failure you have hit building MCP tools?

Top comments (0)