@jgardner04/ghost-mcp-server 1.1.12 → 1.2.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 +2 -1
- package/src/__tests__/helpers/mockExpress.js +38 -0
- package/src/__tests__/index.test.js +312 -0
- package/src/__tests__/mcp_server.test.js +381 -0
- package/src/__tests__/mcp_server_improved.test.js +440 -0
- package/src/config/__tests__/mcp-config.test.js +311 -0
- package/src/controllers/__tests__/imageController.test.js +572 -0
- package/src/controllers/__tests__/postController.test.js +236 -0
- package/src/controllers/__tests__/tagController.test.js +222 -0
- package/src/mcp_server_improved.js +105 -1
- package/src/middleware/__tests__/errorMiddleware.test.js +1113 -0
- package/src/resources/__tests__/ResourceManager.test.js +977 -0
- package/src/routes/__tests__/imageRoutes.test.js +117 -0
- package/src/routes/__tests__/postRoutes.test.js +262 -0
- package/src/routes/__tests__/tagRoutes.test.js +175 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
|
|
3
|
+
import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
|
|
4
|
+
import {
|
|
5
|
+
createMockRequest,
|
|
6
|
+
createMockResponse,
|
|
7
|
+
createMockNext,
|
|
8
|
+
} from '../../__tests__/helpers/mockExpress.js';
|
|
9
|
+
|
|
10
|
+
// Mock dotenv
|
|
11
|
+
vi.mock('dotenv', () => mockDotenv());
|
|
12
|
+
|
|
13
|
+
// Mock logger
|
|
14
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
15
|
+
createContextLogger: createMockContextLogger(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Mock postService functions
|
|
19
|
+
vi.mock('../../services/postService.js', () => ({
|
|
20
|
+
createPostService: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Import after mocks are set up
|
|
24
|
+
import { createPost } from '../postController.js';
|
|
25
|
+
import * as postService from '../../services/postService.js';
|
|
26
|
+
|
|
27
|
+
describe('postController', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('createPost', () => {
|
|
33
|
+
it('should return 201 with new post on success', async () => {
|
|
34
|
+
const mockPost = {
|
|
35
|
+
id: '1',
|
|
36
|
+
title: 'Test Post',
|
|
37
|
+
html: '<p>Test content</p>',
|
|
38
|
+
status: 'draft',
|
|
39
|
+
};
|
|
40
|
+
postService.createPostService.mockResolvedValue(mockPost);
|
|
41
|
+
|
|
42
|
+
const req = createMockRequest({
|
|
43
|
+
body: {
|
|
44
|
+
title: 'Test Post',
|
|
45
|
+
html: '<p>Test content</p>',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const res = createMockResponse();
|
|
49
|
+
const next = createMockNext();
|
|
50
|
+
|
|
51
|
+
await createPost(req, res, next);
|
|
52
|
+
|
|
53
|
+
expect(postService.createPostService).toHaveBeenCalledWith({
|
|
54
|
+
title: 'Test Post',
|
|
55
|
+
html: '<p>Test content</p>',
|
|
56
|
+
});
|
|
57
|
+
expect(res.status).toHaveBeenCalledWith(201);
|
|
58
|
+
expect(res.json).toHaveBeenCalledWith(mockPost);
|
|
59
|
+
expect(next).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should pass through all post fields including feature_image', async () => {
|
|
63
|
+
const mockPost = {
|
|
64
|
+
id: '1',
|
|
65
|
+
title: 'Post with Image',
|
|
66
|
+
html: '<p>Content</p>',
|
|
67
|
+
feature_image: 'https://example.com/image.jpg',
|
|
68
|
+
status: 'published',
|
|
69
|
+
};
|
|
70
|
+
postService.createPostService.mockResolvedValue(mockPost);
|
|
71
|
+
|
|
72
|
+
const req = createMockRequest({
|
|
73
|
+
body: {
|
|
74
|
+
title: 'Post with Image',
|
|
75
|
+
html: '<p>Content</p>',
|
|
76
|
+
feature_image: 'https://example.com/image.jpg',
|
|
77
|
+
status: 'published',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const res = createMockResponse();
|
|
81
|
+
const next = createMockNext();
|
|
82
|
+
|
|
83
|
+
await createPost(req, res, next);
|
|
84
|
+
|
|
85
|
+
expect(postService.createPostService).toHaveBeenCalledWith({
|
|
86
|
+
title: 'Post with Image',
|
|
87
|
+
html: '<p>Content</p>',
|
|
88
|
+
feature_image: 'https://example.com/image.jpg',
|
|
89
|
+
status: 'published',
|
|
90
|
+
});
|
|
91
|
+
expect(res.status).toHaveBeenCalledWith(201);
|
|
92
|
+
expect(res.json).toHaveBeenCalledWith(mockPost);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle posts with tags', async () => {
|
|
96
|
+
const mockPost = {
|
|
97
|
+
id: '1',
|
|
98
|
+
title: 'Tagged Post',
|
|
99
|
+
html: '<p>Content</p>',
|
|
100
|
+
tags: [{ name: 'Technology' }, { name: 'Science' }],
|
|
101
|
+
status: 'draft',
|
|
102
|
+
};
|
|
103
|
+
postService.createPostService.mockResolvedValue(mockPost);
|
|
104
|
+
|
|
105
|
+
const req = createMockRequest({
|
|
106
|
+
body: {
|
|
107
|
+
title: 'Tagged Post',
|
|
108
|
+
html: '<p>Content</p>',
|
|
109
|
+
tags: [{ name: 'Technology' }, { name: 'Science' }],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const res = createMockResponse();
|
|
113
|
+
const next = createMockNext();
|
|
114
|
+
|
|
115
|
+
await createPost(req, res, next);
|
|
116
|
+
|
|
117
|
+
expect(postService.createPostService).toHaveBeenCalledWith({
|
|
118
|
+
title: 'Tagged Post',
|
|
119
|
+
html: '<p>Content</p>',
|
|
120
|
+
tags: [{ name: 'Technology' }, { name: 'Science' }],
|
|
121
|
+
});
|
|
122
|
+
expect(res.status).toHaveBeenCalledWith(201);
|
|
123
|
+
expect(res.json).toHaveBeenCalledWith(mockPost);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle posts with metadata', async () => {
|
|
127
|
+
const mockPost = {
|
|
128
|
+
id: '1',
|
|
129
|
+
title: 'Post with Metadata',
|
|
130
|
+
html: '<p>Content</p>',
|
|
131
|
+
meta_title: 'SEO Title',
|
|
132
|
+
meta_description: 'SEO Description',
|
|
133
|
+
status: 'draft',
|
|
134
|
+
};
|
|
135
|
+
postService.createPostService.mockResolvedValue(mockPost);
|
|
136
|
+
|
|
137
|
+
const req = createMockRequest({
|
|
138
|
+
body: {
|
|
139
|
+
title: 'Post with Metadata',
|
|
140
|
+
html: '<p>Content</p>',
|
|
141
|
+
meta_title: 'SEO Title',
|
|
142
|
+
meta_description: 'SEO Description',
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const res = createMockResponse();
|
|
146
|
+
const next = createMockNext();
|
|
147
|
+
|
|
148
|
+
await createPost(req, res, next);
|
|
149
|
+
|
|
150
|
+
expect(postService.createPostService).toHaveBeenCalledWith({
|
|
151
|
+
title: 'Post with Metadata',
|
|
152
|
+
html: '<p>Content</p>',
|
|
153
|
+
meta_title: 'SEO Title',
|
|
154
|
+
meta_description: 'SEO Description',
|
|
155
|
+
});
|
|
156
|
+
expect(res.status).toHaveBeenCalledWith(201);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should call next() with error when service throws error', async () => {
|
|
160
|
+
const mockError = new Error('Failed to create post');
|
|
161
|
+
postService.createPostService.mockRejectedValue(mockError);
|
|
162
|
+
|
|
163
|
+
const req = createMockRequest({
|
|
164
|
+
body: {
|
|
165
|
+
title: 'Test Post',
|
|
166
|
+
html: '<p>Test content</p>',
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const res = createMockResponse();
|
|
170
|
+
const next = createMockNext();
|
|
171
|
+
|
|
172
|
+
await createPost(req, res, next);
|
|
173
|
+
|
|
174
|
+
expect(postService.createPostService).toHaveBeenCalledWith({
|
|
175
|
+
title: 'Test Post',
|
|
176
|
+
html: '<p>Test content</p>',
|
|
177
|
+
});
|
|
178
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
179
|
+
expect(res.json).not.toHaveBeenCalled();
|
|
180
|
+
expect(next).toHaveBeenCalledWith(mockError);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should handle validation errors from service', async () => {
|
|
184
|
+
const validationError = new Error('Validation failed: title is required');
|
|
185
|
+
validationError.name = 'ValidationError';
|
|
186
|
+
postService.createPostService.mockRejectedValue(validationError);
|
|
187
|
+
|
|
188
|
+
const req = createMockRequest({
|
|
189
|
+
body: {
|
|
190
|
+
html: '<p>Content without title</p>',
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
const res = createMockResponse();
|
|
194
|
+
const next = createMockNext();
|
|
195
|
+
|
|
196
|
+
await createPost(req, res, next);
|
|
197
|
+
|
|
198
|
+
expect(next).toHaveBeenCalledWith(validationError);
|
|
199
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle scheduled posts', async () => {
|
|
203
|
+
const scheduledDate = '2025-12-31T00:00:00Z';
|
|
204
|
+
const mockPost = {
|
|
205
|
+
id: '1',
|
|
206
|
+
title: 'Scheduled Post',
|
|
207
|
+
html: '<p>Future content</p>',
|
|
208
|
+
status: 'scheduled',
|
|
209
|
+
published_at: scheduledDate,
|
|
210
|
+
};
|
|
211
|
+
postService.createPostService.mockResolvedValue(mockPost);
|
|
212
|
+
|
|
213
|
+
const req = createMockRequest({
|
|
214
|
+
body: {
|
|
215
|
+
title: 'Scheduled Post',
|
|
216
|
+
html: '<p>Future content</p>',
|
|
217
|
+
status: 'scheduled',
|
|
218
|
+
published_at: scheduledDate,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
const res = createMockResponse();
|
|
222
|
+
const next = createMockNext();
|
|
223
|
+
|
|
224
|
+
await createPost(req, res, next);
|
|
225
|
+
|
|
226
|
+
expect(postService.createPostService).toHaveBeenCalledWith({
|
|
227
|
+
title: 'Scheduled Post',
|
|
228
|
+
html: '<p>Future content</p>',
|
|
229
|
+
status: 'scheduled',
|
|
230
|
+
published_at: scheduledDate,
|
|
231
|
+
});
|
|
232
|
+
expect(res.status).toHaveBeenCalledWith(201);
|
|
233
|
+
expect(res.json).toHaveBeenCalledWith(mockPost);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { mockDotenv } from '../../__tests__/helpers/testUtils.js';
|
|
3
|
+
import { createMockContextLogger } from '../../__tests__/helpers/mockLogger.js';
|
|
4
|
+
import {
|
|
5
|
+
createMockRequest,
|
|
6
|
+
createMockResponse,
|
|
7
|
+
createMockNext,
|
|
8
|
+
} from '../../__tests__/helpers/mockExpress.js';
|
|
9
|
+
|
|
10
|
+
// Mock dotenv
|
|
11
|
+
vi.mock('dotenv', () => mockDotenv());
|
|
12
|
+
|
|
13
|
+
// Mock logger
|
|
14
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
15
|
+
createContextLogger: createMockContextLogger(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Mock ghostService functions
|
|
19
|
+
vi.mock('../../services/ghostService.js', () => ({
|
|
20
|
+
getTags: vi.fn(),
|
|
21
|
+
createTag: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// Import after mocks are set up
|
|
25
|
+
import { getTags, createTag } from '../tagController.js';
|
|
26
|
+
import * as ghostService from '../../services/ghostService.js';
|
|
27
|
+
|
|
28
|
+
describe('tagController', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('getTags', () => {
|
|
34
|
+
it('should return 200 with tags array on success', async () => {
|
|
35
|
+
const mockTags = [
|
|
36
|
+
{ id: '1', name: 'Technology', slug: 'technology' },
|
|
37
|
+
{ id: '2', name: 'Science', slug: 'science' },
|
|
38
|
+
];
|
|
39
|
+
ghostService.getTags.mockResolvedValue(mockTags);
|
|
40
|
+
|
|
41
|
+
const req = createMockRequest();
|
|
42
|
+
const res = createMockResponse();
|
|
43
|
+
const next = createMockNext();
|
|
44
|
+
|
|
45
|
+
await getTags(req, res, next);
|
|
46
|
+
|
|
47
|
+
expect(ghostService.getTags).toHaveBeenCalledWith(undefined);
|
|
48
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
49
|
+
expect(res.json).toHaveBeenCalledWith(mockTags);
|
|
50
|
+
expect(next).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should filter tags by name when name query parameter is provided', async () => {
|
|
54
|
+
const mockTags = [{ id: '1', name: 'Technology', slug: 'technology' }];
|
|
55
|
+
ghostService.getTags.mockResolvedValue(mockTags);
|
|
56
|
+
|
|
57
|
+
const req = createMockRequest({ query: { name: 'Technology' } });
|
|
58
|
+
const res = createMockResponse();
|
|
59
|
+
const next = createMockNext();
|
|
60
|
+
|
|
61
|
+
await getTags(req, res, next);
|
|
62
|
+
|
|
63
|
+
expect(ghostService.getTags).toHaveBeenCalledWith('Technology');
|
|
64
|
+
expect(res.status).toHaveBeenCalledWith(200);
|
|
65
|
+
expect(res.json).toHaveBeenCalledWith(mockTags);
|
|
66
|
+
expect(next).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should call next() with error when service throws error', async () => {
|
|
70
|
+
const mockError = new Error('Failed to fetch tags');
|
|
71
|
+
ghostService.getTags.mockRejectedValue(mockError);
|
|
72
|
+
|
|
73
|
+
const req = createMockRequest();
|
|
74
|
+
const res = createMockResponse();
|
|
75
|
+
const next = createMockNext();
|
|
76
|
+
|
|
77
|
+
await getTags(req, res, next);
|
|
78
|
+
|
|
79
|
+
expect(ghostService.getTags).toHaveBeenCalledWith(undefined);
|
|
80
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
81
|
+
expect(res.json).not.toHaveBeenCalled();
|
|
82
|
+
expect(next).toHaveBeenCalledWith(mockError);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('createTag', () => {
|
|
87
|
+
it('should return 201 with new tag on success', async () => {
|
|
88
|
+
const mockTag = { id: '1', name: 'New Tag', slug: 'new-tag' };
|
|
89
|
+
ghostService.createTag.mockResolvedValue(mockTag);
|
|
90
|
+
|
|
91
|
+
const req = createMockRequest({
|
|
92
|
+
body: {
|
|
93
|
+
name: 'New Tag',
|
|
94
|
+
description: 'A new tag description',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const res = createMockResponse();
|
|
98
|
+
const next = createMockNext();
|
|
99
|
+
|
|
100
|
+
await createTag(req, res, next);
|
|
101
|
+
|
|
102
|
+
expect(ghostService.createTag).toHaveBeenCalledWith({
|
|
103
|
+
name: 'New Tag',
|
|
104
|
+
description: 'A new tag description',
|
|
105
|
+
slug: undefined,
|
|
106
|
+
});
|
|
107
|
+
expect(res.status).toHaveBeenCalledWith(201);
|
|
108
|
+
expect(res.json).toHaveBeenCalledWith(mockTag);
|
|
109
|
+
expect(next).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return 400 when name is missing', async () => {
|
|
113
|
+
const req = createMockRequest({
|
|
114
|
+
body: {
|
|
115
|
+
description: 'A tag without a name',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const res = createMockResponse();
|
|
119
|
+
const next = createMockNext();
|
|
120
|
+
|
|
121
|
+
await createTag(req, res, next);
|
|
122
|
+
|
|
123
|
+
expect(ghostService.createTag).not.toHaveBeenCalled();
|
|
124
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
125
|
+
expect(res.json).toHaveBeenCalledWith({ message: 'Tag name is required.' });
|
|
126
|
+
expect(next).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return 400 when name is empty string', async () => {
|
|
130
|
+
const req = createMockRequest({
|
|
131
|
+
body: {
|
|
132
|
+
name: '',
|
|
133
|
+
description: 'A tag with empty name',
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const res = createMockResponse();
|
|
137
|
+
const next = createMockNext();
|
|
138
|
+
|
|
139
|
+
await createTag(req, res, next);
|
|
140
|
+
|
|
141
|
+
expect(ghostService.createTag).not.toHaveBeenCalled();
|
|
142
|
+
expect(res.status).toHaveBeenCalledWith(400);
|
|
143
|
+
expect(res.json).toHaveBeenCalledWith({ message: 'Tag name is required.' });
|
|
144
|
+
expect(next).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should call next() with error when service throws error', async () => {
|
|
148
|
+
const mockError = new Error('Failed to create tag');
|
|
149
|
+
ghostService.createTag.mockRejectedValue(mockError);
|
|
150
|
+
|
|
151
|
+
const req = createMockRequest({
|
|
152
|
+
body: {
|
|
153
|
+
name: 'Test Tag',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const res = createMockResponse();
|
|
157
|
+
const next = createMockNext();
|
|
158
|
+
|
|
159
|
+
await createTag(req, res, next);
|
|
160
|
+
|
|
161
|
+
expect(ghostService.createTag).toHaveBeenCalledWith({
|
|
162
|
+
name: 'Test Tag',
|
|
163
|
+
description: undefined,
|
|
164
|
+
slug: undefined,
|
|
165
|
+
});
|
|
166
|
+
expect(res.status).not.toHaveBeenCalled();
|
|
167
|
+
expect(res.json).not.toHaveBeenCalled();
|
|
168
|
+
expect(next).toHaveBeenCalledWith(mockError);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should include slug when provided', async () => {
|
|
172
|
+
const mockTag = { id: '1', name: 'Custom Tag', slug: 'custom-slug' };
|
|
173
|
+
ghostService.createTag.mockResolvedValue(mockTag);
|
|
174
|
+
|
|
175
|
+
const req = createMockRequest({
|
|
176
|
+
body: {
|
|
177
|
+
name: 'Custom Tag',
|
|
178
|
+
slug: 'custom-slug',
|
|
179
|
+
description: 'A tag with custom slug',
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const res = createMockResponse();
|
|
183
|
+
const next = createMockNext();
|
|
184
|
+
|
|
185
|
+
await createTag(req, res, next);
|
|
186
|
+
|
|
187
|
+
expect(ghostService.createTag).toHaveBeenCalledWith({
|
|
188
|
+
name: 'Custom Tag',
|
|
189
|
+
slug: 'custom-slug',
|
|
190
|
+
description: 'A tag with custom slug',
|
|
191
|
+
});
|
|
192
|
+
expect(res.status).toHaveBeenCalledWith(201);
|
|
193
|
+
expect(res.json).toHaveBeenCalledWith(mockTag);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should pass through additional fields', async () => {
|
|
197
|
+
const mockTag = { id: '1', name: 'Tag', slug: 'tag', visibility: 'public' };
|
|
198
|
+
ghostService.createTag.mockResolvedValue(mockTag);
|
|
199
|
+
|
|
200
|
+
const req = createMockRequest({
|
|
201
|
+
body: {
|
|
202
|
+
name: 'Tag',
|
|
203
|
+
visibility: 'public',
|
|
204
|
+
meta_title: 'Custom Meta Title',
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
const res = createMockResponse();
|
|
208
|
+
const next = createMockNext();
|
|
209
|
+
|
|
210
|
+
await createTag(req, res, next);
|
|
211
|
+
|
|
212
|
+
expect(ghostService.createTag).toHaveBeenCalledWith({
|
|
213
|
+
name: 'Tag',
|
|
214
|
+
description: undefined,
|
|
215
|
+
slug: undefined,
|
|
216
|
+
visibility: 'public',
|
|
217
|
+
meta_title: 'Custom Meta Title',
|
|
218
|
+
});
|
|
219
|
+
expect(res.status).toHaveBeenCalledWith(201);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -272,6 +272,110 @@ server.tool(
|
|
|
272
272
|
}
|
|
273
273
|
);
|
|
274
274
|
|
|
275
|
+
// Get Posts Tool
|
|
276
|
+
server.tool(
|
|
277
|
+
'ghost_get_posts',
|
|
278
|
+
'Retrieves a list of posts from Ghost CMS with pagination, filtering, and sorting options.',
|
|
279
|
+
{
|
|
280
|
+
limit: z
|
|
281
|
+
.number()
|
|
282
|
+
.min(1)
|
|
283
|
+
.max(100)
|
|
284
|
+
.optional()
|
|
285
|
+
.describe('Number of posts to retrieve (1-100). Default is 15.'),
|
|
286
|
+
page: z.number().min(1).optional().describe('Page number for pagination. Default is 1.'),
|
|
287
|
+
status: z
|
|
288
|
+
.enum(['published', 'draft', 'scheduled', 'all'])
|
|
289
|
+
.optional()
|
|
290
|
+
.describe('Filter posts by status. Options: published, draft, scheduled, all.'),
|
|
291
|
+
include: z
|
|
292
|
+
.string()
|
|
293
|
+
.optional()
|
|
294
|
+
.describe('Comma-separated list of relations to include (e.g., "tags,authors").'),
|
|
295
|
+
filter: z
|
|
296
|
+
.string()
|
|
297
|
+
.optional()
|
|
298
|
+
.describe('Ghost NQL filter string for advanced filtering (e.g., "featured:true").'),
|
|
299
|
+
order: z
|
|
300
|
+
.string()
|
|
301
|
+
.optional()
|
|
302
|
+
.describe('Sort order for results (e.g., "published_at DESC", "title ASC").'),
|
|
303
|
+
},
|
|
304
|
+
async (input) => {
|
|
305
|
+
console.error(`Executing tool: ghost_get_posts`);
|
|
306
|
+
try {
|
|
307
|
+
await loadServices();
|
|
308
|
+
|
|
309
|
+
// Build options object with provided parameters
|
|
310
|
+
const options = {};
|
|
311
|
+
if (input.limit !== undefined) options.limit = input.limit;
|
|
312
|
+
if (input.page !== undefined) options.page = input.page;
|
|
313
|
+
if (input.status !== undefined) options.status = input.status;
|
|
314
|
+
if (input.include !== undefined) options.include = input.include;
|
|
315
|
+
if (input.filter !== undefined) options.filter = input.filter;
|
|
316
|
+
if (input.order !== undefined) options.order = input.order;
|
|
317
|
+
|
|
318
|
+
const posts = await ghostService.getPosts(options);
|
|
319
|
+
console.error(`Retrieved ${posts.length} posts from Ghost.`);
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }],
|
|
323
|
+
};
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error(`Error in ghost_get_posts:`, error);
|
|
326
|
+
return {
|
|
327
|
+
content: [{ type: 'text', text: `Error retrieving posts: ${error.message}` }],
|
|
328
|
+
isError: true,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Get Post Tool
|
|
335
|
+
server.tool(
|
|
336
|
+
'ghost_get_post',
|
|
337
|
+
'Retrieves a single post from Ghost CMS by ID or slug.',
|
|
338
|
+
{
|
|
339
|
+
id: z.string().optional().describe('The ID of the post to retrieve.'),
|
|
340
|
+
slug: z.string().optional().describe('The slug of the post to retrieve.'),
|
|
341
|
+
include: z
|
|
342
|
+
.string()
|
|
343
|
+
.optional()
|
|
344
|
+
.describe('Comma-separated list of relations to include (e.g., "tags,authors").'),
|
|
345
|
+
},
|
|
346
|
+
async (input) => {
|
|
347
|
+
console.error(`Executing tool: ghost_get_post`);
|
|
348
|
+
try {
|
|
349
|
+
// Validate that at least one of id or slug is provided
|
|
350
|
+
if (!input.id && !input.slug) {
|
|
351
|
+
throw new Error('Either id or slug is required to retrieve a post');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await loadServices();
|
|
355
|
+
|
|
356
|
+
// Build options object
|
|
357
|
+
const options = {};
|
|
358
|
+
if (input.include !== undefined) options.include = input.include;
|
|
359
|
+
|
|
360
|
+
// Determine identifier (prefer ID over slug)
|
|
361
|
+
const identifier = input.id || `slug/${input.slug}`;
|
|
362
|
+
|
|
363
|
+
const post = await ghostService.getPost(identifier, options);
|
|
364
|
+
console.error(`Retrieved post: ${post.title} (ID: ${post.id})`);
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
content: [{ type: 'text', text: JSON.stringify(post, null, 2) }],
|
|
368
|
+
};
|
|
369
|
+
} catch (error) {
|
|
370
|
+
console.error(`Error in ghost_get_post:`, error);
|
|
371
|
+
return {
|
|
372
|
+
content: [{ type: 'text', text: `Error retrieving post: ${error.message}` }],
|
|
373
|
+
isError: true,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
|
|
275
379
|
// --- Main Entry Point ---
|
|
276
380
|
|
|
277
381
|
async function main() {
|
|
@@ -282,7 +386,7 @@ async function main() {
|
|
|
282
386
|
|
|
283
387
|
console.error('Ghost MCP Server running on stdio transport');
|
|
284
388
|
console.error(
|
|
285
|
-
'Available tools: ghost_get_tags, ghost_create_tag, ghost_upload_image, ghost_create_post'
|
|
389
|
+
'Available tools: ghost_get_tags, ghost_create_tag, ghost_upload_image, ghost_create_post, ghost_get_posts, ghost_get_post'
|
|
286
390
|
);
|
|
287
391
|
}
|
|
288
392
|
|