@jgardner04/ghost-mcp-server 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -20
- package/package.json +1 -1
- package/src/__tests__/mcp_server_pages.test.js +520 -0
- package/src/mcp_server_improved.js +274 -1
- package/src/services/__tests__/ghostServiceImproved.pages.test.js +561 -0
- package/src/services/__tests__/pageService.test.js +400 -0
- package/src/services/ghostServiceImproved.js +257 -0
- package/src/services/pageService.js +121 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import sanitizeHtml from 'sanitize-html';
|
|
2
|
+
import Joi from 'joi';
|
|
3
|
+
import { createContextLogger } from '../utils/logger.js';
|
|
4
|
+
import { createPage as createGhostPage } from './ghostServiceImproved.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper to generate a simple meta description from HTML content.
|
|
8
|
+
* Uses sanitize-html to safely strip HTML tags and truncates.
|
|
9
|
+
* @param {string} htmlContent - The HTML content of the page.
|
|
10
|
+
* @param {number} maxLength - The maximum length of the description.
|
|
11
|
+
* @returns {string} A plain text truncated description.
|
|
12
|
+
*/
|
|
13
|
+
const generateSimpleMetaDescription = (htmlContent, maxLength = 500) => {
|
|
14
|
+
if (!htmlContent) return '';
|
|
15
|
+
|
|
16
|
+
// Use sanitize-html to safely remove all HTML tags
|
|
17
|
+
// This prevents ReDoS attacks and properly handles malformed HTML
|
|
18
|
+
const textContent = sanitizeHtml(htmlContent, {
|
|
19
|
+
allowedTags: [], // Remove all HTML tags
|
|
20
|
+
allowedAttributes: {},
|
|
21
|
+
textFilter: function (text) {
|
|
22
|
+
return text.replace(/\s\s+/g, ' ').trim();
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Truncate and add ellipsis if needed
|
|
27
|
+
return textContent.length > maxLength
|
|
28
|
+
? textContent.substring(0, maxLength - 3) + '...'
|
|
29
|
+
: textContent;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validation schema for page input
|
|
34
|
+
* Pages are similar to posts but do NOT support tags
|
|
35
|
+
*/
|
|
36
|
+
const pageInputSchema = Joi.object({
|
|
37
|
+
title: Joi.string().max(255).required(),
|
|
38
|
+
html: Joi.string().required(),
|
|
39
|
+
custom_excerpt: Joi.string().max(500).optional(),
|
|
40
|
+
status: Joi.string().valid('draft', 'published', 'scheduled').optional(),
|
|
41
|
+
published_at: Joi.string().isoDate().optional(),
|
|
42
|
+
// NO tags field - pages don't support tags
|
|
43
|
+
feature_image: Joi.string().uri().optional(),
|
|
44
|
+
feature_image_alt: Joi.string().max(255).optional(),
|
|
45
|
+
feature_image_caption: Joi.string().max(500).optional(),
|
|
46
|
+
meta_title: Joi.string().max(70).optional(),
|
|
47
|
+
meta_description: Joi.string().max(160).optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Service layer function to handle the business logic of creating a page.
|
|
52
|
+
* Transforms input data, generates metadata defaults.
|
|
53
|
+
* Note: Pages do NOT support tags (unlike posts).
|
|
54
|
+
* @param {object} pageInput - Data received from the controller.
|
|
55
|
+
* @returns {Promise<object>} The created page object from the Ghost API.
|
|
56
|
+
*/
|
|
57
|
+
const createPageService = async (pageInput) => {
|
|
58
|
+
const logger = createContextLogger('page-service');
|
|
59
|
+
|
|
60
|
+
// Validate input to prevent format string vulnerabilities
|
|
61
|
+
const { error, value: validatedInput } = pageInputSchema.validate(pageInput);
|
|
62
|
+
if (error) {
|
|
63
|
+
logger.error('Page input validation failed', {
|
|
64
|
+
error: error.details[0].message,
|
|
65
|
+
inputKeys: Object.keys(pageInput),
|
|
66
|
+
});
|
|
67
|
+
throw new Error(`Invalid page input: ${error.details[0].message}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const {
|
|
71
|
+
title,
|
|
72
|
+
html,
|
|
73
|
+
custom_excerpt,
|
|
74
|
+
status,
|
|
75
|
+
published_at,
|
|
76
|
+
// NO tags destructuring - pages don't support tags
|
|
77
|
+
feature_image,
|
|
78
|
+
feature_image_alt,
|
|
79
|
+
feature_image_caption,
|
|
80
|
+
meta_title,
|
|
81
|
+
meta_description,
|
|
82
|
+
} = validatedInput;
|
|
83
|
+
|
|
84
|
+
// NO tag resolution section (removed from postService)
|
|
85
|
+
// Pages do not support tags in Ghost CMS
|
|
86
|
+
|
|
87
|
+
// Metadata defaults
|
|
88
|
+
const finalMetaTitle = meta_title || title;
|
|
89
|
+
const finalMetaDescription =
|
|
90
|
+
meta_description || custom_excerpt || generateSimpleMetaDescription(html);
|
|
91
|
+
const truncatedMetaDescription =
|
|
92
|
+
finalMetaDescription.length > 500
|
|
93
|
+
? finalMetaDescription.substring(0, 497) + '...'
|
|
94
|
+
: finalMetaDescription;
|
|
95
|
+
|
|
96
|
+
// Prepare data for Ghost API
|
|
97
|
+
const pageDataForApi = {
|
|
98
|
+
title,
|
|
99
|
+
html,
|
|
100
|
+
custom_excerpt,
|
|
101
|
+
status: status || 'draft',
|
|
102
|
+
published_at,
|
|
103
|
+
// NO tags field
|
|
104
|
+
feature_image,
|
|
105
|
+
feature_image_alt,
|
|
106
|
+
feature_image_caption,
|
|
107
|
+
meta_title: finalMetaTitle,
|
|
108
|
+
meta_description: truncatedMetaDescription,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
logger.info('Creating Ghost page', {
|
|
112
|
+
title: pageDataForApi.title,
|
|
113
|
+
status: pageDataForApi.status,
|
|
114
|
+
hasFeatureImage: !!pageDataForApi.feature_image,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const newPage = await createGhostPage(pageDataForApi);
|
|
118
|
+
return newPage;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export { createPageService };
|