@proars/ghost-mcp-server 2.1.1 → 2.2.1

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 CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project are listed here. The format follows [Keep a
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.2.1] - 2026-11-02
8
+
9
+ ### Added
10
+
11
+ - `mcpName` in `package.json` and a `server.json` file, so the server can be listed in the official MCP Registry.
12
+
13
+ ## [2.2.0] - 2026-11-02
14
+
15
+ ### Added
16
+
17
+ - `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.
18
+ - README: npm version badge and a demo GIF (`docs/demo.gif`).
19
+ - README: JSON `args` example for the update command, and guidance on the API URL for Ghost(Pro) and custom domains.
20
+
7
21
  ## [2.1.1] - 2026-11-01
8
22
 
9
23
  ### Changed
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
 
@@ -55,11 +58,17 @@ claude mcp add ghost --scope user \
55
58
 
56
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`.
57
60
 
58
- **Updating:** `npx` keeps a cached copy and may not fetch a new release right away. To update, run `rm -rf ~/.npm/_npx` and restart your client, or use `@proars/ghost-mcp-server@latest` in the command so npx checks the registry on every start. To stay on one version, pin it, for example `@proars/ghost-mcp-server@2.1.0`. See the [changelog](CHANGELOG.md) for what changed in each release.
61
+ Any other stdio MCP client can run the same command, `npx -y @proars/ghost-mcp-server`, with the two environment variables.
59
62
 
60
- 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.
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:
61
64
 
62
- Any other stdio MCP client can run the same command, `npx -y @proars/ghost-mcp-server`, with the two environment variables.
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.
63
72
 
64
73
  ## Install from source
65
74
 
@@ -104,17 +113,18 @@ The `.env` file in the project root is read regardless of the directory the clie
104
113
  | Images and themes | `upload_image`, `upload_theme`, `activate_theme` |
105
114
  | Webhooks | `create_webhook`, `update_webhook`, `delete_webhook` |
106
115
  | Site | `get_site`, `get_settings` |
107
- | Shortcuts | `update_post_seo`, `update_post_tags` |
116
+ | Shortcuts | `update_post_seo`, `update_page_seo`, `update_post_tags` |
108
117
  | Discovery and generic calls | `ghost_list_endpoints`, `ghost_api_request` |
109
118
 
110
119
  Ghost has no delete endpoint for tiers, newsletters or offers, so those resources have no `delete_*` tool.
111
120
 
112
- > **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.
113
122
 
114
123
  ### How do I call the tools?
115
124
 
116
- - **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.
117
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.
118
128
  - **Tags:** setting tags replaces the whole set. Tags that don't exist yet are created, and an empty array removes all tags.
119
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`.
120
130
  - **Images:** `upload_image` takes `file_path`, an optional `purpose` (`image`, `profile_image` or `icon`) and `ref`.
@@ -180,7 +190,7 @@ The tests check catalog and tool consistency, request construction, file safety,
180
190
  ## FAQ
181
191
 
182
192
  **Does it work with self-hosted Ghost and Ghost(Pro)?**
183
- 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.
184
194
 
185
195
  **Which clients does it support?**
186
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.1";
7
+ const SERVER_VERSION = "2.2.1";
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,7 @@
1
1
  {
2
2
  "name": "@proars/ghost-mcp-server",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
+ "mcpName": "io.github.proars/ghost-mcp-server",
4
5
  "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
6
  "main": "dist/index.js",
6
7
  "bin": {
@@ -34,13 +35,20 @@
34
35
  "keywords": [
35
36
  "ghost",
36
37
  "ghost-cms",
38
+ "ghost-mcp-server",
39
+ "ghost-cms-mcp",
40
+ "ghost-admin-api-mcp",
41
+ "ghost-api",
37
42
  "mcp",
38
43
  "mcp-server",
39
44
  "model-context-protocol",
40
45
  "claude",
46
+ "claude-code",
47
+ "claude-desktop",
41
48
  "cursor",
42
49
  "ai-agents",
43
50
  "ghost-admin-api",
51
+ "typescript",
44
52
  "seo"
45
53
  ],
46
54
  "license": "MIT",