DEV Community

Cover image for Curate a CMS API into 7 Governed Agent Skills with NodeJS
Nisa Fatima
Nisa Fatima

Posted on

Curate a CMS API into 7 Governed Agent Skills with NodeJS

A production CMS is a sprawl of endpoints: content types, entries, media, users, webhooks, plugins, settings, admin routes. Hand an agent all of it and the agent gets worse, not better. The model's tool selection drifts as the list grows, and half the tools are things a publishing assistant should never be able to call.

The point of this post is the opposite move. Instead of exposing an API and hoping the agent behaves, you curate a small, labeled surface up front. HazelJS Skillgate does that curation from an OpenAPI spec, and that is the part we actually build and run here.

Scope, up front

This post is about curation and classification: taking a spec with many endpoints and turning a chosen slice of it into governed skills. Skillgate selects the surface, marks read versus write, and would deny destructive methods if they ever entered that surface.

Turning a write's approval flag into a real human-approval pause, and enabling an LLM to drive the skills, are runtime concerns handled elsewhere in Agent OS. This demo does not implement them, and this post does not claim it does. What it does show is the curation, and that stands on its own.

The tool-explosion problem

Point an LLM at a full CMS API and you hit four problems at once: tool selection degrades as options pile up, throughput drops while the model reasons over a long list, you lose visibility into what the agent can actually do, and dangerous operations sit one bad call away.

The demo spec here is deliberately smaller than a real CMS, 27 endpoints rather than hundreds, but the problem is identical. Even 27 is too many, and most of them are things a publishing agent has no business touching.

From REST endpoint to agent skill

Skillgate's input is an ordinary REST API described by an OpenAPI spec: the same entries, media, and user routes a CMS already exposes. Each endpoint is described in the standard OpenAPI shape, a method, a path, parameters, a description, and tags. Two representative operations from the spec look like this:

[
  {
    "operationId": "listEntries",
    "summary": "List entries",
    "method": "get",
    "path": "/api/entries",
    "tags": ["entries", "read-only", "editorial"]
  },
  {
    "operationId": "publishEntry",
    "summary": "Publish entry",
    "method": "post",
    "path": "/api/entries/{entryId}/publish",
    "tags": ["entries", "write", "editorial", "approval-required"]
  }
]
Enter fullscreen mode Exit fullscreen mode

Skillgate reads each operation and generates one skill from it. The two skills it produced for those endpoints are:

[
  { "name": "listEntries", "method": "GET", "class": "read", "readOnly": true, "requiresApproval": false },
  { "name": "publishEntry", "method": "POST", "class": "write", "readOnly": false, "requiresApproval": true }
]
Enter fullscreen mode Exit fullscreen mode

The mapping is direct:

  • The operation name and description become the skill's name and description. That text is what a model reads to decide when to reach for the skill.
  • Any parameters the endpoint declares become the skill's typed inputs. This demo spec keeps them minimal, but a fuller spec's path, query, and body parameters map through the same way.
  • The HTTP method decides the class. GET becomes read, POST, PUT, and PATCH become write and pick up the approval flag, and DELETE is destructive.
  • The tags decide inclusion. Only operations tagged editorial pass the opt-in filter and become skills at all.
  • The invoke config from the next section (base URL and headers) is how a skill runs. Calling it makes the real HTTP request to that endpoint and returns the response.

So "turning a CMS API into an agent" is concretely this: for each editorial-tagged endpoint, one tool with inputs mirrored from the spec, a risk class assigned from the method, and a live HTTP call behind it. Everything untagged never becomes a tool. The model planning over those tools is the runtime step that needs an LLM. The transformation and the curation are what Skillgate does, and the report proves both.

The configuration

Curation is a single call. This is the real config from the project:

Skillgate.fromOpenApi(cmsOpenApiSpec, {
  agentName: 'cms-publisher',
  invoke: {
    baseUrl: process.env.CMS_API_BASE_URL || 'http://localhost:5000',
    headers: {
      Authorization: 'Bearer ${CMS_API_TOKEN}',
      'Content-Type': 'application/json',
    },
    ssrfProtection: false,
  },
  include: {
    mode: 'opt-in',
    tags: ['editorial'],
  },
  warnAbove: 12,
  maxTools: 24,
  strictDescriptions: true,
})
Enter fullscreen mode Exit fullscreen mode

The whole curation lives in one place: include.mode: 'opt-in' with tags: ['editorial']. Nothing becomes a skill unless it carries the editorial tag. Everything else in the spec is simply not considered.

What Skillgate produced

The spec has 27 endpoints. After the opt-in editorial filter, Skillgate registered 7 skills and denied none:

  • 2 read-only: listEntries, getEntry
  • 5 writes flagged for approval: createEntry, updateEntry, publishEntry, scheduleEntry, unpublishEntry

That is the entire agent surface. Twenty of the twenty-seven endpoints never enter the picture.

