@proars/ghost-mcp-server 2.1.0 → 2.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [2.2.0] - 2026-11-02
8
+
9
+ ### Added
10
+
11
+ - `update_page_seo` shortcut, matching `update_post_seo` for pages (`PUT /pages/{id}/`). The server now has 60 tools. README documents it in the tool table and the calling conventions.
12
+ - README: npm version badge and a demo GIF (`docs/demo.gif`).
13
+ - README: JSON `args` example for the update command, and guidance on the API URL for Ghost(Pro) and custom domains.
14
+
15
+ ## [2.1.1] - 2026-11-01
16
+
17
+ ### Changed
18
+
19
+ - README: Claude Code install uses `--scope user`, with steps to check and remove the server, a note on updating, and a warning about running `npx` inside a clone of this repository.
20
+
21
+ ## [2.1.0] - 2026-11-01
22
+
23
+ First release on npm as `@proars/ghost-mcp-server`.
24
+
25
+ ### Added
26
+
27
+ - `delete_member` tool (`DELETE /members/{id}/`). The server now has 59 tools.
28
+ - `ghost-mcp-server` command (`bin` with a shebang), so the package runs with `npx -y @proars/ghost-mcp-server`.
29
+ - GitHub Actions workflow that runs typecheck, build and tests on Node 20 and 22.
30
+ - README warning that destructive tools are available, and an install section for `npx`.
31
+
32
+ ### Changed
33
+
34
+ - `npm start` runs the compiled `dist/index.js` instead of `tsx`.
35
+ - Published files are limited to `dist`, `README.md`, `LICENSE` and `.env.example`.
36
+ - `prepublishOnly` builds the project and runs the tests before every publish.
37
+ - Minimum `@modelcontextprotocol/sdk` raised from `^1.0.0` to `^1.32.0`.
38
+ - README rewritten with a quick start, FAQ and access notes.
package/README.md CHANGED
@@ -1,14 +1,17 @@
1
1
  # Ghost MCP Server: manage Ghost CMS from Claude, Cursor and other AI clients
2
2
 
