@proars/ghost-mcp-server 0.0.0-stage → 2.1.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/.env.example +2 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/catalog.js +261 -0
- package/dist/client.js +249 -0
- package/dist/files.js +158 -0
- package/dist/index.js +727 -0
- package/dist/routes.js +101 -0
- package/package.json +60 -4
package/dist/routes.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { CATALOG } from "./catalog.js";
|
|
2
|
+
export const ROUTES = CATALOG.map(([method, path, handler, access, group, source, summary, extra]) => ({
|
|
3
|
+
method,
|
|
4
|
+
path,
|
|
5
|
+
handler,
|
|
6
|
+
access,
|
|
7
|
+
group,
|
|
8
|
+
documented: source === "doc",
|
|
9
|
+
summary,
|
|
10
|
+
...extra,
|
|
11
|
+
}));
|
|
12
|
+
/** Key that ignores parameter names, so /posts/{id}/ and /posts/{post_id}/ are the same route. */
|
|
13
|
+
export function routeKey(method, path) {
|
|
14
|
+
return `${method} ${path.replace(/\{\w+\}/g, "{}")}`;
|
|
15
|
+
}
|
|
16
|
+
const COMPILED = ROUTES.map((route) => {
|
|
17
|
+
const segments = route.path.split("/").filter((segment) => segment.length > 0);
|
|
18
|
+
const source = segments
|
|
19
|
+
.map((segment) => /^\{\w+\}$/.test(segment) ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
20
|
+
.join("/");
|
|
21
|
+
return {
|
|
22
|
+
route,
|
|
23
|
+
pattern: new RegExp(`^/${source}/$`),
|
|
24
|
+
paramCount: segments.filter((segment) => /^\{\w+\}$/.test(segment)).length,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
/** Finds the catalog route for a concrete request path. Static segments win over parameters. */
|
|
28
|
+
export function matchRoute(method, path) {
|
|
29
|
+
const normalized = path.endsWith("/") ? path : `${path}/`;
|
|
30
|
+
let best;
|
|
31
|
+
for (const candidate of COMPILED) {
|
|
32
|
+
if (candidate.route.method !== method || !candidate.pattern.test(normalized)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!best || candidate.paramCount < best.paramCount) {
|
|
36
|
+
best = candidate;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return best?.route;
|
|
40
|
+
}
|
|
41
|
+
const ACCESS_HINTS = {
|
|
42
|
+
PUBLIC: "This route is public and needs no authentication.",
|
|
43
|
+
I: "This route is on the integration-key allowlist, but Ghost still checks permissions, feature flags and your Ghost version.",
|
|
44
|
+
U: "This route is not available to Admin API integration keys: it needs a staff user's access token (from the user's profile page in Ghost Admin, set as GHOST_ADMIN_API_KEY) with a role that allows it, or a user session.",
|
|
45
|
+
SESSION: "This route needs a user cookie session. Neither integration keys nor staff access tokens can call it.",
|
|
46
|
+
FLOW: "This route belongs to a browser login, recovery or setup flow and cannot be used with an integration key.",
|
|
47
|
+
};
|
|
48
|
+
/** Sentence for a tool description when its route is closed to Admin API integration keys. */
|
|
49
|
+
export function accessNote(method, path) {
|
|
50
|
+
const key = routeKey(method, path);
|
|
51
|
+
const route = ROUTES.find((candidate) => routeKey(candidate.method, candidate.path) === key);
|
|
52
|
+
if (!route || route.access === "I" || route.access === "PUBLIC") {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
return ` ${ACCESS_HINTS[route.access]}`;
|
|
56
|
+
}
|
|
57
|
+
/** Explains a 401, 403 or 404 using what the catalog knows about the route. */
|
|
58
|
+
export function hintFor(method, path, status) {
|
|
59
|
+
if (status !== 401 && status !== 403 && status !== 404) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const route = matchRoute(method, path);
|
|
63
|
+
if (!route) {
|
|
64
|
+
return status === 404
|
|
65
|
+
? "This path is not in the route catalog; check it with ghost_list_endpoints."
|
|
66
|
+
: undefined;
|
|
67
|
+
}
|
|
68
|
+
const flag = route.flag ? ` Feature flag: ${route.flag}.` : "";
|
|
69
|
+
if (status === 404) {
|
|
70
|
+
return `The route exists in the catalog but Ghost returned 404: it may be missing in your Ghost version, disabled by a feature flag, or the record does not exist.${flag}`;
|
|
71
|
+
}
|
|
72
|
+
return `${ACCESS_HINTS[route.access]}${flag}`;
|
|
73
|
+
}
|
|
74
|
+
export function filterRoutes(filter, toolForRoute) {
|
|
75
|
+
const group = filter.group?.toLowerCase();
|
|
76
|
+
const search = filter.search?.toLowerCase();
|
|
77
|
+
return ROUTES.filter((route) => {
|
|
78
|
+
if (filter.method && route.method !== filter.method.toUpperCase()) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (filter.access && route.access !== filter.access.toUpperCase()) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (group) {
|
|
85
|
+
const firstSegment = route.path.split("/")[1] ?? "";
|
|
86
|
+
if (!route.group.toLowerCase().includes(group) && !firstSegment.toLowerCase().includes(group)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (search) {
|
|
91
|
+
const haystack = `${route.path} ${route.summary} ${route.handler}`.toLowerCase();
|
|
92
|
+
if (!haystack.includes(search)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (filter.withoutTool && toolForRoute(route)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proars/ghost-mcp-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.1.1",
|
|
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
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ghost-mcp-server": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
".env.example"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"test": "node --import tsx --test test/*.test.mjs",
|
|
21
|
+
"catalog": "node scripts/generate-catalog.mjs",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"prepublishOnly": "npm run build && npm test"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
27
|
+
"dotenv": "^16.4.5"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.11.0",
|
|
31
|
+
"tsx": "^4.7.1",
|
|
32
|
+
"typescript": "^5.3.3"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"ghost",
|
|
36
|
+
"ghost-cms",
|
|
37
|
+
"mcp",
|
|
38
|
+
"mcp-server",
|
|
39
|
+
"model-context-protocol",
|
|
40
|
+
"claude",
|
|
41
|
+
"cursor",
|
|
42
|
+
"ai-agents",
|
|
43
|
+
"ghost-admin-api",
|
|
44
|
+
"seo"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/proars/ghost-mcp-server.git"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/proars/ghost-mcp-server/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/proars/ghost-mcp-server#readme",
|
|
55
|
+
"author": "proars",
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=20"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
}
|
|
62
|
+
}
|