← writing

From idea to live page in five MCP calls

One sentence into Claude. Five MCP calls under the hood. Three seconds later the page is live. Here is the exact recipe, snippet by snippet, and the two small lessons that keep the flow honest.

I said this into Claude the other afternoon, exactly as you would say it to a colleague who already knows the codebase.

publish a new case study with a hero image and three stats. tagged squilla. mention it on the menu.

Three seconds later the page was live. Five MCP calls happened between the sentence landing and the URL being clickable. No clicking through an admin panel. No tabbing between media library and editor and taxonomy picker and menu builder. Just one sentence, and the CMS got out of the way.

I want to walk through what those five calls were, because once you see the shape of it, you understand why an AI-native CMS feels different from a traditional one. The flow is the product. 🚀

Call 1: get the hero image into the library

The agent cannot reference an image that does not exist yet, so the very first thing it does is upload (or import by URL) the hero. Whichever path it picks, the result is the same. A media object comes back with an id and a url, ready to be embedded into a node.

{
  "tool": "core.media.import_url",
  "args": {
    "url": "https://images.example.com/case-studies/acme-hero.jpg",
    "alt": "Acme dashboard rebuild, before and after"
  }
}

The response is a small object with an id, a url, and a slug. That is the handle the next call will reference. This step is non-negotiable. If you skip it and try to create the node first with a string URL stuffed in, you end up with a broken image and no library entry to manage later.

Call 2: create the case study node

Now the agent has something to point at. It calls core.node.create with the node type set to case_study, the title from the sentence I said, a short blurb in fields_data, the three stats as a structured list, and the hero image as a proper media object (not a bare string, which the schema would reject).

{
  "tool": "core.node.create",
  "args": {
    "node_type": "case_study",
    "language_code": "en",
    "title": "Acme dashboard rebuild",
    "status": "published",
    "fields_data": {
      "blurb": "How we cut Acme's reporting latency from 14 seconds to under 400 ms.",
      "stats": [
        { "label": "latency reduction", "value": "96%" },
        { "label": "weekly active users", "value": "4.2k" },
        { "label": "ship time", "value": "6 weeks" }
      ],
      "hero_image": {
        "id": 812,
        "url": "/storage/media/2026/03/acme-hero.jpg",
        "alt": "Acme dashboard rebuild, before and after"
      }
    }
  }
}

The response includes the new node id and the resolved full_url. From this moment on, the page technically exists. It just is not categorised or linked from anywhere yet.

Call 3: tag it with the squilla category

The sentence asked for the post to be tagged squilla, so the agent attaches the term. In practice this is either a dedicated taxonomy attach call, or a quick core.data.update against the node setting taxonomies under fields_data. Either works. I prefer the data.update path when the taxonomy is already a known field on the schema, because it keeps the round trip to one call.

{
  "tool": "core.data.update",
  "args": {
    "table": "content_nodes",
    "id": 187,
    "patch": {
      "fields_data": {
        "taxonomies": { "category": ["squilla"] }
      }
    }
  }
}

Tagged. Now the case study shows up under the squilla category archive without anyone touching a UI.

Call 4: put it on the menu

Tagging is invisible until someone navigates to the archive, and the sentence specifically asked for a mention on the menu. So the agent calls core.menu.upsert and appends a new item pointing at the node's URL.

{
  "tool": "core.menu.upsert",
  "args": {
    "slug": "primary",
    "append": [
      {
        "label": "Acme rebuild",
        "url": "/case/acme-dashboard-rebuild"
      }
    ]
  }
}

One call. The menu is updated atomically, the cache is invalidated, and the new link appears on every page render from this point on.

Call 5: verify before announcing victory

This is the one I want every agent doing every time. Before saying done, render the node from the server side and confirm the page actually looks like the page.

{
  "tool": "core.render.node_preview",
  "args": { "id": 187 }
}

The response is the rendered HTML, exactly as a browser would receive it. The agent skims it for the hero image, the three stats, the menu containing the new item, and the category footer. If anything is missing, it fixes it before claiming success. If everything is there, it tells me the page is live, with the URL.

The two small lessons

There are two things I want to underline from this flow, because they are easy to miss until they bite you.

The first is the upload always comes before the create. The node is going to embed a media object, and that object needs to exist in the library so it can be referenced by id and url. If you reverse the order, you end up with a node that points at a URL the CMS does not own, which means no thumbnail generation, no alt text propagation, no media library entry to swap out later.

The second is render.node_preview is not optional. An agent should never tell the user a page is live without rendering it once and reading what came back. It is the difference between an agent that hopes and an agent that knows. The render is cheap, the cost of being wrong is high, and the user trusts you more the next time when the first time was right.

What used to take four minutes

This same flow, done by a human in a traditional CMS, looks like this. Open media library, upload, copy URL. Open editor, new entry, paste title, paste blurb, paste URL into hero field, add three stats one row at a time. Open taxonomy panel, select category. Open menu builder, add item, drag to position, save. Reload front end to check. About four minutes of clicking, with three context switches and two opportunities to forget the menu step.

Now it is one sentence and three seconds. That is the whole pitch.

Want more like this?

Occasional, opinionated, no listicles.
all writing →