@jgardner04/ghost-mcp-server 1.12.1 → 1.12.3
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 +2 -2
- package/package.json +8 -8
- package/src/__tests__/mcp_server.test.js +1165 -251
- package/src/__tests__/mcp_server_pages.test.js +12 -12
- package/src/errors/index.js +7 -0
- package/src/index.js +3 -16
- package/src/mcp_server.js +1655 -407
- package/src/mcp_server_enhanced.js +1 -1
- package/src/services/__tests__/postService.test.js +1 -1
- package/src/__tests__/mcp_server_improved.test.js +0 -1299
- package/src/mcp_server_improved.js +0 -1718
package/src/mcp_server.js
CHANGED
|
@@ -1,298 +1,359 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
2
5
|
import dotenv from 'dotenv';
|
|
3
|
-
import { createPostService } from './services/postService.js';
|
|
4
|
-
import {
|
|
5
|
-
uploadImage as uploadGhostImage,
|
|
6
|
-
getTags as getGhostTags,
|
|
7
|
-
createTag as createGhostTag,
|
|
8
|
-
} from './services/ghostService.js';
|
|
9
|
-
import { processImage } from './services/imageProcessingService.js';
|
|
10
6
|
import axios from 'axios';
|
|
11
7
|
import fs from 'fs';
|
|
12
8
|
import path from 'path';
|
|
13
9
|
import os from 'os';
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
import { ValidationError } from './errors/index.js';
|
|
12
|
+
import { validateToolInput } from './utils/validation.js';
|
|
17
13
|
import { trackTempFile, cleanupTempFiles } from './utils/tempFileManager.js';
|
|
14
|
+
import {
|
|
15
|
+
createTagSchema,
|
|
16
|
+
updateTagSchema,
|
|
17
|
+
tagQuerySchema,
|
|
18
|
+
ghostIdSchema,
|
|
19
|
+
emailSchema,
|
|
20
|
+
createPostSchema,
|
|
21
|
+
updatePostSchema,
|
|
22
|
+
postQuerySchema,
|
|
23
|
+
createMemberSchema,
|
|
24
|
+
updateMemberSchema,
|
|
25
|
+
memberQuerySchema,
|
|
26
|
+
createTierSchema,
|
|
27
|
+
updateTierSchema,
|
|
28
|
+
tierQuerySchema,
|
|
29
|
+
createNewsletterSchema,
|
|
30
|
+
updateNewsletterSchema,
|
|
31
|
+
newsletterQuerySchema,
|
|
32
|
+
createPageSchema,
|
|
33
|
+
updatePageSchema,
|
|
34
|
+
pageQuerySchema,
|
|
35
|
+
} from './schemas/index.js';
|
|
18
36
|
|
|
19
|
-
// Load environment variables
|
|
37
|
+
// Load environment variables
|
|
20
38
|
dotenv.config();
|
|
21
39
|
|
|
22
|
-
//
|
|
23
|
-
|
|
40
|
+
// Lazy-loaded modules (to avoid Node.js v25 Buffer compatibility issues at startup)
|
|
41
|
+
let ghostService = null;
|
|
42
|
+
let postService = null;
|
|
43
|
+
let pageService = null;
|
|
44
|
+
let newsletterService = null;
|
|
45
|
+
let imageProcessingService = null;
|
|
46
|
+
let urlValidator = null;
|
|
24
47
|
|
|
25
|
-
|
|
48
|
+
const loadServices = async () => {
|
|
49
|
+
if (!ghostService) {
|
|
50
|
+
ghostService = await import('./services/ghostServiceImproved.js');
|
|
51
|
+
postService = await import('./services/postService.js');
|
|
52
|
+
pageService = await import('./services/pageService.js');
|
|
53
|
+
newsletterService = await import('./services/newsletterService.js');
|
|
54
|
+
imageProcessingService = await import('./services/imageProcessingService.js');
|
|
55
|
+
urlValidator = await import('./utils/urlValidator.js');
|
|
56
|
+
}
|
|
57
|
+
};
|
|
26
58
|
|
|
27
|
-
//
|
|
28
|
-
const
|
|
29
|
-
metadata: {
|
|
30
|
-
name: 'Ghost CMS Manager',
|
|
31
|
-
description: 'MCP Server to manage a Ghost CMS instance using the Admin API.',
|
|
32
|
-
// iconUrl: '...',
|
|
33
|
-
},
|
|
34
|
-
});
|
|
59
|
+
// Generate UUID without external dependency
|
|
60
|
+
const generateUuid = () => crypto.randomUUID();
|
|
35
61
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
},
|
|
54
|
-
// Add other relevant tag fields if needed (e.g., feature_image, visibility)
|
|
55
|
-
},
|
|
56
|
-
required: ['id', 'name', 'slug'],
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
mcpServer.addResource(ghostTagResource);
|
|
60
|
-
logger.info('Added MCP Resource', { resourceName: ghostTagResource.name });
|
|
61
|
-
|
|
62
|
-
// Ghost Post Resource
|
|
63
|
-
const ghostPostResource = new Resource({
|
|
64
|
-
name: 'ghost/post',
|
|
65
|
-
description: 'Represents a post in Ghost CMS.',
|
|
66
|
-
schema: {
|
|
67
|
-
type: 'object',
|
|
68
|
-
properties: {
|
|
69
|
-
id: { type: 'string', description: 'Unique ID of the post' },
|
|
70
|
-
uuid: { type: 'string', description: 'UUID of the post' },
|
|
71
|
-
title: { type: 'string', description: 'The title of the post' },
|
|
72
|
-
slug: {
|
|
73
|
-
type: 'string',
|
|
74
|
-
description: 'URL-friendly version of the title',
|
|
75
|
-
},
|
|
76
|
-
html: {
|
|
77
|
-
type: ['string', 'null'],
|
|
78
|
-
description: 'The post content as HTML',
|
|
79
|
-
},
|
|
80
|
-
plaintext: {
|
|
81
|
-
type: ['string', 'null'],
|
|
82
|
-
description: 'The post content as plain text',
|
|
83
|
-
},
|
|
84
|
-
feature_image: {
|
|
85
|
-
type: ['string', 'null'],
|
|
86
|
-
description: 'URL of the featured image',
|
|
87
|
-
},
|
|
88
|
-
feature_image_alt: {
|
|
89
|
-
type: ['string', 'null'],
|
|
90
|
-
description: 'Alt text for the featured image',
|
|
91
|
-
},
|
|
92
|
-
feature_image_caption: {
|
|
93
|
-
type: ['string', 'null'],
|
|
94
|
-
description: 'Caption for the featured image',
|
|
95
|
-
},
|
|
96
|
-
featured: {
|
|
97
|
-
type: 'boolean',
|
|
98
|
-
description: 'Whether the post is featured',
|
|
99
|
-
},
|
|
100
|
-
status: {
|
|
101
|
-
type: 'string',
|
|
102
|
-
enum: ['published', 'draft', 'scheduled'],
|
|
103
|
-
description: 'Publication status',
|
|
104
|
-
},
|
|
105
|
-
visibility: {
|
|
106
|
-
type: 'string',
|
|
107
|
-
enum: ['public', 'members', 'paid'],
|
|
108
|
-
description: 'Access level',
|
|
109
|
-
},
|
|
110
|
-
created_at: {
|
|
111
|
-
type: 'string',
|
|
112
|
-
format: 'date-time',
|
|
113
|
-
description: 'Date/time post was created',
|
|
114
|
-
},
|
|
115
|
-
updated_at: {
|
|
116
|
-
type: 'string',
|
|
117
|
-
format: 'date-time',
|
|
118
|
-
description: 'Date/time post was last updated',
|
|
119
|
-
},
|
|
120
|
-
published_at: {
|
|
121
|
-
type: ['string', 'null'],
|
|
122
|
-
format: 'date-time',
|
|
123
|
-
description: 'Date/time post was published or scheduled',
|
|
124
|
-
},
|
|
125
|
-
custom_excerpt: {
|
|
126
|
-
type: ['string', 'null'],
|
|
127
|
-
description: 'Custom excerpt for the post',
|
|
128
|
-
},
|
|
129
|
-
meta_title: { type: ['string', 'null'], description: 'Custom SEO title' },
|
|
130
|
-
meta_description: {
|
|
131
|
-
type: ['string', 'null'],
|
|
132
|
-
description: 'Custom SEO description',
|
|
133
|
-
},
|
|
134
|
-
tags: {
|
|
135
|
-
type: 'array',
|
|
136
|
-
description: 'Tags associated with the post',
|
|
137
|
-
items: { $ref: '#/definitions/ghost/tag' }, // Reference the ghost/tag resource
|
|
138
|
-
},
|
|
139
|
-
// Add authors or other relevant fields if needed
|
|
140
|
-
},
|
|
141
|
-
required: ['id', 'uuid', 'title', 'slug', 'status', 'visibility', 'created_at', 'updated_at'],
|
|
142
|
-
definitions: {
|
|
143
|
-
// Make the referenced tag resource available within this schema's scope
|
|
144
|
-
'ghost/tag': ghostTagResource.schema,
|
|
145
|
-
},
|
|
146
|
-
},
|
|
62
|
+
// Helper function for default alt text
|
|
63
|
+
const getDefaultAltText = (filePath) => {
|
|
64
|
+
try {
|
|
65
|
+
const originalFilename = path.basename(filePath).split('.').slice(0, -1).join('.');
|
|
66
|
+
const nameWithoutIds = originalFilename
|
|
67
|
+
.replace(/^(processed-|mcp-download-|mcp-upload-)\d+-\d+-?/, '')
|
|
68
|
+
.replace(/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}-?/, '');
|
|
69
|
+
return nameWithoutIds.replace(/[-_]/g, ' ').trim() || 'Uploaded image';
|
|
70
|
+
} catch (_e) {
|
|
71
|
+
return 'Uploaded image';
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Create server instance with new API
|
|
76
|
+
const server = new McpServer({
|
|
77
|
+
name: 'ghost-mcp-server',
|
|
78
|
+
version: '1.0.0',
|
|
147
79
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// ---
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
80
|
+
|
|
81
|
+
// --- Register Tools ---
|
|
82
|
+
|
|
83
|
+
// --- Schema Definitions for Tools ---
|
|
84
|
+
const getTagsSchema = tagQuerySchema.partial();
|
|
85
|
+
const getTagSchema = z
|
|
86
|
+
.object({
|
|
87
|
+
id: ghostIdSchema.optional().describe('The ID of the tag to retrieve.'),
|
|
88
|
+
slug: z.string().optional().describe('The slug of the tag to retrieve.'),
|
|
89
|
+
include: z
|
|
90
|
+
.string()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Additional resources to include (e.g., "count.posts").'),
|
|
93
|
+
})
|
|
94
|
+
.refine((data) => data.id || data.slug, {
|
|
95
|
+
message: 'Either id or slug is required to retrieve a tag',
|
|
96
|
+
});
|
|
97
|
+
const updateTagInputSchema = updateTagSchema.extend({ id: ghostIdSchema });
|
|
98
|
+
const deleteTagSchema = z.object({ id: ghostIdSchema });
|
|
99
|
+
|
|
100
|
+
// Get Tags Tool
|
|
101
|
+
server.tool(
|
|
102
|
+
'ghost_get_tags',
|
|
103
|
+
'Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.',
|
|
104
|
+
getTagsSchema,
|
|
105
|
+
async (rawInput) => {
|
|
106
|
+
const validation = validateToolInput(getTagsSchema, rawInput, 'ghost_get_tags');
|
|
107
|
+
if (!validation.success) {
|
|
108
|
+
return validation.errorResponse;
|
|
109
|
+
}
|
|
110
|
+
const input = validation.data;
|
|
111
|
+
|
|
112
|
+
console.error(`Executing tool: ghost_get_tags`);
|
|
113
|
+
try {
|
|
114
|
+
await loadServices();
|
|
115
|
+
const tags = await ghostService.getTags();
|
|
116
|
+
let result = tags;
|
|
117
|
+
|
|
118
|
+
if (input.name) {
|
|
119
|
+
result = tags.filter((tag) => tag.name.toLowerCase() === input.name.toLowerCase());
|
|
120
|
+
console.error(`Filtered tags by name "${input.name}". Found ${result.length} match(es).`);
|
|
121
|
+
} else {
|
|
122
|
+
console.error(`Retrieved ${tags.length} tags from Ghost.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error(`Error in ghost_get_tags:`, error);
|
|
130
|
+
if (error.name === 'ZodError') {
|
|
131
|
+
const validationError = ValidationError.fromZod(error, 'Tags retrieval');
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
139
|
+
isError: true,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Create Tag Tool
|
|
146
|
+
server.tool(
|
|
147
|
+
'ghost_create_tag',
|
|
148
|
+
'Creates a new tag in Ghost CMS.',
|
|
149
|
+
createTagSchema,
|
|
150
|
+
async (rawInput) => {
|
|
151
|
+
const validation = validateToolInput(createTagSchema, rawInput, 'ghost_create_tag');
|
|
152
|
+
if (!validation.success) {
|
|
153
|
+
return validation.errorResponse;
|
|
154
|
+
}
|
|
155
|
+
const input = validation.data;
|
|
156
|
+
|
|
157
|
+
console.error(`Executing tool: ghost_create_tag with name: ${input.name}`);
|
|
158
|
+
try {
|
|
159
|
+
await loadServices();
|
|
160
|
+
const createdTag = await ghostService.createTag(input);
|
|
161
|
+
console.error(`Tag created successfully. Tag ID: ${createdTag.id}`);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: 'text', text: JSON.stringify(createdTag, null, 2) }],
|
|
165
|
+
};
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`Error in ghost_create_tag:`, error);
|
|
168
|
+
if (error.name === 'ZodError') {
|
|
169
|
+
const validationError = ValidationError.fromZod(error, 'Tag creation');
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
172
|
+
isError: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
177
|
+
isError: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Get Tag Tool
|
|
184
|
+
server.tool(
|
|
185
|
+
'ghost_get_tag',
|
|
186
|
+
'Retrieves a single tag from Ghost CMS by ID or slug.',
|
|
187
|
+
getTagSchema,
|
|
188
|
+
async (rawInput) => {
|
|
189
|
+
const validation = validateToolInput(getTagSchema, rawInput, 'ghost_get_tag');
|
|
190
|
+
if (!validation.success) {
|
|
191
|
+
return validation.errorResponse;
|
|
192
|
+
}
|
|
193
|
+
const { id, slug, include } = validation.data;
|
|
194
|
+
|
|
195
|
+
console.error(`Executing tool: ghost_get_tag`);
|
|
196
|
+
try {
|
|
197
|
+
await loadServices();
|
|
198
|
+
|
|
199
|
+
// If slug is provided, use the slug/slug-name format
|
|
200
|
+
const identifier = slug ? `slug/${slug}` : id;
|
|
201
|
+
const options = include ? { include } : {};
|
|
202
|
+
|
|
203
|
+
const tag = await ghostService.getTag(identifier, options);
|
|
204
|
+
console.error(`Tag retrieved successfully. Tag ID: ${tag.id}`);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: 'text', text: JSON.stringify(tag, null, 2) }],
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(`Error in ghost_get_tag:`, error);
|
|
211
|
+
if (error.name === 'ZodError') {
|
|
212
|
+
const validationError = ValidationError.fromZod(error, 'Tag retrieval');
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
215
|
+
isError: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
220
|
+
isError: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Update Tag Tool
|
|
227
|
+
server.tool(
|
|
228
|
+
'ghost_update_tag',
|
|
229
|
+
'Updates an existing tag in Ghost CMS.',
|
|
230
|
+
updateTagInputSchema,
|
|
231
|
+
async (rawInput) => {
|
|
232
|
+
const validation = validateToolInput(updateTagInputSchema, rawInput, 'ghost_update_tag');
|
|
233
|
+
if (!validation.success) {
|
|
234
|
+
return validation.errorResponse;
|
|
235
|
+
}
|
|
236
|
+
const input = validation.data;
|
|
237
|
+
|
|
238
|
+
console.error(`Executing tool: ghost_update_tag for ID: ${input.id}`);
|
|
239
|
+
try {
|
|
240
|
+
if (!input.id) {
|
|
241
|
+
throw new Error('Tag ID is required');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
await loadServices();
|
|
245
|
+
|
|
246
|
+
// Build update data object with only provided fields (exclude id from update data)
|
|
247
|
+
const { id, ...updateData } = input;
|
|
248
|
+
|
|
249
|
+
const updatedTag = await ghostService.updateTag(id, updateData);
|
|
250
|
+
console.error(`Tag updated successfully. Tag ID: ${updatedTag.id}`);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: 'text', text: JSON.stringify(updatedTag, null, 2) }],
|
|
254
|
+
};
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error(`Error in ghost_update_tag:`, error);
|
|
257
|
+
if (error.name === 'ZodError') {
|
|
258
|
+
const validationError = ValidationError.fromZod(error, 'Tag update');
|
|
259
|
+
return {
|
|
260
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
261
|
+
isError: true,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
266
|
+
isError: true,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Delete Tag Tool
|
|
273
|
+
server.tool(
|
|
274
|
+
'ghost_delete_tag',
|
|
275
|
+
'Deletes a tag from Ghost CMS by ID. This operation is permanent.',
|
|
276
|
+
deleteTagSchema,
|
|
277
|
+
async (rawInput) => {
|
|
278
|
+
const validation = validateToolInput(deleteTagSchema, rawInput, 'ghost_delete_tag');
|
|
279
|
+
if (!validation.success) {
|
|
280
|
+
return validation.errorResponse;
|
|
281
|
+
}
|
|
282
|
+
const { id } = validation.data;
|
|
283
|
+
|
|
284
|
+
console.error(`Executing tool: ghost_delete_tag for ID: ${id}`);
|
|
285
|
+
try {
|
|
286
|
+
if (!id) {
|
|
287
|
+
throw new Error('Tag ID is required');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
await loadServices();
|
|
291
|
+
|
|
292
|
+
await ghostService.deleteTag(id);
|
|
293
|
+
console.error(`Tag deleted successfully. Tag ID: ${id}`);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
content: [{ type: 'text', text: `Tag with ID ${id} has been successfully deleted.` }],
|
|
297
|
+
};
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error(`Error in ghost_delete_tag:`, error);
|
|
300
|
+
if (error.name === 'ZodError') {
|
|
301
|
+
const validationError = ValidationError.fromZod(error, 'Tag deletion');
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
304
|
+
isError: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
309
|
+
isError: true,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// --- Image Schema ---
|
|
316
|
+
const uploadImageSchema = z.object({
|
|
317
|
+
imageUrl: z.string().describe('The publicly accessible URL of the image to upload.'),
|
|
318
|
+
alt: z
|
|
319
|
+
.string()
|
|
320
|
+
.optional()
|
|
321
|
+
.describe('Alt text for the image. If omitted, a default will be generated from the filename.'),
|
|
233
322
|
});
|
|
234
|
-
mcpServer.addTool(createPostTool);
|
|
235
|
-
logger.info('Added MCP Tool', { toolName: createPostTool.name });
|
|
236
323
|
|
|
237
324
|
// Upload Image Tool
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
alt: {
|
|
251
|
-
type: 'string',
|
|
252
|
-
description:
|
|
253
|
-
'Optional: Alt text for the image. If omitted, a default will be generated from the filename.',
|
|
254
|
-
},
|
|
255
|
-
// filenameHint: { type: 'string', description: 'Optional: A hint for the original filename, used for default alt text generation.' }
|
|
256
|
-
},
|
|
257
|
-
required: ['imageUrl'],
|
|
258
|
-
},
|
|
259
|
-
outputSchema: {
|
|
260
|
-
type: 'object',
|
|
261
|
-
properties: {
|
|
262
|
-
url: {
|
|
263
|
-
type: 'string',
|
|
264
|
-
format: 'url',
|
|
265
|
-
description: 'The final URL of the image hosted on Ghost.',
|
|
266
|
-
},
|
|
267
|
-
alt: {
|
|
268
|
-
type: 'string',
|
|
269
|
-
description: 'The alt text determined for the image.',
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
required: ['url', 'alt'],
|
|
273
|
-
},
|
|
274
|
-
implementation: async (input) => {
|
|
275
|
-
logger.toolExecution(uploadImageTool.name, { imageUrl: input.imageUrl });
|
|
276
|
-
const { imageUrl, alt } = input;
|
|
325
|
+
server.tool(
|
|
326
|
+
'ghost_upload_image',
|
|
327
|
+
'Downloads an image from a URL, processes it, uploads it to Ghost CMS, and returns the final Ghost image URL and alt text.',
|
|
328
|
+
uploadImageSchema,
|
|
329
|
+
async (rawInput) => {
|
|
330
|
+
const validation = validateToolInput(uploadImageSchema, rawInput, 'ghost_upload_image');
|
|
331
|
+
if (!validation.success) {
|
|
332
|
+
return validation.errorResponse;
|
|
333
|
+
}
|
|
334
|
+
const { imageUrl, alt } = validation.data;
|
|
335
|
+
|
|
336
|
+
console.error(`Executing tool: ghost_upload_image for URL: ${imageUrl}`);
|
|
277
337
|
let downloadedPath = null;
|
|
278
338
|
let processedPath = null;
|
|
279
339
|
|
|
280
340
|
try {
|
|
281
|
-
|
|
282
|
-
|
|
341
|
+
await loadServices();
|
|
342
|
+
|
|
343
|
+
// 1. Validate URL for SSRF protection
|
|
344
|
+
const urlValidation = urlValidator.validateImageUrl(imageUrl);
|
|
283
345
|
if (!urlValidation.isValid) {
|
|
284
346
|
throw new Error(`Invalid image URL: ${urlValidation.error}`);
|
|
285
347
|
}
|
|
286
348
|
|
|
287
|
-
//
|
|
288
|
-
const axiosConfig = createSecureAxiosConfig(urlValidation.sanitizedUrl);
|
|
349
|
+
// 2. Download the image with security controls
|
|
350
|
+
const axiosConfig = urlValidator.createSecureAxiosConfig(urlValidation.sanitizedUrl);
|
|
289
351
|
const response = await axios(axiosConfig);
|
|
290
|
-
// Generate a unique temporary filename
|
|
291
352
|
const tempDir = os.tmpdir();
|
|
292
|
-
const extension = path.extname(imageUrl.split('?')[0]) || '.tmp';
|
|
353
|
+
const extension = path.extname(imageUrl.split('?')[0]) || '.tmp';
|
|
293
354
|
const originalFilenameHint =
|
|
294
|
-
path.basename(imageUrl.split('?')[0]) || `image-${
|
|
295
|
-
downloadedPath = path.join(tempDir, `mcp-download-${
|
|
355
|
+
path.basename(imageUrl.split('?')[0]) || `image-${generateUuid()}${extension}`;
|
|
356
|
+
downloadedPath = path.join(tempDir, `mcp-download-${generateUuid()}${extension}`);
|
|
296
357
|
|
|
297
358
|
const writer = fs.createWriteStream(downloadedPath);
|
|
298
359
|
response.data.pipe(writer);
|
|
@@ -303,170 +364,1357 @@ const uploadImageTool = new Tool({
|
|
|
303
364
|
});
|
|
304
365
|
// Track temp file for cleanup on process exit
|
|
305
366
|
trackTempFile(downloadedPath);
|
|
306
|
-
|
|
367
|
+
console.error(`Downloaded image to temporary path: ${downloadedPath}`);
|
|
307
368
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
processedPath = await processImage(downloadedPath, tempDir);
|
|
369
|
+
// 3. Process the image
|
|
370
|
+
processedPath = await imageProcessingService.processImage(downloadedPath, tempDir);
|
|
311
371
|
// Track processed file for cleanup on process exit
|
|
312
372
|
if (processedPath !== downloadedPath) {
|
|
313
373
|
trackTempFile(processedPath);
|
|
314
374
|
}
|
|
315
|
-
|
|
375
|
+
console.error(`Processed image path: ${processedPath}`);
|
|
316
376
|
|
|
317
|
-
//
|
|
318
|
-
// Using similar logic from subtask 4.4
|
|
377
|
+
// 4. Determine Alt Text
|
|
319
378
|
const defaultAlt = getDefaultAltText(originalFilenameHint);
|
|
320
379
|
const finalAltText = alt || defaultAlt;
|
|
321
|
-
|
|
380
|
+
console.error(`Using alt text: "${finalAltText}"`);
|
|
322
381
|
|
|
323
|
-
//
|
|
324
|
-
const uploadResult = await
|
|
325
|
-
|
|
382
|
+
// 5. Upload processed image to Ghost
|
|
383
|
+
const uploadResult = await ghostService.uploadImage(processedPath);
|
|
384
|
+
console.error(`Uploaded processed image to Ghost: ${uploadResult.url}`);
|
|
326
385
|
|
|
327
|
-
//
|
|
328
|
-
|
|
386
|
+
// 6. Return result
|
|
387
|
+
const result = {
|
|
329
388
|
url: uploadResult.url,
|
|
330
389
|
alt: finalAltText,
|
|
331
390
|
};
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
394
|
+
};
|
|
332
395
|
} catch (error) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
396
|
+
console.error(`Error in ghost_upload_image:`, error);
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: 'text', text: `Error uploading image: ${error.message}` }],
|
|
399
|
+
isError: true,
|
|
400
|
+
};
|
|
336
401
|
} finally {
|
|
337
|
-
//
|
|
338
|
-
await cleanupTempFiles([downloadedPath, processedPath],
|
|
402
|
+
// Cleanup temporary files with proper async/await
|
|
403
|
+
await cleanupTempFiles([downloadedPath, processedPath], console);
|
|
339
404
|
}
|
|
340
|
-
}
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// --- Post Schema Definitions ---
|
|
409
|
+
const getPostsSchema = postQuerySchema.extend({
|
|
410
|
+
status: z
|
|
411
|
+
.enum(['published', 'draft', 'scheduled', 'all'])
|
|
412
|
+
.optional()
|
|
413
|
+
.describe('Filter posts by status. Options: published, draft, scheduled, all.'),
|
|
414
|
+
});
|
|
415
|
+
const getPostSchema = z
|
|
416
|
+
.object({
|
|
417
|
+
id: ghostIdSchema.optional().describe('The ID of the post to retrieve.'),
|
|
418
|
+
slug: z.string().optional().describe('The slug of the post to retrieve.'),
|
|
419
|
+
include: z
|
|
420
|
+
.string()
|
|
421
|
+
.optional()
|
|
422
|
+
.describe('Comma-separated list of relations to include (e.g., "tags,authors").'),
|
|
423
|
+
})
|
|
424
|
+
.refine((data) => data.id || data.slug, {
|
|
425
|
+
message: 'Either id or slug is required to retrieve a post',
|
|
426
|
+
});
|
|
427
|
+
const searchPostsSchema = z.object({
|
|
428
|
+
query: z.string().min(1).describe('Search query to find in post titles.'),
|
|
429
|
+
status: z
|
|
430
|
+
.enum(['published', 'draft', 'scheduled', 'all'])
|
|
431
|
+
.optional()
|
|
432
|
+
.describe('Filter by post status. Default searches all statuses.'),
|
|
433
|
+
limit: z
|
|
434
|
+
.number()
|
|
435
|
+
.int()
|
|
436
|
+
.min(1)
|
|
437
|
+
.max(50)
|
|
438
|
+
.optional()
|
|
439
|
+
.describe('Maximum number of results (1-50). Default is 15.'),
|
|
341
440
|
});
|
|
441
|
+
const updatePostInputSchema = updatePostSchema.extend({ id: ghostIdSchema });
|
|
442
|
+
const deletePostSchema = z.object({ id: ghostIdSchema });
|
|
342
443
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
444
|
+
// Create Post Tool
|
|
445
|
+
server.tool(
|
|
446
|
+
'ghost_create_post',
|
|
447
|
+
'Creates a new post in Ghost CMS.',
|
|
448
|
+
createPostSchema,
|
|
449
|
+
async (rawInput) => {
|
|
450
|
+
const validation = validateToolInput(createPostSchema, rawInput, 'ghost_create_post');
|
|
451
|
+
if (!validation.success) {
|
|
452
|
+
return validation.errorResponse;
|
|
453
|
+
}
|
|
454
|
+
const input = validation.data;
|
|
455
|
+
|
|
456
|
+
console.error(`Executing tool: ghost_create_post with title: ${input.title}`);
|
|
457
|
+
try {
|
|
458
|
+
await loadServices();
|
|
459
|
+
const createdPost = await postService.createPostService(input);
|
|
460
|
+
console.error(`Post created successfully. Post ID: ${createdPost.id}`);
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
content: [{ type: 'text', text: JSON.stringify(createdPost, null, 2) }],
|
|
464
|
+
};
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.error(`Error in ghost_create_post:`, error);
|
|
467
|
+
if (error.name === 'ZodError') {
|
|
468
|
+
const validationError = ValidationError.fromZod(error, 'Post creation');
|
|
469
|
+
return {
|
|
470
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
471
|
+
isError: true,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
content: [{ type: 'text', text: `Error creating post: ${error.message}` }],
|
|
476
|
+
isError: true,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
353
479
|
}
|
|
354
|
-
|
|
480
|
+
);
|
|
355
481
|
|
|
356
|
-
|
|
357
|
-
|
|
482
|
+
// Get Posts Tool
|
|
483
|
+
server.tool(
|
|
484
|
+
'ghost_get_posts',
|
|
485
|
+
'Retrieves a list of posts from Ghost CMS with pagination, filtering, and sorting options.',
|
|
486
|
+
getPostsSchema,
|
|
487
|
+
async (rawInput) => {
|
|
488
|
+
const validation = validateToolInput(getPostsSchema, rawInput, 'ghost_get_posts');
|
|
489
|
+
if (!validation.success) {
|
|
490
|
+
return validation.errorResponse;
|
|
491
|
+
}
|
|
492
|
+
const input = validation.data;
|
|
358
493
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
description: 'Retrieves a list of tags from Ghost CMS. Can optionally filter by tag name.',
|
|
363
|
-
inputSchema: {
|
|
364
|
-
type: 'object',
|
|
365
|
-
properties: {
|
|
366
|
-
name: {
|
|
367
|
-
type: 'string',
|
|
368
|
-
description: 'Optional: The exact name of the tag to search for.',
|
|
369
|
-
},
|
|
370
|
-
},
|
|
371
|
-
},
|
|
372
|
-
outputSchema: {
|
|
373
|
-
type: 'array',
|
|
374
|
-
items: { $ref: 'ghost/tag#/schema' }, // Output is an array of ghost/tag resources
|
|
375
|
-
},
|
|
376
|
-
implementation: async (input) => {
|
|
377
|
-
logger.toolExecution(getTagsTool.name, input);
|
|
378
|
-
try {
|
|
379
|
-
const tags = await getGhostTags(input?.name); // Pass name if provided
|
|
380
|
-
logger.toolSuccess(getTagsTool.name, tags, { tagCount: tags.length });
|
|
381
|
-
// TODO: Validate/map output against schema if necessary
|
|
382
|
-
return tags;
|
|
383
|
-
} catch (error) {
|
|
384
|
-
logger.toolError(getTagsTool.name, error);
|
|
385
|
-
throw new Error(`Failed to get Ghost tags: ${error.message}`);
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
});
|
|
389
|
-
mcpServer.addTool(getTagsTool);
|
|
390
|
-
logger.info('Added MCP Tool', { toolName: getTagsTool.name });
|
|
494
|
+
console.error(`Executing tool: ghost_get_posts`);
|
|
495
|
+
try {
|
|
496
|
+
await loadServices();
|
|
391
497
|
|
|
392
|
-
//
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
type: '
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
498
|
+
// Build options object with provided parameters
|
|
499
|
+
const options = {};
|
|
500
|
+
if (input.limit !== undefined) options.limit = input.limit;
|
|
501
|
+
if (input.page !== undefined) options.page = input.page;
|
|
502
|
+
if (input.status !== undefined) options.status = input.status;
|
|
503
|
+
if (input.include !== undefined) options.include = input.include;
|
|
504
|
+
if (input.filter !== undefined) options.filter = input.filter;
|
|
505
|
+
if (input.order !== undefined) options.order = input.order;
|
|
506
|
+
|
|
507
|
+
const posts = await ghostService.getPosts(options);
|
|
508
|
+
console.error(`Retrieved ${posts.length} posts from Ghost.`);
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }],
|
|
512
|
+
};
|
|
513
|
+
} catch (error) {
|
|
514
|
+
console.error(`Error in ghost_get_posts:`, error);
|
|
515
|
+
if (error.name === 'ZodError') {
|
|
516
|
+
const validationError = ValidationError.fromZod(error, 'Posts retrieval');
|
|
517
|
+
return {
|
|
518
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
519
|
+
isError: true,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
content: [{ type: 'text', text: `Error retrieving posts: ${error.message}` }],
|
|
524
|
+
isError: true,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
// Get Post Tool
|
|
531
|
+
server.tool(
|
|
532
|
+
'ghost_get_post',
|
|
533
|
+
'Retrieves a single post from Ghost CMS by ID or slug.',
|
|
534
|
+
getPostSchema,
|
|
535
|
+
async (rawInput) => {
|
|
536
|
+
const validation = validateToolInput(getPostSchema, rawInput, 'ghost_get_post');
|
|
537
|
+
if (!validation.success) {
|
|
538
|
+
return validation.errorResponse;
|
|
539
|
+
}
|
|
540
|
+
const input = validation.data;
|
|
541
|
+
|
|
542
|
+
console.error(`Executing tool: ghost_get_post`);
|
|
543
|
+
try {
|
|
544
|
+
await loadServices();
|
|
545
|
+
|
|
546
|
+
// Build options object
|
|
547
|
+
const options = {};
|
|
548
|
+
if (input.include !== undefined) options.include = input.include;
|
|
549
|
+
|
|
550
|
+
// Determine identifier (prefer ID over slug)
|
|
551
|
+
const identifier = input.id || `slug/${input.slug}`;
|
|
552
|
+
|
|
553
|
+
const post = await ghostService.getPost(identifier, options);
|
|
554
|
+
console.error(`Retrieved post: ${post.title} (ID: ${post.id})`);
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
content: [{ type: 'text', text: JSON.stringify(post, null, 2) }],
|
|
558
|
+
};
|
|
559
|
+
} catch (error) {
|
|
560
|
+
console.error(`Error in ghost_get_post:`, error);
|
|
561
|
+
if (error.name === 'ZodError') {
|
|
562
|
+
const validationError = ValidationError.fromZod(error, 'Post retrieval');
|
|
563
|
+
return {
|
|
564
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
565
|
+
isError: true,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
content: [{ type: 'text', text: `Error retrieving post: ${error.message}` }],
|
|
570
|
+
isError: true,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
// Search Posts Tool
|
|
577
|
+
server.tool(
|
|
578
|
+
'ghost_search_posts',
|
|
579
|
+
'Search for posts in Ghost CMS by query string with optional status filtering.',
|
|
580
|
+
searchPostsSchema,
|
|
581
|
+
async (rawInput) => {
|
|
582
|
+
const validation = validateToolInput(searchPostsSchema, rawInput, 'ghost_search_posts');
|
|
583
|
+
if (!validation.success) {
|
|
584
|
+
return validation.errorResponse;
|
|
585
|
+
}
|
|
586
|
+
const input = validation.data;
|
|
587
|
+
|
|
588
|
+
console.error(`Executing tool: ghost_search_posts with query: ${input.query}`);
|
|
589
|
+
try {
|
|
590
|
+
await loadServices();
|
|
591
|
+
|
|
592
|
+
// Build options object with provided parameters
|
|
593
|
+
const options = {};
|
|
594
|
+
if (input.status !== undefined) options.status = input.status;
|
|
595
|
+
if (input.limit !== undefined) options.limit = input.limit;
|
|
596
|
+
|
|
597
|
+
const posts = await ghostService.searchPosts(input.query, options);
|
|
598
|
+
console.error(`Found ${posts.length} posts matching "${input.query}".`);
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }],
|
|
602
|
+
};
|
|
603
|
+
} catch (error) {
|
|
604
|
+
console.error(`Error in ghost_search_posts:`, error);
|
|
605
|
+
if (error.name === 'ZodError') {
|
|
606
|
+
const validationError = ValidationError.fromZod(error, 'Post search');
|
|
607
|
+
return {
|
|
608
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
609
|
+
isError: true,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
content: [{ type: 'text', text: `Error searching posts: ${error.message}` }],
|
|
614
|
+
isError: true,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
// Update Post Tool
|
|
621
|
+
server.tool(
|
|
622
|
+
'ghost_update_post',
|
|
623
|
+
'Updates an existing post in Ghost CMS. Can update title, content, status, tags, images, and SEO fields.',
|
|
624
|
+
updatePostInputSchema,
|
|
625
|
+
async (rawInput) => {
|
|
626
|
+
const validation = validateToolInput(updatePostInputSchema, rawInput, 'ghost_update_post');
|
|
627
|
+
if (!validation.success) {
|
|
628
|
+
return validation.errorResponse;
|
|
629
|
+
}
|
|
630
|
+
const input = validation.data;
|
|
631
|
+
|
|
632
|
+
console.error(`Executing tool: ghost_update_post for post ID: ${input.id}`);
|
|
633
|
+
try {
|
|
634
|
+
await loadServices();
|
|
635
|
+
|
|
636
|
+
// Extract ID from input and build update data
|
|
637
|
+
const { id, ...updateData } = input;
|
|
638
|
+
|
|
639
|
+
const updatedPost = await ghostService.updatePost(id, updateData);
|
|
640
|
+
console.error(`Post updated successfully. Post ID: ${updatedPost.id}`);
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
content: [{ type: 'text', text: JSON.stringify(updatedPost, null, 2) }],
|
|
644
|
+
};
|
|
645
|
+
} catch (error) {
|
|
646
|
+
console.error(`Error in ghost_update_post:`, error);
|
|
647
|
+
if (error.name === 'ZodError') {
|
|
648
|
+
const validationError = ValidationError.fromZod(error, 'Post update');
|
|
649
|
+
return {
|
|
650
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
651
|
+
isError: true,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
content: [{ type: 'text', text: `Error updating post: ${error.message}` }],
|
|
656
|
+
isError: true,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// Delete Post Tool
|
|
663
|
+
server.tool(
|
|
664
|
+
'ghost_delete_post',
|
|
665
|
+
'Deletes a post from Ghost CMS by ID. This operation is permanent and cannot be undone.',
|
|
666
|
+
deletePostSchema,
|
|
667
|
+
async (rawInput) => {
|
|
668
|
+
const validation = validateToolInput(deletePostSchema, rawInput, 'ghost_delete_post');
|
|
669
|
+
if (!validation.success) {
|
|
670
|
+
return validation.errorResponse;
|
|
671
|
+
}
|
|
672
|
+
const { id } = validation.data;
|
|
673
|
+
|
|
674
|
+
console.error(`Executing tool: ghost_delete_post for post ID: ${id}`);
|
|
675
|
+
try {
|
|
676
|
+
await loadServices();
|
|
677
|
+
|
|
678
|
+
await ghostService.deletePost(id);
|
|
679
|
+
console.error(`Post deleted successfully. Post ID: ${id}`);
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
content: [{ type: 'text', text: `Post ${id} has been successfully deleted.` }],
|
|
683
|
+
};
|
|
684
|
+
} catch (error) {
|
|
685
|
+
console.error(`Error in ghost_delete_post:`, error);
|
|
686
|
+
if (error.name === 'ZodError') {
|
|
687
|
+
const validationError = ValidationError.fromZod(error, 'Post deletion');
|
|
688
|
+
return {
|
|
689
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
690
|
+
isError: true,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
content: [{ type: 'text', text: `Error deleting post: ${error.message}` }],
|
|
695
|
+
isError: true,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
// =============================================================================
|
|
702
|
+
// PAGE TOOLS
|
|
703
|
+
// Pages are similar to posts but do NOT support tags
|
|
704
|
+
// =============================================================================
|
|
705
|
+
|
|
706
|
+
// --- Page Schema Definitions ---
|
|
707
|
+
const getPageSchema = z
|
|
708
|
+
.object({
|
|
709
|
+
id: ghostIdSchema.optional().describe('The ID of the page to retrieve.'),
|
|
710
|
+
slug: z.string().optional().describe('The slug of the page to retrieve.'),
|
|
711
|
+
include: z
|
|
712
|
+
.string()
|
|
713
|
+
.optional()
|
|
714
|
+
.describe('Comma-separated list of relations to include (e.g., "authors").'),
|
|
715
|
+
})
|
|
716
|
+
.refine((data) => data.id || data.slug, {
|
|
717
|
+
message: 'Either id or slug is required to retrieve a page',
|
|
718
|
+
});
|
|
719
|
+
const updatePageInputSchema = z
|
|
720
|
+
.object({ id: ghostIdSchema.describe('The ID of the page to update.') })
|
|
721
|
+
.merge(updatePageSchema);
|
|
722
|
+
const deletePageSchema = z.object({ id: ghostIdSchema.describe('The ID of the page to delete.') });
|
|
723
|
+
const searchPagesSchema = z.object({
|
|
724
|
+
query: z
|
|
725
|
+
.string()
|
|
726
|
+
.min(1, 'Search query cannot be empty')
|
|
727
|
+
.describe('Search query to find in page titles.'),
|
|
728
|
+
status: z
|
|
729
|
+
.enum(['published', 'draft', 'scheduled', 'all'])
|
|
730
|
+
.optional()
|
|
731
|
+
.describe('Filter by page status. Default searches all statuses.'),
|
|
732
|
+
limit: z
|
|
733
|
+
.number()
|
|
734
|
+
.int()
|
|
735
|
+
.min(1)
|
|
736
|
+
.max(50)
|
|
737
|
+
.default(15)
|
|
738
|
+
.optional()
|
|
739
|
+
.describe('Maximum number of results (1-50). Default is 15.'),
|
|
429
740
|
});
|
|
430
|
-
mcpServer.addTool(createTagTool);
|
|
431
|
-
logger.info('Added MCP Tool', { toolName: createTagTool.name });
|
|
432
741
|
|
|
433
|
-
//
|
|
742
|
+
// Get Pages Tool
|
|
743
|
+
server.tool(
|
|
744
|
+
'ghost_get_pages',
|
|
745
|
+
'Retrieves a list of pages from Ghost CMS with pagination, filtering, and sorting options.',
|
|
746
|
+
pageQuerySchema,
|
|
747
|
+
async (rawInput) => {
|
|
748
|
+
const validation = validateToolInput(pageQuerySchema, rawInput, 'ghost_get_pages');
|
|
749
|
+
if (!validation.success) {
|
|
750
|
+
return validation.errorResponse;
|
|
751
|
+
}
|
|
752
|
+
const input = validation.data;
|
|
753
|
+
|
|
754
|
+
console.error(`Executing tool: ghost_get_pages`);
|
|
755
|
+
try {
|
|
756
|
+
await loadServices();
|
|
757
|
+
|
|
758
|
+
const options = {};
|
|
759
|
+
if (input.limit !== undefined) options.limit = input.limit;
|
|
760
|
+
if (input.page !== undefined) options.page = input.page;
|
|
761
|
+
if (input.filter !== undefined) options.filter = input.filter;
|
|
762
|
+
if (input.include !== undefined) options.include = input.include;
|
|
763
|
+
if (input.fields !== undefined) options.fields = input.fields;
|
|
764
|
+
if (input.formats !== undefined) options.formats = input.formats;
|
|
765
|
+
if (input.order !== undefined) options.order = input.order;
|
|
434
766
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
logger.error('Failed to start MCP Server', {
|
|
456
|
-
port,
|
|
457
|
-
error: error.message,
|
|
458
|
-
stack: error.stack,
|
|
459
|
-
type: 'server_start_error',
|
|
460
|
-
});
|
|
461
|
-
process.exit(1);
|
|
767
|
+
const pages = await ghostService.getPages(options);
|
|
768
|
+
console.error(`Retrieved ${pages.length} pages from Ghost.`);
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
content: [{ type: 'text', text: JSON.stringify(pages, null, 2) }],
|
|
772
|
+
};
|
|
773
|
+
} catch (error) {
|
|
774
|
+
console.error(`Error in ghost_get_pages:`, error);
|
|
775
|
+
if (error.name === 'ZodError') {
|
|
776
|
+
const validationError = ValidationError.fromZod(error, 'Page query');
|
|
777
|
+
return {
|
|
778
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
779
|
+
isError: true,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
content: [{ type: 'text', text: `Error retrieving pages: ${error.message}` }],
|
|
784
|
+
isError: true,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
462
787
|
}
|
|
463
|
-
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
// Get Page Tool
|
|
791
|
+
server.tool(
|
|
792
|
+
'ghost_get_page',
|
|
793
|
+
'Retrieves a single page from Ghost CMS by ID or slug.',
|
|
794
|
+
getPageSchema,
|
|
795
|
+
async (rawInput) => {
|
|
796
|
+
const validation = validateToolInput(getPageSchema, rawInput, 'ghost_get_page');
|
|
797
|
+
if (!validation.success) {
|
|
798
|
+
return validation.errorResponse;
|
|
799
|
+
}
|
|
800
|
+
const input = validation.data;
|
|
801
|
+
|
|
802
|
+
console.error(`Executing tool: ghost_get_page`);
|
|
803
|
+
try {
|
|
804
|
+
await loadServices();
|
|
805
|
+
|
|
806
|
+
const options = {};
|
|
807
|
+
if (input.include !== undefined) options.include = input.include;
|
|
808
|
+
|
|
809
|
+
const identifier = input.id || `slug/${input.slug}`;
|
|
810
|
+
|
|
811
|
+
const page = await ghostService.getPage(identifier, options);
|
|
812
|
+
console.error(`Retrieved page: ${page.title} (ID: ${page.id})`);
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
content: [{ type: 'text', text: JSON.stringify(page, null, 2) }],
|
|
816
|
+
};
|
|
817
|
+
} catch (error) {
|
|
818
|
+
console.error(`Error in ghost_get_page:`, error);
|
|
819
|
+
if (error.name === 'ZodError') {
|
|
820
|
+
const validationError = ValidationError.fromZod(error, 'Get page');
|
|
821
|
+
return {
|
|
822
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
823
|
+
isError: true,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
content: [{ type: 'text', text: `Error retrieving page: ${error.message}` }],
|
|
828
|
+
isError: true,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
// Create Page Tool
|
|
835
|
+
server.tool(
|
|
836
|
+
'ghost_create_page',
|
|
837
|
+
'Creates a new page in Ghost CMS. Note: Pages do NOT typically use tags (unlike posts).',
|
|
838
|
+
createPageSchema,
|
|
839
|
+
async (rawInput) => {
|
|
840
|
+
const validation = validateToolInput(createPageSchema, rawInput, 'ghost_create_page');
|
|
841
|
+
if (!validation.success) {
|
|
842
|
+
return validation.errorResponse;
|
|
843
|
+
}
|
|
844
|
+
const input = validation.data;
|
|
845
|
+
|
|
846
|
+
console.error(`Executing tool: ghost_create_page with title: ${input.title}`);
|
|
847
|
+
try {
|
|
848
|
+
await loadServices();
|
|
849
|
+
|
|
850
|
+
const createdPage = await pageService.createPageService(input);
|
|
851
|
+
console.error(`Page created successfully. Page ID: ${createdPage.id}`);
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
content: [{ type: 'text', text: JSON.stringify(createdPage, null, 2) }],
|
|
855
|
+
};
|
|
856
|
+
} catch (error) {
|
|
857
|
+
console.error(`Error in ghost_create_page:`, error);
|
|
858
|
+
if (error.name === 'ZodError') {
|
|
859
|
+
const validationError = ValidationError.fromZod(error, 'Page creation');
|
|
860
|
+
return {
|
|
861
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
862
|
+
isError: true,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
content: [{ type: 'text', text: `Error creating page: ${error.message}` }],
|
|
867
|
+
isError: true,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
// Update Page Tool
|
|
874
|
+
server.tool(
|
|
875
|
+
'ghost_update_page',
|
|
876
|
+
'Updates an existing page in Ghost CMS. Can update title, content, status, images, and SEO fields.',
|
|
877
|
+
updatePageInputSchema,
|
|
878
|
+
async (rawInput) => {
|
|
879
|
+
const validation = validateToolInput(updatePageInputSchema, rawInput, 'ghost_update_page');
|
|
880
|
+
if (!validation.success) {
|
|
881
|
+
return validation.errorResponse;
|
|
882
|
+
}
|
|
883
|
+
const input = validation.data;
|
|
884
|
+
|
|
885
|
+
console.error(`Executing tool: ghost_update_page for page ID: ${input.id}`);
|
|
886
|
+
try {
|
|
887
|
+
await loadServices();
|
|
888
|
+
|
|
889
|
+
const { id, ...updateData } = input;
|
|
464
890
|
|
|
465
|
-
|
|
466
|
-
|
|
891
|
+
const updatedPage = await ghostService.updatePage(id, updateData);
|
|
892
|
+
console.error(`Page updated successfully. Page ID: ${updatedPage.id}`);
|
|
467
893
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
894
|
+
return {
|
|
895
|
+
content: [{ type: 'text', text: JSON.stringify(updatedPage, null, 2) }],
|
|
896
|
+
};
|
|
897
|
+
} catch (error) {
|
|
898
|
+
console.error(`Error in ghost_update_page:`, error);
|
|
899
|
+
if (error.name === 'ZodError') {
|
|
900
|
+
const validationError = ValidationError.fromZod(error, 'Page update');
|
|
901
|
+
return {
|
|
902
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
903
|
+
isError: true,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
content: [{ type: 'text', text: `Error updating page: ${error.message}` }],
|
|
908
|
+
isError: true,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
// Delete Page Tool
|
|
915
|
+
server.tool(
|
|
916
|
+
'ghost_delete_page',
|
|
917
|
+
'Deletes a page from Ghost CMS by ID. This operation is permanent and cannot be undone.',
|
|
918
|
+
deletePageSchema,
|
|
919
|
+
async (rawInput) => {
|
|
920
|
+
const validation = validateToolInput(deletePageSchema, rawInput, 'ghost_delete_page');
|
|
921
|
+
if (!validation.success) {
|
|
922
|
+
return validation.errorResponse;
|
|
923
|
+
}
|
|
924
|
+
const { id } = validation.data;
|
|
925
|
+
|
|
926
|
+
console.error(`Executing tool: ghost_delete_page for page ID: ${id}`);
|
|
927
|
+
try {
|
|
928
|
+
await loadServices();
|
|
929
|
+
|
|
930
|
+
await ghostService.deletePage(id);
|
|
931
|
+
console.error(`Page deleted successfully. Page ID: ${id}`);
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
content: [{ type: 'text', text: `Page ${id} has been successfully deleted.` }],
|
|
935
|
+
};
|
|
936
|
+
} catch (error) {
|
|
937
|
+
console.error(`Error in ghost_delete_page:`, error);
|
|
938
|
+
if (error.name === 'ZodError') {
|
|
939
|
+
const validationError = ValidationError.fromZod(error, 'Page deletion');
|
|
940
|
+
return {
|
|
941
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
942
|
+
isError: true,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
content: [{ type: 'text', text: `Error deleting page: ${error.message}` }],
|
|
947
|
+
isError: true,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
// Search Pages Tool
|
|
954
|
+
server.tool(
|
|
955
|
+
'ghost_search_pages',
|
|
956
|
+
'Search for pages in Ghost CMS by query string with optional status filtering.',
|
|
957
|
+
searchPagesSchema,
|
|
958
|
+
async (rawInput) => {
|
|
959
|
+
const validation = validateToolInput(searchPagesSchema, rawInput, 'ghost_search_pages');
|
|
960
|
+
if (!validation.success) {
|
|
961
|
+
return validation.errorResponse;
|
|
962
|
+
}
|
|
963
|
+
const input = validation.data;
|
|
964
|
+
|
|
965
|
+
console.error(`Executing tool: ghost_search_pages with query: ${input.query}`);
|
|
966
|
+
try {
|
|
967
|
+
await loadServices();
|
|
968
|
+
|
|
969
|
+
const options = {};
|
|
970
|
+
if (input.status !== undefined) options.status = input.status;
|
|
971
|
+
if (input.limit !== undefined) options.limit = input.limit;
|
|
972
|
+
|
|
973
|
+
const pages = await ghostService.searchPages(input.query, options);
|
|
974
|
+
console.error(`Found ${pages.length} pages matching "${input.query}".`);
|
|
975
|
+
|
|
976
|
+
return {
|
|
977
|
+
content: [{ type: 'text', text: JSON.stringify(pages, null, 2) }],
|
|
978
|
+
};
|
|
979
|
+
} catch (error) {
|
|
980
|
+
console.error(`Error in ghost_search_pages:`, error);
|
|
981
|
+
if (error.name === 'ZodError') {
|
|
982
|
+
const validationError = ValidationError.fromZod(error, 'Page search');
|
|
983
|
+
return {
|
|
984
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
985
|
+
isError: true,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
return {
|
|
989
|
+
content: [{ type: 'text', text: `Error searching pages: ${error.message}` }],
|
|
990
|
+
isError: true,
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
);
|
|
995
|
+
|
|
996
|
+
// =============================================================================
|
|
997
|
+
// MEMBER TOOLS
|
|
998
|
+
// Member management for Ghost CMS subscribers
|
|
999
|
+
// =============================================================================
|
|
1000
|
+
|
|
1001
|
+
// --- Member Schema Definitions ---
|
|
1002
|
+
const updateMemberInputSchema = z.object({ id: ghostIdSchema }).merge(updateMemberSchema);
|
|
1003
|
+
const deleteMemberSchema = z.object({ id: ghostIdSchema });
|
|
1004
|
+
const getMembersSchema = memberQuerySchema.omit({ search: true });
|
|
1005
|
+
const getMemberSchema = z
|
|
1006
|
+
.object({
|
|
1007
|
+
id: ghostIdSchema.optional().describe('The ID of the member to retrieve.'),
|
|
1008
|
+
email: emailSchema.optional().describe('The email of the member to retrieve.'),
|
|
1009
|
+
})
|
|
1010
|
+
.refine((data) => data.id || data.email, {
|
|
1011
|
+
message: 'Either id or email must be provided',
|
|
1012
|
+
});
|
|
1013
|
+
const searchMembersSchema = z.object({
|
|
1014
|
+
query: z.string().min(1).describe('Search query to match against member name or email.'),
|
|
1015
|
+
limit: z
|
|
1016
|
+
.number()
|
|
1017
|
+
.int()
|
|
1018
|
+
.min(1)
|
|
1019
|
+
.max(50)
|
|
1020
|
+
.optional()
|
|
1021
|
+
.describe('Maximum number of results to return (1-50). Default is 15.'),
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// Create Member Tool
|
|
1025
|
+
server.tool(
|
|
1026
|
+
'ghost_create_member',
|
|
1027
|
+
'Creates a new member (subscriber) in Ghost CMS.',
|
|
1028
|
+
createMemberSchema,
|
|
1029
|
+
async (rawInput) => {
|
|
1030
|
+
const validation = validateToolInput(createMemberSchema, rawInput, 'ghost_create_member');
|
|
1031
|
+
if (!validation.success) {
|
|
1032
|
+
return validation.errorResponse;
|
|
1033
|
+
}
|
|
1034
|
+
const input = validation.data;
|
|
1035
|
+
|
|
1036
|
+
console.error(`Executing tool: ghost_create_member with email: ${input.email}`);
|
|
1037
|
+
try {
|
|
1038
|
+
await loadServices();
|
|
1039
|
+
|
|
1040
|
+
const createdMember = await ghostService.createMember(input);
|
|
1041
|
+
console.error(`Member created successfully. Member ID: ${createdMember.id}`);
|
|
1042
|
+
|
|
1043
|
+
return {
|
|
1044
|
+
content: [{ type: 'text', text: JSON.stringify(createdMember, null, 2) }],
|
|
1045
|
+
};
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
console.error(`Error in ghost_create_member:`, error);
|
|
1048
|
+
if (error.name === 'ZodError') {
|
|
1049
|
+
const validationError = ValidationError.fromZod(error, 'Member creation');
|
|
1050
|
+
return {
|
|
1051
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1052
|
+
isError: true,
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
content: [{ type: 'text', text: `Error creating member: ${error.message}` }],
|
|
1057
|
+
isError: true,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
// Update Member Tool
|
|
1064
|
+
server.tool(
|
|
1065
|
+
'ghost_update_member',
|
|
1066
|
+
'Updates an existing member in Ghost CMS. All fields except id are optional.',
|
|
1067
|
+
updateMemberInputSchema,
|
|
1068
|
+
async (rawInput) => {
|
|
1069
|
+
const validation = validateToolInput(updateMemberInputSchema, rawInput, 'ghost_update_member');
|
|
1070
|
+
if (!validation.success) {
|
|
1071
|
+
return validation.errorResponse;
|
|
1072
|
+
}
|
|
1073
|
+
const input = validation.data;
|
|
1074
|
+
|
|
1075
|
+
console.error(`Executing tool: ghost_update_member for member ID: ${input.id}`);
|
|
1076
|
+
try {
|
|
1077
|
+
await loadServices();
|
|
1078
|
+
|
|
1079
|
+
const { id, ...updateData } = input;
|
|
1080
|
+
|
|
1081
|
+
const updatedMember = await ghostService.updateMember(id, updateData);
|
|
1082
|
+
console.error(`Member updated successfully. Member ID: ${updatedMember.id}`);
|
|
1083
|
+
|
|
1084
|
+
return {
|
|
1085
|
+
content: [{ type: 'text', text: JSON.stringify(updatedMember, null, 2) }],
|
|
1086
|
+
};
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
console.error(`Error in ghost_update_member:`, error);
|
|
1089
|
+
if (error.name === 'ZodError') {
|
|
1090
|
+
const validationError = ValidationError.fromZod(error, 'Member update');
|
|
1091
|
+
return {
|
|
1092
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1093
|
+
isError: true,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
content: [{ type: 'text', text: `Error updating member: ${error.message}` }],
|
|
1098
|
+
isError: true,
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
// Delete Member Tool
|
|
1105
|
+
server.tool(
|
|
1106
|
+
'ghost_delete_member',
|
|
1107
|
+
'Deletes a member from Ghost CMS by ID. This operation is permanent and cannot be undone.',
|
|
1108
|
+
deleteMemberSchema,
|
|
1109
|
+
async (rawInput) => {
|
|
1110
|
+
const validation = validateToolInput(deleteMemberSchema, rawInput, 'ghost_delete_member');
|
|
1111
|
+
if (!validation.success) {
|
|
1112
|
+
return validation.errorResponse;
|
|
1113
|
+
}
|
|
1114
|
+
const { id } = validation.data;
|
|
1115
|
+
|
|
1116
|
+
console.error(`Executing tool: ghost_delete_member for member ID: ${id}`);
|
|
1117
|
+
try {
|
|
1118
|
+
await loadServices();
|
|
1119
|
+
|
|
1120
|
+
await ghostService.deleteMember(id);
|
|
1121
|
+
console.error(`Member deleted successfully. Member ID: ${id}`);
|
|
1122
|
+
|
|
1123
|
+
return {
|
|
1124
|
+
content: [{ type: 'text', text: `Member ${id} has been successfully deleted.` }],
|
|
1125
|
+
};
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
console.error(`Error in ghost_delete_member:`, error);
|
|
1128
|
+
if (error.name === 'ZodError') {
|
|
1129
|
+
const validationError = ValidationError.fromZod(error, 'Member deletion');
|
|
1130
|
+
return {
|
|
1131
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1132
|
+
isError: true,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
content: [{ type: 'text', text: `Error deleting member: ${error.message}` }],
|
|
1137
|
+
isError: true,
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
);
|
|
1142
|
+
|
|
1143
|
+
// Get Members Tool
|
|
1144
|
+
server.tool(
|
|
1145
|
+
'ghost_get_members',
|
|
1146
|
+
'Retrieves a list of members (subscribers) from Ghost CMS with optional filtering, pagination, and includes.',
|
|
1147
|
+
getMembersSchema,
|
|
1148
|
+
async (rawInput) => {
|
|
1149
|
+
const validation = validateToolInput(getMembersSchema, rawInput, 'ghost_get_members');
|
|
1150
|
+
if (!validation.success) {
|
|
1151
|
+
return validation.errorResponse;
|
|
1152
|
+
}
|
|
1153
|
+
const input = validation.data;
|
|
1154
|
+
|
|
1155
|
+
console.error(`Executing tool: ghost_get_members`);
|
|
1156
|
+
try {
|
|
1157
|
+
await loadServices();
|
|
1158
|
+
|
|
1159
|
+
const options = {};
|
|
1160
|
+
if (input.limit !== undefined) options.limit = input.limit;
|
|
1161
|
+
if (input.page !== undefined) options.page = input.page;
|
|
1162
|
+
if (input.filter !== undefined) options.filter = input.filter;
|
|
1163
|
+
if (input.order !== undefined) options.order = input.order;
|
|
1164
|
+
if (input.include !== undefined) options.include = input.include;
|
|
1165
|
+
|
|
1166
|
+
const members = await ghostService.getMembers(options);
|
|
1167
|
+
console.error(`Retrieved ${members.length} members from Ghost.`);
|
|
1168
|
+
|
|
1169
|
+
return {
|
|
1170
|
+
content: [{ type: 'text', text: JSON.stringify(members, null, 2) }],
|
|
1171
|
+
};
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
console.error(`Error in ghost_get_members:`, error);
|
|
1174
|
+
if (error.name === 'ZodError') {
|
|
1175
|
+
const validationError = ValidationError.fromZod(error, 'Member query');
|
|
1176
|
+
return {
|
|
1177
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1178
|
+
isError: true,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
content: [{ type: 'text', text: `Error retrieving members: ${error.message}` }],
|
|
1183
|
+
isError: true,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
// Get Member Tool
|
|
1190
|
+
server.tool(
|
|
1191
|
+
'ghost_get_member',
|
|
1192
|
+
'Retrieves a single member from Ghost CMS by ID or email. Provide either id OR email.',
|
|
1193
|
+
getMemberSchema,
|
|
1194
|
+
async (rawInput) => {
|
|
1195
|
+
const validation = validateToolInput(getMemberSchema, rawInput, 'ghost_get_member');
|
|
1196
|
+
if (!validation.success) {
|
|
1197
|
+
return validation.errorResponse;
|
|
1198
|
+
}
|
|
1199
|
+
const { id, email } = validation.data;
|
|
1200
|
+
|
|
1201
|
+
console.error(`Executing tool: ghost_get_member for ${id ? `ID: ${id}` : `email: ${email}`}`);
|
|
1202
|
+
try {
|
|
1203
|
+
await loadServices();
|
|
1204
|
+
|
|
1205
|
+
const member = await ghostService.getMember({ id, email });
|
|
1206
|
+
console.error(`Retrieved member: ${member.email} (ID: ${member.id})`);
|
|
1207
|
+
|
|
1208
|
+
return {
|
|
1209
|
+
content: [{ type: 'text', text: JSON.stringify(member, null, 2) }],
|
|
1210
|
+
};
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
console.error(`Error in ghost_get_member:`, error);
|
|
1213
|
+
if (error.name === 'ZodError') {
|
|
1214
|
+
const validationError = ValidationError.fromZod(error, 'Member lookup');
|
|
1215
|
+
return {
|
|
1216
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1217
|
+
isError: true,
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
content: [{ type: 'text', text: `Error retrieving member: ${error.message}` }],
|
|
1222
|
+
isError: true,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
// Search Members Tool
|
|
1229
|
+
server.tool(
|
|
1230
|
+
'ghost_search_members',
|
|
1231
|
+
'Searches for members by name or email in Ghost CMS.',
|
|
1232
|
+
searchMembersSchema,
|
|
1233
|
+
async (rawInput) => {
|
|
1234
|
+
const validation = validateToolInput(searchMembersSchema, rawInput, 'ghost_search_members');
|
|
1235
|
+
if (!validation.success) {
|
|
1236
|
+
return validation.errorResponse;
|
|
1237
|
+
}
|
|
1238
|
+
const { query, limit } = validation.data;
|
|
1239
|
+
|
|
1240
|
+
console.error(`Executing tool: ghost_search_members with query: ${query}`);
|
|
1241
|
+
try {
|
|
1242
|
+
await loadServices();
|
|
1243
|
+
|
|
1244
|
+
const options = {};
|
|
1245
|
+
if (limit !== undefined) options.limit = limit;
|
|
1246
|
+
|
|
1247
|
+
const members = await ghostService.searchMembers(query, options);
|
|
1248
|
+
console.error(`Found ${members.length} members matching "${query}".`);
|
|
1249
|
+
|
|
1250
|
+
return {
|
|
1251
|
+
content: [{ type: 'text', text: JSON.stringify(members, null, 2) }],
|
|
1252
|
+
};
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
console.error(`Error in ghost_search_members:`, error);
|
|
1255
|
+
if (error.name === 'ZodError') {
|
|
1256
|
+
const validationError = ValidationError.fromZod(error, 'Member search');
|
|
1257
|
+
return {
|
|
1258
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1259
|
+
isError: true,
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
content: [{ type: 'text', text: `Error searching members: ${error.message}` }],
|
|
1264
|
+
isError: true,
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
// =============================================================================
|
|
1271
|
+
// NEWSLETTER TOOLS
|
|
1272
|
+
// =============================================================================
|
|
1273
|
+
|
|
1274
|
+
// --- Newsletter Schema Definitions ---
|
|
1275
|
+
const getNewsletterSchema = z.object({ id: ghostIdSchema });
|
|
1276
|
+
const updateNewsletterInputSchema = z.object({ id: ghostIdSchema }).merge(updateNewsletterSchema);
|
|
1277
|
+
const deleteNewsletterSchema = z.object({ id: ghostIdSchema });
|
|
1278
|
+
|
|
1279
|
+
// Get Newsletters Tool
|
|
1280
|
+
server.tool(
|
|
1281
|
+
'ghost_get_newsletters',
|
|
1282
|
+
'Retrieves a list of newsletters from Ghost CMS with optional filtering.',
|
|
1283
|
+
newsletterQuerySchema,
|
|
1284
|
+
async (rawInput) => {
|
|
1285
|
+
const validation = validateToolInput(newsletterQuerySchema, rawInput, 'ghost_get_newsletters');
|
|
1286
|
+
if (!validation.success) {
|
|
1287
|
+
return validation.errorResponse;
|
|
1288
|
+
}
|
|
1289
|
+
const input = validation.data;
|
|
1290
|
+
|
|
1291
|
+
console.error(`Executing tool: ghost_get_newsletters`);
|
|
1292
|
+
try {
|
|
1293
|
+
await loadServices();
|
|
1294
|
+
|
|
1295
|
+
const options = {};
|
|
1296
|
+
if (input.limit !== undefined) options.limit = input.limit;
|
|
1297
|
+
if (input.page !== undefined) options.page = input.page;
|
|
1298
|
+
if (input.filter !== undefined) options.filter = input.filter;
|
|
1299
|
+
if (input.order !== undefined) options.order = input.order;
|
|
1300
|
+
|
|
1301
|
+
const newsletters = await ghostService.getNewsletters(options);
|
|
1302
|
+
console.error(`Retrieved ${newsletters.length} newsletters from Ghost.`);
|
|
1303
|
+
|
|
1304
|
+
return {
|
|
1305
|
+
content: [{ type: 'text', text: JSON.stringify(newsletters, null, 2) }],
|
|
1306
|
+
};
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
console.error(`Error in ghost_get_newsletters:`, error);
|
|
1309
|
+
if (error.name === 'ZodError') {
|
|
1310
|
+
const validationError = ValidationError.fromZod(error, 'Newsletter query');
|
|
1311
|
+
return {
|
|
1312
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1313
|
+
isError: true,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
content: [{ type: 'text', text: `Error retrieving newsletters: ${error.message}` }],
|
|
1318
|
+
isError: true,
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
// Get Newsletter Tool
|
|
1325
|
+
server.tool(
|
|
1326
|
+
'ghost_get_newsletter',
|
|
1327
|
+
'Retrieves a single newsletter from Ghost CMS by ID.',
|
|
1328
|
+
getNewsletterSchema,
|
|
1329
|
+
async (rawInput) => {
|
|
1330
|
+
const validation = validateToolInput(getNewsletterSchema, rawInput, 'ghost_get_newsletter');
|
|
1331
|
+
if (!validation.success) {
|
|
1332
|
+
return validation.errorResponse;
|
|
1333
|
+
}
|
|
1334
|
+
const { id } = validation.data;
|
|
1335
|
+
|
|
1336
|
+
console.error(`Executing tool: ghost_get_newsletter for ID: ${id}`);
|
|
1337
|
+
try {
|
|
1338
|
+
await loadServices();
|
|
1339
|
+
|
|
1340
|
+
const newsletter = await ghostService.getNewsletter(id);
|
|
1341
|
+
console.error(`Retrieved newsletter: ${newsletter.name} (ID: ${newsletter.id})`);
|
|
1342
|
+
|
|
1343
|
+
return {
|
|
1344
|
+
content: [{ type: 'text', text: JSON.stringify(newsletter, null, 2) }],
|
|
1345
|
+
};
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
console.error(`Error in ghost_get_newsletter:`, error);
|
|
1348
|
+
if (error.name === 'ZodError') {
|
|
1349
|
+
const validationError = ValidationError.fromZod(error, 'Newsletter retrieval');
|
|
1350
|
+
return {
|
|
1351
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1352
|
+
isError: true,
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
content: [{ type: 'text', text: `Error retrieving newsletter: ${error.message}` }],
|
|
1357
|
+
isError: true,
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
);
|
|
1362
|
+
|
|
1363
|
+
// Create Newsletter Tool
|
|
1364
|
+
server.tool(
|
|
1365
|
+
'ghost_create_newsletter',
|
|
1366
|
+
'Creates a new newsletter in Ghost CMS with customizable sender settings and display options.',
|
|
1367
|
+
createNewsletterSchema,
|
|
1368
|
+
async (rawInput) => {
|
|
1369
|
+
const validation = validateToolInput(
|
|
1370
|
+
createNewsletterSchema,
|
|
1371
|
+
rawInput,
|
|
1372
|
+
'ghost_create_newsletter'
|
|
1373
|
+
);
|
|
1374
|
+
if (!validation.success) {
|
|
1375
|
+
return validation.errorResponse;
|
|
1376
|
+
}
|
|
1377
|
+
const input = validation.data;
|
|
1378
|
+
|
|
1379
|
+
console.error(`Executing tool: ghost_create_newsletter with name: ${input.name}`);
|
|
1380
|
+
try {
|
|
1381
|
+
await loadServices();
|
|
1382
|
+
|
|
1383
|
+
const createdNewsletter = await newsletterService.createNewsletterService(input);
|
|
1384
|
+
console.error(`Newsletter created successfully. Newsletter ID: ${createdNewsletter.id}`);
|
|
1385
|
+
|
|
1386
|
+
return {
|
|
1387
|
+
content: [{ type: 'text', text: JSON.stringify(createdNewsletter, null, 2) }],
|
|
1388
|
+
};
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
console.error(`Error in ghost_create_newsletter:`, error);
|
|
1391
|
+
if (error.name === 'ZodError') {
|
|
1392
|
+
const validationError = ValidationError.fromZod(error, 'Newsletter creation');
|
|
1393
|
+
return {
|
|
1394
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1395
|
+
isError: true,
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
content: [{ type: 'text', text: `Error creating newsletter: ${error.message}` }],
|
|
1400
|
+
isError: true,
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
);
|
|
1405
|
+
|
|
1406
|
+
// Update Newsletter Tool
|
|
1407
|
+
server.tool(
|
|
1408
|
+
'ghost_update_newsletter',
|
|
1409
|
+
'Updates an existing newsletter in Ghost CMS. Can update name, description, sender settings, and display options.',
|
|
1410
|
+
updateNewsletterInputSchema,
|
|
1411
|
+
async (rawInput) => {
|
|
1412
|
+
const validation = validateToolInput(
|
|
1413
|
+
updateNewsletterInputSchema,
|
|
1414
|
+
rawInput,
|
|
1415
|
+
'ghost_update_newsletter'
|
|
1416
|
+
);
|
|
1417
|
+
if (!validation.success) {
|
|
1418
|
+
return validation.errorResponse;
|
|
1419
|
+
}
|
|
1420
|
+
const input = validation.data;
|
|
1421
|
+
|
|
1422
|
+
console.error(`Executing tool: ghost_update_newsletter for newsletter ID: ${input.id}`);
|
|
1423
|
+
try {
|
|
1424
|
+
await loadServices();
|
|
1425
|
+
|
|
1426
|
+
const { id, ...updateData } = input;
|
|
1427
|
+
|
|
1428
|
+
const updatedNewsletter = await ghostService.updateNewsletter(id, updateData);
|
|
1429
|
+
console.error(`Newsletter updated successfully. Newsletter ID: ${updatedNewsletter.id}`);
|
|
1430
|
+
|
|
1431
|
+
return {
|
|
1432
|
+
content: [{ type: 'text', text: JSON.stringify(updatedNewsletter, null, 2) }],
|
|
1433
|
+
};
|
|
1434
|
+
} catch (error) {
|
|
1435
|
+
console.error(`Error in ghost_update_newsletter:`, error);
|
|
1436
|
+
if (error.name === 'ZodError') {
|
|
1437
|
+
const validationError = ValidationError.fromZod(error, 'Newsletter update');
|
|
1438
|
+
return {
|
|
1439
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1440
|
+
isError: true,
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
return {
|
|
1444
|
+
content: [{ type: 'text', text: `Error updating newsletter: ${error.message}` }],
|
|
1445
|
+
isError: true,
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
);
|
|
1450
|
+
|
|
1451
|
+
// Delete Newsletter Tool
|
|
1452
|
+
server.tool(
|
|
1453
|
+
'ghost_delete_newsletter',
|
|
1454
|
+
'Deletes a newsletter from Ghost CMS by ID. This operation is permanent and cannot be undone.',
|
|
1455
|
+
deleteNewsletterSchema,
|
|
1456
|
+
async (rawInput) => {
|
|
1457
|
+
const validation = validateToolInput(
|
|
1458
|
+
deleteNewsletterSchema,
|
|
1459
|
+
rawInput,
|
|
1460
|
+
'ghost_delete_newsletter'
|
|
1461
|
+
);
|
|
1462
|
+
if (!validation.success) {
|
|
1463
|
+
return validation.errorResponse;
|
|
1464
|
+
}
|
|
1465
|
+
const { id } = validation.data;
|
|
1466
|
+
|
|
1467
|
+
console.error(`Executing tool: ghost_delete_newsletter for newsletter ID: ${id}`);
|
|
1468
|
+
try {
|
|
1469
|
+
await loadServices();
|
|
1470
|
+
|
|
1471
|
+
await ghostService.deleteNewsletter(id);
|
|
1472
|
+
console.error(`Newsletter deleted successfully. Newsletter ID: ${id}`);
|
|
1473
|
+
|
|
1474
|
+
return {
|
|
1475
|
+
content: [{ type: 'text', text: `Newsletter ${id} has been successfully deleted.` }],
|
|
1476
|
+
};
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
console.error(`Error in ghost_delete_newsletter:`, error);
|
|
1479
|
+
if (error.name === 'ZodError') {
|
|
1480
|
+
const validationError = ValidationError.fromZod(error, 'Newsletter deletion');
|
|
1481
|
+
return {
|
|
1482
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1483
|
+
isError: true,
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
return {
|
|
1487
|
+
content: [{ type: 'text', text: `Error deleting newsletter: ${error.message}` }],
|
|
1488
|
+
isError: true,
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
);
|
|
1493
|
+
|
|
1494
|
+
// --- Tier Tools ---
|
|
1495
|
+
|
|
1496
|
+
// --- Tier Schema Definitions ---
|
|
1497
|
+
const getTierSchema = z.object({ id: ghostIdSchema });
|
|
1498
|
+
const updateTierInputSchema = z.object({ id: ghostIdSchema }).merge(updateTierSchema);
|
|
1499
|
+
const deleteTierSchema = z.object({ id: ghostIdSchema });
|
|
1500
|
+
|
|
1501
|
+
// Get Tiers Tool
|
|
1502
|
+
server.tool(
|
|
1503
|
+
'ghost_get_tiers',
|
|
1504
|
+
'Retrieves a list of tiers (membership levels) from Ghost CMS with optional filtering by type (free/paid).',
|
|
1505
|
+
tierQuerySchema,
|
|
1506
|
+
async (rawInput) => {
|
|
1507
|
+
const validation = validateToolInput(tierQuerySchema, rawInput, 'ghost_get_tiers');
|
|
1508
|
+
if (!validation.success) {
|
|
1509
|
+
return validation.errorResponse;
|
|
1510
|
+
}
|
|
1511
|
+
const input = validation.data;
|
|
1512
|
+
|
|
1513
|
+
console.error(`Executing tool: ghost_get_tiers`);
|
|
1514
|
+
try {
|
|
1515
|
+
await loadServices();
|
|
1516
|
+
|
|
1517
|
+
const tiers = await ghostService.getTiers(input);
|
|
1518
|
+
console.error(`Retrieved ${tiers.length} tiers`);
|
|
1519
|
+
|
|
1520
|
+
return {
|
|
1521
|
+
content: [{ type: 'text', text: JSON.stringify(tiers, null, 2) }],
|
|
1522
|
+
};
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
console.error(`Error in ghost_get_tiers:`, error);
|
|
1525
|
+
if (error.name === 'ZodError') {
|
|
1526
|
+
const validationError = ValidationError.fromZod(error, 'Tier query');
|
|
1527
|
+
return {
|
|
1528
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1529
|
+
isError: true,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
return {
|
|
1533
|
+
content: [{ type: 'text', text: `Error getting tiers: ${error.message}` }],
|
|
1534
|
+
isError: true,
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
);
|
|
1539
|
+
|
|
1540
|
+
// Get Tier Tool
|
|
1541
|
+
server.tool(
|
|
1542
|
+
'ghost_get_tier',
|
|
1543
|
+
'Retrieves a single tier (membership level) from Ghost CMS by ID.',
|
|
1544
|
+
getTierSchema,
|
|
1545
|
+
async (rawInput) => {
|
|
1546
|
+
const validation = validateToolInput(getTierSchema, rawInput, 'ghost_get_tier');
|
|
1547
|
+
if (!validation.success) {
|
|
1548
|
+
return validation.errorResponse;
|
|
1549
|
+
}
|
|
1550
|
+
const { id } = validation.data;
|
|
1551
|
+
|
|
1552
|
+
console.error(`Executing tool: ghost_get_tier for tier ID: ${id}`);
|
|
1553
|
+
try {
|
|
1554
|
+
await loadServices();
|
|
1555
|
+
|
|
1556
|
+
const tier = await ghostService.getTier(id);
|
|
1557
|
+
console.error(`Tier retrieved successfully. Tier ID: ${tier.id}`);
|
|
1558
|
+
|
|
1559
|
+
return {
|
|
1560
|
+
content: [{ type: 'text', text: JSON.stringify(tier, null, 2) }],
|
|
1561
|
+
};
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
console.error(`Error in ghost_get_tier:`, error);
|
|
1564
|
+
if (error.name === 'ZodError') {
|
|
1565
|
+
const validationError = ValidationError.fromZod(error, 'Tier retrieval');
|
|
1566
|
+
return {
|
|
1567
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1568
|
+
isError: true,
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
return {
|
|
1572
|
+
content: [{ type: 'text', text: `Error getting tier: ${error.message}` }],
|
|
1573
|
+
isError: true,
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
);
|
|
1578
|
+
|
|
1579
|
+
// Create Tier Tool
|
|
1580
|
+
server.tool(
|
|
1581
|
+
'ghost_create_tier',
|
|
1582
|
+
'Creates a new tier (membership level) in Ghost CMS with pricing and benefits.',
|
|
1583
|
+
createTierSchema,
|
|
1584
|
+
async (rawInput) => {
|
|
1585
|
+
const validation = validateToolInput(createTierSchema, rawInput, 'ghost_create_tier');
|
|
1586
|
+
if (!validation.success) {
|
|
1587
|
+
return validation.errorResponse;
|
|
1588
|
+
}
|
|
1589
|
+
const input = validation.data;
|
|
1590
|
+
|
|
1591
|
+
console.error(`Executing tool: ghost_create_tier`);
|
|
1592
|
+
try {
|
|
1593
|
+
await loadServices();
|
|
1594
|
+
|
|
1595
|
+
const tier = await ghostService.createTier(input);
|
|
1596
|
+
console.error(`Tier created successfully. Tier ID: ${tier.id}`);
|
|
1597
|
+
|
|
1598
|
+
return {
|
|
1599
|
+
content: [{ type: 'text', text: JSON.stringify(tier, null, 2) }],
|
|
1600
|
+
};
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
console.error(`Error in ghost_create_tier:`, error);
|
|
1603
|
+
if (error.name === 'ZodError') {
|
|
1604
|
+
const validationError = ValidationError.fromZod(error, 'Tier creation');
|
|
1605
|
+
return {
|
|
1606
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1607
|
+
isError: true,
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
return {
|
|
1611
|
+
content: [{ type: 'text', text: `Error creating tier: ${error.message}` }],
|
|
1612
|
+
isError: true,
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
);
|
|
1617
|
+
|
|
1618
|
+
// Update Tier Tool
|
|
1619
|
+
server.tool(
|
|
1620
|
+
'ghost_update_tier',
|
|
1621
|
+
'Updates an existing tier (membership level) in Ghost CMS. Can update pricing, benefits, and other tier properties.',
|
|
1622
|
+
updateTierInputSchema,
|
|
1623
|
+
async (rawInput) => {
|
|
1624
|
+
const validation = validateToolInput(updateTierInputSchema, rawInput, 'ghost_update_tier');
|
|
1625
|
+
if (!validation.success) {
|
|
1626
|
+
return validation.errorResponse;
|
|
1627
|
+
}
|
|
1628
|
+
const input = validation.data;
|
|
1629
|
+
|
|
1630
|
+
console.error(`Executing tool: ghost_update_tier for tier ID: ${input.id}`);
|
|
1631
|
+
try {
|
|
1632
|
+
await loadServices();
|
|
1633
|
+
|
|
1634
|
+
const { id, ...updateData } = input;
|
|
1635
|
+
|
|
1636
|
+
const updatedTier = await ghostService.updateTier(id, updateData);
|
|
1637
|
+
console.error(`Tier updated successfully. Tier ID: ${updatedTier.id}`);
|
|
1638
|
+
|
|
1639
|
+
return {
|
|
1640
|
+
content: [{ type: 'text', text: JSON.stringify(updatedTier, null, 2) }],
|
|
1641
|
+
};
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
console.error(`Error in ghost_update_tier:`, error);
|
|
1644
|
+
if (error.name === 'ZodError') {
|
|
1645
|
+
const validationError = ValidationError.fromZod(error, 'Tier update');
|
|
1646
|
+
return {
|
|
1647
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1648
|
+
isError: true,
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
return {
|
|
1652
|
+
content: [{ type: 'text', text: `Error updating tier: ${error.message}` }],
|
|
1653
|
+
isError: true,
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
);
|
|
1658
|
+
|
|
1659
|
+
// Delete Tier Tool
|
|
1660
|
+
server.tool(
|
|
1661
|
+
'ghost_delete_tier',
|
|
1662
|
+
'Deletes a tier (membership level) from Ghost CMS by ID. This operation is permanent and cannot be undone.',
|
|
1663
|
+
deleteTierSchema,
|
|
1664
|
+
async (rawInput) => {
|
|
1665
|
+
const validation = validateToolInput(deleteTierSchema, rawInput, 'ghost_delete_tier');
|
|
1666
|
+
if (!validation.success) {
|
|
1667
|
+
return validation.errorResponse;
|
|
1668
|
+
}
|
|
1669
|
+
const { id } = validation.data;
|
|
1670
|
+
|
|
1671
|
+
console.error(`Executing tool: ghost_delete_tier for tier ID: ${id}`);
|
|
1672
|
+
try {
|
|
1673
|
+
await loadServices();
|
|
1674
|
+
|
|
1675
|
+
await ghostService.deleteTier(id);
|
|
1676
|
+
console.error(`Tier deleted successfully. Tier ID: ${id}`);
|
|
1677
|
+
|
|
1678
|
+
return {
|
|
1679
|
+
content: [{ type: 'text', text: `Tier ${id} has been successfully deleted.` }],
|
|
1680
|
+
};
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
console.error(`Error in ghost_delete_tier:`, error);
|
|
1683
|
+
if (error.name === 'ZodError') {
|
|
1684
|
+
const validationError = ValidationError.fromZod(error, 'Tier deletion');
|
|
1685
|
+
return {
|
|
1686
|
+
content: [{ type: 'text', text: JSON.stringify(validationError.toJSON(), null, 2) }],
|
|
1687
|
+
isError: true,
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
return {
|
|
1691
|
+
content: [{ type: 'text', text: `Error deleting tier: ${error.message}` }],
|
|
1692
|
+
isError: true,
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
);
|
|
1697
|
+
|
|
1698
|
+
// --- Main Entry Point ---
|
|
1699
|
+
|
|
1700
|
+
async function main() {
|
|
1701
|
+
console.error('Starting Ghost MCP Server...');
|
|
1702
|
+
|
|
1703
|
+
const transport = new StdioServerTransport();
|
|
1704
|
+
await server.connect(transport);
|
|
1705
|
+
|
|
1706
|
+
console.error('Ghost MCP Server running on stdio transport');
|
|
1707
|
+
console.error(
|
|
1708
|
+
'Available tools: ghost_get_tags, ghost_create_tag, ghost_get_tag, ghost_update_tag, ghost_delete_tag, ghost_upload_image, ' +
|
|
1709
|
+
'ghost_create_post, ghost_get_posts, ghost_get_post, ghost_search_posts, ghost_update_post, ghost_delete_post, ' +
|
|
1710
|
+
'ghost_get_pages, ghost_get_page, ghost_create_page, ghost_update_page, ghost_delete_page, ghost_search_pages, ' +
|
|
1711
|
+
'ghost_create_member, ghost_update_member, ghost_delete_member, ghost_get_members, ghost_get_member, ghost_search_members, ' +
|
|
1712
|
+
'ghost_get_newsletters, ghost_get_newsletter, ghost_create_newsletter, ghost_update_newsletter, ghost_delete_newsletter, ' +
|
|
1713
|
+
'ghost_get_tiers, ghost_get_tier, ghost_create_tier, ghost_update_tier, ghost_delete_tier'
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
main().catch((error) => {
|
|
1718
|
+
console.error('Fatal error starting MCP server:', error);
|
|
1719
|
+
process.exit(1);
|
|
1720
|
+
});
|