diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..80cebd9 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,33 @@ +{ + "inputs": [ + { + "type": "promptString", + "id": "wordpress-api-url", + "description": "WordPress API URL", + "default": "https://your-wordpress-site.com" + }, + { + "type": "promptString", + "id": "wordpress-username", + "description": "WordPress Username" + }, + { + "type": "promptString", + "id": "wordpress-password", + "description": "WordPress Application Password", + "password": true + } + ], + "servers": { + "WordPress MCP": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@instawp/mcp-wp"], + "env": { + "WORDPRESS_API_URL": "${input:wordpress-api-url}", + "WORDPRESS_USERNAME": "${input:wordpress-username}", + "WORDPRESS_PASSWORD": "${input:wordpress-password}" + } + } + } +} diff --git a/README.md b/README.md index d0a1dd9..c1f00b8 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,20 @@ # WordPress MCP Server -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. +This is a Model Context Protocol (MCP) server for WordPress, allowing you to interact with your WordPress site using natural language via MCP-compatible clients like Claude for Desktop and Visual Studio Code. This server exposes various WordPress data and functionality as MCP tools. -## Usage +## Usage -### Claude Desktop +### Visual Studio Code + +1. Make sure you have VS Code version 1.99 or later installed. +2. Ensure you have GitHub Copilot Chat extension installed and configured. +3. In your workspace, create a `.vscode/mcp.json` file (or use the one provided in this repository). +4. When you first use the MCP server, VS Code will prompt you for your WordPress credentials. +5. Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P) and run **MCP: List Servers** to verify the server is configured. +6. Open the Chat view (Ctrl+Alt+I / Cmd+Option+I) and select **Agent** mode from the dropdown. +7. You can now interact with your WordPress site through the chat interface. + +### Claude Desktop 1. Download and install [Claude Desktop](https://claude.ai/download). 2. Open Claude Desktop settings and navigate to the "Developer" tab. @@ -60,7 +70,7 @@ This server currently provides tools to interact with core WordPress data: * `activate_plugin`: Activate a plugin. * `deactivate_plugin`: Deactivate a plugin. * `create_plugin`: Create a new plugin. - + More features and endpoints will be added in future updates. @@ -87,7 +97,9 @@ WORDPRESS_PASSWORD=wp_app_password * **Node.js and npm:** Ensure you have Node.js (version 18 or higher) and npm installed. * **WordPress Site:** You need an active WordPress site with the REST API enabled. * **WordPress API Authentication:** Set up authentication for the WordPress REST API. This typically requires an authentication plugin or method (like Application Passwords). -* **MCP Client:** You need an application that can communicate with the MCP Server. Currently, Claude Desktop is recommended. +* **MCP Client:** You need an application that can communicate with the MCP Server. Supported clients include: + * **Visual Studio Code** (version 1.99+) with GitHub Copilot Chat extension + * **Claude Desktop** ### Installation and Setup @@ -122,7 +134,47 @@ WORDPRESS_PASSWORD=wp_app_password npm run build ``` -5. **Configure Claude Desktop:** +5. **Configure VS Code:** + + * Create a `.vscode/mcp.json` file in your workspace with the following content: + ```json + { + "inputs": [ + { + "type": "promptString", + "id": "wordpress-api-url", + "description": "WordPress API URL", + "default": "https://your-wordpress-site.com" + }, + { + "type": "promptString", + "id": "wordpress-username", + "description": "WordPress Username" + }, + { + "type": "promptString", + "id": "wordpress-password", + "description": "WordPress Application Password", + "password": true + } + ], + "servers": { + "WordPress MCP": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@instawp/mcp-wp"], + "env": { + "WORDPRESS_API_URL": "${input:wordpress-api-url}", + "WORDPRESS_USERNAME": "${input:wordpress-username}", + "WORDPRESS_PASSWORD": "${input:wordpress-password}" + } + } + } + } + ``` + * VS Code will prompt you for your WordPress credentials when you first use the MCP server. + +6. **Configure Claude Desktop (Optional):** * Open Claude Desktop settings and navigate to the "Developer" tab. * Click "Edit Config" to open the `claude_desktop_config.json` file. @@ -131,8 +183,19 @@ WORDPRESS_PASSWORD=wp_app_password ### Running the Server +#### With VS Code + +1. Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P) and run **MCP: List Servers**. +2. Select the "WordPress MCP" server and click "Start". +3. Open the Chat view (Ctrl+Alt+I / Cmd+Option+I) and select **Agent** mode from the dropdown. +4. You can now interact with your WordPress site through the chat interface. + +#### With Claude Desktop + Once you've configured Claude Desktop, the server should start automatically whenever Claude Desktop starts. +#### From Command Line + You can also run the server directly from the command line for testing: ```bash @@ -177,3 +240,44 @@ wp/ ### Contribution Feel free to open issues or make pull requests to improve this project. + +## Using MCP Tools in VS Code + +### VS Code Extension + +This project includes a dedicated VS Code extension that makes it even easier to work with the WordPress MCP server. The extension provides: + +- Easy configuration of WordPress credentials +- Commands for common WordPress operations +- Automatic MCP server management +- Seamless integration with GitHub Copilot Chat + +To use the extension: + +1. Navigate to the `vscode-extension` directory +2. Run `npm install` to install dependencies +3. Run `npm run compile` to build the extension +4. Press F5 in VS Code to launch a new window with the extension loaded +5. Use the "WordPress MCP: Configure Server" command to set up your WordPress credentials +6. Use the "WordPress MCP: Start Server" command to start the MCP server + +For more details, see the [VS Code Extension README](./vscode-extension/README.md). + +### Using with GitHub Copilot Chat + +When using the WordPress MCP server with VS Code, you can interact with your WordPress site through the GitHub Copilot Chat interface in agent mode. Here are some example prompts you can use: + +- "List all posts on my WordPress site" +- "Create a new page titled 'About Us' with the following content: ..." +- "Show me all active plugins on my WordPress site" +- "Upload a new media item from this URL: ..." +- "List all users with administrator role" +- "Create a new category called 'Tutorials'" +- "Update the post with ID 123 to change its title to 'New Title'" + +VS Code will automatically invoke the appropriate MCP tools based on your natural language requests. You can also directly reference specific tools by using the `#` symbol followed by the tool name, for example: + +- "Use #list_posts to show me all published posts" +- "Use #create_page to create a new page" + +For more information on using MCP tools in VS Code, refer to the [VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). diff --git a/package.json b/package.json index a9f06e4..6f5f02b 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,10 @@ "start": "node ./build/server.js", "dev": "tsx watch src/server.ts", "clean": "rimraf build", - "prepare": "npm run build" + "prepare": "npm run build", + "build:extension": "cd vscode-extension && npm run compile", + "package:extension": "cd vscode-extension && npm run package", + "vscode:prepublish": "npm run build && npm run build:extension" }, "keywords": [ "wordpress", @@ -24,7 +27,9 @@ "server", "claude", "ai", - "instawp" + "instawp", + "vscode", + "vscode-extension" ], "author": "Claude", "license": "GPL-3.0-or-later", @@ -49,6 +54,7 @@ "files": [ "build", "README.md", - "LICENSE" + "LICENSE", + "vscode-extension" ] } \ No newline at end of file diff --git a/vscode-extension/.gitignore b/vscode-extension/.gitignore new file mode 100644 index 0000000..db93e0e --- /dev/null +++ b/vscode-extension/.gitignore @@ -0,0 +1,5 @@ +out +node_modules +.vscode-test/ +*.vsix +dist diff --git a/vscode-extension/.vscode/launch.json b/vscode-extension/.vscode/launch.json new file mode 100644 index 0000000..5557cd2 --- /dev/null +++ b/vscode-extension/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": [ + "${workspaceFolder}/out/test/**/*.js" + ], + "preLaunchTask": "npm: test-watch" + } + ] +} diff --git a/vscode-extension/.vscode/tasks.json b/vscode-extension/.vscode/tasks.json new file mode 100644 index 0000000..9bc55d5 --- /dev/null +++ b/vscode-extension/.vscode/tasks.json @@ -0,0 +1,28 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$ts-webpack-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "type": "npm", + "script": "test-watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": "build" + } + ] +} diff --git a/vscode-extension/.vscodeignore b/vscode-extension/.vscodeignore new file mode 100644 index 0000000..d88cd66 --- /dev/null +++ b/vscode-extension/.vscodeignore @@ -0,0 +1,11 @@ +.vscode/** +.vscode-test/** +src/** +.gitignore +.yarnrc +webpack.config.js +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts diff --git a/vscode-extension/README.md b/vscode-extension/README.md new file mode 100644 index 0000000..a23822a --- /dev/null +++ b/vscode-extension/README.md @@ -0,0 +1,92 @@ +# WordPress MCP for VS Code + +This extension integrates WordPress with Visual Studio Code using the Machine Conversation Protocol (MCP). It allows you to interact with your WordPress site directly from VS Code using natural language through GitHub Copilot Chat. + +## Features + +- Seamless integration with GitHub Copilot Chat in VS Code +- Easy configuration of WordPress credentials +- Commands for common WordPress operations +- Automatic MCP server management + +## Requirements + +- Visual Studio Code 1.99.0 or higher +- GitHub Copilot Chat extension +- Node.js 18.0.0 or higher +- A WordPress site with REST API enabled +- WordPress application password for authentication + +## Installation + +1. Install this extension from the VS Code Marketplace +2. Configure your WordPress credentials using the "WordPress MCP: Configure Server" command +3. Start the MCP server using the "WordPress MCP: Start Server" command + +## Usage + +### Configuration + +1. Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P) +2. Run "WordPress MCP: Configure Server" +3. Enter your WordPress API URL, username, and application password + +### Starting the Server + +1. Open the Command Palette +2. Run "WordPress MCP: Start Server" + +### Using with GitHub Copilot Chat + +1. Open the Chat view (Ctrl+Alt+I / Cmd+Option+I) +2. Select "Agent" mode from the dropdown +3. Enter natural language queries about your WordPress site + +Example queries: +- "List all posts on my WordPress site" +- "Create a new page titled 'About Us'" +- "Show me all active plugins" +- "Upload a new media item from this URL: ..." + +### Quick Commands + +This extension provides several commands for common WordPress operations: + +- **WordPress MCP: List Posts** - Shows all posts on your site +- **WordPress MCP: Create New Post** - Creates a new post +- **WordPress MCP: List Pages** - Shows all pages on your site +- **WordPress MCP: Create New Page** - Creates a new page +- **WordPress MCP: List Plugins** - Shows all plugins on your site +- **WordPress MCP: List Media** - Shows all media items on your site + +## Extension Settings + +This extension contributes the following settings: + +* `wordpress-mcp.apiUrl`: WordPress API URL +* `wordpress-mcp.username`: WordPress Username +* `wordpress-mcp.password`: WordPress Application Password +* `wordpress-mcp.autoStart`: Automatically start the WordPress MCP server when VS Code starts + +## Known Issues + +- The extension requires GitHub Copilot Chat to be installed and configured +- Some WordPress operations may require administrator privileges + +## Release Notes + +### 0.1.0 + +Initial release of WordPress MCP for VS Code + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This extension is licensed under the GPL-3.0 License. + +--- + +**Powered by [InstaWP](https://instawp.com/)** diff --git a/vscode-extension/package.json b/vscode-extension/package.json new file mode 100644 index 0000000..923deaa --- /dev/null +++ b/vscode-extension/package.json @@ -0,0 +1,185 @@ +{ + "name": "vscode-wordpress-mcp", + "displayName": "WordPress MCP", + "description": "WordPress integration for VS Code using Machine Conversation Protocol", + "version": "0.1.0", + "engines": { + "vscode": "^1.99.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onStartupFinished" + ], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "wordpress-mcp.startServer", + "title": "WordPress MCP: Start Server" + }, + { + "command": "wordpress-mcp.stopServer", + "title": "WordPress MCP: Stop Server" + }, + { + "command": "wordpress-mcp.restartServer", + "title": "WordPress MCP: Restart Server" + }, + { + "command": "wordpress-mcp.configureServer", + "title": "WordPress MCP: Configure Server" + }, + { + "command": "wordpress-mcp.showQuickActions", + "title": "WordPress MCP: Show Quick Actions" + }, + { + "command": "wordpress-mcp.listPosts", + "title": "WordPress MCP: List Posts" + }, + { + "command": "wordpress-mcp.createPost", + "title": "WordPress MCP: Create New Post" + }, + { + "command": "wordpress-mcp.listPages", + "title": "WordPress MCP: List Pages" + }, + { + "command": "wordpress-mcp.createPage", + "title": "WordPress MCP: Create New Page" + }, + { + "command": "wordpress-mcp.listPlugins", + "title": "WordPress MCP: List Plugins" + }, + { + "command": "wordpress-mcp.listMedia", + "title": "WordPress MCP: List Media" + }, + { + "command": "wordpress-mcp.addSiteProfile", + "title": "WordPress MCP: Add Site Profile" + }, + { + "command": "wordpress-mcp.editSiteProfile", + "title": "WordPress MCP: Edit Site Profile" + }, + { + "command": "wordpress-mcp.deleteSiteProfile", + "title": "WordPress MCP: Delete Site Profile" + }, + { + "command": "wordpress-mcp.switchSiteProfile", + "title": "WordPress MCP: Switch Site Profile" + }, + { + "command": "wordpress-mcp.listSiteProfiles", + "title": "WordPress MCP: List Site Profiles" + }, + { + "command": "wordpress-mcp.browseThemes", + "title": "WordPress MCP: Browse Themes" + }, + { + "command": "wordpress-mcp.activateTheme", + "title": "WordPress MCP: Activate Theme" + }, + { + "command": "wordpress-mcp.toggleDebugMode", + "title": "WordPress MCP: Toggle Debug Mode" + }, + { + "command": "wordpress-mcp.viewDebugLogs", + "title": "WordPress MCP: View Debug Logs" + }, + { + "command": "wordpress-mcp.runSqlQuery", + "title": "WordPress MCP: Run SQL Query" + }, + { + "command": "wordpress-mcp.optimizeDatabase", + "title": "WordPress MCP: Optimize Database" + }, + { + "command": "wordpress-mcp.manageUsers", + "title": "WordPress MCP: Manage Users" + }, + { + "command": "wordpress-mcp.createUser", + "title": "WordPress MCP: Create User" + }, + { + "command": "wordpress-mcp.siteHealthCheck", + "title": "WordPress MCP: Site Health Check" + }, + { + "command": "wordpress-mcp.clearCache", + "title": "WordPress MCP: Clear Cache" + } + ], + "configuration": { + "title": "WordPress MCP", + "properties": { + "wordpress-mcp.apiUrl": { + "type": "string", + "default": "", + "description": "WordPress API URL" + }, + "wordpress-mcp.username": { + "type": "string", + "default": "", + "description": "WordPress Username" + }, + "wordpress-mcp.password": { + "type": "string", + "default": "", + "description": "WordPress Application Password" + }, + "wordpress-mcp.autoStart": { + "type": "boolean", + "default": false, + "description": "Automatically start the WordPress MCP server when VS Code starts" + }, + "wordpress-mcp.siteName": { + "type": "string", + "default": "WordPress", + "description": "Display name for the WordPress site in the status bar" + } + } + } + }, + "scripts": { + "vscode:prepublish": "npm run package", + "compile": "webpack", + "watch": "webpack --watch", + "package": "webpack --mode production --devtool hidden-source-map", + "compile-tests": "tsc -p . --outDir out", + "watch-tests": "tsc -p . -w --outDir out", + "pretest": "npm run compile-tests && npm run compile && npm run lint", + "lint": "eslint src --ext ts", + "test": "node ./out/test/runTest.js" + }, + "devDependencies": { + "@types/vscode": "^1.99.0", + "@types/glob": "^8.1.0", + "@types/mocha": "^10.0.6", + "@types/node": "20.x", + "@typescript-eslint/eslint-plugin": "^6.19.1", + "@typescript-eslint/parser": "^6.19.1", + "eslint": "^8.56.0", + "glob": "^10.3.10", + "mocha": "^10.2.0", + "typescript": "^5.3.3", + "ts-loader": "^9.5.1", + "webpack": "^5.90.0", + "webpack-cli": "^5.1.4", + "@vscode/test-electron": "^2.3.9" + }, + "dependencies": { + "@instawp/mcp-wp": "^0.0.3", + "uuid": "^9.0.1" + } +} diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts new file mode 100644 index 0000000..912be6a --- /dev/null +++ b/vscode-extension/src/extension.ts @@ -0,0 +1,701 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { spawn, ChildProcess } from 'child_process'; +import { SiteProfileManager } from './siteProfileManager'; +import { registerSiteCommands } from './siteCommands'; +import { registerWordPressCommands } from './wordpressCommands'; + +let serverProcess: ChildProcess | undefined; +let outputChannel: vscode.OutputChannel; +let statusBarItem: vscode.StatusBarItem; +let siteProfileManager: SiteProfileManager; + +// Server status type +type ServerStatus = 'running' | 'starting' | 'stopped' | 'error'; + +export function activate(context: vscode.ExtensionContext) { + console.log('WordPress MCP extension is now active'); + + // Create output channel for the extension + outputChannel = vscode.window.createOutputChannel('WordPress MCP'); + + // Initialize site profile manager + siteProfileManager = new SiteProfileManager(context); + + // Create status bar item + createStatusBarItem(context); + + // Register commands + const startServerCommand = vscode.commands.registerCommand('wordpress-mcp.startServer', startServer); + const stopServerCommand = vscode.commands.registerCommand('wordpress-mcp.stopServer', stopServer); + const restartServerCommand = vscode.commands.registerCommand('wordpress-mcp.restartServer', restartServer); + const configureServerCommand = vscode.commands.registerCommand('wordpress-mcp.configureServer', configureServer); + const showQuickActionsCommand = vscode.commands.registerCommand('wordpress-mcp.showQuickActions', showQuickActions); + + // Register site profile commands + registerSiteCommands(context, siteProfileManager); + + // Register WordPress-specific commands + registerWordPressCommands(context); + + // Register WordPress-specific commands + const listPostsCommand = vscode.commands.registerCommand('wordpress-mcp.listPosts', listPosts); + const createPostCommand = vscode.commands.registerCommand('wordpress-mcp.createPost', createPost); + const listPagesCommand = vscode.commands.registerCommand('wordpress-mcp.listPages', listPages); + const createPageCommand = vscode.commands.registerCommand('wordpress-mcp.createPage', createPage); + const listPluginsCommand = vscode.commands.registerCommand('wordpress-mcp.listPlugins', listPlugins); + const listMediaCommand = vscode.commands.registerCommand('wordpress-mcp.listMedia', listMedia); + + // Add commands to subscriptions + context.subscriptions.push( + startServerCommand, + stopServerCommand, + restartServerCommand, + configureServerCommand, + showQuickActionsCommand, + listPostsCommand, + createPostCommand, + listPagesCommand, + createPageCommand, + listPluginsCommand, + listMediaCommand, + outputChannel, + statusBarItem + ); + + // Auto-start server if configured + const config = vscode.workspace.getConfiguration('wordpress-mcp'); + if (config.get('autoStart')) { + startServer(); + } + + // Create or update MCP configuration file + ensureMcpConfig(context); +} + +export function deactivate() { + // Stop the server when the extension is deactivated + stopServer(); + + // Dispose of the status bar item + if (statusBarItem) { + statusBarItem.dispose(); + } +} + +// Create and initialize the status bar item +function createStatusBarItem(context: vscode.ExtensionContext) { + statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); + statusBarItem.command = 'wordpress-mcp.showQuickActions'; + updateStatusBar('stopped'); +} + +// Update the status bar appearance based on server status +function updateStatusBar(status: ServerStatus) { + if (!statusBarItem) { + return; + } + + const config = vscode.workspace.getConfiguration('wordpress-mcp'); + const siteName = config.get('siteName') || 'WordPress'; + + switch (status) { + case 'running': + statusBarItem.text = `$(check) ${siteName}`; + statusBarItem.tooltip = `WordPress MCP Server is running (${siteName})`; + break; + case 'starting': + statusBarItem.text = `$(sync~spin) ${siteName}`; + statusBarItem.tooltip = `WordPress MCP Server is starting... (${siteName})`; + break; + case 'error': + statusBarItem.text = `$(error) ${siteName}`; + statusBarItem.tooltip = `WordPress MCP Server encountered an error (${siteName})`; + break; + case 'stopped': + default: + statusBarItem.text = `$(circle-slash) ${siteName}`; + statusBarItem.tooltip = `WordPress MCP Server is stopped (${siteName})`; + break; + } + + statusBarItem.show(); +} + +// Show quick actions menu for WordPress operations +async function showQuickActions() { + const isRunning = !!serverProcess; + const currentProfile = siteProfileManager.getCurrentProfile(); + const siteName = currentProfile?.name || 'WordPress'; + + const actions = [ + { + label: isRunning ? '$(stop) Stop Server' : '$(play) Start Server', + description: isRunning ? 'Stop the WordPress MCP server' : 'Start the WordPress MCP server', + action: isRunning ? 'stop' : 'start' + }, + { + label: '$(refresh) Restart Server', + description: 'Restart the WordPress MCP server', + action: 'restart' + }, + { + label: '$(gear) Configure Server', + description: 'Configure WordPress credentials', + action: 'configure' + }, + { + label: '$(output) Show Server Output', + description: 'Show the server output log', + action: 'output' + }, + { kind: vscode.QuickPickItemKind.Separator, label: 'Site Profiles' }, + { + label: '$(globe) Switch Site', + description: `Current: ${siteName}`, + action: 'switchSite' + }, + { + label: '$(add) Add Site Profile', + description: 'Add a new WordPress site profile', + action: 'addSite' + }, + { + label: '$(edit) Edit Site Profile', + description: 'Edit an existing WordPress site profile', + action: 'editSite' + }, + { + label: '$(list-unordered) List Site Profiles', + description: 'View all WordPress site profiles', + action: 'listSites' + }, + { kind: vscode.QuickPickItemKind.Separator, label: 'WordPress Content' }, + { + label: '$(file-text) List Posts', + description: 'List all posts on your WordPress site', + action: 'listPosts' + }, + { + label: '$(new-file) Create New Post', + description: 'Create a new post on your WordPress site', + action: 'createPost' + }, + { + label: '$(file) List Pages', + description: 'List all pages on your WordPress site', + action: 'listPages' + }, + { + label: '$(new-file) Create New Page', + description: 'Create a new page on your WordPress site', + action: 'createPage' + }, + { + label: '$(extensions) List Plugins', + description: 'List all plugins on your WordPress site', + action: 'listPlugins' + }, + { + label: '$(file-media) List Media', + description: 'List all media items on your WordPress site', + action: 'listMedia' + }, + { kind: vscode.QuickPickItemKind.Separator, label: 'Advanced WordPress' }, + { + label: '$(paintcan) Browse Themes', + description: 'Browse and preview WordPress themes', + action: 'browseThemes' + }, + { + label: '$(bug) Toggle Debug Mode', + description: 'Enable or disable WordPress debug mode', + action: 'toggleDebugMode' + }, + { + label: '$(database) Run SQL Query', + description: 'Run an SQL query against the WordPress database', + action: 'runSqlQuery' + }, + { + label: '$(person) Manage Users', + description: 'Manage WordPress users', + action: 'manageUsers' + }, + { + label: '$(pulse) Site Health Check', + description: 'Run a WordPress site health check', + action: 'siteHealthCheck' + }, + { + label: '$(clear-all) Clear Cache', + description: 'Clear WordPress caches', + action: 'clearCache' + } + ]; + + const selectedItem = await vscode.window.showQuickPick(actions, { + placeHolder: 'Select a WordPress MCP action' + }); + + if (!selectedItem) { + return; // User cancelled + } + + // Execute the selected action + switch (selectedItem.action) { + case 'start': + startServer(); + break; + case 'stop': + stopServer(); + break; + case 'restart': + restartServer(); + break; + case 'configure': + configureServer(); + break; + case 'output': + outputChannel.show(); + break; + // Site profile actions + case 'switchSite': + vscode.commands.executeCommand('wordpress-mcp.switchSiteProfile'); + break; + case 'addSite': + vscode.commands.executeCommand('wordpress-mcp.addSiteProfile'); + break; + case 'editSite': + vscode.commands.executeCommand('wordpress-mcp.editSiteProfile'); + break; + case 'listSites': + vscode.commands.executeCommand('wordpress-mcp.listSiteProfiles'); + break; + // WordPress content actions + case 'listPosts': + listPosts(); + break; + case 'createPost': + createPost(); + break; + case 'listPages': + listPages(); + break; + case 'createPage': + createPage(); + break; + case 'listPlugins': + listPlugins(); + break; + case 'listMedia': + listMedia(); + break; + // Advanced WordPress actions + case 'browseThemes': + vscode.commands.executeCommand('wordpress-mcp.browseThemes'); + break; + case 'toggleDebugMode': + vscode.commands.executeCommand('wordpress-mcp.toggleDebugMode'); + break; + case 'runSqlQuery': + vscode.commands.executeCommand('wordpress-mcp.runSqlQuery'); + break; + case 'manageUsers': + vscode.commands.executeCommand('wordpress-mcp.manageUsers'); + break; + case 'siteHealthCheck': + vscode.commands.executeCommand('wordpress-mcp.siteHealthCheck'); + break; + case 'clearCache': + vscode.commands.executeCommand('wordpress-mcp.clearCache'); + break; + } +} + +async function startServer() { + if (serverProcess) { + vscode.window.showInformationMessage('WordPress MCP server is already running'); + return; + } + + try { + // Update status bar to show starting state + updateStatusBar('starting'); + + // Get configuration + const config = vscode.workspace.getConfiguration('wordpress-mcp'); + const apiUrl = config.get('apiUrl'); + const username = config.get('username'); + const password = config.get('password'); + + // Check if configuration is complete + if (!apiUrl || !username || !password) { + updateStatusBar('stopped'); + const configureNow = 'Configure Now'; + const response = await vscode.window.showWarningMessage( + 'WordPress MCP server is not fully configured. Please configure it before starting.', + configureNow + ); + + if (response === configureNow) { + configureServer(); + } + return; + } + + // Start the server process + outputChannel.appendLine('Starting WordPress MCP server...'); + + // Use npx to run the MCP server + serverProcess = spawn('npx', ['-y', '@instawp/mcp-wp'], { + env: { + ...process.env, + WORDPRESS_API_URL: apiUrl, + WORDPRESS_USERNAME: username, + WORDPRESS_PASSWORD: password + } + }); + + // Handle server output + serverProcess.stdout?.on('data', (data) => { + const output = data.toString(); + outputChannel.append(output); + + // Check for successful startup message + if (output.includes('WordPress MCP Server running') || + output.includes('initialized successfully')) { + updateStatusBar('running'); + } + }); + + serverProcess.stderr?.on('data', (data) => { + outputChannel.append(data.toString()); + // If we get stderr output, there might be an issue + if (!data.toString().includes('warning')) { // Ignore warnings + updateStatusBar('error'); + } + }); + + // Handle server exit + serverProcess.on('close', (code) => { + outputChannel.appendLine(`WordPress MCP server exited with code ${code}`); + serverProcess = undefined; + updateStatusBar('stopped'); + + if (code !== 0) { + vscode.window.showErrorMessage(`WordPress MCP server exited with code ${code}`); + } + }); + + // Set a timeout to check if server started successfully + setTimeout(() => { + if (serverProcess) { + vscode.window.showInformationMessage('WordPress MCP server started'); + updateStatusBar('running'); + } + }, 3000); + + outputChannel.show(); + + } catch (error) { + updateStatusBar('error'); + vscode.window.showErrorMessage(`Failed to start WordPress MCP server: ${error}`); + outputChannel.appendLine(`Error: ${error}`); + } +} + +function stopServer() { + if (!serverProcess) { + vscode.window.showInformationMessage('WordPress MCP server is not running'); + return; + } + + try { + outputChannel.appendLine('Stopping WordPress MCP server...'); + updateStatusBar('stopped'); + serverProcess.kill(); + serverProcess = undefined; + vscode.window.showInformationMessage('WordPress MCP server stopped'); + } catch (error) { + updateStatusBar('error'); + vscode.window.showErrorMessage(`Failed to stop WordPress MCP server: ${error}`); + outputChannel.appendLine(`Error: ${error}`); + } +} + +async function restartServer() { + await stopServer(); + setTimeout(() => { + startServer(); + }, 1000); // Wait for 1 second before restarting +} + +async function configureServer() { + // Check if we have any profiles + const profiles = siteProfileManager.getProfiles(); + const currentProfile = siteProfileManager.getCurrentProfile(); + + if (profiles.length === 0) { + // No profiles exist, create one + const createProfile = 'Create Profile'; + const response = await vscode.window.showInformationMessage( + 'No WordPress site profiles found. Would you like to create one?', + createProfile, 'Cancel' + ); + + if (response === createProfile) { + vscode.commands.executeCommand('wordpress-mcp.addSiteProfile'); + } + return; + } + + // If we have multiple profiles, ask which one to configure + if (profiles.length > 1 && !currentProfile) { + const switchProfile = 'Switch Profile'; + const response = await vscode.window.showInformationMessage( + 'Multiple WordPress site profiles found. Would you like to switch to a specific profile?', + switchProfile, 'Cancel' + ); + + if (response === switchProfile) { + vscode.commands.executeCommand('wordpress-mcp.switchSiteProfile'); + } + return; + } + + // If we have a current profile, edit it + if (currentProfile) { + vscode.commands.executeCommand('wordpress-mcp.editSiteProfile'); + return; + } + + // If we have only one profile, use it + if (profiles.length === 1) { + await siteProfileManager.switchProfile(profiles[0].id); + vscode.commands.executeCommand('wordpress-mcp.editSiteProfile'); + return; + } + + // Fallback to the old configuration method + const config = vscode.workspace.getConfiguration('wordpress-mcp'); + + // Prompt for API URL + const apiUrl = await vscode.window.showInputBox({ + prompt: 'Enter WordPress API URL', + value: config.get('apiUrl') || '', + placeHolder: 'https://your-wordpress-site.com' + }); + + if (apiUrl === undefined) return; // User cancelled + + // Prompt for username + const username = await vscode.window.showInputBox({ + prompt: 'Enter WordPress Username', + value: config.get('username') || '' + }); + + if (username === undefined) return; // User cancelled + + // Prompt for password + const password = await vscode.window.showInputBox({ + prompt: 'Enter WordPress Application Password', + value: config.get('password') || '', + password: true + }); + + if (password === undefined) return; // User cancelled + + // Ask for a site name + const name = await vscode.window.showInputBox({ + prompt: 'Enter a name for this WordPress site', + value: 'My WordPress Site' + }); + + if (name === undefined) return; // User cancelled + + // Create a new profile + try { + const profile = await siteProfileManager.addProfile({ + name, + apiUrl, + username, + password + }); + + vscode.window.showInformationMessage(`WordPress site "${name}" added successfully`); + + // Ask if user wants to start the server + const startNow = 'Start Now'; + const response = await vscode.window.showInformationMessage( + 'Do you want to start the WordPress MCP server now?', + startNow, 'Later' + ); + + if (response === startNow) { + startServer(); + } + } catch (error) { + vscode.window.showErrorMessage(`Failed to add WordPress site: ${error}`); + } +} + +// Ensure the .vscode/mcp.json file exists and is up to date +function ensureMcpConfig(context: vscode.ExtensionContext) { + if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) { + return; // No workspace open + } + + // Check if we have a current profile + const currentProfile = siteProfileManager.getCurrentProfile(); + + if (currentProfile) { + // Use the current profile to update the MCP config + const workspaceFolder = vscode.workspace.workspaceFolders[0].uri.fsPath; + const vscodeFolder = path.join(workspaceFolder, '.vscode'); + const mcpConfigPath = path.join(vscodeFolder, 'mcp.json'); + + // Create .vscode folder if it doesn't exist + if (!fs.existsSync(vscodeFolder)) { + fs.mkdirSync(vscodeFolder, { recursive: true }); + } + + // Create or update mcp.json + const mcpConfig = { + "inputs": [ + { + "type": "promptString", + "id": "wordpress-api-url", + "description": "WordPress API URL", + "default": currentProfile.apiUrl + }, + { + "type": "promptString", + "id": "wordpress-username", + "description": "WordPress Username", + "default": currentProfile.username + }, + { + "type": "promptString", + "id": "wordpress-password", + "description": "WordPress Application Password", + "password": true, + "default": currentProfile.password + } + ], + "servers": { + [`WordPress MCP - ${currentProfile.name}`]: { + "type": "stdio", + "command": "npx", + "args": ["-y", "@instawp/mcp-wp"], + "env": { + "WORDPRESS_API_URL": "${input:wordpress-api-url}", + "WORDPRESS_USERNAME": "${input:wordpress-username}", + "WORDPRESS_PASSWORD": "${input:wordpress-password}" + } + } + } + }; + + fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2)); + } else { + // No profile exists, create a default MCP config + const workspaceFolder = vscode.workspace.workspaceFolders[0].uri.fsPath; + const vscodeFolder = path.join(workspaceFolder, '.vscode'); + const mcpConfigPath = path.join(vscodeFolder, 'mcp.json'); + + // Create .vscode folder if it doesn't exist + if (!fs.existsSync(vscodeFolder)) { + fs.mkdirSync(vscodeFolder, { recursive: true }); + } + + // Create or update mcp.json + const mcpConfig = { + "inputs": [ + { + "type": "promptString", + "id": "wordpress-api-url", + "description": "WordPress API URL", + "default": "https://your-wordpress-site.com" + }, + { + "type": "promptString", + "id": "wordpress-username", + "description": "WordPress Username" + }, + { + "type": "promptString", + "id": "wordpress-password", + "description": "WordPress Application Password", + "password": true + } + ], + "servers": { + "WordPress MCP": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@instawp/mcp-wp"], + "env": { + "WORDPRESS_API_URL": "${input:wordpress-api-url}", + "WORDPRESS_USERNAME": "${input:wordpress-username}", + "WORDPRESS_PASSWORD": "${input:wordpress-password}" + } + } + } + }; + + fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2)); + } +} + +// WordPress-specific commands that use the Copilot Chat interface +async function listPosts() { + await openChatWithPrompt("List all posts on my WordPress site"); +} + +async function createPost() { + const title = await vscode.window.showInputBox({ + prompt: 'Enter post title', + placeHolder: 'My New Post' + }); + + if (!title) return; // User cancelled + + await openChatWithPrompt(`Create a new post titled "${title}" on my WordPress site`); +} + +async function listPages() { + await openChatWithPrompt("List all pages on my WordPress site"); +} + +async function createPage() { + const title = await vscode.window.showInputBox({ + prompt: 'Enter page title', + placeHolder: 'About Us' + }); + + if (!title) return; // User cancelled + + await openChatWithPrompt(`Create a new page titled "${title}" on my WordPress site`); +} + +async function listPlugins() { + await openChatWithPrompt("List all plugins on my WordPress site"); +} + +async function listMedia() { + await openChatWithPrompt("List all media items on my WordPress site"); +} + +// Helper function to open Copilot Chat with a specific prompt +async function openChatWithPrompt(prompt: string) { + // First, make sure the chat view is open + await vscode.commands.executeCommand('workbench.action.chat.open'); + + // Then, set the chat to agent mode + await vscode.commands.executeCommand('workbench.action.chat.selectMode', 'agent'); + + // Finally, send the prompt + await vscode.commands.executeCommand('workbench.action.chat.sendRequest', prompt); +} diff --git a/vscode-extension/src/siteCommands.ts b/vscode-extension/src/siteCommands.ts new file mode 100644 index 0000000..f26be81 --- /dev/null +++ b/vscode-extension/src/siteCommands.ts @@ -0,0 +1,429 @@ +import * as vscode from 'vscode'; +import { SiteProfileManager, WordPressSiteProfile } from './siteProfileManager'; + +/** + * Register commands for managing WordPress site profiles + */ +export function registerSiteCommands(context: vscode.ExtensionContext, profileManager: SiteProfileManager) { + // Add a new site profile + const addSiteCommand = vscode.commands.registerCommand('wordpress-mcp.addSiteProfile', async () => { + await addSiteProfile(profileManager); + }); + + // Edit an existing site profile + const editSiteCommand = vscode.commands.registerCommand('wordpress-mcp.editSiteProfile', async () => { + await editSiteProfile(profileManager); + }); + + // Delete a site profile + const deleteSiteCommand = vscode.commands.registerCommand('wordpress-mcp.deleteSiteProfile', async () => { + await deleteSiteProfile(profileManager); + }); + + // Switch between site profiles + const switchSiteCommand = vscode.commands.registerCommand('wordpress-mcp.switchSiteProfile', async () => { + await switchSiteProfile(profileManager); + }); + + // List all site profiles + const listSitesCommand = vscode.commands.registerCommand('wordpress-mcp.listSiteProfiles', async () => { + await listSiteProfiles(profileManager); + }); + + // Add commands to subscriptions + context.subscriptions.push( + addSiteCommand, + editSiteCommand, + deleteSiteCommand, + switchSiteCommand, + listSitesCommand + ); +} + +/** + * Add a new WordPress site profile + */ +async function addSiteProfile(profileManager: SiteProfileManager): Promise { + // Get site name + const name = await vscode.window.showInputBox({ + prompt: 'Enter a name for this WordPress site', + placeHolder: 'My WordPress Site', + validateInput: (value) => { + return value.trim() ? null : 'Site name is required'; + } + }); + + if (!name) { + return; // User cancelled + } + + // Get API URL + const apiUrl = await vscode.window.showInputBox({ + prompt: 'Enter the WordPress API URL', + placeHolder: 'https://your-wordpress-site.com', + validateInput: (value) => { + try { + new URL(value); + return null; + } catch (e) { + return 'Please enter a valid URL'; + } + } + }); + + if (!apiUrl) { + return; // User cancelled + } + + // Get username + const username = await vscode.window.showInputBox({ + prompt: 'Enter WordPress Username', + validateInput: (value) => { + return value.trim() ? null : 'Username is required'; + } + }); + + if (!username) { + return; // User cancelled + } + + // Get password + const password = await vscode.window.showInputBox({ + prompt: 'Enter WordPress Application Password', + password: true, + validateInput: (value) => { + return value.trim() ? null : 'Password is required'; + } + }); + + if (!password) { + return; // User cancelled + } + + // Create the profile + try { + const profile = await profileManager.addProfile({ + name, + apiUrl, + username, + password + }); + + vscode.window.showInformationMessage(`WordPress site "${name}" added successfully`); + + // Ask if user wants to switch to this profile + const switchNow = 'Switch Now'; + const response = await vscode.window.showInformationMessage( + `Do you want to switch to "${name}" now?`, + switchNow, 'Later' + ); + + if (response === switchNow) { + await profileManager.switchProfile(profile.id); + vscode.window.showInformationMessage(`Switched to WordPress site "${name}"`); + } + } catch (error) { + vscode.window.showErrorMessage(`Failed to add WordPress site: ${error}`); + } +} + +/** + * Edit an existing WordPress site profile + */ +async function editSiteProfile(profileManager: SiteProfileManager): Promise { + const profiles = profileManager.getProfiles(); + + if (profiles.length === 0) { + vscode.window.showInformationMessage('No WordPress site profiles found. Add one first.'); + return; + } + + // Let user select a profile to edit + const items = profiles.map(p => ({ + label: p.name, + description: p.apiUrl, + detail: p.isDefault ? 'Default site' : undefined, + profile: p + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a WordPress site to edit' + }); + + if (!selected) { + return; // User cancelled + } + + const profile = selected.profile; + + // Get updated site name + const name = await vscode.window.showInputBox({ + prompt: 'Enter a name for this WordPress site', + value: profile.name, + validateInput: (value) => { + return value.trim() ? null : 'Site name is required'; + } + }); + + if (!name) { + return; // User cancelled + } + + // Get updated API URL + const apiUrl = await vscode.window.showInputBox({ + prompt: 'Enter the WordPress API URL', + value: profile.apiUrl, + validateInput: (value) => { + try { + new URL(value); + return null; + } catch (e) { + return 'Please enter a valid URL'; + } + } + }); + + if (!apiUrl) { + return; // User cancelled + } + + // Get updated username + const username = await vscode.window.showInputBox({ + prompt: 'Enter WordPress Username', + value: profile.username, + validateInput: (value) => { + return value.trim() ? null : 'Username is required'; + } + }); + + if (!username) { + return; // User cancelled + } + + // Get updated password + const password = await vscode.window.showInputBox({ + prompt: 'Enter WordPress Application Password (leave empty to keep current)', + password: true + }); + + if (password === undefined) { + return; // User cancelled + } + + // Update the profile + try { + await profileManager.updateProfile(profile.id, { + name, + apiUrl, + username, + ...(password ? { password } : {}) + }); + + vscode.window.showInformationMessage(`WordPress site "${name}" updated successfully`); + + // If this is the current profile, ask if user wants to reload settings + const currentProfile = profileManager.getCurrentProfile(); + if (currentProfile && currentProfile.id === profile.id) { + const reloadNow = 'Reload Now'; + const response = await vscode.window.showInformationMessage( + `Do you want to reload the settings for "${name}" now?`, + reloadNow, 'Later' + ); + + if (response === reloadNow) { + await profileManager.switchProfile(profile.id); + vscode.window.showInformationMessage(`Reloaded settings for WordPress site "${name}"`); + } + } + } catch (error) { + vscode.window.showErrorMessage(`Failed to update WordPress site: ${error}`); + } +} + +/** + * Delete a WordPress site profile + */ +async function deleteSiteProfile(profileManager: SiteProfileManager): Promise { + const profiles = profileManager.getProfiles(); + + if (profiles.length === 0) { + vscode.window.showInformationMessage('No WordPress site profiles found.'); + return; + } + + // Let user select a profile to delete + const items = profiles.map(p => ({ + label: p.name, + description: p.apiUrl, + detail: p.isDefault ? 'Default site' : undefined, + profile: p + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a WordPress site to delete' + }); + + if (!selected) { + return; // User cancelled + } + + const profile = selected.profile; + + // Confirm deletion + const confirmDelete = 'Delete'; + const response = await vscode.window.showWarningMessage( + `Are you sure you want to delete the WordPress site "${profile.name}"?`, + { modal: true }, + confirmDelete, 'Cancel' + ); + + if (response !== confirmDelete) { + return; // User cancelled + } + + // Delete the profile + try { + await profileManager.deleteProfile(profile.id); + vscode.window.showInformationMessage(`WordPress site "${profile.name}" deleted successfully`); + } catch (error) { + vscode.window.showErrorMessage(`Failed to delete WordPress site: ${error}`); + } +} + +/** + * Switch between WordPress site profiles + */ +async function switchSiteProfile(profileManager: SiteProfileManager): Promise { + const profiles = profileManager.getProfiles(); + + if (profiles.length === 0) { + vscode.window.showInformationMessage('No WordPress site profiles found. Add one first.'); + return; + } + + const currentProfile = profileManager.getCurrentProfile(); + + // Let user select a profile to switch to + const items = profiles.map(p => ({ + label: p.name, + description: p.apiUrl, + detail: p.id === currentProfile?.id ? 'Current site' : (p.isDefault ? 'Default site' : undefined), + profile: p + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a WordPress site to switch to' + }); + + if (!selected) { + return; // User cancelled + } + + const profile = selected.profile; + + // Switch to the selected profile + try { + await profileManager.switchProfile(profile.id); + vscode.window.showInformationMessage(`Switched to WordPress site "${profile.name}"`); + } catch (error) { + vscode.window.showErrorMessage(`Failed to switch WordPress site: ${error}`); + } +} + +/** + * List all WordPress site profiles + */ +async function listSiteProfiles(profileManager: SiteProfileManager): Promise { + const profiles = profileManager.getProfiles(); + + if (profiles.length === 0) { + vscode.window.showInformationMessage('No WordPress site profiles found. Add one first.'); + return; + } + + const currentProfile = profileManager.getCurrentProfile(); + + // Create a markdown table of profiles + const tableHeader = '| Name | URL | Status |\n| ---- | --- | ------ |\n'; + const tableRows = profiles.map(p => { + const status = []; + if (p.id === currentProfile?.id) { + status.push('Current'); + } + if (p.isDefault) { + status.push('Default'); + } + + return `| ${p.name} | ${p.apiUrl} | ${status.join(', ') || '-'} |`; + }).join('\n'); + + const markdown = `# WordPress Site Profiles\n\n${tableHeader}${tableRows}`; + + // Show the profiles in a webview + const panel = vscode.window.createWebviewPanel( + 'wordpressSiteProfiles', + 'WordPress Site Profiles', + vscode.ViewColumn.One, + { + enableScripts: false + } + ); + + panel.webview.html = ` + + + + + + WordPress Site Profiles + + + +

WordPress Site Profiles

+ + + + + + + + + + ${profiles.map(p => { + const status = []; + if (p.id === currentProfile?.id) { + status.push('Current'); + } + if (p.isDefault) { + status.push('Default'); + } + + return ` + + + + `; + }).join('')} + +
NameURLStatus
${p.name}${p.apiUrl}${status.join(', ') || '-'}
+ + + `; +} diff --git a/vscode-extension/src/siteProfileManager.ts b/vscode-extension/src/siteProfileManager.ts new file mode 100644 index 0000000..271b8e3 --- /dev/null +++ b/vscode-extension/src/siteProfileManager.ts @@ -0,0 +1,252 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; +import { v4 as uuidv4 } from 'uuid'; + +/** + * Interface representing a WordPress site profile + */ +export interface WordPressSiteProfile { + id: string; + name: string; + apiUrl: string; + username: string; + password: string; + isDefault?: boolean; + lastConnected?: string; // ISO date string + customSettings?: Record; +} + +/** + * Manager for WordPress site profiles + */ +export class SiteProfileManager { + private context: vscode.ExtensionContext; + private currentProfileId: string | null = null; + private static readonly PROFILES_KEY = 'wordpressSiteProfiles'; + private static readonly CURRENT_PROFILE_KEY = 'currentWordPressSiteProfileId'; + + constructor(context: vscode.ExtensionContext) { + this.context = context; + this.currentProfileId = this.context.globalState.get(SiteProfileManager.CURRENT_PROFILE_KEY, null); + } + + /** + * Get all site profiles + */ + public getProfiles(): WordPressSiteProfile[] { + return this.context.globalState.get(SiteProfileManager.PROFILES_KEY, []); + } + + /** + * Add a new site profile + */ + public async addProfile(profile: Omit): Promise { + const profiles = this.getProfiles(); + + // Generate a unique ID for the profile + const newProfile: WordPressSiteProfile = { + ...profile, + id: uuidv4(), + lastConnected: new Date().toISOString() + }; + + // If this is the first profile, make it the default + if (profiles.length === 0) { + newProfile.isDefault = true; + } + + profiles.push(newProfile); + await this.context.globalState.update(SiteProfileManager.PROFILES_KEY, profiles); + + // If this is the first profile or it's marked as default, set it as current + if (newProfile.isDefault) { + await this.switchProfile(newProfile.id); + } + + return newProfile; + } + + /** + * Update an existing site profile + */ + public async updateProfile(id: string, updates: Partial>): Promise { + const profiles = this.getProfiles(); + const index = profiles.findIndex(p => p.id === id); + + if (index === -1) { + return false; + } + + profiles[index] = { + ...profiles[index], + ...updates + }; + + await this.context.globalState.update(SiteProfileManager.PROFILES_KEY, profiles); + + // If this is the current profile, update the VS Code settings + if (this.currentProfileId === id) { + await this.updateVSCodeSettings(profiles[index]); + } + + return true; + } + + /** + * Delete a site profile + */ + public async deleteProfile(id: string): Promise { + const profiles = this.getProfiles(); + const index = profiles.findIndex(p => p.id === id); + + if (index === -1) { + return false; + } + + const wasDefault = profiles[index].isDefault; + profiles.splice(index, 1); + + // If we deleted the default profile, set a new default if possible + if (wasDefault && profiles.length > 0) { + profiles[0].isDefault = true; + } + + await this.context.globalState.update(SiteProfileManager.PROFILES_KEY, profiles); + + // If we deleted the current profile, switch to the new default or clear current + if (this.currentProfileId === id) { + if (profiles.length > 0) { + const newDefault = profiles.find(p => p.isDefault) || profiles[0]; + await this.switchProfile(newDefault.id); + } else { + this.currentProfileId = null; + await this.context.globalState.update(SiteProfileManager.CURRENT_PROFILE_KEY, null); + await this.clearVSCodeSettings(); + } + } + + return true; + } + + /** + * Get the current site profile + */ + public getCurrentProfile(): WordPressSiteProfile | null { + if (!this.currentProfileId) { + return null; + } + + const profiles = this.getProfiles(); + return profiles.find(p => p.id === this.currentProfileId) || null; + } + + /** + * Switch to a different site profile + */ + public async switchProfile(profileId: string): Promise { + const profiles = this.getProfiles(); + const profile = profiles.find(p => p.id === profileId); + + if (!profile) { + return false; + } + + this.currentProfileId = profileId; + await this.context.globalState.update(SiteProfileManager.CURRENT_PROFILE_KEY, profileId); + + // Update the profile's last connected timestamp + await this.updateProfile(profileId, { + lastConnected: new Date().toISOString() + }); + + // Update VS Code settings for this profile + await this.updateVSCodeSettings(profile); + + // Update MCP configuration for this profile + await this.updateMcpConfig(profile); + + return true; + } + + /** + * Update VS Code settings for a profile + */ + private async updateVSCodeSettings(profile: WordPressSiteProfile): Promise { + const config = vscode.workspace.getConfiguration('wordpress-mcp'); + + await config.update('apiUrl', profile.apiUrl, vscode.ConfigurationTarget.Global); + await config.update('username', profile.username, vscode.ConfigurationTarget.Global); + await config.update('password', profile.password, vscode.ConfigurationTarget.Global); + await config.update('siteName', profile.name, vscode.ConfigurationTarget.Global); + } + + /** + * Clear VS Code settings + */ + private async clearVSCodeSettings(): Promise { + const config = vscode.workspace.getConfiguration('wordpress-mcp'); + + await config.update('apiUrl', '', vscode.ConfigurationTarget.Global); + await config.update('username', '', vscode.ConfigurationTarget.Global); + await config.update('password', '', vscode.ConfigurationTarget.Global); + await config.update('siteName', 'WordPress', vscode.ConfigurationTarget.Global); + } + + /** + * Update MCP configuration for a profile + */ + private async updateMcpConfig(profile: WordPressSiteProfile): Promise { + if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) { + return; // No workspace open + } + + const workspaceFolder = vscode.workspace.workspaceFolders[0].uri.fsPath; + const vscodeFolder = path.join(workspaceFolder, '.vscode'); + const mcpConfigPath = path.join(vscodeFolder, 'mcp.json'); + + // Create .vscode folder if it doesn't exist + if (!fs.existsSync(vscodeFolder)) { + fs.mkdirSync(vscodeFolder, { recursive: true }); + } + + // Create or update mcp.json + const mcpConfig = { + "inputs": [ + { + "type": "promptString", + "id": "wordpress-api-url", + "description": "WordPress API URL", + "default": profile.apiUrl + }, + { + "type": "promptString", + "id": "wordpress-username", + "description": "WordPress Username", + "default": profile.username + }, + { + "type": "promptString", + "id": "wordpress-password", + "description": "WordPress Application Password", + "password": true, + "default": profile.password + } + ], + "servers": { + [`WordPress MCP - ${profile.name}`]: { + "type": "stdio", + "command": "npx", + "args": ["-y", "@instawp/mcp-wp"], + "env": { + "WORDPRESS_API_URL": "${input:wordpress-api-url}", + "WORDPRESS_USERNAME": "${input:wordpress-username}", + "WORDPRESS_PASSWORD": "${input:wordpress-password}" + } + } + } + }; + + fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2)); + } +} diff --git a/vscode-extension/src/test/runTest.ts b/vscode-extension/src/test/runTest.ts new file mode 100644 index 0000000..40b02c1 --- /dev/null +++ b/vscode-extension/src/test/runTest.ts @@ -0,0 +1,22 @@ +import * as path from 'path'; +import { runTests } from '@vscode/test-electron'; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to the extension test script + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + + // Download VS Code, unzip it and run the integration test + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch (err) { + console.error('Failed to run tests', err); + process.exit(1); + } +} + +main(); diff --git a/vscode-extension/src/test/suite/extension.test.ts b/vscode-extension/src/test/suite/extension.test.ts new file mode 100644 index 0000000..8e44611 --- /dev/null +++ b/vscode-extension/src/test/suite/extension.test.ts @@ -0,0 +1,26 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; + +suite('Extension Test Suite', () => { + vscode.window.showInformationMessage('Start all tests.'); + + test('Extension should be present', () => { + assert.ok(vscode.extensions.getExtension('vscode-wordpress-mcp')); + }); + + test('Commands should be registered', async () => { + const commands = await vscode.commands.getCommands(); + + // Check for our commands + assert.ok(commands.includes('wordpress-mcp.startServer')); + assert.ok(commands.includes('wordpress-mcp.stopServer')); + assert.ok(commands.includes('wordpress-mcp.restartServer')); + assert.ok(commands.includes('wordpress-mcp.configureServer')); + assert.ok(commands.includes('wordpress-mcp.listPosts')); + assert.ok(commands.includes('wordpress-mcp.createPost')); + assert.ok(commands.includes('wordpress-mcp.listPages')); + assert.ok(commands.includes('wordpress-mcp.createPage')); + assert.ok(commands.includes('wordpress-mcp.listPlugins')); + assert.ok(commands.includes('wordpress-mcp.listMedia')); + }); +}); diff --git a/vscode-extension/src/test/suite/index.ts b/vscode-extension/src/test/suite/index.ts new file mode 100644 index 0000000..bd78b77 --- /dev/null +++ b/vscode-extension/src/test/suite/index.ts @@ -0,0 +1,37 @@ +import * as path from 'path'; +import * as Mocha from 'mocha'; +import * as glob from 'glob'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true + }); + + const testsRoot = path.resolve(__dirname, '..'); + + return new Promise((c, e) => { + glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run(failures => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + e(err); + } + }); + }); +} diff --git a/vscode-extension/src/wordpressCommands.ts b/vscode-extension/src/wordpressCommands.ts new file mode 100644 index 0000000..7613c59 --- /dev/null +++ b/vscode-extension/src/wordpressCommands.ts @@ -0,0 +1,214 @@ +import * as vscode from 'vscode'; + +/** + * Register additional WordPress-specific commands + */ +export function registerWordPressCommands(context: vscode.ExtensionContext) { + // Theme management commands + const browsethemesCommand = vscode.commands.registerCommand('wordpress-mcp.browseThemes', browseThemes); + const activateThemeCommand = vscode.commands.registerCommand('wordpress-mcp.activateTheme', activateTheme); + + // Debug commands + const toggleDebugCommand = vscode.commands.registerCommand('wordpress-mcp.toggleDebugMode', toggleDebugMode); + const viewDebugLogsCommand = vscode.commands.registerCommand('wordpress-mcp.viewDebugLogs', viewDebugLogs); + + // Database commands + const runSqlQueryCommand = vscode.commands.registerCommand('wordpress-mcp.runSqlQuery', runSqlQuery); + const optimizeDatabaseCommand = vscode.commands.registerCommand('wordpress-mcp.optimizeDatabase', optimizeDatabase); + + // User management commands + const manageUsersCommand = vscode.commands.registerCommand('wordpress-mcp.manageUsers', manageUsers); + const createUserCommand = vscode.commands.registerCommand('wordpress-mcp.createUser', createUser); + + // Site health commands + const siteHealthCheckCommand = vscode.commands.registerCommand('wordpress-mcp.siteHealthCheck', siteHealthCheck); + const clearCacheCommand = vscode.commands.registerCommand('wordpress-mcp.clearCache', clearCache); + + // Add commands to subscriptions + context.subscriptions.push( + browsethemesCommand, + activateThemeCommand, + toggleDebugCommand, + viewDebugLogsCommand, + runSqlQueryCommand, + optimizeDatabaseCommand, + manageUsersCommand, + createUserCommand, + siteHealthCheckCommand, + clearCacheCommand + ); +} + +/** + * Helper function to open Copilot Chat with a specific prompt + */ +async function openChatWithPrompt(prompt: string) { + // First, make sure the chat view is open + await vscode.commands.executeCommand('workbench.action.chat.open'); + + // Then, set the chat to agent mode + await vscode.commands.executeCommand('workbench.action.chat.selectMode', 'agent'); + + // Finally, send the prompt + await vscode.commands.executeCommand('workbench.action.chat.sendRequest', prompt); +} + +/** + * Browse and preview themes + */ +async function browseThemes() { + await openChatWithPrompt("List all themes on my WordPress site and show their details"); +} + +/** + * Activate a theme + */ +async function activateTheme() { + const themeName = await vscode.window.showInputBox({ + prompt: 'Enter the theme name to activate', + placeHolder: 'twentytwentyfour' + }); + + if (!themeName) return; // User cancelled + + await openChatWithPrompt(`Activate the WordPress theme "${themeName}"`); +} + +/** + * Toggle WordPress debug mode + */ +async function toggleDebugMode() { + const options = ['Enable', 'Disable']; + const selected = await vscode.window.showQuickPick(options, { + placeHolder: 'Enable or disable WordPress debug mode?' + }); + + if (!selected) return; // User cancelled + + const action = selected === 'Enable' ? 'enable' : 'disable'; + await openChatWithPrompt(`${action} WordPress debug mode`); +} + +/** + * View WordPress debug logs + */ +async function viewDebugLogs() { + await openChatWithPrompt("Show the WordPress debug logs"); +} + +/** + * Run an SQL query against the WordPress database + */ +async function runSqlQuery() { + const query = await vscode.window.showInputBox({ + prompt: 'Enter SQL query to run', + placeHolder: 'SELECT * FROM wp_posts LIMIT 10', + validateInput: (value) => { + return value.trim() ? null : 'SQL query is required'; + } + }); + + if (!query) return; // User cancelled + + await openChatWithPrompt(`Run the following SQL query against the WordPress database: ${query}`); +} + +/** + * Optimize the WordPress database + */ +async function optimizeDatabase() { + const confirm = await vscode.window.showWarningMessage( + 'This will optimize the WordPress database tables. Continue?', + { modal: true }, + 'Optimize', 'Cancel' + ); + + if (confirm !== 'Optimize') return; // User cancelled + + await openChatWithPrompt("Optimize the WordPress database tables"); +} + +/** + * Manage WordPress users + */ +async function manageUsers() { + const options = [ + 'List all users', + 'List administrators', + 'List editors', + 'List authors', + 'List contributors', + 'List subscribers' + ]; + + const selected = await vscode.window.showQuickPick(options, { + placeHolder: 'Select a user management action' + }); + + if (!selected) return; // User cancelled + + await openChatWithPrompt(selected); +} + +/** + * Create a new WordPress user + */ +async function createUser() { + // Get username + const username = await vscode.window.showInputBox({ + prompt: 'Enter username', + validateInput: (value) => { + return value.trim() ? null : 'Username is required'; + } + }); + + if (!username) return; // User cancelled + + // Get email + const email = await vscode.window.showInputBox({ + prompt: 'Enter email address', + validateInput: (value) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(value) ? null : 'Please enter a valid email address'; + } + }); + + if (!email) return; // User cancelled + + // Get role + const roles = ['administrator', 'editor', 'author', 'contributor', 'subscriber']; + const role = await vscode.window.showQuickPick(roles, { + placeHolder: 'Select user role' + }); + + if (!role) return; // User cancelled + + await openChatWithPrompt(`Create a new WordPress user with username "${username}", email "${email}", and role "${role}"`); +} + +/** + * Run a site health check + */ +async function siteHealthCheck() { + await openChatWithPrompt("Run a WordPress site health check and show the results"); +} + +/** + * Clear WordPress caches + */ +async function clearCache() { + const options = [ + 'Clear all caches', + 'Clear object cache', + 'Clear page cache', + 'Clear transients' + ]; + + const selected = await vscode.window.showQuickPick(options, { + placeHolder: 'Select which cache to clear' + }); + + if (!selected) return; // User cancelled + + await openChatWithPrompt(`${selected} in WordPress`); +} diff --git a/vscode-extension/tsconfig.json b/vscode-extension/tsconfig.json new file mode 100644 index 0000000..8d7dc55 --- /dev/null +++ b/vscode-extension/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "outDir": "out", + "lib": ["ES2022"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "exclude": ["node_modules", ".vscode-test"] +} diff --git a/vscode-extension/vsc-extension-quickstart.md b/vscode-extension/vsc-extension-quickstart.md new file mode 100644 index 0000000..e1d81b1 --- /dev/null +++ b/vscode-extension/vsc-extension-quickstart.md @@ -0,0 +1,56 @@ +# Welcome to WordPress MCP for VS Code + +## What's in the folder + +* This folder contains all of the files necessary for your extension. +* `package.json` - this is the manifest file in which you declare your extension and command. +* `src/extension.ts` - this is the main file where you will provide the implementation of your commands. +* The `src` folder contains all of the source code for the extension. + +## Get up and running straight away + +* Press `F5` to open a new window with your extension loaded. +* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `WordPress MCP`. +* Set breakpoints in your code inside `src/extension.ts` to debug your extension. +* Find output from your extension in the debug console. + +## Make changes + +* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. +* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. + +## Explore the API + +* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. + +## Building the Extension + +* To build the extension, run `npm run compile`. +* To package the extension for distribution, run `npm run package`. + +## Publishing the Extension + +* To publish the extension to the VS Code Marketplace, you'll need to install the `vsce` tool: + ``` + npm install -g @vscode/vsce + ``` +* Then, you can package the extension: + ``` + vsce package + ``` +* This will create a `.vsix` file that you can install manually or publish to the marketplace. + +## Working with Markdown + +You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: + +* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux). +* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux). +* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets. + +## For more information + +* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) +* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) + +**Enjoy!** diff --git a/vscode-extension/webpack.config.js b/vscode-extension/webpack.config.js new file mode 100644 index 0000000..5f6b441 --- /dev/null +++ b/vscode-extension/webpack.config.js @@ -0,0 +1,39 @@ +//@ts-check + +'use strict'; + +const path = require('path'); + +/**@type {import('webpack').Configuration}*/ +const config = { + target: 'node', + entry: './src/extension.ts', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'extension.js', + libraryTarget: 'commonjs2', + devtoolModuleFilenameTemplate: '../[resource-path]' + }, + devtool: 'source-map', + externals: { + vscode: 'commonjs vscode' + }, + resolve: { + extensions: ['.ts', '.js'] + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + loader: 'ts-loader' + } + ] + } + ] + } +}; + +module.exports = config;