# The same AI build went from 30 minutes to 4 — here's what we changed in our MCP server

> Source: <https://dev.to/neletomartin/the-same-ai-build-went-from-30-minutes-to-4-heres-what-we-changed-in-our-mcp-server-2l8e>
> Published: 2026-09-08 08:00:00+00:00

We build a lot of sites by prompt. It's the whole point of shipping a CMS with a [native MCP server](https://neleto.io/docs/developer/mcp): you point Claude at a finished HTML page, and it comes back as a component-driven site with every headline, link and image editable.

So we did what you should always do with your own product — we sat down and timed it.

Our original build recipe took around **30 minutes** for a single landing page. We rewrote it, cut the redundant steps, and got that to **14 minutes**. Good progress. But then we looked at where those 14 minutes actually went, and the answer was uncomfortable.

The site itself — fifteen components, a layout, a page, all wired up — took **3 minutes 40 seconds.** The remaining ten minutes were five images. Fourteen kilobytes of them.

Five small images cost nearly three times more than the entire rest of the site. That's not a slow step you optimise. That's a design flaw, and no amount of prompt tuning was going to fix it — it had to be fixed in the server.

The obvious theory is network or storage. It was neither — it was the AI.

When an agent uploads a file through a tool call, the bytes travel as base64 text — and that text has to be *generated by the model, token by token*. A 5 KB image becomes roughly 7,000 characters the model has to type out, one piece at a time. The CMS wasn't slow. The upload was slow because a language model was laboriously spelling out a picture.

It was also fragile in a way that's genuinely hard to debug. One of our test files failed twice with a padding error. The cause turned out to be a long run of repeated characters inside the base64 getting collapsed somewhere in transit — the server received 1,621 characters where 1,624 were sent. It doesn't fail loudly at the transport layer; it shows up much later as a corrupt-file error that points you at entirely the wrong problem.

The second issue was quieter but bigger in aggregate: **our API was too chatty.** Every write returned the full saved record. Save a page with eleven elements and you got back every element's complete component definition — template, CSS, JavaScript, form schema — repeated once per element. That's about 55 KB of JSON echoing back content the caller had just sent. Multiply across a whole build and the majority of the token cost wasn't instructions. It was us talking to ourselves.

Two changes, both at the protocol level.

**1. Images can now be ingested inline.** Instead of uploading a file and then wiring its ID into your content, you put the URL directly in the element data:

```
"data": {
  "image": { "sourceUrl": "https://example.com/studio.jpg" }
}
```

The server fetches it, stores it, and substitutes the real file ID — inside the same save, on the same transaction. No base64. No separate upload step. The bytes never pass through the model at all.

**2. Responses can be quiet.** Every write tool now takes an optional `verbose: false`:

```
{ "id": 53 }
```

That's the whole response now, instead of three kilobytes. For pages and layouts you still get back the element ID mapping — the one thing the caller genuinely can't work out for itself — and nothing else. It defaults to `true`, so nothing breaks for anyone already calling it.

Same page, same components, same five images, three points in the journey:

|  | Original recipe | Rewritten recipe | After the server changes | 
|---|---|---|---|
| Full build with images | ~30 min | ~14 min | **4 min 0 s** | 
| Image phase | ~10 min | 10 min 25 s | **0 s** — folded into the page save | 
| Tokens | — | ~225,000 | **~40,000** | 
| Tool calls | — | 21 + uploads | **19** | 

Worth separating those two jumps honestly, because they're different kinds of work.

Getting from 30 to 14 minutes was **prompt engineering** — fewer redundant calls, better use of defaults, dropping steps that existed out of superstition. Useful, but we'd taken it about as far as it went.

Getting from 14 to 4 was **protocol work**, and it's the bigger result: roughly **3.5× faster and 6× cheaper** on an identical build. The saving comes from deleting a phase rather than speeding one up. Because a page can now be created *with* its elements *and* resolve image URLs in one call, the sequence went from "create empty page → upload five files → write everything back" to a single save.

Which is the general lesson for anyone building agent tooling: once you've tidied the prompt, the remaining cost is almost always in the API's shape, not the agent's instructions.

**Your agent runs cost less.** Token usage on a standard page build dropped by roughly 85%. If you're running builds repeatedly — client sites, templates, a course, a test suite — that's the difference between an experiment and something you do routinely.

**There's one less way to break your content.** Our element data is replace-semantics: send a partial update and the omitted values are cleared. The old image workflow forced a second write pass over content you'd already saved, which is exactly the situation where that bites. We managed to blank an entire page's text this way during testing — every section still rendered, just empty. With inline ingest there is no second pass, so the trap is gone. (It's the same class of silent failure we built [self-verifying checks](https://neleto.io/blog/self-verifying-ai) to catch — deleting the failure mode outright is even better than catching it.)

**It composes with everything else.** `sourceUrl` accepts any URL the server can reach: a client's existing media library, a CDN, a generated image, or a file already in Neleto. For an agency migrating a site, that means images can move over in the same call that creates the page.

A few edges worth knowing before you hit them.

Inline ingest needs a URL the *server* can reach. If your images only exist on your laptop, you still need to get them somewhere hosted first — the base64 path still exists for small files, and it's fine at a few KB. It's large payloads where it falls apart.

There's also a rough edge we found in our own API while testing this: a file's `serveUrl` returns a 400 if you fetch it bare, without a scaling query string appended. Which means the most obvious thing you'd try — ingesting from a file already in your Neleto media library — fails unless you add `?options=w_800,f_webp`. We're fixing it. Until then, append the options.

And one lesson that isn't about images at all. The inline ingest worked the instant it deployed, because element data is free-form — no contract change. The `verbose` flag needed a schema change, and schemas are negotiated once when a client connects, so it stayed invisible to our own editor until we reconnected. If you build MCP tooling: behaviour-level changes reach agents immediately, schema-level ones need a client round-trip. Plan your rollouts accordingly.

Both changes are live. If you're already connected to the Neleto MCP server, inline ingest works right now — put a URL where a file ID goes. For `verbose`, reconnect your client so it picks up the updated schema.

If you haven't built a site by prompt yet, this is a decent moment. The slowest, most fragile part of the workflow just stopped existing.

**Try it yourself:** [start a free Neleto project](https://neleto.io/pricing), point an agent at it, and drop an image URL straight into the element data — watch the whole upload phase disappear. [That's what a native MCP server is for](https://neleto.io/features), and if you want the deep dive on how it works, [we wrote one](https://neleto.io/blog/mcp-server-explained).

**Fast websites. Easy content. AI native.**
