Every MCP server I've read that wraps a third-party REST API has the same shape: someone picked fifteen or twenty endpoints that seemed useful, hand-wrote a Zod schema for each, and shipped it.
That works for about three months.
Then the API adds a field, deprecates an enum value, renames a query parameter. The wrapper doesn't notice, because nothing in it is connected to the API's own description of itself. The schemas drift. The model starts getting rejected by the upstream API for reasons it can't see, and you get to debug an LLM guessing at a shape that stopped being true in February.
The other failure is quieter. You need endpoint twenty-one — the one about persistent disks, or scaling, or environment groups — and the author skipped it. Now you're back to curl, except the agent is holding half the context and you're holding the other half.
I wanted neither, so I built render-useful-mcp: an MCP server for Render where every API tool is generated from Render's own OpenAPI document. All 207 endpoints, no curation.
The generating part took a weekend. Everything after that was the actual work.
Make the generator refuse to guess
The tempting way to write a spec-to-tools generator is to make it forgiving. Skip what you can't parse, fall back to { type: "object" } when a $ref gets hairy, log a warning and move on. You get 207 tools on the first run and feel great.
You also get tools that lie. A parameter typed as a free-form object when the API actually wants one of four enum values is worse than no tool at all — the model will confidently produce garbage, and the failure surfaces three layers away from the cause.
So the generator is fail-closed. It aborts the build, loudly, on:
- An operation whose tag doesn't map to a known toolset. Render added a resource category and I haven't classified it yet. That's my problem to solve, not something to paper over.
- A tool name collision. Two operations deriving the same name means my naming scheme is wrong.
-
A cyclic
$refit can't flatten. JSON Schema handles recursion; the "just inline everything" shortcut doesn't. Better to know. - A path parameter that doesn't appear in the URL template, or appears in the template but isn't declared. Either direction means I'd build a URL wrong at runtime.
CI runs the generator. A spec change that the generator can't handle correctly fails the build instead of shipping a subtly broken tool. That's the entire value proposition — a hand-written wrapper cannot have this property, because there's nothing to check it against.
The payoff is that Render's own constraints reach the model intact. Enums stay enums. String patterns stay patterns. Required fields stay required. The model gets rejected by my schema, locally, with a readable message, instead of by Render's API after a round trip.
The problem nobody warns you about
207 API tools plus 4 workflow tools plus a meta-tool is 212 entries in tools/list.
You cannot ship that.
Every tool definition — name, description, full input schema — goes into the model's context on every single turn. 212 of them is tens of thousands of tokens spent before the user has typed anything. Worse than the cost is the dilution: a model choosing between 212 near-neighbours picks wrong far more often than one choosing between fifteen. Complete coverage, delivered naively, makes the server less useful than the curated subset I was trying to beat.
The fix is to separate what the server can do from what it exposes.
The 207 tools are grouped into 17 toolsets by the tag on their originating operation: services, postgres, disks, blueprints, logs, metrics, webhooks, network, maintenance, workflows, and so on. Everything is on by default, because a server that hides capability by default is a server that fails silently for the person who needed the hidden thing. But one environment variable narrows it:
RENDER_MCP_TOOLSETS=services,logs,metrics
Now the model sees a few dozen tools instead of 212, chosen by you, for the job you're actually doing. There's a RENDER_MCP_READ_ONLY=true on top of that, which drops every mutating operation — the right default for anything you'd point at production.
The interesting constraint is that this selection is read once, at startup, and never changes. I'll come back to why.
Four tools that aren't in the spec
Generated tools are a faithful mirror of the API, which means they inherit the API's ergonomics. Some sequences that are one thought for a human are four calls for an agent, and agents are bad at four-call sequences.
So there are four hand-written tools on top:
-
render_find_service— you know the service as "the api", Render knows it assrv-d1a2b3c4e5f6g7h8i9j0. Fuzzy-matches by name and returns close alternatives when there's no exact hit, instead of failing. -
render_wait_for_deploy— blocks until a deploy reaches a terminal state, so the agent stops re-polling in a loop and burning turns. -
render_service_status— one call that answers "is this thing healthy?" by combining service detail, latest deploy, and recent error logs. This is the tool I use most. -
render_recent_logs— the log query you actually want, without constructing a filter object by hand.
This is the split I'd defend generally: generate the surface, hand-write the intent. The spec knows what the API can do. Only you know what people are trying to accomplish with it.
Keeping up with an API you don't control
A GitHub Action runs weekly. It fetches Render's current OpenAPI document, regenerates the catalogue, and diffs the result against what's committed.
If the tool set is unchanged, it exits quietly. No noise. If something changed, it opens a PR with the diff and a summary — and flags whether the change is breaking, meaning removed operations or tightened constraints, versus additive.
Small detail with a lesson in it: Render's OpenAPI document isn't served from a stable URL. The documented .json and .yaml endpoints 404. So the fetcher extracts the spec from the docs HTML and validates the extracted result before handing it to the generator, because scraping something structural without validating it is just a slower way to be wrong.
If you're building anything similar against an API whose spec isn't formally published: assume the retrieval path will break, and make it fail loudly when it does.
The 2026-07-28 rewrite
MCP's 2026-07-28 revision turned the protocol's core stateless — request/response, no session handshake, no long-lived connection. Great for anyone deploying to serverless. For a stdio server like mine, the migration was smaller than expected but forced one real design decision.
Under the new model, list endpoints don't vary per connection. tools/list responses carry cache hints — a TTL and a scope — so clients can treat the tool list as a cacheable resource rather than re-fetching it every turn. Mine ships a 5-minute TTL.
That's fundamentally incompatible with a tool list that mutates mid-session. The earlier design let the model call render_toolsets to enable a toolset at runtime, emitting notifications/tools/list_changed so new tools appeared immediately. Clever, and now wrong: you cannot advertise a list as cacheable and then change it underneath the cache.
So the enabled set became immutable for the process lifetime, and render_toolsets was demoted to reporting — it tells you which toolsets exist and which are on, and tells you to change the env var and restart. Less magical, correct, and honestly easier to reason about. The server declares no listChanged capability at all, and a test asserts that.
The general lesson: caching semantics and runtime mutability are the same design decision wearing two hats. Pick one.
Try it
npx render-useful-mcp
It needs RENDER_API_KEY. It's on npm and in the MCP Registry, published with OIDC trusted publishing and provenance attestation — no long-lived npm token exists to steal. There's a Claude Code plugin in the repo. No telemetry, no analytics, no backend; the process contacts exactly one host, and your key is never written to disk and is redacted from log output.
Positioning, honestly: Render maintains an official MCP server, hosted, with OAuth, and if it covers what you need you should use it. Their docs are upfront about the boundaries — creation for a subset of service types, and for existing resources only deploys and environment variables. Mine covers the operational surface outside that: scaling, disks, blueprints, environment groups, custom domains, webhooks, maintenance, workflows. I'm not affiliated with Render.
MIT. Issues and PRs welcome, particularly from anyone who has fought the tool-count problem and solved it differently — I don't think static toolsets are the last word on it.
Top comments (1)
“Generate the surface, hand-write the intent” is the right split. One additional artifact I’d generate is a machine-reviewable capability manifest beside the schemas: operationId, source-spec digest, HTTP method/path, required OAuth scopes, side-effect/risk class, idempotency support, pagination model, sensitive input/output fields, and whether the response can contain credentials or logs. OpenAPI often tells you part of this, but it rarely captures the real approval boundary; ambiguous operations should fail into an explicit review queue rather than inherit “mutating” as one broad category.
That manifest also gives you stronger release tests. Replay representative valid/invalid requests against a mock or sandbox, snapshot normalized error/result envelopes, and assert that read-only mode removes not just POST/PUT/DELETE but any GET endpoint with an operational side effect. I’d test
oneOf/discriminators, nullable-vs-optional fields, style/explode query encoding, defaults, 204 responses, binary payloads, and pagination links because generators commonly preserve the JSON Schema while still constructing the wrong wire request. For the cached immutable catalogue, bind the tools/list digest to the enabled toolsets, spec version, auth-scope mapping, and risk-policy version; then an audit record can prove exactly which generated contract authorized a call.