@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 +70 -20
- package/package.json +1 -1
- package/src/__tests__/mcp_server_improved.test.js +153 -0
- package/src/__tests__/mcp_server_pages.test.js +520 -0
- package/src/mcp_server_improved.js +319 -1
- package/src/services/__tests__/ghostServiceImproved.pages.test.js +561 -0
- package/src/services/__tests__/pageService.test.js +400 -0
- package/src/services/ghostServiceImproved.js +289 -0
- package/src/services/pageService.js +121 -0
package/README.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# Ghost MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/js/%40jgardner04%2Fghost-mcp-server)
|
|
4
|
+
[](https://github.com/jgardner04/Ghost-MCP-Server/actions)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](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
|
|
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
|
|
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
|
|
71
|
-
cd
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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
|
@@ -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
|
+
});
|