@jgardner04/ghost-mcp-server 1.13.1 → 1.13.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.
Files changed (35) hide show
  1. package/package.json +6 -18
  2. package/src/__tests__/mcp_server.test.js +204 -117
  3. package/src/__tests__/mcp_server_pages.test.js +32 -18
  4. package/src/config/mcp-config.js +1 -1
  5. package/src/controllers/__tests__/tagController.test.js +12 -8
  6. package/src/controllers/tagController.js +2 -2
  7. package/src/errors/__tests__/index.test.js +3 -3
  8. package/src/errors/index.js +1 -1
  9. package/src/index.js +1 -1
  10. package/src/mcp_server.js +37 -33
  11. package/src/schemas/__tests__/postSchemas.test.js +19 -0
  12. package/src/schemas/__tests__/tagSchemas.test.js +1 -1
  13. package/src/schemas/common.js +2 -2
  14. package/src/schemas/memberSchemas.js +20 -8
  15. package/src/schemas/newsletterSchemas.js +10 -10
  16. package/src/schemas/pageSchemas.js +16 -13
  17. package/src/schemas/postSchemas.js +22 -13
  18. package/src/schemas/tagSchemas.js +12 -7
  19. package/src/schemas/tierSchemas.js +17 -8
  20. package/src/services/__tests__/ghostServiceImproved.members.test.js +15 -6
  21. package/src/services/__tests__/ghostServiceImproved.newsletters.test.js +21 -12
  22. package/src/services/__tests__/ghostServiceImproved.pages.test.js +59 -16
  23. package/src/services/__tests__/ghostServiceImproved.posts.test.js +233 -0
  24. package/src/services/__tests__/ghostServiceImproved.tags.test.js +13 -2
  25. package/src/services/__tests__/ghostServiceImproved.tiers.test.js +18 -19
  26. package/src/services/__tests__/memberService.test.js +0 -28
  27. package/src/services/__tests__/tierService.test.js +0 -28
  28. package/src/services/ghostServiceImproved.js +108 -379
  29. package/src/services/imageProcessingService.js +1 -1
  30. package/src/services/memberService.js +0 -13
  31. package/src/services/tierService.js +0 -13
  32. package/src/utils/__tests__/nqlSanitizer.test.js +38 -0
  33. package/src/utils/__tests__/urlValidator.test.js +137 -1
  34. package/src/utils/nqlSanitizer.js +11 -0
  35. package/src/utils/urlValidator.js +25 -2
@@ -403,7 +403,7 @@ describe('ghostServiceImproved - Tags', () => {
403
403
  });
404
404
 
