@jgardner04/ghost-mcp-server 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # Ghost MCP Server
2
2
 
3
+ [![npm version](https://badge.fury.io/js/%40jgardner04%2Fghost-mcp-server.svg)](https://badge.fury.io/js/%40jgardner04%2Fghost-mcp-server)
4
+ [![CI](https://github.com/jgardner04/Ghost-MCP-Server/actions/workflows/ci.yml/badge.svg)](https://github.com/jgardner04/Ghost-MCP-Server/actions)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Coverage](https://img.shields.io/badge/coverage-90%25-brightgreen.svg)](https://github.com/jgardner04/Ghost-MCP-Server)
7
+
3
8
  This project (`ghost-mcp-server`) implements a **Model Context Protocol (MCP) Server** that allows an MCP client (like Cursor or Claude Desktop) to interact with a Ghost CMS instance via defined tools.
4
9
 
5
10
  ## Requirements
6
11
 
7
- - Node.js 14.0.0 or higher
12
+ - Node.js 18.0.0 or higher
8
13
  - Ghost Admin API URL and Key
9
14
 
10
15
  ## Ghost MCP Server Details
@@ -62,13 +67,70 @@ Below is a guide for using the available MCP tools:
62
67
  - `meta_description` (string, optional): Custom SEO description.
63
68
  - **Output**: The created `ghost/post` resource.
64
69
 
65
- ## Installation and Running
70
+ ## Installation
71
+
72
+ ### NPM Installation (Recommended)
73
+
74
+ Install globally using npm:
75
+
76
+ ```bash
77
+ npm install -g @jgardner04/ghost-mcp-server
78
+ ```
79
+
80
+ Or use npx to run without installing:
81
+
82
+ ```bash
83
+ npx @jgardner04/ghost-mcp-server
84
+ ```
85
+
86
+ ### Available Commands
87
+
88
+ After installation, the following CLI commands are available:
89
+
90
+ - **`ghost-mcp-server`**: Starts the Express REST API server and MCP server (default)
91
+ - **`ghost-mcp`**: Starts the improved MCP server with transport configuration support
92
+
93
+ ### Configuration
94
+
95
+ Before running the server, configure your Ghost Admin API credentials:
96
+
97
+ 1. Create a `.env` file in your working directory:
98
+
99
+ ```dotenv
100
+ # Required:
101
+ GHOST_ADMIN_API_URL=https://your-ghost-site.com
102
+ GHOST_ADMIN_API_KEY=your_admin_api_key
103
+ ```
104
+
105
+ 2. Find your Ghost Admin API URL and Key in your Ghost Admin settings under Integrations -> Custom Integrations.
106
+
107
+ ### Running the Server
108
+
109
+ After installation and configuration:
110
+
111
+ ```bash
112
+ # Using the global installation
113
+ ghost-mcp-server
114
+
115
+ # Or using npx
116
+ npx @jgardner04/ghost-mcp-server
117
+
118
+ # Run the MCP server with transport options
119
+ ghost-mcp
120
+
121
+ # Or with specific transport
122
+ MCP_TRANSPORT=stdio ghost-mcp
123
+ ```
124
+
125
+ ## Development Setup
126
+
127
+ For contributors or advanced users who want to modify the source code:
66
128
 
67
129
  1. **Clone the Repository**:
68
130
 
69
131
  ```bash
70
- git clone <repository_url>
71
- cd ghost-mcp-server
132
+ git clone https://github.com/jgardner04/Ghost-MCP-Server.git
133
+ cd Ghost-MCP-Server
72
134
  ```
73
135
 
74
136
  2. **Install Dependencies**:
@@ -78,20 +140,9 @@ Below is a guide for using the available MCP tools:
78
140
  ```
79
141
 
80
142
  3. **Configure Environment Variables**:
81
- Create a `.env` file in the project root and add your Ghost Admin API credentials:
82
-
83
- ```dotenv
84
- # Required:
85
- GHOST_ADMIN_API_URL=https://your-ghost-site.com
86
- GHOST_ADMIN_API_KEY=your_admin_api_key
87
-
88
- # If using 1Password CLI for secrets:
89
- # You might store the API key in 1Password and use `op run --env-file=.env -- ...`
90
- ```
91
-
92
- - Find your Ghost Admin API URL and Key in your Ghost Admin settings under Integrations -> Custom Integrations.
143
+ Create a `.env` file in the project root (see Configuration section above).
93
144
 
94
- 4. **Run the Server**:
145
+ 4. **Run from Source**:
95
146
 
96
147
  ```bash
97
148
  npm start
@@ -99,8 +150,6 @@ Below is a guide for using the available MCP tools:
99
150
  # node src/index.js
100
151
  ```
101
152
 
102
- This command will start the MCP server.
103
-
104
153
  5. **Development Mode (using nodemon)**:
105
154
  For development with automatic restarting:
106
155
  ```bash
@@ -112,4 +161,5 @@ Below is a guide for using the available MCP tools:
112
161
  - **401 Unauthorized Error from Ghost:** Check that your `GHOST_ADMIN_API_URL` and `GHOST_ADMIN_API_KEY` in the `.env` file are correct and that the Custom Integration in Ghost is enabled.
113
162
  - **MCP Server Connection Issues:** Ensure the MCP server is running (check console logs). Verify the port (`MCP_PORT`, default 3001) is not blocked by a firewall. Check that the client is connecting to the correct address and port.
114
163
  - **Tool Execution Errors:** Check the server console logs for detailed error messages from the specific tool implementation (e.g., `ghost_create_post`, `ghost_upload_image`). Common issues include invalid input (check against tool schemas in `src/mcp_server.js` and the README guide), problems downloading from `imageUrl`, image processing failures, or upstream errors from the Ghost API.
115
- - **Dependency Installation Issues:** Ensure you have a compatible Node.js version installed (see Requirements section). Try removing `node_modules` and `package-lock.json` and running `npm install` again.
164
+ - **Command Not Found:** If `ghost-mcp-server` or `ghost-mcp` commands are not found after global installation, ensure npm's global bin directory is in your PATH. You can find it with `npm bin -g`.
165
+ - **Dependency Installation Issues:** Ensure you have a compatible Node.js version installed (Node.js 18.0.0 or higher - see Requirements section). For global installation issues, try `npm install -g @jgardner04/ghost-mcp-server --force`. For development setup, try removing `node_modules` and `package-lock.json` and running `npm install` again.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgardner04/ghost-mcp-server",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "A Model Context Protocol (MCP) server for interacting with Ghost CMS via the Admin API",
5
5
  "author": "Jonathan Gardner",
6
6
  "type": "module",
@@ -49,6 +49,7 @@ const mockValidateImageUrl = vi.fn();
49
49
  const mockCreateSecureAxiosConfig = vi.fn();
50
50
  const mockUpdatePost = vi.fn();
51
51
  const mockDeletePost = vi.fn();
52
+ const mockSearchPosts = vi.fn();
52
53
 
53
54
  vi.mock('../services/ghostService.js', () => ({
54
55
  getPosts: (...args) => mockGetPosts(...args),
@@ -65,6 +66,7 @@ vi.mock('../services/postService.js', () => ({
65
66
  vi.mock('../services/ghostServiceImproved.js', () => ({
66
67
  updatePost: (...args) => mockUpdatePost(...args),
67
68
  deletePost: (...args) => mockDeletePost(...args),
69
+ searchPosts: (...args) => mockSearchPosts(...args),
68
70
  }));
69
71
 
70
72
  vi.mock('../services/imageProcessingService.js', () => ({
@@ -754,3 +756,154 @@ describe('mcp_server_improved - ghost_delete_post tool', () => {
754
756
  expect(result.content[0].text).toContain('Network error');
755
757
  });
756
758
  });
759
+
760
+ describe('mcp_server_improved - ghost_search_posts tool', () => {
761
+ beforeEach(async () => {
762
+ vi.clearAllMocks();
763
+ // Don't clear mockTools - they're registered once on module load
764
+ if (mockTools.size === 0) {
765
+ await import('../mcp_server_improved.js');
766
+ }
767
+ });
768
+
769
+ it('should register ghost_search_posts tool', () => {
770
+ expect(mockTools.has('ghost_search_posts')).toBe(true);
771
+ });
772
+
773
+ it('should have correct schema with required query and optional parameters', () => {
774
+ const tool = mockTools.get('ghost_search_posts');
775
+ expect(tool).toBeDefined();
776
+ expect(tool.description).toContain('Search');
777
+ expect(tool.schema).toBeDefined();
778
+ expect(tool.schema.query).toBeDefined();
779
+ expect(tool.schema.status).toBeDefined();
780
+ expect(tool.schema.limit).toBeDefined();
781
+ });
782
+
783
+ it('should search posts with query only', async () => {
784
+ const mockPosts = [
785
+ { id: '1', title: 'JavaScript Tips', slug: 'javascript-tips', status: 'published' },
786
+ { id: '2', title: 'JavaScript Tricks', slug: 'javascript-tricks', status: 'published' },
787
+ ];
788
+ mockSearchPosts.mockResolvedValue(mockPosts);
789
+
790
+ const tool = mockTools.get('ghost_search_posts');
791
+ const result = await tool.handler({ query: 'JavaScript' });
792
+
793
+ expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', {});
794
+ expect(result.content[0].text).toContain('JavaScript Tips');
795
+ expect(result.content[0].text).toContain('JavaScript Tricks');
796
+ });
797
+
798
+ it('should search posts with query and status filter', async () => {
799
+ const mockPosts = [
800
+ { id: '1', title: 'Published Post', slug: 'published-post', status: 'published' },
801
+ ];
802
+ mockSearchPosts.mockResolvedValue(mockPosts);
803
+
804
+ const tool = mockTools.get('ghost_search_posts');
805
+ await tool.handler({ query: 'test', status: 'published' });
806
+
807
+ expect(mockSearchPosts).toHaveBeenCalledWith('test', { status: 'published' });
808
+ });
809
+
810
+ it('should search posts with query and limit', async () => {
811
+ const mockPosts = [{ id: '1', title: 'Test Post', slug: 'test-post' }];
812
+ mockSearchPosts.mockResolvedValue(mockPosts);
813
+
814
+ const tool = mockTools.get('ghost_search_posts');
815
+ await tool.handler({ query: 'test', limit: 10 });
816
+
817
+ expect(mockSearchPosts).toHaveBeenCalledWith('test', { limit: 10 });
818
+ });
819
+
820
+ it('should validate limit is between 1 and 50', () => {
821
+ const tool = mockTools.get('ghost_search_posts');
822
+ const schema = tool.schema;
823
+
824
+ expect(schema.limit).toBeDefined();
825
+ expect(() => schema.limit.parse(0)).toThrow();
826
+ expect(() => schema.limit.parse(51)).toThrow();
827
+ expect(schema.limit.parse(25)).toBe(25);
828
+ });
829
+
830
+ it('should validate status enum values', () => {
831
+ const tool = mockTools.get('ghost_search_posts');
832
+ const schema = tool.schema;
833
+
834
+ expect(schema.status).toBeDefined();
835
+ expect(() => schema.status.parse('invalid')).toThrow();
836
+ expect(schema.status.parse('published')).toBe('published');
837
+ expect(schema.status.parse('draft')).toBe('draft');
838
+ expect(schema.status.parse('scheduled')).toBe('scheduled');
839
+ expect(schema.status.parse('all')).toBe('all');
840
+ });
841
+
842
+ it('should pass all parameters combined', async () => {
843
+ const mockPosts = [{ id: '1', title: 'Test Post' }];
844
+ mockSearchPosts.mockResolvedValue(mockPosts);
845
+
846
+ const tool = mockTools.get('ghost_search_posts');
847
+ await tool.handler({
848
+ query: 'JavaScript',
849
+ status: 'published',
850
+ limit: 20,
851
+ });
852
+
853
+ expect(mockSearchPosts).toHaveBeenCalledWith('JavaScript', {
854
+ status: 'published',
855
+ limit: 20,
856
+ });
857
+ });
858
+
859
+ it('should handle errors from searchPosts', async () => {
860
+ mockSearchPosts.mockRejectedValue(new Error('Search query is required'));
861
+
862
+ const tool = mockTools.get('ghost_search_posts');
863
+ const result = await tool.handler({ query: '' });
864
+
865
+ expect(result.isError).toBe(true);
866
+ expect(result.content[0].text).toContain('Search query is required');
867
+ });
868
+
869
+ it('should handle Ghost API errors', async () => {
870
+ mockSearchPosts.mockRejectedValue(new Error('Ghost API error'));
871
+
872
+ const tool = mockTools.get('ghost_search_posts');
873
+ const result = await tool.handler({ query: 'test' });
874
+
875
+ expect(result.isError).toBe(true);
876
+ expect(result.content[0].text).toContain('Ghost API error');
877
+ });
878
+
879
+ it('should return formatted JSON response', async () => {
880
+ const mockPosts = [
881
+ {
882
+ id: '1',
883
+ title: 'Test Post',
884
+ slug: 'test-post',
885
+ html: '<p>Content</p>',
886
+ status: 'published',
887
+ },
888
+ ];
889
+ mockSearchPosts.mockResolvedValue(mockPosts);
890
+
891
+ const tool = mockTools.get('ghost_search_posts');
892
+ const result = await tool.handler({ query: 'Test' });
893
+
894
+ expect(result.content).toBeDefined();
895
+ expect(result.content[0].type).toBe('text');
896
+ expect(result.content[0].text).toContain('"id": "1"');
897
+ expect(result.content[0].text).toContain('"title": "Test Post"');
898
+ });
899
+
900
+ it('should return empty array when no results found', async () => {
901
+ mockSearchPosts.mockResolvedValue([]);
902
+
903
+ const tool = mockTools.get('ghost_search_posts');
904
+ const result = await tool.handler({ query: 'nonexistent' });
905
+
906
+ expect(result.content[0].text).toBe('[]');
907
+ expect(result.isError).toBeUndefined();
908
+ });
909
+ });