├── .gitignore ├── .npmignore ├── wp-sites-example.json ├── package.json ├── smithery.yaml ├── Dockerfile ├── LICENSE ├── dist └── index.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | *.tgz -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | *.tgz -------------------------------------------------------------------------------- /wp-sites-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "wordpress": { 4 | "command": "node", 5 | "args": ["C:/Users/user/mcp-servers/server-wp-mcp-dev/dist/index.js"], 6 | "env": { 7 | "WP_SITES_PATH": "C:/Users/user/mcp-servers/wp-sites.json" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server-wp-mcp", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "node dist/index.js" 8 | }, 9 | "keywords": ["modelcontextprotocol", "mcp", "server", "ai", "claude", "wordpress", "wp"], 10 | "author": "mzimmer", 11 | "license": "MIT", 12 | "description": "The WordPress MCP server enables AI assistants to interact with WordPress sites through a standardized interface. It handles authentication and provides a secure way to discover and interact with WordPress REST API endpoints.", 13 | "dependencies": { 14 | "@modelcontextprotocol/sdk": "^1.0.4", 15 | "axios": "^1.7.9" 16 | } 17 | } -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - wpSitesPath 10 | properties: 11 | wpSitesPath: 12 | type: string 13 | description: Absolute path to the wp-sites.json configuration file containing 14 | WordPress site details. 15 | commandFunction: 16 | # A function that produces the CLI command to start the MCP on stdio. 17 | |- 18 | config => ({command: 'node', args: ['dist/index.js'], env: {WP_SITES_PATH: config.wpSitesPath}}) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Use the official Node.js image 3 | FROM node:18-alpine 4 | 5 | # Create and change to the app directory 6 | WORKDIR /usr/src/app 7 | 8 | # Install server dependencies 9 | COPY package.json /usr/src/app/ 10 | COPY package-lock.json /usr/src/app/ 11 | RUN npm install --omit=dev 12 | 13 | # Copy the server code 14 | COPY dist /usr/src/app/dist 15 | 16 | # Set the environment variable for WordPress site configuration 17 | # This is an example path; ensure the actual path is correctly set when running the container 18 | ENV WP_SITES_PATH=/path/to/wp-sites.json 19 | 20 | # Expose the port the app runs on (example port, change as needed) 21 | EXPOSE 3000 22 | 23 | # Command to run the app 24 | CMD ["node", "dist/index.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 emzimmer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { ErrorCode, McpError, ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"; 5 | import axios from 'axios'; 6 | import fs from 'fs/promises'; 7 | 8 | // Load site config from config file 9 | async function loadSiteConfig() { 10 | const configPath = process.env.WP_SITES_PATH; 11 | if (!configPath) { 12 | throw new Error("WP_SITES_PATH environment variable is required"); 13 | } 14 | 15 | try { 16 | const configData = await fs.readFile(configPath, 'utf8'); 17 | const config = JSON.parse(configData); 18 | 19 | // Validate and normalize the config 20 | const normalizedConfig = {}; 21 | for (const [alias, site] of Object.entries(config)) { 22 | if (!site.URL || !site.USER || !site.PASS) { 23 | console.error(`Invalid configuration for site ${alias}: missing required fields`); 24 | continue; 25 | } 26 | 27 | normalizedConfig[alias.toLowerCase()] = { 28 | url: site.URL.replace(/\/$/, ''), 29 | username: site.USER, 30 | auth: site.PASS 31 | }; 32 | } 33 | 34 | return normalizedConfig; 35 | } catch (error) { 36 | if (error.code === 'ENOENT') { 37 | throw new Error(`Config file not found at: ${configPath}`); 38 | } 39 | throw new Error(`Failed to load config: ${error.message}`); 40 | } 41 | } 42 | 43 | // WordPress client class 44 | class WordPressClient { 45 | constructor(site) { 46 | const config = { 47 | baseURL: `${site.url}/wp-json`, 48 | headers: { 49 | 'Content-Type': 'application/json', 50 | 'Accept': 'application/json' 51 | } 52 | }; 53 | 54 | if (site.auth) { 55 | const credentials = `${site.username}:${site.auth.replace(/\s+/g, '')}`; 56 | config.headers['Authorization'] = `Basic ${Buffer.from(credentials).toString('base64')}`; 57 | } 58 | 59 | this.client = axios.create(config); 60 | } 61 | 62 | async discoverEndpoints() { 63 | const response = await this.client.get('/'); 64 | const routes = response.data?.routes ?? {}; 65 | return Object.entries(routes).map(([path, info]) => ({ 66 | methods: info.methods ?? [], 67 | namespace: info.namespace ?? 'wp/v2', 68 | endpoints: [path] 69 | })); 70 | } 71 | 72 | async makeRequest(endpoint, method = 'GET', params) { 73 | const path = endpoint.replace(/^\/wp-json/, '').replace(/^\/?/, '/'); 74 | const config = { method, url: path }; 75 | 76 | if (method === 'GET' && params) { 77 | config.params = params; 78 | } else if (params) { 79 | config.data = params; 80 | } 81 | 82 | const response = await this.client.request(config); 83 | return response.data; 84 | } 85 | } 86 | 87 | // Start the server 88 | async function main() { 89 | try { 90 | // Load configuration 91 | const siteConfig = await loadSiteConfig(); 92 | const clients = new Map(); 93 | 94 | for (const [alias, site] of Object.entries(siteConfig)) { 95 | clients.set(alias, new WordPressClient(site)); 96 | } 97 | 98 | // Initialize server 99 | const server = new Server({ 100 | name: "server-wp-mcp", 101 | version: "1.0.0" 102 | }, { 103 | capabilities: { tools: {} } 104 | }); 105 | 106 | // Tool definitions 107 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 108 | tools: [{ 109 | name: "wp_discover_endpoints", 110 | description: "The discovery operation maps all available REST API endpoints on a WordPress site and returns their methods and namespaces. This allows you to understand what operations are possible on a target WordPress site without having to manually specify endpoints, which is important because different WordPress websites can have many different and varying endpoints.", 111 | inputSchema: { 112 | type: "object", 113 | properties: { 114 | site: { type: "string", description: "Site alias" } 115 | }, 116 | required: ["site"] 117 | } 118 | }, { 119 | name: "wp_call_endpoint", 120 | description: "The call operation executes specific REST API requests to the target WordPress sites using provided parameters and authentication. It handles both read and write operations. It determines which endpoint to use after the discovery operation is conducted.", 121 | inputSchema: { 122 | type: "object", 123 | properties: { 124 | site: { type: "string" }, 125 | endpoint: { type: "string" }, 126 | method: { type: "string", enum: ["GET", "POST", "PUT", "DELETE", "PATCH"] }, 127 | params: { type: "object" } 128 | }, 129 | required: ["site", "endpoint"] 130 | } 131 | }] 132 | })); 133 | 134 | // Tool handlers 135 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 136 | const { name, arguments: args } = request.params; 137 | 138 | switch (name) { 139 | case "wp_discover_endpoints": { 140 | const client = clients.get(args.site.toLowerCase()); 141 | if (!client) throw new McpError(ErrorCode.InvalidParams, `Unknown site: ${args.site}`); 142 | const endpoints = await client.discoverEndpoints(); 143 | return { content: [{ type: "text", text: JSON.stringify(endpoints, null, 2) }] }; 144 | } 145 | case "wp_call_endpoint": { 146 | const client = clients.get(args.site.toLowerCase()); 147 | if (!client) throw new McpError(ErrorCode.InvalidParams, `Unknown site: ${args.site}`); 148 | const result = await client.makeRequest(args.endpoint, args.method, args.params); 149 | return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; 150 | } 151 | default: 152 | throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); 153 | } 154 | }); 155 | 156 | // Start server 157 | const transport = new StdioServerTransport(); 158 | await server.connect(transport); 159 | 160 | console.error(`WordPress MCP server started with ${clients.size} site(s) configured`); 161 | } catch (error) { 162 | console.error(`Server failed to start: ${error.message}`); 163 | process.exit(1); 164 | } 165 | } 166 | 167 | main(); -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # WordPress MCP Server 2 | [![smithery badge](https://smithery.ai/badge/server-wp-mcp)](https://smithery.ai/server/server-wp-mcp) 3 | 4 | A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that enables AI assistants to interact with WordPress sites through the WordPress REST API. Supports multiple WordPress sites with secure authentication, enabling content management, post operations, and site configuration through natural language. 5 | 6 | ## Features 7 | 8 | - **Multi-Site Support**: Connect to multiple WordPress sites simultaneously 9 | - **REST API Integration**: Full access to WordPress REST API endpoints 10 | - **Secure Authentication**: Uses application passwords for secure API access 11 | - **Dynamic Endpoint Discovery**: Automatically maps available endpoints for each site 12 | - **Flexible Operations**: Support for GET, POST, PUT, DELETE, and PATCH methods 13 | - **Error Handling**: Graceful error handling with meaningful messages 14 | - **Simple Configuration**: Easy-to-maintain JSON configuration file 15 | 16 | ## Installation 17 | 18 | ### Installing via Smithery 19 | 20 | To install WordPress Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/server-wp-mcp): 21 | 22 | ```bash 23 | npx -y @smithery/cli install server-wp-mcp --client claude 24 | ``` 25 | 26 | ### Manual Installation 27 | ```bash 28 | npm install server-wp-mcp 29 | ``` 30 | 31 | ## Tools Reference 32 | 33 | ### `wp_discover_endpoints` 34 | Maps all available REST API endpoints on a WordPress site. 35 | 36 | **Arguments:** 37 | ```json 38 | { 39 | "site": { 40 | "type": "string", 41 | "description": "Site alias (as defined in configuration)", 42 | "required": true 43 | } 44 | } 45 | ``` 46 | 47 | **Returns:** 48 | List of available endpoints with their methods and namespaces. 49 | 50 | ### `wp_call_endpoint` 51 | Executes REST API requests to WordPress sites. 52 | 53 | **Arguments:** 54 | ```json 55 | { 56 | "site": { 57 | "type": "string", 58 | "description": "Site alias", 59 | "required": true 60 | }, 61 | "endpoint": { 62 | "type": "string", 63 | "description": "API endpoint path", 64 | "required": true 65 | }, 66 | "method": { 67 | "type": "string", 68 | "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"], 69 | "description": "HTTP method", 70 | "default": "GET" 71 | }, 72 | "params": { 73 | "type": "object", 74 | "description": "Request parameters or body data", 75 | "required": false 76 | } 77 | } 78 | ``` 79 | 80 | ## Configuration 81 | 82 | ### Getting an Application Password 83 | 84 | 1. Log in to your WordPress admin dashboard 85 | 2. Go to Users → Profile 86 | 3. Scroll to the "Application Passwords" section 87 | 4. Enter a name for the application (e.g., "MCP Server") 88 | 5. Click "Add New Application Password" 89 | 6. Copy the generated password (you won't be able to see it again) 90 | 91 | Note: Application Passwords require WordPress 5.6 or later and HTTPS. 92 | 93 | ### Configuration File Setup 94 | 95 | Create a JSON configuration file (e.g., `wp-sites.json`) with your WordPress site details: 96 | 97 | ```json 98 | { 99 | "myblog": { 100 | "URL": "https://myblog.com", 101 | "USER": "yourusername", 102 | "PASS": "abcd 1234 efgh 5678" 103 | }, 104 | "testsite": { 105 | "URL": "https://test.example.com", 106 | "USER": "anotherusername", 107 | "PASS": "wxyz 9876 lmno 5432" 108 | } 109 | } 110 | ``` 111 | 112 | Each site configuration requires: 113 | - `URL`: WordPress site URL (must include http:// or https://) 114 | - `USER`: WordPress username 115 | - `PASS`: Application password (spaces will be automatically removed) 116 | 117 | The configuration key (e.g., "myblog", "testsite") becomes the site alias you'll use when interacting with the server. 118 | 119 | ### Usage with Claude Desktop 120 | 121 | Add to your `claude_desktop_config.json`: 122 | 123 | ```json 124 | { 125 | "mcpServers": { 126 | "wordpress": { 127 | "command": "node", 128 | "args": ["path/to/server/dist/index.js"], 129 | "env": { 130 | "WP_SITES_PATH": "/absolute/path/to/wp-sites.json" 131 | } 132 | } 133 | } 134 | } 135 | ``` 136 | 137 | The `WP_SITES_PATH` environment variable must point to the absolute path of your configuration file. 138 | 139 | ### Example Usage 140 | 141 | Once configured, you can ask Claude to perform various WordPress operations: 142 | 143 | #### List and Query Posts 144 | ``` 145 | Can you show me all posts from myblog published in the last month? 146 | ``` 147 | ``` 148 | Find all posts on testsite tagged with "technology" and "AI" 149 | ``` 150 | ``` 151 | Show me draft posts from myblog that need review 152 | ``` 153 | 154 | #### Create and Edit Content 155 | ``` 156 | Create a new draft post on testsite titled "The Future of AI" with these key points: [points] 157 | ``` 158 | ``` 159 | Update the featured image on myblog's latest post about machine learning 160 | ``` 161 | ``` 162 | Add a new category called "Tech News" to myblog 163 | ``` 164 | 165 | #### Manage Comments 166 | ``` 167 | Show me all pending comments on myblog's latest post 168 | ``` 169 | ``` 170 | Find comments from testsite that might be spam 171 | ``` 172 | ``` 173 | List the most engaged commenters on myblog 174 | ``` 175 | 176 | #### Plugin Management 177 | ``` 178 | What plugins are currently active on myblog? 179 | ``` 180 | ``` 181 | Check if any plugins on testsite need updates 182 | ``` 183 | ``` 184 | Tell me about the security plugins installed on myblog 185 | ``` 186 | 187 | #### User Management 188 | ``` 189 | Show me all users with editor role on testsite 190 | ``` 191 | ``` 192 | Create a new author account on myblog 193 | ``` 194 | ``` 195 | Update user roles and permissions on testsite 196 | ``` 197 | 198 | #### Site Settings and Configuration 199 | ``` 200 | What theme is currently active on myblog? 201 | ``` 202 | ``` 203 | Check the permalink structure on testsite 204 | ``` 205 | ``` 206 | Show me the current media library settings on myblog 207 | ``` 208 | 209 | #### Maintenance and Diagnostics 210 | ``` 211 | Check if there are any broken links on myblog 212 | ``` 213 | ``` 214 | Show me the PHP version and other system info for testsite 215 | ``` 216 | ``` 217 | List any pending database updates on myblog 218 | ``` 219 | 220 | ## Error Handling 221 | 222 | The server handles common errors including: 223 | - Invalid configuration file path or format 224 | - Invalid site configurations 225 | - Authentication failures 226 | - Missing or invalid endpoints 227 | - API rate limiting 228 | - Network errors 229 | 230 | All errors are returned with descriptive messages to help diagnose issues. 231 | 232 | ## Security Considerations 233 | 234 | - Keep your `wp-sites.json` file secure and never commit it to version control 235 | - Consider using environment variables for sensitive data in production 236 | - Store the config file outside of public directories 237 | - Use HTTPS for all WordPress sites 238 | - Regularly rotate application passwords 239 | - Follow the principle of least privilege when assigning user roles 240 | 241 | ## Dependencies 242 | - @modelcontextprotocol/sdk - MCP protocol implementation 243 | - axios - HTTP client for API requests 244 | 245 | ## License 246 | MIT 247 | --------------------------------------------------------------------------------