@jgardner04/ghost-mcp-server 1.7.0 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgardner04/ghost-mcp-server",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "A Model Context Protocol (MCP) server for interacting with Ghost CMS via the Admin API",
5
5
  "author": "Jonathan Gardner",
6
6
  "type": "module",
@@ -0,0 +1,275 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ emailSchema,
4
+ urlSchema,
5
+ isoDateSchema,
6
+ slugSchema,
7
+ ghostIdSchema,
8
+ nqlFilterSchema,
9
+ limitSchema,
10
+ pageSchema,
11
+ postStatusSchema,
12
+ visibilitySchema,
13
+ htmlContentSchema,
14
+ titleSchema,
15
+ excerptSchema,
16
+ metaTitleSchema,
17
+ metaDescriptionSchema,
18
+ featuredSchema,
19
+ featureImageSchema,
20
+ featureImageAltSchema,
21
+ tagNameSchema,
22
+ } from '../common.js';
23
+
24
+ describe('Common Schemas', () => {
25
+ describe('emailSchema', () => {
26
+ it('should accept valid email addresses', () => {
27
+ expect(() => emailSchema.parse('test@example.com')).not.toThrow();
28
+ expect(() => emailSchema.parse('user.name+tag@example.co.uk')).not.toThrow();
29
+ });
30
+
31
+ it('should reject invalid email addresses', () => {
32
+ expect(() => emailSchema.parse('not-an-email')).toThrow();
33
+ expect(() => emailSchema.parse('missing@domain')).toThrow();
34
+ expect(() => emailSchema.parse('@example.com')).toThrow();
35
+ });
36
+ });
37
+
38
+ describe('urlSchema', () => {
39
+ it('should accept valid URLs', () => {
40
+ expect(() => urlSchema.parse('https://example.com')).not.toThrow();
41
+ expect(() => urlSchema.parse('http://localhost:3000/path')).not.toThrow();
42
+ });
43
+
44
+ it('should reject invalid URLs', () => {
45
+ expect(() => urlSchema.parse('not-a-url')).toThrow();
46
+ expect(() => urlSchema.parse('://invalid')).toThrow();
47
+ });
48
+ });
49
+
50
+ describe('isoDateSchema', () => {
51
+ it('should accept valid ISO 8601 datetime strings', () => {
52
+ expect(() => isoDateSchema.parse('2024-01-15T10:30:00Z')).not.toThrow();
53
+ expect(() => isoDateSchema.parse('2024-01-15T10:30:00.000Z')).not.toThrow();
54
+ });
55
+
56
+ it('should reject invalid datetime strings', () => {
57
+ expect(() => isoDateSchema.parse('2024-01-15')).toThrow();
58
+ expect(() => isoDateSchema.parse('not-a-date')).toThrow();
59
+ });
60
+ });
61
+
62
+ describe('slugSchema', () => {
63
+ it('should accept valid slugs', () => {
64
+ expect(() => slugSchema.parse('my-blog-post')).not.toThrow();
65
+ expect(() => slugSchema.parse('post-123')).not.toThrow();
66
+ expect(() => slugSchema.parse('simple')).not.toThrow();
67
+ });
68
+
69
+ it('should reject invalid slugs', () => {
70
+ expect(() => slugSchema.parse('My Post')).toThrow(); // spaces
71
+ expect(() => slugSchema.parse('post_123')).toThrow(); // underscores
72
+ expect(() => slugSchema.parse('Post-123')).toThrow(); // uppercase
73
+ expect(() => slugSchema.parse('post!')).toThrow(); // special chars
74
+ });
75
+ });
76
+
77
+ describe('ghostIdSchema', () => {
78
+ it('should accept valid Ghost IDs', () => {
79
+ expect(() => ghostIdSchema.parse('507f1f77bcf86cd799439011')).not.toThrow();
80
+ expect(() => ghostIdSchema.parse('abcdef1234567890abcdef12')).not.toThrow();
81
+ });
82
+
83
+ it('should reject invalid Ghost IDs', () => {
84
+ expect(() => ghostIdSchema.parse('short')).toThrow(); // too short
85
+ expect(() => ghostIdSchema.parse('507f1f77bcf86cd799439011abc')).toThrow(); // too long
86
+ expect(() => ghostIdSchema.parse('507f1f77bcf86cd79943901G')).toThrow(); // invalid char
87
+ expect(() => ghostIdSchema.parse('507F1F77BCF86CD799439011')).toThrow(); // uppercase
88
+ });
89
+ });
90
+
91
+ describe('nqlFilterSchema', () => {
92
+ it('should accept valid NQL filter strings', () => {
93
+ expect(() => nqlFilterSchema.parse('status:published')).not.toThrow();
94
+ expect(() => nqlFilterSchema.parse('tag:news+featured:true')).not.toThrow();
95
+ expect(() => nqlFilterSchema.parse("author:'John Doe'")).not.toThrow();
96
+ });
97
+
98
+ it('should reject NQL strings with disallowed characters', () => {
99
+ expect(() => nqlFilterSchema.parse('status;DROP TABLE')).toThrow();
100
+ expect(() => nqlFilterSchema.parse('test&invalid')).toThrow();
101
+ });
102
+
103
+ it('should allow undefined/optional', () => {
104
+ expect(() => nqlFilterSchema.parse(undefined)).not.toThrow();
105
+ });
106
+ });
107
+
108
+ describe('limitSchema', () => {
109
+ it('should accept valid limits', () => {
110
+ expect(limitSchema.parse(1)).toBe(1);
111
+ expect(limitSchema.parse(50)).toBe(50);
112
+ expect(limitSchema.parse(100)).toBe(100);
113
+ });
114
+
115
+ it('should reject invalid limits', () => {
116
+ expect(() => limitSchema.parse(0)).toThrow();
117
+ expect(() => limitSchema.parse(101)).toThrow();
118
+ expect(() => limitSchema.parse(-1)).toThrow();
119
+ expect(() => limitSchema.parse(1.5)).toThrow();
120
+ });
121
+
122
+ it('should use default value', () => {
123
+ expect(limitSchema.parse(undefined)).toBe(15);
124
+ });
125
+ });
126
+
127
+ describe('pageSchema', () => {
128
+ it('should accept valid page numbers', () => {
129
+ expect(pageSchema.parse(1)).toBe(1);
130
+ expect(pageSchema.parse(100)).toBe(100);
131
+ });
132
+
133
+ it('should reject invalid page numbers', () => {
134
+ expect(() => pageSchema.parse(0)).toThrow();
135
+ expect(() => pageSchema.parse(-1)).toThrow();
136
+ expect(() => pageSchema.parse(1.5)).toThrow();
137
+ });
138
+
139
+ it('should use default value', () => {
140
+ expect(pageSchema.parse(undefined)).toBe(1);
141
+ });
142
+ });
143
+
144
+ describe('postStatusSchema', () => {
145
+ it('should accept valid statuses', () => {
146
+ expect(() => postStatusSchema.parse('draft')).not.toThrow();
147
+ expect(() => postStatusSchema.parse('published')).not.toThrow();
148
+ expect(() => postStatusSchema.parse('scheduled')).not.toThrow();
149
+ });
150
+
151
+ it('should reject invalid statuses', () => {
152
+ expect(() => postStatusSchema.parse('invalid')).toThrow();
153
+ expect(() => postStatusSchema.parse('DRAFT')).toThrow();
154
+ });
155
+ });
156
+
157
+ describe('visibilitySchema', () => {
158
+ it('should accept valid visibility values', () => {
159
+ expect(() => visibilitySchema.parse('public')).not.toThrow();
160
+ expect(() => visibilitySchema.parse('members')).not.toThrow();
161
+ expect(() => visibilitySchema.parse('paid')).not.toThrow();
162
+ expect(() => visibilitySchema.parse('tiers')).not.toThrow();
163
+ });
164
+
165
+ it('should reject invalid visibility values', () => {
166
+ expect(() => visibilitySchema.parse('private')).toThrow();
167
+ expect(() => visibilitySchema.parse('PUBLIC')).toThrow();
168
+ });
169
+ });
170
+
171
+ describe('htmlContentSchema', () => {
172
+ it('should accept non-empty HTML strings', () => {
173
+ expect(() => htmlContentSchema.parse('<p>Hello World</p>')).not.toThrow();
174
+ expect(() => htmlContentSchema.parse('Plain text')).not.toThrow();
175
+ });
176
+
177
+ it('should reject empty strings', () => {
178
+ expect(() => htmlContentSchema.parse('')).toThrow();
179
+ });
180
+ });
181
+
182
+ describe('titleSchema', () => {
183
+ it('should accept valid titles', () => {
184
+ expect(() => titleSchema.parse('My Blog Post')).not.toThrow();
185
+ expect(() => titleSchema.parse('A'.repeat(255))).not.toThrow();
186
+ });
187
+
188
+ it('should reject invalid titles', () => {
189
+ expect(() => titleSchema.parse('')).toThrow();
190
+ expect(() => titleSchema.parse('A'.repeat(256))).toThrow();
191
+ });
192
+ });
193
+
194
+ describe('excerptSchema', () => {
195
+ it('should accept valid excerpts', () => {
196
+ expect(() => excerptSchema.parse('A short description')).not.toThrow();
197
+ expect(() => excerptSchema.parse('A'.repeat(500))).not.toThrow();
198
+ expect(() => excerptSchema.parse(undefined)).not.toThrow();
199
+ });
200
+
201
+ it('should reject too long excerpts', () => {
202
+ expect(() => excerptSchema.parse('A'.repeat(501))).toThrow();
203
+ });
204
+ });
205
+
206
+ describe('metaTitleSchema', () => {
207
+ it('should accept valid meta titles', () => {
208
+ expect(() => metaTitleSchema.parse('SEO Title')).not.toThrow();
209
+ expect(() => metaTitleSchema.parse('A'.repeat(300))).not.toThrow();
210
+ expect(() => metaTitleSchema.parse(undefined)).not.toThrow();
211
+ });
212
+
213
+ it('should reject too long meta titles', () => {
214
+ expect(() => metaTitleSchema.parse('A'.repeat(301))).toThrow();
215
+ });
216
+ });
217
+
218
+ describe('metaDescriptionSchema', () => {
219
+ it('should accept valid meta descriptions', () => {
220
+ expect(() => metaDescriptionSchema.parse('SEO description')).not.toThrow();
221
+ expect(() => metaDescriptionSchema.parse('A'.repeat(500))).not.toThrow();
222
+ expect(() => metaDescriptionSchema.parse(undefined)).not.toThrow();
223
+ });
224
+
225
+ it('should reject too long meta descriptions', () => {
226
+ expect(() => metaDescriptionSchema.parse('A'.repeat(501))).toThrow();
227
+ });
228
+ });
229
+
230
+ describe('featuredSchema', () => {
231
+ it('should accept boolean values', () => {
232
+ expect(featuredSchema.parse(true)).toBe(true);
233
+ expect(featuredSchema.parse(false)).toBe(false);
234
+ });
235
+
236
+ it('should use default value', () => {
237
+ expect(featuredSchema.parse(undefined)).toBe(false);
238
+ });
239
+ });
240
+
241
+ describe('featureImageSchema', () => {
242
+ it('should accept valid image URLs', () => {
243
+ expect(() => featureImageSchema.parse('https://example.com/image.jpg')).not.toThrow();
244
+ expect(() => featureImageSchema.parse(undefined)).not.toThrow();
245
+ });
246
+
247
+ it('should reject invalid URLs', () => {
248
+ expect(() => featureImageSchema.parse('not-a-url')).toThrow();
249
+ });
250
+ });
251
+
252
+ describe('featureImageAltSchema', () => {
253
+ it('should accept valid alt text', () => {
254
+ expect(() => featureImageAltSchema.parse('Image description')).not.toThrow();
255
+ expect(() => featureImageAltSchema.parse('A'.repeat(125))).not.toThrow();
256
+ expect(() => featureImageAltSchema.parse(undefined)).not.toThrow();
257
+ });
258
+
259
+ it('should reject too long alt text', () => {
260
+ expect(() => featureImageAltSchema.parse('A'.repeat(126))).toThrow();
261
+ });
262
+ });
263
+
264
+ describe('tagNameSchema', () => {
265
+ it('should accept valid tag names', () => {
266
+ expect(() => tagNameSchema.parse('Technology')).not.toThrow();
267
+ expect(() => tagNameSchema.parse('A'.repeat(191))).not.toThrow();
268
+ });
269
+
270
+ it('should reject invalid tag names', () => {
271
+ expect(() => tagNameSchema.parse('')).toThrow();
272
+ expect(() => tagNameSchema.parse('A'.repeat(192))).toThrow();
273
+ });
274
+ });
275
+ });
@@ -0,0 +1,194 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ createPostSchema,
4
+ updatePostSchema,
5
+ postQuerySchema,
6
+ postIdSchema,
7
+ postOutputSchema,
8
+ } from '../postSchemas.js';
9
+
10
+ describe('Post Schemas', () => {
11
+ describe('createPostSchema', () => {
12
+ it('should accept valid post creation data', () => {
13
+ const validPost = {
14
+ title: 'My Blog Post',
15
+ html: '<p>This is the content of the post.</p>',
16
+ status: 'draft',
17
+ };
18
+
19
+ expect(() => createPostSchema.parse(validPost)).not.toThrow();
20
+ });
21
+
22
+ it('should accept minimal post creation data', () => {
23
+ const minimalPost = {
24
+ title: 'Title',
25
+ html: '<p>Content</p>',
26
+ };
27
+
28
+ const result = createPostSchema.parse(minimalPost);
29
+ expect(result.title).toBe('Title');
30
+ expect(result.status).toBe('draft'); // default
31
+ expect(result.visibility).toBe('public'); // default
32
+ expect(result.featured).toBe(false); // default
33
+ });
34
+
35
+ it('should accept post with all fields', () => {
36
+ const fullPost = {
37
+ title: 'Complete Post',
38
+ html: '<p>Full content</p>',
39
+ slug: 'complete-post',
40
+ status: 'published',
41
+ visibility: 'members',
42
+ featured: true,
43
+ feature_image: 'https://example.com/image.jpg',
44
+ feature_image_alt: 'Image description',
45
+ feature_image_caption: 'Photo caption',
46
+ excerpt: 'Brief summary',
47
+ custom_excerpt: 'Custom summary',
48
+ meta_title: 'SEO Title',
49
+ meta_description: 'SEO Description',
50
+ tags: ['tech', 'news'],
51
+ authors: ['author@example.com'],
52
+ published_at: '2024-01-15T10:30:00.000Z',
53
+ canonical_url: 'https://example.com/original',
54
+ };
55
+
56
+ expect(() => createPostSchema.parse(fullPost)).not.toThrow();
57
+ });
58
+
59
+ it('should reject post without title', () => {
60
+ const invalidPost = {
61
+ html: '<p>Content</p>',
62
+ };
63
+
64
+ expect(() => createPostSchema.parse(invalidPost)).toThrow();
65
+ });
66
+
67
+ it('should reject post without html', () => {
68
+ const invalidPost = {
69
+ title: 'Title',
70
+ };
71
+
72
+ expect(() => createPostSchema.parse(invalidPost)).toThrow();
73
+ });
74
+
75
+ it('should reject post with invalid status', () => {
76
+ const invalidPost = {
77
+ title: 'Title',
78
+ html: '<p>Content</p>',
79
+ status: 'invalid',
80
+ };
81
+
82
+ expect(() => createPostSchema.parse(invalidPost)).toThrow();
83
+ });
84
+
85
+ it('should reject post with too long title', () => {
86
+ const invalidPost = {
87
+ title: 'A'.repeat(256),
88
+ html: '<p>Content</p>',
89
+ };
90
+
91
+ expect(() => createPostSchema.parse(invalidPost)).toThrow();
92
+ });
93
+ });
94
+
95
+ describe('updatePostSchema', () => {
96
+ it('should accept partial post updates', () => {
97
+ const update = {
98
+ title: 'Updated Title',
99
+ };
100
+
101
+ expect(() => updatePostSchema.parse(update)).not.toThrow();
102
+ });
103
+
104
+ it('should accept empty update object', () => {
105
+ expect(() => updatePostSchema.parse({})).not.toThrow();
106
+ });
107
+ });
108
+
109
+ describe('postQuerySchema', () => {
110
+ it('should accept valid query parameters', () => {
111
+ const query = {
112
+ limit: 20,
113
+ page: 2,
114
+ filter: 'status:published+featured:true',
115
+ };
116
+
117
+ expect(() => postQuerySchema.parse(query)).not.toThrow();
118
+ });
119
+
120
+ it('should accept query with include parameter', () => {
121
+ const query = {
122
+ include: 'tags,authors',
123
+ };
124
+
125
+ expect(() => postQuerySchema.parse(query)).not.toThrow();
126
+ });
127
+
128
+ it('should accept empty query object', () => {
129
+ const result = postQuerySchema.parse({});
130
+ expect(result).toBeDefined();
131
+ // Note: optional fields with defaults don't apply when field is omitted
132
+ });
133
+ });
134
+
135
+ describe('postIdSchema', () => {
136
+ it('should accept valid Ghost ID', () => {
137
+ const validId = {
138
+ id: '507f1f77bcf86cd799439011',
139
+ };
140
+
141
+ expect(() => postIdSchema.parse(validId)).not.toThrow();
142
+ });
143
+
144
+ it('should reject invalid Ghost ID', () => {
145
+ const invalidId = {
146
+ id: 'invalid-id',
147
+ };
148
+
149
+ expect(() => postIdSchema.parse(invalidId)).toThrow();
150
+ });
151
+ });
152
+
153
+ describe('postOutputSchema', () => {
154
+ it('should accept valid post output from Ghost API', () => {
155
+ const apiPost = {
156
+ id: '507f1f77bcf86cd799439011',
157
+ uuid: '550e8400-e29b-41d4-a716-446655440000',
158
+ title: 'My Post',
159
+ slug: 'my-post',
160
+ html: '<p>Content</p>',
161
+ comment_id: null,
162
+ feature_image: 'https://example.com/image.jpg',
163
+ feature_image_alt: 'Alt text',
164
+ feature_image_caption: 'Caption',
165
+ featured: false,
166
+ status: 'published',
167
+ visibility: 'public',
168
+ created_at: '2024-01-15T10:30:00.000Z',
169
+ updated_at: '2024-01-15T10:30:00.000Z',
170
+ published_at: '2024-01-15T10:30:00.000Z',
171
+ custom_excerpt: 'Excerpt',
172
+ codeinjection_head: null,
173
+ codeinjection_foot: null,
174
+ custom_template: null,
175
+ canonical_url: null,
176
+ url: 'https://example.com/my-post',
177
+ excerpt: 'Auto excerpt',
178
+ reading_time: 5,
179
+ email_only: false,
180
+ og_image: null,
181
+ og_title: null,
182
+ og_description: null,
183
+ twitter_image: null,
184
+ twitter_title: null,
185
+ twitter_description: null,
186
+ meta_title: null,
187
+ meta_description: null,
188
+ email_subject: null,
189
+ };
190
+
191
+ expect(() => postOutputSchema.parse(apiPost)).not.toThrow();
192
+ });
193
+ });
194
+ });