3
3
  [![CI](https://github.com/proars/ghost-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/proars/ghost-mcp-server/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@proars/ghost-mcp-server)](https://www.npmjs.com/package/@proars/ghost-mcp-server)
4
5
  ![Node.js 20+](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)
5
- ![Tools](https://img.shields.io/badge/tools-59-blue)
6
+ ![Tools](https://img.shields.io/badge/tools-60-blue)
6
7
  ![Routes](https://img.shields.io/badge/Admin%20API%20routes-253-orange)
7
8
  ![License: MIT](https://img.shields.io/badge/license-MIT-green)
8
9
 
9
10
  Ghost MCP Server is an open-source [Model Context Protocol](https://modelcontextprotocol.io) server for Ghost CMS, written in TypeScript. It connects an MCP client such as Claude Desktop, Claude Code or Cursor to the Ghost Admin API over stdio. With it, an AI assistant can create and edit posts and pages, set SEO metadata, manage tags, members and newsletters, upload images and themes, and export content.
10
11
 
11
- It exposes **59 tools**: 55 dedicated API tools, two shortcuts for SEO and tags, a route search and a generic request tool. The built-in catalog covers **253 Admin API routes** taken from a pinned Ghost source snapshot. What works on your site depends on your credentials, your Ghost version and its feature flags. Routes and sources are listed in [GHOST_API.list](GHOST_API.list).
12
+ It exposes **60 tools**: 55 dedicated API tools, three shortcuts for SEO and tags, a route search and a generic request tool. The built-in catalog covers **253 Admin API routes** taken from a pinned Ghost source snapshot. What works on your site depends on your credentials, your Ghost version and its feature flags. Routes and sources are listed in [GHOST_API.list](GHOST_API.list).
13
+
14
+ ![Demo: Claude Code creates a draft post in Ghost through the MCP server](https://raw.githubusercontent.com/proars/ghost-mcp-server/main/docs/demo.gif)
12
15
 
13
16
  ## Quick start
14
17
 
@@ -45,14 +48,28 @@ Config file locations: Claude Desktop on macOS uses `~/Library/Application Suppo
45
48
  **Claude Code:**
46
49
 
47
50
  ```bash
48
- claude mcp add ghost \
51
+ claude mcp add ghost --scope user \
49
52
  -e GHOST_API_URL=https://your-blog.com \
50
53
  -e GHOST_ADMIN_API_KEY=64f1a2b3c4d5e6f7a8b9c0d1:3e5f7a9b1c2d4e6f8a0b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e0f2a3b5c7d9e1f \
51
54
  -- npx -y @proars/ghost-mcp-server
52
55
  ```
53
56
 
57
+ `--scope user` makes the server available in every folder. Without it, the server is registered only for the folder where you ran the command. Claude Code starts and stops the server itself, so you don't need to keep a terminal open.
58
+
59
+ Check the connection with `claude mcp get ghost`, which should report `Connected`, or type `/mcp` inside a `claude` session. To remove the server, run `claude mcp remove ghost -s user`.
60
+
54
61
  Any other stdio MCP client can run the same command, `npx -y @proars/ghost-mcp-server`, with the two environment variables.
55
62
 
63
+ **Updating:** `npx` uses a cache, so `@latest` may not be checked against the registry on every start. To force a registry freshness check whenever the command runs, use `npx --prefer-online -y @proars/ghost-mcp-server@latest`. In a JSON client configuration, set `args` to:
64
+
65
+ ```json
66
+ ["--prefer-online", "-y", "@proars/ghost-mcp-server@latest"]
67
+ ```
68
+
69
+ To stay on one version, pin it, for example `@proars/ghost-mcp-server@2.2.0`. See the [changelog](CHANGELOG.md) for what changed in each release.
70
+
71
+ Don't run `npx` from inside a clone of this repository: npm finds the local `package.json` with the same name and fails with `ghost-mcp-server: command not found`. Run it from any other folder.
72
+
56
73
  ## Install from source
57
74
 
58
75
  Use this if you want to change the code or run an unpublished version.
@@ -96,17 +113,18 @@ The `.env` file in the project root is read regardless of the directory the clie
96
113
  | Images and themes | `upload_image`, `upload_theme`, `activate_theme` |
97
114
  | Webhooks | `create_webhook`, `update_webhook`, `delete_webhook` |
98
115
  | Site | `get_site`, `get_settings` |
99
- | Shortcuts | `update_post_seo`, `update_post_tags` |
116
+ | Shortcuts | `update_post_seo`, `update_page_seo`, `update_post_tags` |
100
117
  | Discovery and generic calls | `ghost_list_endpoints`, `ghost_api_request` |
101
118
 
102
119
  Ghost has no delete endpoint for tiers, newsletters or offers, so those resources have no `delete_*` tool.
103
120
 
104
- > **Warning: this server exposes every supported function, including destructive ones.** `delete_post`, `delete_page`, `delete_tag`, `delete_member`, `delete_user`, `activate_theme`, `upload_theme` and `ghost_api_request` can permanently remove or overwrite content, subscribers and site settings. Deleted records usually cannot be restored. Create a separate integration key for the server, make a backup (Ghost Admin → Settings → Labs → Export) before bulk work, and turn on manual approval for all `update_*`, `delete_*` and generic calls in your client.
121
+ > **Warning: this server exposes every supported function, including destructive ones.** `delete_post`, `delete_page`, `delete_tag`, `delete_member`, `delete_user`, `activate_theme`, `upload_theme` and `ghost_api_request` can permanently remove or overwrite content, subscribers and site settings. Deleted records usually cannot be restored. Create a separate integration key for the server, export a backup of your content from Ghost Admin before bulk work, and turn on manual approval for all `update_*`, `delete_*` and generic calls in your client.
105
122
 
106
123
  ### How do I call the tools?
107
124
 
108
- - **Create and update:** put the record fields in `data` and pass the record `id` as a separate argument. Post and page updates need `data.updated_at` from a fresh read. The SEO and tag shortcuts take `updated_at` directly.
125
+ - **Create and update:** put the record fields in `data` and pass the record `id` as a separate argument. Post and page updates need `data.updated_at` from a fresh read. The SEO shortcuts (`update_post_seo`, `update_page_seo`) and the tag shortcut take `updated_at` directly.
109
126
  - **Lists:** use `limit`, `page`, `filter`, `order`, `include` and `fields`. Posts and pages come back with compact fields and tags by default. Single-record reads return the full record.
127
+ - **SEO:** `update_post_seo` and `update_page_seo` set `meta_title`, `meta_description`, `og_title` and `og_description` in one call. Pass `id`, the current `updated_at` and at least one of those fields. Fields you leave out stay unchanged.
110
128
  - **Tags:** setting tags replaces the whole set. Tags that don't exist yet are created, and an empty array removes all tags.
111
129
  - **Publishing:** call `update_post` with `data.status="published"`, or with `"scheduled"` and a future `data.published_at`. To send the post by email, add `newsletter` (a slug) and, optionally, `email_segment`.
112
130
  - **Images:** `upload_image` takes `file_path`, an optional `purpose` (`image`, `profile_image` or `icon`) and `ref`.
@@ -172,7 +190,7 @@ The tests check catalog and tool consistency, request construction, file safety,
172
190
  ## FAQ
173
191
 
174
192
  **Does it work with self-hosted Ghost and Ghost(Pro)?**
175
- Yes, as long as you can reach the Admin API and have a key. Use the Admin domain of your site as `GHOST_API_URL`.
193
+ Yes, as long as you can reach the Admin API and have a key. Set `GHOST_API_URL` to the API URL shown on your custom integration page in Ghost Admin. This matters most for Ghost(Pro) and for sites behind a custom domain, because the server never follows redirects: an address that redirects to another one will fail.
176
194
 
177
195
  **Which clients does it support?**
178
196
  Any MCP client that can start a stdio server, including Claude Desktop, Claude Code and Cursor.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { assertSafePath, assertSafeSegment, buildForm, ghostRequest, GhostError, } from "./client.js";
6
6
  import { accessNote, filterRoutes, hintFor, matchRoute, routeKey, ROUTES } from "./routes.js";
7
- const SERVER_VERSION = "2.1.0";
7
+ const SERVER_VERSION = "2.2.0";
8
8
  const MAX_RESULT_CHARS = 100_000;
9
9
  /** Calls Ghost and, on 401, 403 and 404, adds what the route catalog knows about the failure. */
10
10
  async function request(method, path, query = {}, body, form, options) {
@@ -412,56 +412,61 @@ const STANDALONE_SPECS = [
412
412
  },
413
413
  ];
414
414
  const SEO_FIELDS = ["meta_title", "meta_description", "og_title", "og_description"];
415
- const seoTool = {
416
- name: "update_post_seo",
417
- description: "Shortcut to update SEO fields of a post (meta_title, meta_description, og_title, og_description). Requires id and the current updated_at. At least one SEO field must be provided. [PUT /admin/posts/{id}/]",
418
- inputSchema: {
419
- type: "object",
420
- properties: {
421
- id: { type: "string", description: "Post id." },
422
- updated_at: {
423
- type: "string",
424
- description: "Current updated_at value of the post, as returned by get_posts.",
415
+ function seoToolFor(kind) {
416
+ const path = `${kind}s`;
417
+ return {
418
+ name: `update_${kind}_seo`,
419
+ description: `Shortcut to update SEO fields of a ${kind} (meta_title, meta_description, og_title, og_description). Requires id and the current updated_at. At least one SEO field must be provided. [PUT /admin/${path}/{id}/]`,
420
+ inputSchema: {
421
+ type: "object",
422
+ properties: {
423
+ id: { type: "string", description: `${kind === "post" ? "Post" : "Page"} id.` },
424
+ updated_at: {
425
+ type: "string",
426
+ description: `Current updated_at value of the ${kind}, as returned by get_${path}.`,
427
+ },
428
+ meta_title: { type: "string", description: "SEO meta title." },
429
+ meta_description: { type: "string", description: "SEO meta description." },
430
+ og_title: { type: "string", description: "Open Graph title." },
431
+ og_description: { type: "string", description: "Open Graph description." },
425
432
  },
426
- meta_title: { type: "string", description: "SEO meta title." },
427
- meta_description: { type: "string", description: "SEO meta description." },
428
- og_title: { type: "string", description: "Open Graph title." },
429
- og_description: { type: "string", description: "Open Graph description." },
433
+ required: ["id", "updated_at"],
430
434
  },
431
- required: ["id", "updated_at"],
432
- },
433
- // Both overwrite existing values (SEO fields, or the whole tag list), so they are destructive.
434
- annotations: { readOnlyHint: false, destructiveHint: true },
435
- isMutation: () => true,
436
- run: async (args) => {
437
- const id = requireString(args, "id");
438
- const updated_at = requireString(args, "updated_at");
439
- const fields = {};
440
- for (const key of SEO_FIELDS) {
441
- const value = args[key];
442
- if (value !== undefined) {
443
- if (typeof value !== "string") {
444
- throw new Error(`Argument "${key}" must be a string.`);
435
+ // Both overwrite existing values (SEO fields, or the whole tag list), so they are destructive.
436
+ annotations: { readOnlyHint: false, destructiveHint: true },
437
+ isMutation: () => true,
438
+ run: async (args) => {
439
+ const id = requireString(args, "id");
440
+ const updated_at = requireString(args, "updated_at");
441
+ const fields = {};
442
+ for (const key of SEO_FIELDS) {
443
+ const value = args[key];
444
+ if (value !== undefined) {
445
+ if (typeof value !== "string") {
446
+ throw new Error(`Argument "${key}" must be a string.`);
447
+ }
448
+ fields[key] = value;
445
449
  }
446
- fields[key] = value;
447
450
  }
448
- }
449
- if (Object.keys(fields).length === 0) {
450
- throw new Error(`Provide at least one of: ${SEO_FIELDS.join(", ")}.`);
451
- }
452
- assertSafeSegment("id", id);
453
- const result = await request("PUT", `/posts/${encodeURIComponent(id)}/`, {}, { posts: [{ updated_at, ...fields }] });
454
- const post = result.posts?.[0] ?? {};
455
- return {
456
- id: post.id,
457
- updated_at: post.updated_at,
458
- meta_title: post.meta_title,
459
- meta_description: post.meta_description,
460
- og_title: post.og_title,
461
- og_description: post.og_description,
462
- };
463
- },
464
- };
451
+ if (Object.keys(fields).length === 0) {
452
+ throw new Error(`Provide at least one of: ${SEO_FIELDS.join(", ")}.`);
453
+ }
454
+ assertSafeSegment("id", id);
455
+ const result = await request("PUT", `/${path}/${encodeURIComponent(id)}/`, {}, { [path]: [{ updated_at, ...fields }] });
456
+ const record = result[path]?.[0] ?? {};
457
+ return {
458
+ id: record.id,
459
+ updated_at: record.updated_at,
460
+ meta_title: record.meta_title,
461
+ meta_description: record.meta_description,
462
+ og_title: record.og_title,
463
+ og_description: record.og_description,
464
+ };
465
+ },
466
+ };
467
+ }
468
+ const seoTool = seoToolFor("post");
469
+ const pageSeoTool = seoToolFor("page");
465
470
  const tagsTool = {
466
471
  name: "update_post_tags",
467
472
  description: "Shortcut to replace the tags of a post (tag names; missing tags are created). Requires id and the current updated_at. An empty list removes all tags. [PUT /admin/posts/{id}/]",
@@ -669,6 +674,7 @@ const TOOLS = [
669
674
  ...RESOURCES.flatMap(specsForResource).map(toolFromSpec),
670
675
  ...STANDALONE_SPECS.map(toolFromSpec),
671
676
  seoTool,
677
+ pageSeoTool,
672
678
  tagsTool,
673
679
  endpointsTool,
674
680
  rawTool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proars/ghost-mcp-server",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "MCP server for Ghost CMS: manage posts, pages, tags, SEO metadata, members, newsletters and themes through the Ghost Admin API from Claude, Cursor and other AI agents",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "dist",
11
11
  "README.md",
12
12
  "LICENSE",
13
+ "CHANGELOG.md",
13
14
  ".env.example"
14
15
  ],
15
16
  "type": "module",