Build1 publisher2 min readPublished
One MCP server needs a generated config file for every desktop client it serves
Cursor, Claude Desktop, Windsurf, OpenClaw and Hermes each expect a different JSON config for the same MCP server, according to a dev.to guide. Its fix generates each file from code and adds two server-side shims for requests a config file cannot correct.
The Engineer · Build desk
What happened
- The differing keys the guide lists are type versus transport, url versus serverUrl, and mcpServers versus mcp.servers.
- One function, mcpStreamableUpstreamUrl, trims a trailing slash and returns <root>/mcp/, so every client config and proxy hop resolves the same upstream address.
- Cursor sends "params": [] for tools/list and notifications/initialized, so the server rewrites non-object params to an empty object before pydantic validates them.
- A one-time, idempotent monkey-patch of the session layer gives clients that never send initialize a stateless session.
- The proxy forwards Authorization, Content-Type, the platform header and the trace id, and fills in a default platform instead of failing a request.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision A server that accepts clients without the initialize handshake never agrees a version with them, so the pinned protocol version has to be recorded next to the generated configs.
- exposure Requests that arrive without the platform header are still served, so the audit trail files them under a default platform instead of the agent that actually sent them.
- constraint A blanket rewrite of non-object params to an empty object would also erase a non-empty list, and the per-method log line would be the only record that it happened.
The Model Context Protocol specification covers the wire and stops there. Over a network it uses Streamable HTTP. The client POSTs JSON-RPC to a single endpoint, and the server answers with a JSON object or a stream [5]. The lifecycle adds an initialize handshake with capability negotiation and version agreement [6]. According to the post, where a client stores the endpoint, and whether it calls the field url or serverUrl, are product decisions each client team makes on its own schedule [7]. The post's summary of the outcome is that "MCP-compatible" describes "the wire format, not the paste-ability" [8].
The fix it proposes is to treat the client file as build output. One function per platform emits the file, one builds the auth headers, and one place decides the upstream URL [4]. A base-URL change then becomes one edit instead of five pasted files [3]. I'd make the same call for any server shipped to more than two clients. The post puts the overall split at "20% protocol and 80% edge-case handling" [15].
Most of that 80% is in the shape differences. The post counts "four differences that each cost an afternoon" and then names three pairs: type or transport, url or serverUrl, mcpServers or mcp.servers [2][16]. The excerpt does not say which client expects which key.
Cursor's empty-list params are a different kind of problem. They are a wire behaviour, and no generated file changes what Cursor sends [9]. The normaliser has to sit in the server, ahead of pydantic, and it logs each method it rewrites [9].
The session patch is the piece I would review hardest [10]. It applies once and is idempotent. That is correct for a patch that more than one code path might trigger. The cost is somewhere else. A monkey-patch depends on the SDK's internal session code staying the same. In my view, that means rerunning the clients that skip initialize after every SDK upgrade, before the upgrade ships.
The auth headers exist for audit. buildMcpAuthHeaders emits the API key and an agent platform header because, the post says, "a gateway that cannot attribute the caller cannot audit it" [12]. The post attaches a condition to its audit claim. Per-agent audit "stops being a guess" once every client arrives through generated configuration and the platform header travels with each request [14].
What to watch
- A published mapping of which client expects serverUrl, transport or mcp.servers, from the client vendors or the guide's author.
- An MCP SDK release that changes the session code apply_mcp_session_compat patches.
- Whether Cursor starts sending object-shaped params for tools/list; if it does, the normalisation shim becomes dead code.