Where the dangerous routes went

This is the most useful thing to understand about the result, and it's a place the obvious mental model gets it slightly wrong.

The spec does contain destructive operations: deleteContentType, deleteEntry, deleteMedia, deleteUser, deleteWebhook, uninstallPlugin. None of them show up as denied in the report. They are kept away from the agent by the opt-in tag filter, because none of them carry the editorial tag, so they are never even considered for classification. The report's denied list is empty, and that is expected.

So there are two independent mechanisms protecting you, and this demo leans on the first:

  1. The opt-in filter never lets an untagged route become a candidate.
  2. Classification would deny a destructive method if it did become a candidate. If someone tagged a DELETE route editorial, Skillgate would class it destructive and deny it unless you set allowDestructive.

Two locks on the same door. The demo shows the tag-filter lock doing the work.

The proof: the report

Skillgate can print exactly what it curated. This is a trimmed slice of the real report() output, one read skill and one write skill:

{
  "included": [
    {
      "name": "listEntries",
      "method": "GET",
      "path": "/api/entries",
      "tags": ["entries", "read-only", "editorial"],
      "class": "read",
      "readOnly": true,
      "requiresApproval": false
    },
    {
      "name": "publishEntry",
      "method": "POST",
      "path": "/api/entries/{entryId}/publish",
      "tags": ["entries", "write", "editorial", "approval-required"],
      "class": "write",
      "readOnly": false,
      "requiresApproval": true
    }
  ],
  "denied": [],
  "warnings": []
}
Enter fullscreen mode Exit fullscreen mode

The full report lists all seven skills. Every write in the set carries requiresApproval: true, which brings us to the honest limit of the demo.

The approval flag is a contract, not the enforcement

Every write here is labeled requiresApproval: true, including publish and schedule. That label is Skillgate's classification: a contract that says these operations should not run without a human in the loop.

The label is not the pause. Enforcing it, suspending the run, capturing a decision, resuming after a crash, is the Agent OS runtime's job, not Skillgate's, and it is not wired into this demo. Calling a write here does not produce a real approval pause. What the demo does prove is that the sensitive operations were separated from the safe ones and tagged as such before any model saw the list. The enforcement layer is documented in the Agent OS and Gamma links at the end.

Tool-count guardrails

warnAbove: 12 and maxTools: 24 guard against tool explosion. Skillgate warns past twelve tools and refuses past twenty-four, which forces you to justify the surface instead of dumping the whole API.

At seven skills this demo sits well under the line, so the guardrail does not fire here. It is the mechanism that would stop a careless tags filter from pulling in fifty endpoints, which is the entire reason curation is opt-in rather than opt-out.

MCP export

The project exposes an MCP export entry point, wired to skillgate.toMcpServer():

curl -s http://localhost:3001/cms/mcp/export
Enter fullscreen mode Exit fullscreen mode

The idea is that the same curated, classified skills can be surfaced to an MCP client so an editor triggers them from their own tooling rather than a separate dashboard. To be clear about the state of this demo: the export entry point exists, but a working editor integration is not wired up. Treat this as the seam where that integration would attach, not as a finished feature.

Run it

npm install --legacy-peer-deps
npm run build
npm run dev
Enter fullscreen mode Exit fullscreen mode

The app runs on http://localhost:3001, with the HazelJS Inspector at http://localhost:3001/__hazel.

Print the curation decision:

curl -s http://localhost:3001/cms/skillgate/report
Enter fullscreen mode Exit fullscreen mode

One honest setup note, the same as the DevOps demo: no LLM provider is configured out of the box, so on boot you'll see "No AI providers configured." Skillgate performs its curation and produces the report with or without a model attached. Add a provider when you want an agent loop to actually plan over these seven skills.

Environment variables, all optional for the demo:

  • PORT (defaults to 3001)
  • CMS_API_BASE_URL (defaults to http://localhost:5000)
  • CMS_API_TOKEN (used in the auth header, not required for the demo)

What's real today, and what's next

Real and reproducible right now:

  • 27 endpoints curated down to 7 via a single editorial tag filter
  • Read and write classes assigned automatically
  • Six destructive routes kept out of the surface entirely
  • A printable report of the whole decision
  • An MCP export entry point

The layers you would add next:

  • Enforcing the requiresApproval flags as live approval pauses
  • Making those pauses crash-safe with durable runs
  • A real MCP editor integration on top of the export
  • An LLM so the agent can plan and call the curated skills

Complete Project: CMS Publishing Agent

Takeaway

"Turn any API into an agent" is only honest with one word attached: curation. You do not turn every endpoint into a tool. You choose a small, labeled subset, and Skillgate makes that choice explicit, auditable, and safe by default. Here that meant 27 endpoints becoming 7, with every destructive route left outside the door. That focused surface is what keeps an agent's tool selection sharp, and it is worth building before you ever attach a model.

Learn more

Top comments (0)