@latellu/atlas-mcp 1.0.5 → 1.0.7

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.
Files changed (3) hide show
  1. package/README.md +39 -12
  2. package/dist/index.js +68 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  MCP (Model Context Protocol) server for Atlas CMS — enables AI agents to manage content.
7
7
 
8
+ **Docs & dashboard:** concepts and API reference live at **[docs.atlas.latellu.com](https://docs.atlas.latellu.com)**; the app where you create workspaces, content, and API keys is **[cms.atlas.latellu.com](https://cms.atlas.latellu.com)**.
9
+
8
10
  ## Overview
9
11
 
10
12
  This MCP server exposes Atlas CMS content management operations as tools for AI agents (Claude, Cursor, etc.). It enables AI agents to:
@@ -14,6 +16,19 @@ This MCP server exposes Atlas CMS content management operations as tools for AI
14
16
  - Create, update, publish, and delete content
15
17
  - Upload media files from local filesystem
16
18
 
19
+ Building a custom app instead of wiring up an AI agent? Use the [`@latellu/atlas-sdk`](https://www.npmjs.com/package/@latellu/atlas-sdk) TypeScript SDK — this server only exposes tools for MCP clients (Claude Desktop, Cursor, etc.), it isn't a general-purpose HTTP client.
20
+
21
+ ## Core Concepts
22
+
23
+ New to Atlas? A **workspace** is your tenant — it owns all content types, entries, pages, media, and API keys, isolated from other workspaces. Within a workspace:
24
+
25
+ - **[Content type](https://docs.atlas.latellu.com/docs/content-types)** — a schema (e.g. "Article") defining the fields an entry can have.
26
+ - **[Entry](https://docs.atlas.latellu.com/docs/entries)** — a record of a content type, with a **draft → published → archived** lifecycle.
27
+ - **[Page](https://docs.atlas.latellu.com/docs/pages)** — like an entry, but composed of ordered **blocks** and carries its own SEO metadata.
28
+ - **[Locale](https://docs.atlas.latellu.com/docs/localization)** — a language variant; localized fields fall back to the workspace's default locale when untranslated.
29
+
30
+ See the [Getting Started guide](https://docs.atlas.latellu.com/docs/getting-started) for the full picture.
31
+
17
32
  ## Features
18
33
 
19
34
  ### Schema & Discovery
@@ -63,23 +78,30 @@ atlas-mcp
63
78
 
64
79
  ### Getting an API Key
65
80
 
66
- 1. Open the Atlas dashboard: **https://cms.atlas.latellu.com**
67
- 2. Go to **Settings API Keys**
68
- 3. Click **Create API Key**, give it a name, and save
69
- 4. Copy the key — it is only shown once
81
+ 1. Open **[cms.atlas.latellu.com/dashboard/api-keys](https://cms.atlas.latellu.com/dashboard/api-keys)** (sidebar: **Developer → API Keys**)
82
+ 2. Click **Create API Key**, give it a name, pick its scopes, and save
83
+ 3. Copy the key it is only shown once
70
84
 
71
85
  **Key types:**
72
- - `atlas_live_*` — Read-only access (delivery API). Use for fetching content.
73
- - `atlas_mgmt_*` — Read-write access (management API). Use for creating/updating content.
86
+ - `atlas_live_*` — Delivery API key. Required for every **read** tool (`get_workspace_schema`, `list_*`, `get_*`).
87
+ - `atlas_mgmt_*` — Management API key. Required for every **write** tool (`create_*`, `update_*`, `delete_*`, `publish_*`, `unpublish_*`, `archive_*`, `duplicate_*`, `upload_media`).
88
+
89
+ The backend enforces one key type per route group — a management key cannot read `/api/v1/public/*`, and a delivery key cannot write to `/api/v1/manage/*`. **For the full tool set (read + write), configure both keys.** A single key only unlocks the half of the tools matching its type; calling a tool that needs the other key type returns a clear error naming the missing env var.
90
+
91
+ See [docs.atlas.latellu.com/docs/authentication](https://docs.atlas.latellu.com/docs/authentication) for how scopes work and the full authentication reference.
74
92
 
75
93
  ### Environment Variables
76
94
 
77
95
  | Variable | Required | Description |
78
96
  |----------|----------|-------------|
79
- | `ATLAS_API_KEY` | Yes | Atlas API key (`atlas_live_*` for read-only, `atlas_mgmt_*` for full access) |
97
+ | `ATLAS_API_KEY` | One of the three below | Single key, auto-detected by prefix (`atlas_live_*` enables read tools, `atlas_mgmt_*` enables write tools) — kept for backward compatibility |
98
+ | `ATLAS_LIVE_API_KEY` | No | Delivery key (`atlas_live_*`), explicitly enables read tools |
99
+ | `ATLAS_MGMT_API_KEY` | No | Management key (`atlas_mgmt_*`), explicitly enables write tools |
80
100
  | `ATLAS_API_URL` | No | Atlas API base URL (default: `https://api.atlas.latellu.com`) |
81
101
  | `MCP_ALLOWED_UPLOAD_PATHS` | No | Comma-separated list of allowed directories for media upload |
82
102
 
103
+ At least one of `ATLAS_API_KEY`, `ATLAS_LIVE_API_KEY`, or `ATLAS_MGMT_API_KEY` must be set. Set `ATLAS_LIVE_API_KEY` and `ATLAS_MGMT_API_KEY` together to enable both read and write tools in the same server.
104
+
83
105
  ### Claude Desktop Configuration
84
106
 
85
107
  Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
@@ -91,7 +113,8 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
91
113
  "command": "npx",
92
114
  "args": ["-y", "@latellu/atlas-mcp"],
93
115
  "env": {
94
- "ATLAS_API_KEY": "atlas_mgmt_xxx...",
116
+ "ATLAS_LIVE_API_KEY": "atlas_live_xxx...",
117
+ "ATLAS_MGMT_API_KEY": "atlas_mgmt_xxx...",
95
118
  "MCP_ALLOWED_UPLOAD_PATHS": "/Users/you/Documents,/Users/you/Pictures"
96
119
  }
97
120
  }
@@ -99,6 +122,8 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
99
122
  }
100
123
  ```
101
124
 
125
+ Write-only setups (no read tools needed) can still set just `ATLAS_API_KEY` to a single `atlas_mgmt_*` key.
126
+
102
127
  ### Cursor Configuration
103
128
 
104
129
  Add to `.cursor/mcp.json`:
@@ -110,7 +135,8 @@ Add to `.cursor/mcp.json`:
110
135
  "command": "npx",
111
136
  "args": ["-y", "@latellu/atlas-mcp"],
112
137
  "env": {
113
- "ATLAS_API_KEY": "atlas_mgmt_xxx..."
138
+ "ATLAS_LIVE_API_KEY": "atlas_live_xxx...",
139
+ "ATLAS_MGMT_API_KEY": "atlas_mgmt_xxx..."
114
140
  }
115
141
  }
116
142
  }
@@ -159,8 +185,9 @@ AI: → Calls upload_media with file_path='/Users/you/Pictures/tokyo.png'
159
185
  ### Permission Model
160
186
 
161
187
  **Layer 1: API Key**
162
- - `atlas_live_*` — Read-only access (delivery API)
163
- - `atlas_mgmt_*` — Read-write access (management API)
188
+ - `atlas_live_*` — Read tools only (delivery API)
189
+ - `atlas_mgmt_*` — Write tools only (management API)
190
+ - Configure both (`ATLAS_LIVE_API_KEY` + `ATLAS_MGMT_API_KEY`) for the full tool set
164
191
 
165
192
  **Layer 2: Filesystem Permission**
166
193
  - Configurable via `MCP_ALLOWED_UPLOAD_PATHS` env var
@@ -172,7 +199,7 @@ AI: → Calls upload_media with file_path='/Users/you/Pictures/tokyo.png'
172
199
 
173
200
  ### Best Practices
174
201
 
175
- 1. **Scope API keys**: For management keys, grant only the scopes each tool needs — `content:write` for create/update/delete/reorder, `content:publish` for publish/unpublish/archive/schedule, `media:write` for uploads/deletes. Leave unused scopes off.
202
+ 1. **Scope API keys**: For management keys, grant only the scopes each tool needs — `content:write` for create/update/delete/reorder, `content:publish` for publish/unpublish/archive/schedule, `media:write` for uploads/deletes. Leave unused scopes off. Scopes are picked when you create the key at [Developer → API Keys](https://cms.atlas.latellu.com/dashboard/api-keys); see the [scope reference](https://docs.atlas.latellu.com/docs/authentication) for the full list.
176
203
  2. **Restrict upload paths**: Only allow necessary directories
177
204
  3. **Rotate keys**: Regularly rotate API keys
178
205
  4. **Monitor usage**: Review MCP tool calls for suspicious activity
package/dist/index.js CHANGED
@@ -12556,27 +12556,44 @@ function detectMimeType(fileName) {
12556
12556
  }
12557
12557
  var AtlasClient = class {
12558
12558
  baseUrl;
12559
- apiKey;
12560
- constructor(baseUrl, apiKey) {
12559
+ liveKey;
12560
+ mgmtKey;
12561
+ constructor(baseUrl, keys) {
12561
12562
  this.baseUrl = baseUrl;
12562
- this.apiKey = apiKey;
12563
+ this.liveKey = keys.liveKey;
12564
+ this.mgmtKey = keys.mgmtKey;
12563
12565
  }
12564
- async request(path2, options) {
12566
+ async httpRequest(path2, apiKey, options) {
12565
12567
  const url = `${this.baseUrl}${path2}`;
12566
12568
  const response = await fetch(url, {
12567
12569
  ...options,
12568
12570
  headers: {
12569
12571
  "Content-Type": "application/json",
12570
- "X-API-Key": this.apiKey,
12572
+ "X-API-Key": apiKey,
12571
12573
  ...options?.headers
12572
12574
  }
12573
12575
  });
12574
12576
  return this.handleResponse(response);
12575
12577
  }
12576
- // Write requests to /api/v1/manage/* carry an Idempotency-Key so that
12577
- // AI-agent-driven retries don't risk double-create/double-publish.
12578
- async writeRequest(path2, options) {
12579
- return this.request(path2, {
12578
+ // Reads against /api/v1/public/* require a delivery key (atlas_live_*).
12579
+ async publicRequest(path2, options) {
12580
+ if (!this.liveKey) {
12581
+ throw new Error(
12582
+ "This operation requires a delivery API key (atlas_live_*). Set ATLAS_LIVE_API_KEY (or ATLAS_API_KEY to an atlas_live_* key) to enable read operations."
12583
+ );
12584
+ }
12585
+ return this.httpRequest(path2, this.liveKey, options);
12586
+ }
12587
+ // Writes against /api/v1/manage/* require a management key (atlas_mgmt_*)
12588
+ // and carry an Idempotency-Key so AI-agent-driven retries don't risk
12589
+ // double-create/double-publish.
12590
+ async manageRequest(path2, options) {
12591
+ if (!this.mgmtKey) {
12592
+ throw new Error(
12593
+ "This operation requires a management API key (atlas_mgmt_*). Set ATLAS_MGMT_API_KEY (or ATLAS_API_KEY to an atlas_mgmt_* key) to enable write operations."
12594
+ );
12595
+ }
12596
+ return this.httpRequest(path2, this.mgmtKey, {
12580
12597
  ...options,
12581
12598
  headers: {
12582
12599
  "Idempotency-Key": crypto.randomUUID(),
@@ -12596,7 +12613,7 @@ var AtlasClient = class {
12596
12613
  }
12597
12614
  // Schema endpoints
12598
12615
  async getWorkspaceSchema() {
12599
- return this.request("/api/v1/public/schema");
12616
+ return this.publicRequest("/api/v1/public/schema");
12600
12617
  }
12601
12618
  async listContentTypes() {
12602
12619
  const schema = await this.getWorkspaceSchema();
@@ -12617,45 +12634,45 @@ var AtlasClient = class {
12617
12634
  if (params?.page) searchParams.set("page", params.page.toString());
12618
12635
  if (params?.limit) searchParams.set("limit", params.limit.toString());
12619
12636
  if (params?.status) searchParams.set("status", params.status);
12620
- return this.request(`/api/v1/public/entries?${searchParams.toString()}`);
12637
+ return this.publicRequest(`/api/v1/public/entries?${searchParams.toString()}`);
12621
12638
  }
12622
12639
  async getEntry(contentType, slug) {
12623
- return this.request(`/api/v1/public/entries/${slug}`);
12640
+ return this.publicRequest(`/api/v1/public/entries/${slug}`);
12624
12641
  }
12625
12642
  async createEntry(contentType, data, slug) {
12626
- return this.writeRequest(`/api/v1/manage/content-types/${contentType}/entries`, {
12643
+ return this.manageRequest(`/api/v1/manage/content-types/${contentType}/entries`, {
12627
12644
  method: "POST",
12628
12645
  body: JSON.stringify({ slug, data })
12629
12646
  });
12630
12647
  }
12631
12648
  async updateEntry(contentType, slug, data) {
12632
- return this.writeRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}`, {
12649
+ return this.manageRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}`, {
12633
12650
  method: "PUT",
12634
12651
  body: JSON.stringify({ data })
12635
12652
  });
12636
12653
  }
12637
12654
  async deleteEntry(contentType, slug) {
12638
- return this.writeRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}`, {
12655
+ return this.manageRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}`, {
12639
12656
  method: "DELETE"
12640
12657
  });
12641
12658
  }
12642
12659
  async publishEntry(contentType, slug) {
12643
- return this.writeRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}/publish`, {
12660
+ return this.manageRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}/publish`, {
12644
12661
  method: "PATCH"
12645
12662
  });
12646
12663
  }
12647
12664
  async unpublishEntry(contentType, slug) {
12648
- return this.writeRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}/unpublish`, {
12665
+ return this.manageRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}/unpublish`, {
12649
12666
  method: "PATCH"
12650
12667
  });
12651
12668
  }
12652
12669
  async archiveEntry(contentType, slug) {
12653
- return this.writeRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}/archive`, {
12670
+ return this.manageRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}/archive`, {
12654
12671
  method: "PATCH"
12655
12672
  });
12656
12673
  }
12657
12674
  async duplicateEntry(contentType, slug) {
12658
- return this.writeRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}/duplicate`, {
12675
+ return this.manageRequest(`/api/v1/manage/content-types/${contentType}/entries/${slug}/duplicate`, {
12659
12676
  method: "POST"
12660
12677
  });
12661
12678
  }
@@ -12664,38 +12681,43 @@ var AtlasClient = class {
12664
12681
  const searchParams = new URLSearchParams();
12665
12682
  if (params?.page) searchParams.set("page", params.page.toString());
12666
12683
  if (params?.limit) searchParams.set("limit", params.limit.toString());
12667
- return this.request(`/api/v1/public/pages?${searchParams.toString()}`);
12684
+ return this.publicRequest(`/api/v1/public/pages?${searchParams.toString()}`);
12668
12685
  }
12669
12686
  async getPage(slug) {
12670
- return this.request(`/api/v1/public/pages/${slug}`);
12687
+ return this.publicRequest(`/api/v1/public/pages/${slug}`);
12671
12688
  }
12672
12689
  async createPage(data) {
12673
- return this.writeRequest("/api/v1/manage/pages", {
12690
+ return this.manageRequest("/api/v1/manage/pages", {
12674
12691
  method: "POST",
12675
12692
  body: JSON.stringify(data)
12676
12693
  });
12677
12694
  }
12678
12695
  async updatePage(slug, data) {
12679
- return this.writeRequest(`/api/v1/manage/pages/${slug}`, {
12696
+ return this.manageRequest(`/api/v1/manage/pages/${slug}`, {
12680
12697
  method: "PUT",
12681
12698
  body: JSON.stringify(data)
12682
12699
  });
12683
12700
  }
12684
12701
  async deletePage(slug) {
12685
- return this.writeRequest(`/api/v1/manage/pages/${slug}`, {
12702
+ return this.manageRequest(`/api/v1/manage/pages/${slug}`, {
12686
12703
  method: "DELETE"
12687
12704
  });
12688
12705
  }
12689
12706
  async publishPage(slug) {
12690
- return this.writeRequest(`/api/v1/manage/pages/${slug}/publish`, {
12707
+ return this.manageRequest(`/api/v1/manage/pages/${slug}/publish`, {
12691
12708
  method: "PATCH"
12692
12709
  });
12693
12710
  }
12694
12711
  // Media endpoints
12695
12712
  async getMedia(id) {
12696
- return this.request(`/api/v1/public/media/${id}`);
12713
+ return this.publicRequest(`/api/v1/public/media/${id}`);
12697
12714
  }
12698
12715
  async uploadMedia(filePath, folder, altText) {
12716
+ if (!this.mgmtKey) {
12717
+ throw new Error(
12718
+ "This operation requires a management API key (atlas_mgmt_*). Set ATLAS_MGMT_API_KEY (or ATLAS_API_KEY to an atlas_mgmt_* key) to enable write operations."
12719
+ );
12720
+ }
12699
12721
  const fs = await import("fs");
12700
12722
  const path2 = await import("path");
12701
12723
  if (!fs.existsSync(filePath)) {
@@ -12711,7 +12733,7 @@ var AtlasClient = class {
12711
12733
  const response = await fetch(`${this.baseUrl}/api/v1/manage/media/upload`, {
12712
12734
  method: "POST",
12713
12735
  headers: {
12714
- "X-API-Key": this.apiKey,
12736
+ "X-API-Key": this.mgmtKey,
12715
12737
  "Idempotency-Key": crypto.randomUUID()
12716
12738
  },
12717
12739
  body: formData
@@ -12740,13 +12762,25 @@ function validateFilePath(filePath, allowedPaths) {
12740
12762
 
12741
12763
  // src/index.ts
12742
12764
  var ATLAS_API_URL = process.env.ATLAS_API_URL || "https://api.atlas.latellu.com";
12743
- var ATLAS_API_KEY = process.env.ATLAS_API_KEY;
12744
12765
  var MCP_ALLOWED_UPLOAD_PATHS = process.env.MCP_ALLOWED_UPLOAD_PATHS ? process.env.MCP_ALLOWED_UPLOAD_PATHS.split(",").map((p) => p.trim()) : [];
12745
- if (!ATLAS_API_KEY) {
12746
- console.error("ATLAS_API_KEY environment variable is required");
12766
+ var LIVE_KEY_PREFIX = "atlas_live_";
12767
+ var MGMT_KEY_PREFIX = "atlas_mgmt_";
12768
+ function resolveKeys() {
12769
+ const legacyCandidates = [process.env.ATLAS_LIVE_API_KEY, process.env.ATLAS_MGMT_API_KEY, process.env.ATLAS_API_KEY].filter(
12770
+ (v) => !!v
12771
+ );
12772
+ const liveKey2 = process.env.ATLAS_LIVE_API_KEY ?? legacyCandidates.find((k) => k.startsWith(LIVE_KEY_PREFIX));
12773
+ const mgmtKey2 = process.env.ATLAS_MGMT_API_KEY ?? legacyCandidates.find((k) => k.startsWith(MGMT_KEY_PREFIX));
12774
+ return { liveKey: liveKey2, mgmtKey: mgmtKey2 };
12775
+ }
12776
+ var { liveKey, mgmtKey } = resolveKeys();
12777
+ if (!liveKey && !mgmtKey) {
12778
+ console.error(
12779
+ "No valid Atlas API key found. Set ATLAS_API_KEY to an atlas_live_* or atlas_mgmt_* key, or set ATLAS_LIVE_API_KEY / ATLAS_MGMT_API_KEY explicitly (both, for full read+write access)."
12780
+ );
12747
12781
  process.exit(1);
12748
12782
  }
12749
- var atlasClient = new AtlasClient(ATLAS_API_URL, ATLAS_API_KEY);
12783
+ var atlasClient = new AtlasClient(ATLAS_API_URL, { liveKey, mgmtKey });
12750
12784
  var server = new McpServer({
12751
12785
  name: "atlas-mcp",
12752
12786
  version: "1.0.0"
@@ -13279,7 +13313,9 @@ server.tool(
13279
13313
  async function main() {
13280
13314
  const transport = new StdioServerTransport();
13281
13315
  await server.connect(transport);
13282
- console.error("Atlas MCP server started");
13316
+ console.error(
13317
+ `Atlas MCP server started (read tools: ${liveKey ? "enabled" : "disabled, needs atlas_live_*"}, write tools: ${mgmtKey ? "enabled" : "disabled, needs atlas_mgmt_*"})`
13318
+ );
13283
13319
  }
13284
13320
  main().catch((error) => {
13285
13321
  console.error("Fatal error:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latellu/atlas-mcp",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },