# I generated 207 MCP tools from an OpenAPI spec. Generating them was the easy part.

> Source: <https://dev.to/lusrodri/i-generated-207-mcp-tools-from-an-openapi-spec-generating-them-was-the-easy-part-1n8c>
> Published: 2026-07-30 00:23:08+00:00

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](https://github.com/LuSrodri/render-useful-mcp): an MCP server for [Render](https://render.com) 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.

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:

`$ref`

it can't flatten.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.

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.

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, 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.

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.

MCP's [2026-07-28 revision](https://modelcontextprotocol.io/) 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.

```
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](https://render.com/docs/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.
