@jgardner04/ghost-mcp-server 1.13.4 → 1.13.5
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 +68 -0
- package/package.json +7 -3
- package/src/__tests__/helpers/testUtils.js +15 -1
- package/src/__tests__/mcp_server.test.js +69 -1
- package/src/__tests__/mcp_server_pages.test.js +23 -6
- package/src/mcp_server.js +393 -1143
- package/src/services/__tests__/createResourceService.test.js +468 -0
- package/src/services/__tests__/ghostServiceImproved.members.test.js +0 -3
- package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +2 -2
- package/src/services/__tests__/ghostServiceImproved.pages.test.js +0 -3
- package/src/services/__tests__/ghostServiceImproved.posts.test.js +0 -1
- package/src/services/__tests__/ghostServiceImproved.tags.test.js +0 -3
- package/src/services/__tests__/ghostServiceImproved.tiers.test.js +3 -5
- package/src/services/createResourceService.js +138 -0
- package/src/services/ghostApiClient.js +240 -0
- package/src/services/ghostServiceImproved.js +76 -915
- package/src/services/images.js +27 -0
- package/src/services/members.js +127 -0
- package/src/services/newsletters.js +63 -0
- package/src/services/pages.js +116 -0
- package/src/services/posts.js +116 -0
- package/src/services/tags.js +118 -0
- package/src/services/tiers.js +72 -0
- package/src/services/validators.js +218 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { ValidationError, NotFoundError } from '../errors/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Input validation helpers
|
|
6
|
+
*/
|
|
7
|
+
export const validators = {
|
|
8
|
+
/**
|
|
9
|
+
* Validates that an ID is a non-empty string. Used by CRUD helpers to enforce
|
|
10
|
+
* consistent ID validation across all resource types.
|
|
11
|
+
* @param {string} id - The resource ID to validate
|
|
12
|
+
* @param {string} entityName - Human-readable resource name for error messages (e.g., 'Post', 'Tag')
|
|
13
|
+
* @throws {ValidationError} If the ID is falsy, not a string, or empty/whitespace
|
|
14
|
+
*/
|
|
15
|
+
requireId(id, entityName) {
|
|
16
|
+
if (!id || typeof id !== 'string' || id.trim().length === 0) {
|
|
17
|
+
throw new ValidationError(`${entityName} ID is required`);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validates scheduling fields for posts and pages. Ensures published_at is present
|
|
23
|
+
* when status is 'scheduled' and that the date is valid and in the future.
|
|
24
|
+
* @param {Object} data - The resource data containing status and/or published_at
|
|
25
|
+
* @param {string} [data.status] - Resource status ('draft', 'published', 'scheduled')
|
|
26
|
+
* @param {string} [data.published_at] - ISO 8601 date string for scheduling
|
|
27
|
+
* @param {string} [resourceLabel='Resource'] - Human-readable label for error messages
|
|
28
|
+
* @throws {ValidationError} If scheduling validation fails
|
|
29
|
+
*/
|
|
30
|
+
validateScheduledStatus(data, resourceLabel = 'Resource') {
|
|
31
|
+
const errors = [];
|
|
32
|
+
|
|
33
|
+
if (data.status === 'scheduled' && !data.published_at) {
|
|
34
|
+
errors.push({
|
|
35
|
+
field: 'published_at',
|
|
36
|
+
message: 'published_at is required when status is scheduled',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (data.published_at) {
|
|
41
|
+
const publishDate = new Date(data.published_at);
|
|
42
|
+
if (isNaN(publishDate.getTime())) {
|
|
43
|
+
errors.push({ field: 'published_at', message: 'Invalid date format' });
|
|
44
|
+
} else if (data.status === 'scheduled' && publishDate <= new Date()) {
|
|
45
|
+
errors.push({ field: 'published_at', message: 'Scheduled date must be in the future' });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (errors.length > 0) {
|
|
50
|
+
throw new ValidationError(`${resourceLabel} validation failed`, errors);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validates post creation data. Requires a title and either html or mobiledoc content.
|
|
56
|
+
* Also validates status values and delegates to validateScheduledStatus for scheduling.
|
|
57
|
+
* @param {Object} postData - The post data to validate
|
|
58
|
+
* @param {string} postData.title - Post title (required, non-empty)
|
|
59
|
+
* @param {string} [postData.html] - HTML content (required if mobiledoc not provided)
|
|
60
|
+
* @param {string} [postData.mobiledoc] - Mobiledoc content (required if html not provided)
|
|
61
|
+
* @param {string} [postData.status] - Post status ('draft', 'published', 'scheduled')
|
|
62
|
+
* @param {string} [postData.published_at] - ISO 8601 date for scheduled posts
|
|
63
|
+
* @throws {ValidationError} If validation fails
|
|
64
|
+
*/
|
|
65
|
+
validatePostData(postData) {
|
|
66
|
+
const errors = [];
|
|
67
|
+
|
|
68
|
+
if (!postData.title || postData.title.trim().length === 0) {
|
|
69
|
+
errors.push({ field: 'title', message: 'Title is required' });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!postData.html && !postData.mobiledoc) {
|
|
73
|
+
errors.push({ field: 'content', message: 'Either html or mobiledoc content is required' });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (postData.status && !['draft', 'published', 'scheduled'].includes(postData.status)) {
|
|
77
|
+
errors.push({
|
|
78
|
+
field: 'status',
|
|
79
|
+
message: 'Invalid status. Must be draft, published, or scheduled',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (errors.length > 0) {
|
|
84
|
+
throw new ValidationError('Post validation failed', errors);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.validateScheduledStatus(postData, 'Post');
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validates tag creation data. Requires a non-empty name and validates slug format.
|
|
92
|
+
* @param {Object} tagData - The tag data to validate
|
|
93
|
+
* @param {string} tagData.name - Tag name (required, non-empty)
|
|
94
|
+
* @param {string} [tagData.slug] - Tag slug (lowercase letters, numbers, hyphens only)
|
|
95
|
+
* @throws {ValidationError} If validation fails
|
|
96
|
+
*/
|
|
97
|
+
validateTagData(tagData) {
|
|
98
|
+
const errors = [];
|
|
99
|
+
|
|
100
|
+
if (!tagData.name || tagData.name.trim().length === 0) {
|
|
101
|
+
errors.push({ field: 'name', message: 'Tag name is required' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (tagData.slug && !/^[a-z0-9-]+$/.test(tagData.slug)) {
|
|
105
|
+
errors.push({
|
|
106
|
+
field: 'slug',
|
|
107
|
+
message: 'Slug must contain only lowercase letters, numbers, and hyphens',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (errors.length > 0) {
|
|
112
|
+
throw new ValidationError('Tag validation failed', errors);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validates tag update data. Name is optional but cannot be empty if provided.
|
|
118
|
+
* Validates slug format if provided.
|
|
119
|
+
* @param {Object} updateData - The tag update data to validate
|
|
120
|
+
* @param {string} [updateData.name] - Tag name (cannot be empty if provided)
|
|
121
|
+
* @param {string} [updateData.slug] - Tag slug (lowercase letters, numbers, hyphens only)
|
|
122
|
+
* @throws {ValidationError} If validation fails
|
|
123
|
+
*/
|
|
124
|
+
validateTagUpdateData(updateData) {
|
|
125
|
+
const errors = [];
|
|
126
|
+
|
|
127
|
+
// Name is optional in updates, but if provided, it cannot be empty
|
|
128
|
+
if (updateData.name !== undefined && updateData.name.trim().length === 0) {
|
|
129
|
+
errors.push({ field: 'name', message: 'Tag name cannot be empty' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Validate slug format if provided
|
|
133
|
+
if (updateData.slug && !/^[a-z0-9-]+$/.test(updateData.slug)) {
|
|
134
|
+
errors.push({
|
|
135
|
+
field: 'slug',
|
|
136
|
+
message: 'Slug must contain only lowercase letters, numbers, and hyphens',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (errors.length > 0) {
|
|
141
|
+
throw new ValidationError('Tag update validation failed', errors);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validates that an image path is a non-empty string and that the file exists on disk.
|
|
147
|
+
* @param {string} imagePath - Absolute path to the image file
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
* @throws {ValidationError} If imagePath is falsy or not a string
|
|
150
|
+
* @throws {NotFoundError} If the file does not exist at the given path
|
|
151
|
+
*/
|
|
152
|
+
async validateImagePath(imagePath) {
|
|
153
|
+
if (!imagePath || typeof imagePath !== 'string') {
|
|
154
|
+
throw new ValidationError('Image path is required and must be a string');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check if file exists
|
|
158
|
+
try {
|
|
159
|
+
await fs.access(imagePath);
|
|
160
|
+
} catch {
|
|
161
|
+
throw new NotFoundError('Image file', imagePath);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validates page creation data. Requires a title and either html or mobiledoc content.
|
|
167
|
+
* Also validates status values and delegates to validateScheduledStatus for scheduling.
|
|
168
|
+
* @param {Object} pageData - The page data to validate
|
|
169
|
+
* @param {string} pageData.title - Page title (required, non-empty)
|
|
170
|
+
* @param {string} [pageData.html] - HTML content (required if mobiledoc not provided)
|
|
171
|
+
* @param {string} [pageData.mobiledoc] - Mobiledoc content (required if html not provided)
|
|
172
|
+
* @param {string} [pageData.status] - Page status ('draft', 'published', 'scheduled')
|
|
173
|
+
* @param {string} [pageData.published_at] - ISO 8601 date for scheduled pages
|
|
174
|
+
* @throws {ValidationError} If validation fails
|
|
175
|
+
*/
|
|
176
|
+
validatePageData(pageData) {
|
|
177
|
+
const errors = [];
|
|
178
|
+
|
|
179
|
+
if (!pageData.title || pageData.title.trim().length === 0) {
|
|
180
|
+
errors.push({ field: 'title', message: 'Title is required' });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!pageData.html && !pageData.mobiledoc) {
|
|
184
|
+
errors.push({ field: 'content', message: 'Either html or mobiledoc content is required' });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (pageData.status && !['draft', 'published', 'scheduled'].includes(pageData.status)) {
|
|
188
|
+
errors.push({
|
|
189
|
+
field: 'status',
|
|
190
|
+
message: 'Invalid status. Must be draft, published, or scheduled',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (errors.length > 0) {
|
|
195
|
+
throw new ValidationError('Page validation failed', errors);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.validateScheduledStatus(pageData, 'Page');
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validates newsletter creation data. Requires a non-empty name.
|
|
203
|
+
* @param {Object} newsletterData - The newsletter data to validate
|
|
204
|
+
* @param {string} newsletterData.name - Newsletter name (required, non-empty)
|
|
205
|
+
* @throws {ValidationError} If validation fails
|
|
206
|
+
*/
|
|
207
|
+
validateNewsletterData(newsletterData) {
|
|
208
|
+
const errors = [];
|
|
209
|
+
|
|
210
|
+
if (!newsletterData.name || newsletterData.name.trim().length === 0) {
|
|
211
|
+
errors.push({ field: 'name', message: 'Newsletter name is required' });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (errors.length > 0) {
|
|
215
|
+
throw new ValidationError('Newsletter validation failed', errors);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
};
|