@jgardner04/ghost-mcp-server 1.12.2 → 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/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 -1301
- package/src/mcp_server_improved.js +0 -1720
|
@@ -1,81 +1,151 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
// Mock MCP SDK
|
|
7
|
-
vi.mock('@modelcontextprotocol/sdk/server/index.js', () => {
|
|
8
|
-
const mockServerInstance = {
|
|
9
|
-
addResource: vi.fn(),
|
|
10
|
-
addTool: vi.fn(),
|
|
11
|
-
listen: vi.fn().mockResolvedValue(undefined),
|
|
12
|
-
listResources: vi.fn().mockReturnValue([{ name: 'ghost/tag' }, { name: 'ghost/post' }]),
|
|
13
|
-
listTools: vi
|
|
14
|
-
.fn()
|
|
15
|
-
.mockReturnValue([
|
|
16
|
-
{ name: 'ghost_create_post' },
|
|
17
|
-
{ name: 'ghost_upload_image' },
|
|
18
|
-
{ name: 'ghost_get_tags' },
|
|
19
|
-
{ name: 'ghost_create_tag' },
|
|
20
|
-
]),
|
|
21
|
-
};
|
|
3
|
+
// Mock the McpServer to capture tool registrations
|
|
4
|
+
const mockTools = new Map();
|
|
22
5
|
|
|
6
|
+
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => {
|
|
23
7
|
return {
|
|
24
|
-
|
|
25
|
-
constructor() {
|
|
26
|
-
|
|
8
|
+
McpServer: class MockMcpServer {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
27
11
|
}
|
|
28
12
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
Resource: class MockResource {
|
|
34
|
-
constructor(config) {
|
|
35
|
-
this.name = config.name;
|
|
36
|
-
this.description = config.description;
|
|
37
|
-
this.schema = config.schema;
|
|
13
|
+
tool(name, description, schema, handler) {
|
|
14
|
+
mockTools.set(name, { name, description, schema, handler });
|
|
38
15
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
toolImplementations[config.name] = config.implementation;
|
|
43
|
-
this.name = config.name;
|
|
44
|
-
this.description = config.description;
|
|
45
|
-
this.inputSchema = config.inputSchema;
|
|
46
|
-
this.outputSchema = config.outputSchema;
|
|
47
|
-
this.implementation = config.implementation;
|
|
16
|
+
|
|
17
|
+
connect(_transport) {
|
|
18
|
+
return Promise.resolve();
|
|
48
19
|
}
|
|
49
20
|
},
|
|
50
21
|
};
|
|
51
22
|
});
|
|
52
23
|
|
|
24
|
+
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => {
|
|
25
|
+
return {
|
|
26
|
+
StdioServerTransport: class MockStdioServerTransport {},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
53
30
|
// Mock dotenv
|
|
54
31
|
vi.mock('dotenv', () => ({
|
|
55
32
|
default: { config: vi.fn() },
|
|
56
33
|
}));
|
|
57
34
|
|
|
58
|
-
// Mock
|
|
35
|
+
// Mock crypto
|
|
36
|
+
vi.mock('crypto', () => ({
|
|
37
|
+
default: { randomUUID: vi.fn().mockReturnValue('test-uuid-1234') },
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Mock services - will be lazy loaded
|
|
41
|
+
const mockGetPosts = vi.fn();
|
|
42
|
+
const mockGetPost = vi.fn();
|
|
43
|
+
const mockGetTags = vi.fn();
|
|
44
|
+
const mockGetTag = vi.fn();
|
|
45
|
+
const mockCreateTag = vi.fn();
|
|
46
|
+
const mockUpdateTag = vi.fn();
|
|
47
|
+
const mockDeleteTag = vi.fn();
|
|
48
|
+
const mockUploadImage = vi.fn();
|
|
59
49
|
const mockCreatePostService = vi.fn();
|
|
60
|
-
const mockUploadGhostImage = vi.fn();
|
|
61
|
-
const mockGetGhostTags = vi.fn();
|
|
62
|
-
const mockCreateGhostTag = vi.fn();
|
|
63
50
|
const mockProcessImage = vi.fn();
|
|
51
|
+
const mockValidateImageUrl = vi.fn();
|
|
52
|
+
const mockCreateSecureAxiosConfig = vi.fn();
|
|
53
|
+
const mockUpdatePost = vi.fn();
|
|
54
|
+
const mockDeletePost = vi.fn();
|
|
55
|
+
const mockSearchPosts = vi.fn();
|
|
56
|
+
|
|
57
|
+
// Page mocks
|
|
58
|
+
const mockGetPages = vi.fn();
|
|
59
|
+
const mockGetPage = vi.fn();
|
|
60
|
+
const mockCreatePageService = vi.fn();
|
|
61
|
+
const mockUpdatePage = vi.fn();
|
|
62
|
+
const mockDeletePage = vi.fn();
|
|
63
|
+
const mockSearchPages = vi.fn();
|
|
64
|
+
|
|
65
|
+
// Member mocks
|
|
66
|
+
const mockCreateMember = vi.fn();
|
|
67
|
+
const mockUpdateMember = vi.fn();
|
|
68
|
+
const mockDeleteMember = vi.fn();
|
|
69
|
+
const mockGetMembers = vi.fn();
|
|
70
|
+
const mockGetMember = vi.fn();
|
|
71
|
+
const mockSearchMembers = vi.fn();
|
|
72
|
+
|
|
73
|
+
// Newsletter mocks
|
|
74
|
+
const mockGetNewsletters = vi.fn();
|
|
75
|
+
const mockGetNewsletter = vi.fn();
|
|
76
|
+
const mockCreateNewsletterService = vi.fn();
|
|
77
|
+
const mockUpdateNewsletter = vi.fn();
|
|
78
|
+
const mockDeleteNewsletter = vi.fn();
|
|
79
|
+
|
|
80
|
+
// Tier mocks
|
|
81
|
+
const mockGetTiers = vi.fn();
|
|
82
|
+
const mockGetTier = vi.fn();
|
|
83
|
+
const mockCreateTier = vi.fn();
|
|
84
|
+
const mockUpdateTier = vi.fn();
|
|
85
|
+
const mockDeleteTier = vi.fn();
|
|
64
86
|
|
|
65
87
|
vi.mock('../services/postService.js', () => ({
|
|
66
88
|
createPostService: (...args) => mockCreatePostService(...args),
|
|
67
89
|
}));
|
|
68
90
|
|
|
69
|
-
vi.mock('../services/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
vi.mock('../services/pageService.js', () => ({
|
|
92
|
+
createPageService: (...args) => mockCreatePageService(...args),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
vi.mock('../services/newsletterService.js', () => ({
|
|
96
|
+
createNewsletterService: (...args) => mockCreateNewsletterService(...args),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
vi.mock('../services/ghostServiceImproved.js', () => ({
|
|
100
|
+
// Posts
|
|
101
|
+
getPosts: (...args) => mockGetPosts(...args),
|
|
102
|
+
getPost: (...args) => mockGetPost(...args),
|
|
103
|
+
updatePost: (...args) => mockUpdatePost(...args),
|
|
104
|
+
deletePost: (...args) => mockDeletePost(...args),
|
|
105
|
+
searchPosts: (...args) => mockSearchPosts(...args),
|
|
106
|
+
// Tags
|
|
107
|
+
getTags: (...args) => mockGetTags(...args),
|
|
108
|
+
getTag: (...args) => mockGetTag(...args),
|
|
109
|
+
createTag: (...args) => mockCreateTag(...args),
|
|
110
|
+
updateTag: (...args) => mockUpdateTag(...args),
|
|
111
|
+
deleteTag: (...args) => mockDeleteTag(...args),
|
|
112
|
+
// Images
|
|
113
|
+
uploadImage: (...args) => mockUploadImage(...args),
|
|
114
|
+
// Pages
|
|
115
|
+
getPages: (...args) => mockGetPages(...args),
|
|
116
|
+
getPage: (...args) => mockGetPage(...args),
|
|
117
|
+
updatePage: (...args) => mockUpdatePage(...args),
|
|
118
|
+
deletePage: (...args) => mockDeletePage(...args),
|
|
119
|
+
searchPages: (...args) => mockSearchPages(...args),
|
|
120
|
+
// Members
|
|
121
|
+
createMember: (...args) => mockCreateMember(...args),
|
|
122
|
+
updateMember: (...args) => mockUpdateMember(...args),
|
|
123
|
+
deleteMember: (...args) => mockDeleteMember(...args),
|
|
124
|
+
getMembers: (...args) => mockGetMembers(...args),
|
|
125
|
+
getMember: (...args) => mockGetMember(...args),
|
|
126
|
+
searchMembers: (...args) => mockSearchMembers(...args),
|
|
127
|
+
// Newsletters
|
|
128
|
+
getNewsletters: (...args) => mockGetNewsletters(...args),
|
|
129
|
+
getNewsletter: (...args) => mockGetNewsletter(...args),
|
|
130
|
+
updateNewsletter: (...args) => mockUpdateNewsletter(...args),
|
|
131
|
+
deleteNewsletter: (...args) => mockDeleteNewsletter(...args),
|
|
132
|
+
// Tiers
|
|
133
|
+
getTiers: (...args) => mockGetTiers(...args),
|
|
134
|
+
getTier: (...args) => mockGetTier(...args),
|
|
135
|
+
createTier: (...args) => mockCreateTier(...args),
|
|
136
|
+
updateTier: (...args) => mockUpdateTier(...args),
|
|
137
|
+
deleteTier: (...args) => mockDeleteTier(...args),
|
|
73
138
|
}));
|
|
74
139
|
|
|
75
140
|
vi.mock('../services/imageProcessingService.js', () => ({
|
|
76
141
|
processImage: (...args) => mockProcessImage(...args),
|
|
77
142
|
}));
|
|
78
143
|
|
|
144
|
+
vi.mock('../utils/urlValidator.js', () => ({
|
|
145
|
+
validateImageUrl: (...args) => mockValidateImageUrl(...args),
|
|
146
|
+
createSecureAxiosConfig: (...args) => mockCreateSecureAxiosConfig(...args),
|
|
147
|
+
}));
|
|
148
|
+
|
|
79
149
|
// Mock axios
|
|
80
150
|
const mockAxios = vi.fn();
|
|
81
151
|
vi.mock('axios', () => ({
|
|
@@ -103,285 +173,1129 @@ vi.mock('os', () => ({
|
|
|
103
173
|
default: { tmpdir: vi.fn().mockReturnValue('/tmp') },
|
|
104
174
|
}));
|
|
105
175
|
|
|
106
|
-
// Mock
|
|
107
|
-
vi.mock('
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
vi.mock('../utils/urlValidator.js', () => ({
|
|
115
|
-
validateImageUrl: (...args) => mockValidateImageUrl(...args),
|
|
116
|
-
createSecureAxiosConfig: (...args) => mockCreateSecureAxiosConfig(...args),
|
|
117
|
-
}));
|
|
118
|
-
|
|
119
|
-
// Mock logger
|
|
120
|
-
vi.mock('../utils/logger.js', () => ({
|
|
121
|
-
createContextLogger: vi.fn().mockReturnValue({
|
|
122
|
-
info: vi.fn(),
|
|
123
|
-
error: vi.fn(),
|
|
124
|
-
warn: vi.fn(),
|
|
125
|
-
debug: vi.fn(),
|
|
126
|
-
toolExecution: vi.fn(),
|
|
127
|
-
toolSuccess: vi.fn(),
|
|
128
|
-
toolError: vi.fn(),
|
|
129
|
-
fileOperation: vi.fn(),
|
|
130
|
-
}),
|
|
131
|
-
}));
|
|
132
|
-
|
|
133
|
-
describe('mcp_server', () => {
|
|
134
|
-
let mcpServerModule;
|
|
176
|
+
// Mock path
|
|
177
|
+
vi.mock('path', async () => {
|
|
178
|
+
const actual = await vi.importActual('path');
|
|
179
|
+
return {
|
|
180
|
+
default: actual,
|
|
181
|
+
...actual,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
135
184
|
|
|
185
|
+
describe('mcp_server - ghost_get_posts tool', () => {
|
|
136
186
|
beforeEach(async () => {
|
|
137
187
|
vi.clearAllMocks();
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
188
|
+
// Don't clear mockTools - they're registered once on module load
|
|
189
|
+
// Import the module to register tools (only first time)
|
|
190
|
+
if (mockTools.size === 0) {
|
|
191
|
+
await import('../mcp_server.js');
|
|
141
192
|
}
|
|
142
193
|
});
|
|
143
194
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
195
|
+
it('should register ghost_get_posts tool', () => {
|
|
196
|
+
expect(mockTools.has('ghost_get_posts')).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should have correct schema with all optional parameters', () => {
|
|
200
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
201
|
+
expect(tool).toBeDefined();
|
|
202
|
+
expect(tool.description).toContain('posts');
|
|
203
|
+
expect(tool.schema).toBeDefined();
|
|
204
|
+
// Zod schemas store field definitions in schema.shape
|
|
205
|
+
expect(tool.schema.shape.limit).toBeDefined();
|
|
206
|
+
expect(tool.schema.shape.page).toBeDefined();
|
|
207
|
+
expect(tool.schema.shape.status).toBeDefined();
|
|
208
|
+
expect(tool.schema.shape.include).toBeDefined();
|
|
209
|
+
expect(tool.schema.shape.filter).toBeDefined();
|
|
210
|
+
expect(tool.schema.shape.order).toBeDefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should retrieve posts with default options', async () => {
|
|
214
|
+
const mockPosts = [
|
|
215
|
+
{ id: '1', title: 'Post 1', slug: 'post-1', status: 'published' },
|
|
216
|
+
{ id: '2', title: 'Post 2', slug: 'post-2', status: 'draft' },
|
|
217
|
+
];
|
|
218
|
+
mockGetPosts.mockResolvedValue(mockPosts);
|
|
219
|
+
|
|
220
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
221
|
+
const result = await tool.handler({});
|
|
222
|
+
|
|
223
|
+
expect(mockGetPosts).toHaveBeenCalledWith({});
|
|
224
|
+
expect(result.content[0].text).toContain('Post 1');
|
|
225
|
+
expect(result.content[0].text).toContain('Post 2');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should pass limit and page parameters', async () => {
|
|
229
|
+
const mockPosts = [{ id: '1', title: 'Post 1', slug: 'post-1' }];
|
|
230
|
+
mockGetPosts.mockResolvedValue(mockPosts);
|
|
231
|
+
|
|
232
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
233
|
+
await tool.handler({ limit: 10, page: 2 });
|
|
234
|
+
|
|
235
|
+
expect(mockGetPosts).toHaveBeenCalledWith({ limit: 10, page: 2 });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should validate limit is between 1 and 100', () => {
|
|
239
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
240
|
+
// Zod schemas store field definitions in schema.shape
|
|
241
|
+
const shape = tool.schema.shape;
|
|
242
|
+
|
|
243
|
+
// Test that limit schema exists and has proper validation
|
|
244
|
+
expect(shape.limit).toBeDefined();
|
|
245
|
+
expect(() => shape.limit.parse(0)).toThrow();
|
|
246
|
+
expect(() => shape.limit.parse(101)).toThrow();
|
|
247
|
+
expect(shape.limit.parse(50)).toBe(50);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should validate page is at least 1', () => {
|
|
251
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
252
|
+
// Zod schemas store field definitions in schema.shape
|
|
253
|
+
const shape = tool.schema.shape;
|
|
254
|
+
|
|
255
|
+
expect(shape.page).toBeDefined();
|
|
256
|
+
expect(() => shape.page.parse(0)).toThrow();
|
|
257
|
+
expect(shape.page.parse(1)).toBe(1);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should pass status filter', async () => {
|
|
261
|
+
const mockPosts = [{ id: '1', title: 'Published Post', status: 'published' }];
|
|
262
|
+
mockGetPosts.mockResolvedValue(mockPosts);
|
|
263
|
+
|
|
264
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
265
|
+
await tool.handler({ status: 'published' });
|
|
266
|
+
|
|
267
|
+
expect(mockGetPosts).toHaveBeenCalledWith({ status: 'published' });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should validate status enum values', () => {
|
|
271
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
272
|
+
// Zod schemas store field definitions in schema.shape
|
|
273
|
+
const shape = tool.schema.shape;
|
|
274
|
+
|
|
275
|
+
expect(shape.status).toBeDefined();
|
|
276
|
+
expect(() => shape.status.parse('invalid')).toThrow();
|
|
277
|
+
expect(shape.status.parse('published')).toBe('published');
|
|
278
|
+
expect(shape.status.parse('draft')).toBe('draft');
|
|
279
|
+
expect(shape.status.parse('scheduled')).toBe('scheduled');
|
|
280
|
+
expect(shape.status.parse('all')).toBe('all');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should pass include parameter', async () => {
|
|
284
|
+
const mockPosts = [
|
|
285
|
+
{
|
|
147
286
|
id: '1',
|
|
148
|
-
title: '
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
287
|
+
title: 'Post with tags',
|
|
288
|
+
tags: [{ name: 'tech' }],
|
|
289
|
+
authors: [{ name: 'John' }],
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
mockGetPosts.mockResolvedValue(mockPosts);
|
|
153
293
|
|
|
154
|
-
|
|
155
|
-
|
|
294
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
295
|
+
await tool.handler({ include: 'tags,authors' });
|
|
156
296
|
|
|
157
|
-
|
|
158
|
-
|
|
297
|
+
expect(mockGetPosts).toHaveBeenCalledWith({ include: 'tags,authors' });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should pass filter parameter (NQL)', async () => {
|
|
301
|
+
const mockPosts = [{ id: '1', title: 'Featured Post', featured: true }];
|
|
302
|
+
mockGetPosts.mockResolvedValue(mockPosts);
|
|
303
|
+
|
|
304
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
305
|
+
await tool.handler({ filter: 'featured:true' });
|
|
306
|
+
|
|
307
|
+
expect(mockGetPosts).toHaveBeenCalledWith({ filter: 'featured:true' });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should pass order parameter', async () => {
|
|
311
|
+
const mockPosts = [
|
|
312
|
+
{ id: '1', title: 'Newest', published_at: '2025-12-10' },
|
|
313
|
+
{ id: '2', title: 'Older', published_at: '2025-12-01' },
|
|
314
|
+
];
|
|
315
|
+
mockGetPosts.mockResolvedValue(mockPosts);
|
|
316
|
+
|
|
317
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
318
|
+
await tool.handler({ order: 'published_at DESC' });
|
|
319
|
+
|
|
320
|
+
expect(mockGetPosts).toHaveBeenCalledWith({ order: 'published_at DESC' });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should pass all parameters combined', async () => {
|
|
324
|
+
const mockPosts = [{ id: '1', title: 'Test Post' }];
|
|
325
|
+
mockGetPosts.mockResolvedValue(mockPosts);
|
|
326
|
+
|
|
327
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
328
|
+
await tool.handler({
|
|
329
|
+
limit: 20,
|
|
330
|
+
page: 1,
|
|
331
|
+
status: 'published',
|
|
332
|
+
include: 'tags,authors',
|
|
333
|
+
filter: 'featured:true',
|
|
334
|
+
order: 'published_at DESC',
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
expect(mockGetPosts).toHaveBeenCalledWith({
|
|
338
|
+
limit: 20,
|
|
339
|
+
page: 1,
|
|
340
|
+
status: 'published',
|
|
341
|
+
include: 'tags,authors',
|
|
342
|
+
filter: 'featured:true',
|
|
343
|
+
order: 'published_at DESC',
|
|
159
344
|
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should handle errors from ghostService', async () => {
|
|
348
|
+
mockGetPosts.mockRejectedValue(new Error('Ghost API error'));
|
|
160
349
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
mockCreatePostService.mockResolvedValue(createdPost);
|
|
350
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
351
|
+
const result = await tool.handler({});
|
|
164
352
|
|
|
165
|
-
|
|
166
|
-
|
|
353
|
+
expect(result.isError).toBe(true);
|
|
354
|
+
expect(result.content[0].text).toContain('Ghost API error');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should return formatted JSON response', async () => {
|
|
358
|
+
const mockPosts = [
|
|
359
|
+
{
|
|
360
|
+
id: '1',
|
|
361
|
+
title: 'Test Post',
|
|
362
|
+
slug: 'test-post',
|
|
167
363
|
html: '<p>Content</p>',
|
|
168
|
-
tags: ['tech'],
|
|
169
364
|
status: 'published',
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
365
|
+
},
|
|
366
|
+
];
|
|
367
|
+
mockGetPosts.mockResolvedValue(mockPosts);
|
|
173
368
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
});
|
|
369
|
+
const tool = mockTools.get('ghost_get_posts');
|
|
370
|
+
const result = await tool.handler({});
|
|
177
371
|
|
|
178
|
-
|
|
179
|
-
|
|
372
|
+
expect(result.content).toBeDefined();
|
|
373
|
+
expect(result.content[0].type).toBe('text');
|
|
374
|
+
expect(result.content[0].text).toContain('"id": "1"');
|
|
375
|
+
expect(result.content[0].text).toContain('"title": "Test Post"');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
180
378
|
|
|
181
|
-
|
|
379
|
+
describe('mcp_server - ghost_get_post tool', () => {
|
|
380
|
+
beforeEach(async () => {
|
|
381
|
+
vi.clearAllMocks();
|
|
382
|
+
// Don't clear mockTools - they're registered once on module load
|
|
383
|
+
if (mockTools.size === 0) {
|
|
384
|
+
await import('../mcp_server.js');
|
|
385
|
+
}
|
|
386
|
+
});
|
|
182
387
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
);
|
|
186
|
-
});
|
|
388
|
+
it('should register ghost_get_post tool', () => {
|
|
389
|
+
expect(mockTools.has('ghost_get_post')).toBe(true);
|
|
187
390
|
});
|
|
188
391
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const mockWriter = {
|
|
201
|
-
on: vi.fn((event, cb) => {
|
|
202
|
-
if (event === 'finish') setTimeout(cb, 0);
|
|
203
|
-
return mockWriter;
|
|
204
|
-
}),
|
|
205
|
-
};
|
|
206
|
-
mockCreateWriteStream.mockReturnValue(mockWriter);
|
|
207
|
-
|
|
208
|
-
const mockStream = { pipe: vi.fn().mockReturnValue(mockWriter) };
|
|
209
|
-
mockAxios.mockResolvedValue({ data: mockStream });
|
|
210
|
-
|
|
211
|
-
mockProcessImage.mockResolvedValue('/tmp/processed-image.jpg');
|
|
212
|
-
mockUploadGhostImage.mockResolvedValue({
|
|
213
|
-
url: 'https://ghost.com/content/images/image.jpg',
|
|
214
|
-
});
|
|
215
|
-
});
|
|
392
|
+
it('should have correct schema requiring one of id or slug', () => {
|
|
393
|
+
const tool = mockTools.get('ghost_get_post');
|
|
394
|
+
expect(tool).toBeDefined();
|
|
395
|
+
expect(tool.description).toContain('post');
|
|
396
|
+
expect(tool.schema).toBeDefined();
|
|
397
|
+
// ghost_get_post uses a refined schema, access via _def.schema.shape
|
|
398
|
+
const shape = tool.schema._def.schema.shape;
|
|
399
|
+
expect(shape.id).toBeDefined();
|
|
400
|
+
expect(shape.slug).toBeDefined();
|
|
401
|
+
expect(shape.include).toBeDefined();
|
|
402
|
+
});
|
|
216
403
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
404
|
+
it('should retrieve post by ID', async () => {
|
|
405
|
+
const mockPost = {
|
|
406
|
+
id: '507f1f77bcf86cd799439011',
|
|
407
|
+
title: 'Test Post',
|
|
408
|
+
slug: 'test-post',
|
|
409
|
+
html: '<p>Content</p>',
|
|
410
|
+
status: 'published',
|
|
411
|
+
};
|
|
412
|
+
mockGetPost.mockResolvedValue(mockPost);
|
|
413
|
+
|
|
414
|
+
const tool = mockTools.get('ghost_get_post');
|
|
415
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
|
|
416
|
+
|
|
417
|
+
expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {});
|
|
418
|
+
expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"');
|
|
419
|
+
expect(result.content[0].text).toContain('"title": "Test Post"');
|
|
420
|
+
});
|
|
222
421
|
|
|
223
|
-
|
|
422
|
+
it('should retrieve post by slug', async () => {
|
|
423
|
+
const mockPost = {
|
|
424
|
+
id: '507f1f77bcf86cd799439011',
|
|
425
|
+
title: 'Test Post',
|
|
426
|
+
slug: 'test-post',
|
|
427
|
+
html: '<p>Content</p>',
|
|
428
|
+
status: 'published',
|
|
429
|
+
};
|
|
430
|
+
mockGetPost.mockResolvedValue(mockPost);
|
|
431
|
+
|
|
432
|
+
const tool = mockTools.get('ghost_get_post');
|
|
433
|
+
const result = await tool.handler({ slug: 'test-post' });
|
|
434
|
+
|
|
435
|
+
expect(mockGetPost).toHaveBeenCalledWith('slug/test-post', {});
|
|
436
|
+
expect(result.content[0].text).toContain('"title": "Test Post"');
|
|
437
|
+
});
|
|
224
438
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
439
|
+
it('should pass include parameter with ID', async () => {
|
|
440
|
+
const mockPost = {
|
|
441
|
+
id: '507f1f77bcf86cd799439011',
|
|
442
|
+
title: 'Post with relations',
|
|
443
|
+
tags: [{ name: 'tech' }],
|
|
444
|
+
authors: [{ name: 'John' }],
|
|
445
|
+
};
|
|
446
|
+
mockGetPost.mockResolvedValue(mockPost);
|
|
229
447
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const result = await toolImplementations.ghost_upload_image(input);
|
|
448
|
+
const tool = mockTools.get('ghost_get_post');
|
|
449
|
+
await tool.handler({ id: '507f1f77bcf86cd799439011', include: 'tags,authors' });
|
|
233
450
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
expect(mockProcessImage).toHaveBeenCalled();
|
|
237
|
-
expect(mockUploadGhostImage).toHaveBeenCalled();
|
|
238
|
-
expect(result.url).toBe('https://ghost.com/content/images/image.jpg');
|
|
451
|
+
expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
452
|
+
include: 'tags,authors',
|
|
239
453
|
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should pass include parameter with slug', async () => {
|
|
457
|
+
const mockPost = {
|
|
458
|
+
id: '507f1f77bcf86cd799439011',
|
|
459
|
+
title: 'Post with relations',
|
|
460
|
+
slug: 'test-post',
|
|
461
|
+
tags: [{ name: 'tech' }],
|
|
462
|
+
};
|
|
463
|
+
mockGetPost.mockResolvedValue(mockPost);
|
|
464
|
+
|
|
465
|
+
const tool = mockTools.get('ghost_get_post');
|
|
466
|
+
await tool.handler({ slug: 'test-post', include: 'tags' });
|
|
467
|
+
|
|
468
|
+
expect(mockGetPost).toHaveBeenCalledWith('slug/test-post', { include: 'tags' });
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should prefer ID over slug when both provided', async () => {
|
|
472
|
+
const mockPost = { id: '507f1f77bcf86cd799439011', title: 'Test Post', slug: 'test-post' };
|
|
473
|
+
mockGetPost.mockResolvedValue(mockPost);
|
|
240
474
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const result = await toolImplementations.ghost_upload_image(input);
|
|
475
|
+
const tool = mockTools.get('ghost_get_post');
|
|
476
|
+
await tool.handler({ id: '507f1f77bcf86cd799439011', slug: 'wrong-slug' });
|
|
244
477
|
|
|
245
|
-
|
|
478
|
+
expect(mockGetPost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should handle not found errors', async () => {
|
|
482
|
+
mockGetPost.mockRejectedValue(new Error('Post not found'));
|
|
483
|
+
|
|
484
|
+
const tool = mockTools.get('ghost_get_post');
|
|
485
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439099' });
|
|
486
|
+
|
|
487
|
+
expect(result.isError).toBe(true);
|
|
488
|
+
expect(result.content[0].text).toContain('Post not found');
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should handle errors from ghostService', async () => {
|
|
492
|
+
mockGetPost.mockRejectedValue(new Error('Ghost API error'));
|
|
493
|
+
|
|
494
|
+
const tool = mockTools.get('ghost_get_post');
|
|
495
|
+
const result = await tool.handler({ slug: 'test' });
|
|
496
|
+
|
|
497
|
+
expect(result.isError).toBe(true);
|
|
498
|
+
expect(result.content[0].text).toContain('Ghost API error');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('should return formatted JSON response', async () => {
|
|
502
|
+
const mockPost = {
|
|
503
|
+
id: '507f1f77bcf86cd799439011',
|
|
504
|
+
uuid: 'uuid-123',
|
|
505
|
+
title: 'Test Post',
|
|
506
|
+
slug: 'test-post',
|
|
507
|
+
html: '<p>Content</p>',
|
|
508
|
+
status: 'published',
|
|
509
|
+
created_at: '2025-12-10T00:00:00.000Z',
|
|
510
|
+
updated_at: '2025-12-10T00:00:00.000Z',
|
|
511
|
+
};
|
|
512
|
+
mockGetPost.mockResolvedValue(mockPost);
|
|
513
|
+
|
|
514
|
+
const tool = mockTools.get('ghost_get_post');
|
|
515
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
|
|
516
|
+
|
|
517
|
+
expect(result.content).toBeDefined();
|
|
518
|
+
expect(result.content[0].type).toBe('text');
|
|
519
|
+
expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"');
|
|
520
|
+
expect(result.content[0].text).toContain('"title": "Test Post"');
|
|
521
|
+
expect(result.content[0].text).toContain('"status": "published"');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('should handle validation error when neither id nor slug provided', async () => {
|
|
525
|
+
const tool = mockTools.get('ghost_get_post');
|
|
526
|
+
const result = await tool.handler({});
|
|
527
|
+
|
|
528
|
+
expect(result.isError).toBe(true);
|
|
529
|
+
expect(result.content[0].text).toContain('Either id or slug is required');
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe('mcp_server - ghost_update_post tool', () => {
|
|
534
|
+
beforeEach(async () => {
|
|
535
|
+
vi.clearAllMocks();
|
|
536
|
+
// Don't clear mockTools - they're registered once on module load
|
|
537
|
+
if (mockTools.size === 0) {
|
|
538
|
+
await import('../mcp_server.js');
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should register ghost_update_post tool', () => {
|
|
543
|
+
expect(mockTools.has('ghost_update_post')).toBe(true);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should have correct schema with required id field', () => {
|
|
547
|
+
const tool = mockTools.get('ghost_update_post');
|
|
548
|
+
expect(tool).toBeDefined();
|
|
549
|
+
expect(tool.description).toContain('Updates an existing post');
|
|
550
|
+
expect(tool.schema).toBeDefined();
|
|
551
|
+
// Zod schemas store field definitions in schema.shape
|
|
552
|
+
expect(tool.schema.shape.id).toBeDefined();
|
|
553
|
+
expect(tool.schema.shape.title).toBeDefined();
|
|
554
|
+
expect(tool.schema.shape.html).toBeDefined();
|
|
555
|
+
expect(tool.schema.shape.status).toBeDefined();
|
|
556
|
+
expect(tool.schema.shape.tags).toBeDefined();
|
|
557
|
+
expect(tool.schema.shape.feature_image).toBeDefined();
|
|
558
|
+
expect(tool.schema.shape.feature_image_alt).toBeDefined();
|
|
559
|
+
expect(tool.schema.shape.feature_image_caption).toBeDefined();
|
|
560
|
+
expect(tool.schema.shape.meta_title).toBeDefined();
|
|
561
|
+
expect(tool.schema.shape.meta_description).toBeDefined();
|
|
562
|
+
expect(tool.schema.shape.published_at).toBeDefined();
|
|
563
|
+
expect(tool.schema.shape.custom_excerpt).toBeDefined();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should update post title', async () => {
|
|
567
|
+
const mockUpdatedPost = {
|
|
568
|
+
id: '507f1f77bcf86cd799439011',
|
|
569
|
+
title: 'Updated Title',
|
|
570
|
+
slug: 'test-post',
|
|
571
|
+
html: '<p>Content</p>',
|
|
572
|
+
status: 'published',
|
|
573
|
+
updated_at: '2025-12-10T12:00:00.000Z',
|
|
574
|
+
};
|
|
575
|
+
mockUpdatePost.mockResolvedValue(mockUpdatedPost);
|
|
576
|
+
|
|
577
|
+
const tool = mockTools.get('ghost_update_post');
|
|
578
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated Title' });
|
|
579
|
+
|
|
580
|
+
expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
581
|
+
title: 'Updated Title',
|
|
246
582
|
});
|
|
583
|
+
expect(result.content[0].text).toContain('"title": "Updated Title"');
|
|
584
|
+
});
|
|
247
585
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
586
|
+
it('should update post content', async () => {
|
|
587
|
+
const mockUpdatedPost = {
|
|
588
|
+
id: '507f1f77bcf86cd799439011',
|
|
589
|
+
title: 'Test Post',
|
|
590
|
+
html: '<p>Updated content</p>',
|
|
591
|
+
status: 'published',
|
|
592
|
+
updated_at: '2025-12-10T12:00:00.000Z',
|
|
593
|
+
};
|
|
594
|
+
mockUpdatePost.mockResolvedValue(mockUpdatedPost);
|
|
595
|
+
|
|
596
|
+
const tool = mockTools.get('ghost_update_post');
|
|
597
|
+
const result = await tool.handler({
|
|
598
|
+
id: '507f1f77bcf86cd799439011',
|
|
599
|
+
html: '<p>Updated content</p>',
|
|
600
|
+
});
|
|
251
601
|
|
|
252
|
-
|
|
253
|
-
|
|
602
|
+
expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
603
|
+
html: '<p>Updated content</p>',
|
|
254
604
|
});
|
|
605
|
+
expect(result.content[0].text).toContain('Updated content');
|
|
606
|
+
});
|
|
255
607
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
608
|
+
it('should update post status', async () => {
|
|
609
|
+
const mockUpdatedPost = {
|
|
610
|
+
id: '507f1f77bcf86cd799439011',
|
|
611
|
+
title: 'Test Post',
|
|
612
|
+
html: '<p>Content</p>',
|
|
613
|
+
status: 'published',
|
|
614
|
+
updated_at: '2025-12-10T12:00:00.000Z',
|
|
615
|
+
};
|
|
616
|
+
mockUpdatePost.mockResolvedValue(mockUpdatedPost);
|
|
617
|
+
|
|
618
|
+
const tool = mockTools.get('ghost_update_post');
|
|
619
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011', status: 'published' });
|
|
620
|
+
|
|
621
|
+
expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
622
|
+
status: 'published',
|
|
623
|
+
});
|
|
624
|
+
expect(result.content[0].text).toContain('"status": "published"');
|
|
625
|
+
});
|
|
259
626
|
|
|
260
|
-
|
|
627
|
+
it('should update post tags', async () => {
|
|
628
|
+
const mockUpdatedPost = {
|
|
629
|
+
id: '507f1f77bcf86cd799439011',
|
|
630
|
+
title: 'Test Post',
|
|
631
|
+
html: '<p>Content</p>',
|
|
632
|
+
tags: [{ name: 'tech' }, { name: 'javascript' }],
|
|
633
|
+
updated_at: '2025-12-10T12:00:00.000Z',
|
|
634
|
+
};
|
|
635
|
+
mockUpdatePost.mockResolvedValue(mockUpdatedPost);
|
|
636
|
+
|
|
637
|
+
const tool = mockTools.get('ghost_update_post');
|
|
638
|
+
const result = await tool.handler({
|
|
639
|
+
id: '507f1f77bcf86cd799439011',
|
|
640
|
+
tags: ['tech', 'javascript'],
|
|
261
641
|
});
|
|
262
642
|
|
|
263
|
-
|
|
264
|
-
|
|
643
|
+
expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
644
|
+
tags: ['tech', 'javascript'],
|
|
645
|
+
});
|
|
646
|
+
expect(result.content[0].text).toContain('tech');
|
|
647
|
+
expect(result.content[0].text).toContain('javascript');
|
|
648
|
+
});
|
|
265
649
|
|
|
266
|
-
|
|
267
|
-
|
|
650
|
+
it('should update post featured image', async () => {
|
|
651
|
+
const mockUpdatedPost = {
|
|
652
|
+
id: '507f1f77bcf86cd799439011',
|
|
653
|
+
title: 'Test Post',
|
|
654
|
+
feature_image: 'https://example.com/new-image.jpg',
|
|
655
|
+
feature_image_alt: 'New image',
|
|
656
|
+
updated_at: '2025-12-10T12:00:00.000Z',
|
|
657
|
+
};
|
|
658
|
+
mockUpdatePost.mockResolvedValue(mockUpdatedPost);
|
|
659
|
+
|
|
660
|
+
const tool = mockTools.get('ghost_update_post');
|
|
661
|
+
const result = await tool.handler({
|
|
662
|
+
id: '507f1f77bcf86cd799439011',
|
|
663
|
+
feature_image: 'https://example.com/new-image.jpg',
|
|
664
|
+
feature_image_alt: 'New image',
|
|
665
|
+
});
|
|
268
666
|
|
|
269
|
-
|
|
667
|
+
expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
668
|
+
feature_image: 'https://example.com/new-image.jpg',
|
|
669
|
+
feature_image_alt: 'New image',
|
|
270
670
|
});
|
|
671
|
+
expect(result.content[0].text).toContain('new-image.jpg');
|
|
672
|
+
});
|
|
271
673
|
|
|
272
|
-
|
|
273
|
-
|
|
674
|
+
it('should update SEO meta fields', async () => {
|
|
675
|
+
const mockUpdatedPost = {
|
|
676
|
+
id: '507f1f77bcf86cd799439011',
|
|
677
|
+
title: 'Test Post',
|
|
678
|
+
meta_title: 'SEO Title',
|
|
679
|
+
meta_description: 'SEO Description',
|
|
680
|
+
updated_at: '2025-12-10T12:00:00.000Z',
|
|
681
|
+
};
|
|
682
|
+
mockUpdatePost.mockResolvedValue(mockUpdatedPost);
|
|
683
|
+
|
|
684
|
+
const tool = mockTools.get('ghost_update_post');
|
|
685
|
+
const result = await tool.handler({
|
|
686
|
+
id: '507f1f77bcf86cd799439011',
|
|
687
|
+
meta_title: 'SEO Title',
|
|
688
|
+
meta_description: 'SEO Description',
|
|
689
|
+
});
|
|
274
690
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
);
|
|
691
|
+
expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
692
|
+
meta_title: 'SEO Title',
|
|
693
|
+
meta_description: 'SEO Description',
|
|
279
694
|
});
|
|
695
|
+
expect(result.content[0].text).toContain('SEO Title');
|
|
696
|
+
expect(result.content[0].text).toContain('SEO Description');
|
|
697
|
+
});
|
|
280
698
|
|
|
281
|
-
|
|
282
|
-
|
|
699
|
+
it('should update multiple fields at once', async () => {
|
|
700
|
+
const mockUpdatedPost = {
|
|
701
|
+
id: '507f1f77bcf86cd799439011',
|
|
702
|
+
title: 'Updated Title',
|
|
703
|
+
html: '<p>Updated content</p>',
|
|
704
|
+
status: 'published',
|
|
705
|
+
tags: [{ name: 'tech' }],
|
|
706
|
+
updated_at: '2025-12-10T12:00:00.000Z',
|
|
707
|
+
};
|
|
708
|
+
mockUpdatePost.mockResolvedValue(mockUpdatedPost);
|
|
709
|
+
|
|
710
|
+
const tool = mockTools.get('ghost_update_post');
|
|
711
|
+
const result = await tool.handler({
|
|
712
|
+
id: '507f1f77bcf86cd799439011',
|
|
713
|
+
title: 'Updated Title',
|
|
714
|
+
html: '<p>Updated content</p>',
|
|
715
|
+
status: 'published',
|
|
716
|
+
tags: ['tech'],
|
|
717
|
+
});
|
|
283
718
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
719
|
+
expect(mockUpdatePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
720
|
+
title: 'Updated Title',
|
|
721
|
+
html: '<p>Updated content</p>',
|
|
722
|
+
status: 'published',
|
|
723
|
+
tags: ['tech'],
|
|
288
724
|
});
|
|
725
|
+
expect(result.content[0].text).toContain('Updated Title');
|
|
289
726
|
});
|
|
290
727
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const tags = [
|
|
294
|
-
{ id: '1', name: 'Tag1', slug: 'tag1' },
|
|
295
|
-
{ id: '2', name: 'Tag2', slug: 'tag2' },
|
|
296
|
-
];
|
|
297
|
-
mockGetGhostTags.mockResolvedValue(tags);
|
|
728
|
+
it('should handle not found errors', async () => {
|
|
729
|
+
mockUpdatePost.mockRejectedValue(new Error('Post not found'));
|
|
298
730
|
|
|
299
|
-
|
|
731
|
+
const tool = mockTools.get('ghost_update_post');
|
|
732
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439099', title: 'New Title' });
|
|
300
733
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
734
|
+
expect(result.isError).toBe(true);
|
|
735
|
+
expect(result.content[0].text).toContain('Post not found');
|
|
736
|
+
});
|
|
304
737
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
mockGetGhostTags.mockResolvedValue(tags);
|
|
738
|
+
it('should handle validation errors', async () => {
|
|
739
|
+
mockUpdatePost.mockRejectedValue(new Error('Validation failed: Title is required'));
|
|
308
740
|
|
|
309
|
-
|
|
741
|
+
const tool = mockTools.get('ghost_update_post');
|
|
742
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: '' });
|
|
310
743
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
744
|
+
expect(result.isError).toBe(true);
|
|
745
|
+
expect(result.content[0].text).toContain('Validation failed');
|
|
746
|
+
});
|
|
314
747
|
|
|
315
|
-
|
|
316
|
-
|
|
748
|
+
it('should handle Ghost API errors', async () => {
|
|
749
|
+
mockUpdatePost.mockRejectedValue(new Error('Ghost API error: Server timeout'));
|
|
317
750
|
|
|
318
|
-
|
|
751
|
+
const tool = mockTools.get('ghost_update_post');
|
|
752
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated' });
|
|
319
753
|
|
|
320
|
-
|
|
321
|
-
|
|
754
|
+
expect(result.isError).toBe(true);
|
|
755
|
+
expect(result.content[0].text).toContain('Ghost API error');
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('should return formatted JSON response', async () => {
|
|
759
|
+
const mockUpdatedPost = {
|
|
760
|
+
id: '507f1f77bcf86cd799439011',
|
|
761
|
+
uuid: 'uuid-123',
|
|
762
|
+
title: 'Updated Post',
|
|
763
|
+
slug: 'updated-post',
|
|
764
|
+
html: '<p>Updated content</p>',
|
|
765
|
+
status: 'published',
|
|
766
|
+
created_at: '2025-12-09T00:00:00.000Z',
|
|
767
|
+
updated_at: '2025-12-10T12:00:00.000Z',
|
|
768
|
+
};
|
|
769
|
+
mockUpdatePost.mockResolvedValue(mockUpdatedPost);
|
|
770
|
+
|
|
771
|
+
const tool = mockTools.get('ghost_update_post');
|
|
772
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011', title: 'Updated Post' });
|
|
773
|
+
|
|
774
|
+
expect(result.content).toBeDefined();
|
|
775
|
+
expect(result.content[0].type).toBe('text');
|
|
776
|
+
expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"');
|
|
777
|
+
expect(result.content[0].text).toContain('"title": "Updated Post"');
|
|
778
|
+
expect(result.content[0].text).toContain('"status": "published"');
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
describe('mcp_server - ghost_delete_post tool', () => {
|
|
783
|
+
beforeEach(async () => {
|
|
784
|
+
vi.clearAllMocks();
|
|
785
|
+
// Don't clear mockTools - they're registered once on module load
|
|
786
|
+
if (mockTools.size === 0) {
|
|
787
|
+
await import('../mcp_server.js');
|
|
788
|
+
}
|
|
789
|
+
});
|
|
322
790
|
|
|
323
|
-
|
|
324
|
-
|
|
791
|
+
it('should register ghost_delete_post tool', () => {
|
|
792
|
+
expect(mockTools.has('ghost_delete_post')).toBe(true);
|
|
793
|
+
});
|
|
325
794
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
795
|
+
it('should have correct schema with required id field', () => {
|
|
796
|
+
const tool = mockTools.get('ghost_delete_post');
|
|
797
|
+
expect(tool).toBeDefined();
|
|
798
|
+
expect(tool.description).toContain('Deletes a post');
|
|
799
|
+
expect(tool.description).toContain('permanent');
|
|
800
|
+
expect(tool.schema).toBeDefined();
|
|
801
|
+
// Zod schemas store field definitions in schema.shape
|
|
802
|
+
expect(tool.schema.shape.id).toBeDefined();
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('should delete post by ID', async () => {
|
|
806
|
+
mockDeletePost.mockResolvedValue({ deleted: true });
|
|
807
|
+
|
|
808
|
+
const tool = mockTools.get('ghost_delete_post');
|
|
809
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
|
|
810
|
+
|
|
811
|
+
expect(mockDeletePost).toHaveBeenCalledWith('507f1f77bcf86cd799439011');
|
|
812
|
+
expect(result.content[0].text).toContain(
|
|
813
|
+
'Post 507f1f77bcf86cd799439011 has been successfully deleted'
|
|
814
|
+
);
|
|
815
|
+
expect(result.isError).toBeUndefined();
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('should handle not found errors', async () => {
|
|
819
|
+
mockDeletePost.mockRejectedValue(new Error('Post not found'));
|
|
820
|
+
|
|
821
|
+
const tool = mockTools.get('ghost_delete_post');
|
|
822
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439099' });
|
|
823
|
+
|
|
824
|
+
expect(result.isError).toBe(true);
|
|
825
|
+
expect(result.content[0].text).toContain('Post not found');
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('should handle Ghost API errors', async () => {
|
|
829
|
+
mockDeletePost.mockRejectedValue(new Error('Ghost API error: Permission denied'));
|
|
830
|
+
|
|
831
|
+
const tool = mockTools.get('ghost_delete_post');
|
|
832
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
|
|
833
|
+
|
|
834
|
+
expect(result.isError).toBe(true);
|
|
835
|
+
expect(result.content[0].text).toContain('Ghost API error');
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('should return success message on successful deletion', async () => {
|
|
839
|
+
mockDeletePost.mockResolvedValue({ deleted: true });
|
|
840
|
+
|
|
841
|
+
const tool = mockTools.get('ghost_delete_post');
|
|
842
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
|
|
843
|
+
|
|
844
|
+
expect(result.content).toBeDefined();
|
|
845
|
+
expect(result.content[0].type).toBe('text');
|
|
846
|
+
expect(result.content[0].text).toBe(
|
|
847
|
+
'Post 507f1f77bcf86cd799439011 has been successfully deleted.'
|
|
848
|
+
);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('should handle network errors', async () => {
|
|
852
|
+
mockDeletePost.mockRejectedValue(new Error('Network error: Connection refused'));
|
|
853
|
+
|
|
854
|
+
const tool = mockTools.get('ghost_delete_post');
|
|
855
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439012' });
|
|
856
|
+
|
|
857
|
+
expect(result.isError).toBe(true);
|
|
858
|
+
expect(result.content[0].text).toContain('Network error');
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
describe('mcp_server - ghost_search_posts tool', () => {
|
|
863
|
+
beforeEach(async () => {
|
|
864
|
+
vi.clearAllMocks();
|
|
865
|
+
// Don't clear mockTools - they're registered once on module load
|
|
866
|
+
if (mockTools.size === 0) {
|
|
867
|
+
await import('../mcp_server.js');
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
it('should register ghost_search_posts tool', () => {
|
|
872
|
+
expect(mockTools.has('ghost_search_posts')).toBe(true);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it('should have correct schema with required query and optional parameters', () => {
|
|
876
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
877
|
+
expect(tool).toBeDefined();
|
|
878
|
+
expect(tool.description).toContain('Search');
|
|
879
|
+
expect(tool.schema).toBeDefined();
|
|
880
|
+
// Zod schemas store field definitions in schema.shape
|
|
881
|
+
expect(tool.schema.shape.query).toBeDefined();
|
|
882
|
+
expect(tool.schema.shape.status).toBeDefined();
|
|
883
|
+
expect(tool.schema.shape.limit).toBeDefined();
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('should search posts with query only', async () => {
|
|
887
|
+
const mockPosts = [
|
|
888
|
+
{ id: '1', title: 'JavaScript Tips', slug: 'javascript-tips', status: 'published' },
|
|
889
|
+
{ id: '2', title: 'JavaScript Tricks', slug: 'javascript-tricks', status: 'published' },
|
|
890
|
+
];
|
|
891
|
+
mockSearchPosts.mockResolvedValue(mockPosts);
|
|
892
|
+
|
|
893
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
894
|
+
const result = await tool.handler({ query: 'JavaScript' });
|
|
895
|
+
|
|
896
|
+
expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', {});
|
|
897
|
+
expect(result.content[0].text).toContain('JavaScript Tips');
|
|
898
|
+
expect(result.content[0].text).toContain('JavaScript Tricks');
|
|
330
899
|
});
|
|
331
900
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
901
|
+
it('should search posts with query and status filter', async () => {
|
|
902
|
+
const mockPosts = [
|
|
903
|
+
{ id: '1', title: 'Published Post', slug: 'published-post', status: 'published' },
|
|
904
|
+
];
|
|
905
|
+
mockSearchPosts.mockResolvedValue(mockPosts);
|
|
336
906
|
|
|
337
|
-
|
|
338
|
-
|
|
907
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
908
|
+
await tool.handler({ query: 'test', status: 'published' });
|
|
339
909
|
|
|
340
|
-
|
|
341
|
-
|
|
910
|
+
expect(mockSearchPosts).toHaveBeenCalledWith('test', { status: 'published' });
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
it('should search posts with query and limit', async () => {
|
|
914
|
+
const mockPosts = [{ id: '1', title: 'Test Post', slug: 'test-post' }];
|
|
915
|
+
mockSearchPosts.mockResolvedValue(mockPosts);
|
|
916
|
+
|
|
917
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
918
|
+
await tool.handler({ query: 'test', limit: 10 });
|
|
919
|
+
|
|
920
|
+
expect(mockSearchPosts).toHaveBeenCalledWith('test', { limit: 10 });
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('should validate limit is between 1 and 50', () => {
|
|
924
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
925
|
+
// Zod schemas store field definitions in schema.shape
|
|
926
|
+
const shape = tool.schema.shape;
|
|
927
|
+
|
|
928
|
+
expect(shape.limit).toBeDefined();
|
|
929
|
+
expect(() => shape.limit.parse(0)).toThrow();
|
|
930
|
+
expect(() => shape.limit.parse(51)).toThrow();
|
|
931
|
+
expect(shape.limit.parse(25)).toBe(25);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('should validate status enum values', () => {
|
|
935
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
936
|
+
// Zod schemas store field definitions in schema.shape
|
|
937
|
+
const shape = tool.schema.shape;
|
|
938
|
+
|
|
939
|
+
expect(shape.status).toBeDefined();
|
|
940
|
+
expect(() => shape.status.parse('invalid')).toThrow();
|
|
941
|
+
expect(shape.status.parse('published')).toBe('published');
|
|
942
|
+
expect(shape.status.parse('draft')).toBe('draft');
|
|
943
|
+
expect(shape.status.parse('scheduled')).toBe('scheduled');
|
|
944
|
+
expect(shape.status.parse('all')).toBe('all');
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it('should pass all parameters combined', async () => {
|
|
948
|
+
const mockPosts = [{ id: '1', title: 'Test Post' }];
|
|
949
|
+
mockSearchPosts.mockResolvedValue(mockPosts);
|
|
950
|
+
|
|
951
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
952
|
+
await tool.handler({
|
|
953
|
+
query: 'JavaScript',
|
|
954
|
+
status: 'published',
|
|
955
|
+
limit: 20,
|
|
342
956
|
});
|
|
343
957
|
|
|
344
|
-
|
|
345
|
-
|
|
958
|
+
expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', {
|
|
959
|
+
status: 'published',
|
|
960
|
+
limit: 20,
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('should handle errors from searchPosts', async () => {
|
|
965
|
+
// Empty query is now caught by Zod validation
|
|
966
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
967
|
+
const result = await tool.handler({ query: '' });
|
|
968
|
+
|
|
969
|
+
expect(result.isError).toBe(true);
|
|
970
|
+
expect(result.content[0].text).toContain('VALIDATION_ERROR');
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
it('should handle Ghost API errors', async () => {
|
|
974
|
+
mockSearchPosts.mockRejectedValue(new Error('Ghost API error'));
|
|
975
|
+
|
|
976
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
977
|
+
const result = await tool.handler({ query: 'test' });
|
|
978
|
+
|
|
979
|
+
expect(result.isError).toBe(true);
|
|
980
|
+
expect(result.content[0].text).toContain('Ghost API error');
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it('should return formatted JSON response', async () => {
|
|
984
|
+
const mockPosts = [
|
|
985
|
+
{
|
|
346
986
|
id: '1',
|
|
347
|
-
|
|
348
|
-
slug: '
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
987
|
+
title: 'Test Post',
|
|
988
|
+
slug: 'test-post',
|
|
989
|
+
html: '<p>Content</p>',
|
|
990
|
+
status: 'published',
|
|
991
|
+
},
|
|
992
|
+
];
|
|
993
|
+
mockSearchPosts.mockResolvedValue(mockPosts);
|
|
352
994
|
|
|
353
|
-
|
|
354
|
-
|
|
995
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
996
|
+
const result = await tool.handler({ query: 'Test' });
|
|
355
997
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
998
|
+
expect(result.content).toBeDefined();
|
|
999
|
+
expect(result.content[0].type).toBe('text');
|
|
1000
|
+
expect(result.content[0].text).toContain('"id": "1"');
|
|
1001
|
+
expect(result.content[0].text).toContain('"title": "Test Post"');
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it('should return empty array when no results found', async () => {
|
|
1005
|
+
mockSearchPosts.mockResolvedValue([]);
|
|
359
1006
|
|
|
360
|
-
|
|
361
|
-
|
|
1007
|
+
const tool = mockTools.get('ghost_search_posts');
|
|
1008
|
+
const result = await tool.handler({ query: 'nonexistent' });
|
|
362
1009
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
1010
|
+
expect(result.content[0].text).toBe('[]');
|
|
1011
|
+
expect(result.isError).toBeUndefined();
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
describe('ghost_get_tag', () => {
|
|
1016
|
+
beforeEach(() => {
|
|
1017
|
+
vi.clearAllMocks();
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it('should be registered as a tool', () => {
|
|
1021
|
+
expect(mockTools.has('ghost_get_tag')).toBe(true);
|
|
1022
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
1023
|
+
expect(tool.name).toBe('ghost_get_tag');
|
|
1024
|
+
expect(tool.description).toBeDefined();
|
|
1025
|
+
expect(tool.schema).toBeDefined();
|
|
1026
|
+
expect(tool.handler).toBeDefined();
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
it('should have correct schema with id and slug as optional', () => {
|
|
1030
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
1031
|
+
// ghost_get_tag uses a refined schema, access via _def.schema.shape
|
|
1032
|
+
const shape = tool.schema._def.schema.shape;
|
|
1033
|
+
expect(shape.id).toBeDefined();
|
|
1034
|
+
expect(shape.slug).toBeDefined();
|
|
1035
|
+
expect(shape.include).toBeDefined();
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
it('should retrieve tag by ID', async () => {
|
|
1039
|
+
const mockTag = {
|
|
1040
|
+
id: '507f1f77bcf86cd799439011',
|
|
1041
|
+
name: 'Test Tag',
|
|
1042
|
+
slug: 'test-tag',
|
|
1043
|
+
description: 'A test tag',
|
|
1044
|
+
};
|
|
1045
|
+
mockGetTag.mockResolvedValue(mockTag);
|
|
1046
|
+
|
|
1047
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
1048
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
|
|
1049
|
+
|
|
1050
|
+
expect(mockGetTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {});
|
|
1051
|
+
expect(result.content).toBeDefined();
|
|
1052
|
+
expect(result.content[0].type).toBe('text');
|
|
1053
|
+
expect(result.content[0].text).toContain('"id": "507f1f77bcf86cd799439011"');
|
|
1054
|
+
expect(result.content[0].text).toContain('"name": "Test Tag"');
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it('should retrieve tag by slug', async () => {
|
|
1058
|
+
const mockTag = {
|
|
1059
|
+
id: '507f1f77bcf86cd799439011',
|
|
1060
|
+
name: 'Test Tag',
|
|
1061
|
+
slug: 'test-tag',
|
|
1062
|
+
description: 'A test tag',
|
|
1063
|
+
};
|
|
1064
|
+
mockGetTag.mockResolvedValue(mockTag);
|
|
1065
|
+
|
|
1066
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
1067
|
+
const result = await tool.handler({ slug: 'test-tag' });
|
|
1068
|
+
|
|
1069
|
+
expect(mockGetTag).toHaveBeenCalledWith('slug/test-tag', {});
|
|
1070
|
+
expect(result.content[0].text).toContain('"slug": "test-tag"');
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
it('should support include parameter for post count', async () => {
|
|
1074
|
+
const mockTag = {
|
|
1075
|
+
id: '507f1f77bcf86cd799439011',
|
|
1076
|
+
name: 'Test Tag',
|
|
1077
|
+
slug: 'test-tag',
|
|
1078
|
+
count: { posts: 5 },
|
|
1079
|
+
};
|
|
1080
|
+
mockGetTag.mockResolvedValue(mockTag);
|
|
1081
|
+
|
|
1082
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
1083
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011', include: 'count.posts' });
|
|
1084
|
+
|
|
1085
|
+
expect(mockGetTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { include: 'count.posts' });
|
|
1086
|
+
expect(result.content[0].text).toContain('"count"');
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it('should return error when neither id nor slug provided', async () => {
|
|
1090
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
1091
|
+
const result = await tool.handler({});
|
|
1092
|
+
|
|
1093
|
+
expect(result.content[0].type).toBe('text');
|
|
1094
|
+
expect(result.content[0].text).toContain('Either id or slug is required');
|
|
1095
|
+
expect(result.isError).toBe(true);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it('should handle not found error', async () => {
|
|
1099
|
+
mockGetTag.mockRejectedValue(new Error('Tag not found'));
|
|
1100
|
+
|
|
1101
|
+
const tool = mockTools.get('ghost_get_tag');
|
|
1102
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439099' });
|
|
1103
|
+
|
|
1104
|
+
expect(result.isError).toBe(true);
|
|
1105
|
+
expect(result.content[0].text).toContain('Tag not found');
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
describe('ghost_update_tag', () => {
|
|
1110
|
+
beforeEach(() => {
|
|
1111
|
+
vi.clearAllMocks();
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
it('should be registered as a tool', () => {
|
|
1115
|
+
expect(mockTools.has('ghost_update_tag')).toBe(true);
|
|
1116
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1117
|
+
expect(tool.name).toBe('ghost_update_tag');
|
|
1118
|
+
expect(tool.description).toBeDefined();
|
|
1119
|
+
expect(tool.schema).toBeDefined();
|
|
1120
|
+
expect(tool.handler).toBeDefined();
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('should have correct schema with all update fields', () => {
|
|
1124
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1125
|
+
// Zod schemas store field definitions in schema.shape
|
|
1126
|
+
expect(tool.schema.shape.id).toBeDefined();
|
|
1127
|
+
expect(tool.schema.shape.name).toBeDefined();
|
|
1128
|
+
expect(tool.schema.shape.slug).toBeDefined();
|
|
1129
|
+
expect(tool.schema.shape.description).toBeDefined();
|
|
1130
|
+
expect(tool.schema.shape.feature_image).toBeDefined();
|
|
1131
|
+
expect(tool.schema.shape.meta_title).toBeDefined();
|
|
1132
|
+
expect(tool.schema.shape.meta_description).toBeDefined();
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
it('should update tag name', async () => {
|
|
1136
|
+
const mockUpdatedTag = {
|
|
1137
|
+
id: '507f1f77bcf86cd799439011',
|
|
1138
|
+
name: 'Updated Tag',
|
|
1139
|
+
slug: 'updated-tag',
|
|
1140
|
+
};
|
|
1141
|
+
mockUpdateTag.mockResolvedValue(mockUpdatedTag);
|
|
1142
|
+
|
|
1143
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1144
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011', name: 'Updated Tag' });
|
|
1145
|
+
|
|
1146
|
+
expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', { name: 'Updated Tag' });
|
|
1147
|
+
expect(result.content[0].text).toContain('"name": "Updated Tag"');
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
it('should update tag description', async () => {
|
|
1151
|
+
const mockUpdatedTag = {
|
|
1152
|
+
id: '507f1f77bcf86cd799439011',
|
|
1153
|
+
name: 'Test Tag',
|
|
1154
|
+
description: 'New description',
|
|
1155
|
+
};
|
|
1156
|
+
mockUpdateTag.mockResolvedValue(mockUpdatedTag);
|
|
1157
|
+
|
|
1158
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1159
|
+
const result = await tool.handler({
|
|
1160
|
+
id: '507f1f77bcf86cd799439011',
|
|
1161
|
+
description: 'New description',
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
1165
|
+
description: 'New description',
|
|
366
1166
|
});
|
|
1167
|
+
expect(result.content[0].text).toContain('"description": "New description"');
|
|
367
1168
|
});
|
|
368
1169
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
1170
|
+
it('should update multiple fields at once', async () => {
|
|
1171
|
+
const mockUpdatedTag = {
|
|
1172
|
+
id: '507f1f77bcf86cd799439011',
|
|
1173
|
+
name: 'Updated Tag',
|
|
1174
|
+
slug: 'updated-tag',
|
|
1175
|
+
description: 'Updated description',
|
|
1176
|
+
meta_title: 'Updated Meta',
|
|
1177
|
+
};
|
|
1178
|
+
mockUpdateTag.mockResolvedValue(mockUpdatedTag);
|
|
1179
|
+
|
|
1180
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1181
|
+
await tool.handler({
|
|
1182
|
+
id: '507f1f77bcf86cd799439011',
|
|
1183
|
+
name: 'Updated Tag',
|
|
1184
|
+
description: 'Updated description',
|
|
1185
|
+
meta_title: 'Updated Meta',
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
1189
|
+
name: 'Updated Tag',
|
|
1190
|
+
description: 'Updated description',
|
|
1191
|
+
meta_title: 'Updated Meta',
|
|
373
1192
|
});
|
|
374
1193
|
});
|
|
375
1194
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
1195
|
+
it('should update tag feature image', async () => {
|
|
1196
|
+
const mockUpdatedTag = {
|
|
1197
|
+
id: '507f1f77bcf86cd799439011',
|
|
1198
|
+
name: 'Test Tag',
|
|
1199
|
+
feature_image: 'https://example.com/image.jpg',
|
|
1200
|
+
};
|
|
1201
|
+
mockUpdateTag.mockResolvedValue(mockUpdatedTag);
|
|
1202
|
+
|
|
1203
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1204
|
+
await tool.handler({
|
|
1205
|
+
id: '507f1f77bcf86cd799439011',
|
|
1206
|
+
feature_image: 'https://example.com/image.jpg',
|
|
380
1207
|
});
|
|
381
1208
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
expect(module.startMCPServer).toBeDefined();
|
|
1209
|
+
expect(mockUpdateTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011', {
|
|
1210
|
+
feature_image: 'https://example.com/image.jpg',
|
|
385
1211
|
});
|
|
386
1212
|
});
|
|
1213
|
+
|
|
1214
|
+
it('should return error when id is missing', async () => {
|
|
1215
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1216
|
+
const result = await tool.handler({ name: 'Test' });
|
|
1217
|
+
|
|
1218
|
+
expect(result.isError).toBe(true);
|
|
1219
|
+
expect(result.content[0].text).toContain('VALIDATION_ERROR');
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
it('should handle validation error', async () => {
|
|
1223
|
+
mockUpdateTag.mockRejectedValue(new Error('Validation failed'));
|
|
1224
|
+
|
|
1225
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1226
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011', name: '' });
|
|
1227
|
+
|
|
1228
|
+
expect(result.isError).toBe(true);
|
|
1229
|
+
expect(result.content[0].text).toContain('Validation failed');
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
it('should handle not found error', async () => {
|
|
1233
|
+
mockUpdateTag.mockRejectedValue(new Error('Tag not found'));
|
|
1234
|
+
|
|
1235
|
+
const tool = mockTools.get('ghost_update_tag');
|
|
1236
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439099', name: 'Test' });
|
|
1237
|
+
|
|
1238
|
+
expect(result.isError).toBe(true);
|
|
1239
|
+
expect(result.content[0].text).toContain('Tag not found');
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
describe('ghost_delete_tag', () => {
|
|
1244
|
+
beforeEach(() => {
|
|
1245
|
+
vi.clearAllMocks();
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
it('should be registered as a tool', () => {
|
|
1249
|
+
expect(mockTools.has('ghost_delete_tag')).toBe(true);
|
|
1250
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1251
|
+
expect(tool.name).toBe('ghost_delete_tag');
|
|
1252
|
+
expect(tool.description).toBeDefined();
|
|
1253
|
+
expect(tool.schema).toBeDefined();
|
|
1254
|
+
expect(tool.handler).toBeDefined();
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
it('should have correct schema with id field', () => {
|
|
1258
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1259
|
+
// Zod schemas store field definitions in schema.shape
|
|
1260
|
+
expect(tool.schema.shape.id).toBeDefined();
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
it('should delete tag successfully', async () => {
|
|
1264
|
+
mockDeleteTag.mockResolvedValue({ success: true });
|
|
1265
|
+
|
|
1266
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1267
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
|
|
1268
|
+
|
|
1269
|
+
expect(mockDeleteTag).toHaveBeenCalledWith('507f1f77bcf86cd799439011');
|
|
1270
|
+
expect(result.content[0].text).toContain('successfully deleted');
|
|
1271
|
+
expect(result.isError).toBeUndefined();
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it('should return error when id is missing', async () => {
|
|
1275
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1276
|
+
const result = await tool.handler({});
|
|
1277
|
+
|
|
1278
|
+
expect(result.isError).toBe(true);
|
|
1279
|
+
expect(result.content[0].text).toContain('VALIDATION_ERROR');
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
it('should handle not found error', async () => {
|
|
1283
|
+
mockDeleteTag.mockRejectedValue(new Error('Tag not found'));
|
|
1284
|
+
|
|
1285
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1286
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439099' });
|
|
1287
|
+
|
|
1288
|
+
expect(result.isError).toBe(true);
|
|
1289
|
+
expect(result.content[0].text).toContain('Tag not found');
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
it('should handle deletion error', async () => {
|
|
1293
|
+
mockDeleteTag.mockRejectedValue(new Error('Failed to delete tag'));
|
|
1294
|
+
|
|
1295
|
+
const tool = mockTools.get('ghost_delete_tag');
|
|
1296
|
+
const result = await tool.handler({ id: '507f1f77bcf86cd799439011' });
|
|
1297
|
+
|
|
1298
|
+
expect(result.isError).toBe(true);
|
|
1299
|
+
expect(result.content[0].text).toContain('Failed to delete tag');
|
|
1300
|
+
});
|
|
387
1301
|
});
|