Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ build
package-lock.json
logs
*.code-workspace
.claude/
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
72 changes: 68 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
9 changes: 6 additions & 3 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ 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)
...pluginTools, // ~5 tools
...mediaTools, // ~5 tools
...userTools, // ~5 tools
...pluginRepositoryTools, // ~2 tools
...commentTools // ~5 tools
...commentTools, // ~5 tools
...sqlQueryTools // 1 tool (database queries)
];

// Combine all handlers
Expand All @@ -27,5 +29,6 @@ export const toolHandlers = {
...mediaHandlers,
...userHandlers,
...pluginRepositoryHandlers,
...commentHandlers
...commentHandlers,
...sqlQueryHandlers
};
149 changes: 149 additions & 0 deletions src/tools/sql-query.ts
Original file line number Diff line number Diff line change
@@ -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<typeof executeSqlQuerySchema>;

// 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
}
};
}
}
};