Initial commit: MCP Hub Community Edition v3.0.0
Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
589
plugins/wordpress_advanced/handlers/database.py
Normal file
589
plugins/wordpress_advanced/handlers/database.py
Normal file
@@ -0,0 +1,589 @@
|
||||
"""
|
||||
Database Operations Handler
|
||||
|
||||
Manages WordPress database operations including:
|
||||
- Export/Import
|
||||
- Backup/Restore
|
||||
- Optimization and Repair
|
||||
- Search and Query operations
|
||||
|
||||
All operations require 'write' or 'admin' scope for security.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.wordpress.client import WordPressClient
|
||||
from plugins.wordpress.wp_cli import WPCLIManager
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator"""
|
||||
return [
|
||||
# DB Export (already exists in wp_cli.py, documented here for completeness)
|
||||
{
|
||||
"name": "wp_db_export",
|
||||
"method_name": "wp_db_export",
|
||||
"description": "Export WordPress database to SQL file. Creates a timestamped backup file in /tmp directory.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tables": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Specific tables to export (default: all tables)",
|
||||
},
|
||||
"exclude_tables": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Tables to exclude from export",
|
||||
},
|
||||
"add_drop_table": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Include DROP TABLE statements",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
# DB Import
|
||||
{
|
||||
"name": "wp_db_import",
|
||||
"method_name": "wp_db_import",
|
||||
"description": "Import database from SQL file. DESTRUCTIVE: replaces current database. Requires admin scope.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {"type": "string", "description": "Path to SQL file on server"},
|
||||
"url": {"type": "string", "description": "URL to download SQL file from"},
|
||||
"skip_optimization": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Skip database optimization after import",
|
||||
},
|
||||
},
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# DB Size Info
|
||||
{
|
||||
"name": "wp_db_size",
|
||||
"method_name": "wp_db_size",
|
||||
"description": "Get database size statistics including total size, table sizes, and row counts.",
|
||||
"schema": {"type": "object", "properties": {}},
|
||||
"scope": "read",
|
||||
},
|
||||
# DB Tables List
|
||||
{
|
||||
"name": "wp_db_tables",
|
||||
"method_name": "wp_db_tables",
|
||||
"description": "List all database tables with detailed information (size, engine, rows, collation).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prefix_only": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Show only WordPress tables (with wp_ prefix)",
|
||||
}
|
||||
},
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
# DB Search
|
||||
{
|
||||
"name": "wp_db_search",
|
||||
"method_name": "wp_db_search",
|
||||
"description": "Search database for specific strings. Useful for finding content, debugging, or data migration.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"search_string": {"type": "string", "description": "String to search for"},
|
||||
"tables": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Specific tables to search",
|
||||
},
|
||||
"regex": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Use regex pattern matching",
|
||||
},
|
||||
"case_sensitive": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Case-sensitive search",
|
||||
},
|
||||
"max_results": {
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"description": "Maximum results to return",
|
||||
},
|
||||
},
|
||||
"required": ["search_string"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
# DB Query (read-only SELECT)
|
||||
{
|
||||
"name": "wp_db_query",
|
||||
"method_name": "wp_db_query",
|
||||
"description": "Execute read-only SQL query (SELECT, SHOW, DESCRIBE only). For advanced users and debugging.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "SQL query to execute (SELECT only)",
|
||||
},
|
||||
"max_rows": {
|
||||
"type": "integer",
|
||||
"default": 1000,
|
||||
"minimum": 1,
|
||||
"maximum": 10000,
|
||||
"description": "Maximum rows to return",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
# DB Repair
|
||||
{
|
||||
"name": "wp_db_repair",
|
||||
"method_name": "wp_db_repair",
|
||||
"description": "Repair corrupted database tables. Runs REPAIR TABLE on all WordPress tables.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tables": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Specific tables to repair (default: all tables)",
|
||||
}
|
||||
},
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
class DatabaseHandler:
|
||||
"""Handles WordPress database operations"""
|
||||
|
||||
def __init__(self, client: WordPressClient, wp_cli: WPCLIManager | None = None):
|
||||
"""
|
||||
Initialize Database Handler
|
||||
|
||||
Args:
|
||||
client: WordPress REST API client
|
||||
wp_cli: WP-CLI manager (optional, for advanced operations)
|
||||
"""
|
||||
self.client = client
|
||||
self.wp_cli = wp_cli
|
||||
self.logger = client.logger
|
||||
|
||||
async def wp_db_export(
|
||||
self,
|
||||
tables: list[str] | None = None,
|
||||
exclude_tables: list[str] | None = None,
|
||||
add_drop_table: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Export WordPress database to SQL file
|
||||
|
||||
Uses WP-CLI: wp db export
|
||||
"""
|
||||
if not self.wp_cli:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "WP-CLI not available. Container name not configured.",
|
||||
}
|
||||
|
||||
try:
|
||||
# Build WP-CLI command
|
||||
cmd = "db export /tmp/backup-$(date +%Y%m%d-%H%M%S).sql"
|
||||
|
||||
if tables:
|
||||
cmd += f" --tables={','.join(tables)}"
|
||||
|
||||
if exclude_tables:
|
||||
cmd += f" --exclude_tables={','.join(exclude_tables)}"
|
||||
|
||||
if not add_drop_table:
|
||||
cmd += " --no-add-drop-table"
|
||||
|
||||
# Execute export
|
||||
result = await self.wp_cli.execute_command(cmd)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Database exported successfully",
|
||||
"file": result.get("output", ""),
|
||||
"command": cmd,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database export failed: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
async def wp_db_import(
|
||||
self, file_path: str | None = None, url: str | None = None, skip_optimization: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Import database from SQL file
|
||||
|
||||
⚠️ DESTRUCTIVE OPERATION - Requires admin scope
|
||||
"""
|
||||
if not self.wp_cli:
|
||||
return {"success": False, "error": "WP-CLI not available"}
|
||||
|
||||
if not file_path and not url:
|
||||
return {"success": False, "error": "Either file_path or url is required"}
|
||||
|
||||
try:
|
||||
# Download file if URL provided
|
||||
if url:
|
||||
# Use wget or curl to download
|
||||
download_cmd = f"wget -O /tmp/import.sql '{url}'"
|
||||
await self.wp_cli.execute_command(f"eval '{download_cmd}'")
|
||||
file_path = "/tmp/import.sql"
|
||||
|
||||
# Import database
|
||||
cmd = f"db import {file_path}"
|
||||
await self.wp_cli.execute_command(cmd)
|
||||
|
||||
# Optimize unless skipped
|
||||
if not skip_optimization:
|
||||
await self.wp_cli.execute_command("db optimize")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Database imported successfully",
|
||||
"file": file_path,
|
||||
"optimized": not skip_optimization,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database import failed: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
async def wp_db_size(self) -> dict[str, Any]:
|
||||
"""Get database size statistics"""
|
||||
if not self.wp_cli:
|
||||
return {"success": False, "error": "WP-CLI not available"}
|
||||
|
||||
try:
|
||||
# Get total database size first
|
||||
total_query = """
|
||||
SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS total_mb,
|
||||
SUM(table_rows) AS total_rows,
|
||||
COUNT(*) AS table_count
|
||||
FROM information_schema.TABLES
|
||||
WHERE table_schema = DATABASE()
|
||||
"""
|
||||
|
||||
total_cmd = f'db query "{total_query}" --skip-column-names'
|
||||
total_result = await self.wp_cli.execute_command(total_cmd)
|
||||
|
||||
# Parse tab-separated output
|
||||
total_output = total_result.get("output", "0\t0\t0").strip()
|
||||
total_parts = total_output.split("\t")
|
||||
|
||||
total_size_mb = (
|
||||
float(total_parts[0]) if len(total_parts) > 0 and total_parts[0] else 0.0
|
||||
)
|
||||
total_rows = int(total_parts[1]) if len(total_parts) > 1 and total_parts[1] else 0
|
||||
table_count = int(total_parts[2]) if len(total_parts) > 2 and total_parts[2] else 0
|
||||
|
||||
# Get individual table sizes (top 50 largest tables)
|
||||
tables_query = """
|
||||
SELECT table_name,
|
||||
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb,
|
||||
table_rows
|
||||
FROM information_schema.TABLES
|
||||
WHERE table_schema = DATABASE()
|
||||
ORDER BY (data_length + index_length) DESC
|
||||
LIMIT 50
|
||||
"""
|
||||
|
||||
tables_cmd = f'db query "{tables_query}" --skip-column-names'
|
||||
tables_result = await self.wp_cli.execute_command(tables_cmd)
|
||||
|
||||
# Parse table results (tab-separated)
|
||||
tables_output = tables_result.get("output", "").strip()
|
||||
tables = []
|
||||
|
||||
if tables_output:
|
||||
for line in tables_output.split("\n"):
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 3:
|
||||
tables.append(
|
||||
{
|
||||
"table_name": parts[0],
|
||||
"size_mb": float(parts[1]) if parts[1] else 0.0,
|
||||
"table_rows": int(parts[2]) if parts[2] else 0,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"total_size_mb": total_size_mb,
|
||||
"total_rows": total_rows,
|
||||
"table_count": table_count,
|
||||
"tables": tables,
|
||||
"note": "Showing top 50 largest tables",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database size check failed: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
async def wp_db_tables(self, prefix_only: bool = True) -> dict[str, Any]:
|
||||
"""List all database tables with details"""
|
||||
if not self.wp_cli:
|
||||
return {"success": False, "error": "WP-CLI not available"}
|
||||
|
||||
try:
|
||||
# Query for table information
|
||||
query = """
|
||||
SELECT
|
||||
table_name,
|
||||
engine,
|
||||
table_rows,
|
||||
ROUND((data_length / 1024 / 1024), 2),
|
||||
ROUND((index_length / 1024 / 1024), 2),
|
||||
ROUND(((data_length + index_length) / 1024 / 1024), 2),
|
||||
table_collation
|
||||
FROM information_schema.TABLES
|
||||
WHERE table_schema = DATABASE()
|
||||
"""
|
||||
|
||||
if prefix_only:
|
||||
# Get WordPress table prefix
|
||||
prefix_result = await self.wp_cli.execute_command("config get table_prefix")
|
||||
prefix = prefix_result.get("output", "wp_").strip()
|
||||
query += f" AND table_name LIKE '{prefix}%'"
|
||||
|
||||
query += " ORDER BY (data_length + index_length) DESC"
|
||||
|
||||
# Use --skip-column-names for MariaDB compatibility (no --format=json)
|
||||
cmd = f'db query "{query}" --skip-column-names'
|
||||
result = await self.wp_cli.execute_command(cmd)
|
||||
|
||||
# Parse tab-separated output
|
||||
tables_output = result.get("output", "").strip()
|
||||
tables = []
|
||||
|
||||
if tables_output:
|
||||
for line in tables_output.split("\n"):
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 7:
|
||||
tables.append(
|
||||
{
|
||||
"name": parts[0],
|
||||
"engine": parts[1],
|
||||
"rows": int(parts[2]) if parts[2] and parts[2] != "NULL" else 0,
|
||||
"data_size_mb": (
|
||||
float(parts[3]) if parts[3] and parts[3] != "NULL" else 0.0
|
||||
),
|
||||
"index_size_mb": (
|
||||
float(parts[4]) if parts[4] and parts[4] != "NULL" else 0.0
|
||||
),
|
||||
"total_size_mb": (
|
||||
float(parts[5]) if parts[5] and parts[5] != "NULL" else 0.0
|
||||
),
|
||||
"collation": parts[6] if parts[6] != "NULL" else None,
|
||||
}
|
||||
)
|
||||
|
||||
return {"success": True, "tables": tables, "total": len(tables)}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database tables list failed: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
async def wp_db_search(
|
||||
self,
|
||||
search_string: str,
|
||||
tables: list[str] | None = None,
|
||||
regex: bool = False,
|
||||
case_sensitive: bool = False,
|
||||
max_results: int = 100,
|
||||
) -> dict[str, Any]:
|
||||
"""Search database for specific strings"""
|
||||
if not self.wp_cli:
|
||||
return {"success": False, "error": "WP-CLI not available"}
|
||||
|
||||
try:
|
||||
# Build search-replace command in dry-run mode
|
||||
cmd = f'search-replace "{search_string}" "{search_string}" --dry-run --format=count'
|
||||
|
||||
if tables:
|
||||
cmd += f" {' '.join(tables)}"
|
||||
|
||||
if regex:
|
||||
cmd += " --regex"
|
||||
|
||||
if not case_sensitive:
|
||||
cmd += " --skip-columns=guid" # Common practice
|
||||
|
||||
result = await self.wp_cli.execute_command(cmd)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"search_string": search_string,
|
||||
"matches_found": result.get("output", "0"),
|
||||
"dry_run": True,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database search failed: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
async def wp_db_query(self, query: str, max_rows: int = 1000) -> dict[str, Any]:
|
||||
"""
|
||||
Execute read-only SQL query
|
||||
|
||||
Security: Only SELECT, SHOW, DESCRIBE queries allowed
|
||||
"""
|
||||
if not self.wp_cli:
|
||||
return {"success": False, "error": "WP-CLI not available"}
|
||||
|
||||
# Validate query (additional server-side validation)
|
||||
query_upper = query.strip().upper()
|
||||
allowed_starts = ("SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN")
|
||||
|
||||
if not any(query_upper.startswith(cmd) for cmd in allowed_starts):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Only SELECT, SHOW, DESCRIBE, and EXPLAIN queries allowed",
|
||||
}
|
||||
|
||||
forbidden = [
|
||||
"INSERT",
|
||||
"UPDATE",
|
||||
"DELETE",
|
||||
"DROP",
|
||||
"ALTER",
|
||||
"CREATE",
|
||||
"TRUNCATE",
|
||||
"REPLACE",
|
||||
"GRANT",
|
||||
"REVOKE",
|
||||
]
|
||||
|
||||
for keyword in forbidden:
|
||||
if keyword in query_upper:
|
||||
return {"success": False, "error": f"Forbidden keyword in query: {keyword}"}
|
||||
|
||||
try:
|
||||
# Add LIMIT if not present
|
||||
if "LIMIT" not in query_upper:
|
||||
query = f"{query.rstrip(';')} LIMIT {max_rows}"
|
||||
|
||||
# Execute query with --skip-column-names for MariaDB compatibility
|
||||
# First, get column names separately if it's a SELECT
|
||||
results = []
|
||||
|
||||
if query_upper.startswith("SELECT"):
|
||||
# For SELECT queries, we need to parse the tab-separated output
|
||||
cmd = f'db query "{query}" --skip-column-names'
|
||||
result = await self.wp_cli.execute_command(cmd)
|
||||
|
||||
# Get output
|
||||
output = result.get("output", "").strip()
|
||||
|
||||
if output:
|
||||
# Parse tab-separated values
|
||||
lines = output.split("\n")
|
||||
|
||||
# For simple queries, try to detect column structure
|
||||
# We'll return as a list of dictionaries with column indices
|
||||
for idx, line in enumerate(lines):
|
||||
values = line.split("\t")
|
||||
# Create a row dict with column indices
|
||||
row = {f"col_{i}": val for i, val in enumerate(values)}
|
||||
results.append(row)
|
||||
|
||||
# Limit results
|
||||
if idx >= max_rows - 1:
|
||||
break
|
||||
else:
|
||||
# For SHOW, DESCRIBE, etc., just return raw output
|
||||
cmd = f'db query "{query}"'
|
||||
result = await self.wp_cli.execute_command(cmd)
|
||||
output = result.get("output", "").strip()
|
||||
|
||||
# Return as formatted message
|
||||
return {
|
||||
"success": True,
|
||||
"output": output,
|
||||
"query": query,
|
||||
"note": "Results displayed as plain text",
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"results": results,
|
||||
"row_count": len(results),
|
||||
"query": query,
|
||||
"note": "Columns are numbered as col_0, col_1, etc. due to MariaDB compatibility mode.",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database query failed: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
async def wp_db_repair(self, tables: list[str] | None = None) -> dict[str, Any]:
|
||||
"""Repair corrupted database tables"""
|
||||
if not self.wp_cli:
|
||||
return {"success": False, "error": "WP-CLI not available"}
|
||||
|
||||
try:
|
||||
# Get list of tables to repair
|
||||
if not tables:
|
||||
# Get all WordPress tables
|
||||
table_list = await self.wp_db_tables(prefix_only=True)
|
||||
if not table_list.get("success"):
|
||||
return table_list
|
||||
|
||||
tables = [t["name"] for t in table_list.get("tables", [])]
|
||||
|
||||
# Repair each table
|
||||
results = []
|
||||
for table in tables:
|
||||
try:
|
||||
query = f"REPAIR TABLE {table}"
|
||||
cmd = f'db query "{query}" --format=json'
|
||||
result = await self.wp_cli.execute_command(cmd)
|
||||
|
||||
repair_result = json.loads(result.get("output", "[]"))
|
||||
|
||||
results.append(
|
||||
{
|
||||
"table": table,
|
||||
"status": "Repaired" if repair_result else "OK",
|
||||
"message": str(repair_result),
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
results.append({"table": table, "status": "Failed", "message": str(e)})
|
||||
|
||||
# Count successes/failures
|
||||
success_count = sum(1 for r in results if r["status"] != "Failed")
|
||||
failed_count = len(results) - success_count
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"total_tables": len(results),
|
||||
"success_count": success_count,
|
||||
"failed_count": failed_count,
|
||||
"results": results,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Database repair failed: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
Reference in New Issue
Block a user