@jgardner04/ghost-mcp-server 1.7.0 → 1.9.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.
@@ -0,0 +1,168 @@
1
+ import { z } from 'zod';
2
+ import { ghostIdSchema, emailSchema, slugSchema } from './common.js';
3
+
4
+ /**
5
+ * Newsletter Schemas for Ghost CMS
6
+ * Provides input/output validation for newsletter operations
7
+ */
8
+
9
+ // ----- Input Schemas -----
10
+
11
+ /**
12
+ * Schema for creating a new newsletter
13
+ * Required: name
14
+ * Optional: description, sender details, design options, etc.
15
+ */
16
+ export const createNewsletterSchema = z.object({
17
+ name: z.string().min(1, 'Name cannot be empty').max(191, 'Name cannot exceed 191 characters'),
18
+ description: z.string().max(2000, 'Description cannot exceed 2000 characters').optional(),
19
+ slug: slugSchema.optional(),
20
+ sender_name: z.string().max(191, 'Sender name cannot exceed 191 characters').optional(),
21
+ sender_email: emailSchema.optional(),
22
+ sender_reply_to: z
23
+ .enum(['newsletter', 'support'], {
24
+ errorMap: () => ({ message: 'Sender reply-to must be newsletter or support' }),
25
+ })
26
+ .default('newsletter'),
27
+ status: z
28
+ .enum(['active', 'archived'], {
29
+ errorMap: () => ({ message: 'Status must be active or archived' }),
30
+ })
31
+ .default('active'),
32
+ visibility: z
33
+ .enum(['members', 'paid'], {
34
+ errorMap: () => ({ message: 'Visibility must be members or paid' }),
35
+ })
36
+ .default('members'),
37
+ subscribe_on_signup: z
38
+ .boolean()
39
+ .default(true)
40
+ .describe('Whether new members are automatically subscribed'),
41
+ sort_order: z
42
+ .number()
43
+ .int()
44
+ .min(0, 'Sort order must be non-negative')
45
+ .optional()
46
+ .describe('Display order for newsletters'),
47
+ header_image: z.string().url('Invalid header image URL').optional(),
48
+ show_header_icon: z.boolean().default(true),
49
+ show_header_title: z.boolean().default(true),
50
+ title_font_category: z
51
+ .enum(['serif', 'sans-serif'], {
52
+ errorMap: () => ({ message: 'Title font category must be serif or sans-serif' }),
53
+ })
54
+ .default('sans-serif'),
55
+ title_alignment: z
56
+ .enum(['left', 'center'], {
57
+ errorMap: () => ({ message: 'Title alignment must be left or center' }),
58
+ })
59
+ .default('center'),
60
+ show_feature_image: z.boolean().default(true),
61
+ body_font_category: z
62
+ .enum(['serif', 'sans-serif'], {
63
+ errorMap: () => ({ message: 'Body font category must be serif or sans-serif' }),
64
+ })
65
+ .default('sans-serif'),
66
+ footer_content: z.string().optional(),
67
+ show_badge: z.boolean().default(true),
68
+ show_header_name: z.boolean().default(true).optional(),
69
+ show_post_title_section: z.boolean().default(true).optional(),
70
+ });
71
+
72
+ /**
73
+ * Schema for updating an existing newsletter
74
+ * All fields optional
75
+ */
76
+ export const updateNewsletterSchema = createNewsletterSchema.partial();
77
+
78
+ /**
79
+ * Schema for newsletter query/filter parameters
80
+ */
81
+ export const newsletterQuerySchema = z.object({
82
+ limit: z.number().int().min(1).max(100).default(15).optional(),
83
+ page: z.number().int().min(1).default(1).optional(),
84
+ filter: z
85
+ .string()
86
+ .regex(/^[a-zA-Z0-9_\-:.'"\s,[\]<>=!+]+$/, 'Invalid filter: contains disallowed characters')
87
+ .optional()
88
+ .describe('NQL filter string (e.g., "status:active")'),
89
+ order: z
90
+ .string()
91
+ .optional()
92
+ .describe('Order results (e.g., "sort_order ASC", "created_at DESC")'),
93
+ });
94
+
95
+ /**
96
+ * Schema for newsletter ID parameter
97
+ */
98
+ export const newsletterIdSchema = z.object({
99
+ id: ghostIdSchema,
100
+ });
101
+
102
+ /**
103
+ * Schema for newsletter slug parameter
104
+ */
105
+ export const newsletterSlugSchema = z.object({
106
+ slug: slugSchema,
107
+ });
108
+
109
+ // ----- Output Schemas -----
110
+
111
+ /**
112
+ * Schema for a Ghost newsletter object (as returned by the API)
113
+ */
114
+ export const newsletterOutputSchema = z.object({
115
+ id: ghostIdSchema,
116
+ uuid: z.string().uuid(),
117
+ name: z.string(),
118
+ description: z.string().nullable().optional(),
119
+ slug: z.string(),
120
+ sender_name: z.string().nullable().optional(),
121
+ sender_email: z.string().email().nullable().optional(),
122
+ sender_reply_to: z.enum(['newsletter', 'support']),
123
+ status: z.enum(['active', 'archived']),
124
+ visibility: z.enum(['members', 'paid']),
125
+ subscribe_on_signup: z.boolean(),
126
+ sort_order: z.number(),
127
+ header_image: z.string().url().nullable().optional(),
128
+ show_header_icon: z.boolean(),
129
+ show_header_title: z.boolean(),
130
+ title_font_category: z.string(),
131
+ title_alignment: z.enum(['left', 'center']),
132
+ show_feature_image: z.boolean(),
133
+ body_font_category: z.string(),
134
+ footer_content: z.string().nullable().optional(),
135
+ show_badge: z.boolean(),
136
+ show_header_name: z.boolean().optional(),
137
+ show_post_title_section: z.boolean().optional(),
138
+ created_at: z.string().datetime(),
139
+ updated_at: z.string().datetime(),
140
+ count: z
141
+ .object({
142
+ members: z.number().optional(),
143
+ posts: z.number().optional(),
144
+ })
145
+ .optional(),
146
+ });
147
+
148
+ /**
149
+ * Schema for array of newsletters
150
+ */
151
+ export const newslettersArraySchema = z.array(newsletterOutputSchema);
152
+
153
+ /**
154
+ * Schema for paginated newsletter response
155
+ */
156
+ export const newslettersPaginatedSchema = z.object({
157
+ newsletters: newslettersArraySchema,
158
+ meta: z.object({
159
+ pagination: z.object({
160
+ page: z.number(),
161
+ limit: z.number(),
162
+ pages: z.number(),
163
+ total: z.number(),
164
+ next: z.number().nullable(),
165
+ prev: z.number().nullable(),
166
+ }),
167
+ }),
168
+ });
@@ -0,0 +1,215 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ ghostIdSchema,
4
+ slugSchema,
5
+ titleSchema,
6
+ htmlContentSchema,
7
+ excerptSchema,
8
+ metaTitleSchema,
9
+ metaDescriptionSchema,
10
+ canonicalUrlSchema,
11
+ featureImageSchema,
12
+ featureImageAltSchema,
13
+ featuredSchema,
14
+ postStatusSchema,
15
+ visibilitySchema,
16
+ authorsSchema,
17
+ tagsSchema,
18
+ customExcerptSchema,
19
+ ogImageSchema,
20
+ twitterImageSchema,
21
+ isoDateSchema,
22
+ } from './common.js';
23
+
24
+ /**
25
+ * Page Schemas for Ghost CMS
26
+ * Pages are similar to posts but represent static content (About, Contact, etc.)
27
+ * Provides input/output validation for page operations
28
+ */
29
+
30
+ // ----- Input Schemas -----
31
+
32
+ /**
33
+ * Schema for creating a new page
34
+ * Required: title, html
35
+ * Optional: various metadata, feature image, authors, tags, etc.
36
+ * Note: Pages typically don't have tags, but Ghost API supports them
37
+ */
38
+ export const createPageSchema = z.object({
39
+ title: titleSchema,
40
+ html: htmlContentSchema.describe('HTML content of the page'),
41
+ slug: slugSchema.optional(),
42
+ status: postStatusSchema.default('draft'),
43
+ visibility: visibilitySchema.default('public'),
44
+ featured: featuredSchema,
45
+ feature_image: featureImageSchema,
46
+ feature_image_alt: featureImageAltSchema,
47
+ feature_image_caption: z.string().max(500, 'Caption cannot exceed 500 characters').optional(),
48
+ excerpt: excerptSchema,
49
+ custom_excerpt: customExcerptSchema,
50
+ meta_title: metaTitleSchema,
51
+ meta_description: metaDescriptionSchema,
52
+ og_image: ogImageSchema,
53
+ og_title: z.string().max(300, 'OG title cannot exceed 300 characters').optional(),
54
+ og_description: z.string().max(500, 'OG description cannot exceed 500 characters').optional(),
55
+ twitter_image: twitterImageSchema,
56
+ twitter_title: z.string().max(300, 'Twitter title cannot exceed 300 characters').optional(),
57
+ twitter_description: z
58
+ .string()
59
+ .max(500, 'Twitter description cannot exceed 500 characters')
60
+ .optional(),
61
+ canonical_url: canonicalUrlSchema,
62
+ tags: tagsSchema.describe('Array of tag names or IDs (rarely used for pages)'),
63
+ authors: authorsSchema.describe('Array of author IDs or emails'),
64
+ published_at: isoDateSchema.optional().describe('Scheduled publish time (ISO 8601 format)'),
65
+ updated_at: isoDateSchema.optional(),
66
+ created_at: isoDateSchema.optional(),
67
+ codeinjection_head: z.string().optional(),
68
+ codeinjection_foot: z.string().optional(),
69
+ custom_template: z.string().optional().describe('Custom template filename'),
70
+ show_title_and_feature_image: z
71
+ .boolean()
72
+ .default(true)
73
+ .describe('Whether to show title and feature image on page'),
74
+ });
75
+
76
+ /**
77
+ * Schema for updating an existing page
78
+ * All fields optional
79
+ */
80
+ export const updatePageSchema = createPageSchema.partial();
81
+
82
+ /**
83
+ * Schema for page query/filter parameters
84
+ */
85
+ export const pageQuerySchema = z.object({
86
+ limit: z.number().int().min(1).max(100).default(15).optional(),
87
+ page: z.number().int().min(1).default(1).optional(),
88
+ filter: z
89
+ .string()
90
+ .regex(/^[a-zA-Z0-9_\-:.'"\s,[\]<>=!+]+$/, 'Invalid filter: contains disallowed characters')
91
+ .optional()
92
+ .describe('NQL filter string (e.g., "status:published+featured:true")'),
93
+ include: z
94
+ .string()
95
+ .optional()
96
+ .describe('Comma-separated list of relations (e.g., "tags,authors")'),
97
+ fields: z.string().optional().describe('Comma-separated list of fields to return'),
98
+ formats: z
99
+ .string()
100
+ .optional()
101
+ .describe('Comma-separated list of formats (html, plaintext, mobiledoc)'),
102
+ order: z.string().optional().describe('Order results (e.g., "published_at DESC", "title ASC")'),
103
+ });
104
+
105
+ /**
106
+ * Schema for page ID parameter
107
+ */
108
+ export const pageIdSchema = z.object({
109
+ id: ghostIdSchema,
110
+ });
111
+
112
+ /**
113
+ * Schema for page slug parameter
114
+ */
115
+ export const pageSlugSchema = z.object({
116
+ slug: slugSchema,
117
+ });
118
+
119
+ // ----- Output Schemas -----
120
+
121
+ /**
122
+ * Schema for an author object (nested in page)
123
+ * Reusing from postSchemas would be better, but duplicated here for independence
124
+ */
125
+ export const authorOutputSchema = z.object({
126
+ id: ghostIdSchema,
127
+ name: z.string(),
128
+ slug: z.string(),
129
+ email: z.string().email().optional(),
130
+ profile_image: z.string().url().nullable().optional(),
131
+ cover_image: z.string().url().nullable().optional(),
132
+ bio: z.string().nullable().optional(),
133
+ website: z.string().url().nullable().optional(),
134
+ location: z.string().nullable().optional(),
135
+ facebook: z.string().nullable().optional(),
136
+ twitter: z.string().nullable().optional(),
137
+ url: z.string().url(),
138
+ });
139
+
140
+ /**
141
+ * Schema for a tag object (nested in page)
142
+ */
143
+ export const tagOutputSchema = z.object({
144
+ id: ghostIdSchema,
145
+ name: z.string(),
146
+ slug: z.string(),
147
+ description: z.string().nullable().optional(),
148
+ feature_image: z.string().url().nullable().optional(),
149
+ visibility: visibilitySchema,
150
+ url: z.string().url(),
151
+ });
152
+
153
+ /**
154
+ * Schema for a Ghost page object (as returned by the API)
155
+ */
156
+ export const pageOutputSchema = z.object({
157
+ id: ghostIdSchema,
158
+ uuid: z.string().uuid(),
159
+ title: z.string(),
160
+ slug: z.string(),
161
+ html: z.string().nullable().optional(),
162
+ comment_id: z.string().nullable().optional(),
163
+ feature_image: z.string().url().nullable().optional(),
164
+ feature_image_alt: z.string().nullable().optional(),
165
+ feature_image_caption: z.string().nullable().optional(),
166
+ featured: z.boolean(),
167
+ status: postStatusSchema,
168
+ visibility: visibilitySchema,
169
+ created_at: z.string().datetime(),
170
+ updated_at: z.string().datetime(),
171
+ published_at: z.string().datetime().nullable().optional(),
172
+ custom_excerpt: z.string().nullable().optional(),
173
+ codeinjection_head: z.string().nullable().optional(),
174
+ codeinjection_foot: z.string().nullable().optional(),
175
+ custom_template: z.string().nullable().optional(),
176
+ canonical_url: z.string().url().nullable().optional(),
177
+ url: z.string().url(),
178
+ excerpt: z.string().nullable().optional(),
179
+ reading_time: z.number().nullable().optional(),
180
+ og_image: z.string().url().nullable().optional(),
181
+ og_title: z.string().nullable().optional(),
182
+ og_description: z.string().nullable().optional(),
183
+ twitter_image: z.string().url().nullable().optional(),
184
+ twitter_title: z.string().nullable().optional(),
185
+ twitter_description: z.string().nullable().optional(),
186
+ meta_title: z.string().nullable().optional(),
187
+ meta_description: z.string().nullable().optional(),
188
+ show_title_and_feature_image: z.boolean().optional(),
189
+ authors: z.array(authorOutputSchema).optional(),
190
+ tags: z.array(tagOutputSchema).optional(),
191
+ primary_author: authorOutputSchema.nullable().optional(),
192
+ primary_tag: tagOutputSchema.nullable().optional(),
193
+ });
194
+
195
+ /**
196
+ * Schema for array of pages
197
+ */
198
+ export const pagesArraySchema = z.array(pageOutputSchema);
199
+
200
+ /**
201
+ * Schema for paginated page response
202
+ */
203
+ export const pagesPaginatedSchema = z.object({
204
+ pages: pagesArraySchema,
205
+ meta: z.object({
206
+ pagination: z.object({
207
+ page: z.number(),
208
+ limit: z.number(),
209
+ pages: z.number(),
210
+ total: z.number(),
211
+ next: z.number().nullable(),
212
+ prev: z.number().nullable(),
213
+ }),
214
+ }),
215
+ });
@@ -0,0 +1,210 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ ghostIdSchema,
4
+ slugSchema,
5
+ titleSchema,
6
+ htmlContentSchema,
7
+ excerptSchema,
8
+ metaTitleSchema,
9
+ metaDescriptionSchema,
10
+ canonicalUrlSchema,
11
+ featureImageSchema,
12
+ featureImageAltSchema,
13
+ featuredSchema,
14
+ postStatusSchema,
15
+ visibilitySchema,
16
+ authorsSchema,
17
+ tagsSchema,
18
+ customExcerptSchema,
19
+ ogImageSchema,
20
+ twitterImageSchema,
21
+ isoDateSchema,
22
+ } from './common.js';
23
+
24
+ /**
25
+ * Post Schemas for Ghost CMS
26
+ * Provides input/output validation for post operations
27
+ */
28
+
29
+ // ----- Input Schemas -----
30
+
31
+ /**
32
+ * Schema for creating a new post
33
+ * Required: title, html
34
+ * Optional: various metadata, feature image, authors, tags, etc.
35
+ */
36
+ export const createPostSchema = z.object({
37
+ title: titleSchema,
38
+ html: htmlContentSchema.describe('HTML content of the post'),
39
+ slug: slugSchema.optional(),
40
+ status: postStatusSchema.default('draft'),
41
+ visibility: visibilitySchema.default('public'),
42
+ featured: featuredSchema,
43
+ feature_image: featureImageSchema,
44
+ feature_image_alt: featureImageAltSchema,
45
+ feature_image_caption: z.string().max(500, 'Caption cannot exceed 500 characters').optional(),
46
+ excerpt: excerptSchema,
47
+ custom_excerpt: customExcerptSchema,
48
+ meta_title: metaTitleSchema,
49
+ meta_description: metaDescriptionSchema,
50
+ og_image: ogImageSchema,
51
+ og_title: z.string().max(300, 'OG title cannot exceed 300 characters').optional(),
52
+ og_description: z.string().max(500, 'OG description cannot exceed 500 characters').optional(),
53
+ twitter_image: twitterImageSchema,
54
+ twitter_title: z.string().max(300, 'Twitter title cannot exceed 300 characters').optional(),
55
+ twitter_description: z
56
+ .string()
57
+ .max(500, 'Twitter description cannot exceed 500 characters')
58
+ .optional(),
59
+ canonical_url: canonicalUrlSchema,
60
+ tags: tagsSchema.describe('Array of tag names or IDs to associate with the post'),
61
+ authors: authorsSchema.describe('Array of author IDs or emails'),
62
+ published_at: isoDateSchema.optional().describe('Scheduled publish time (ISO 8601 format)'),
63
+ updated_at: isoDateSchema.optional(),
64
+ created_at: isoDateSchema.optional(),
65
+ codeinjection_head: z.string().optional(),
66
+ codeinjection_foot: z.string().optional(),
67
+ custom_template: z.string().optional().describe('Custom template filename'),
68
+ email_only: z.boolean().default(false).describe('Whether post is email-only'),
69
+ });
70
+
71
+ /**
72
+ * Schema for updating an existing post
73
+ * All fields optional
74
+ */
75
+ export const updatePostSchema = createPostSchema.partial();
76
+
77
+ /**
78
+ * Schema for post query/filter parameters
79
+ */
80
+ export const postQuerySchema = z.object({
81
+ limit: z.number().int().min(1).max(100).default(15).optional(),
82
+ page: z.number().int().min(1).default(1).optional(),
83
+ filter: z
84
+ .string()
85
+ .regex(/^[a-zA-Z0-9_\-:.'"\s,[\]<>=!+]+$/, 'Invalid filter: contains disallowed characters')
86
+ .optional()
87
+ .describe('NQL filter string (e.g., "status:published+featured:true")'),
88
+ include: z
89
+ .string()
90
+ .optional()
91
+ .describe('Comma-separated list of relations (e.g., "tags,authors")'),
92
+ fields: z.string().optional().describe('Comma-separated list of fields to return'),
93
+ formats: z
94
+ .string()
95
+ .optional()
96
+ .describe('Comma-separated list of formats (html, plaintext, mobiledoc)'),
97
+ order: z.string().optional().describe('Order results (e.g., "published_at DESC", "title ASC")'),
98
+ });
99
+
100
+ /**
101
+ * Schema for post ID parameter
102
+ */
103
+ export const postIdSchema = z.object({
104
+ id: ghostIdSchema,
105
+ });
106
+
107
+ /**
108
+ * Schema for post slug parameter
109
+ */
110
+ export const postSlugSchema = z.object({
111
+ slug: slugSchema,
112
+ });
113
+
114
+ // ----- Output Schemas -----
115
+
116
+ /**
117
+ * Schema for an author object (nested in post)
118
+ */
119
+ export const authorOutputSchema = z.object({
120
+ id: ghostIdSchema,
121
+ name: z.string(),
122
+ slug: z.string(),
123
+ email: z.string().email().optional(),
124
+ profile_image: z.string().url().nullable().optional(),
125
+ cover_image: z.string().url().nullable().optional(),
126
+ bio: z.string().nullable().optional(),
127
+ website: z.string().url().nullable().optional(),
128
+ location: z.string().nullable().optional(),
129
+ facebook: z.string().nullable().optional(),
130
+ twitter: z.string().nullable().optional(),
131
+ url: z.string().url(),
132
+ });
133
+
134
+ /**
135
+ * Schema for a tag object (nested in post)
136
+ */
137
+ export const tagOutputSchema = z.object({
138
+ id: ghostIdSchema,
139
+ name: z.string(),
140
+ slug: z.string(),
141
+ description: z.string().nullable().optional(),
142
+ feature_image: z.string().url().nullable().optional(),
143
+ visibility: visibilitySchema,
144
+ url: z.string().url(),
145
+ });
146
+
147
+ /**
148
+ * Schema for a Ghost post object (as returned by the API)
149
+ */
150
+ export const postOutputSchema = z.object({
151
+ id: ghostIdSchema,
152
+ uuid: z.string().uuid(),
153
+ title: z.string(),
154
+ slug: z.string(),
155
+ html: z.string().nullable().optional(),
156
+ comment_id: z.string().nullable().optional(),
157
+ feature_image: z.string().url().nullable().optional(),
158
+ feature_image_alt: z.string().nullable().optional(),
159
+ feature_image_caption: z.string().nullable().optional(),
160
+ featured: z.boolean(),
161
+ status: postStatusSchema,
162
+ visibility: visibilitySchema,
163
+ created_at: z.string().datetime(),
164
+ updated_at: z.string().datetime(),
165
+ published_at: z.string().datetime().nullable().optional(),
166
+ custom_excerpt: z.string().nullable().optional(),
167
+ codeinjection_head: z.string().nullable().optional(),
168
+ codeinjection_foot: z.string().nullable().optional(),
169
+ custom_template: z.string().nullable().optional(),
170
+ canonical_url: z.string().url().nullable().optional(),
171
+ url: z.string().url(),
172
+ excerpt: z.string().nullable().optional(),
173
+ reading_time: z.number().nullable().optional(),
174
+ email_only: z.boolean().optional(),
175
+ og_image: z.string().url().nullable().optional(),
176
+ og_title: z.string().nullable().optional(),
177
+ og_description: z.string().nullable().optional(),
178
+ twitter_image: z.string().url().nullable().optional(),
179
+ twitter_title: z.string().nullable().optional(),
180
+ twitter_description: z.string().nullable().optional(),
181
+ meta_title: z.string().nullable().optional(),
182
+ meta_description: z.string().nullable().optional(),
183
+ email_subject: z.string().nullable().optional(),
184
+ authors: z.array(authorOutputSchema).optional(),
185
+ tags: z.array(tagOutputSchema).optional(),
186
+ primary_author: authorOutputSchema.nullable().optional(),
187
+ primary_tag: tagOutputSchema.nullable().optional(),
188
+ });
189
+
190
+ /**
191
+ * Schema for array of posts
192
+ */
193
+ export const postsArraySchema = z.array(postOutputSchema);
194
+
195
+ /**
196
+ * Schema for paginated post response
197
+ */
198
+ export const postsPaginatedSchema = z.object({
199
+ posts: postsArraySchema,
200
+ meta: z.object({
201
+ pagination: z.object({
202
+ page: z.number(),
203
+ limit: z.number(),
204
+ pages: z.number(),
205
+ total: z.number(),
206
+ next: z.number().nullable(),
207
+ prev: z.number().nullable(),
208
+ }),
209
+ }),
210
+ });