@jgardner04/ghost-mcp-server 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/mcp_server_improved.test.js +285 -0
- package/src/mcp_server_improved.js +318 -2
- package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +306 -0
- package/src/services/__tests__/newsletterService.test.js +217 -0
- package/src/services/ghostServiceImproved.js +138 -3
- package/src/services/newsletterService.js +47 -0
package/package.json
CHANGED
|
@@ -41,7 +41,10 @@ vi.mock('crypto', () => ({
|
|
|
41
41
|
const mockGetPosts = vi.fn();
|
|
42
42
|
const mockGetPost = vi.fn();
|
|
43
43
|
const mockGetTags = vi.fn();
|
|
44
|
+
const mockGetTag = vi.fn();
|
|
44
45
|
const mockCreateTag = vi.fn();
|
|
46
|
+
const mockUpdateTag = vi.fn();
|
|
47
|
+
const mockDeleteTag = vi.fn();
|
|
45
48
|
const mockUploadImage = vi.fn();
|
|
46
49
|
const mockCreatePostService = vi.fn();
|
|
47
50
|
const mockProcessImage = vi.fn();
|
|
@@ -67,6 +70,9 @@ vi.mock('../services/ghostServiceImproved.js', () => ({
|
|
|
67
70
|
updatePost: (...args) => mockUpdatePost(...args),
|
|
68
71
|
deletePost: (...args) => mockDeletePost(...args),
|
|
69
72
|
searchPosts: (...args) => mockSearchPosts(...args),
|
|
73
|
+
getTag: (...args) => mockGetTag(...args),
|
|
74
|
+
updateTag: (...args) => mockUpdateTag(...args),
|
|
75
|
+
deleteTag: (...args) => mockDeleteTag(...args),
|
|
70
76
|
}));
|
|
71
77
|
|
|
72
78
|
vi.mock('../services/imageProcessingService.js', () => ({
|
|
@@ -907,3 +913,282 @@ describe('mcp_server_improved - ghost_search_posts tool', () => {
|
|
|
907
913
|
expect(result.isError).toBeUndefined();
|
|
908
914
|
});
|
|
909
915
|
});
|
|
916
|
+
|
|
917
|
+
describe('ghost_get_tag', () => {
|
|
918
|
+
beforeEach(() => {
|
|
919
|
+
vi.clearAllMocks();
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it('should be registered as a tool', () => {
|
|
923
|
+
expect(mockTools.has('ghost_get_tag')).toBe(true);
|
|
924
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
925
|
+
expect(tool.name).toBe('ghost_get_tag');
|
|
926
|
+
expect(tool.description).toBeDefined();
|
|
927
|
+
expect(tool.schema).toBeDefined();
|
|
928
|
+
expect(tool.handler).toBeDefined();
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('should have correct schema with id and slug as optional', () => {
|
|
932
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
933
|
+
expect(tool.schema.id).toBeDefined();
|
|
934
|
+
expect(tool.schema.slug).toBeDefined();
|
|
935
|
+
expect(tool.schema.include).toBeDefined();
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('should retrieve tag by ID', async () => {
|
|
939
|
+
const mockTag = {
|
|
940
|
+
id: '123',
|
|
941
|
+
name: 'Test Tag',
|
|
942
|
+
slug: 'test-tag',
|
|
943
|
+
description: 'A test tag',
|
|
944
|
+
};
|
|
945
|
+
mockGetTag.mockResolvedValue(mockTag);
|
|
946
|
+
|
|
947
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
948
|
+
const result = await tool.handler({ id: '123' });
|
|
949
|
+
|
|
950
|
+
expect(mockGetTag).toHaveBeenCalledWith('123', {});
|
|
951
|
+
expect(result.content).toBeDefined();
|
|
952
|
+
expect(result.content[0].type).toBe('text');
|
|
953
|
+
expect(result.content[0].text).toContain('"id": "123"');
|
|
954
|
+
expect(result.content[0].text).toContain('"name": "Test Tag"');
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it('should retrieve tag by slug', async () => {
|
|
958
|
+
const mockTag = {
|
|
959
|
+
id: '123',
|
|
960
|
+
name: 'Test Tag',
|
|
961
|
+
slug: 'test-tag',
|
|
962
|
+
description: 'A test tag',
|
|
963
|
+
};
|
|
964
|
+
mockGetTag.mockResolvedValue(mockTag);
|
|
965
|
+
|
|
966
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
967
|
+
const result = await tool.handler({ slug: 'test-tag' });
|
|
968
|
+
|
|
969
|
+
expect(mockGetTag).toHaveBeenCalledWith('slug/test-tag', {});
|
|
970
|
+
expect(result.content[0].text).toContain('"slug": "test-tag"');
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it('should support include parameter for post count', async () => {
|
|
974
|
+
const mockTag = {
|
|
975
|
+
id: '123',
|
|
976
|
+
name: 'Test Tag',
|
|
977
|
+
slug: 'test-tag',
|
|
978
|
+
count: { posts: 5 },
|
|
979
|
+
};
|
|
980
|
+
mockGetTag.mockResolvedValue(mockTag);
|
|
981
|
+
|
|
982
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
983
|
+
const result = await tool.handler({ id: '123', include: 'count.posts' });
|
|
984
|
+
|
|
985
|
+
expect(mockGetTag).toHaveBeenCalledWith('123', { include: 'count.posts' });
|
|
986
|
+
expect(result.content[0].text).toContain('"count"');
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it('should return error when neither id nor slug provided', async () => {
|
|
990
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
991
|
+
const result = await tool.handler({});
|
|
992
|
+
|
|
993
|
+
expect(result.content[0].type).toBe('text');
|
|
994
|
+
expect(result.content[0].text).toContain('Either id or slug must be provided');
|
|
995
|
+
expect(result.isError).toBe(true);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it('should handle not found error', async () => {
|
|
999
|
+
mockGetTag.mockRejectedValue(new Error('Tag not found'));
|
|
1000
|
+
|
|
1001
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
1002
|
+
const result = await tool.handler({ id: '999' });
|
|
1003
|
+
|
|
1004
|
+
expect(result.isError).toBe(true);
|
|
1005
|
+
expect(result.content[0].text).toContain('Tag not found');
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
describe('ghost_update_tag', () => {
|
|
1010
|
+
beforeEach(() => {
|
|
1011
|
+
vi.clearAllMocks();
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('should be registered as a tool', () => {
|
|
1015
|
+
expect(mockTools.has('ghost_update_tag')).toBe(true);
|
|
1016
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1017
|
+
expect(tool.name).toBe('ghost_update_tag');
|
|
1018
|
+
expect(tool.description).toBeDefined();
|
|
1019
|
+
expect(tool.schema).toBeDefined();
|
|
1020
|
+
expect(tool.handler).toBeDefined();
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it('should have correct schema with all update fields', () => {
|
|
1024
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1025
|
+
expect(tool.schema.id).toBeDefined();
|
|
1026
|
+
expect(tool.schema.name).toBeDefined();
|
|
1027
|
+
expect(tool.schema.slug).toBeDefined();
|
|
1028
|
+
expect(tool.schema.description).toBeDefined();
|
|
1029
|
+
expect(tool.schema.feature_image).toBeDefined();
|
|
1030
|
+
expect(tool.schema.meta_title).toBeDefined();
|
|
1031
|
+
expect(tool.schema.meta_description).toBeDefined();
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it('should update tag name', async () => {
|
|
1035
|
+
const mockUpdatedTag = {
|
|
1036
|
+
id: '123',
|
|
1037
|
+
name: 'Updated Tag',
|
|
1038
|
+
slug: 'updated-tag',
|
|
1039
|
+
};
|
|
1040
|
+
mockUpdateTag.mockResolvedValue(mockUpdatedTag);
|
|
1041
|
+
|
|
1042
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1043
|
+
const result = await tool.handler({ id: '123', name: 'Updated Tag' });
|
|
1044
|
+
|
|
1045
|
+
expect(mockUpdateTag).toHaveBeenCalledWith('123', { name: 'Updated Tag' });
|
|
1046
|
+
expect(result.content[0].text).toContain('"name": "Updated Tag"');
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
it('should update tag description', async () => {
|
|
1050
|
+
const mockUpdatedTag = {
|
|
1051
|
+
id: '123',
|
|
1052
|
+
name: 'Test Tag',
|
|
1053
|
+
description: 'New description',
|
|
1054
|
+
};
|
|
1055
|
+
mockUpdateTag.mockResolvedValue(mockUpdatedTag);
|
|
1056
|
+
|
|
1057
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1058
|
+
const result = await tool.handler({ id: '123', description: 'New description' });
|
|
1059
|
+
|
|
1060
|
+
expect(mockUpdateTag).toHaveBeenCalledWith('123', { description: 'New description' });
|
|
1061
|
+
expect(result.content[0].text).toContain('"description": "New description"');
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
it('should update multiple fields at once', async () => {
|
|
1065
|
+
const mockUpdatedTag = {
|
|
1066
|
+
id: '123',
|
|
1067
|
+
name: 'Updated Tag',
|
|
1068
|
+
slug: 'updated-tag',
|
|
1069
|
+
description: 'Updated description',
|
|
1070
|
+
meta_title: 'Updated Meta',
|
|
1071
|
+
};
|
|
1072
|
+
mockUpdateTag.mockResolvedValue(mockUpdatedTag);
|
|
1073
|
+
|
|
1074
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1075
|
+
await tool.handler({
|
|
1076
|
+
id: '123',
|
|
1077
|
+
name: 'Updated Tag',
|
|
1078
|
+
description: 'Updated description',
|
|
1079
|
+
meta_title: 'Updated Meta',
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
expect(mockUpdateTag).toHaveBeenCalledWith('123', {
|
|
1083
|
+
name: 'Updated Tag',
|
|
1084
|
+
description: 'Updated description',
|
|
1085
|
+
meta_title: 'Updated Meta',
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it('should update tag feature image', async () => {
|
|
1090
|
+
const mockUpdatedTag = {
|
|
1091
|
+
id: '123',
|
|
1092
|
+
name: 'Test Tag',
|
|
1093
|
+
feature_image: 'https://example.com/image.jpg',
|
|
1094
|
+
};
|
|
1095
|
+
mockUpdateTag.mockResolvedValue(mockUpdatedTag);
|
|
1096
|
+
|
|
1097
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1098
|
+
await tool.handler({
|
|
1099
|
+
id: '123',
|
|
1100
|
+
feature_image: 'https://example.com/image.jpg',
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
expect(mockUpdateTag).toHaveBeenCalledWith('123', {
|
|
1104
|
+
feature_image: 'https://example.com/image.jpg',
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
it('should return error when id is missing', async () => {
|
|
1109
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1110
|
+
const result = await tool.handler({ name: 'Test' });
|
|
1111
|
+
|
|
1112
|
+
expect(result.isError).toBe(true);
|
|
1113
|
+
expect(result.content[0].text).toContain('Tag ID is required');
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
it('should handle validation error', async () => {
|
|
1117
|
+
mockUpdateTag.mockRejectedValue(new Error('Validation failed'));
|
|
1118
|
+
|
|
1119
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1120
|
+
const result = await tool.handler({ id: '123', name: '' });
|
|
1121
|
+
|
|
1122
|
+
expect(result.isError).toBe(true);
|
|
1123
|
+
expect(result.content[0].text).toContain('Validation failed');
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it('should handle not found error', async () => {
|
|
1127
|
+
mockUpdateTag.mockRejectedValue(new Error('Tag not found'));
|
|
1128
|
+
|
|
1129
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1130
|
+
const result = await tool.handler({ id: '999', name: 'Test' });
|
|
1131
|
+
|
|
1132
|
+
expect(result.isError).toBe(true);
|
|
1133
|
+
expect(result.content[0].text).toContain('Tag not found');
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
describe('ghost_delete_tag', () => {
|
|
1138
|
+
beforeEach(() => {
|
|
1139
|
+
vi.clearAllMocks();
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it('should be registered as a tool', () => {
|
|
1143
|
+
expect(mockTools.has('ghost_delete_tag')).toBe(true);
|
|
1144
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1145
|
+
expect(tool.name).toBe('ghost_delete_tag');
|
|
1146
|
+
expect(tool.description).toBeDefined();
|
|
1147
|
+
expect(tool.schema).toBeDefined();
|
|
1148
|
+
expect(tool.handler).toBeDefined();
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it('should have correct schema with id field', () => {
|
|
1152
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1153
|
+
expect(tool.schema.id).toBeDefined();
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it('should delete tag successfully', async () => {
|
|
1157
|
+
mockDeleteTag.mockResolvedValue({ success: true });
|
|
1158
|
+
|
|
1159
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1160
|
+
const result = await tool.handler({ id: '123' });
|
|
1161
|
+
|
|
1162
|
+
expect(mockDeleteTag).toHaveBeenCalledWith('123');
|
|
1163
|
+
expect(result.content[0].text).toContain('successfully deleted');
|
|
1164
|
+
expect(result.isError).toBeUndefined();
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it('should return error when id is missing', async () => {
|
|
1168
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1169
|
+
const result = await tool.handler({});
|
|
1170
|
+
|
|
1171
|
+
expect(result.isError).toBe(true);
|
|
1172
|
+
expect(result.content[0].text).toContain('Tag ID is required');
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
it('should handle not found error', async () => {
|
|
1176
|
+
mockDeleteTag.mockRejectedValue(new Error('Tag not found'));
|
|
1177
|
+
|
|
1178
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1179
|
+
const result = await tool.handler({ id: '999' });
|
|
1180
|
+
|
|
1181
|
+
expect(result.isError).toBe(true);
|
|
1182
|
+
expect(result.content[0].text).toContain('Tag not found');
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it('should handle deletion error', async () => {
|
|
1186
|
+
mockDeleteTag.mockRejectedValue(new Error('Failed to delete tag'));
|
|
1187
|
+
|
|
1188
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1189
|
+
const result = await tool.handler({ id: '123' });
|
|
1190
|
+
|
|
1191
|
+
expect(result.isError).toBe(true);
|
|
1192
|
+
expect(result.content[0].text).toContain('Failed to delete tag');
|
|
1193
|
+
});
|
|
1194
|
+
});
|
|
@@ -122,6 +122,129 @@ server.tool(
|
|
|
122
122
|
}
|
|
123
123
|
);
|
|
124
124
|
|
|
125
|
+
// Get Tag Tool
|
|
126
|
+
server.tool(
|
|
127
|
+
'ghost_get_tag',
|
|
128
|
+
'Retrieves a single tag from Ghost CMS by ID or slug.',
|
|
129
|
+
{
|
|
130
|
+
id: z.string().optional().describe('The ID of the tag to retrieve.'),
|
|
131
|
+
slug: z.string().optional().describe('The slug of the tag to retrieve.'),
|
|
132
|
+
include: z
|
|
133
|
+
.string()
|
|
134
|
+
.optional()
|
|
135
|
+
.describe('Additional resources to include (e.g., "count.posts").'),
|
|
136
|
+
},
|
|
137
|
+
async ({ id, slug, include }) => {
|
|
138
|
+
console.error(`Executing tool: ghost_get_tag`);
|
|
139
|
+
try {
|
|
140
|
+
if (!id && !slug) {
|
|
141
|
+
throw new Error('Either id or slug must be provided');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await loadServices();
|
|
145
|
+
|
|
146
|
+
// If slug is provided, use the slug/slug-name format
|
|
147
|
+
const identifier = slug ? `slug/${slug}` : id;
|
|
148
|
+
const options = include ? { include } : {};
|
|
149
|
+
|
|
150
|
+
const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
|
|
151
|
+
const tag = await ghostServiceImproved.getTag(identifier, options);
|
|
152
|
+
console.error(`Tag retrieved successfully. Tag ID: ${tag.id}`);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: 'text', text: JSON.stringify(tag, null, 2) }],
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(`Error in ghost_get_tag:`, error);
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
161
|
+
isError: true,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Update Tag Tool
|
|
168
|
+
server.tool(
|
|
169
|
+
'ghost_update_tag',
|
|
170
|
+
'Updates an existing tag in Ghost CMS.',
|
|
171
|
+
{
|
|
172
|
+
id: z.string().describe('The ID of the tag to update.'),
|
|
173
|
+
name: z.string().optional().describe('The new name for the tag.'),
|
|
174
|
+
slug: z.string().optional().describe('The new slug for the tag.'),
|
|
175
|
+
description: z.string().optional().describe('The new description for the tag.'),
|
|
176
|
+
feature_image: z.string().url().optional().describe('URL of the feature image for the tag.'),
|
|
177
|
+
meta_title: z.string().optional().describe('SEO meta title for the tag.'),
|
|
178
|
+
meta_description: z.string().optional().describe('SEO meta description for the tag.'),
|
|
179
|
+
},
|
|
180
|
+
async ({ id, name, slug, description, feature_image, meta_title, meta_description }) => {
|
|
181
|
+
console.error(`Executing tool: ghost_update_tag for ID: ${id}`);
|
|
182
|
+
try {
|
|
183
|
+
if (!id) {
|
|
184
|
+
throw new Error('Tag ID is required');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await loadServices();
|
|
188
|
+
|
|
189
|
+
// Build update data object with only provided fields
|
|
190
|
+
const updateData = {};
|
|
191
|
+
if (name !== undefined) updateData.name = name;
|
|
192
|
+
if (slug !== undefined) updateData.slug = slug;
|
|
193
|
+
if (description !== undefined) updateData.description = description;
|
|
194
|
+
if (feature_image !== undefined) updateData.feature_image = feature_image;
|
|
195
|
+
if (meta_title !== undefined) updateData.meta_title = meta_title;
|
|
196
|
+
if (meta_description !== undefined) updateData.meta_description = meta_description;
|
|
197
|
+
|
|
198
|
+
const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
|
|
199
|
+
const updatedTag = await ghostServiceImproved.updateTag(id, updateData);
|
|
200
|
+
console.error(`Tag updated successfully. Tag ID: ${updatedTag.id}`);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: 'text', text: JSON.stringify(updatedTag, null, 2) }],
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error(`Error in ghost_update_tag:`, error);
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
209
|
+
isError: true,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Delete Tag Tool
|
|
216
|
+
server.tool(
|
|
217
|
+
'ghost_delete_tag',
|
|
218
|
+
'Deletes a tag from Ghost CMS by ID. This operation is permanent.',
|
|
219
|
+
{
|
|
220
|
+
id: z.string().describe('The ID of the tag to delete.'),
|
|
221
|
+
},
|
|
222
|
+
async ({ id }) => {
|
|
223
|
+
console.error(`Executing tool: ghost_delete_tag for ID: ${id}`);
|
|
224
|
+
try {
|
|
225
|
+
if (!id) {
|
|
226
|
+
throw new Error('Tag ID is required');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await loadServices();
|
|
230
|
+
|
|
231
|
+
const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
|
|
232
|
+
await ghostServiceImproved.deleteTag(id);
|
|
233
|
+
console.error(`Tag deleted successfully. Tag ID: ${id}`);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: 'text', text: `Tag with ID ${id} has been successfully deleted.` }],
|
|
237
|
+
};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error(`Error in ghost_delete_tag:`, error);
|
|
240
|
+
return {
|
|
241
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
242
|
+
isError: true,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
|
|
125
248
|
// Upload Image Tool
|
|
126
249
|
server.tool(
|
|
127
250
|
'ghost_upload_image',
|
|
@@ -780,6 +903,198 @@ server.tool(
|
|
|
780
903
|
}
|
|
781
904
|
);
|
|
782
905
|
|
|
906
|
+
// =============================================================================
|
|
907
|
+
// NEWSLETTER TOOLS
|
|
908
|
+
// =============================================================================
|
|
909
|
+
|
|
910
|
+
// Get Newsletters Tool
|
|
911
|
+
server.tool(
|
|
912
|
+
'ghost_get_newsletters',
|
|
913
|
+
'Retrieves a list of newsletters from Ghost CMS with optional filtering.',
|
|
914
|
+
{
|
|
915
|
+
limit: z
|
|
916
|
+
.number()
|
|
917
|
+
.min(1)
|
|
918
|
+
.max(100)
|
|
919
|
+
.optional()
|
|
920
|
+
.describe('Number of newsletters to retrieve (1-100). Default is all.'),
|
|
921
|
+
filter: z.string().optional().describe('Ghost NQL filter string for advanced filtering.'),
|
|
922
|
+
},
|
|
923
|
+
async (input) => {
|
|
924
|
+
console.error(`Executing tool: ghost_get_newsletters`);
|
|
925
|
+
try {
|
|
926
|
+
await loadServices();
|
|
927
|
+
|
|
928
|
+
const options = {};
|
|
929
|
+
if (input.limit !== undefined) options.limit = input.limit;
|
|
930
|
+
if (input.filter !== undefined) options.filter = input.filter;
|
|
931
|
+
|
|
932
|
+
const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
|
|
933
|
+
const newsletters = await ghostServiceImproved.getNewsletters(options);
|
|
934
|
+
console.error(`Retrieved ${newsletters.length} newsletters from Ghost.`);
|
|
935
|
+
|
|
936
|
+
return {
|
|
937
|
+
content: [{ type: 'text', text: JSON.stringify(newsletters, null, 2) }],
|
|
938
|
+
};
|
|
939
|
+
} catch (error) {
|
|
940
|
+
console.error(`Error in ghost_get_newsletters:`, error);
|
|
941
|
+
return {
|
|
942
|
+
content: [{ type: 'text', text: `Error retrieving newsletters: ${error.message}` }],
|
|
943
|
+
isError: true,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
// Get Newsletter Tool
|
|
950
|
+
server.tool(
|
|
951
|
+
'ghost_get_newsletter',
|
|
952
|
+
'Retrieves a single newsletter from Ghost CMS by ID.',
|
|
953
|
+
{
|
|
954
|
+
id: z.string().describe('The ID of the newsletter to retrieve.'),
|
|
955
|
+
},
|
|
956
|
+
async ({ id }) => {
|
|
957
|
+
console.error(`Executing tool: ghost_get_newsletter for ID: ${id}`);
|
|
958
|
+
try {
|
|
959
|
+
await loadServices();
|
|
960
|
+
|
|
961
|
+
const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
|
|
962
|
+
const newsletter = await ghostServiceImproved.getNewsletter(id);
|
|
963
|
+
console.error(`Retrieved newsletter: ${newsletter.name} (ID: ${newsletter.id})`);
|
|
964
|
+
|
|
965
|
+
return {
|
|
966
|
+
content: [{ type: 'text', text: JSON.stringify(newsletter, null, 2) }],
|
|
967
|
+
};
|
|
968
|
+
} catch (error) {
|
|
969
|
+
console.error(`Error in ghost_get_newsletter:`, error);
|
|
970
|
+
return {
|
|
971
|
+
content: [{ type: 'text', text: `Error retrieving newsletter: ${error.message}` }],
|
|
972
|
+
isError: true,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
// Create Newsletter Tool
|
|
979
|
+
server.tool(
|
|
980
|
+
'ghost_create_newsletter',
|
|
981
|
+
'Creates a new newsletter in Ghost CMS with customizable sender settings and display options.',
|
|
982
|
+
{
|
|
983
|
+
name: z.string().describe('The name of the newsletter.'),
|
|
984
|
+
description: z.string().optional().describe('A description for the newsletter.'),
|
|
985
|
+
sender_name: z.string().optional().describe('The sender name for newsletter emails.'),
|
|
986
|
+
sender_email: z
|
|
987
|
+
.string()
|
|
988
|
+
.email()
|
|
989
|
+
.optional()
|
|
990
|
+
.describe('The sender email address for newsletter emails.'),
|
|
991
|
+
sender_reply_to: z
|
|
992
|
+
.enum(['newsletter', 'support'])
|
|
993
|
+
.optional()
|
|
994
|
+
.describe('Reply-to address setting. Options: newsletter, support.'),
|
|
995
|
+
subscribe_on_signup: z
|
|
996
|
+
.boolean()
|
|
997
|
+
.optional()
|
|
998
|
+
.describe('Whether new members are automatically subscribed to this newsletter on signup.'),
|
|
999
|
+
show_header_icon: z
|
|
1000
|
+
.boolean()
|
|
1001
|
+
.optional()
|
|
1002
|
+
.describe('Whether to show the site icon in the newsletter header.'),
|
|
1003
|
+
show_header_title: z
|
|
1004
|
+
.boolean()
|
|
1005
|
+
.optional()
|
|
1006
|
+
.describe('Whether to show the site title in the newsletter header.'),
|
|
1007
|
+
},
|
|
1008
|
+
async (input) => {
|
|
1009
|
+
console.error(`Executing tool: ghost_create_newsletter with name: ${input.name}`);
|
|
1010
|
+
try {
|
|
1011
|
+
await loadServices();
|
|
1012
|
+
|
|
1013
|
+
const newsletterService = await import('./services/newsletterService.js');
|
|
1014
|
+
const createdNewsletter = await newsletterService.createNewsletterService(input);
|
|
1015
|
+
console.error(`Newsletter created successfully. Newsletter ID: ${createdNewsletter.id}`);
|
|
1016
|
+
|
|
1017
|
+
return {
|
|
1018
|
+
content: [{ type: 'text', text: JSON.stringify(createdNewsletter, null, 2) }],
|
|
1019
|
+
};
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
console.error(`Error in ghost_create_newsletter:`, error);
|
|
1022
|
+
return {
|
|
1023
|
+
content: [{ type: 'text', text: `Error creating newsletter: ${error.message}` }],
|
|
1024
|
+
isError: true,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
// Update Newsletter Tool
|
|
1031
|
+
server.tool(
|
|
1032
|
+
'ghost_update_newsletter',
|
|
1033
|
+
'Updates an existing newsletter in Ghost CMS. Can update name, description, sender settings, and display options.',
|
|
1034
|
+
{
|
|
1035
|
+
id: z.string().describe('The ID of the newsletter to update.'),
|
|
1036
|
+
name: z.string().optional().describe('New name for the newsletter.'),
|
|
1037
|
+
description: z.string().optional().describe('New description for the newsletter.'),
|
|
1038
|
+
sender_name: z.string().optional().describe('New sender name for newsletter emails.'),
|
|
1039
|
+
sender_email: z.string().email().optional().describe('New sender email address.'),
|
|
1040
|
+
subscribe_on_signup: z
|
|
1041
|
+
.boolean()
|
|
1042
|
+
.optional()
|
|
1043
|
+
.describe('Whether new members are automatically subscribed to this newsletter on signup.'),
|
|
1044
|
+
},
|
|
1045
|
+
async (input) => {
|
|
1046
|
+
console.error(`Executing tool: ghost_update_newsletter for newsletter ID: ${input.id}`);
|
|
1047
|
+
try {
|
|
1048
|
+
await loadServices();
|
|
1049
|
+
|
|
1050
|
+
const { id, ...updateData } = input;
|
|
1051
|
+
|
|
1052
|
+
const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
|
|
1053
|
+
const updatedNewsletter = await ghostServiceImproved.updateNewsletter(id, updateData);
|
|
1054
|
+
console.error(`Newsletter updated successfully. Newsletter ID: ${updatedNewsletter.id}`);
|
|
1055
|
+
|
|
1056
|
+
return {
|
|
1057
|
+
content: [{ type: 'text', text: JSON.stringify(updatedNewsletter, null, 2) }],
|
|
1058
|
+
};
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
console.error(`Error in ghost_update_newsletter:`, error);
|
|
1061
|
+
return {
|
|
1062
|
+
content: [{ type: 'text', text: `Error updating newsletter: ${error.message}` }],
|
|
1063
|
+
isError: true,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
// Delete Newsletter Tool
|
|
1070
|
+
server.tool(
|
|
1071
|
+
'ghost_delete_newsletter',
|
|
1072
|
+
'Deletes a newsletter from Ghost CMS by ID. This operation is permanent and cannot be undone.',
|
|
1073
|
+
{
|
|
1074
|
+
id: z.string().describe('The ID of the newsletter to delete.'),
|
|
1075
|
+
},
|
|
1076
|
+
async ({ id }) => {
|
|
1077
|
+
console.error(`Executing tool: ghost_delete_newsletter for newsletter ID: ${id}`);
|
|
1078
|
+
try {
|
|
1079
|
+
await loadServices();
|
|
1080
|
+
|
|
1081
|
+
const ghostServiceImproved = await import('./services/ghostServiceImproved.js');
|
|
1082
|
+
await ghostServiceImproved.deleteNewsletter(id);
|
|
1083
|
+
console.error(`Newsletter deleted successfully. Newsletter ID: ${id}`);
|
|
1084
|
+
|
|
1085
|
+
return {
|
|
1086
|
+
content: [{ type: 'text', text: `Newsletter ${id} has been successfully deleted.` }],
|
|
1087
|
+
};
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
console.error(`Error in ghost_delete_newsletter:`, error);
|
|
1090
|
+
return {
|
|
1091
|
+
content: [{ type: 'text', text: `Error deleting newsletter: ${error.message}` }],
|
|
1092
|
+
isError: true,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
|
|
783
1098
|
// --- Main Entry Point ---
|
|
784
1099
|
|
|
785
1100
|
async function main() {
|
|
@@ -790,9 +1105,10 @@ async function main() {
|
|
|
790
1105
|
|
|
791
1106
|
console.error('Ghost MCP Server running on stdio transport');
|
|
792
1107
|
console.error(
|
|
793
|
-
'Available tools: ghost_get_tags, ghost_create_tag, ghost_upload_image, ' +
|
|
1108
|
+
'Available tools: ghost_get_tags, ghost_create_tag, ghost_get_tag, ghost_update_tag, ghost_delete_tag, ghost_upload_image, ' +
|
|
794
1109
|
'ghost_create_post, ghost_get_posts, ghost_get_post, ghost_search_posts, ghost_update_post, ghost_delete_post, ' +
|
|
795
|
-
'ghost_get_pages, ghost_get_page, ghost_create_page, ghost_update_page, ghost_delete_page, ghost_search_pages'
|
|
1110
|
+
'ghost_get_pages, ghost_get_page, ghost_create_page, ghost_update_page, ghost_delete_page, ghost_search_pages, ' +
|
|
1111
|
+
'ghost_get_newsletters, ghost_get_newsletter, ghost_create_newsletter, ghost_update_newsletter, ghost_delete_newsletter'
|
|
796
1112
|
);
|
|
797
1113
|
}
|
|
798
1114
|
|