├── .env.example ├── .gitignore ├── README.md ├── claude_desktop_config.json.example ├── package.json ├── src ├── cli.ts ├── server.ts ├── tools │ ├── categories.ts │ ├── comments.ts │ ├── index.ts │ ├── media.ts │ ├── pages.ts │ ├── plugin-repository.ts │ ├── plugins.ts │ ├── posts.ts │ └── users.ts ├── types │ └── wordpress-types.ts └── wordpress.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # WordPress API configuration 2 | # The standard WordPress REST API endpoint 3 | WORDPRESS_API_URL=https://your-wordpress-site.com 4 | # For authentication using Application Passwords (WordPress 5.6+) 5 | WORDPRESS_USERNAME=your_username 6 | WORDPRESS_PASSWORD=your_app_password 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | build 4 | package-lock.json 5 | logs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress MCP Server 2 | 3 | This is a Model Context Protocol (MCP) server for WordPress, allowing you to interact with your WordPress site using natural language via an MCP-compatible client like Claude for Desktop. This server exposes various WordPress data and functionality as MCP tools. 4 | 5 | ## Usage 6 | 7 | ### Claude Desktop 8 | 9 | 1. Download and install [Claude Desktop](https://claude.ai/download). 10 | 2. Open Claude Desktop settings and navigate to the "Developer" tab. 11 | 3. Copy the contents of the `claude_desktop_config.json.example` file. 12 | 4. Click "Edit Config" to open the `claude_desktop_config.json` file. 13 | 5. Copy paste the contents of the example file into the config file. Make sure to replace the placeholder values with your actual values for the WordPress site. To generate the application keys, follow this guide - [Application Passwords](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide#Getting-Credentials). 14 | 6. Save the configuration. 15 | 7. Restart Claude Desktop. 16 | 17 | ## Features 18 | 19 | This server currently provides tools to interact with core WordPress data: 20 | 21 | * **Posts:** 22 | * `list_posts`: List all posts (supports pagination and searching). 23 | * `get_post`: Retrieve a specific post by ID. 24 | * `create_post`: Create a new post. 25 | * `update_post`: Update an existing post. 26 | * `delete_post`: Delete a post. 27 | * **Pages:** 28 | * `list_pages`: List all pages (supports pagination and filtering). 29 | * `get_page`: Retrieve a specific page by ID. 30 | * `create_page`: Create a new page. 31 | * `update_page`: Update an existing page. 32 | * `delete_page`: Delete a page. 33 | * **Media:** 34 | * `list_media`: List all media items (supports pagination and searching). 35 | * `get_media`: Retrieve a specific media item by ID. 36 | * `create_media`: Create a new media item from a URL. 37 | * `update_media`: Update an existing media item. 38 | * `delete_media`: Delete a media item. 39 | * **Users:** 40 | * `list_users`: List all users with filtering, sorting, and pagination options. 41 | * `get_user`: Retrieve a specific user by ID. 42 | * `create_user`: Create a new user. 43 | * `update_user`: Update an existing user. 44 | * `delete_user`: Delete a user. 45 | * **Categories:** 46 | * `list_categories`: List all categories with filtering, sorting, and pagination options. 47 | * `get_category`: Retrieve a specific category by ID. 48 | * `create_category`: Create a new category. 49 | * `update_category`: Update an existing category. 50 | * `delete_category`: Delete a category. 51 | * **Comments:** 52 | * `list_comments`: List all comments with filtering, sorting, and pagination options. 53 | * `get_comment`: Retrieve a specific comment by ID. 54 | * `create_comment`: Create a new comment. 55 | * `update_comment`: Update an existing comment. 56 | * `delete_comment`: Delete a comment. 57 | * **Plugins:** 58 | * `list_plugins`: List all plugins installed on the site. 59 | * `get_plugin`: Retrieve details about a specific plugin. 60 | * `activate_plugin`: Activate a plugin. 61 | * `deactivate_plugin`: Deactivate a plugin. 62 | * `create_plugin`: Create a new plugin. 63 | 64 | 65 | More features and endpoints will be added in future updates. 66 | 67 | ## Using with npx and .env file 68 | 69 | You can run this MCP server directly using npx without installing it globally: 70 | 71 | ```bash 72 | npx -y @instawp/mcp-wp 73 | ``` 74 | 75 | Make sure you have a `.env` file in your current directory with the following variables: 76 | 77 | ```env 78 | WORDPRESS_API_URL=https://your-wordpress-site.com 79 | WORDPRESS_USERNAME=wp_username 80 | WORDPRESS_PASSWORD=wp_app_password 81 | ``` 82 | 83 | ## Development 84 | 85 | ### Prerequisites 86 | 87 | * **Node.js and npm:** Ensure you have Node.js (version 18 or higher) and npm installed. 88 | * **WordPress Site:** You need an active WordPress site with the REST API enabled. 89 | * **WordPress API Authentication:** Set up authentication for the WordPress REST API. This typically requires an authentication plugin or method (like Application Passwords). 90 | * **MCP Client:** You need an application that can communicate with the MCP Server. Currently, Claude Desktop is recommended. 91 | 92 | ### Installation and Setup 93 | 94 | 1. **Clone the Repository:** 95 | 96 | ```bash 97 | git clone 98 | cd wordpress-mcp-server 99 | ``` 100 | 101 | 2. **Install Dependencies:** 102 | 103 | ```bash 104 | npm install 105 | ``` 106 | 107 | 3. **Create a `.env` file:** 108 | 109 | Create a `.env` file in the root of your project directory and add your WordPress API credentials: 110 | 111 | ```env 112 | WORDPRESS_API_URL=https://your-wordpress-site.com 113 | WORDPRESS_USERNAME=wp_username 114 | WORDPRESS_PASSWORD=wp_app_password 115 | ``` 116 | 117 | Replace the placeholders with your actual values. 118 | 119 | 4. **Build the Server:** 120 | 121 | ```bash 122 | npm run build 123 | ``` 124 | 125 | 5. **Configure Claude Desktop:** 126 | 127 | * Open Claude Desktop settings and navigate to the "Developer" tab. 128 | * Click "Edit Config" to open the `claude_desktop_config.json` file. 129 | * Add a new server configuration under the `mcpServers` section. You will need to provide the **absolute** path to the `build/server.js` file and your WordPress environment variables. 130 | * Save the configuration. 131 | 132 | ### Running the Server 133 | 134 | Once you've configured Claude Desktop, the server should start automatically whenever Claude Desktop starts. 135 | 136 | You can also run the server directly from the command line for testing: 137 | 138 | ```bash 139 | npm start 140 | ``` 141 | 142 | or in development mode: 143 | 144 | ```bash 145 | npm run dev 146 | ``` 147 | 148 | ### Security 149 | 150 | * **Never commit your API keys or secrets to version control.** 151 | * **Use HTTPS for communication between the client and server.** 152 | * **Validate all inputs received from the client to prevent injection attacks.** 153 | * **Implement proper error handling and rate limiting.** 154 | 155 | ## Project Overview 156 | 157 | ### MCP WordPress Tools 158 | 159 | Welcome to the MCP WordPress Tools project. This repository provides custom tools for managing WordPress functionalities, including media and plugins integration. 160 | 161 | ### Folder Structure 162 | 163 | ``` 164 | wp/ 165 | ├── README.md # This documentation file 166 | └── src/ 167 | └── tools/ 168 | ├── media.ts # Handles media operations 169 | └── plugins.ts # Handles plugin operations 170 | ``` 171 | 172 | ### Getting Started 173 | 174 | 1. Explore the source code under the `src/tools/` directory to review how media and plugin functionalities are implemented. 175 | 2. Update or extend functionalities as needed to integrate with your WordPress workflow. 176 | 177 | ### Contribution 178 | 179 | Feel free to open issues or make pull requests to improve this project. 180 | -------------------------------------------------------------------------------- /claude_desktop_config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "wordpress": { 4 | "command": "npx", 5 | "args": ["-y", "@instawp/mcp-wp"], 6 | "env": { 7 | "WORDPRESS_API_URL": "https://wpsite.instawp.xyz", 8 | "WORDPRESS_USERNAME": "username", 9 | "WORDPRESS_PASSWORD": "Application Password" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@instawp/mcp-wp", 3 | "version": "0.0.3", 4 | "description": "A Model Context Protocol server for interacting with WordPress.", 5 | "type": "module", 6 | "main": "./build/server.js", 7 | "exports": "./build/server.js", 8 | "bin": { 9 | "mcp-wp": "./build/server.js" 10 | }, 11 | "engines": { 12 | "node": ">=18.0.0" 13 | }, 14 | "scripts": { 15 | "build": "tsc --project tsconfig.json", 16 | "start": "node ./build/server.js", 17 | "dev": "tsx watch src/server.ts", 18 | "clean": "rimraf build", 19 | "prepare": "npm run build" 20 | }, 21 | "keywords": [ 22 | "wordpress", 23 | "mcp", 24 | "server", 25 | "claude", 26 | "ai", 27 | "instawp" 28 | ], 29 | "author": "Claude", 30 | "license": "GPL-3.0-or-later", 31 | "dependencies": { 32 | "@modelcontextprotocol/sdk": "^1.4.1", 33 | "axios": "^1.6.7", 34 | "dotenv": "^16.4.5", 35 | "fs-extra": "^11.2.0", 36 | "zod": "^3.23.8", 37 | "zod-to-json-schema": "^3.24.1" 38 | }, 39 | "devDependencies": { 40 | "@types/fs-extra": "^11.0.4", 41 | "@types/node": "^22.10.0", 42 | "rimraf": "^5.0.5", 43 | "tsx": "^4.7.1", 44 | "typescript": "^5.3.3" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | }, 49 | "files": [ 50 | "build", 51 | "README.md", 52 | "LICENSE" 53 | ] 54 | } -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // src/cli.ts 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import { spawn } from 'child_process'; 6 | 7 | // Function to check if required environment variables are set 8 | function checkEnvironmentVariables() { 9 | const requiredVars = ['WORDPRESS_API_URL', 'WORDPRESS_USERNAME', 'WORDPRESS_APP_PASSWORD']; 10 | const missingVars = requiredVars.filter(varName => !process.env[varName]); 11 | 12 | if (missingVars.length > 0) { 13 | console.error(`Missing required environment variables: ${missingVars.join(', ')}`); 14 | console.error('Please set these variables in your .env file or environment'); 15 | process.exit(1); 16 | } 17 | } 18 | 19 | // Main function to run the MCP server 20 | async function main() { 21 | console.log('Starting WordPress MCP Server...'); 22 | 23 | // Check for .env file in current directory 24 | const envPath = path.join(process.cwd(), '.env'); 25 | if (!fs.existsSync(envPath)) { 26 | console.warn('No .env file found in current directory.'); 27 | console.warn('Make sure your WordPress credentials are set in your environment variables.'); 28 | } 29 | 30 | checkEnvironmentVariables(); 31 | 32 | // Start the server 33 | const serverPath = path.join(__dirname, 'server.js'); 34 | const serverProcess = spawn('node', [serverPath], { 35 | stdio: 'inherit', 36 | env: process.env 37 | }); 38 | 39 | // Handle server process events 40 | serverProcess.on('close', (code) => { 41 | if (code !== 0) { 42 | console.error(`Server process exited with code ${code}`); 43 | process.exit(code || 1); 44 | } 45 | process.exit(0); 46 | }); 47 | 48 | // Forward termination signals to the child process 49 | ['SIGINT', 'SIGTERM'].forEach(signal => { 50 | process.on(signal, () => { 51 | // Fix: Convert string signal to number for kill method 52 | serverProcess.kill(); 53 | }); 54 | }); 55 | } 56 | 57 | // Run the main function 58 | main().catch(error => { 59 | console.error('Failed to start server:', error); 60 | process.exit(1); 61 | }); 62 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // src/server.ts 3 | import * as dotenv from 'dotenv'; 4 | dotenv.config(); // Load environment variables from .env first 5 | 6 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 7 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 8 | import { allTools, toolHandlers } from './tools/index.js'; 9 | import { z } from 'zod'; 10 | import { zodToJsonSchema } from 'zod-to-json-schema'; 11 | 12 | 13 | // Create MCP server instance 14 | const server = new McpServer({ 15 | name: "wordpress", 16 | version: "0.0.1" 17 | }, { 18 | capabilities: { 19 | tools: allTools.reduce((acc, tool) => { 20 | acc[tool.name] = tool; 21 | return acc; 22 | }, {} as Record) 23 | } 24 | }); 25 | 26 | // Register each tool from our tools list with its corresponding handler 27 | for (const tool of allTools) { 28 | const handler = toolHandlers[tool.name as keyof typeof toolHandlers]; 29 | if (!handler) continue; 30 | 31 | const wrappedHandler = async (args: any) => { 32 | // The handler functions are already typed with their specific parameter types 33 | const result = await handler(args); 34 | return { 35 | content: result.toolResult.content.map((item: { type: string; text: string }) => ({ 36 | ...item, 37 | type: "text" as const 38 | })), 39 | isError: result.toolResult.isError 40 | }; 41 | }; 42 | 43 | // console.log(`Registering tool: ${tool.name}`); 44 | // console.log(`Input schema: ${JSON.stringify(tool.inputSchema)}`); 45 | 46 | // const zodSchema = z.any().optional(); 47 | // const jsonSchema = zodToJsonSchema(z.object(tool.inputSchema.properties as z.ZodRawShape)); 48 | 49 | // const schema = z.object(tool.inputSchema as z.ZodRawShape).catchall(z.unknown()); 50 | 51 | // The inputSchema is already in JSON Schema format with properties 52 | // server.tool(tool.name, tool.inputSchema.shape, wrappedHandler); 53 | // const zodSchema = z.any().optional(); 54 | // const jsonSchema = zodToJsonSchema(z.object(tool.inputSchema.properties as z.ZodRawShape)); 55 | // const parsedSchema = z.any().optional().parse(jsonSchema); 56 | 57 | const zodSchema = z.object(tool.inputSchema.properties as z.ZodRawShape); 58 | server.tool(tool.name, zodSchema.shape, wrappedHandler) 59 | 60 | } 61 | 62 | async function main() { 63 | const { logToFile } = await import('./wordpress.js'); 64 | logToFile('Starting WordPress MCP server...'); 65 | 66 | if (!process.env.WORDPRESS_API_URL) { 67 | logToFile('Missing required environment variables. Please check your .env file.'); 68 | process.exit(1); 69 | } 70 | 71 | try { 72 | logToFile('Initializing WordPress client...'); 73 | const { initWordPress } = await import('./wordpress.js'); 74 | await initWordPress(); 75 | logToFile('WordPress client initialized successfully.'); 76 | 77 | logToFile('Setting up server transport...'); 78 | const transport = new StdioServerTransport(); 79 | await server.connect(transport); 80 | logToFile('WordPress MCP Server running on stdio'); 81 | } catch (error) { 82 | logToFile(`Failed to initialize server: ${error}`); 83 | process.exit(1); 84 | } 85 | } 86 | 87 | // Handle process signals and errors 88 | process.on('SIGTERM', () => { 89 | console.log('Received SIGTERM signal, shutting down...'); 90 | process.exit(0); 91 | }); 92 | process.on('SIGINT', () => { 93 | console.log('Received SIGINT signal, shutting down...'); 94 | process.exit(0); 95 | }); 96 | process.on('uncaughtException', (error) => { 97 | console.error('Uncaught exception:', error); 98 | process.exit(1); 99 | }); 100 | process.on('unhandledRejection', (error) => { 101 | console.error('Unhandled rejection:', error); 102 | process.exit(1); 103 | }); 104 | 105 | main().catch((error) => { 106 | console.error('Startup error:', error); 107 | process.exit(1); 108 | }); -------------------------------------------------------------------------------- /src/tools/categories.ts: -------------------------------------------------------------------------------- 1 | // src/tools/categories.ts 2 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 3 | import { makeWordPressRequest } from '../wordpress.js'; 4 | import { WPCategory } from '../types/wordpress-types.js'; 5 | import { z } from 'zod'; 6 | import { zodToJsonSchema } from 'zod-to-json-schema'; 7 | 8 | const listCategoriesSchema = z.object({ 9 | page: z.number().optional().describe("Page number (default 1)"), 10 | per_page: z.number().min(1).max(100).optional().describe("Items per page (default 10, max 100)"), 11 | search: z.string().optional().describe("Search term for category name"), 12 | parent: z.number().optional().describe("Parent category ID"), 13 | orderby: z.enum(['id', 'include', 'name', 'slug', 'count']).optional().describe("Sort categories by parameter"), 14 | order: z.enum(['asc', 'desc']).optional().describe("Order sort attribute ascending or descending"), 15 | hide_empty: z.boolean().optional().describe("Whether to hide categories with no posts") 16 | }); 17 | 18 | const getCategorySchema = z.object({ 19 | id: z.number().describe("Category ID") 20 | }).strict(); 21 | 22 | const createCategorySchema = z.object({ 23 | name: z.string().describe("Category name"), 24 | slug: z.string().optional().describe("Category slug"), 25 | description: z.string().optional().describe("Category description"), 26 | parent: z.number().optional().describe("Parent category ID") 27 | }).strict(); 28 | 29 | const updateCategorySchema = z.object({ 30 | id: z.number().describe("Category ID"), 31 | name: z.string().optional().describe("Category name"), 32 | slug: z.string().optional().describe("Category slug"), 33 | description: z.string().optional().describe("Category description"), 34 | parent: z.number().optional().describe("Parent category ID") 35 | }).strict(); 36 | 37 | const deleteCategorySchema = z.object({ 38 | id: z.number().describe("Category ID"), 39 | force: z.boolean().optional().describe("Whether to bypass trash and force deletion") 40 | }).strict(); 41 | 42 | type ListCategoriesParams = z.infer; 43 | type GetCategoryParams = z.infer; 44 | type CreateCategoryParams = z.infer; 45 | type UpdateCategoryParams = z.infer; 46 | type DeleteCategoryParams = z.infer; 47 | 48 | export const categoryTools: Tool[] = [ 49 | { 50 | name: "list_categories", 51 | description: "Lists all categories with filtering, sorting, and pagination options", 52 | inputSchema: { type: "object", properties: listCategoriesSchema.shape } 53 | }, 54 | { 55 | name: "get_category", 56 | description: "Gets a category by ID", 57 | inputSchema: { type: "object", properties: getCategorySchema.shape } 58 | }, 59 | { 60 | name: "create_category", 61 | description: "Creates a new category", 62 | inputSchema: { type: "object", properties: createCategorySchema.shape } 63 | }, 64 | { 65 | name: "update_category", 66 | description: "Updates an existing category", 67 | inputSchema: { type: "object", properties: updateCategorySchema.shape } 68 | }, 69 | { 70 | name: "delete_category", 71 | description: "Deletes a category", 72 | inputSchema: { type: "object", properties: deleteCategorySchema.shape } 73 | } 74 | ]; 75 | 76 | export const categoryHandlers = { 77 | list_categories: async (params: ListCategoriesParams) => { 78 | try { 79 | const response = await makeWordPressRequest('GET', "categories", params); 80 | const categories: WPCategory[] = response; 81 | return { 82 | toolResult: { 83 | content: [{ type: 'text', text: JSON.stringify(categories, null, 2) }], 84 | }, 85 | }; 86 | } catch (error: any) { 87 | const errorMessage = error.response?.data?.message || error.message; 88 | return { 89 | toolResult: { 90 | isError: true, 91 | content: [{ type: 'text', text: `Error listing categories: ${errorMessage}` }], 92 | }, 93 | }; 94 | } 95 | }, 96 | get_category: async (params: GetCategoryParams) => { 97 | try { 98 | const response = await makeWordPressRequest('GET', `categories/${params.id}`); 99 | const category: WPCategory = response; 100 | return { 101 | toolResult: { 102 | content: [{ type: 'text', text: JSON.stringify(category, null, 2) }], 103 | }, 104 | }; 105 | } catch (error: any) { 106 | const errorMessage = error.response?.data?.message || error.message; 107 | return { 108 | toolResult: { 109 | isError: true, 110 | content: [{ type: 'text', text: `Error getting category: ${errorMessage}` }], 111 | }, 112 | }; 113 | } 114 | }, 115 | create_category: async (params: CreateCategoryParams) => { 116 | try { 117 | const response = await makeWordPressRequest('POST', "categories", params); 118 | const category: WPCategory = response; 119 | return { 120 | toolResult: { 121 | content: [{ type: 'text', text: JSON.stringify(category, null, 2) }], 122 | }, 123 | }; 124 | } catch (error: any) { 125 | const errorMessage = error.response?.data?.message || error.message; 126 | return { 127 | toolResult: { 128 | isError: true, 129 | content: [{ type: 'text', text: `Error creating category: ${errorMessage}` }], 130 | }, 131 | }; 132 | } 133 | }, 134 | update_category: async (params: UpdateCategoryParams) => { 135 | try { 136 | const { id, ...updateData } = params; 137 | const response = await makeWordPressRequest('POST', `categories/${id}`, updateData); 138 | const category: WPCategory = response; 139 | return { 140 | toolResult: { 141 | content: [{ type: 'text', text: JSON.stringify(category, null, 2) }], 142 | }, 143 | }; 144 | } catch (error: any) { 145 | const errorMessage = error.response?.data?.message || error.message; 146 | return { 147 | toolResult: { 148 | isError: true, 149 | content: [{ type: 'text', text: `Error updating category: ${errorMessage}` }], 150 | }, 151 | }; 152 | } 153 | }, 154 | delete_category: async (params: DeleteCategoryParams) => { 155 | try { 156 | const response = await makeWordPressRequest('DELETE', `categories/${params.id}`, { force: params.force }); 157 | const category: WPCategory = response; 158 | return { 159 | toolResult: { 160 | content: [{ type: 'text', text: JSON.stringify(category, null, 2) }], 161 | }, 162 | }; 163 | } catch (error: any) { 164 | const errorMessage = error.response?.data?.message || error.message; 165 | return { 166 | toolResult: { 167 | isError: true, 168 | content: [{ type: 'text', text: `Error deleting category: ${errorMessage}` }], 169 | }, 170 | }; 171 | } 172 | } 173 | }; 174 | -------------------------------------------------------------------------------- /src/tools/comments.ts: -------------------------------------------------------------------------------- 1 | // src/tools/comments.ts 2 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 3 | import { makeWordPressRequest } from '../wordpress.js'; 4 | import { WPComment } from '../types/wordpress-types.js'; 5 | import { z } from 'zod'; 6 | 7 | // Schema for listing comments 8 | const listCommentsSchema = z.object({ 9 | page: z.number().optional().describe("Page number (default 1)"), 10 | per_page: z.number().min(1).max(100).optional().describe("Items per page (default 10, max 100)"), 11 | search: z.string().optional().describe("Search term for comment content"), 12 | after: z.string().optional().describe("ISO8601 date string to get comments published after this date"), 13 | author: z.union([z.number(), z.array(z.number())]).optional().describe("Author ID or array of IDs"), 14 | author_email: z.string().email().optional().describe("Author email address"), 15 | author_exclude: z.array(z.number()).optional().describe("Array of author IDs to exclude"), 16 | post: z.number().optional().describe("Post ID to retrieve comments for"), 17 | status: z.enum(['approve', 'hold', 'spam', 'trash']).optional().describe("Comment status"), 18 | type: z.string().optional().describe("Comment type"), 19 | orderby: z.enum(['date', 'date_gmt', 'id', 'include', 'post', 'parent', 'type']).optional().describe("Sort comments by parameter"), 20 | order: z.enum(['asc', 'desc']).optional().describe("Order sort attribute ascending or descending") 21 | }); 22 | 23 | // Schema for getting a single comment 24 | const getCommentSchema = z.object({ 25 | id: z.number().describe("Comment ID") 26 | }).strict(); 27 | 28 | // Schema for creating a comment 29 | const createCommentSchema = z.object({ 30 | post: z.number().describe("The ID of the post object the comment is for"), 31 | author: z.number().optional().describe("The ID of the user object, if the author is a registered user"), 32 | author_name: z.string().optional().describe("Display name for the comment author"), 33 | author_email: z.string().email().optional().describe("Email address for the comment author"), 34 | author_url: z.string().url().optional().describe("URL for the comment author"), 35 | content: z.string().describe("The content of the comment"), 36 | parent: z.number().optional().describe("The ID of the parent comment"), 37 | status: z.enum(['approve', 'hold']).optional().describe("State of the comment") 38 | }).strict(); 39 | 40 | // Schema for updating a comment 41 | const updateCommentSchema = z.object({ 42 | id: z.number().describe("Comment ID"), 43 | post: z.number().optional().describe("The ID of the post object the comment is for"), 44 | author: z.number().optional().describe("The ID of the user object, if the author is a registered user"), 45 | author_name: z.string().optional().describe("Display name for the comment author"), 46 | author_email: z.string().email().optional().describe("Email address for the comment author"), 47 | author_url: z.string().url().optional().describe("URL for the comment author"), 48 | content: z.string().optional().describe("The content of the comment"), 49 | parent: z.number().optional().describe("The ID of the parent comment"), 50 | status: z.enum(['approve', 'hold', 'spam', 'trash']).optional().describe("State of the comment") 51 | }).strict(); 52 | 53 | // Schema for deleting a comment 54 | const deleteCommentSchema = z.object({ 55 | id: z.number().describe("Comment ID"), 56 | force: z.boolean().optional().describe("Whether to bypass trash and force deletion") 57 | }).strict(); 58 | 59 | // TypeScript types for the parameters 60 | type ListCommentsParams = z.infer; 61 | type GetCommentParams = z.infer; 62 | type CreateCommentParams = z.infer; 63 | type UpdateCommentParams = z.infer; 64 | type DeleteCommentParams = z.infer; 65 | 66 | // Define the tools 67 | export const commentTools: Tool[] = [ 68 | { 69 | name: "list_comments", 70 | description: "Lists comments with filtering, sorting, and pagination options", 71 | inputSchema: { type: "object", properties: listCommentsSchema.shape } 72 | }, 73 | { 74 | name: "get_comment", 75 | description: "Gets a comment by ID", 76 | inputSchema: { type: "object", properties: getCommentSchema.shape } 77 | }, 78 | { 79 | name: "create_comment", 80 | description: "Creates a new comment", 81 | inputSchema: { type: "object", properties: createCommentSchema.shape } 82 | }, 83 | { 84 | name: "update_comment", 85 | description: "Updates an existing comment", 86 | inputSchema: { type: "object", properties: updateCommentSchema.shape } 87 | }, 88 | { 89 | name: "delete_comment", 90 | description: "Deletes a comment", 91 | inputSchema: { type: "object", properties: deleteCommentSchema.shape } 92 | } 93 | ]; 94 | 95 | // Implement the handlers 96 | export const commentHandlers = { 97 | list_comments: async (params: ListCommentsParams) => { 98 | try { 99 | const response = await makeWordPressRequest('GET', "comments", params); 100 | const comments: WPComment[] = response; 101 | return { 102 | toolResult: { 103 | content: [{ type: 'text', text: JSON.stringify(comments, null, 2) }], 104 | }, 105 | }; 106 | } catch (error: any) { 107 | const errorMessage = error.response?.data?.message || error.message; 108 | return { 109 | toolResult: { 110 | isError: true, 111 | content: [{ type: 'text', text: `Error listing comments: ${errorMessage}` }], 112 | }, 113 | }; 114 | } 115 | }, 116 | 117 | get_comment: async (params: GetCommentParams) => { 118 | try { 119 | const response = await makeWordPressRequest('GET', `comments/${params.id}`); 120 | const comment: WPComment = response; 121 | return { 122 | toolResult: { 123 | content: [{ type: 'text', text: JSON.stringify(comment, null, 2) }], 124 | }, 125 | }; 126 | } catch (error: any) { 127 | const errorMessage = error.response?.data?.message || error.message; 128 | return { 129 | toolResult: { 130 | isError: true, 131 | content: [{ type: 'text', text: `Error getting comment: ${errorMessage}` }], 132 | }, 133 | }; 134 | } 135 | }, 136 | 137 | create_comment: async (params: CreateCommentParams) => { 138 | try { 139 | const response = await makeWordPressRequest('POST', "comments", params); 140 | const comment: WPComment = response; 141 | return { 142 | toolResult: { 143 | content: [{ type: 'text', text: JSON.stringify(comment, null, 2) }], 144 | }, 145 | }; 146 | } catch (error: any) { 147 | const errorMessage = error.response?.data?.message || error.message; 148 | return { 149 | toolResult: { 150 | isError: true, 151 | content: [{ type: 'text', text: `Error creating comment: ${errorMessage}` }], 152 | }, 153 | }; 154 | } 155 | }, 156 | 157 | update_comment: async (params: UpdateCommentParams) => { 158 | try { 159 | const { id, ...updateData } = params; 160 | const response = await makeWordPressRequest('POST', `comments/${id}`, updateData); 161 | const comment: WPComment = response; 162 | return { 163 | toolResult: { 164 | content: [{ type: 'text', text: JSON.stringify(comment, null, 2) }], 165 | }, 166 | }; 167 | } catch (error: any) { 168 | const errorMessage = error.response?.data?.message || error.message; 169 | return { 170 | toolResult: { 171 | isError: true, 172 | content: [{ type: 'text', text: `Error updating comment: ${errorMessage}` }], 173 | }, 174 | }; 175 | } 176 | }, 177 | 178 | delete_comment: async (params: DeleteCommentParams) => { 179 | try { 180 | const response = await makeWordPressRequest('DELETE', `comments/${params.id}`, { force: params.force }); 181 | const comment: WPComment = response; 182 | return { 183 | toolResult: { 184 | content: [{ type: 'text', text: JSON.stringify(comment, null, 2) }], 185 | }, 186 | }; 187 | } catch (error: any) { 188 | const errorMessage = error.response?.data?.message || error.message; 189 | return { 190 | toolResult: { 191 | isError: true, 192 | content: [{ type: 'text', text: `Error deleting comment: ${errorMessage}` }], 193 | }, 194 | }; 195 | } 196 | } 197 | }; 198 | -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | // src/tools/index.ts 2 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 3 | import { postTools, postHandlers } from './posts.js'; 4 | import { pageTools, pageHandlers } from './pages.js'; 5 | import { pluginTools, pluginHandlers } from './plugins.js'; 6 | import { mediaTools, mediaHandlers } from './media.js'; 7 | import { categoryTools, categoryHandlers } from './categories.js'; 8 | import { userTools, userHandlers } from './users.js'; 9 | import { pluginRepositoryTools, pluginRepositoryHandlers } from './plugin-repository.js'; 10 | import { commentTools, commentHandlers } from './comments.js'; 11 | 12 | // Combine all tools 13 | export const allTools: Tool[] = [ 14 | ...postTools, 15 | ...pageTools, 16 | ...pluginTools, 17 | ...mediaTools, 18 | ...categoryTools, 19 | ...userTools, 20 | ...pluginRepositoryTools, 21 | ...commentTools 22 | ]; 23 | 24 | // Combine all handlers 25 | export const toolHandlers = { 26 | ...postHandlers, 27 | ...pageHandlers, 28 | ...pluginHandlers, 29 | ...mediaHandlers, 30 | ...categoryHandlers, 31 | ...userHandlers, 32 | ...pluginRepositoryHandlers, 33 | ...commentHandlers 34 | }; -------------------------------------------------------------------------------- /src/tools/media.ts: -------------------------------------------------------------------------------- 1 | // src/tools/media.ts 2 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 3 | import { makeWordPressRequest } from '../wordpress.js'; 4 | import { z } from 'zod'; 5 | 6 | // Schema for listing media items 7 | const listMediaSchema = z.object({ 8 | page: z.number().optional().describe("Page number"), 9 | per_page: z.number().min(1).max(100).optional().describe("Items per page"), 10 | search: z.string().optional().describe("Search term for media") 11 | }).strict(); 12 | 13 | // Schema for creating a new media item 14 | const createMediaSchema = z.object({ 15 | title: z.string().describe("Media title"), 16 | alt_text: z.string().optional().describe("Alternate text for the media"), 17 | caption: z.string().optional().describe("Caption of the media"), 18 | description: z.string().optional().describe("Description of the media"), 19 | source_url: z.string().describe("Source URL of the media file") 20 | }).strict(); 21 | 22 | // Schema for editing an existing media item 23 | const editMediaSchema = z.object({ 24 | id: z.number().describe("Media ID to edit"), 25 | title: z.string().optional().describe("Media title"), 26 | alt_text: z.string().optional().describe("Alternate text for the media"), 27 | caption: z.string().optional().describe("Caption of the media"), 28 | description: z.string().optional().describe("Description of the media") 29 | }).strict(); 30 | 31 | // Schema for deleting a media item 32 | const deleteMediaSchema = z.object({ 33 | id: z.number().describe("Media ID to delete"), 34 | force: z.boolean().optional().describe("Force deletion bypassing trash") 35 | }).strict(); 36 | 37 | // Define the tool set for media operations 38 | export const mediaTools: Tool[] = [ 39 | { 40 | name: "list_media", 41 | description: "Lists media items with filtering and pagination options", 42 | inputSchema: { type: "object", properties: listMediaSchema.shape } 43 | }, 44 | { 45 | name: "create_media", 46 | description: "Creates a new media item", 47 | inputSchema: { type: "object", properties: createMediaSchema.shape } 48 | }, 49 | { 50 | name: "edit_media", 51 | description: "Updates an existing media item", 52 | inputSchema: { type: "object", properties: editMediaSchema.shape } 53 | }, 54 | { 55 | name: "delete_media", 56 | description: "Deletes a media item", 57 | inputSchema: { type: "object", properties: deleteMediaSchema.shape } 58 | } 59 | ]; 60 | 61 | // Define handlers for each media operation 62 | export const mediaHandlers = { 63 | list_media: async (params: z.infer) => { 64 | try { 65 | const response = await makeWordPressRequest("GET", "media", params); 66 | return { 67 | toolResult: { 68 | content: [{ type: "text", text: JSON.stringify(response, null, 2) }] 69 | } 70 | }; 71 | } catch (error: any) { 72 | const errorMessage = error.response?.data?.message || error.message; 73 | return { 74 | toolResult: { 75 | isError: true, 76 | content: [{ type: "text", text: `Error listing media: ${errorMessage}` }] 77 | } 78 | }; 79 | } 80 | }, 81 | create_media: async (params: z.infer) => { 82 | try { 83 | if (params.source_url && params.source_url.startsWith('http')) { 84 | // Download the media file from the URL and upload as multipart form-data 85 | const axios = (await import('axios')).default; 86 | const FormData = (await import('form-data')).default; 87 | const fileRes = await axios.get(params.source_url, { responseType: 'arraybuffer' }); 88 | // Derive a filename from the title or fallback 89 | const filename = params.title ? `${params.title.replace(/\s+/g, '_')}.jpg` : 'upload.jpg'; 90 | 91 | const form = new FormData(); 92 | form.append('file', Buffer.from(fileRes.data), { 93 | filename: filename, 94 | contentType: fileRes.headers['content-type'] || 'application/octet-stream' 95 | }); 96 | // Append additional fields if provided 97 | if (params.title) form.append('title', params.title); 98 | if (params.alt_text) form.append('alt_text', params.alt_text); 99 | if (params.caption) form.append('caption', params.caption); 100 | if (params.description) form.append('description', params.description); 101 | 102 | // Use the enhanced makeWordPressRequest function with FormData support 103 | const response = await makeWordPressRequest( 104 | 'POST', 105 | 'media', 106 | form, 107 | { 108 | isFormData: true, 109 | headers: form.getHeaders(), 110 | rawResponse: true 111 | } 112 | ); 113 | return { 114 | toolResult: { 115 | content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] 116 | } 117 | }; 118 | } else { 119 | const response = await makeWordPressRequest("POST", "media", params); 120 | return { 121 | toolResult: { 122 | content: [{ type: "text", text: JSON.stringify(response, null, 2) }] 123 | } 124 | }; 125 | } 126 | } catch (error: any) { 127 | const errorMessage = error.response?.data?.message || error.message; 128 | return { 129 | toolResult: { 130 | isError: true, 131 | content: [{ type: "text", text: `Error creating media: ${errorMessage}` }] 132 | } 133 | }; 134 | } 135 | }, 136 | edit_media: async (params: z.infer) => { 137 | try { 138 | const { id, ...updateData } = params; 139 | const response = await makeWordPressRequest("POST", `media/${id}`, updateData); 140 | return { 141 | toolResult: { 142 | content: [{ type: "text", text: JSON.stringify(response, null, 2) }] 143 | } 144 | }; 145 | } catch (error: any) { 146 | const errorMessage = error.response?.data?.message || error.message; 147 | return { 148 | toolResult: { 149 | isError: true, 150 | content: [{ type: "text", text: `Error editing media: ${errorMessage}` }] 151 | } 152 | }; 153 | } 154 | }, 155 | delete_media: async (params: z.infer) => { 156 | try { 157 | const { id, ...deleteData } = params; 158 | const response = await makeWordPressRequest("DELETE", `media/${id}`, deleteData); 159 | return { 160 | toolResult: { 161 | content: [{ type: "text", text: JSON.stringify(response, null, 2) }] 162 | } 163 | }; 164 | } catch (error: any) { 165 | const errorMessage = error.response?.data?.message || error.message; 166 | return { 167 | toolResult: { 168 | isError: true, 169 | content: [{ type: "text", text: `Error deleting media: ${errorMessage}` }] 170 | } 171 | }; 172 | } 173 | } 174 | }; 175 | -------------------------------------------------------------------------------- /src/tools/pages.ts: -------------------------------------------------------------------------------- 1 | // src/tools/pages.ts 2 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 3 | import { makeWordPressRequest } from '../wordpress.js'; 4 | import { WPPage } from '../types/wordpress-types.js'; 5 | import { z } from 'zod'; 6 | import { zodToJsonSchema } from 'zod-to-json-schema'; 7 | 8 | const listPagesSchema = z.object({ 9 | page: z.number().optional().describe("Page number (default 1)"), 10 | per_page: z.number().min(1).max(100).optional().describe("Items per page (default 10, max 100)"), 11 | search: z.string().optional().describe("Search term for page content or title"), 12 | after: z.string().optional().describe("ISO8601 date string to get pages published after this date"), 13 | author: z.union([z.number(), z.array(z.number())]).optional().describe("Author ID or array of IDs"), 14 | parent: z.number().optional().describe("Parent page ID"), 15 | status: z.enum(['publish', 'future', 'draft', 'pending', 'private']).optional().describe("Page status"), 16 | menu_order: z.number().optional().describe("Menu order value"), 17 | orderby: z.enum(['date', 'id', 'include', 'title', 'slug', 'menu_order']).optional().describe("Sort pages by parameter"), 18 | order: z.enum(['asc', 'desc']).optional().describe("Order sort attribute ascending or descending") 19 | }); 20 | 21 | const getPageSchema = z.object({ 22 | id: z.number().describe("Page ID") 23 | }).strict(); 24 | 25 | const createPageSchema = z.object({ 26 | title: z.string().describe("Page title"), 27 | content: z.string().describe("Page content"), 28 | status: z.enum(['publish', 'future', 'draft', 'pending', 'private']).optional().default('draft').describe("Page status"), 29 | excerpt: z.string().optional().describe("Page excerpt"), 30 | author: z.number().optional().describe("Author ID"), 31 | featured_media: z.number().optional().describe("Featured image ID"), 32 | parent: z.number().optional().describe("Parent page ID"), 33 | menu_order: z.number().optional().describe("Menu order value"), 34 | template: z.string().optional().describe("Page template"), 35 | slug: z.string().optional().describe("Page slug") 36 | }).strict(); 37 | 38 | const updatePageSchema = z.object({ 39 | id: z.number().describe("Page ID"), 40 | title: z.string().optional().describe("Page title"), 41 | content: z.string().optional().describe("Page content"), 42 | status: z.enum(['publish', 'future', 'draft', 'pending', 'private']).optional().describe("Page status"), 43 | excerpt: z.string().optional().describe("Page excerpt"), 44 | author: z.number().optional().describe("Author ID"), 45 | featured_media: z.number().optional().describe("Featured image ID"), 46 | parent: z.number().optional().describe("Parent page ID"), 47 | menu_order: z.number().optional().describe("Menu order value"), 48 | template: z.string().optional().describe("Page template"), 49 | slug: z.string().optional().describe("Page slug") 50 | }).strict(); 51 | 52 | const deletePageSchema = z.object({ 53 | id: z.number().describe("Page ID"), 54 | force: z.boolean().optional().describe("Whether to bypass trash and force deletion") 55 | }).strict(); 56 | 57 | type ListPagesParams = z.infer; 58 | type GetPageParams = z.infer; 59 | type CreatePageParams = z.infer; 60 | type UpdatePageParams = z.infer; 61 | type DeletePageParams = z.infer; 62 | 63 | export const pageTools: Tool[] = [ 64 | { 65 | name: "list_pages", 66 | description: "Lists all pages with filtering, sorting, and pagination options", 67 | inputSchema: { type: "object", properties: listPagesSchema.shape } 68 | }, 69 | { 70 | name: "get_page", 71 | description: "Gets a page by ID", 72 | inputSchema: { type: "object", properties: getPageSchema.shape } 73 | }, 74 | { 75 | name: "create_page", 76 | description: "Creates a new page", 77 | inputSchema: { type: "object", properties: createPageSchema.shape } 78 | }, 79 | { 80 | name: "update_page", 81 | description: "Updates an existing page", 82 | inputSchema: { type: "object", properties: updatePageSchema.shape } 83 | }, 84 | { 85 | name: "delete_page", 86 | description: "Deletes a page", 87 | inputSchema: { type: "object", properties: deletePageSchema.shape } 88 | } 89 | ]; 90 | 91 | export const pageHandlers = { 92 | list_pages: async (params: ListPagesParams) => { 93 | try { 94 | const response = await makeWordPressRequest('GET', "pages", params); 95 | const pages: WPPage[] = response; 96 | return { 97 | toolResult: { 98 | content: [{ type: 'text', text: JSON.stringify(pages, null, 2) }], 99 | }, 100 | }; 101 | } catch (error: any) { 102 | const errorMessage = error.response?.data?.message || error.message; 103 | return { 104 | toolResult: { 105 | isError: true, 106 | content: [{ type: 'text', text: `Error listing pages: ${errorMessage}` }], 107 | }, 108 | }; 109 | } 110 | }, 111 | get_page: async (params: GetPageParams) => { 112 | try { 113 | const response = await makeWordPressRequest('GET', `pages/${params.id}`); 114 | const page: WPPage = response; 115 | return { 116 | toolResult: { 117 | content: [{ type: 'text', text: JSON.stringify(page, null, 2) }], 118 | }, 119 | }; 120 | } catch (error: any) { 121 | const errorMessage = error.response?.data?.message || error.message; 122 | return { 123 | toolResult: { 124 | isError: true, 125 | content: [{ type: 'text', text: `Error getting page: ${errorMessage}` }], 126 | }, 127 | }; 128 | } 129 | }, 130 | create_page: async (params: CreatePageParams) => { 131 | try { 132 | const response = await makeWordPressRequest('POST', "pages", params); 133 | const page: WPPage = response; 134 | return { 135 | toolResult: { 136 | content: [{ type: 'text', text: JSON.stringify(page, null, 2) }], 137 | }, 138 | }; 139 | } catch (error: any) { 140 | const errorMessage = error.response?.data?.message || error.message; 141 | return { 142 | toolResult: { 143 | isError: true, 144 | content: [{ type: 'text', text: `Error creating page: ${errorMessage}` }], 145 | }, 146 | }; 147 | } 148 | }, 149 | update_page: async (params: UpdatePageParams) => { 150 | try { 151 | const { id, ...updateData } = params; 152 | const response = await makeWordPressRequest('PUT', `pages/${id}`, updateData); 153 | const page: WPPage = response; 154 | return { 155 | toolResult: { 156 | content: [{ type: 'text', text: JSON.stringify(page, null, 2) }], 157 | }, 158 | }; 159 | } catch (error: any) { 160 | const errorMessage = error.response?.data?.message || error.message; 161 | return { 162 | toolResult: { 163 | isError: true, 164 | content: [{ type: 'text', text: `Error updating page: ${errorMessage}` }], 165 | }, 166 | }; 167 | } 168 | }, 169 | delete_page: async (params: DeletePageParams) => { 170 | try { 171 | const response = await makeWordPressRequest('DELETE', `pages/${params.id}`, { force: params.force }); 172 | const page: WPPage = response; 173 | return { 174 | toolResult: { 175 | content: [{ type: 'text', text: JSON.stringify(page, null, 2) }], 176 | }, 177 | }; 178 | } catch (error: any) { 179 | const errorMessage = error.response?.data?.message || error.message; 180 | return { 181 | toolResult: { 182 | isError: true, 183 | content: [{ type: 'text', text: `Error deleting page: ${errorMessage}` }], 184 | }, 185 | }; 186 | } 187 | } 188 | }; -------------------------------------------------------------------------------- /src/tools/plugin-repository.ts: -------------------------------------------------------------------------------- 1 | // src/tools/plugin-repository.ts 2 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 3 | import { searchWordPressPluginRepository } from '../wordpress.js'; 4 | import { z } from 'zod'; 5 | import { zodToJsonSchema } from 'zod-to-json-schema'; 6 | 7 | // Define the schema for plugin repository search 8 | const searchPluginRepositorySchema = z.object({ 9 | search: z.string().describe("Search query for WordPress.org plugin repository"), 10 | page: z.number().min(1).optional().default(1).describe("Page number (1-based)"), 11 | per_page: z.number().min(1).max(100).optional().default(10).describe("Number of results per page (max 100)") 12 | }).strict(); 13 | 14 | // Define the schema for getting plugin details 15 | const getPluginDetailsSchema = z.object({ 16 | slug: z.string().describe("Plugin slug from WordPress.org repository") 17 | }).strict(); 18 | 19 | type SearchPluginRepositoryParams = z.infer; 20 | type GetPluginDetailsParams = z.infer; 21 | 22 | // Define the plugin repository tools 23 | export const pluginRepositoryTools: Tool[] = [ 24 | { 25 | name: "search_plugin_repository", 26 | description: "Search for plugins in the WordPress.org plugin repository", 27 | inputSchema: { type: "object", properties: searchPluginRepositorySchema.shape } 28 | }, 29 | { 30 | name: "get_plugin_details", 31 | description: "Get detailed information about a plugin from the WordPress.org repository", 32 | inputSchema: { type: "object", properties: getPluginDetailsSchema.shape } 33 | } 34 | ]; 35 | 36 | // Define handlers for plugin repository operations 37 | export const pluginRepositoryHandlers = { 38 | search_plugin_repository: async (params: SearchPluginRepositoryParams) => { 39 | try { 40 | const response = await searchWordPressPluginRepository( 41 | params.search, 42 | params.page, 43 | params.per_page 44 | ); 45 | 46 | // Format the response to be more user-friendly 47 | const formattedPlugins = response.plugins.map((plugin: any) => ({ 48 | name: plugin.name, 49 | slug: plugin.slug, 50 | version: plugin.version, 51 | author: plugin.author, 52 | requires_wp: plugin.requires, 53 | tested: plugin.tested, 54 | rating: plugin.rating, 55 | active_installs: plugin.active_installs, 56 | downloaded: plugin.downloaded, 57 | last_updated: plugin.last_updated, 58 | short_description: plugin.short_description, 59 | download_link: plugin.download_link, 60 | homepage: plugin.homepage 61 | })); 62 | 63 | const result = { 64 | info: { 65 | page: response.info.page, 66 | pages: response.info.pages, 67 | results: response.info.results 68 | }, 69 | plugins: formattedPlugins 70 | }; 71 | 72 | return { 73 | toolResult: { 74 | content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], 75 | }, 76 | }; 77 | } catch (error: any) { 78 | const errorMessage = error.response?.data?.message || error.message; 79 | return { 80 | toolResult: { 81 | isError: true, 82 | content: [{ type: 'text', text: `Error searching plugin repository: ${errorMessage}` }], 83 | }, 84 | }; 85 | } 86 | }, 87 | 88 | get_plugin_details: async (params: GetPluginDetailsParams) => { 89 | try { 90 | // For plugin details, we use a different action in the WordPress.org API 91 | const apiUrl = 'https://api.wordpress.org/plugins/info/1.2/'; 92 | const requestData = { 93 | action: 'plugin_information', 94 | request: { 95 | slug: params.slug, 96 | fields: { 97 | description: true, 98 | sections: true, 99 | tested: true, 100 | requires: true, 101 | rating: true, 102 | ratings: true, 103 | downloaded: true, 104 | downloadlink: true, 105 | last_updated: true, 106 | homepage: true, 107 | tags: true, 108 | compatibility: true, 109 | author: true, 110 | contributors: true, 111 | banners: true, 112 | icons: true 113 | } 114 | } 115 | }; 116 | 117 | // Use axios directly for this specific request 118 | const axios = (await import('axios')).default; 119 | const response = await axios.post(apiUrl, requestData, { 120 | headers: { 121 | 'Content-Type': 'application/json' 122 | } 123 | }); 124 | 125 | // Format the plugin details 126 | const plugin = response.data; 127 | const formattedPlugin = { 128 | name: plugin.name, 129 | slug: plugin.slug, 130 | version: plugin.version, 131 | author: plugin.author, 132 | author_profile: plugin.author_profile, 133 | contributors: plugin.contributors, 134 | requires_wp: plugin.requires, 135 | tested: plugin.tested, 136 | requires_php: plugin.requires_php, 137 | rating: plugin.rating, 138 | ratings: plugin.ratings, 139 | active_installs: plugin.active_installs, 140 | downloaded: plugin.downloaded, 141 | last_updated: plugin.last_updated, 142 | added: plugin.added, 143 | homepage: plugin.homepage, 144 | description: plugin.description, 145 | short_description: plugin.short_description, 146 | download_link: plugin.download_link, 147 | tags: plugin.tags, 148 | sections: plugin.sections, 149 | banners: plugin.banners, 150 | icons: plugin.icons 151 | }; 152 | 153 | return { 154 | toolResult: { 155 | content: [{ type: 'text', text: JSON.stringify(formattedPlugin, null, 2) }], 156 | }, 157 | }; 158 | } catch (error: any) { 159 | const errorMessage = error.response?.data?.message || error.message; 160 | return { 161 | toolResult: { 162 | isError: true, 163 | content: [{ type: 'text', text: `Error getting plugin details: ${errorMessage}` }], 164 | }, 165 | }; 166 | } 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /src/tools/plugins.ts: -------------------------------------------------------------------------------- 1 | // src/tools/plugins.ts 2 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 3 | import { makeWordPressRequest } from '../wordpress.js'; 4 | import { WPPlugin } from '../types/wordpress-types.js'; 5 | import { z } from 'zod'; 6 | import { zodToJsonSchema } from 'zod-to-json-schema'; 7 | 8 | // Note: Plugin operations require authentication with admin privileges 9 | // and use a different endpoint than the standard WP API (wp-json/wp/v2/plugins) 10 | 11 | // Make schema empty since the WordPress REST API plugins endpoint doesn't accept parameters 12 | // in the same way as other endpoints 13 | const listPluginsSchema = z.object({ 14 | status: z.enum(['active', 'inactive']).optional().default('active').describe("Filter plugins by status (active, inactive)") 15 | }).strict(); 16 | 17 | const getPluginSchema = z.object({ 18 | plugin: z.string().describe("Plugin slug (e.g., 'akismet', 'elementor', 'wordpress-seo')") 19 | }).strict(); 20 | 21 | const activatePluginSchema = z.object({ 22 | plugin: z.string().describe("Plugin slug (e.g., 'akismet', 'elementor', 'wordpress-seo')") 23 | }).strict(); 24 | 25 | const deactivatePluginSchema = z.object({ 26 | plugin: z.string().describe("Plugin slug (e.g., 'akismet', 'elementor', 'wordpress-seo')") 27 | }).strict(); 28 | 29 | const createPluginSchema = z.object({ 30 | slug: z.string({ required_error: "Plugin slug is required" }).describe("WordPress.org plugin directory slug, e.g., 'akismet', 'elementor', 'wordpress-seo'"), 31 | status: z.enum(['inactive', 'active']).optional().default('active').describe("Plugin activation status") 32 | }).strict(); 33 | 34 | type ListPluginsParams = z.infer; 35 | type GetPluginParams = z.infer; 36 | type ActivatePluginParams = z.infer; 37 | type DeactivatePluginParams = z.infer; 38 | type CreatePluginParams = z.infer; 39 | 40 | // Define tool set for plugin operations 41 | export const pluginTools: Tool[] = [ 42 | { 43 | name: "list_plugins", 44 | description: "Lists all plugins with filtering options", 45 | inputSchema: { type: "object", properties: listPluginsSchema.shape } 46 | }, 47 | { 48 | name: "get_plugin", 49 | description: "Retrieves plugin details", 50 | inputSchema: { type: "object", properties: getPluginSchema.shape } 51 | }, 52 | { 53 | name: "activate_plugin", 54 | description: "Activates a plugin", 55 | inputSchema: { type: "object", properties: activatePluginSchema.shape } 56 | }, 57 | { 58 | name: "deactivate_plugin", 59 | description: "Deactivates a plugin", 60 | inputSchema: { type: "object", properties: deactivatePluginSchema.shape } 61 | }, 62 | { 63 | name: "create_plugin", 64 | description: "Creates a plugin from the WordPress.org repository", 65 | inputSchema: { type: "object", properties: createPluginSchema.shape } 66 | } 67 | ]; 68 | 69 | // Define handlers for each plugin operation 70 | export const pluginHandlers = { 71 | list_plugins: async (params: z.infer) => { 72 | try { 73 | const response = await makeWordPressRequest("GET", "plugins", params); 74 | return { 75 | toolResult: { 76 | content: [{ type: "text", text: JSON.stringify(response, null, 2) }] 77 | } 78 | }; 79 | } catch (error: any) { 80 | const errorMessage = error.response?.data?.message || error.message; 81 | return { 82 | toolResult: { 83 | isError: true, 84 | content: [{ type: "text", text: `Error listing plugins: ${errorMessage}` }] 85 | } 86 | }; 87 | } 88 | }, 89 | get_plugin: async (params: z.infer) => { 90 | try { 91 | const response = await makeWordPressRequest("GET", `plugins/${params.plugin}`); 92 | return { 93 | toolResult: { 94 | content: [{ type: "text", text: JSON.stringify(response, null, 2) }] 95 | } 96 | }; 97 | } catch (error: any) { 98 | const errorMessage = error.response?.data?.message || error.message; 99 | return { 100 | toolResult: { 101 | isError: true, 102 | content: [{ type: "text", text: `Error retrieving plugin: ${errorMessage}` }] 103 | } 104 | }; 105 | } 106 | }, 107 | activate_plugin: async (params: z.infer) => { 108 | try { 109 | const response = await makeWordPressRequest("POST", `plugins/${params.plugin}/activate`, params); 110 | return { 111 | toolResult: { 112 | content: [{ type: "text", text: JSON.stringify(response, null, 2) }] 113 | } 114 | }; 115 | } catch (error: any) { 116 | const errorMessage = error.response?.data?.message || error.message; 117 | return { 118 | toolResult: { 119 | isError: true, 120 | content: [{ type: "text", text: `Error activating plugin: ${errorMessage}` }] 121 | } 122 | }; 123 | } 124 | }, 125 | deactivate_plugin: async (params: z.infer) => { 126 | try { 127 | const response = await makeWordPressRequest("POST", `plugins/${params.plugin}/deactivate`, params); 128 | return { 129 | toolResult: { 130 | content: [{ type: "text", text: JSON.stringify(response, null, 2) }] 131 | } 132 | }; 133 | } catch (error: any) { 134 | const errorMessage = error.response?.data?.message || error.message; 135 | return { 136 | toolResult: { 137 | isError: true, 138 | content: [{ type: "text", text: `Error deactivating plugin: ${errorMessage}` }] 139 | } 140 | }; 141 | } 142 | }, 143 | create_plugin: async (params: z.infer) => { 144 | try { 145 | const response = await makeWordPressRequest("POST", "plugins", params); 146 | return { 147 | toolResult: { 148 | content: [{ type: "text", text: JSON.stringify(response, null, 2) }] 149 | } 150 | }; 151 | } catch (error: any) { 152 | const errorMessage = error.response?.data?.message || error.message; 153 | return { 154 | toolResult: { 155 | isError: true, 156 | content: [{ type: "text", text: `Error creating plugin: ${errorMessage}` }] 157 | } 158 | }; 159 | } 160 | } 161 | }; -------------------------------------------------------------------------------- /src/tools/posts.ts: -------------------------------------------------------------------------------- 1 | // src/tools/posts.ts 2 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 3 | import { makeWordPressRequest } from '../wordpress.js'; 4 | import { WPPost } from '../types/wordpress-types.js'; 5 | import { z } from 'zod'; 6 | import { zodToJsonSchema } from 'zod-to-json-schema'; 7 | 8 | const listPostsSchema = z.object({ 9 | page: z.number().optional().describe("Page number (default 1)"), 10 | per_page: z.number().min(1).max(100).optional().describe("Items per page (default 10, max 100)"), 11 | search: z.string().optional().describe("Search term for post content or title"), 12 | after: z.string().optional().describe("ISO8601 date string to get posts published after this date"), 13 | author: z.union([z.number(), z.array(z.number())]).optional().describe("Author ID or array of IDs"), 14 | categories: z.union([z.number(), z.array(z.number())]).optional().describe("Category ID or array of IDs"), 15 | tags: z.union([z.number(), z.array(z.number())]).optional().describe("Tag ID or array of IDs"), 16 | status: z.enum(['publish', 'future', 'draft', 'pending', 'private']).optional().describe("Post status"), 17 | orderby: z.enum(['date', 'id', 'include', 'title', 'slug', 'modified']).optional().describe("Sort posts by parameter"), 18 | order: z.enum(['asc', 'desc']).optional().describe("Order sort attribute ascending or descending") 19 | }); 20 | 21 | const getPostSchema = z.object({ 22 | id: z.number().describe("Post ID") 23 | }).strict(); 24 | 25 | const createPostSchema = z.object({ 26 | title: z.string().describe("Post title"), 27 | content: z.string().describe("Post content"), 28 | status: z.enum(['publish', 'future', 'draft', 'pending', 'private']).optional().default('draft').describe("Post status"), 29 | excerpt: z.string().optional().describe("Post excerpt"), 30 | author: z.number().optional().describe("Author ID"), 31 | categories: z.array(z.number()).optional().describe("Array of category IDs"), 32 | tags: z.array(z.number()).optional().describe("Array of tag IDs"), 33 | featured_media: z.number().optional().describe("Featured image ID"), 34 | format: z.enum(['standard', 'aside', 'chat', 'gallery', 'link', 'image', 'quote', 'status', 'video', 'audio']).optional().describe("Post format"), 35 | slug: z.string().optional().describe("Post slug") 36 | }).strict(); 37 | 38 | const updatePostSchema = z.object({ 39 | id: z.number().describe("Post ID"), 40 | title: z.string().optional().describe("Post title"), 41 | content: z.string().optional().describe("Post content"), 42 | status: z.enum(['publish', 'future', 'draft', 'pending', 'private']).optional().describe("Post status"), 43 | excerpt: z.string().optional().describe("Post excerpt"), 44 | author: z.number().optional().describe("Author ID"), 45 | categories: z.array(z.number()).optional().describe("Array of category IDs"), 46 | tags: z.array(z.number()).optional().describe("Array of tag IDs"), 47 | featured_media: z.number().optional().describe("Featured image ID"), 48 | format: z.enum(['standard', 'aside', 'chat', 'gallery', 'link', 'image', 'quote', 'status', 'video', 'audio']).optional().describe("Post format"), 49 | slug: z.string().optional().describe("Post slug") 50 | }).strict(); 51 | 52 | const deletePostSchema = z.object({ 53 | id: z.number().describe("Post ID"), 54 | force: z.boolean().optional().describe("Whether to bypass trash and force deletion") 55 | }).strict(); 56 | 57 | type ListPostsParams = z.infer; 58 | type GetPostParams = z.infer; 59 | type CreatePostParams = z.infer; 60 | type UpdatePostParams = z.infer; 61 | type DeletePostParams = z.infer; 62 | 63 | export const postTools: Tool[] = [ 64 | { 65 | name: "list_posts", 66 | description: "Lists all posts with filtering, sorting, and pagination options", 67 | inputSchema: { type: "object", properties: listPostsSchema.shape } 68 | }, 69 | { 70 | name: "get_post", 71 | description: "Gets a post by ID", 72 | inputSchema: { type: "object", properties: getPostSchema.shape } 73 | }, 74 | { 75 | name: "create_post", 76 | description: "Creates a new post", 77 | inputSchema: { type: "object", properties: createPostSchema.shape } 78 | }, 79 | { 80 | name: "update_post", 81 | description: "Updates an existing post", 82 | inputSchema: { type: "object", properties: updatePostSchema.shape } 83 | }, 84 | { 85 | name: "delete_post", 86 | description: "Deletes a post", 87 | inputSchema: { type: "object", properties: deletePostSchema.shape } 88 | } 89 | ]; 90 | 91 | export const postHandlers = { 92 | list_posts: async (params: ListPostsParams) => { 93 | try { 94 | const response = await makeWordPressRequest('GET', "posts", params); 95 | const posts: WPPost[] = response; 96 | return { 97 | toolResult: { 98 | content: [{ type: 'text', text: JSON.stringify(posts, null, 2) }], 99 | }, 100 | }; 101 | } catch (error: any) { 102 | const errorMessage = error.response?.data?.message || error.message; 103 | return { 104 | toolResult: { 105 | isError: true, 106 | content: [{ type: 'text', text: `Error listing posts: ${errorMessage}` }], 107 | }, 108 | }; 109 | } 110 | }, 111 | get_post: async (params: GetPostParams) => { 112 | try { 113 | const response = await makeWordPressRequest('GET', `posts/${params.id}`); 114 | const post: WPPost = response; 115 | return { 116 | toolResult: { 117 | content: [{ type: 'text', text: JSON.stringify(post, null, 2) }], 118 | }, 119 | }; 120 | } catch (error: any) { 121 | const errorMessage = error.response?.data?.message || error.message; 122 | return { 123 | toolResult: { 124 | isError: true, 125 | content: [{ type: 'text', text: `Error getting post: ${errorMessage}` }], 126 | }, 127 | }; 128 | } 129 | }, 130 | create_post: async (params: CreatePostParams) => { 131 | try { 132 | const response = await makeWordPressRequest('POST', "posts", params); 133 | const post: WPPost = response; 134 | return { 135 | toolResult: { 136 | content: [{ type: 'text', text: JSON.stringify(post, null, 2) }], 137 | }, 138 | }; 139 | } catch (error: any) { 140 | const errorMessage = error.response?.data?.message || error.message; 141 | return { 142 | toolResult: { 143 | isError: true, 144 | content: [{ type: 'text', text: `Error creating post: ${errorMessage}` }], 145 | }, 146 | }; 147 | } 148 | }, 149 | update_post: async (params: UpdatePostParams) => { 150 | try { 151 | const { id, ...updateData } = params; 152 | const response = await makeWordPressRequest('POST', `posts/${id}`, updateData); 153 | const post: WPPost = response; 154 | return { 155 | toolResult: { 156 | content: [{ type: 'text', text: JSON.stringify(post, null, 2) }], 157 | }, 158 | }; 159 | } catch (error: any) { 160 | const errorMessage = error.response?.data?.message || error.message; 161 | return { 162 | toolResult: { 163 | isError: true, 164 | content: [{ type: 'text', text: `Error updating post: ${errorMessage}` }], 165 | }, 166 | }; 167 | } 168 | }, 169 | delete_post: async (params: DeletePostParams) => { 170 | try { 171 | const response = await makeWordPressRequest('DELETE', `posts/${params.id}`, { force: params.force }); 172 | const post: WPPost = response; 173 | return { 174 | toolResult: { 175 | content: [{ type: 'text', text: JSON.stringify(post, null, 2) }], 176 | }, 177 | }; 178 | } catch (error: any) { 179 | const errorMessage = error.response?.data?.message || error.message; 180 | return { 181 | toolResult: { 182 | isError: true, 183 | content: [{ type: 'text', text: `Error deleting post: ${errorMessage}` }], 184 | }, 185 | }; 186 | } 187 | } 188 | }; -------------------------------------------------------------------------------- /src/tools/users.ts: -------------------------------------------------------------------------------- 1 | // src/tools/users.ts 2 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 3 | import { makeWordPressRequest } from '../wordpress.js'; 4 | import { WPUser } from '../types/wordpress-types.js'; 5 | import { z } from 'zod'; 6 | import { zodToJsonSchema } from 'zod-to-json-schema'; 7 | 8 | const listUsersSchema = z.object({ 9 | page: z.number().optional().describe("Page number (default 1)"), 10 | per_page: z.number().min(1).max(100).optional().describe("Items per page (default 10, max 100)"), 11 | search: z.string().optional().describe("Search term for user content or name"), 12 | context: z.enum(['view', 'embed', 'edit']).optional().describe("Scope under which the request is made"), 13 | orderby: z.enum(['id', 'include', 'name', 'registered_date', 'slug', 'email', 'url']).optional().describe("Sort users by parameter"), 14 | order: z.enum(['asc', 'desc']).optional().describe("Order sort attribute ascending or descending"), 15 | roles: z.array(z.string()).optional().describe("Array of role names to filter by") 16 | }); 17 | 18 | const getUserSchema = z.object({ 19 | id: z.number().describe("User ID"), 20 | context: z.enum(['view', 'embed', 'edit']).optional().describe("Scope under which the request is made") 21 | }).strict(); 22 | 23 | const createUserSchema = z.object({ 24 | username: z.string().describe("User login name"), 25 | name: z.string().optional().describe("Display name for the user"), 26 | first_name: z.string().optional().describe("First name for the user"), 27 | last_name: z.string().optional().describe("Last name for the user"), 28 | email: z.string().email().describe("Email address for the user"), 29 | url: z.string().url().optional().describe("URL of the user"), 30 | description: z.string().optional().describe("Description of the user"), 31 | locale: z.string().optional().describe("Locale for the user"), 32 | nickname: z.string().optional().describe("Nickname for the user"), 33 | slug: z.string().optional().describe("Slug for the user"), 34 | roles: z.array(z.string()).optional().describe("Roles assigned to the user"), 35 | password: z.string().describe("Password for the user") 36 | }).strict(); 37 | 38 | const updateUserSchema = z.object({ 39 | id: z.number().describe("User ID"), 40 | username: z.string().optional().describe("User login name"), 41 | name: z.string().optional().describe("Display name for the user"), 42 | first_name: z.string().optional().describe("First name for the user"), 43 | last_name: z.string().optional().describe("Last name for the user"), 44 | email: z.string().email().optional().describe("Email address for the user"), 45 | url: z.string().url().optional().describe("URL of the user"), 46 | description: z.string().optional().describe("Description of the user"), 47 | locale: z.string().optional().describe("Locale for the user"), 48 | nickname: z.string().optional().describe("Nickname for the user"), 49 | slug: z.string().optional().describe("Slug for the user"), 50 | roles: z.array(z.string()).optional().describe("Roles assigned to the user"), 51 | password: z.string().optional().describe("Password for the user") 52 | }).strict(); 53 | 54 | const deleteUserSchema = z.object({ 55 | id: z.number().describe("User ID"), 56 | force: z.boolean().optional().describe("Whether to bypass trash and force deletion"), 57 | reassign: z.number().optional().describe("User ID to reassign posts to") 58 | }).strict(); 59 | 60 | type ListUsersParams = z.infer; 61 | type GetUserParams = z.infer; 62 | type CreateUserParams = z.infer; 63 | type UpdateUserParams = z.infer; 64 | type DeleteUserParams = z.infer; 65 | 66 | export const userTools: Tool[] = [ 67 | { 68 | name: "list_users", 69 | description: "Lists all users with filtering, sorting, and pagination options", 70 | inputSchema: { type: "object", properties: listUsersSchema.shape } 71 | }, 72 | { 73 | name: "get_user", 74 | description: "Gets a user by ID", 75 | inputSchema: { type: "object", properties: getUserSchema.shape } 76 | }, 77 | { 78 | name: "create_user", 79 | description: "Creates a new user", 80 | inputSchema: { type: "object", properties: createUserSchema.shape } 81 | }, 82 | { 83 | name: "update_user", 84 | description: "Updates an existing user", 85 | inputSchema: { type: "object", properties: updateUserSchema.shape } 86 | }, 87 | { 88 | name: "delete_user", 89 | description: "Deletes a user", 90 | inputSchema: { type: "object", properties: deleteUserSchema.shape } 91 | } 92 | ]; 93 | 94 | export const userHandlers = { 95 | list_users: async (params: ListUsersParams) => { 96 | try { 97 | const response = await makeWordPressRequest('GET', "users", params); 98 | const users: WPUser[] = response; 99 | return { 100 | toolResult: { 101 | content: [{ type: 'text', text: JSON.stringify(users, null, 2) }], 102 | }, 103 | }; 104 | } catch (error: any) { 105 | const errorMessage = error.response?.data?.message || error.message; 106 | return { 107 | toolResult: { 108 | isError: true, 109 | content: [{ type: 'text', text: `Error listing users: ${errorMessage}` }], 110 | }, 111 | }; 112 | } 113 | }, 114 | get_user: async (params: GetUserParams) => { 115 | try { 116 | const response = await makeWordPressRequest('GET', `users/${params.id}`, { context: params.context }); 117 | const user: WPUser = response; 118 | return { 119 | toolResult: { 120 | content: [{ type: 'text', text: JSON.stringify(user, null, 2) }], 121 | }, 122 | }; 123 | } catch (error: any) { 124 | const errorMessage = error.response?.data?.message || error.message; 125 | return { 126 | toolResult: { 127 | isError: true, 128 | content: [{ type: 'text', text: `Error getting user: ${errorMessage}` }], 129 | }, 130 | }; 131 | } 132 | }, 133 | create_user: async (params: CreateUserParams) => { 134 | try { 135 | const response = await makeWordPressRequest('POST', "users", params); 136 | const user: WPUser = response; 137 | return { 138 | toolResult: { 139 | content: [{ type: 'text', text: JSON.stringify(user, null, 2) }], 140 | }, 141 | }; 142 | } catch (error: any) { 143 | const errorMessage = error.response?.data?.message || error.message; 144 | return { 145 | toolResult: { 146 | isError: true, 147 | content: [{ type: 'text', text: `Error creating user: ${errorMessage}` }], 148 | }, 149 | }; 150 | } 151 | }, 152 | update_user: async (params: UpdateUserParams) => { 153 | try { 154 | const { id, ...updateData } = params; 155 | const response = await makeWordPressRequest('POST', `users/${id}`, updateData); 156 | const user: WPUser = response; 157 | return { 158 | toolResult: { 159 | content: [{ type: 'text', text: JSON.stringify(user, null, 2) }], 160 | }, 161 | }; 162 | } catch (error: any) { 163 | const errorMessage = error.response?.data?.message || error.message; 164 | return { 165 | toolResult: { 166 | isError: true, 167 | content: [{ type: 'text', text: `Error updating user: ${errorMessage}` }], 168 | }, 169 | }; 170 | } 171 | }, 172 | delete_user: async (params: DeleteUserParams) => { 173 | try { 174 | const response = await makeWordPressRequest('DELETE', `users/${params.id}`, { 175 | force: params.force, 176 | reassign: params.reassign 177 | }); 178 | const user: WPUser = response; 179 | return { 180 | toolResult: { 181 | content: [{ type: 'text', text: JSON.stringify(user, null, 2) }], 182 | }, 183 | }; 184 | } catch (error: any) { 185 | const errorMessage = error.response?.data?.message || error.message; 186 | return { 187 | toolResult: { 188 | isError: true, 189 | content: [{ type: 'text', text: `Error deleting user: ${errorMessage}` }], 190 | }, 191 | }; 192 | } 193 | } 194 | }; 195 | -------------------------------------------------------------------------------- /src/types/wordpress-types.ts: -------------------------------------------------------------------------------- 1 | // src/types/wordpress-types.ts 2 | 3 | // Common fields for WordPress Posts and Pages 4 | interface WPContent { 5 | id: number; 6 | date: string; 7 | date_gmt: string; 8 | guid: { 9 | rendered: string; 10 | }; 11 | modified: string; 12 | modified_gmt: string; 13 | slug: string; 14 | status: string; 15 | type: string; 16 | link: string; 17 | title: { 18 | rendered: string; 19 | }; 20 | content: { 21 | rendered: string; 22 | protected: boolean; 23 | }; 24 | excerpt: { 25 | rendered: string; 26 | protected: boolean; 27 | }; 28 | author: number; 29 | featured_media: number; 30 | comment_status: string; 31 | ping_status: string; 32 | template: string; 33 | meta: Record[]; 34 | _links: Record; 35 | } 36 | 37 | // WordPress Post type 38 | export interface WPPost extends WPContent { 39 | format: string; 40 | sticky: boolean; 41 | categories: number[]; 42 | tags: number[]; 43 | } 44 | 45 | // WordPress Page type 46 | export interface WPPage extends WPContent { 47 | parent: number; 48 | menu_order: number; 49 | } 50 | 51 | // WordPress Category type 52 | export interface WPCategory { 53 | id: number; 54 | count: number; 55 | description: string; 56 | link: string; 57 | name: string; 58 | slug: string; 59 | taxonomy: string; 60 | parent: number; 61 | meta: Record[]; 62 | _links: Record; 63 | } 64 | 65 | // WordPress User type 66 | export interface WPUser { 67 | id: number; 68 | username: string; 69 | name: string; 70 | first_name: string; 71 | last_name: string; 72 | email: string; 73 | url: string; 74 | description: string; 75 | link: string; 76 | locale: string; 77 | nickname: string; 78 | slug: string; 79 | roles: string[]; 80 | registered_date: string; 81 | capabilities: Record; 82 | extra_capabilities: Record; 83 | avatar_urls: Record; 84 | meta: Record[]; 85 | _links: Record; 86 | } 87 | 88 | // WordPress Plugin type 89 | export interface WPPlugin { 90 | plugin: string; 91 | status: string; 92 | name: string; 93 | plugin_uri: string; 94 | author: string; 95 | author_uri: string; 96 | description: { 97 | raw: string; 98 | rendered: string; 99 | }; 100 | version: string; 101 | network_only: boolean; 102 | requires_wp: string; 103 | requires_php: string; 104 | textdomain: string; 105 | } 106 | 107 | // WordPress Comment type 108 | export interface WPComment { 109 | id: number; 110 | post: number; 111 | parent: number; 112 | author: number; 113 | author_name: string; 114 | author_url: string; 115 | author_email?: string; // May not be returned based on permissions 116 | author_ip?: string; // May not be returned based on permissions 117 | author_user_agent?: string; // May not be returned based on permissions 118 | date: string; 119 | date_gmt: string; 120 | content: { 121 | rendered: string; 122 | raw?: string; // May not be returned based on permissions 123 | }; 124 | link: string; 125 | status: string; 126 | type: string; 127 | meta: Record[]; 128 | _links: Record; 129 | } -------------------------------------------------------------------------------- /src/wordpress.ts: -------------------------------------------------------------------------------- 1 | // src/wordpress.ts 2 | import * as dotenv from 'dotenv'; 3 | import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | 7 | // Global WordPress API client instance 8 | let wpClient: AxiosInstance; 9 | 10 | /** 11 | * Initialize the WordPress API client with authentication 12 | */ 13 | export async function initWordPress() { 14 | const apiUrl = process.env.WORDPRESS_API_URL; 15 | const username = process.env.WORDPRESS_USERNAME; 16 | const appPassword = process.env.WORDPRESS_PASSWORD; 17 | 18 | if (!apiUrl) { 19 | throw new Error('WordPress API URL not found in environment variables'); 20 | } 21 | 22 | // Ensure the API URL has the WordPress REST API path 23 | let baseURL = apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`; 24 | 25 | // Add the WordPress REST API path if not already included 26 | if (!baseURL.includes('/wp-json/wp/v2')) { 27 | baseURL = baseURL + 'wp-json/wp/v2/'; 28 | } else if (!baseURL.endsWith('/')) { 29 | // Ensure the URL ends with a trailing slash 30 | baseURL = baseURL + '/'; 31 | } 32 | 33 | const config: AxiosRequestConfig = { 34 | baseURL, 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | }, 38 | }; 39 | 40 | // Add authentication if credentials are provided 41 | if (username && appPassword) { 42 | logToFile('Adding authentication headers'); 43 | logToFile(`Username: ${username}`); 44 | logToFile(`App Password: ${appPassword}`); 45 | 46 | const auth = Buffer.from(`${username}:${appPassword}`).toString('base64'); 47 | config.headers = { 48 | ...config.headers, 49 | 'Authorization': `Basic ${auth}` 50 | }; 51 | } 52 | 53 | wpClient = axios.create(config); 54 | 55 | // Verify connection to WordPress API 56 | try { 57 | await wpClient.get(''); 58 | logToFile('Successfully connected to WordPress API'); 59 | } catch (error: any) { 60 | logToFile(`Failed to connect to WordPress API: ${error.message}`); 61 | throw new Error(`Failed to connect to WordPress API: ${error.message}`); 62 | } 63 | } 64 | 65 | // Configure logging 66 | const META_URL = import.meta.url.replace(/^file:/, ''); 67 | const LOG_DIR = path.join(META_URL.split('/').slice(0, -1).join('/'), '../logs'); 68 | const LOG_FILE = path.join(LOG_DIR, 'wordpress-api.log'); 69 | 70 | // Ensure log directory exists 71 | if (!fs.existsSync(LOG_DIR)) { 72 | fs.mkdirSync(LOG_DIR, { recursive: true }); 73 | } 74 | 75 | export function logToFile(message: string) { 76 | const timestamp = new Date().toISOString(); 77 | const logMessage = `[${timestamp}] ${message}\n`; 78 | fs.appendFileSync(LOG_FILE, logMessage); 79 | } 80 | 81 | /** 82 | * Make a request to the WordPress API 83 | * @param method HTTP method 84 | * @param endpoint API endpoint (relative to the baseURL) 85 | * @param data Request data 86 | * @param options Additional request options 87 | * @returns Response data 88 | */ 89 | export async function makeWordPressRequest( 90 | method: string, 91 | endpoint: string, 92 | data?: any, 93 | options?: { 94 | headers?: Record; 95 | isFormData?: boolean; 96 | rawResponse?: boolean; 97 | } 98 | ) { 99 | if (!wpClient) { 100 | throw new Error('WordPress client not initialized'); 101 | } 102 | 103 | // Log data (skip for FormData which can't be stringified) 104 | if (!options?.isFormData) { 105 | logToFile(`Data: ${JSON.stringify(data, null, 2)}`); 106 | } else { 107 | logToFile('Request contains FormData (not shown in logs)'); 108 | } 109 | 110 | // Handle potential leading slash in endpoint 111 | const path = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; 112 | 113 | try { 114 | const fullUrl = `${wpClient.defaults.baseURL}${path}`; 115 | 116 | // Prepare request config 117 | const requestConfig: any = { 118 | method, 119 | url: path, 120 | headers: options?.headers || {} 121 | }; 122 | 123 | // Handle different data formats based on method and options 124 | if (method === 'GET') { 125 | requestConfig.params = data; 126 | } else if (options?.isFormData) { 127 | // For FormData, pass it directly without stringifying 128 | requestConfig.data = data; 129 | } else if (method === 'POST') { 130 | requestConfig.data = JSON.stringify(data); 131 | } else { 132 | requestConfig.data = data; 133 | } 134 | 135 | const requestLog = ` 136 | REQUEST: 137 | URL: ${fullUrl} 138 | Method: ${method} 139 | Headers: ${JSON.stringify({...wpClient.defaults.headers, ...requestConfig.headers}, null, 2)} 140 | Data: ${options?.isFormData ? '(FormData not shown)' : JSON.stringify(data, null, 2)} 141 | `; 142 | logToFile(requestLog); 143 | 144 | const response = await wpClient.request(requestConfig); 145 | 146 | const responseLog = ` 147 | RESPONSE: 148 | Status: ${response.status} 149 | Data: ${JSON.stringify(response.data, null, 2)} 150 | `; 151 | logToFile(responseLog); 152 | 153 | return options?.rawResponse ? response : response.data; 154 | } catch (error: any) { 155 | const errorLog = ` 156 | ERROR: 157 | Message: ${error.message} 158 | Status: ${error.response?.status || 'N/A'} 159 | Data: ${JSON.stringify(error.response?.data || {}, null, 2)} 160 | `; 161 | console.error(errorLog); 162 | logToFile(errorLog); 163 | throw error; 164 | } 165 | } 166 | 167 | /** 168 | * Make a request to the WordPress.org Plugin Repository API 169 | * @param searchQuery Search query string 170 | * @param page Page number (1-based) 171 | * @param perPage Number of results per page 172 | * @returns Response data from WordPress.org Plugin API 173 | */ 174 | export async function searchWordPressPluginRepository(searchQuery: string, page: number = 1, perPage: number = 10) { 175 | try { 176 | // WordPress.org Plugin API endpoint 177 | const apiUrl = 'https://api.wordpress.org/plugins/info/1.2/'; 178 | 179 | // Build the request data according to WordPress.org Plugin API format 180 | const requestData = { 181 | action: 'query_plugins', 182 | request: { 183 | search: searchQuery, 184 | page: page, 185 | per_page: perPage, 186 | fields: { 187 | description: true, 188 | sections: false, 189 | tested: true, 190 | requires: true, 191 | rating: true, 192 | ratings: false, 193 | downloaded: true, 194 | downloadlink: true, 195 | last_updated: true, 196 | homepage: true, 197 | tags: true 198 | } 199 | } 200 | }; 201 | 202 | const requestLog = ` 203 | WORDPRESS.ORG PLUGIN API REQUEST: 204 | URL: ${apiUrl} 205 | Data: ${JSON.stringify(requestData, null, 2)} 206 | `; 207 | logToFile(requestLog); 208 | 209 | const response = await axios.post(apiUrl, requestData, { 210 | headers: { 211 | 'Content-Type': 'application/json' 212 | } 213 | }); 214 | 215 | const responseLog = ` 216 | WORDPRESS.ORG PLUGIN API RESPONSE: 217 | Status: ${response.status} 218 | Info: ${JSON.stringify(response.data.info, null, 2)} 219 | Plugins Count: ${response.data.plugins?.length || 0} 220 | `; 221 | logToFile(responseLog); 222 | 223 | return response.data; 224 | } catch (error: any) { 225 | const errorLog = ` 226 | WORDPRESS.ORG PLUGIN API ERROR: 227 | Message: ${error.message} 228 | Status: ${error.response?.status || 'N/A'} 229 | Data: ${JSON.stringify(error.response?.data || {}, null, 2)} 230 | `; 231 | console.error(errorLog); 232 | logToFile(errorLog); 233 | throw error; 234 | } 235 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "outDir": "./build", 11 | "rootDir": "./src", 12 | "declaration": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "build"] 16 | } --------------------------------------------------------------------------------