The Problem with Hand-Written API Wrappers

Most MCP servers that wrap a third-party REST API follow the same pattern: pick fifteen or twenty endpoints, hand-write a Zod schema for each, and ship it. That works for about three months. Then the API adds a field, deprecates an enum value, or renames a query parameter. The wrapper doesn't notice because nothing connects it to the API's own description of itself. Schemas drift, and the model starts getting rejected by the upstream API for reasons it can't see.

There's a quieter failure too: you need endpoint twenty-one—the one about persistent disks or scaling—and the author skipped it. Now you're back to curl, with the agent holding half the context and you holding the other half.

The Solution: Generate Everything, Fail Loudly

The author of render-useful-mcp built a different kind of server. It generates a tool for every single endpoint in Render's OpenAPI document—all 207, no curation. The generating part took a weekend. Everything after that was the actual work.

The key design decision was to make the generator fail-closed. The tempting path is to skip what you can't parse, fall back to { type: "object" } when a $ref gets hairy, and log a warning. You get 207 tools on the first run and feel great. But 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 aborts the build, loudly, on:

  • An operation whose tag doesn't map to a known toolset.
  • A tool name collision.
  • A cyclic $ref it can't flatten.
  • A path parameter that doesn't appear in the URL template, or appears in the template but isn't declared.

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: Render's own constraints reach the model intact. Enums stay enums, string patterns stay patterns, required fields stay required. The model gets rejected by the 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 equals 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.

The fix: 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, 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 also RENDER_MCP_READ_ONLY=true, which drops every mutating operation—the right default for anything you'd point at production.

The interesting constraint: this selection is read once, at startup, and never changes. We'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 as srv-d1a2b3c4e5f6g7h8i9j0. Fuzzy-matches by name and returns close alternatives when there's no exact hit.
  • render_wait_for_deploy — blocks until a deploy reaches a terminal state, so the agent stops re-polling in a loop.
  • render_service_status — one call that answers "is this thing healthy?" by combining service detail, latest deploy, and recent error logs. The author's most-used tool.
  • render_recent_logs — the log query you actually want, without constructing a filter object by hand.

This is the split the author defends 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. If something changed, it opens a PR with the diff and a summary—and flags whether the change is breaking (removed operations or tightened constraints) versus additive.

Small detail with a lesson: 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. 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 this one, 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. The server 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. This one covers the operational surface outside that: scaling, disks, blueprints, environment groups, custom domains, webhooks, maintenance, workflows. The author is not affiliated with Render.

MIT. Issues and PRs welcome, particularly from anyone who has fought the tool-count problem and solved it differently—static toolsets aren't the last word on it. github.com/LuSrodri/render-useful-mcp