@damusix/ghost-mcp 0.4.0 → 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.
- package/README.md +18 -0
- package/dist/index.mjs +51 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/skills/ghost-writing/SKILL.md +40 -0
- package/skills/ghost-writing/references/blocks.md +68 -0
- package/skills/ghost-writing/references/workflows.md +68 -0
- package/skills/ghost-writing/references/writing.md +48 -0
package/README.md
CHANGED
|
@@ -158,6 +158,24 @@ Search Ghost documentation via `docs.ghost.org/llms.txt`.
|
|
|
158
158
|
- **Admin mode** (default): Full access to all Ghost Admin API and Content API actions. Requires `GHOST_ADMIN_API_KEY`.
|
|
159
159
|
- **Content mode**: Read-only access to Content API actions only. Requires `GHOST_CONTENT_API_KEY`. Admin actions are rejected with a clear error.
|
|
160
160
|
|
|
161
|
+
## Bundled skill: `ghost-writing`
|
|
162
|
+
|
|
163
|
+
The package ships an [Agent Skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview)
|
|
164
|
+
at [`skills/ghost-writing/`](skills/ghost-writing/SKILL.md) that pairs with the
|
|
165
|
+
MCP server: which tool to use when, the full Koenig block catalog with guidance
|
|
166
|
+
on choosing between similar blocks, and best practices for structuring good
|
|
167
|
+
posts (drafting workflow, long-post `blockFile` iteration, publishing,
|
|
168
|
+
newsletters, members-only content).
|
|
169
|
+
|
|
170
|
+
Install it:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npx skills add damusix/ghost-mcp
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Claude picks it up automatically the next time a conversation involves writing
|
|
177
|
+
Ghost posts.
|
|
178
|
+
|
|
161
179
|
## Development
|
|
162
180
|
|
|
163
181
|
Spin up a throwaway Ghost 6 + MySQL 8 stack to exercise the server against a
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { z } from "zod";
|
|
@@ -143,7 +144,11 @@ const postWriteFields = {
|
|
|
143
144
|
"draft",
|
|
144
145
|
"scheduled"
|
|
145
146
|
]).optional().describe("Post status"),
|
|
146
|
-
tags: z.array(z.union([
|
|
147
|
+
tags: z.array(z.union([
|
|
148
|
+
z.string(),
|
|
149
|
+
z.object({ id: z.string() }),
|
|
150
|
+
z.object({ name: z.string() })
|
|
151
|
+
])).optional().describe("Tags to assign — a tag name string, or an object { id } or { name }"),
|
|
147
152
|
authors: z.array(z.object({ id: z.string() })).optional().describe("Authors to assign (by id)"),
|
|
148
153
|
featured: z.boolean().optional().describe("Whether the post is featured"),
|
|
149
154
|
visibility: z.string().optional().describe("Post visibility (public, members, paid, tiers)"),
|
|
@@ -295,7 +300,11 @@ const pageWriteFields = {
|
|
|
295
300
|
"draft",
|
|
296
301
|
"scheduled"
|
|
297
302
|
]).optional().describe("Page status"),
|
|
298
|
-
tags: z.array(z.union([
|
|
303
|
+
tags: z.array(z.union([
|
|
304
|
+
z.string(),
|
|
305
|
+
z.object({ id: z.string() }),
|
|
306
|
+
z.object({ name: z.string() })
|
|
307
|
+
])).optional().describe("Tags to assign — a tag name string, or an object { id } or { name }"),
|
|
299
308
|
authors: z.array(z.object({ id: z.string() })).optional().describe("Authors to assign (by id)"),
|
|
300
309
|
featured: z.boolean().optional().describe("Whether the page is featured"),
|
|
301
310
|
visibility: z.string().optional().describe("Page visibility"),
|
|
@@ -680,7 +689,7 @@ const adminNewsletterActions = [
|
|
|
680
689
|
];
|
|
681
690
|
//#endregion
|
|
682
691
|
//#region src/actions/admin/offers.ts
|
|
683
|
-
const browseParams$7 = z.object({
|
|
692
|
+
const browseParams$7 = z.object({ filter: z.string().optional().describe("NQL filter expression (e.g. \"status:active\" or \"status:archived\")") });
|
|
684
693
|
const readParams$8 = z.object({ id: z.string().describe("Offer ID") });
|
|
685
694
|
const addSchema$2 = z.object({
|
|
686
695
|
name: z.string().describe("Internal name for the offer (required)"),
|
|
@@ -712,7 +721,8 @@ const adminOfferActions = [
|
|
|
712
721
|
method: "GET",
|
|
713
722
|
path: "/offers/",
|
|
714
723
|
inputSchema: browseParams$7,
|
|
715
|
-
description: "Browse all offers"
|
|
724
|
+
description: "Browse all offers (Ghost returns the full list; filter by status)",
|
|
725
|
+
example: { filter: "status:active" }
|
|
716
726
|
},
|
|
717
727
|
{
|
|
718
728
|
name: "offers.read",
|
|
@@ -1295,7 +1305,32 @@ function initRegistry() {
|
|
|
1295
1305
|
}
|
|
1296
1306
|
initRegistry();
|
|
1297
1307
|
//#endregion
|
|
1308
|
+
//#region src/koenig/util.ts
|
|
1309
|
+
function isRecord(value) {
|
|
1310
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1311
|
+
}
|
|
1312
|
+
//#endregion
|
|
1298
1313
|
//#region src/tools/use-ghost-api.ts
|
|
1314
|
+
/**
|
|
1315
|
+
* Surface the Ghost API error body instead of just the HTTP status text.
|
|
1316
|
+
* `@logosdx/fetch` puts the parsed response on `err.data`; Ghost returns
|
|
1317
|
+
* `{ errors: [{ message, context, type, property, ... }] }`. Without this the
|
|
1318
|
+
* caller only sees "Unprocessable Entity" and cannot tell what actually failed.
|
|
1319
|
+
*/
|
|
1320
|
+
function formatApiError(err) {
|
|
1321
|
+
if (isRecord(err)) {
|
|
1322
|
+
const status = typeof err.status === "number" ? err.status : void 0;
|
|
1323
|
+
if (isRecord(err.data) && Array.isArray(err.data.errors) && err.data.errors.length > 0) return JSON.stringify({
|
|
1324
|
+
status,
|
|
1325
|
+
errors: err.data.errors
|
|
1326
|
+
});
|
|
1327
|
+
if (typeof err.message === "string") return JSON.stringify({
|
|
1328
|
+
status,
|
|
1329
|
+
error: err.message
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
return JSON.stringify({ error: String(err) });
|
|
1333
|
+
}
|
|
1299
1334
|
const extToMime = {};
|
|
1300
1335
|
for (const [mime, meta] of Object.entries(mimeDb)) for (const ext of meta.extensions ?? []) extToMime[`.${ext}`] = mime;
|
|
1301
1336
|
const useGhostApiSchema = z.object({
|
|
@@ -1358,30 +1393,30 @@ async function handleUseGhostApi(input, mode) {
|
|
|
1358
1393
|
if (actionDef.method === "GET") {
|
|
1359
1394
|
const queryParams = extractQueryParams(validPayload, usedPathParams);
|
|
1360
1395
|
const [response, err] = await attempt(async () => engine.get(path, { params: queryParams }));
|
|
1361
|
-
if (err) return
|
|
1362
|
-
return JSON.stringify(response);
|
|
1396
|
+
if (err) return formatApiError(err);
|
|
1397
|
+
return JSON.stringify(response.data);
|
|
1363
1398
|
}
|
|
1364
1399
|
if (actionDef.method === "DELETE") {
|
|
1365
1400
|
const [response, err] = await attempt(async () => engine.delete(path));
|
|
1366
|
-
if (err) return
|
|
1401
|
+
if (err) return formatApiError(err);
|
|
1367
1402
|
const resourcePrefix = `/${actionDef.name.split(".")[0]}`;
|
|
1368
1403
|
await engine.invalidatePath(resourcePrefix);
|
|
1369
|
-
return JSON.stringify(response ?? { success: true });
|
|
1404
|
+
return JSON.stringify(response?.data ?? { success: true });
|
|
1370
1405
|
}
|
|
1371
1406
|
const resourceKey = actionDef.name.split(".")[0];
|
|
1372
1407
|
const body = extractBodyPayload(validPayload, usedPathParams);
|
|
1373
1408
|
const wrappedBody = { [resourceKey]: [body] };
|
|
1374
1409
|
if (actionDef.method === "POST") {
|
|
1375
1410
|
const [response, err] = await attempt(async () => engine.post(path, wrappedBody));
|
|
1376
|
-
if (err) return
|
|
1377
|
-
return JSON.stringify(response);
|
|
1411
|
+
if (err) return formatApiError(err);
|
|
1412
|
+
return JSON.stringify(response.data);
|
|
1378
1413
|
}
|
|
1379
1414
|
if (actionDef.method === "PUT") {
|
|
1380
1415
|
const [response, err] = await attempt(async () => engine.put(path, wrappedBody));
|
|
1381
|
-
if (err) return
|
|
1416
|
+
if (err) return formatApiError(err);
|
|
1382
1417
|
const resourcePrefix = `/${actionDef.name.split(".")[0]}`;
|
|
1383
1418
|
await engine.invalidatePath(resourcePrefix);
|
|
1384
|
-
return JSON.stringify(response);
|
|
1419
|
+
return JSON.stringify(response.data);
|
|
1385
1420
|
}
|
|
1386
1421
|
return JSON.stringify({ error: `Unsupported method: ${actionDef.method}` });
|
|
1387
1422
|
}
|
|
@@ -1407,8 +1442,8 @@ async function handleFileUpload(actionName, payload, path) {
|
|
|
1407
1442
|
const [response, err] = await attempt(async () => adminApi.post(path, formData, { onBeforeReq: (opts) => {
|
|
1408
1443
|
delete opts.headers["Content-Type"];
|
|
1409
1444
|
} }));
|
|
1410
|
-
if (err) return
|
|
1411
|
-
return JSON.stringify(response);
|
|
1445
|
+
if (err) return formatApiError(err);
|
|
1446
|
+
return JSON.stringify(response.data);
|
|
1412
1447
|
}
|
|
1413
1448
|
//#endregion
|
|
1414
1449
|
//#region src/tools/ghost-api-help.ts
|
|
@@ -1784,11 +1819,6 @@ const NODE_SPECS = {
|
|
|
1784
1819
|
}
|
|
1785
1820
|
};
|
|
1786
1821
|
//#endregion
|
|
1787
|
-
//#region src/koenig/util.ts
|
|
1788
|
-
function isRecord(value) {
|
|
1789
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1790
|
-
}
|
|
1791
|
-
//#endregion
|
|
1792
1822
|
//#region src/koenig/cards.ts
|
|
1793
1823
|
function passthrough(def, fields) {
|
|
1794
1824
|
const spec = NODE_SPECS[def.nodeType];
|
|
@@ -2477,12 +2507,13 @@ const koenigHelpSchema = z.object({ block: z.string().optional().describe("A blo
|
|
|
2477
2507
|
function handleKoenigHelp(input) {
|
|
2478
2508
|
return blockHelp(input.block);
|
|
2479
2509
|
}
|
|
2510
|
+
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
2480
2511
|
//#endregion
|
|
2481
2512
|
//#region src/index.ts
|
|
2482
2513
|
const GHOST_API_MODE = process.env.GHOST_API_MODE || "admin";
|
|
2483
2514
|
const server = new McpServer({
|
|
2484
2515
|
name: "ghost-mcp",
|
|
2485
|
-
version:
|
|
2516
|
+
version: VERSION
|
|
2486
2517
|
});
|
|
2487
2518
|
server.tool("use_ghost_api", "Execute a Ghost API action (browse, read, add, edit, delete posts, pages, tags, members, newsletters, and more)", useGhostApiSchema.shape, async ({ api, action, payload }) => {
|
|
2488
2519
|
return { content: [{
|