@latellu/atlas-agent-skills 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atlas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Work with Atlas CMS from Claude Code: bundles the Atlas MCP server (read/write content via tools) and the atlas-cms skill that teaches agents when and how to use the MCP tools, the @latellu/atlas-sdk delivery/management clients, and the @latellu/atlas-cli type generator.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Latellu",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latellu/atlas-agent-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Agent skill + MCP configuration for Atlas CMS. Works as a Claude Code plugin and as plain markdown guidance for any AI coding agent (Cursor, Codex, opencode, Gemini CLI, ...).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Latellu (https://github.com/tenriajeng)",
|
|
@@ -82,9 +82,7 @@ types, then build the page"), route each sub-task to its own single reference.
|
|
|
82
82
|
|
|
83
83
|
Choosing between MCP and the management SDK for writes: MCP is for operating content
|
|
84
84
|
interactively in an agent session (one-off edits, content entry, audits); the SDK is for
|
|
85
|
-
code that outlives the session (migrations, sync jobs, backends) and for
|
|
86
|
-
lacks (bulk, scheduling, page unpublish/archive, media delete, **writing translations**,
|
|
87
|
-
per-field validation errors).
|
|
85
|
+
code that outlives the session (migrations, sync jobs, backends) and for structured per-field error objects.
|
|
88
86
|
|
|
89
87
|
## Hard capability boundaries (don't go looking for these)
|
|
90
88
|
|
|
@@ -71,9 +71,9 @@ Rules that will bite you if unknown:
|
|
|
71
71
|
});
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
- **Via MCP
|
|
75
|
-
`
|
|
76
|
-
|
|
74
|
+
- **Via MCP (≥ 1.1.0)**: pass the same `translations` map as a tool argument on
|
|
75
|
+
`create_entry`/`update_entry`. Older MCP servers silently drop it — if a
|
|
76
|
+
translation write appears to succeed but nothing changes, check the server version.
|
|
77
77
|
|
|
78
78
|
## Writing pages and blocks
|
|
79
79
|
|
|
@@ -104,19 +104,17 @@ The real write shape (`POST/PUT /pages`) differs from what the reading API shows
|
|
|
104
104
|
|
|
105
105
|
- `update` with `blocks` replaces the block list; reordering alone has a dedicated
|
|
106
106
|
endpoint (`blocksReorder(slug, ids)` in the SDK).
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
- MCP `create_page`/`update_page`
|
|
114
|
-
|
|
115
|
-
`
|
|
107
|
+
- **Finding `block_type_id`: read the workspace schema.** Block types are content
|
|
108
|
+
types with `is_block: true`; `GET /api/v1/public/schema` (or the MCP
|
|
109
|
+
`get_workspace_schema` tool) returns each content type's `id`, `is_block`, and field
|
|
110
|
+
list. Use the `id` of an `is_block: true` type as `block_type_id`, and compose the
|
|
111
|
+
block's `data` against its fields exactly like an entry. (Older backends omit
|
|
112
|
+
`id`/`is_block` from the schema — there, block-type UUIDs are dashboard-only.)
|
|
113
|
+
- MCP `create_page`/`update_page` caveats (≥ 1.1.0): `title` is an alias mapped to
|
|
114
|
+
`seo.title` (on update, `seo` is replaced wholesale — title alone drops the other seo
|
|
115
|
+
fields), and `seo_translations`/per-block `translations` are regular arguments.
|
|
116
116
|
- **A page without blocks cannot be published** — publish returns an error requiring at
|
|
117
|
-
least one block.
|
|
118
|
-
workflow can create and edit pages but **never publish a new one**; publishing needs
|
|
119
|
-
someone to add a block in the dashboard first (or block-type UUIDs supplied to you).
|
|
117
|
+
least one block. Compose at least one block (see above) before `publish_page`.
|
|
120
118
|
- **The write plane has no reads**, and write responses don't echo `seo_translations` —
|
|
121
119
|
you cannot fully verify a translation write without a live key for the same workspace.
|
|
122
120
|
Trick: `PUT /pages/:slug` with body `{}` is a safe no-op that returns the page's
|
|
@@ -128,6 +126,7 @@ The real write shape (`POST/PUT /pages`) differs from what the reading API shows
|
|
|
128
126
|
2. Uploads first → collect media UUIDs.
|
|
129
127
|
3. Compose `data` per the table above; localizable required fields go in `translations`.
|
|
130
128
|
4. Create (draft) → read back → verify → publish.
|
|
131
|
-
5. On `VALIDATION_ERROR
|
|
132
|
-
|
|
133
|
-
markdown in richtext, and
|
|
129
|
+
5. On `VALIDATION_ERROR`: the error text carries per-field detail (MCP ≥ 1.1.0 and the
|
|
130
|
+
SDK both surface it) — fix the named field. The usual culprits are
|
|
131
|
+
slug-instead-of-UUID in relation/image, markdown in richtext, and
|
|
132
|
+
required-localizable fields sitting in `data`.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Atlas MCP tools
|
|
2
2
|
|
|
3
3
|
The `@latellu/atlas-mcp` server (stdio) exposes Atlas content operations as agent tools.
|
|
4
|
+
This file describes `@latellu/atlas-mcp` **≥ 1.2.0** (the bundled `.mcp.json` uses
|
|
5
|
+
`npx -y`, so it always runs the latest). Older servers lack `translations`,
|
|
6
|
+
`seo_translations`, per-field error detail, the schema `id`/`is_block` fields, page/entry lifecycle tools (unpublish/archive/duplicate/schedule), bulk operations, reorder tools, media alt-text editing, list meta+cursor pagination, automatic 429 retry, and pre-parsed block data in get_page.
|
|
4
7
|
|
|
5
8
|
**Environment** — read tools need `ATLAS_LIVE_API_KEY`, write tools need
|
|
6
9
|
`ATLAS_MGMT_API_KEY`; with only one set, the other half of the tools fails client-side
|
|
@@ -18,7 +21,7 @@ errors).
|
|
|
18
21
|
|
|
19
22
|
| Tool | Params | Returns |
|
|
20
23
|
|---|---|---|
|
|
21
|
-
| `get_workspace_schema` | — | `{ workspace: { slug, name, locales, default_locale }, content_types: [...] }` |
|
|
24
|
+
| `get_workspace_schema` | — | `{ workspace: { slug, name, locales, default_locale }, content_types: [...] }` — each content type carries `id` and `is_block`; types with `is_block: true` are the valid `block_type_id` values for page blocks |
|
|
22
25
|
| `list_content_types` | — | `content_types` array (client-side view of the schema) |
|
|
23
26
|
| `get_content_type` | `content_type` | one content type with its full field list; error text if the slug doesn't exist |
|
|
24
27
|
|
|
@@ -26,83 +29,79 @@ errors).
|
|
|
26
29
|
|
|
27
30
|
| Tool | Params | Notes |
|
|
28
31
|
|---|---|---|
|
|
29
|
-
| `list_entries` | `content_type`, `page?`=1, `limit?`=20, `status?` | `status` is
|
|
30
|
-
| `get_entry` | `content_type`, `slug` | resolves by `slug` alone — `content_type` is accepted but
|
|
31
|
-
| `create_entry` | `content_type`, `data`, `slug?` | creates a **draft**; slug auto-generated when omitted |
|
|
32
|
-
| `update_entry` | `content_type`, `slug`, `data` | **full replace** of `data
|
|
32
|
+
| `list_entries` | `content_type`, `page?`=1, `limit?`=20, `status?`, `cursor?` | `status` is enum (draft \| published \| archived \| scheduled); live key **cannot** reveal drafts. Returns `{ data, meta }` where `meta` has `total_data`, `total_pages` (offset mode) or `next_cursor` (cursor mode). Use `cursor: meta.next_cursor` to page past the 1000-page limit. |
|
|
33
|
+
| `get_entry` | `content_type`, `slug` | resolves by `slug` alone — `content_type` is accepted but errors clearly if the slug belongs to a different content type |
|
|
34
|
+
| `create_entry` | `content_type`, `data`, `slug?`, `translations?` | creates a **draft**; slug auto-generated when omitted; `translations` = `{locale: {data: {...}}}` (required+localizable fields go HERE) |
|
|
35
|
+
| `update_entry` | `content_type`, `slug`, `data`, `translations?` | **full replace** of `data` (and per-locale `translations`), not a patch |
|
|
33
36
|
| `publish_entry` / `unpublish_entry` / `archive_entry` | `content_type`, `slug` | need the `content:publish` scope |
|
|
34
37
|
| `duplicate_entry` | `content_type`, `slug` | copies to a new draft |
|
|
35
|
-
| `
|
|
38
|
+
| `schedule_entry` | `content_type`, `slug`, `publish_at` | schedules publish for a future time (ISO 8601 UTC); needs `content:publish` scope |
|
|
39
|
+
| `bulk_entries` | `content_type`, `operations` | batch publish/archive/schedule/delete by entry UUIDs; each op in array: `{ op, id, publish_at? }` |
|
|
40
|
+
| `bulk_delete_entries` | `content_type`, `ids` | batch delete by entry UUIDs |
|
|
41
|
+
| `reorder_entries` | `content_type`, `ids` | reorder entries; pass the complete ordered list of entry UUIDs |
|
|
42
|
+
| `delete_entry` | `content_type`, `slug` | returns JSON `{ deleted: true, ... }` |
|
|
36
43
|
|
|
37
44
|
### Pages — read: live key · write: mgmt key
|
|
38
45
|
|
|
39
46
|
| Tool | Params | Notes |
|
|
40
47
|
|---|---|---|
|
|
41
|
-
| `list_pages` | `page?`=1, `limit?`=20 | summaries, no blocks |
|
|
42
|
-
| `get_page` | `slug` | block tree — each block's `data` is
|
|
43
|
-
| `create_page` | `
|
|
44
|
-
| `update_page` | `slug`, `title?`, `blocks?`, `seo?` | |
|
|
45
|
-
| `publish_page` | `slug` |
|
|
46
|
-
| `
|
|
48
|
+
| `list_pages` | `page?`=1, `limit?`=20, `cursor?` | summaries, no blocks. Returns `{ data, meta }` with cursor pagination option (same as `list_entries`). |
|
|
49
|
+
| `get_page` | `slug` | block tree — each block's `data` is already parsed to an object (no JSON.parse needed); nested children and translations included |
|
|
50
|
+
| `create_page` | `slug`, `title?`, `blocks?`, `seo?`, `seo_translations?` | draft; `title` is an alias for `seo.title`; `seo` accepts `title`/`description`/`keywords`/`og_image`/`canonical`; blocks = `[{block_type_id, parent_id?, position, data, translations?}]` |
|
|
51
|
+
| `update_page` | `slug`, `title?`, `blocks?`, `seo?`, `seo_translations?` | passing `blocks` replaces the whole list; `seo` replaces wholesale (title alone drops other seo fields) |
|
|
52
|
+
| `publish_page` | `slug` | publish a page; fails on blockless pages |
|
|
53
|
+
| `unpublish_page` | `slug` | unpublish a page; needs `content:publish` scope |
|
|
54
|
+
| `archive_page` | `slug` | archive a page; needs `content:publish` scope |
|
|
55
|
+
| `duplicate_page` | `slug` | copy to a new draft |
|
|
56
|
+
| `schedule_page` | `slug`, `publish_at` | schedule publish for a future time (ISO 8601 UTC); needs `content:publish` scope |
|
|
57
|
+
| `reorder_page_blocks` | `slug`, `block_ids` | reorder blocks; pass the complete ordered list of block IDs |
|
|
58
|
+
| `delete_page` | `slug` | returns JSON `{ deleted: true, ... }` |
|
|
47
59
|
|
|
48
60
|
### Media
|
|
49
61
|
|
|
50
62
|
| Tool | Params | Notes |
|
|
51
63
|
|---|---|---|
|
|
52
64
|
| `get_media` | `id` | live key |
|
|
53
|
-
| `upload_media` | `file_path` (absolute), `folder?`="/", `alt_text?` | mgmt key; reads the local file, multipart upload |
|
|
65
|
+
| `upload_media` | `file_path` (absolute), `folder?`="/", `alt_text?` | mgmt key; reads the local file, multipart upload. Pre-validates client-side: ≤10 MB, extension allowlist (.jpg .jpeg .png .gif .webp .mp4 .webm .mov .pdf). |
|
|
66
|
+
| `update_media` | `id`, `alt_text?`, `folder?` | mgmt key; edit metadata (alt text and/or folder). |
|
|
67
|
+
| `delete_media` | `id` | mgmt key; returns JSON `{ deleted: true, ... }` |
|
|
54
68
|
|
|
55
69
|
## Recommended write workflow
|
|
56
70
|
|
|
57
71
|
1. **Schema first.** Call `get_content_type` (or `get_workspace_schema`) before any
|
|
58
72
|
`create_entry`/`update_entry`. The `data` argument is an unvalidated map; the backend
|
|
59
|
-
rejects wrong shapes with 400
|
|
60
|
-
|
|
61
|
-
field names/types up front is the difference between one
|
|
73
|
+
rejects wrong shapes with 400. Error results include per-field detail
|
|
74
|
+
(`title: title is required`) and a `traceId` — read them instead of guessing.
|
|
75
|
+
Knowing the exact field names/types up front is still the difference between one
|
|
76
|
+
call and five.
|
|
62
77
|
2. **Read-modify-write for updates.** `update_entry` replaces `data` wholesale:
|
|
63
78
|
`get_entry` → merge your change into the full object → `update_entry` with everything.
|
|
64
79
|
3. **Create → verify → publish.** `create_entry` makes a draft; `publish_entry` needs
|
|
65
80
|
the `content:publish` scope (403 without it, with a clear message).
|
|
66
|
-
4. **Before `upload_media`**:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
(so `.svg`, `.heic` fail). Allowed: `image/*`, `video/*`, `application/pdf` (i.e.
|
|
70
|
-
`.jpg .jpeg .png .gif .webp .mp4 .webm .mov .pdf`).
|
|
81
|
+
4. **Before `upload_media`**: the tool pre-validates client-side (≤ 10 MB, extension
|
|
82
|
+
allowlist), so failures are caught before upload. Allowed extensions:
|
|
83
|
+
`.jpg .jpeg .png .gif .webp .mp4 .webm .mov .pdf`.
|
|
71
84
|
|
|
72
85
|
## Capability gaps — route elsewhere instead of retrying
|
|
73
86
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`duplicate_page`**.
|
|
83
|
-
- **No** bulk operations, entry/page **scheduling**, or block **reorder** tools.
|
|
84
|
-
- **No `delete_media` / `update_media`** (alt-text edits) — media is upload-and-read
|
|
85
|
-
only through MCP.
|
|
86
|
-
- Pagination is offset-only (`page`/`limit`); there is no cursor option, so listings
|
|
87
|
-
past page 1000 (`PAGE_TOO_DEEP`) are unreachable via MCP.
|
|
88
|
-
- Composing page **blocks** needs `block_type_id` UUIDs that no read surface exposes
|
|
89
|
-
(dashboard-only knowledge) — prefer entries for agent-authored content. Also:
|
|
90
|
-
`create_page`'s `title` argument is ignored by the backend (the title is `seo.title`),
|
|
91
|
-
and its `seo` accepts only `title`/`description`.
|
|
87
|
+
The MCP manage plane is now fully covered. These gaps remain:
|
|
88
|
+
|
|
89
|
+
- **No automatic merge on `update_entry`** — the tool replaces `data` wholesale, so you
|
|
90
|
+
must read-modify-write yourself: `get_entry` → merge your changes → `update_entry`.
|
|
91
|
+
The management SDK offers an `update` overload that accepts a patch, but MCP does not.
|
|
92
|
+
- **Schema is dashboard-only.** Creating content types, fields, or block types is not
|
|
93
|
+
exposed via MCP, SDK, or API. "Create a content type" = ask the user to do it in the
|
|
94
|
+
dashboard, then continue.
|
|
92
95
|
|
|
93
96
|
Before composing any `data` payload, read `authoring.md` — it defines the exact value
|
|
94
|
-
format per field type (richtext = Tiptap HTML, relation/image = UUIDs, etc.)
|
|
95
|
-
matters double here because MCP swallows per-field validation errors.
|
|
97
|
+
format per field type (richtext = Tiptap HTML, relation/image = UUIDs, etc.).
|
|
96
98
|
|
|
97
99
|
## Gotchas
|
|
98
100
|
|
|
99
101
|
- **Drafts are invisible to live keys** — `list_entries(status="draft")` returning
|
|
100
102
|
empty means key scoping, not an empty workspace.
|
|
101
|
-
- **`
|
|
102
|
-
|
|
103
|
-
whatever the backend finds by slug.
|
|
104
|
-
- **No client-side 429 retry** — on `Atlas API error: 429`, back off exponentially and
|
|
105
|
-
retry the tool call yourself.
|
|
103
|
+
- **`get_entry` errors if the slug belongs to a different content type** — content types
|
|
104
|
+
used to share slug namespaces, now you get a clear error instead of silent mismatch.
|
|
106
105
|
- Every write sends a fresh auto-generated `Idempotency-Key` per call, so re-issuing a
|
|
107
106
|
failed create as a *new tool call* is a new operation — check whether the first call
|
|
108
107
|
actually landed (e.g. `get_entry`) before retrying creates.
|
|
@@ -69,7 +69,7 @@ await client.pages.create({ slug: "landing", seo: { title: "Landing" }, blocks:
|
|
|
69
69
|
await client.pages.update("landing", { seo: { title: "Landing v2" } });
|
|
70
70
|
await client.pages.publish("landing"); // fails unless the page has ≥1 block — see authoring.md
|
|
71
71
|
await client.pages.unpublish("landing"); // pages have the FULL lifecycle here
|
|
72
|
-
await client.pages.archive("landing"); //
|
|
72
|
+
await client.pages.archive("landing"); // (MCP also has parity since v1.2.0)
|
|
73
73
|
await client.pages.schedule("landing", "2026-08-01T09:00:00Z");
|
|
74
74
|
await client.pages.delete("landing");
|
|
75
75
|
await client.pages.blocksReorder("landing", ["block-id-2", "block-id-1", "block-id-3"]);
|
|
@@ -113,8 +113,8 @@ try {
|
|
|
113
113
|
if (e instanceof AtlasError) {
|
|
114
114
|
e.status; // 400
|
|
115
115
|
e.code; // "VALIDATION_ERROR"
|
|
116
|
-
e.errors; // [{ field: "title", message: "must be a string" }, ...] ← per-field detail
|
|
117
|
-
} //
|
|
116
|
+
e.errors; // [{ field: "title", message: "must be a string" }, ...] ← per-field detail
|
|
117
|
+
} // (structured here; MCP ≥ 1.1.0 appends it to the error text) — surface it to users
|
|
118
118
|
}
|
|
119
119
|
```
|
|
120
120
|
|