405
405
  describe('updateTag', () => {
406
- it('should update tag with valid ID and data', async () => {
406
+ it('should send only update fields, not the full existing tag', async () => {
407
407
  const tagId = 'tag-1';
408
408
  const updateData = {
409
409
  name: 'Updated JavaScript',
@@ -414,6 +414,8 @@ describe('ghostServiceImproved - Tags', () => {
414
414
  id: tagId,
415
415
  name: 'JavaScript',
416
416
  slug: 'javascript',
417
+ url: 'https://example.com/tag/javascript',
418
+ visibility: 'public',
417
419
  };
418
420
 
419
421
  const mockUpdatedTag = {
@@ -427,7 +429,16 @@ describe('ghostServiceImproved - Tags', () => {
427
429
  const result = await updateTag(tagId, updateData);
428
430
 
429
431
  expect(api.tags.read).toHaveBeenCalled();
430
- expect(api.tags.edit).toHaveBeenCalled();
432
+ // Should send ONLY updateData, NOT the full existing tag
433
+ expect(api.tags.edit).toHaveBeenCalledWith(
434
+ { id: tagId, name: 'Updated JavaScript', description: 'Updated description' },
435
+ {}
436
+ );
437
+ // Verify read-only fields are NOT sent
438
+ const editCallData = api.tags.edit.mock.calls[0][0];
439
+ expect(editCallData).not.toHaveProperty('url');
440
+ expect(editCallData).not.toHaveProperty('visibility');
441
+ expect(editCallData).not.toHaveProperty('slug');
431
442
  expect(result).toEqual(mockUpdatedTag);
432
443
  });
433
444
 
@@ -270,14 +270,7 @@ describe('ghostServiceImproved - Tiers', () => {
270
270
 
271
271
  const result = await getTier('tier-1');
272
272
 
273
- expect(api.tiers.read).toHaveBeenCalledWith(
274
- expect.objectContaining({
275
- id: 'tier-1',
276
- }),
277
- expect.objectContaining({
278
- id: 'tier-1',
279
- })
280
- );
273
+ expect(api.tiers.read).toHaveBeenCalledWith({}, { id: 'tier-1' });
281
274
  expect(result).toEqual(mockTier);
282
275
  });
283
276
 
@@ -300,12 +293,15 @@ describe('ghostServiceImproved - Tiers', () => {
300
293
  });
301
294
 
302
295
  describe('updateTier', () => {
303
- it('should update a tier', async () => {
296
+ it('should send only update fields and updated_at, not the full existing tier', async () => {
304
297
  const existingTier = {
305
298
  id: 'tier-1',
299
+ slug: 'premium',
306
300
  name: 'Premium',
307
301
  currency: 'USD',
308
302
  monthly_price: 999,
303
+ type: 'paid',
304
+ active: true,
309
305
  updated_at: '2024-01-01T00:00:00.000Z',
310
306
  };
311
307
 
@@ -324,19 +320,22 @@ describe('ghostServiceImproved - Tiers', () => {
324
320
 
325
321
  const result = await updateTier('tier-1', updateData);
326
322
 
327
- expect(api.tiers.read).toHaveBeenCalledWith(
328
- expect.objectContaining({ id: 'tier-1' }),
329
- expect.objectContaining({ id: 'tier-1' })
330
- );
323
+ expect(api.tiers.read).toHaveBeenCalledWith({}, { id: 'tier-1' });
324
+ // Should send ONLY updateData + updated_at, NOT the full existing tier
331
325
  expect(api.tiers.edit).toHaveBeenCalledWith(
332
- expect.objectContaining({
333
- ...existingTier,
334
- ...updateData,
335
- }),
336
- expect.objectContaining({
326
+ {
337
327
  id: 'tier-1',
338
- })
328
+ name: 'Premium Plus',
329
+ monthly_price: 1299,
330
+ updated_at: '2024-01-01T00:00:00.000Z',
331
+ },
332
+ {}
339
333
  );
334
+ // Verify read-only fields are NOT sent
335
+ const editCallData = api.tiers.edit.mock.calls[0][0];
336
+ expect(editCallData).not.toHaveProperty('slug');
337
+ expect(editCallData).not.toHaveProperty('type');
338
+ expect(editCallData).not.toHaveProperty('active');
340
339
  expect(result).toEqual(mockUpdatedTier);
341
340
  });
342
341
 
@@ -6,7 +6,6 @@ import {
6
6
  validateMemberLookup,
7
7
  validateSearchQuery,
8
8
  validateSearchOptions,
9
- sanitizeNqlValue,
10
9
  } from '../memberService.js';
11
10
 
12
11
  describe('memberService - Validation', () => {
@@ -447,31 +446,4 @@ describe('memberService - Validation', () => {
447
446
  );
448
447
  });
449
448
  });
450
-
451
- describe('sanitizeNqlValue', () => {
452
- it('should escape backslashes', () => {
453
- expect(sanitizeNqlValue('test\\value')).toBe('test\\\\value');
454
- });
455
-
456
- it('should escape single quotes', () => {
457
- expect(sanitizeNqlValue("test'value")).toBe("test\\'value");
458
- });
459
-
460
- it('should escape double quotes', () => {
461
- expect(sanitizeNqlValue('test"value')).toBe('test\\"value');
462
- });
463
-
464
- it('should handle multiple special characters', () => {
465
- expect(sanitizeNqlValue('test\'value"with\\chars')).toBe('test\\\'value\\"with\\\\chars');
466
- });
467
-
468
- it('should not modify strings without special characters', () => {
469
- expect(sanitizeNqlValue('normalvalue')).toBe('normalvalue');
470
- expect(sanitizeNqlValue('test@example.com')).toBe('test@example.com');
471
- });
472
-
473
- it('should handle empty string', () => {
474
- expect(sanitizeNqlValue('')).toBe('');
475
- });
476
- });
477
449
  });
@@ -3,7 +3,6 @@ import {
3
3
  validateTierData,
4
4
  validateTierUpdateData,
5
5
  validateTierQueryOptions,
6
- sanitizeNqlValue,
7
6
  } from '../tierService.js';
8
7
  import { ValidationError } from '../../errors/index.js';
9
8
 
@@ -342,31 +341,4 @@ describe('tierService - Validation', () => {
342
341
  ).not.toThrow();
343
342
  });
344
343
  });
345
-
346
- describe('sanitizeNqlValue', () => {
347
- it('should return value if undefined or null', () => {
348
- expect(sanitizeNqlValue(null)).toBe(null);
349
- expect(sanitizeNqlValue(undefined)).toBe(undefined);
350
- });
351
-
352
- it('should escape backslashes', () => {
353
- expect(sanitizeNqlValue('test\\value')).toBe('test\\\\value');
354
- });
355
-
356
- it('should escape single quotes', () => {
357
- expect(sanitizeNqlValue("test'value")).toBe("test\\'value");
358
- });
359
-
360
- it('should escape double quotes', () => {
361
- expect(sanitizeNqlValue('test"value')).toBe('test\\"value');
362
- });
363
-
364
- it('should escape multiple special characters', () => {
365
- expect(sanitizeNqlValue('test\\value"with\'quotes')).toBe('test\\\\value\\"with\\\'quotes');
366
- });
367
-
368
- it('should handle strings without special characters', () => {
369
- expect(sanitizeNqlValue('simple-value')).toBe('simple-value');
370
- });
371
- });
372
344
  });