diff --git a/.gitignore b/.gitignore index b1484b4..7085b5d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build package-lock.json logs *.code-workspace +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md index 9b3059d..a54daf0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,6 +121,8 @@ Handles ALL taxonomies (categories, tags, custom taxonomies) with a single set o - **Comments** (`comments.ts`): Comment management (~5 tools) - **Plugins** (`plugins.ts`): Plugin activation/deactivation (~5 tools) - **Plugin Repository** (`plugin-repository.ts`): WordPress.org plugin search (~2 tools) +- **SQL Queries** (`sql-query.ts`): Execute read-only database queries (1 tool, requires custom endpoint) + - **Note**: Uses `/mcp/v1/query` endpoint by default; customize via `WORDPRESS_SQL_ENDPOINT` environment variable ### Key Features diff --git a/README.md b/README.md index 63481a7..d7f3ae4 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,10 @@ Handles ALL taxonomies (categories, tags, custom taxonomies) with a single set o * `deactivate_plugin`: Deactivate a plugin. * `create_plugin`: Create a new plugin. * **Plugin Repository:** - * `search_plugins`: Search for plugins in the WordPress.org repository. - * `get_plugin_info`: Get detailed information about a plugin from the repository. + * `search_plugins`: Search for plugins in the WordPress.org repository. + * `get_plugin_info`: Get detailed information about a plugin from the repository. +* **Database Queries:** + * `execute_sql_query`: Execute read-only SQL queries against the WordPress database (requires custom endpoint setup). ### **Key Advantages** @@ -117,6 +119,67 @@ Make sure you have a `.env` file in your current directory with the following va WORDPRESS_API_URL=https://your-wordpress-site.com WORDPRESS_USERNAME=wp_username WORDPRESS_PASSWORD=wp_app_password + +# Optional: Custom SQL query endpoint (default: /mcp/v1/query) +WORDPRESS_SQL_ENDPOINT=/mcp/v1/query +``` + +## Enabling SQL Query Tool (Optional) + +The `execute_sql_query` tool allows you to run read-only SQL queries against your WordPress database. This is an optional feature that requires adding a custom REST API endpoint to your WordPress site. + +**Security Notes:** +- This tool only accepts read-only queries (SELECT, WITH...SELECT, EXPLAIN) for safety +- Queries containing INSERT, UPDATE, DELETE, DROP, or other modifying statements will be rejected +- Multi-statement queries are blocked to prevent SQL injection +- Queries and results are logged to `logs/wordpress-api.log` - avoid including sensitive data in queries +- This tool requires admin-level permissions (`manage_options` capability) + +**Configuration:** By default, the tool expects the endpoint at `/mcp/v1/query`. You can customize this by setting the `WORDPRESS_SQL_ENDPOINT` environment variable (e.g., `WORDPRESS_SQL_ENDPOINT=/custom/v1/query`). + +To enable this feature, add the following code to your WordPress site (via a custom plugin or your theme's `functions.php`): + +```php +add_action('rest_api_init', function() { + register_rest_route('mcp/v1', '/query', array( + 'methods' => 'POST', + 'callback' => function($request) { + global $wpdb; + + $query = $request->get_param('query'); + + // Additional security check + if (!current_user_can('manage_options')) { + return new WP_Error('unauthorized', 'Unauthorized', array('status' => 401)); + } + + // Only allow SELECT queries + if (stripos(trim($query), 'SELECT') !== 0) { + return new WP_Error('invalid_query', 'Only SELECT queries allowed', array('status' => 400)); + } + + $results = $wpdb->get_results($query, ARRAY_A); + + if ($wpdb->last_error) { + return new WP_Error('query_error', $wpdb->last_error, array('status' => 400)); + } + + return array( + 'results' => $results, + 'num_rows' => count($results) + ); + }, + 'permission_callback' => function() { + return current_user_can('manage_options'); + } + )); +}); +``` + +After adding this code, you can use the `execute_sql_query` tool to run queries like: + +```sql +SELECT * FROM wp_posts WHERE post_type = 'post' AND post_status = 'publish' LIMIT 10 ``` ## Development @@ -197,7 +260,7 @@ npm run dev The server uses a **unified tool architecture** to reduce complexity: -``` +```text src/ ├── server.ts # MCP server entry point ├── wordpress.ts # WordPress REST API client @@ -212,7 +275,8 @@ src/ ├── users.ts # User management (~5 tools) ├── comments.ts # Comment management (~5 tools) ├── plugins.ts # Plugin management (~5 tools) - └── plugin-repository.ts # WordPress.org plugin search (~2 tools) + ├── plugin-repository.ts # WordPress.org plugin search (~2 tools) + └── sql-query.ts # Database queries (1 tool) ``` ### Key Features diff --git a/src/tools/index.ts b/src/tools/index.ts index e062c5e..8bb7836 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -7,8 +7,9 @@ import { mediaTools, mediaHandlers } from './media.js'; import { userTools, userHandlers } from './users.js'; import { pluginRepositoryTools, pluginRepositoryHandlers } from './plugin-repository.js'; import { commentTools, commentHandlers } from './comments.js'; +import { sqlQueryTools, sqlQueryHandlers } from './sql-query.js'; -// Combine all tools - now significantly reduced from ~65 to ~35 tools +// Combine all tools - significantly reduced from ~65 to ~39 tools export const allTools: Tool[] = [ ...unifiedContentTools, // 8 tools (replaces posts, pages, custom-post-types) ...unifiedTaxonomyTools, // 8 tools (replaces categories, custom-taxonomies) @@ -16,7 +17,8 @@ export const allTools: Tool[] = [ ...mediaTools, // ~5 tools ...userTools, // ~5 tools ...pluginRepositoryTools, // ~2 tools - ...commentTools // ~5 tools + ...commentTools, // ~5 tools + ...sqlQueryTools // 1 tool (database queries) ]; // Combine all handlers @@ -27,5 +29,6 @@ export const toolHandlers = { ...mediaHandlers, ...userHandlers, ...pluginRepositoryHandlers, - ...commentHandlers + ...commentHandlers, + ...sqlQueryHandlers }; \ No newline at end of file diff --git a/src/tools/sql-query.ts b/src/tools/sql-query.ts new file mode 100644 index 0000000..37da1fa --- /dev/null +++ b/src/tools/sql-query.ts @@ -0,0 +1,149 @@ +// src/tools/sql-query.ts +import { z } from 'zod'; +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { makeWordPressRequest } from '../wordpress.js'; + +// Schema for SQL query execution +const executeSqlQuerySchema = z.object({ + query: z.string().describe('SQL query to execute (read-only queries: SELECT, WITH...SELECT, EXPLAIN only)') +}); + +// Type definition +type ExecuteSqlQueryParams = z.infer; + +// Tools +export const sqlQueryTools: Tool[] = [ + { + name: 'execute_sql_query', + description: 'Execute a SQL query against the WordPress database. For safety, only SELECT queries are allowed. Requires the WP Fusion Database Query endpoint to be enabled.', + inputSchema: { + type: 'object', + properties: executeSqlQuerySchema.shape, + required: ['query'] + } + } +]; + +// Handlers +export const sqlQueryHandlers = { + execute_sql_query: async (params: ExecuteSqlQueryParams) => { + try { + const query = params.query.trim(); + const trimmedQuery = query.toUpperCase(); + + // Validate that it's a read-only query + const isSelect = trimmedQuery.startsWith('SELECT'); + const isWithSelect = trimmedQuery.startsWith('WITH '); + const isExplainSelect = trimmedQuery.startsWith('EXPLAIN SELECT') || trimmedQuery.startsWith('EXPLAIN '); + + if (!(isSelect || isWithSelect || isExplainSelect)) { + return { + toolResult: { + content: [{ + type: 'text' as const, + text: 'Error: Only read-only queries are allowed (SELECT, WITH...SELECT, EXPLAIN SELECT). Please use a valid read-only statement.' + }], + isError: true + } + }; + } + + // Disallow multiple statements (semicolon followed by non-whitespace) + // Remove quoted strings first to avoid false positives + const queryWithoutStrings = query.replace(/(['"]).*?\1/g, ''); + if (/;\s*\S/.test(queryWithoutStrings)) { + return { + toolResult: { + content: [{ + type: 'text' as const, + text: 'Error: Multiple SQL statements are not allowed. Please execute one query at a time.' + }], + isError: true + } + }; + } + + // Check for dangerous patterns + const dangerousPatterns = [ + /DROP\s+/i, + /DELETE\s+/i, + /UPDATE\s+/i, + /INSERT\s+/i, + /TRUNCATE\s+/i, + /ALTER\s+/i, + /CREATE\s+/i, + /GRANT\s+/i, + /REVOKE\s+/i + ]; + + for (const pattern of dangerousPatterns) { + if (pattern.test(query)) { + return { + toolResult: { + content: [{ + type: 'text' as const, + text: `Error: Query contains potentially dangerous SQL statement. Only read-only queries are allowed.` + }], + isError: true + } + }; + } + } + + // Execute the query via the custom endpoint + // Use environment variable or default to /mcp/v1/query + const sqlEndpoint = process.env.WORDPRESS_SQL_ENDPOINT || '/mcp/v1/query'; + const response = await makeWordPressRequest( + 'POST', + sqlEndpoint, + { query }, + { headers: { 'Content-Type': 'application/json' } } + ); + + // Handle large result sets + const text = JSON.stringify(response, null, 2); + const MAX_LENGTH = 50000; + const resultText = text.length > MAX_LENGTH + ? text.slice(0, MAX_LENGTH) + '\n\n...(truncated - result too large)' + : text; + + return { + toolResult: { + content: [{ + type: 'text' as const, + text: resultText + }] + } + }; + + } catch (error: any) { + // Check if it's a 404 error (endpoint not found) + if (error.response?.status === 404) { + return { + toolResult: { + content: [{ + type: 'text' as const, + text: `Error: SQL query endpoint not found (HTTP 404). The custom REST API endpoint is not enabled on your WordPress site. + +To enable this feature, see the setup instructions in README.md under "Enabling SQL Query Tool (Optional)". + +Expected endpoint: ${process.env.WORDPRESS_SQL_ENDPOINT || '/mcp/v1/query'} +You can customize this by setting the WORDPRESS_SQL_ENDPOINT environment variable.` + }], + isError: true + } + }; + } + + return { + toolResult: { + content: [{ + type: 'text' as const, + text: `Error executing SQL query: ${error.message}` + }], + isError: true + } + }; + } + } +};