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:
airano
2026-02-17 08:34:44 +03:30
commit cf62e65c55
237 changed files with 87596 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
"""WordPress plugin package"""
from plugins.wordpress.plugin import WordPressPlugin
__all__ = ["WordPressPlugin"]

448
plugins/wordpress/client.py Normal file
View File

@@ -0,0 +1,448 @@
"""
WordPress REST API Client
Handles all HTTP communication with WordPress REST API.
Separates API communication from business logic.
"""
import base64
import json
import logging
from typing import Any
import aiohttp
class ConfigurationError(Exception):
"""Raised when site configuration is invalid or incomplete."""
pass
class AuthenticationError(Exception):
"""Raised when authentication fails (401/403)."""
pass
class WordPressClient:
"""
WordPress REST API client for HTTP communication.
Handles authentication, request formatting, and error handling
for all WordPress and WooCommerce API endpoints.
"""
def __init__(self, site_url: str, username: str, app_password: str):
"""
Initialize WordPress API client.
Args:
site_url: WordPress site URL (e.g., https://example.com)
username: WordPress username
app_password: WordPress application password
Raises:
ConfigurationError: If required parameters are missing or invalid
"""
# Validate required parameters
if not site_url:
raise ConfigurationError(
"Site URL is not configured. "
"Please set the URL environment variable (e.g., WORDPRESS_SITE1_URL)."
)
if not username:
raise ConfigurationError(
"Username is not configured. "
"Please set the USERNAME environment variable (e.g., WORDPRESS_SITE1_USERNAME)."
)
if not app_password:
raise ConfigurationError(
"App password is not configured. "
"Please set the APP_PASSWORD environment variable (e.g., WORDPRESS_SITE1_APP_PASSWORD)."
)
self.site_url = site_url.rstrip("/")
self.api_base = f"{self.site_url}/wp-json/wp/v2"
self.wc_api_base = f"{self.site_url}/wp-json/wc/v3"
self.username = username
self.app_password = app_password
# Initialize logger
self.logger = logging.getLogger(f"WordPressClient.{site_url}")
# Create auth header
credentials = f"{self.username}:{self.app_password}"
token = base64.b64encode(credentials.encode()).decode()
self.auth_header = f"Basic {token}"
async def request(
self,
method: str,
endpoint: str,
params: dict | None = None,
json_data: dict | None = None,
data: Any | None = None,
headers_override: dict | None = None,
use_custom_namespace: bool = False,
use_woocommerce: bool = False,
) -> dict[str, Any]:
"""
Make authenticated request to WordPress REST API.
Args:
method: HTTP method (GET, POST, PUT, DELETE, PATCH)
endpoint: API endpoint (without base URL)
params: Query parameters
json_data: JSON body data
data: Raw data or FormData for file uploads
headers_override: Override default headers
use_custom_namespace: If True, use wp-json root instead of wp/v2
use_woocommerce: If True, use WooCommerce API base
Returns:
Dict: API response as JSON
Raises:
Exception: On API errors with status code and message
"""
# Build URL based on endpoint type
if use_custom_namespace:
# For custom namespaces like seo-api-bridge/v1
url = f"{self.site_url}/wp-json/{endpoint}"
elif use_woocommerce:
# For WooCommerce endpoints
url = f"{self.wc_api_base}/{endpoint}"
else:
# Standard WordPress endpoints
url = f"{self.api_base}/{endpoint}"
# Setup headers
headers = {"Authorization": self.auth_header}
if headers_override:
headers.update(headers_override)
# Filter out None, empty strings, and empty lists from params
# to avoid WordPress/WooCommerce API validation errors
# Phase K.2.1: Enhanced parameter filtering
def should_include(v):
"""Check if value should be included in request."""
if v is None:
return False
if isinstance(v, str) and v.strip() == "":
return False
return not (isinstance(v, list) and len(v) == 0)
if params:
params = {k: v for k, v in params.items() if should_include(v)}
# Filter None and empty values from JSON data for POST/PUT/PATCH requests
if json_data:
json_data = {k: v for k, v in json_data.items() if should_include(v)}
# Make request
async with (
aiohttp.ClientSession() as session,
session.request(
method, url, params=params, json=json_data, data=data, headers=headers
) as response,
):
# Handle errors with structured error messages
if response.status >= 400:
error_text = await response.text()
# Parse structured error response
error_info = self._parse_error_response(
response.status, error_text, use_woocommerce
)
# Log the error for debugging
self.logger.error(
f"API error: {error_info['error_code']} - {error_info['message']}"
)
# Raise appropriate exception
if response.status in (401, 403):
raise AuthenticationError(
f"[{error_info['error_code']}] {error_info['message']}"
)
raise Exception(f"[{error_info['error_code']}] {error_info['message']}")
# Return JSON response
return await response.json()
def _parse_error_response(
self, status_code: int, error_text: str, use_woocommerce: bool = False
) -> dict[str, Any]:
"""
Parse error response and return structured error info.
Args:
status_code: HTTP status code
error_text: Raw error response text
use_woocommerce: Whether this is a WooCommerce API call
Returns:
Dict with error_code, message, and details
"""
# Try to parse as JSON
try:
error_json = json.loads(error_text)
wp_error_code = error_json.get("code", "unknown_error")
wp_message = error_json.get("message", error_text)
except (json.JSONDecodeError, TypeError):
wp_error_code = "unknown_error"
wp_message = error_text
# Map status codes to structured error codes
error_codes = {
400: "BAD_REQUEST",
401: "AUTH_FAILED",
403: "ACCESS_DENIED",
404: "NOT_FOUND",
405: "METHOD_NOT_ALLOWED",
409: "CONFLICT",
422: "VALIDATION_ERROR",
429: "RATE_LIMITED",
500: "SERVER_ERROR",
502: "BAD_GATEWAY",
503: "SERVICE_UNAVAILABLE",
}
error_code = error_codes.get(status_code, f"HTTP_{status_code}")
# Create user-friendly messages for common errors
# Phase K.2.1: Enhanced error messages with helpful hints
friendly_messages = {
400: (
f"Invalid request parameters. {wp_message}. "
"Hints: Check parameter types match the expected format. "
'For categories/tags use IDs (e.g., [62] or "62,63"). '
"For billing/shipping use JSON objects."
),
401: (
"Authentication failed. Please verify: "
"1) Username is correct, "
"2) Application Password is valid, "
"3) User has required permissions. "
f"Details: {wp_message}"
),
403: (
"Access denied. The user does not have permission for this action. "
"Some operations (like coupons) require admin-level WooCommerce permissions. "
f"Details: {wp_message}"
),
404: (
"Resource not found. The requested endpoint or item does not exist. "
f"Details: {wp_message}"
),
}
# Add WooCommerce-specific hints
if use_woocommerce and status_code == 401:
friendly_messages[401] = (
"WooCommerce authentication failed. Please verify: "
"1) WooCommerce REST API is enabled, "
"2) Application Password has WooCommerce permissions, "
"3) User has 'manage_woocommerce' capability. "
f"Details: {wp_message}"
)
message = friendly_messages.get(status_code, f"{wp_message}")
return {
"error_code": error_code,
"status_code": status_code,
"message": message,
"wp_error_code": wp_error_code,
"raw_response": error_text[:500], # Limit raw response length
}
async def get(
self,
endpoint: str,
params: dict | None = None,
use_custom_namespace: bool = False,
use_woocommerce: bool = False,
) -> dict[str, Any]:
"""
Make GET request.
Args:
endpoint: API endpoint
params: Query parameters
use_custom_namespace: Use custom namespace instead of wp/v2
use_woocommerce: Use WooCommerce API base
Returns:
Dict: API response
"""
return await self.request(
"GET",
endpoint,
params=params,
use_custom_namespace=use_custom_namespace,
use_woocommerce=use_woocommerce,
)
async def post(
self,
endpoint: str,
json_data: dict | None = None,
data: Any | None = None,
headers_override: dict | None = None,
use_custom_namespace: bool = False,
use_woocommerce: bool = False,
) -> dict[str, Any]:
"""
Make POST request.
Args:
endpoint: API endpoint
json_data: JSON body data
data: Raw data or FormData for file uploads
headers_override: Override default headers
use_custom_namespace: Use custom namespace instead of wp/v2
use_woocommerce: Use WooCommerce API base
Returns:
Dict: API response
"""
return await self.request(
"POST",
endpoint,
json_data=json_data,
data=data,
headers_override=headers_override,
use_custom_namespace=use_custom_namespace,
use_woocommerce=use_woocommerce,
)
async def put(
self,
endpoint: str,
json_data: dict,
use_custom_namespace: bool = False,
use_woocommerce: bool = False,
) -> dict[str, Any]:
"""
Make PUT request.
Args:
endpoint: API endpoint
json_data: JSON body data
use_custom_namespace: Use custom namespace instead of wp/v2
use_woocommerce: Use WooCommerce API base
Returns:
Dict: API response
"""
return await self.request(
"PUT",
endpoint,
json_data=json_data,
use_custom_namespace=use_custom_namespace,
use_woocommerce=use_woocommerce,
)
async def patch(
self,
endpoint: str,
json_data: dict,
use_custom_namespace: bool = False,
use_woocommerce: bool = False,
) -> dict[str, Any]:
"""
Make PATCH request.
Args:
endpoint: API endpoint
json_data: JSON body data
use_custom_namespace: Use custom namespace instead of wp/v2
use_woocommerce: Use WooCommerce API base
Returns:
Dict: API response
"""
return await self.request(
"PATCH",
endpoint,
json_data=json_data,
use_custom_namespace=use_custom_namespace,
use_woocommerce=use_woocommerce,
)
async def delete(
self,
endpoint: str,
params: dict | None = None,
use_custom_namespace: bool = False,
use_woocommerce: bool = False,
) -> dict[str, Any]:
"""
Make DELETE request.
Args:
endpoint: API endpoint
params: Query parameters
use_custom_namespace: Use custom namespace instead of wp/v2
use_woocommerce: Use WooCommerce API base
Returns:
Dict: API response
"""
return await self.request(
"DELETE",
endpoint,
params=params,
use_custom_namespace=use_custom_namespace,
use_woocommerce=use_woocommerce,
)
async def check_woocommerce(self) -> dict[str, Any]:
"""
Check if WooCommerce is installed and accessible.
Returns:
Dict with 'available' bool and version info
"""
try:
# Try to access WooCommerce system status endpoint
response = await self.get("system_status", use_woocommerce=True)
return {
"available": True,
"version": response.get("environment", {}).get("version", "unknown"),
}
except Exception:
return {"available": False, "version": None}
async def check_site_health(self) -> dict[str, Any]:
"""
Check WordPress site health and accessibility.
Returns:
Dict with health status information
"""
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.site_url}/wp-json") as response:
if response.status == 200:
data = await response.json()
return {
"healthy": True,
"accessible": True,
"name": data.get("name", "Unknown"),
"description": data.get("description", "Unknown"),
"url": data.get("url", self.site_url),
"routes": len(data.get("routes", {})),
}
else:
return {
"healthy": False,
"accessible": False,
"message": f"Site returned status {response.status}",
}
except Exception as e:
return {
"healthy": False,
"accessible": False,
"message": f"Health check failed: {str(e)}",
}

View File

@@ -0,0 +1,72 @@
"""
WordPress Handlers
Modular handlers for WordPress functionality.
Each handler is responsible for a specific domain of WordPress operations.
Part of Option B clean architecture refactoring.
"""
from plugins.wordpress.handlers.comments import CommentsHandler
from plugins.wordpress.handlers.comments import get_tool_specifications as get_comments_specs
from plugins.wordpress.handlers.coupons import CouponsHandler
from plugins.wordpress.handlers.coupons import get_tool_specifications as get_coupons_specs
from plugins.wordpress.handlers.customers import CustomersHandler
from plugins.wordpress.handlers.customers import get_tool_specifications as get_customers_specs
from plugins.wordpress.handlers.media import MediaHandler
from plugins.wordpress.handlers.media import get_tool_specifications as get_media_specs
from plugins.wordpress.handlers.menus import MenusHandler
from plugins.wordpress.handlers.menus import get_tool_specifications as get_menus_specs
from plugins.wordpress.handlers.orders import OrdersHandler
from plugins.wordpress.handlers.orders import get_tool_specifications as get_orders_specs
from plugins.wordpress.handlers.posts import PostsHandler
from plugins.wordpress.handlers.posts import get_tool_specifications as get_posts_specs
from plugins.wordpress.handlers.products import ProductsHandler
from plugins.wordpress.handlers.products import get_tool_specifications as get_products_specs
from plugins.wordpress.handlers.reports import ReportsHandler
from plugins.wordpress.handlers.reports import get_tool_specifications as get_reports_specs
from plugins.wordpress.handlers.seo import SEOHandler
from plugins.wordpress.handlers.seo import get_tool_specifications as get_seo_specs
from plugins.wordpress.handlers.site import SiteHandler
from plugins.wordpress.handlers.site import get_tool_specifications as get_site_specs
from plugins.wordpress.handlers.taxonomy import TaxonomyHandler
from plugins.wordpress.handlers.taxonomy import get_tool_specifications as get_taxonomy_specs
from plugins.wordpress.handlers.users import UsersHandler
from plugins.wordpress.handlers.users import get_tool_specifications as get_users_specs
from plugins.wordpress.handlers.wp_cli import WPCLIHandler
from plugins.wordpress.handlers.wp_cli import get_tool_specifications as get_wp_cli_specs
__all__ = [
# Core Handlers
"PostsHandler",
"MediaHandler",
"TaxonomyHandler",
"CommentsHandler",
"UsersHandler",
"SiteHandler",
# WooCommerce Handlers
"ProductsHandler",
"OrdersHandler",
"CustomersHandler",
"ReportsHandler",
"CouponsHandler",
# Advanced Handlers
"SEOHandler",
"WPCLIHandler",
"MenusHandler",
# Tool specifications
"get_posts_specs",
"get_media_specs",
"get_taxonomy_specs",
"get_comments_specs",
"get_users_specs",
"get_site_specs",
"get_products_specs",
"get_orders_specs",
"get_customers_specs",
"get_reports_specs",
"get_coupons_specs",
"get_seo_specs",
"get_wp_cli_specs",
"get_menus_specs",
]

View File

@@ -0,0 +1,365 @@
"""Comments Handler - manages WordPress comments and moderation"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# === COMMENTS ===
{
"name": "list_comments",
"method_name": "list_comments",
"description": "List WordPress comments. Returns paginated list of comments with author, content, and moderation status.",
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Number of comments per page (1-100)",
"default": 10,
"minimum": 1,
"maximum": 100,
},
"page": {
"type": "integer",
"description": "Page number",
"default": 1,
"minimum": 1,
},
"post_id": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Filter by post ID (optional)",
},
"status": {
"type": "string",
"description": "Filter by comment status",
"enum": ["approve", "hold", "spam", "trash", "all"],
"default": "approve",
},
},
},
"scope": "read",
},
{
"name": "get_comment",
"method_name": "get_comment",
"description": "Get a specific WordPress comment by ID. Returns complete comment data including content, author, and moderation status.",
"schema": {
"type": "object",
"properties": {
"comment_id": {
"type": "integer",
"description": "Comment ID to retrieve",
"minimum": 1,
}
},
"required": ["comment_id"],
},
"scope": "read",
},
{
"name": "create_comment",
"method_name": "create_comment",
"description": "Create a new WordPress comment on a post. Supports author information and moderation status.",
"schema": {
"type": "object",
"properties": {
"post_id": {
"type": "integer",
"description": "Post ID to attach comment to",
"minimum": 1,
},
"content": {
"type": "string",
"description": "Comment content/text",
"minLength": 1,
},
"author_name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Author name (for unauthenticated comments)",
},
"author_email": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Author email address",
},
"status": {
"type": "string",
"description": "Comment moderation status",
"enum": ["approve", "hold", "spam"],
"default": "hold",
},
},
"required": ["post_id", "content"],
},
"scope": "write",
},
{
"name": "update_comment",
"method_name": "update_comment",
"description": "Update an existing WordPress comment. Can update content, status, author information, etc.",
"schema": {
"type": "object",
"properties": {
"comment_id": {
"type": "integer",
"description": "Comment ID to update",
"minimum": 1,
},
"content": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Comment content/text",
},
"author_name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Author name",
},
"author_email": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Author email",
},
"status": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Comment moderation status",
"enum": ["approve", "hold", "spam"],
},
},
"required": ["comment_id"],
},
"scope": "write",
},
{
"name": "delete_comment",
"method_name": "delete_comment",
"description": "Delete or trash a WordPress comment. Can permanently delete or move to trash.",
"schema": {
"type": "object",
"properties": {
"comment_id": {
"type": "integer",
"description": "Comment ID to delete",
"minimum": 1,
},
"force": {
"type": "boolean",
"description": "Permanently delete (true) or move to trash (false)",
"default": False,
},
},
"required": ["comment_id"],
},
"scope": "write",
},
]
class CommentsHandler:
"""Handle comment-related operations for WordPress"""
def __init__(self, client: WordPressClient):
"""
Initialize comments handler.
Args:
client: WordPress API client instance
"""
self.client = client
async def list_comments(
self, per_page: int = 10, page: int = 1, post_id: int | None = None, status: str = "approve"
) -> str:
"""
List WordPress comments.
Args:
per_page: Number of comments per page (1-100)
page: Page number
post_id: Optional filter by post ID
status: Comment status filter (approve, hold, spam, trash, all)
Returns:
JSON string with comments list
"""
try:
params = {"per_page": per_page, "page": page, "status": status}
if post_id:
params["post"] = post_id
comments = await self.client.get("comments", params=params)
# Format response
result = {
"total": len(comments),
"page": page,
"per_page": per_page,
"comments": [
{
"id": c["id"],
"post_id": c["post"],
"author_name": c["author_name"],
"author_email": c.get("author_email", ""),
"content": c["content"]["rendered"][:200],
"status": c["status"],
"date": c["date"],
"link": c.get("link", ""),
}
for c in comments
],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list comments: {str(e)}"}, indent=2
)
async def get_comment(self, comment_id: int) -> str:
"""
Get a specific WordPress comment.
Args:
comment_id: Comment ID to retrieve
Returns:
JSON string with comment data
"""
try:
comment = await self.client.get(f"comments/{comment_id}")
result = {
"id": comment["id"],
"post_id": comment["post"],
"author_name": comment["author_name"],
"author_email": comment.get("author_email", ""),
"author_url": comment.get("author_url", ""),
"content": comment["content"]["rendered"],
"status": comment["status"],
"date": comment["date"],
"link": comment.get("link", ""),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get comment {comment_id}: {str(e)}"},
indent=2,
)
async def create_comment(
self,
post_id: int,
content: str,
author_name: str | None = None,
author_email: str | None = None,
status: str = "hold",
) -> str:
"""
Create a new WordPress comment.
Args:
post_id: Post ID to attach comment to
content: Comment content/text
author_name: Author name (for unauthenticated comments)
author_email: Author email address
status: Comment moderation status (approve, hold, spam)
Returns:
JSON string with created comment data
"""
try:
data = {"post": post_id, "content": content, "status": status}
if author_name:
data["author_name"] = author_name
if author_email:
data["author_email"] = author_email
comment = await self.client.post("comments", json_data=data)
result = {
"id": comment["id"],
"post_id": comment["post"],
"author_name": comment["author_name"],
"content": comment["content"]["rendered"],
"status": comment["status"],
"message": f"Comment created successfully with ID {comment['id']}",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to create comment: {str(e)}"}, indent=2
)
async def update_comment(
self,
comment_id: int,
content: str | None = None,
author_name: str | None = None,
author_email: str | None = None,
status: str | None = None,
) -> str:
"""
Update an existing WordPress comment.
Args:
comment_id: Comment ID to update
content: Comment content/text
author_name: Author name
author_email: Author email
status: Comment moderation status
Returns:
JSON string with updated comment data
"""
try:
# Build data dict with only provided values
data = {}
if content is not None:
data["content"] = content
if author_name is not None:
data["author_name"] = author_name
if author_email is not None:
data["author_email"] = author_email
if status is not None:
data["status"] = status
comment = await self.client.post(f"comments/{comment_id}", json_data=data)
result = {
"id": comment["id"],
"post_id": comment["post"],
"content": comment["content"]["rendered"],
"status": comment["status"],
"message": f"Comment {comment_id} updated successfully",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update comment {comment_id}: {str(e)}"},
indent=2,
)
async def delete_comment(self, comment_id: int, force: bool = False) -> str:
"""
Delete or trash a WordPress comment.
Args:
comment_id: Comment ID to delete
force: Permanently delete (True) or move to trash (False)
Returns:
JSON string with deletion result
"""
try:
params = {"force": "true" if force else "false"}
result = await self.client.delete(f"comments/{comment_id}", params=params)
message = f"Comment {comment_id} {'permanently deleted' if force else 'moved to trash'}"
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to delete comment {comment_id}: {str(e)}"},
indent=2,
)

View File

@@ -0,0 +1,495 @@
"""Coupons Handler - manages WooCommerce coupons"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
{
"name": "list_coupons",
"method_name": "list_coupons",
"description": "List WooCommerce coupons. Returns paginated coupon list with discount details and usage restrictions.",
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Number of coupons per page (1-100)",
"default": 10,
"minimum": 1,
"maximum": 100,
},
"page": {
"type": "integer",
"description": "Page number",
"default": 1,
"minimum": 1,
},
"search": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Search term to filter coupons by code",
},
},
},
"scope": "read",
},
{
"name": "create_coupon",
"method_name": "create_coupon",
"description": "Create a new WooCommerce coupon. Supports percentage and fixed discounts with usage limits and restrictions.",
"schema": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Coupon code (e.g., 'SAVE20')",
"minLength": 1,
},
"amount": {
"type": "string",
"description": "Discount amount (e.g., '20' for 20% or $20)",
},
"discount_type": {
"type": "string",
"description": "Type of discount",
"enum": ["percent", "fixed_cart", "fixed_product"],
"default": "percent",
},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Coupon description (internal note)",
},
"date_expires": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Expiration date in ISO 8601 format (e.g., '2024-12-31T23:59:59')",
},
"minimum_amount": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Minimum order amount required to use coupon",
},
"maximum_amount": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Maximum order amount allowed to use coupon",
},
"individual_use": {
"type": "boolean",
"description": "If true, coupon cannot be combined with other coupons",
"default": False,
},
"product_ids": {
"anyOf": [
{"type": "array", "items": {"type": "integer"}},
{"type": "null"},
],
"description": "Array of product IDs coupon applies to",
},
"excluded_product_ids": {
"anyOf": [
{"type": "array", "items": {"type": "integer"}},
{"type": "null"},
],
"description": "Array of product IDs coupon does NOT apply to",
},
"usage_limit": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Maximum number of times coupon can be used",
},
"usage_limit_per_user": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Maximum number of times coupon can be used per user",
},
"limit_usage_to_x_items": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Maximum number of items coupon applies to",
},
"free_shipping": {
"type": "boolean",
"description": "If true, grants free shipping",
"default": False,
},
},
"required": ["code", "amount"],
},
"scope": "write",
},
{
"name": "update_coupon",
"method_name": "update_coupon",
"description": "Update an existing WooCommerce coupon. Can update discount amount, restrictions, and expiration.",
"schema": {
"type": "object",
"properties": {
"coupon_id": {
"type": "integer",
"description": "Coupon ID to update",
"minimum": 1,
},
"code": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Coupon code (e.g., 'SAVE20')",
},
"discount_type": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Type of discount",
"enum": ["percent", "fixed_cart", "fixed_product"],
},
"amount": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Discount amount",
},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Coupon description (internal note)",
},
"date_expires": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Expiration date in ISO 8601 format",
},
"minimum_amount": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Minimum order amount",
},
"maximum_amount": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Maximum order amount",
},
"individual_use": {
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"description": "If true, coupon cannot be combined with other coupons",
},
"product_ids": {
"anyOf": [
{"type": "array", "items": {"type": "integer"}},
{"type": "null"},
],
"description": "Array of product IDs coupon applies to",
},
"excluded_product_ids": {
"anyOf": [
{"type": "array", "items": {"type": "integer"}},
{"type": "null"},
],
"description": "Array of product IDs coupon does NOT apply to",
},
"usage_limit": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Maximum number of uses",
},
"usage_limit_per_user": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Maximum uses per user",
},
"limit_usage_to_x_items": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Maximum number of items coupon applies to",
},
"free_shipping": {
"anyOf": [{"type": "boolean"}, {"type": "null"}],
"description": "If true, grants free shipping",
},
},
"required": ["coupon_id"],
},
"scope": "write",
},
{
"name": "delete_coupon",
"method_name": "delete_coupon",
"description": "Delete a WooCommerce coupon. Can force delete or move to trash.",
"schema": {
"type": "object",
"properties": {
"coupon_id": {
"type": "integer",
"description": "Coupon ID to delete",
"minimum": 1,
},
"force": {
"type": "boolean",
"description": "Force permanent delete (bypass trash)",
"default": False,
},
},
"required": ["coupon_id"],
},
"scope": "write",
},
]
class CouponsHandler:
"""Handle coupon-related operations for WooCommerce"""
def __init__(self, client: WordPressClient):
"""
Initialize coupons handler.
Args:
client: WordPress API client instance
"""
self.client = client
async def list_coupons(
self, per_page: int = 10, page: int = 1, search: str | None = None
) -> str:
"""
List WooCommerce coupons.
Args:
per_page: Number of coupons per page (1-100)
page: Page number
search: Search term to filter coupons by code
Returns:
JSON string with coupons list
"""
try:
params = {"per_page": per_page, "page": page}
if search:
params["search"] = search
coupons = await self.client.get("coupons", params=params, use_woocommerce=True)
result = {
"total": len(coupons),
"page": page,
"per_page": per_page,
"coupons": [
{
"id": coupon["id"],
"code": coupon["code"],
"discount_type": coupon["discount_type"],
"amount": coupon["amount"],
"description": coupon.get("description", ""),
"date_expires": coupon.get("date_expires"),
"usage_count": coupon.get("usage_count", 0),
"usage_limit": coupon.get("usage_limit"),
"individual_use": coupon.get("individual_use", False),
"free_shipping": coupon.get("free_shipping", False),
"minimum_amount": coupon.get("minimum_amount", "0"),
"maximum_amount": coupon.get("maximum_amount", "0"),
}
for coupon in coupons
],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list coupons: {str(e)}"}, indent=2
)
async def create_coupon(
self,
code: str,
amount: str,
discount_type: str = "percent",
description: str | None = None,
date_expires: str | None = None,
minimum_amount: str | None = None,
maximum_amount: str | None = None,
individual_use: bool = False,
product_ids: list[int] | None = None,
excluded_product_ids: list[int] | None = None,
usage_limit: int | None = None,
usage_limit_per_user: int | None = None,
limit_usage_to_x_items: int | None = None,
free_shipping: bool = False,
) -> str:
"""
Create a new WooCommerce coupon.
Args:
code: Coupon code (e.g., 'SAVE20')
amount: Discount amount (e.g., '20' for 20% or $20)
discount_type: Type of discount (percent, fixed_cart, fixed_product)
description: Coupon description (internal note)
date_expires: Expiration date in ISO 8601 format
minimum_amount: Minimum order amount required
maximum_amount: Maximum order amount allowed
individual_use: If true, coupon cannot be combined with others
product_ids: Product IDs coupon applies to
excluded_product_ids: Product IDs coupon does NOT apply to
usage_limit: Maximum number of times coupon can be used
usage_limit_per_user: Maximum uses per user
limit_usage_to_x_items: Maximum number of items coupon applies to
free_shipping: If true, grants free shipping
Returns:
JSON string with created coupon data
"""
try:
data = {
"code": code,
"discount_type": discount_type,
"amount": amount,
"individual_use": individual_use,
"free_shipping": free_shipping,
}
# Add optional fields
if description:
data["description"] = description
if date_expires:
data["date_expires"] = date_expires
if minimum_amount:
data["minimum_amount"] = minimum_amount
if maximum_amount:
data["maximum_amount"] = maximum_amount
if product_ids:
data["product_ids"] = product_ids
if excluded_product_ids:
data["excluded_product_ids"] = excluded_product_ids
if usage_limit:
data["usage_limit"] = usage_limit
if usage_limit_per_user:
data["usage_limit_per_user"] = usage_limit_per_user
if limit_usage_to_x_items:
data["limit_usage_to_x_items"] = limit_usage_to_x_items
coupon = await self.client.post("coupons", json_data=data, use_woocommerce=True)
result = {
"id": coupon["id"],
"code": coupon["code"],
"discount_type": coupon["discount_type"],
"amount": coupon["amount"],
"date_expires": coupon.get("date_expires"),
"message": f"Coupon '{code}' created successfully with ID {coupon['id']}",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to create coupon: {str(e)}"}, indent=2
)
async def update_coupon(
self,
coupon_id: int,
code: str | None = None,
discount_type: str | None = None,
amount: str | None = None,
description: str | None = None,
date_expires: str | None = None,
minimum_amount: str | None = None,
maximum_amount: str | None = None,
individual_use: bool | None = None,
product_ids: list[int] | None = None,
excluded_product_ids: list[int] | None = None,
usage_limit: int | None = None,
usage_limit_per_user: int | None = None,
limit_usage_to_x_items: int | None = None,
free_shipping: bool | None = None,
) -> str:
"""
Update an existing WooCommerce coupon.
Args:
coupon_id: Coupon ID to update
code: Coupon code
discount_type: Type of discount
amount: Discount amount
description: Coupon description
date_expires: Expiration date in ISO 8601 format
minimum_amount: Minimum order amount
maximum_amount: Maximum order amount
individual_use: If true, coupon cannot be combined with others
product_ids: Product IDs coupon applies to
excluded_product_ids: Product IDs coupon does NOT apply to
usage_limit: Maximum number of uses
usage_limit_per_user: Maximum uses per user
limit_usage_to_x_items: Maximum number of items coupon applies to
free_shipping: If true, grants free shipping
Returns:
JSON string with updated coupon data
"""
try:
# Build data dict with only provided values
data = {}
if code is not None:
data["code"] = code
if discount_type is not None:
data["discount_type"] = discount_type
if amount is not None:
data["amount"] = amount
if description is not None:
data["description"] = description
if date_expires is not None:
data["date_expires"] = date_expires
if minimum_amount is not None:
data["minimum_amount"] = minimum_amount
if maximum_amount is not None:
data["maximum_amount"] = maximum_amount
if individual_use is not None:
data["individual_use"] = individual_use
if product_ids is not None:
data["product_ids"] = product_ids
if excluded_product_ids is not None:
data["excluded_product_ids"] = excluded_product_ids
if usage_limit is not None:
data["usage_limit"] = usage_limit
if usage_limit_per_user is not None:
data["usage_limit_per_user"] = usage_limit_per_user
if limit_usage_to_x_items is not None:
data["limit_usage_to_x_items"] = limit_usage_to_x_items
if free_shipping is not None:
data["free_shipping"] = free_shipping
if not data:
raise Exception("No update data provided")
coupon = await self.client.put(
f"coupons/{coupon_id}", json_data=data, use_woocommerce=True
)
result = {
"id": coupon["id"],
"code": coupon["code"],
"discount_type": coupon["discount_type"],
"amount": coupon["amount"],
"message": f"Coupon ID {coupon_id} updated successfully",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update coupon {coupon_id}: {str(e)}"},
indent=2,
)
async def delete_coupon(self, coupon_id: int, force: bool = False) -> str:
"""
Delete a WooCommerce coupon.
Args:
coupon_id: Coupon ID to delete
force: Force permanent delete (True) or move to trash (False)
Returns:
JSON string with deletion result
"""
try:
params = {"force": "true" if force else "false"}
result = await self.client.delete(
f"coupons/{coupon_id}", params=params, use_woocommerce=True
)
action = "permanently deleted" if force else "moved to trash"
return json.dumps(
{
"success": True,
"coupon_id": coupon_id,
"message": f"Coupon {action} successfully",
"result": result,
},
indent=2,
)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to delete coupon {coupon_id}: {str(e)}"},
indent=2,
)

View File

@@ -0,0 +1,373 @@
"""Customers Handler - manages WooCommerce customers"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
{
"name": "list_customers",
"method_name": "list_customers",
"description": "List WooCommerce customers. Returns paginated customer list with email, orders count, and total spent.",
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Number of customers per page (1-100)",
"default": 10,
"minimum": 1,
"maximum": 100,
},
"page": {
"type": "integer",
"description": "Page number",
"default": 1,
"minimum": 1,
},
"search": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Search by name or email",
},
"email": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Filter by specific email address",
},
"role": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Filter by role (customer, subscriber, etc.)",
},
},
},
"scope": "read",
},
{
"name": "get_customer",
"method_name": "get_customer",
"description": "Get detailed information about a specific WooCommerce customer. Returns customer details, billing, shipping, and order history.",
"schema": {
"type": "object",
"properties": {
"customer_id": {
"type": "integer",
"description": "Customer ID to retrieve",
"minimum": 1,
}
},
"required": ["customer_id"],
},
"scope": "read",
},
{
"name": "create_customer",
"method_name": "create_customer",
"description": "Create a new WooCommerce customer. Requires email, optionally includes name, username, password, billing, and shipping address.",
"schema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "Customer email address (required)",
"minLength": 1,
},
"first_name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Customer first name",
},
"last_name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Customer last name",
},
"username": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Username (generated from email if not provided)",
},
"password": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Password (auto-generated if not provided)",
},
"billing": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Billing address object",
},
"shipping": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Shipping address object",
},
},
"required": ["email"],
},
"scope": "write",
},
{
"name": "update_customer",
"method_name": "update_customer",
"description": "Update an existing WooCommerce customer. Can update name, email, billing, shipping, and other customer fields.",
"schema": {
"type": "object",
"properties": {
"customer_id": {
"type": "integer",
"description": "Customer ID to update",
"minimum": 1,
},
"first_name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Customer first name",
},
"last_name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Customer last name",
},
"email": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Customer email address",
},
"billing": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Billing address object",
},
"shipping": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Shipping address object",
},
},
"required": ["customer_id"],
},
"scope": "write",
},
]
class CustomersHandler:
"""Handle WooCommerce customer operations"""
def __init__(self, client: WordPressClient):
"""
Initialize customers handler.
Args:
client: WordPress API client instance
"""
self.client = client
async def list_customers(
self,
per_page: int = 10,
page: int = 1,
search: str | None = None,
email: str | None = None,
role: str | None = None,
) -> str:
"""
List WooCommerce customers.
Args:
per_page: Number of customers per page (1-100)
page: Page number
search: Search by name or email
email: Filter by specific email
role: Filter by role (customer, subscriber, etc.)
Returns:
JSON string with customers list
"""
try:
# Build query parameters
params = {"per_page": per_page, "page": page}
# Add optional filters
if search:
params["search"] = search
if email:
params["email"] = email
if role:
params["role"] = role
# Make request to WooCommerce API
customers = await self.client.get("customers", params=params, use_woocommerce=True)
# Format response
result = {
"total": len(customers),
"page": page,
"per_page": per_page,
"customers": [
{
"id": customer["id"],
"email": customer["email"],
"first_name": customer.get("first_name", ""),
"last_name": customer.get("last_name", ""),
"username": customer.get("username", ""),
"role": customer.get("role", ""),
"date_created": customer.get("date_created", ""),
"orders_count": customer.get("orders_count", 0),
"total_spent": customer.get("total_spent", "0"),
"avatar_url": customer.get("avatar_url", ""),
}
for customer in customers
],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list customers: {str(e)}"}, indent=2
)
async def get_customer(self, customer_id: int) -> str:
"""
Get detailed information about a specific customer.
Args:
customer_id: Customer ID to retrieve
Returns:
JSON string with customer data
"""
try:
customer = await self.client.get(f"customers/{customer_id}", use_woocommerce=True)
# Format detailed response
result = {
"id": customer["id"],
"email": customer["email"],
"username": customer.get("username", ""),
"first_name": customer.get("first_name", ""),
"last_name": customer.get("last_name", ""),
"role": customer.get("role", ""),
"date_created": customer.get("date_created", ""),
"date_modified": customer.get("date_modified", ""),
"orders_count": customer.get("orders_count", 0),
"total_spent": customer.get("total_spent", "0"),
"avatar_url": customer.get("avatar_url", ""),
"billing": customer.get("billing", {}),
"shipping": customer.get("shipping", {}),
"is_paying_customer": customer.get("is_paying_customer", False),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get customer {customer_id}: {str(e)}"},
indent=2,
)
async def create_customer(
self,
email: str,
first_name: str | None = None,
last_name: str | None = None,
username: str | None = None,
password: str | None = None,
billing: dict | None = None,
shipping: dict | None = None,
) -> str:
"""
Create a new WooCommerce customer.
Args:
email: Customer email (required)
first_name: First name
last_name: Last name
username: Username (generated from email if not provided)
password: Password (auto-generated if not provided)
billing: Billing address dictionary
shipping: Shipping address dictionary
Returns:
JSON string with created customer data
"""
try:
# Build customer data
data = {"email": email}
if first_name:
data["first_name"] = first_name
if last_name:
data["last_name"] = last_name
if username:
data["username"] = username
if password:
data["password"] = password
if billing:
data["billing"] = billing
if shipping:
data["shipping"] = shipping
# Create customer via WooCommerce API
customer = await self.client.post("customers", json_data=data, use_woocommerce=True)
result = {
"id": customer["id"],
"email": customer["email"],
"username": customer.get("username", ""),
"first_name": customer.get("first_name", ""),
"last_name": customer.get("last_name", ""),
"message": f"Customer created successfully with ID {customer['id']}",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to create customer: {str(e)}"}, indent=2
)
async def update_customer(
self,
customer_id: int,
first_name: str | None = None,
last_name: str | None = None,
email: str | None = None,
billing: dict | None = None,
shipping: dict | None = None,
) -> str:
"""
Update an existing WooCommerce customer.
Args:
customer_id: Customer ID to update
first_name: First name
last_name: Last name
email: Email address
billing: Billing address dictionary
shipping: Shipping address dictionary
Returns:
JSON string with updated customer data
"""
try:
# Build data dict with only provided values
data = {}
if first_name is not None:
data["first_name"] = first_name
if last_name is not None:
data["last_name"] = last_name
if email is not None:
data["email"] = email
if billing is not None:
data["billing"] = billing
if shipping is not None:
data["shipping"] = shipping
# Update customer via WooCommerce API
customer = await self.client.put(
f"customers/{customer_id}", json_data=data, use_woocommerce=True
)
result = {
"id": customer["id"],
"email": customer["email"],
"first_name": customer.get("first_name", ""),
"last_name": customer.get("last_name", ""),
"message": f"Customer {customer_id} updated successfully",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update customer {customer_id}: {str(e)}"},
indent=2,
)

View File

@@ -0,0 +1,418 @@
"""Media Handler - manages WordPress media library operations"""
import json
from typing import Any
import aiohttp
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# === MEDIA ===
{
"name": "list_media",
"method_name": "list_media",
"description": "List media library items. Returns images, videos, documents with URLs and metadata.",
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Number of media items per page",
"default": 20,
"minimum": 1,
"maximum": 100,
},
"page": {
"type": "integer",
"description": "Page number for pagination",
"default": 1,
"minimum": 1,
},
"media_type": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Filter by media type (image, video, audio, or application)",
"enum": ["image", "video", "audio", "application"],
},
},
},
"scope": "read",
},
{
"name": "get_media",
"method_name": "get_media",
"description": "Get detailed information about a media item. Returns full metadata including URLs, dimensions, and MIME type.",
"schema": {
"type": "object",
"properties": {
"media_id": {
"type": "integer",
"description": "Media ID to retrieve",
"minimum": 1,
}
},
"required": ["media_id"],
},
"scope": "read",
},
{
"name": "upload_media_from_url",
"method_name": "upload_media_from_url",
"description": "Upload media from URL to media library (sideload). Downloads file from public URL and uploads to WordPress.",
"schema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "Public URL of the media file to upload (image, video, document, etc.)",
},
"title": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Media title (used in media library)",
},
"alt_text": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Alternative text for accessibility (important for images)",
},
"caption": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Media caption (displayed below image when inserted into content)",
},
},
"required": ["url"],
},
"scope": "write",
},
{
"name": "update_media",
"method_name": "update_media",
"description": "Update media metadata. Supports title, description, slug, alt text, caption, status, and associated post.",
"schema": {
"type": "object",
"properties": {
"media_id": {
"type": "integer",
"description": "Media ID to update",
"minimum": 1,
},
"title": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Media title (displayed in media library)",
},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Media description (full text content, displayed in attachment page)",
},
"slug": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Media URL slug (e.g., 'my-image-name')",
},
"alt_text": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Alternative text for accessibility (important for images)",
},
"caption": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Media caption (shown below image in content)",
},
"status": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Publication status of the media",
"enum": ["publish", "draft", "private"],
},
"post": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "ID of the post/page to attach this media to",
},
},
"required": ["media_id"],
},
"scope": "write",
},
{
"name": "delete_media",
"method_name": "delete_media",
"description": "Delete media from library. Can permanently delete or move to trash.",
"schema": {
"type": "object",
"properties": {
"media_id": {
"type": "integer",
"description": "Media ID to delete",
"minimum": 1,
},
"force": {
"type": "boolean",
"description": "Permanently delete (true) or move to trash (false)",
"default": False,
},
},
"required": ["media_id"],
},
"scope": "write",
},
]
class MediaHandler:
"""Handle media-related operations for WordPress"""
def __init__(self, client: WordPressClient):
"""
Initialize media handler.
Args:
client: WordPress API client instance
"""
self.client = client
# === MEDIA ===
async def list_media(
self, per_page: int = 20, page: int = 1, media_type: str | None = None
) -> str:
"""
List media library items.
Args:
per_page: Number of media items per page (1-100)
page: Page number
media_type: Filter by media type (image, video, audio, application)
Returns:
JSON string with media list
"""
try:
params = {"per_page": per_page, "page": page}
if media_type:
params["media_type"] = media_type
media = await self.client.get("media", params=params)
# Format response
result = {
"total": len(media),
"page": page,
"per_page": per_page,
"media": [
{
"id": m["id"],
"title": m["title"]["rendered"],
"mime_type": m["mime_type"],
"media_type": m.get("media_type", ""),
"url": m["source_url"],
"date": m["date"],
"alt_text": m.get("alt_text", ""),
"link": m.get("link", ""),
}
for m in media
],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list media: {str(e)}"}, indent=2
)
async def get_media(self, media_id: int) -> str:
"""
Get detailed information about a specific media item.
Args:
media_id: Media ID to retrieve
Returns:
JSON string with media data
"""
try:
media = await self.client.get(f"media/{media_id}")
result = {
"id": media["id"],
"title": media["title"]["rendered"],
"mime_type": media["mime_type"],
"media_type": media.get("media_type", ""),
"url": media["source_url"],
"alt_text": media.get("alt_text", ""),
"caption": media.get("caption", {}).get("rendered", ""),
"description": media.get("description", {}).get("rendered", ""),
"date": media["date"],
"modified": media.get("modified", ""),
"link": media.get("link", ""),
"media_details": media.get("media_details", {}),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get media {media_id}: {str(e)}"}, indent=2
)
async def upload_media_from_url(
self,
url: str,
title: str | None = None,
alt_text: str | None = None,
caption: str | None = None,
) -> str:
"""
Upload media from URL to media library (sideload).
Downloads file from a public URL and uploads it to WordPress media library.
Args:
url: Public URL of the media file to upload
title: Media title (used in media library)
alt_text: Alternative text for accessibility
caption: Media caption (displayed below image)
Returns:
JSON string with uploaded media data
"""
try:
# Download file from URL
async with aiohttp.ClientSession() as session, session.get(url) as response:
if response.status >= 400:
raise Exception(f"Failed to download from URL: HTTP {response.status}")
file_content = await response.read()
content_type = response.headers.get("Content-Type", "application/octet-stream")
# Extract filename from URL
filename = url.split("/")[-1].split("?")[0]
if not filename:
filename = "downloaded_file"
# Create FormData for upload
form = aiohttp.FormData()
form.add_field("file", file_content, filename=filename, content_type=content_type)
# Upload to WordPress using client's upload method
# Note: We need to use the client's base_url and auth directly for file upload
upload_url = f"{self.client.base_url}/media"
headers = {
"Authorization": self.client.auth_header,
"Content-Disposition": f'attachment; filename="{filename}"',
}
async with aiohttp.ClientSession() as session:
async with session.post(upload_url, data=form, headers=headers) as response:
if response.status >= 400:
error_text = await response.text()
raise Exception(f"Upload failed (HTTP {response.status}): {error_text}")
media = await response.json()
# Update metadata if provided
if title or alt_text or caption:
update_data = {}
if title:
update_data["title"] = title
if alt_text:
update_data["alt_text"] = alt_text
if caption:
update_data["caption"] = caption
await self.client.post(f"media/{media['id']}", json_data=update_data)
result = {
"id": media["id"],
"title": media["title"]["rendered"],
"url": media["source_url"],
"mime_type": media["mime_type"],
"message": f"Media uploaded from URL successfully with ID {media['id']}",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to upload media from URL: {str(e)}"}, indent=2
)
async def update_media(
self,
media_id: int,
title: str | None = None,
description: str | None = None,
slug: str | None = None,
alt_text: str | None = None,
caption: str | None = None,
status: str | None = None,
post: int | None = None,
) -> str:
"""
Update media metadata.
Args:
media_id: Media ID to update
title: Media title
description: Media description
slug: Media URL slug
alt_text: Alternative text for accessibility
caption: Media caption
status: Publication status (publish, draft, private)
post: ID of post/page to attach media to
Returns:
JSON string with updated media data
"""
try:
# Build data dict with only provided values
data = {}
if title is not None:
data["title"] = title
if description is not None:
data["description"] = description
if slug is not None:
data["slug"] = slug
if alt_text is not None:
data["alt_text"] = alt_text
if caption is not None:
data["caption"] = caption
if status is not None:
data["status"] = status
if post is not None:
data["post"] = post
media = await self.client.post(f"media/{media_id}", json_data=data)
result = {
"id": media["id"],
"title": media["title"]["rendered"],
"alt_text": media.get("alt_text", ""),
"caption": media.get("caption", {}).get("rendered", ""),
"url": media["source_url"],
"message": f"Media {media_id} updated successfully",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update media {media_id}: {str(e)}"},
indent=2,
)
async def delete_media(self, media_id: int, force: bool = False) -> str:
"""
Delete or trash media from library.
Args:
media_id: Media ID to delete
force: Permanently delete (True) or move to trash (False)
Returns:
JSON string with deletion result
"""
try:
params = {"force": "true" if force else "false"}
result = await self.client.delete(f"media/{media_id}", params=params)
message = f"Media {media_id} {'permanently deleted' if force else 'moved to trash'}"
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to delete media {media_id}: {str(e)}"},
indent=2,
)

View File

@@ -0,0 +1,401 @@
"""Menus Handler - manages WordPress navigation menus"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
{
"name": "list_menus",
"method_name": "list_menus",
"description": "List all WordPress navigation menus. Returns list of menus with their locations and item counts.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "get_menu",
"method_name": "get_menu",
"description": "Get detailed information about a specific menu including all items. Returns menu details with hierarchical structure of menu items.",
"schema": {
"type": "object",
"properties": {
"menu_id": {
"type": "integer",
"description": "Menu ID to retrieve",
"minimum": 1,
}
},
"required": ["menu_id"],
},
"scope": "read",
},
{
"name": "create_menu",
"method_name": "create_menu",
"description": "Create a new navigation menu. Can optionally assign to theme locations.",
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Menu name (displayed in admin)",
"minLength": 1,
},
"slug": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Menu slug (auto-generated from name if not provided)",
},
"locations": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Theme locations to assign menu to (e.g., ['primary', 'footer'])",
},
},
"required": ["name"],
},
"scope": "write",
},
{
"name": "list_menu_items",
"method_name": "list_menu_items",
"description": "List all items in a specific menu. Returns hierarchical list of menu items with links and ordering.",
"schema": {
"type": "object",
"properties": {
"menu_id": {
"type": "integer",
"description": "Menu ID to get items from",
"minimum": 1,
}
},
"required": ["menu_id"],
},
"scope": "read",
},
{
"name": "create_menu_item",
"method_name": "create_menu_item",
"description": "Add a new item to a menu. Supports linking to posts, pages, categories, or custom URLs.",
"schema": {
"type": "object",
"properties": {
"menu_id": {
"type": "integer",
"description": "Menu ID to add item to",
"minimum": 1,
},
"title": {
"type": "string",
"description": "Item title/label displayed in menu",
"minLength": 1,
},
"type": {
"type": "string",
"description": "Item type: 'post_type' (post/page), 'taxonomy' (category/tag), or 'custom' (URL)",
"enum": ["post_type", "taxonomy", "custom"],
},
"object_id": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "ID of linked post/page/term (required for post_type/taxonomy)",
},
"url": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Custom URL (required for type=custom)",
},
"parent": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Parent item ID for creating sub-menu items",
},
},
"required": ["menu_id", "title", "type"],
},
"scope": "write",
},
{
"name": "update_menu_item",
"method_name": "update_menu_item",
"description": "Update an existing menu item. Can change title, URL, parent, or menu order.",
"schema": {
"type": "object",
"properties": {
"item_id": {
"type": "integer",
"description": "Menu item ID to update",
"minimum": 1,
},
"title": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New item title",
},
"url": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New URL",
},
"parent": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "New parent item ID (0 for top-level)",
},
"menu_order": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Position in menu (lower numbers appear first)",
},
},
"required": ["item_id"],
},
"scope": "write",
},
]
class MenusHandler:
"""Handle menu-related operations for WordPress"""
def __init__(self, client: WordPressClient):
"""
Initialize menus handler.
Args:
client: WordPress API client instance
"""
self.client = client
async def list_menus(self) -> str:
"""
List all WordPress navigation menus.
Returns list of all menus with their locations and item counts.
Returns:
JSON string with total count and menu list
Example response:
{
"total": 3,
"menus": [
{
"id": 2,
"name": "Primary Menu",
"slug": "primary-menu",
"locations": ["primary"],
"count": 8
}
]
}
"""
try:
# WordPress REST API for menus (requires plugin support)
# Try custom endpoint first, fallback to standard if not available
try:
menus = await self.client.get("menus", use_custom_namespace=True)
except:
# Fallback: use wp/v2/navigation endpoint (WP 5.9+)
menus = await self.client.get("navigation")
result = {"total": len(menus) if isinstance(menus, list) else 0, "menus": menus}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list menus: {str(e)}"}, indent=2
)
async def get_menu(self, menu_id: int) -> str:
"""
Get detailed information about a specific menu including all items.
Args:
menu_id: Menu ID
Returns:
JSON string with menu details and items
"""
try:
# Get menu details
try:
menu = await self.client.get(f"menus/{menu_id}", use_custom_namespace=True)
except:
menu = await self.client.get(f"navigation/{menu_id}")
# Get menu items
menu_items = await self.list_menu_items(menu_id)
result = {
"menu": menu,
"items": json.loads(menu_items) if isinstance(menu_items, str) else menu_items,
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get menu {menu_id}: {str(e)}"}, indent=2
)
async def create_menu(
self, name: str, slug: str | None = None, locations: list[str] | None = None
) -> str:
"""
Create a new navigation menu.
Args:
name: Menu name
slug: Menu slug (auto-generated if not provided)
locations: Theme locations to assign menu to
Returns:
JSON string with created menu details
"""
try:
data = {"name": name}
if slug:
data["slug"] = slug
if locations:
data["locations"] = locations
try:
menu = await self.client.post("menus", json_data=data, use_custom_namespace=True)
except:
# Try navigation endpoint
menu = await self.client.post("navigation", json_data=data)
result = {
"id": menu.get("id"),
"name": menu.get("name"),
"slug": menu.get("slug"),
"message": f"Menu '{name}' created successfully",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to create menu: {str(e)}"}, indent=2
)
async def list_menu_items(self, menu_id: int) -> str:
"""
List all items in a specific menu.
Args:
menu_id: Menu ID
Returns:
JSON string with menu items list
"""
try:
params = {"menus": menu_id, "per_page": 100}
items = await self.client.get("menu-items", params=params)
result = {
"total": len(items) if isinstance(items, list) else 0,
"menu_id": menu_id,
"items": items,
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{
"error": str(e),
"message": f"Failed to list menu items for menu {menu_id}: {str(e)}",
},
indent=2,
)
async def create_menu_item(
self,
menu_id: int,
title: str,
type: str,
object_id: int | None = None,
url: str | None = None,
parent: int | None = None,
) -> str:
"""
Add a new item to a menu.
Args:
menu_id: Menu ID to add item to
title: Item title/label
type: Item type (post_type, taxonomy, custom)
object_id: ID of linked post/term (required for post_type/taxonomy)
url: Custom URL (required for type=custom)
parent: Parent item ID for creating sub-menu items
Returns:
JSON string with created menu item
"""
try:
data = {"menus": menu_id, "title": title, "type": type}
if object_id:
data["object_id"] = object_id
if url:
data["url"] = url
if parent:
data["parent"] = parent
item = await self.client.post("menu-items", json_data=data)
result = {
"id": item.get("id"),
"title": item.get("title", {}).get("rendered", title),
"type": item.get("type"),
"url": item.get("url"),
"message": f"Menu item '{title}' created successfully",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to create menu item: {str(e)}"}, indent=2
)
async def update_menu_item(
self,
item_id: int,
title: str | None = None,
url: str | None = None,
parent: int | None = None,
menu_order: int | None = None,
) -> str:
"""
Update an existing menu item.
Args:
item_id: Menu item ID
title: New title
url: New URL
parent: New parent item ID
menu_order: Position in menu
Returns:
JSON string with updated menu item
"""
try:
# Build data dict with only provided values
data = {}
if title is not None:
data["title"] = title
if url is not None:
data["url"] = url
if parent is not None:
data["parent"] = parent
if menu_order is not None:
data["menu_order"] = menu_order
item = await self.client.put(f"menu-items/{item_id}", json_data=data)
result = {
"id": item.get("id"),
"title": item.get("title", {}).get("rendered", title),
"url": item.get("url"),
"menu_order": item.get("menu_order"),
"message": f"Menu item {item_id} updated successfully",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update menu item {item_id}: {str(e)}"},
indent=2,
)

View File

@@ -0,0 +1,449 @@
"""Orders Handler - manages WooCommerce orders"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
{
"name": "list_orders",
"method_name": "list_orders",
"description": "List WooCommerce orders. Returns paginated order list with customer details, totals, and status.",
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Number of orders per page (1-100)",
"default": 10,
"minimum": 1,
"maximum": 100,
},
"page": {
"type": "integer",
"description": "Page number",
"default": 1,
"minimum": 1,
},
"status": {
"anyOf": [
{
"type": "string",
"enum": [
"any",
"pending",
"processing",
"on-hold",
"completed",
"cancelled",
"refunded",
"failed",
"trash",
],
},
{"type": "null"},
],
"description": "Filter by order status (optional)",
},
"customer": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Filter by customer ID",
},
"after": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Filter orders after this date (ISO 8601 format)",
},
"before": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Filter orders before this date (ISO 8601 format)",
},
},
},
"scope": "read",
},
{
"name": "get_order",
"method_name": "get_order",
"description": "Get detailed information about a specific WooCommerce order. Returns full order details including line items, totals, billing, and shipping.",
"schema": {
"type": "object",
"properties": {
"order_id": {
"type": "integer",
"description": "Order ID to retrieve",
"minimum": 1,
}
},
"required": ["order_id"],
},
"scope": "read",
},
{
"name": "update_order_status",
"method_name": "update_order_status",
"description": "Update WooCommerce order status. Change order status to pending, processing, completed, etc.",
"schema": {
"type": "object",
"properties": {
"order_id": {
"type": "integer",
"description": "Order ID to update",
"minimum": 1,
},
"status": {
"type": "string",
"description": "New order status",
"enum": [
"pending",
"processing",
"on-hold",
"completed",
"cancelled",
"refunded",
"failed",
],
},
},
"required": ["order_id", "status"],
},
"scope": "write",
},
{
"name": "create_order",
"method_name": "create_order",
"description": "Create a new WooCommerce order. Supports line items, billing, shipping, and payment method configuration.",
"schema": {
"type": "object",
"properties": {
"customer_id": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Customer ID (optional)",
},
"line_items": {
"anyOf": [
{
"type": "array",
"items": {
"type": "object",
"properties": {
"product_id": {
"type": "integer",
"description": "Product ID",
},
"quantity": {
"type": "integer",
"description": "Quantity",
"minimum": 1,
},
"variation_id": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Variation ID (for variable products)",
},
},
"required": ["product_id", "quantity"],
},
},
{"type": "null"},
],
"description": "Order line items with product IDs and quantities",
},
"billing": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Billing address object",
},
"shipping": {
"anyOf": [{"type": "object"}, {"type": "null"}],
"description": "Shipping address object",
},
"payment_method": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Payment method ID (e.g., 'bacs', 'cod', 'paypal')",
},
"status": {
"type": "string",
"description": "Order status",
"enum": ["pending", "processing", "on-hold", "completed"],
"default": "pending",
},
},
},
"scope": "write",
},
{
"name": "delete_order",
"method_name": "delete_order",
"description": "Delete or trash a WooCommerce order. Can permanently delete or move to trash.",
"schema": {
"type": "object",
"properties": {
"order_id": {
"type": "integer",
"description": "Order ID to delete",
"minimum": 1,
},
"force": {
"type": "boolean",
"description": "Permanently delete (true) or move to trash (false)",
"default": False,
},
},
"required": ["order_id"],
},
"scope": "write",
},
]
class OrdersHandler:
"""Handle WooCommerce order-related operations"""
def __init__(self, client: WordPressClient):
"""
Initialize orders handler.
Args:
client: WordPress API client instance
"""
self.client = client
async def list_orders(
self,
per_page: int = 10,
page: int = 1,
status: str | None = None,
customer: int | None = None,
after: str | None = None,
before: str | None = None,
) -> str:
"""
List WooCommerce orders with filters.
Args:
per_page: Number of orders per page (1-100)
page: Page number
status: Filter by order status (any, pending, processing, on-hold, completed, cancelled, refunded, failed, trash)
customer: Filter by customer ID
after: Filter orders after this date (ISO 8601 format)
before: Filter orders before this date (ISO 8601 format)
Returns:
JSON string with orders list
"""
try:
# Build query parameters
params = {"per_page": per_page, "page": page}
# Add optional filters
if status:
params["status"] = status
if customer is not None:
params["customer"] = customer
if after:
params["after"] = after
if before:
params["before"] = before
# Make request to WooCommerce API
orders = await self.client.get("orders", params=params, use_woocommerce=True)
# Format response
result = {
"total": len(orders),
"page": page,
"per_page": per_page,
"orders": [
{
"id": order["id"],
"number": order["number"],
"status": order["status"],
"date_created": order["date_created"],
"date_modified": order.get("date_modified", ""),
"total": order["total"],
"currency": order["currency"],
"customer_id": order["customer_id"],
"billing": {
"first_name": order["billing"].get("first_name", ""),
"last_name": order["billing"].get("last_name", ""),
"email": order["billing"].get("email", ""),
},
"line_items_count": len(order.get("line_items", [])),
"payment_method_title": order.get("payment_method_title", ""),
"transaction_id": order.get("transaction_id", ""),
}
for order in orders
],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list orders: {str(e)}"}, indent=2
)
async def get_order(self, order_id: int) -> str:
"""
Get detailed information about a specific order.
Args:
order_id: Order ID to retrieve
Returns:
JSON string with order data
"""
try:
order = await self.client.get(f"orders/{order_id}", use_woocommerce=True)
# Format detailed response
result = {
"id": order["id"],
"number": order["number"],
"status": order["status"],
"currency": order["currency"],
"date_created": order["date_created"],
"date_modified": order.get("date_modified", ""),
"discount_total": order["discount_total"],
"shipping_total": order["shipping_total"],
"total": order["total"],
"total_tax": order["total_tax"],
"customer_id": order["customer_id"],
"customer_note": order.get("customer_note", ""),
"billing": order["billing"],
"shipping": order["shipping"],
"payment_method": order["payment_method"],
"payment_method_title": order.get("payment_method_title", ""),
"transaction_id": order.get("transaction_id", ""),
"line_items": [
{
"id": item["id"],
"name": item["name"],
"product_id": item["product_id"],
"quantity": item["quantity"],
"subtotal": item["subtotal"],
"total": item["total"],
"sku": item.get("sku", ""),
}
for item in order.get("line_items", [])
],
"shipping_lines": order.get("shipping_lines", []),
"fee_lines": order.get("fee_lines", []),
"coupon_lines": order.get("coupon_lines", []),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get order {order_id}: {str(e)}"}, indent=2
)
async def update_order_status(self, order_id: int, status: str) -> str:
"""
Update order status.
Args:
order_id: Order ID to update
status: New status (pending, processing, on-hold, completed, cancelled, refunded, failed)
Returns:
JSON string with updated order data
"""
try:
data = {"status": status}
order = await self.client.put(
f"orders/{order_id}", json_data=data, use_woocommerce=True
)
result = {
"id": order["id"],
"number": order["number"],
"status": order["status"],
"message": f"Order #{order['number']} status updated to '{status}'",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update order {order_id} status: {str(e)}"},
indent=2,
)
async def create_order(
self,
customer_id: int | None = None,
line_items: list[dict] | None = None,
billing: dict | None = None,
shipping: dict | None = None,
payment_method: str | None = None,
status: str = "pending",
) -> str:
"""
Create a new order.
Args:
customer_id: Customer ID (optional)
line_items: List of line items [{"product_id": 123, "quantity": 1}]
billing: Billing address dictionary
shipping: Shipping address dictionary
payment_method: Payment method ID (e.g., 'bacs', 'cod', 'paypal')
status: Order status (default: pending)
Returns:
JSON string with created order data
"""
try:
data = {"status": status}
if customer_id is not None:
data["customer_id"] = customer_id
if line_items:
data["line_items"] = line_items
if billing:
data["billing"] = billing
if shipping:
data["shipping"] = shipping
if payment_method:
data["payment_method"] = payment_method
order = await self.client.post("orders", json_data=data, use_woocommerce=True)
result = {
"id": order["id"],
"number": order["number"],
"status": order["status"],
"total": order["total"],
"currency": order["currency"],
"message": f"Order #{order['number']} created successfully with ID {order['id']}",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to create order: {str(e)}"}, indent=2
)
async def delete_order(self, order_id: int, force: bool = False) -> str:
"""
Delete or trash an order.
Args:
order_id: Order ID to delete
force: Permanently delete (True) or move to trash (False)
Returns:
JSON string with deletion result
"""
try:
params = {"force": "true" if force else "false"}
result = await self.client.delete(
f"orders/{order_id}", params=params, use_woocommerce=True
)
message = f"Order {order_id} {'permanently deleted' if force else 'moved to trash'}"
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to delete order {order_id}: {str(e)}"},
indent=2,
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,299 @@
"""Reports Handler - manages WooCommerce reporting and analytics"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# === WOOCOMMERCE REPORTS ===
{
"name": "get_sales_report",
"method_name": "get_sales_report",
"description": "Get WooCommerce sales report. Returns sales data with totals and date ranges. Note: WooCommerce v3 API has limited reporting capabilities.",
"schema": {
"type": "object",
"properties": {
"period": {
"type": "string",
"description": "Report period (week, month, last_month, year)",
"enum": ["week", "month", "last_month", "year"],
"default": "week",
},
"date_min": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Start date for report (ISO 8601 format, e.g., '2024-01-01')",
},
"date_max": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "End date for report (ISO 8601 format, e.g., '2024-12-31')",
},
},
},
"scope": "read",
},
{
"name": "get_top_sellers",
"method_name": "get_top_sellers",
"description": "Get top selling products report. Returns products with highest sales quantities.",
"schema": {
"type": "object",
"properties": {
"period": {
"type": "string",
"description": "Report period (week, month, last_month, year)",
"enum": ["week", "month", "last_month", "year"],
"default": "week",
},
"date_min": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Start date for report (ISO 8601 format)",
},
"date_max": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "End date for report (ISO 8601 format)",
},
},
},
"scope": "read",
},
{
"name": "get_customer_report",
"method_name": "get_customer_report",
"description": "Get customer statistics report. Returns customer count and spending data. Falls back to customer list if reports endpoint unavailable.",
"schema": {
"type": "object",
"properties": {
"period": {
"type": "string",
"description": "Report period (week, month, last_month, year)",
"enum": ["week", "month", "last_month", "year"],
"default": "week",
},
"date_min": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Start date for report (ISO 8601 format)",
},
"date_max": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "End date for report (ISO 8601 format)",
},
},
},
"scope": "read",
},
]
class ReportsHandler:
"""Handle WooCommerce reporting operations"""
def __init__(self, client: WordPressClient):
"""
Initialize reports handler.
Args:
client: WordPress API client instance
"""
self.client = client
# === WOOCOMMERCE REPORTS ===
async def get_sales_report(
self, period: str = "week", date_min: str | None = None, date_max: str | None = None
) -> str:
"""
Get WooCommerce sales report.
Args:
period: Report period (week, month, last_month, year)
date_min: Start date for report (ISO 8601 format, e.g., '2024-01-01')
date_max: End date for report (ISO 8601 format, e.g., '2024-12-31')
Returns:
JSON string with sales report data
Note:
WooCommerce v3 API has limited reporting capabilities.
For advanced analytics, use WooCommerce Analytics extension.
"""
try:
params = {"period": period}
if date_min:
params["date_min"] = date_min
if date_max:
params["date_max"] = date_max
report_data = await self.client.get(
"reports/sales", params=params, use_woocommerce=True
)
# Format the response based on what the API returns
result = {
"period": period,
"sales_data": report_data if isinstance(report_data, list) else [report_data],
"note": "WooCommerce v3 API has limited reporting. For advanced analytics, use WooCommerce Analytics extension.",
}
return json.dumps(result, indent=2)
except Exception as e:
error_msg = str(e)
# Check if reports endpoint is not available
if "404" in error_msg or "not found" in error_msg.lower():
return json.dumps(
{
"error": "Sales reports endpoint not available",
"message": "WooCommerce v3 API has limited reporting capabilities. Consider using WooCommerce Analytics or custom queries.",
"details": error_msg,
},
indent=2,
)
return json.dumps(
{"error": str(e), "message": f"Failed to get sales report: {str(e)}"}, indent=2
)
async def get_top_sellers(
self, period: str = "week", date_min: str | None = None, date_max: str | None = None
) -> str:
"""
Get top selling products report.
Args:
period: Report period (week, month, last_month, year)
date_min: Start date for report (ISO 8601 format)
date_max: End date for report (ISO 8601 format)
Returns:
JSON string with top sellers data
"""
try:
params = {"period": period}
if date_min:
params["date_min"] = date_min
if date_max:
params["date_max"] = date_max
top_sellers = await self.client.get(
"reports/top_sellers", params=params, use_woocommerce=True
)
result = {
"period": period,
"total_products": len(top_sellers) if isinstance(top_sellers, list) else 0,
"top_sellers": [
{
"product_id": item.get("product_id"),
"title": item.get("title"),
"quantity": item.get("quantity", 0),
}
for item in (top_sellers if isinstance(top_sellers, list) else [])
],
}
return json.dumps(result, indent=2)
except Exception as e:
error_msg = str(e)
if "404" in error_msg or "not found" in error_msg.lower():
return json.dumps(
{
"error": "Top sellers endpoint not available",
"message": "WooCommerce v3 API has limited reporting capabilities.",
"details": error_msg,
},
indent=2,
)
return json.dumps(
{"error": str(e), "message": f"Failed to get top sellers: {str(e)}"}, indent=2
)
async def get_customer_report(
self, period: str = "week", date_min: str | None = None, date_max: str | None = None
) -> str:
"""
Get customer statistics report.
Args:
period: Report period (week, month, last_month, year)
date_min: Start date for report (ISO 8601 format)
date_max: End date for report (ISO 8601 format)
Returns:
JSON string with customer report data
Note:
Falls back to customer list endpoint if reports endpoint is unavailable.
"""
try:
params = {"period": period}
if date_min:
params["date_min"] = date_min
if date_max:
params["date_max"] = date_max
try:
customer_data = await self.client.get(
"reports/customers", params=params, use_woocommerce=True
)
result = {
"period": period,
"customer_data": (
customer_data if isinstance(customer_data, list) else [customer_data]
),
"note": "Customer reporting may be limited in WooCommerce v3 API.",
}
return json.dumps(result, indent=2)
except Exception as report_error:
# Check if it's a 404 error - use fallback
if "404" in str(report_error) or "not found" in str(report_error).lower():
return await self._get_customer_report_fallback()
raise report_error
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get customer report: {str(e)}"}, indent=2
)
async def _get_customer_report_fallback(self) -> str:
"""
Fallback method when customer reports endpoint is not available.
Uses the customers list endpoint to generate basic statistics.
Returns:
JSON string with basic customer statistics
"""
try:
# Use customers list to generate basic stats
customers = await self.client.get(
"customers", params={"per_page": 100}, use_woocommerce=True
)
# Calculate basic stats
total_customers = len(customers) if isinstance(customers, list) else 0
total_spent = (
sum(float(c.get("total_spent", 0)) for c in customers)
if isinstance(customers, list)
else 0
)
avg_spent = total_spent / total_customers if total_customers > 0 else 0
result = {
"total_customers": total_customers,
"total_spent": f"{total_spent:.2f}",
"average_spent_per_customer": f"{avg_spent:.2f}",
"note": "Generated from customer list (fallback method). For detailed analytics, use WooCommerce Analytics.",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{
"error": str(e),
"message": "Customer report and fallback both unavailable",
"details": str(e),
},
indent=2,
)

View File

@@ -0,0 +1,617 @@
"""SEO Handler - manages WordPress SEO plugin operations (Yoast/RankMath)"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# === SEO (Rank Math / Yoast) ===
{
"name": "get_post_seo",
"method_name": "get_post_seo",
"description": "Get SEO metadata for a WordPress post or page. Returns Rank Math or Yoast SEO fields including focus keyword, meta title, description, and social media settings. Requires SEO API Bridge plugin.",
"schema": {
"type": "object",
"properties": {
"post_id": {"type": "integer", "description": "Post or Page ID", "minimum": 1}
},
"required": ["post_id"],
},
"scope": "read",
},
{
"name": "get_product_seo",
"method_name": "get_product_seo",
"description": "Get SEO metadata for a WooCommerce product. Returns Rank Math or Yoast SEO fields including focus keyword, meta title, description, and social media settings. Requires SEO API Bridge plugin.",
"schema": {
"type": "object",
"properties": {
"product_id": {"type": "integer", "description": "Product ID", "minimum": 1}
},
"required": ["product_id"],
},
"scope": "read",
},
{
"name": "update_post_seo",
"method_name": "update_post_seo",
"description": "Update SEO metadata for a WordPress post or page. Supports both Rank Math and Yoast SEO fields. Automatically detects which plugin is active. Requires SEO API Bridge plugin.",
"schema": {
"type": "object",
"properties": {
"post_id": {
"type": "integer",
"description": "Post or Page ID to update",
"minimum": 1,
},
"focus_keyword": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Primary focus keyword for SEO",
},
"seo_title": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "SEO meta title (appears in search results)",
},
"meta_description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "SEO meta description (appears in search results)",
},
"additional_keywords": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Additional keywords (comma-separated)",
},
"canonical_url": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Canonical URL for this content",
},
"robots": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Robots meta directives (e.g., ['noindex', 'nofollow'])",
},
"og_title": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Open Graph title for Facebook",
},
"og_description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Open Graph description for Facebook",
},
"og_image": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Open Graph image URL for Facebook",
},
"twitter_title": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Twitter Card title",
},
"twitter_description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Twitter Card description",
},
"twitter_image": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Twitter Card image URL",
},
},
"required": ["post_id"],
},
"scope": "write",
},
{
"name": "update_product_seo",
"method_name": "update_product_seo",
"description": "Update SEO metadata for a WooCommerce product. Same as update_post_seo but specifically for products. Requires SEO API Bridge plugin.",
"schema": {
"type": "object",
"properties": {
"product_id": {
"type": "integer",
"description": "Product ID to update",
"minimum": 1,
},
"focus_keyword": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Primary focus keyword for SEO",
},
"seo_title": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "SEO meta title (appears in search results)",
},
"meta_description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "SEO meta description (appears in search results)",
},
"additional_keywords": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Additional keywords (comma-separated)",
},
"canonical_url": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Canonical URL for this product",
},
"og_title": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Open Graph title for Facebook",
},
"og_description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Open Graph description for Facebook",
},
"og_image": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Open Graph image URL for Facebook",
},
"twitter_title": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Twitter Card title",
},
"twitter_description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Twitter Card description",
},
"twitter_image": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Twitter Card image URL",
},
},
"required": ["product_id"],
},
"scope": "write",
},
]
class SEOHandler:
"""Handle SEO-related operations for WordPress (Yoast SEO and RankMath)"""
def __init__(self, client: WordPressClient):
"""
Initialize SEO handler.
Args:
client: WordPress API client instance
"""
self.client = client
async def _check_seo_plugins(self) -> dict[str, Any]:
"""
Check if Rank Math or Yoast SEO is installed and SEO API Bridge is active.
Returns:
Dict with plugin status information
"""
try:
# First, try to use the new health check endpoint (v1.1.0+)
try:
status_result = await self.client.get(
"seo-api-bridge/v1/status", use_custom_namespace=True
)
if status_result and isinstance(status_result, dict):
# Successfully got status from dedicated endpoint
rank_math_info = status_result.get("seo_plugins", {}).get("rank_math", {})
yoast_info = status_result.get("seo_plugins", {}).get("yoast", {})
return {
"rank_math": {
"active": rank_math_info.get("active", False),
"version": rank_math_info.get("version"),
},
"yoast": {
"active": yoast_info.get("active", False),
"version": yoast_info.get("version"),
},
"api_bridge_active": True,
"api_bridge_version": status_result.get("version"),
"message": status_result.get("message", "SEO API Bridge is active"),
}
except Exception:
# Health check endpoint not available, fall back to old method
pass
# Fallback: Try to check posts for SEO meta fields
result = await self.client.get("posts", params={"per_page": 1})
# If no posts, try products
if not result or (isinstance(result, list) and len(result) == 0):
result = await self.client.get(
"products", params={"per_page": 1}, use_woocommerce=True
)
if not result or (isinstance(result, list) and len(result) == 0):
return {
"rank_math": {"active": False},
"yoast": {"active": False},
"api_bridge_active": False,
"message": "No posts or products available to check SEO plugin status. Please install SEO API Bridge v1.1.0+ or create content with SEO metadata.",
}
# Check if meta fields are present (indicates SEO API Bridge is active)
first_item = result[0] if isinstance(result, list) and len(result) > 0 else {}
meta = first_item.get("meta", {})
# Check for Rank Math fields
rank_math_active = any(
key in meta
for key in [
"rank_math_focus_keyword",
"rank_math_seo_title",
"rank_math_description",
]
)
# Check for Yoast SEO fields
yoast_active = any(
key in meta
for key in ["_yoast_wpseo_focuskw", "_yoast_wpseo_title", "_yoast_wpseo_metadesc"]
)
api_bridge_active = rank_math_active or yoast_active
return {
"rank_math": {"active": rank_math_active},
"yoast": {"active": yoast_active},
"api_bridge_active": api_bridge_active,
"message": (
"SEO API Bridge required. Please install and activate the plugin, then upgrade to v1.1.0+ for better detection."
if not api_bridge_active
else "SEO fields accessible via meta (legacy detection)"
),
}
except Exception as e:
return {
"rank_math": {"active": False},
"yoast": {"active": False},
"api_bridge_active": False,
"message": f"SEO plugin check failed: {str(e)}",
}
# === SEO METHODS ===
async def get_post_seo(self, post_id: int) -> str:
"""
Get SEO metadata for a post or page.
Args:
post_id: Post or Page ID
Returns:
JSON string with SEO metadata
"""
try:
result = await self.client.get(f"posts/{post_id}")
# Extract SEO meta fields
meta = result.get("meta", {})
# Check which SEO plugin is active
has_rank_math = "rank_math_focus_keyword" in meta
has_yoast = "_yoast_wpseo_focuskw" in meta
if not has_rank_math and not has_yoast:
return json.dumps(
{
"error": "SEO API Bridge plugin not detected",
"message": "Please install and activate the SEO API Bridge WordPress plugin.",
},
indent=2,
)
# Format SEO data
seo_data = {
"post_id": post_id,
"post_title": result.get("title", {}).get("rendered", ""),
"plugin_detected": "rank_math" if has_rank_math else "yoast",
}
if has_rank_math:
seo_data.update(
{
"focus_keyword": meta.get("rank_math_focus_keyword", ""),
"seo_title": meta.get("rank_math_seo_title", ""),
"meta_description": meta.get("rank_math_description", ""),
"additional_keywords": meta.get("rank_math_additional_keywords", ""),
"canonical_url": meta.get("rank_math_canonical_url", ""),
"robots": meta.get("rank_math_robots", []),
"breadcrumb_title": meta.get("rank_math_breadcrumb_title", ""),
"open_graph": {
"title": meta.get("rank_math_facebook_title", ""),
"description": meta.get("rank_math_facebook_description", ""),
"image": meta.get("rank_math_facebook_image", ""),
"image_id": meta.get("rank_math_facebook_image_id", ""),
},
"twitter": {
"title": meta.get("rank_math_twitter_title", ""),
"description": meta.get("rank_math_twitter_description", ""),
"image": meta.get("rank_math_twitter_image", ""),
"image_id": meta.get("rank_math_twitter_image_id", ""),
"card_type": meta.get("rank_math_twitter_card_type", ""),
},
}
)
elif has_yoast:
seo_data.update(
{
"focus_keyword": meta.get("_yoast_wpseo_focuskw", ""),
"seo_title": meta.get("_yoast_wpseo_title", ""),
"meta_description": meta.get("_yoast_wpseo_metadesc", ""),
"canonical_url": meta.get("_yoast_wpseo_canonical", ""),
"noindex": meta.get("_yoast_wpseo_meta-robots-noindex", ""),
"nofollow": meta.get("_yoast_wpseo_meta-robots-nofollow", ""),
"breadcrumb_title": meta.get("_yoast_wpseo_bctitle", ""),
"open_graph": {
"title": meta.get("_yoast_wpseo_opengraph-title", ""),
"description": meta.get("_yoast_wpseo_opengraph-description", ""),
"image": meta.get("_yoast_wpseo_opengraph-image", ""),
"image_id": meta.get("_yoast_wpseo_opengraph-image-id", ""),
},
"twitter": {
"title": meta.get("_yoast_wpseo_twitter-title", ""),
"description": meta.get("_yoast_wpseo_twitter-description", ""),
"image": meta.get("_yoast_wpseo_twitter-image", ""),
"image_id": meta.get("_yoast_wpseo_twitter-image-id", ""),
},
}
)
return json.dumps(seo_data, indent=2)
except Exception as e:
return json.dumps(
{
"error": str(e),
"message": f"Failed to get SEO data for post {post_id}: {str(e)}",
},
indent=2,
)
async def get_product_seo(self, product_id: int) -> str:
"""
Get SEO metadata for a WooCommerce product.
Uses SEO API Bridge endpoint for products.
Args:
product_id: Product ID
Returns:
JSON string with SEO metadata
"""
try:
# Use SEO API Bridge endpoint for products (same as update_product_seo)
result = await self.client.get(
f"seo-api-bridge/v1/products/{product_id}/seo", use_custom_namespace=True
)
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{
"error": str(e),
"message": f"Failed to get SEO data for product {product_id}: {str(e)}",
},
indent=2,
)
async def update_post_seo(
self,
post_id: int,
focus_keyword: str | None = None,
seo_title: str | None = None,
meta_description: str | None = None,
additional_keywords: str | None = None,
canonical_url: str | None = None,
robots: list[str] | None = None,
og_title: str | None = None,
og_description: str | None = None,
og_image: str | None = None,
twitter_title: str | None = None,
twitter_description: str | None = None,
twitter_image: str | None = None,
) -> str:
"""
Update SEO metadata for a post or page.
Automatically detects whether Rank Math or Yoast is active and uses appropriate field names.
Args:
post_id: Post or Page ID
focus_keyword: Primary focus keyword
seo_title: Meta title
meta_description: Meta description
additional_keywords: Additional keywords
canonical_url: Canonical URL
robots: Robots meta directives
og_title: Open Graph title
og_description: Open Graph description
og_image: Open Graph image URL
twitter_title: Twitter Card title
twitter_description: Twitter Card description
twitter_image: Twitter Card image URL
Returns:
JSON string with update result
"""
try:
# First check which SEO plugin is active
seo_check = await self._check_seo_plugins()
if not seo_check.get("api_bridge_active"):
return json.dumps(
{
"error": "SEO API Bridge plugin not detected",
"message": "Please install and activate the SEO API Bridge WordPress plugin.",
},
indent=2,
)
# Build meta object based on active plugin
meta = {}
if seo_check.get("rank_math", {}).get("active"):
# Use Rank Math field names
if focus_keyword is not None:
meta["rank_math_focus_keyword"] = focus_keyword
if seo_title is not None:
meta["rank_math_seo_title"] = seo_title
if meta_description is not None:
meta["rank_math_description"] = meta_description
if additional_keywords is not None:
meta["rank_math_additional_keywords"] = additional_keywords
if canonical_url is not None:
meta["rank_math_canonical_url"] = canonical_url
if robots is not None:
meta["rank_math_robots"] = robots
if og_title is not None:
meta["rank_math_facebook_title"] = og_title
if og_description is not None:
meta["rank_math_facebook_description"] = og_description
if og_image is not None:
meta["rank_math_facebook_image"] = og_image
if twitter_title is not None:
meta["rank_math_twitter_title"] = twitter_title
if twitter_description is not None:
meta["rank_math_twitter_description"] = twitter_description
if twitter_image is not None:
meta["rank_math_twitter_image"] = twitter_image
elif seo_check.get("yoast", {}).get("active"):
# Use Yoast field names
if focus_keyword is not None:
meta["_yoast_wpseo_focuskw"] = focus_keyword
if seo_title is not None:
meta["_yoast_wpseo_title"] = seo_title
if meta_description is not None:
meta["_yoast_wpseo_metadesc"] = meta_description
if canonical_url is not None:
meta["_yoast_wpseo_canonical"] = canonical_url
if og_title is not None:
meta["_yoast_wpseo_opengraph-title"] = og_title
if og_description is not None:
meta["_yoast_wpseo_opengraph-description"] = og_description
if og_image is not None:
meta["_yoast_wpseo_opengraph-image"] = og_image
if twitter_title is not None:
meta["_yoast_wpseo_twitter-title"] = twitter_title
if twitter_description is not None:
meta["_yoast_wpseo_twitter-description"] = twitter_description
if twitter_image is not None:
meta["_yoast_wpseo_twitter-image"] = twitter_image
# Update post with meta fields
data = {"meta": meta}
await self.client.post(f"posts/{post_id}", json_data=data)
# Read back saved values for confirmation
saved_seo_json = await self.get_post_seo(post_id)
saved_seo = json.loads(saved_seo_json)
response = {
"post_id": post_id,
"updated_fields": list(meta.keys()),
"message": f"SEO metadata updated successfully for post {post_id}",
"current_values": saved_seo,
}
return json.dumps(response, indent=2)
except Exception as e:
return json.dumps(
{
"error": str(e),
"message": f"Failed to update SEO data for post {post_id}: {str(e)}",
},
indent=2,
)
async def update_product_seo(
self,
product_id: int,
focus_keyword: str | None = None,
seo_title: str | None = None,
meta_description: str | None = None,
additional_keywords: str | None = None,
canonical_url: str | None = None,
og_title: str | None = None,
og_description: str | None = None,
og_image: str | None = None,
twitter_title: str | None = None,
twitter_description: str | None = None,
twitter_image: str | None = None,
) -> str:
"""
Update SEO metadata for a WooCommerce product.
Uses SEO API Bridge endpoint for products.
Args:
product_id: Product ID
focus_keyword: Primary focus keyword
seo_title: Meta title
meta_description: Meta description
additional_keywords: Additional keywords
canonical_url: Canonical URL
og_title: Open Graph title
og_description: Open Graph description
og_image: Open Graph image URL
twitter_title: Twitter Card title
twitter_description: Twitter Card description
twitter_image: Twitter Card image URL
Returns:
JSON string with update result
"""
try:
# Build request data with only provided fields
data = {}
if focus_keyword is not None:
data["focus_keyword"] = focus_keyword
if seo_title is not None:
data["seo_title"] = seo_title
if meta_description is not None:
data["meta_description"] = meta_description
if additional_keywords is not None:
data["additional_keywords"] = additional_keywords
if canonical_url is not None:
data["canonical_url"] = canonical_url
if og_title is not None:
data["og_title"] = og_title
if og_description is not None:
data["og_description"] = og_description
if og_image is not None:
data["og_image"] = og_image
if twitter_title is not None:
data["twitter_title"] = twitter_title
if twitter_description is not None:
data["twitter_description"] = twitter_description
if twitter_image is not None:
data["twitter_image"] = twitter_image
# Use SEO API Bridge endpoint for products
await self.client.post(
f"seo-api-bridge/v1/products/{product_id}/seo",
json_data=data,
use_custom_namespace=True,
)
# Read back saved values for confirmation
saved_seo_json = await self.get_product_seo(product_id)
saved_seo = json.loads(saved_seo_json)
response = {
"product_id": product_id,
"updated_fields": list(data.keys()),
"message": f"SEO metadata updated successfully for product {product_id}",
"current_values": saved_seo,
}
return json.dumps(response, indent=2)
except Exception as e:
return json.dumps(
{
"error": str(e),
"message": f"Failed to update SEO data for product {product_id}: {str(e)}",
},
indent=2,
)

View File

@@ -0,0 +1,243 @@
"""Site Handler - manages WordPress site management operations"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# === PLUGINS ===
{
"name": "list_plugins",
"method_name": "list_plugins",
"description": "List installed WordPress plugins. Shows plugin status (active/inactive), version, and details.",
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Filter plugins by status",
"enum": ["active", "inactive", "all"],
"default": "all",
}
},
},
"scope": "read",
},
# === THEMES ===
{
"name": "list_themes",
"method_name": "list_themes",
"description": "List installed WordPress themes. Returns all themes with their status and metadata.",
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Filter themes by status",
"enum": ["active", "inactive", "all"],
"default": "all",
}
},
},
"scope": "read",
},
{
"name": "get_active_theme",
"method_name": "get_active_theme",
"description": "Get information about the currently active WordPress theme including name, version, and author.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
# === SETTINGS ===
{
"name": "get_settings",
"method_name": "get_settings",
"description": "Get WordPress site settings. Includes site title, description, URL, email, timezone, and language.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
# === HEALTH ===
{
"name": "get_site_health",
"method_name": "get_site_health",
"description": "Check WordPress site health and accessibility. Returns comprehensive health status including WordPress, WooCommerce, and SEO plugin availability.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
]
class SiteHandler:
"""Handle site management operations for WordPress"""
def __init__(self, client: WordPressClient):
"""
Initialize site handler.
Args:
client: WordPress API client instance
"""
self.client = client
# === PLUGINS ===
async def list_plugins(self, status: str = "all") -> str:
"""
List installed WordPress plugins.
Args:
status: Filter by plugin status (active, inactive, all)
Returns:
JSON string with plugins list
"""
try:
# Build endpoint with status filter
params = {}
if status != "all":
params["status"] = status
plugins = await self.client.get("plugins", params=params)
result = {
"total": len(plugins),
"plugins": [
{
"plugin": p["plugin"],
"name": p["name"],
"version": p["version"],
"status": p["status"],
"description": p.get("description", {}).get("raw", "")[:100],
}
for p in plugins
],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list plugins: {str(e)}"}, indent=2
)
# === THEMES ===
async def list_themes(self, status: str = "all") -> str:
"""
List installed WordPress themes.
Args:
status: Filter by theme status (active, inactive, all)
Returns:
JSON string with themes list
"""
try:
# Build endpoint with status filter
params = {}
if status != "all":
params["status"] = status
themes = await self.client.get("themes", params=params)
result = {
"total": len(themes),
"themes": [
{
"stylesheet": t["stylesheet"],
"name": t["name"]["rendered"],
"version": t["version"],
"status": t["status"],
}
for t in themes
],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list themes: {str(e)}"}, indent=2
)
async def get_active_theme(self) -> str:
"""
Get the currently active WordPress theme.
Returns:
JSON string with active theme details
"""
try:
themes = await self.client.get("themes", params={"status": "active"})
if themes:
theme = themes[0]
result = {
"stylesheet": theme["stylesheet"],
"name": theme["name"]["rendered"],
"version": theme["version"],
"author": theme.get("author", {}).get("raw", "Unknown"),
}
else:
result = {"message": "No active theme found"}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get active theme: {str(e)}"}, indent=2
)
# === SETTINGS ===
async def get_settings(self) -> str:
"""
Get WordPress site settings.
Returns:
JSON string with site settings including title, description, URL, email, timezone, and language
"""
try:
settings = await self.client.get("settings")
result = {
"title": settings.get("title", ""),
"description": settings.get("description", ""),
"url": settings.get("url", ""),
"email": settings.get("email", ""),
"timezone": settings.get("timezone_string", ""),
"language": settings.get("language", ""),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get settings: {str(e)}"}, indent=2
)
# === HEALTH ===
async def get_site_health(self) -> str:
"""
Check WordPress site health and accessibility.
Returns:
JSON string with comprehensive health status
"""
try:
health = await self.client.check_site_health()
return json.dumps(health, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get site health: {str(e)}"}, indent=2
)
async def health_check(self) -> dict[str, Any]:
"""
Perform health check using client's check_site_health method.
This method is used internally and returns a dict instead of JSON string.
Returns:
Dict with health status information
"""
return await self.client.check_site_health()

View File

@@ -0,0 +1,689 @@
"""Taxonomy Handler - manages WordPress categories, tags, and custom taxonomies"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# === CATEGORIES ===
{
"name": "list_categories",
"method_name": "list_categories",
"description": "List WordPress post categories. Returns hierarchical list of categories with post counts.",
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Number of categories per page (1-100)",
"default": 100,
"minimum": 1,
"maximum": 100,
},
"page": {
"type": "integer",
"description": "Page number",
"default": 1,
"minimum": 1,
},
"hide_empty": {
"type": "boolean",
"description": "Hide categories with no posts",
"default": False,
},
},
},
"scope": "read",
},
{
"name": "create_category",
"method_name": "create_category",
"description": "Create a new WordPress category. Supports hierarchical categories with parent relationships.",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Category name", "minLength": 1},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Category description",
},
"parent": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Parent category ID for hierarchy",
},
},
"required": ["name"],
},
"scope": "write",
},
{
"name": "update_category",
"method_name": "update_category",
"description": "Update an existing WordPress category. Can modify name, description, and parent category.",
"schema": {
"type": "object",
"properties": {
"category_id": {
"type": "integer",
"description": "Category ID to update",
"minimum": 1,
},
"name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New category name",
},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New category description",
},
"parent": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "New parent category ID",
},
},
"required": ["category_id"],
},
"scope": "write",
},
{
"name": "delete_category",
"method_name": "delete_category",
"description": "Delete a WordPress category. Can permanently delete or reassign posts to another category.",
"schema": {
"type": "object",
"properties": {
"category_id": {
"type": "integer",
"description": "Category ID to delete",
"minimum": 1,
},
"force": {
"type": "boolean",
"description": "Force permanent deletion",
"default": False,
},
},
"required": ["category_id"],
},
"scope": "write",
},
# === TAGS ===
{
"name": "list_tags",
"method_name": "list_tags",
"description": "List WordPress post tags. Returns all tags with usage counts and metadata.",
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Number of tags per page (1-100)",
"default": 100,
"minimum": 1,
"maximum": 100,
},
"page": {
"type": "integer",
"description": "Page number",
"default": 1,
"minimum": 1,
},
"hide_empty": {
"type": "boolean",
"description": "Hide tags with no posts",
"default": False,
},
},
},
"scope": "read",
},
{
"name": "create_tag",
"method_name": "create_tag",
"description": "Create a new WordPress tag. Tag slug is auto-generated from name if not provided.",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Tag name", "minLength": 1},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Tag description",
},
},
"required": ["name"],
},
"scope": "write",
},
{
"name": "update_tag",
"method_name": "update_tag",
"description": "Update an existing WordPress tag. Can modify name and description.",
"schema": {
"type": "object",
"properties": {
"tag_id": {"type": "integer", "description": "Tag ID to update", "minimum": 1},
"name": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New tag name",
},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "New tag description",
},
},
"required": ["tag_id"],
},
"scope": "write",
},
{
"name": "delete_tag",
"method_name": "delete_tag",
"description": "Delete a WordPress tag. Permanently removes the tag from all posts.",
"schema": {
"type": "object",
"properties": {
"tag_id": {"type": "integer", "description": "Tag ID to delete", "minimum": 1},
"force": {
"type": "boolean",
"description": "Force permanent deletion",
"default": False,
},
},
"required": ["tag_id"],
},
"scope": "write",
},
# === CUSTOM TAXONOMIES ===
{
"name": "list_taxonomies",
"method_name": "list_taxonomies",
"description": "List all registered taxonomies including built-in (category, post_tag) and custom taxonomies. Returns configuration and post type associations.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
{
"name": "list_taxonomy_terms",
"method_name": "list_taxonomy_terms",
"description": "List terms of a specific taxonomy. Works with any registered taxonomy including categories, tags, and custom taxonomies.",
"schema": {
"type": "object",
"properties": {
"taxonomy": {
"type": "string",
"description": "Taxonomy slug (e.g., 'category', 'post_tag', 'product_cat')",
},
"per_page": {
"type": "integer",
"description": "Number of terms per page",
"default": 100,
"minimum": 1,
"maximum": 100,
},
"page": {
"type": "integer",
"description": "Page number",
"default": 1,
"minimum": 1,
},
"hide_empty": {
"type": "boolean",
"description": "Hide terms with no posts",
"default": False,
},
"parent": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Filter by parent term ID (for hierarchical taxonomies)",
},
},
"required": ["taxonomy"],
},
"scope": "read",
},
{
"name": "create_taxonomy_term",
"method_name": "create_taxonomy_term",
"description": "Create a new term in a taxonomy. Supports hierarchical taxonomies with parent terms. Works with any registered taxonomy.",
"schema": {
"type": "object",
"properties": {
"taxonomy": {
"type": "string",
"description": "Taxonomy slug (e.g., 'category', 'product_cat')",
},
"name": {"type": "string", "description": "Term name", "minLength": 1},
"description": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Term description",
},
"parent": {
"anyOf": [{"type": "integer"}, {"type": "null"}],
"description": "Parent term ID for hierarchical taxonomies (e.g., subcategories)",
},
},
"required": ["taxonomy", "name"],
},
"scope": "write",
},
]
class TaxonomyHandler:
"""Handle taxonomy-related operations for WordPress"""
def __init__(self, client: WordPressClient):
"""
Initialize taxonomy handler.
Args:
client: WordPress API client instance
"""
self.client = client
# === CATEGORIES ===
async def list_categories(
self, per_page: int = 100, page: int = 1, hide_empty: bool = False
) -> str:
"""
List WordPress post categories.
Args:
per_page: Number of categories per page (1-100)
page: Page number
hide_empty: Hide categories with no posts
Returns:
JSON string with categories list
"""
try:
params = {
"per_page": per_page,
"page": page,
"hide_empty": "true" if hide_empty else "false",
}
categories = await self.client.get("categories", params=params)
result = {
"total": len(categories),
"categories": [
{
"id": cat["id"],
"name": cat["name"],
"slug": cat["slug"],
"description": cat.get("description", ""),
"count": cat.get("count", 0),
"parent": cat.get("parent", 0),
}
for cat in categories
],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list categories: {str(e)}"}, indent=2
)
async def create_category(
self, name: str, description: str | None = None, parent: int | None = None
) -> str:
"""
Create a new WordPress category.
Args:
name: Category name
description: Category description
parent: Parent category ID for hierarchy
Returns:
JSON string with created category data
"""
try:
data = {"name": name}
if description:
data["description"] = description
if parent:
data["parent"] = parent
category = await self.client.post("categories", json_data=data)
result = {
"id": category["id"],
"name": category["name"],
"slug": category["slug"],
"message": f"Category '{name}' created successfully with ID {category['id']}",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to create category: {str(e)}"}, indent=2
)
async def update_category(
self,
category_id: int,
name: str | None = None,
description: str | None = None,
parent: int | None = None,
) -> str:
"""
Update an existing WordPress category.
Args:
category_id: Category ID to update
name: New category name
description: New category description
parent: New parent category ID
Returns:
JSON string with updated category data
"""
try:
# Build data dict with only provided values
data = {}
if name is not None:
data["name"] = name
if description is not None:
data["description"] = description
if parent is not None:
data["parent"] = parent
category = await self.client.post(f"categories/{category_id}", json_data=data)
result = {
"id": category["id"],
"name": category["name"],
"slug": category["slug"],
"message": f"Category {category_id} updated successfully",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update category {category_id}: {str(e)}"},
indent=2,
)
async def delete_category(self, category_id: int, force: bool = False) -> str:
"""
Delete a WordPress category.
Args:
category_id: Category ID to delete
force: Force permanent deletion
Returns:
JSON string with deletion result
"""
try:
params = {"force": "true" if force else "false"}
result = await self.client.delete(f"categories/{category_id}", params=params)
message = f"Category {category_id} deleted successfully"
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to delete category {category_id}: {str(e)}"},
indent=2,
)
# === TAGS ===
async def list_tags(self, per_page: int = 100, page: int = 1, hide_empty: bool = False) -> str:
"""
List WordPress post tags.
Args:
per_page: Number of tags per page (1-100)
page: Page number
hide_empty: Hide tags with no posts
Returns:
JSON string with tags list
"""
try:
params = {
"per_page": per_page,
"page": page,
"hide_empty": "true" if hide_empty else "false",
}
tags = await self.client.get("tags", params=params)
result = {
"total": len(tags),
"tags": [
{
"id": tag["id"],
"name": tag["name"],
"slug": tag["slug"],
"description": tag.get("description", ""),
"count": tag.get("count", 0),
}
for tag in tags
],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list tags: {str(e)}"}, indent=2
)
async def create_tag(self, name: str, description: str | None = None) -> str:
"""
Create a new WordPress tag.
Args:
name: Tag name
description: Tag description
Returns:
JSON string with created tag data
"""
try:
data = {"name": name}
if description:
data["description"] = description
tag = await self.client.post("tags", json_data=data)
result = {
"id": tag["id"],
"name": tag["name"],
"slug": tag["slug"],
"message": f"Tag '{name}' created successfully with ID {tag['id']}",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to create tag: {str(e)}"}, indent=2
)
async def update_tag(
self, tag_id: int, name: str | None = None, description: str | None = None
) -> str:
"""
Update an existing WordPress tag.
Args:
tag_id: Tag ID to update
name: New tag name
description: New tag description
Returns:
JSON string with updated tag data
"""
try:
# Build data dict with only provided values
data = {}
if name is not None:
data["name"] = name
if description is not None:
data["description"] = description
tag = await self.client.post(f"tags/{tag_id}", json_data=data)
result = {
"id": tag["id"],
"name": tag["name"],
"slug": tag["slug"],
"message": f"Tag {tag_id} updated successfully",
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update tag {tag_id}: {str(e)}"}, indent=2
)
async def delete_tag(self, tag_id: int, force: bool = False) -> str:
"""
Delete a WordPress tag.
Args:
tag_id: Tag ID to delete
force: Force permanent deletion
Returns:
JSON string with deletion result
"""
try:
params = {"force": "true" if force else "false"}
result = await self.client.delete(f"tags/{tag_id}", params=params)
message = f"Tag {tag_id} deleted successfully"
return json.dumps({"success": True, "message": message, "result": result}, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to delete tag {tag_id}: {str(e)}"}, indent=2
)
# === CUSTOM TAXONOMIES ===
async def list_taxonomies(self) -> str:
"""
List all registered taxonomies.
Returns list of all taxonomies including built-in (category, post_tag)
and custom taxonomies (portfolio_category, etc.).
Returns:
JSON string with total count and taxonomies list
"""
try:
taxonomies_data = await self.client.get("taxonomies")
# Convert dict to list
taxonomies = []
if isinstance(taxonomies_data, dict):
for slug, data in taxonomies_data.items():
taxonomies.append(
{
"slug": slug,
"name": data.get("name", slug),
"description": data.get("description", ""),
"types": data.get("types", []),
"hierarchical": data.get("hierarchical", False),
"rest_base": data.get("rest_base", slug),
}
)
result = {"total": len(taxonomies), "taxonomies": taxonomies}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list taxonomies: {str(e)}"}, indent=2
)
async def list_taxonomy_terms(
self,
taxonomy: str,
per_page: int = 100,
page: int = 1,
hide_empty: bool = False,
parent: int | None = None,
) -> str:
"""
List terms of a specific taxonomy.
Args:
taxonomy: Taxonomy slug (e.g., 'category', 'product_category')
per_page: Number of terms per page
page: Page number
hide_empty: Hide terms with no posts
parent: Filter by parent term ID
Returns:
JSON string with terms list
"""
try:
params = {
"per_page": per_page,
"page": page,
"hide_empty": "true" if hide_empty else "false",
}
if parent is not None:
params["parent"] = parent
terms = await self.client.get(taxonomy, params=params)
result = {
"total": len(terms) if isinstance(terms, list) else 0,
"taxonomy": taxonomy,
"page": page,
"per_page": per_page,
"terms": terms,
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{
"error": str(e),
"message": f"Failed to list taxonomy terms for '{taxonomy}': {str(e)}",
},
indent=2,
)
async def create_taxonomy_term(
self, taxonomy: str, name: str, description: str | None = None, parent: int | None = None
) -> str:
"""
Create a new term in a taxonomy.
Args:
taxonomy: Taxonomy slug (e.g., 'category', 'product_category')
name: Term name
description: Term description
parent: Parent term ID for hierarchical taxonomies
Returns:
JSON string with created term details
"""
try:
data = {"name": name}
if description:
data["description"] = description
if parent:
data["parent"] = parent
term = await self.client.post(taxonomy, json_data=data)
return json.dumps(term, indent=2)
except Exception as e:
return json.dumps(
{
"error": str(e),
"message": f"Failed to create taxonomy term in '{taxonomy}': {str(e)}",
},
indent=2,
)

View File

@@ -0,0 +1,134 @@
"""Users Handler - manages WordPress user operations"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# === USERS ===
{
"name": "list_users",
"method_name": "list_users",
"description": "List WordPress users. Returns paginated list of users with name, username, email, and roles.",
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Number of users per page (1-100)",
"default": 10,
"minimum": 1,
"maximum": 100,
},
"page": {
"type": "integer",
"description": "Page number",
"default": 1,
"minimum": 1,
},
"roles": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Filter by user roles (e.g., ['administrator', 'editor', 'author'])",
},
},
},
"scope": "read",
},
{
"name": "get_current_user",
"method_name": "get_current_user",
"description": "Get information about the currently authenticated user including ID, name, username, email, and roles.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
]
class UsersHandler:
"""Handle user-related operations for WordPress"""
def __init__(self, client: WordPressClient):
"""
Initialize users handler.
Args:
client: WordPress API client instance
"""
self.client = client
# === USERS ===
async def list_users(
self, per_page: int = 10, page: int = 1, roles: list[str] | None = None
) -> str:
"""
List WordPress users.
Args:
per_page: Number of users per page (1-100)
page: Page number
roles: Filter by user roles (e.g., ['administrator', 'editor'])
Returns:
JSON string with users list
"""
try:
params = {"per_page": per_page, "page": page}
if roles:
params["roles"] = ",".join(roles)
users = await self.client.get("users", params=params)
# Format response
result = {
"total": len(users),
"page": page,
"per_page": per_page,
"users": [
{
"id": user["id"],
"name": user["name"],
"username": user["slug"],
"email": user.get("email", "N/A"),
"roles": user.get("roles", []),
"link": user.get("link", ""),
}
for user in users
],
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list users: {str(e)}"}, indent=2
)
async def get_current_user(self) -> str:
"""
Get current authenticated user.
Returns:
JSON string with current user data
"""
try:
user = await self.client.get("users/me")
result = {
"id": user["id"],
"name": user["name"],
"username": user["slug"],
"email": user.get("email", "N/A"),
"roles": user.get("roles", []),
"capabilities": user.get("capabilities", {}),
"description": user.get("description", ""),
"link": user.get("link", ""),
"registered_date": user.get("registered_date", ""),
}
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get current user: {str(e)}"}, indent=2
)

View File

@@ -0,0 +1,520 @@
"""WP-CLI Handler - manages WordPress WP-CLI operations"""
import json
from typing import Any
from plugins.wordpress.wp_cli import WPCLIManager
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# === CACHE MANAGEMENT (4 tools) ===
{
"name": "wp_cache_flush",
"method_name": "wp_cache_flush",
"description": "Flush WordPress object cache. Clears all cached objects from Redis, Memcached, or file cache. Safe to run anytime.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
{
"name": "wp_cache_type",
"method_name": "wp_cache_type",
"description": "Get the object cache type being used (Redis, Memcached, file-based, etc.).",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
{
"name": "wp_transient_delete_all",
"method_name": "wp_transient_delete_all",
"description": "Delete all expired transients from the database. Improves database performance by cleaning up temporary cached data.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
{
"name": "wp_transient_list",
"method_name": "wp_transient_list",
"description": "List transients in the database (limited to first 100). Shows transient keys with expiration times. Useful for debugging caching issues.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
# === DATABASE OPERATIONS (3 tools) ===
{
"name": "wp_db_check",
"method_name": "wp_db_check",
"description": "Check WordPress database for errors. Runs integrity checks to ensure tables are healthy. Read-only operation.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
{
"name": "wp_db_optimize",
"method_name": "wp_db_optimize",
"description": "Optimize WordPress database tables. Runs OPTIMIZE TABLE on all WordPress tables to reclaim space and improve performance.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
{
"name": "wp_db_export",
"method_name": "wp_db_export",
"description": "Export WordPress database to SQL file in /tmp directory. Creates timestamped backup file for database recovery.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
# === PLUGIN/THEME INFO (4 tools) ===
{
"name": "wp_plugin_list_detailed",
"method_name": "wp_plugin_list_detailed",
"description": "List all WordPress plugins with detailed information including versions, status (active/inactive), and available updates.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
{
"name": "wp_theme_list_detailed",
"method_name": "wp_theme_list_detailed",
"description": "List all WordPress themes with detailed information including versions, status, and active theme identification.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
{
"name": "wp_plugin_verify_checksums",
"method_name": "wp_plugin_verify_checksums",
"description": "Verify plugin file integrity against WordPress.org checksums. Detects tampering or corruption. Only works for plugins from WordPress.org repository.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
{
"name": "wp_core_verify_checksums",
"method_name": "wp_core_verify_checksums",
"description": "Verify WordPress core files against official checksums. Critical security tool for detecting tampering or unauthorized modifications.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
# === SEARCH & REPLACE + UPDATES (4 tools) ===
{
"name": "wp_search_replace_dry_run",
"method_name": "wp_search_replace_dry_run",
"description": "Search and replace in database (DRY RUN ONLY). Previews what would be changed. NEVER makes actual changes. Use for migration planning.",
"schema": {
"type": "object",
"properties": {
"old_string": {
"type": "string",
"description": "String to search for in database",
"minLength": 1,
"maxLength": 500,
},
"new_string": {
"type": "string",
"description": "String to replace with",
"minLength": 1,
"maxLength": 500,
},
"tables": {
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
"description": "Optional list of specific tables to search (default: all tables)",
},
},
"required": ["old_string", "new_string"],
},
"scope": "write",
},
{
"name": "wp_plugin_update",
"method_name": "wp_plugin_update",
"description": "Update WordPress plugin(s). Default is DRY RUN mode (shows available updates). Set dry_run=false to apply updates. Always backup first!",
"schema": {
"type": "object",
"properties": {
"plugin_name": {
"type": "string",
"description": "Plugin slug to update, or 'all' for all plugins",
"minLength": 1,
},
"dry_run": {
"type": "boolean",
"description": "If true, only show available updates without applying them (default: true)",
"default": True,
},
},
"required": ["plugin_name"],
},
"scope": "write",
},
{
"name": "wp_theme_update",
"method_name": "wp_theme_update",
"description": "Update WordPress theme(s). Default is DRY RUN mode (shows available updates). Set dry_run=false to apply updates. WARNING: Updating active theme can break site appearance!",
"schema": {
"type": "object",
"properties": {
"theme_name": {
"type": "string",
"description": "Theme slug to update, or 'all' for all themes",
"minLength": 1,
},
"dry_run": {
"type": "boolean",
"description": "If true, only show available updates without applying them (default: true)",
"default": True,
},
},
"required": ["theme_name"],
},
"scope": "write",
},
{
"name": "wp_core_update",
"method_name": "wp_core_update",
"description": "Update WordPress core. Default is DRY RUN mode (shows available updates). Set dry_run=false to apply updates. CRITICAL: Always backup before core updates!",
"schema": {
"type": "object",
"properties": {
"version": {
"anyOf": [{"type": "string"}, {"type": "null"}],
"description": "Specific version to update to, or null for latest version",
},
"dry_run": {
"type": "boolean",
"description": "If true, only show available updates without applying them (default: true)",
"default": True,
},
},
},
"scope": "write",
},
]
class WPCLIHandler:
"""Handle WP-CLI operations for WordPress"""
def __init__(self, wp_cli: WPCLIManager):
"""
Initialize WP-CLI handler.
Args:
wp_cli: WPCLIManager instance for executing WP-CLI commands
"""
self.wp_cli = wp_cli
# === CACHE MANAGEMENT ===
async def wp_cache_flush(self) -> str:
"""
Flush WordPress object cache.
Clears all cached objects from the object cache (Redis, Memcached, or file).
Safe to run anytime - will not affect database or content.
Returns:
JSON string with flush status and message
"""
try:
result = await self.wp_cli.wp_cache_flush()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to flush cache: {str(e)}"}, indent=2
)
async def wp_cache_type(self) -> str:
"""
Get the object cache type being used.
Shows which caching backend is active (e.g., Redis, Memcached, file-based).
Returns:
JSON string with cache type information
"""
try:
result = await self.wp_cli.wp_cache_type()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to get cache type: {str(e)}"}, indent=2
)
async def wp_transient_delete_all(self) -> str:
"""
Delete all expired transients from the database.
Transients are temporary cached data stored in the WordPress database.
This command only deletes expired transients, improving database performance.
Returns:
JSON string with count of deleted transients
"""
try:
result = await self.wp_cli.wp_transient_delete_all()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to delete transients: {str(e)}"}, indent=2
)
async def wp_transient_list(self) -> str:
"""
List transients in the database (limited to first 100).
Shows transient keys with their expiration times.
Useful for debugging caching issues.
Returns:
JSON string with total count and list of transients (max 100)
"""
try:
result = await self.wp_cli.wp_transient_list()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list transients: {str(e)}"}, indent=2
)
# === DATABASE OPERATIONS ===
async def wp_db_check(self) -> str:
"""
Check WordPress database for errors.
Runs database integrity checks to ensure tables are healthy.
Safe to run - read-only operation.
Returns:
JSON string with health status and tables checked
"""
try:
result = await self.wp_cli.wp_db_check()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to check database: {str(e)}"}, indent=2
)
async def wp_db_optimize(self) -> str:
"""
Optimize WordPress database tables.
Runs OPTIMIZE TABLE on all WordPress tables to reclaim space
and improve performance. Safe operation - non-destructive.
Returns:
JSON string with optimization results
"""
try:
result = await self.wp_cli.wp_db_optimize()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to optimize database: {str(e)}"}, indent=2
)
async def wp_db_export(self) -> str:
"""
Export WordPress database to SQL file in /tmp.
Creates a database backup in the /tmp directory with timestamp.
Safe - exports are only saved to /tmp for security.
Returns:
JSON string with export file path and size
"""
try:
result = await self.wp_cli.wp_db_export()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to export database: {str(e)}"}, indent=2
)
# === PLUGIN/THEME INFO ===
async def wp_plugin_list_detailed(self) -> str:
"""
List all WordPress plugins with detailed information.
Shows plugin names, versions, status (active/inactive), and available updates.
Useful for inventory management and update planning.
Returns:
JSON string with total count and plugin list
"""
try:
result = await self.wp_cli.wp_plugin_list_detailed()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list plugins: {str(e)}"}, indent=2
)
async def wp_theme_list_detailed(self) -> str:
"""
List all WordPress themes with detailed information.
Shows theme names, versions, status, and identifies the active theme.
Useful for theme management and updates.
Returns:
JSON string with total count, theme list, and active theme
"""
try:
result = await self.wp_cli.wp_theme_list_detailed()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to list themes: {str(e)}"}, indent=2
)
async def wp_plugin_verify_checksums(self) -> str:
"""
Verify plugin file integrity against WordPress.org checksums.
Checks all plugins against official checksums to detect tampering or corruption.
Important security tool for detecting malware or unauthorized modifications.
Note: Only works for plugins from WordPress.org repository.
Premium/custom plugins will be skipped.
Returns:
JSON string with verification results
"""
try:
result = await self.wp_cli.wp_plugin_verify_checksums()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to verify plugin checksums: {str(e)}"},
indent=2,
)
async def wp_core_verify_checksums(self) -> str:
"""
Verify WordPress core files against official checksums.
Checks WordPress core files for tampering, corruption, or unauthorized modifications.
Critical security tool for ensuring WordPress integrity.
Returns:
JSON string with verification status and any modified files
"""
try:
result = await self.wp_cli.wp_core_verify_checksums()
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to verify core checksums: {str(e)}"}, indent=2
)
# === SEARCH & REPLACE + UPDATES ===
async def wp_search_replace_dry_run(
self, old_string: str, new_string: str, tables: list[str] | None = None
) -> str:
"""
Search and replace in database (DRY RUN ONLY - no actual changes).
Previews what would be changed if you run search-replace.
ALWAYS runs in dry-run mode - never makes actual changes.
Security: This tool ONLY shows what would be changed. To make actual
changes, you must use WP-CLI directly with appropriate backups.
Args:
old_string: String to search for
new_string: String to replace with
tables: Optional list of specific tables to search (default: all tables)
Returns:
JSON string with preview of changes
"""
try:
result = await self.wp_cli.wp_search_replace_dry_run(
old_string=old_string, new_string=new_string, tables=tables
)
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to run search-replace dry run: {str(e)}"},
indent=2,
)
async def wp_plugin_update(self, plugin_name: str, dry_run: bool = True) -> str:
"""
Update WordPress plugin(s) - DRY RUN by default.
Shows available updates or performs actual update.
Default behavior is DRY RUN for safety.
Security:
- Default: dry_run=True (only shows what would be updated)
- Before actual update: backup database and files
- Check plugin compatibility before major version updates
Args:
plugin_name: Plugin slug or "all" for all plugins
dry_run: If True, only show available updates (default: True)
Returns:
JSON string with update information or results
"""
try:
result = await self.wp_cli.wp_plugin_update(plugin_name=plugin_name, dry_run=dry_run)
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update plugin: {str(e)}"}, indent=2
)
async def wp_theme_update(self, theme_name: str, dry_run: bool = True) -> str:
"""
Update WordPress theme(s) - DRY RUN by default.
Shows available updates or performs actual update.
Default behavior is DRY RUN for safety.
Security:
- Default: dry_run=True (only shows what would be updated)
- Before actual update: backup database and files
- Test theme compatibility after updates
- WARNING: Updating active theme can break site appearance
Args:
theme_name: Theme slug or "all" for all themes
dry_run: If True, only show available updates (default: True)
Returns:
JSON string with update information or results
"""
try:
result = await self.wp_cli.wp_theme_update(theme_name=theme_name, dry_run=dry_run)
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update theme: {str(e)}"}, indent=2
)
async def wp_core_update(self, version: str | None = None, dry_run: bool = True) -> str:
"""
Update WordPress core - DRY RUN by default.
Shows available updates or performs actual core update.
Default behavior is DRY RUN for safety.
Security:
- Default: dry_run=True (only shows what would be updated)
- CRITICAL: Always backup database and files before core updates
- Check plugin/theme compatibility before major version updates
- Test thoroughly on staging environment first
- Major version updates may have breaking changes
Args:
version: Specific version to update to, or None for latest (default: None)
dry_run: If True, only show available updates (default: True)
Returns:
JSON string with update information or results
"""
try:
result = await self.wp_cli.wp_core_update(version=version, dry_run=dry_run)
return json.dumps(result, indent=2)
except Exception as e:
return json.dumps(
{"error": str(e), "message": f"Failed to update WordPress core: {str(e)}"}, indent=2
)

390
plugins/wordpress/plugin.py Normal file
View File

@@ -0,0 +1,390 @@
"""
WordPress Plugin - Option B Clean Architecture
Complete WordPress content management through REST API and WP-CLI.
Refactored to use modular handlers for better organization and maintainability.
Note: WooCommerce functionality split to separate plugin in Phase D.1.
"""
from typing import Any
from plugins.base import BasePlugin
from plugins.wordpress import handlers
from plugins.wordpress.client import WordPressClient
class WordPressPlugin(BasePlugin):
"""
WordPress project plugin - Option B architecture.
Provides comprehensive WordPress content management capabilities including:
- Content (posts, pages, custom post types)
- Media library
- Taxonomy (categories, tags, custom taxonomies)
- Comments
- Users
- Site management (plugins, themes, settings)
- SEO (Yoast, RankMath)
- Menus and navigation
- WP-CLI operations (cache, database, updates)
- Internal link analysis
Note: WooCommerce functionality moved to separate woocommerce plugin (Phase D.1)
Total: 65 tools
"""
@staticmethod
def get_plugin_name() -> str:
"""Return plugin type identifier"""
return "wordpress"
@staticmethod
def get_required_config_keys() -> list[str]:
"""Return required configuration keys"""
return ["url", "username", "app_password"]
def __init__(self, config: dict[str, Any], project_id: str | None = None):
"""
Initialize WordPress plugin with handlers.
Args:
config: Configuration dictionary containing:
- url: WordPress site URL
- username: WordPress username
- app_password: WordPress application password
- container: (Optional) Docker container name for WP-CLI
project_id: Optional project ID (auto-generated if not provided)
"""
super().__init__(config, project_id=project_id)
# Create WordPress API client
self.client = WordPressClient(
site_url=config["url"], username=config["username"], app_password=config["app_password"]
)
# Initialize core WordPress handlers
self.posts = handlers.PostsHandler(self.client)
self.media = handlers.MediaHandler(self.client)
self.taxonomy = handlers.TaxonomyHandler(self.client)
self.comments = handlers.CommentsHandler(self.client)
self.users = handlers.UsersHandler(self.client)
self.site = handlers.SiteHandler(self.client)
self.seo = handlers.SEOHandler(self.client)
self.menus = handlers.MenusHandler(self.client)
# Note: WooCommerce handlers moved to woocommerce plugin (Phase D.1)
# self.products, self.orders, self.customers, self.reports, self.coupons
# WP-CLI handler (optional - requires container)
container_name = config.get("container")
if container_name:
from plugins.wordpress.wp_cli import WPCLIManager
wp_cli_manager = WPCLIManager(container_name)
self.wp_cli = handlers.WPCLIHandler(wp_cli_manager)
else:
self.wp_cli = None
# Note: Database, Bulk, and System operations moved to wordpress_advanced plugin
@staticmethod
def get_tool_specifications() -> list[dict[str, Any]]:
"""
Return all tool specifications for ToolGenerator.
This method is called by ToolGenerator to create unified tools
with site parameter routing.
Returns:
List of tool specification dictionaries (65 tools)
"""
specs = []
# Core WordPress handlers
specs.extend(handlers.get_posts_specs()) # 13 tools
specs.extend(handlers.get_media_specs()) # 5 tools
specs.extend(handlers.get_taxonomy_specs()) # 11 tools
specs.extend(handlers.get_comments_specs()) # 5 tools
specs.extend(handlers.get_users_specs()) # 2 tools
specs.extend(handlers.get_site_specs()) # 5 tools
# Advanced content handlers
specs.extend(handlers.get_seo_specs()) # 4 tools
specs.extend(handlers.get_menus_specs()) # 5 tools
specs.extend(handlers.get_wp_cli_specs()) # 15 tools
# Note: WooCommerce specs moved to woocommerce plugin (Phase D.1)
# Note: Database, Bulk, and System specs in wordpress_advanced plugin
return specs
async def health_check(self) -> dict[str, Any]:
"""
Check WordPress site health and feature availability.
Returns:
Dict with health status, WooCommerce, and SEO plugin availability
"""
return await self.site.health_check()
# ========================================
# Method Delegation to Handlers
# ========================================
# All methods delegate to appropriate handlers
# This maintains backward compatibility with existing code
# === Posts & Pages ===
async def list_posts(self, **kwargs):
return await self.posts.list_posts(**kwargs)
async def get_post(self, **kwargs):
return await self.posts.get_post(**kwargs)
async def create_post(self, **kwargs):
return await self.posts.create_post(**kwargs)
async def update_post(self, **kwargs):
return await self.posts.update_post(**kwargs)
async def delete_post(self, **kwargs):
return await self.posts.delete_post(**kwargs)
async def list_pages(self, **kwargs):
return await self.posts.list_pages(**kwargs)
async def create_page(self, **kwargs):
return await self.posts.create_page(**kwargs)
async def update_page(self, **kwargs):
return await self.posts.update_page(**kwargs)
async def delete_page(self, **kwargs):
return await self.posts.delete_page(**kwargs)
async def list_post_types(self, **kwargs):
return await self.posts.list_post_types(**kwargs)
async def get_post_type_info(self, **kwargs):
return await self.posts.get_post_type_info(**kwargs)
async def list_custom_posts(self, **kwargs):
return await self.posts.list_custom_posts(**kwargs)
async def create_custom_post(self, **kwargs):
return await self.posts.create_custom_post(**kwargs)
async def get_internal_links(self, **kwargs):
return await self.posts.get_internal_links(**kwargs)
# === Media ===
async def list_media(self, **kwargs):
return await self.media.list_media(**kwargs)
async def get_media(self, **kwargs):
return await self.media.get_media(**kwargs)
async def upload_media_from_url(self, **kwargs):
return await self.media.upload_media_from_url(**kwargs)
async def update_media(self, **kwargs):
return await self.media.update_media(**kwargs)
async def delete_media(self, **kwargs):
return await self.media.delete_media(**kwargs)
# === Taxonomy (Categories & Tags) ===
async def list_categories(self, **kwargs):
return await self.taxonomy.list_categories(**kwargs)
async def create_category(self, **kwargs):
return await self.taxonomy.create_category(**kwargs)
async def update_category(self, **kwargs):
return await self.taxonomy.update_category(**kwargs)
async def delete_category(self, **kwargs):
return await self.taxonomy.delete_category(**kwargs)
async def list_tags(self, **kwargs):
return await self.taxonomy.list_tags(**kwargs)
async def create_tag(self, **kwargs):
return await self.taxonomy.create_tag(**kwargs)
async def update_tag(self, **kwargs):
return await self.taxonomy.update_tag(**kwargs)
async def delete_tag(self, **kwargs):
return await self.taxonomy.delete_tag(**kwargs)
async def list_taxonomies(self, **kwargs):
return await self.taxonomy.list_taxonomies(**kwargs)
async def list_taxonomy_terms(self, **kwargs):
return await self.taxonomy.list_taxonomy_terms(**kwargs)
async def create_taxonomy_term(self, **kwargs):
return await self.taxonomy.create_taxonomy_term(**kwargs)
# === Comments ===
async def list_comments(self, **kwargs):
return await self.comments.list_comments(**kwargs)
async def get_comment(self, **kwargs):
return await self.comments.get_comment(**kwargs)
async def create_comment(self, **kwargs):
return await self.comments.create_comment(**kwargs)
async def update_comment(self, **kwargs):
return await self.comments.update_comment(**kwargs)
async def delete_comment(self, **kwargs):
return await self.comments.delete_comment(**kwargs)
# === Users ===
async def list_users(self, **kwargs):
return await self.users.list_users(**kwargs)
async def get_current_user(self, **kwargs):
return await self.users.get_current_user(**kwargs)
# === Site Management ===
async def list_plugins(self, **kwargs):
return await self.site.list_plugins(**kwargs)
async def list_themes(self, **kwargs):
return await self.site.list_themes(**kwargs)
async def get_active_theme(self, **kwargs):
return await self.site.get_active_theme(**kwargs)
async def get_settings(self, **kwargs):
return await self.site.get_settings(**kwargs)
async def get_site_health(self, **kwargs):
return await self.site.get_site_health(**kwargs)
# Note: WooCommerce methods moved to woocommerce plugin (Phase D.1)
# === SEO ===
async def get_post_seo(self, **kwargs):
return await self.seo.get_post_seo(**kwargs)
async def get_product_seo(self, **kwargs):
return await self.seo.get_product_seo(**kwargs)
async def update_post_seo(self, **kwargs):
return await self.seo.update_post_seo(**kwargs)
async def update_product_seo(self, **kwargs):
return await self.seo.update_product_seo(**kwargs)
# === Menus ===
async def list_menus(self, **kwargs):
return await self.menus.list_menus(**kwargs)
async def get_menu(self, **kwargs):
return await self.menus.get_menu(**kwargs)
async def create_menu(self, **kwargs):
return await self.menus.create_menu(**kwargs)
async def list_menu_items(self, **kwargs):
return await self.menus.list_menu_items(**kwargs)
async def create_menu_item(self, **kwargs):
return await self.menus.create_menu_item(**kwargs)
async def update_menu_item(self, **kwargs):
return await self.menus.update_menu_item(**kwargs)
# === WP-CLI Operations ===
async def wp_cache_flush(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_cache_flush(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_cache_type(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_cache_type(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_transient_delete_all(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_transient_delete_all(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_transient_list(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_transient_list(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_db_check(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_db_check(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_db_optimize(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_db_optimize(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_db_export(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_db_export(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_plugin_list_detailed(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_plugin_list_detailed(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_theme_list_detailed(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_theme_list_detailed(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_plugin_verify_checksums(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_plugin_verify_checksums(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_core_verify_checksums(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_core_verify_checksums(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_search_replace_dry_run(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_search_replace_dry_run(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_plugin_update(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_plugin_update(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_theme_update(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_theme_update(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
async def wp_core_update(self, **kwargs):
if self.wp_cli:
return await self.wp_cli.wp_core_update(**kwargs)
return '{"error": "WP-CLI not available. Container not configured."}'
# Note: Database, Bulk, and System operations moved to wordpress_advanced plugin
# === Legacy compatibility methods ===
# These methods are kept for potential backward compatibility
# but are not exposed as tools in Option B architecture
async def check_woocommerce(self) -> dict[str, Any]:
"""Check if WooCommerce is available (legacy method)"""
return await self.client.check_woocommerce()
async def check_seo_plugins(self) -> dict[str, Any]:
"""Check SEO plugins availability (legacy method)"""
return await self.seo._check_seo_plugins()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
"""
WordPress Pydantic Schemas
Type-safe validation schemas for WordPress data structures.
Part of Option B clean architecture refactoring.
"""
from plugins.wordpress.schemas.common import (
ErrorResponse,
PaginationParams,
StatusFilter,
SuccessResponse,
)
from plugins.wordpress.schemas.media import MediaBase, MediaResponse, MediaUpdate, MediaUpload
from plugins.wordpress.schemas.order import (
OrderBase,
OrderCreate,
OrderLineItem,
OrderResponse,
OrderUpdate,
)
from plugins.wordpress.schemas.post import PostBase, PostCreate, PostResponse, PostUpdate
from plugins.wordpress.schemas.product import (
ProductBase,
ProductCategory,
ProductCreate,
ProductResponse,
ProductUpdate,
ProductVariation,
)
from plugins.wordpress.schemas.seo import SEOData, SEOUpdate
# Note: Database, Bulk, and System schemas moved to wordpress_advanced plugin
__all__ = [
# Common
"PaginationParams",
"StatusFilter",
"ErrorResponse",
"SuccessResponse",
# Posts
"PostBase",
"PostCreate",
"PostUpdate",
"PostResponse",
# Media
"MediaBase",
"MediaUpload",
"MediaUpdate",
"MediaResponse",
# Products
"ProductBase",
"ProductCreate",
"ProductUpdate",
"ProductResponse",
"ProductCategory",
"ProductVariation",
# Orders
"OrderBase",
"OrderCreate",
"OrderUpdate",
"OrderResponse",
"OrderLineItem",
# SEO
"SEOData",
"SEOUpdate",
# Note: Database, Bulk, and System schemas moved to wordpress_advanced plugin
]

View File

@@ -0,0 +1,45 @@
"""
Common Pydantic Schemas
Shared validation schemas used across WordPress handlers.
"""
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
class PaginationParams(BaseModel):
"""Pagination parameters for list endpoints"""
per_page: int = Field(default=10, ge=1, le=100, description="Number of items per page (1-100)")
page: int = Field(default=1, ge=1, description="Page number (starts at 1)")
model_config = ConfigDict(extra="forbid")
class StatusFilter(BaseModel):
"""Status filter for posts/pages/products"""
status: str = Field(default="any", description="Filter by status")
@classmethod
@field_validator("status")
def validate_status(cls, v):
allowed = ["publish", "draft", "pending", "private", "any", "future", "trash"]
if v not in allowed:
raise ValueError(f"Status must be one of: {', '.join(allowed)}")
return v
class ErrorResponse(BaseModel):
"""Standard error response"""
error: bool = Field(default=True)
message: str = Field(..., description="Error message")
code: str | None = Field(None, description="Error code")
details: dict[str, Any] | None = Field(None, description="Additional error details")
class SuccessResponse(BaseModel):
"""Standard success response"""
success: bool = Field(default=True)
message: str = Field(..., description="Success message")
data: dict[str, Any] | None = Field(None, description="Response data")

View File

@@ -0,0 +1,56 @@
"""
Media Pydantic Schemas
Validation schemas for WordPress media library operations.
"""
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator
class MediaBase(BaseModel):
"""Base media schema"""
model_config = ConfigDict(extra="allow")
title: str | None = Field(None, description="Media title")
alt_text: str | None = Field(None, description="Alternative text")
caption: str | None = Field(None, description="Media caption")
description: str | None = Field(None, description="Media description")
class MediaUpload(MediaBase):
"""Schema for uploading media from URL"""
url: HttpUrl = Field(..., description="Source URL of the media file")
filename: str | None = Field(None, description="Desired filename")
@classmethod
@field_validator("filename")
def validate_filename(cls, v):
if v is not None:
# Basic filename validation
if "/" in v or "\\" in v:
raise ValueError("Filename cannot contain path separators")
if len(v) > 255:
raise ValueError("Filename too long (max 255 characters)")
return v
class MediaUpdate(MediaBase):
"""Schema for updating media metadata"""
# All fields optional for updates
pass
class MediaResponse(BaseModel):
"""Schema for media response data"""
model_config = ConfigDict(extra="allow")
id: int
title: str
alt_text: str
caption: str
description: str
mime_type: str
media_type: str
source_url: str
link: str
date: str

View File

@@ -0,0 +1,152 @@
"""
Order Pydantic Schemas
Validation schemas for WooCommerce orders and related entities.
"""
from typing import Any
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
class OrderLineItem(BaseModel):
"""Schema for order line item"""
model_config = ConfigDict(extra="allow")
product_id: int = Field(..., description="Product ID")
quantity: int = Field(default=1, ge=1, description="Quantity")
variation_id: int | None = Field(None, description="Variation ID")
subtotal: str | None = Field(None, description="Line subtotal")
total: str | None = Field(None, description="Line total")
class ShippingAddress(BaseModel):
"""Schema for shipping address"""
model_config = ConfigDict(extra="allow")
first_name: str | None = Field(None, description="First name")
last_name: str | None = Field(None, description="Last name")
company: str | None = Field(None, description="Company name")
address_1: str | None = Field(None, description="Address line 1")
address_2: str | None = Field(None, description="Address line 2")
city: str | None = Field(None, description="City")
state: str | None = Field(None, description="State/Province")
postcode: str | None = Field(None, description="Postal code")
country: str | None = Field(None, description="Country code (ISO 3166-1 alpha-2)")
class BillingAddress(ShippingAddress):
"""Schema for billing address (extends shipping)"""
email: EmailStr | None = Field(None, description="Email address")
phone: str | None = Field(None, description="Phone number")
class OrderBase(BaseModel):
"""Base order schema"""
status: str | None = Field("pending", description="Order status")
customer_id: int | None = Field(None, description="Customer user ID")
billing: BillingAddress | None = Field(None, description="Billing address")
shipping: ShippingAddress | None = Field(None, description="Shipping address")
payment_method: str | None = Field(None, description="Payment method ID")
payment_method_title: str | None = Field(None, description="Payment method title")
transaction_id: str | None = Field(None, description="Transaction ID")
customer_note: str | None = Field(None, description="Customer note")
line_items: list[OrderLineItem] | None = Field(None, description="Order line items")
shipping_lines: list[dict[str, Any]] | None = Field(None, description="Shipping lines")
fee_lines: list[dict[str, Any]] | None = Field(None, description="Fee lines")
coupon_lines: list[dict[str, Any]] | None = Field(None, description="Coupon lines")
model_config = ConfigDict(extra="allow")
@classmethod
@field_validator("status")
def validate_status(cls, v):
if v is not None:
allowed = [
"pending",
"processing",
"on-hold",
"completed",
"cancelled",
"refunded",
"failed",
"trash",
]
if v not in allowed:
raise ValueError(f"Status must be one of: {', '.join(allowed)}")
return v
class OrderCreate(OrderBase):
"""Schema for creating a new order"""
line_items: list[OrderLineItem] = Field(
..., min_length=1, description="Order line items (required)"
)
model_config = ConfigDict(extra="allow")
class OrderUpdate(OrderBase):
"""Schema for updating an existing order"""
# All fields optional for updates
pass
class OrderStatusUpdate(BaseModel):
"""Schema for updating order status"""
status: str = Field(..., description="New order status")
@classmethod
@field_validator("status")
def validate_status(cls, v):
allowed = [
"pending",
"processing",
"on-hold",
"completed",
"cancelled",
"refunded",
"failed",
]
if v not in allowed:
raise ValueError(f"Status must be one of: {', '.join(allowed)}")
return v
class OrderResponse(BaseModel):
"""Schema for order response data"""
model_config = ConfigDict(extra="allow")
id: int
number: str
status: str
total: str
date_created: str
billing: dict[str, Any]
shipping: dict[str, Any]
line_items: list[dict[str, Any]]
class CustomerBase(BaseModel):
"""Base customer schema"""
model_config = ConfigDict(extra="allow")
email: EmailStr | None = Field(None, description="Customer email")
first_name: str | None = Field(None, description="First name")
last_name: str | None = Field(None, description="Last name")
username: str | None = Field(None, description="Username")
billing: BillingAddress | None = Field(None, description="Billing address")
shipping: ShippingAddress | None = Field(None, description="Shipping address")
class CustomerCreate(CustomerBase):
"""Schema for creating a new customer"""
email: EmailStr = Field(..., description="Customer email (required)")
model_config = ConfigDict(extra="allow")
class CustomerUpdate(CustomerBase):
"""Schema for updating an existing customer"""
# All fields optional for updates
pass

View File

@@ -0,0 +1,102 @@
"""
Post Pydantic Schemas
Validation schemas for WordPress posts, pages, and custom post types.
"""
from pydantic import BaseModel, ConfigDict, Field, field_validator
class PostBase(BaseModel):
"""Base post schema with common fields"""
title: str | None = Field(None, description="Post title")
content: str | None = Field(None, description="Post content (HTML)")
excerpt: str | None = Field(None, description="Post excerpt")
status: str | None = Field("draft", description="Post status")
slug: str | None = Field(None, description="Post slug (URL-friendly)")
author: int | None = Field(None, description="Author user ID")
featured_media: int | None = Field(None, description="Featured image media ID")
comment_status: str | None = Field(None, description="Comment status (open/closed)")
ping_status: str | None = Field(None, description="Ping status (open/closed)")
format: str | None = Field(None, description="Post format")
meta: dict | None = Field(None, description="Post meta fields")
sticky: bool | None = Field(None, description="Sticky post flag")
categories: list[int] | None = Field(None, description="Category IDs")
tags: list[int] | None = Field(None, description="Tag IDs")
model_config = ConfigDict(extra="allow") # Allow additional fields for custom post types
@classmethod
@field_validator("status")
def validate_status(cls, v):
if v is not None:
allowed = ["publish", "draft", "pending", "private", "future"]
if v not in allowed:
raise ValueError(f"Status must be one of: {', '.join(allowed)}")
return v
@classmethod
@field_validator("comment_status", "ping_status")
def validate_comment_ping_status(cls, v):
if v is not None:
allowed = ["open", "closed"]
if v not in allowed:
raise ValueError("Status must be 'open' or 'closed'")
return v
class PostCreate(PostBase):
"""Schema for creating a new post"""
title: str = Field(..., min_length=1, description="Post title (required)")
content: str = Field(..., description="Post content (required)")
model_config = ConfigDict(extra="allow")
class PostUpdate(PostBase):
"""Schema for updating an existing post"""
# All fields optional for updates
pass
class PostResponse(BaseModel):
"""Schema for post response data"""
model_config = ConfigDict(extra="allow")
id: int
title: str
content: str
excerpt: str
status: str
slug: str
link: str
date: str
modified: str
author: int
featured_media: int
categories: list[int]
tags: list[int]
class PageCreate(PostCreate):
"""Schema for creating a new page"""
parent: int | None = Field(None, description="Parent page ID")
menu_order: int | None = Field(None, description="Menu order")
template: str | None = Field(None, description="Page template")
class PageUpdate(PostUpdate):
"""Schema for updating an existing page"""
parent: int | None = Field(None, description="Parent page ID")
menu_order: int | None = Field(None, description="Menu order")
template: str | None = Field(None, description="Page template")
class CustomPostCreate(PostCreate):
"""Schema for creating custom post types"""
post_type: str = Field(..., description="Custom post type")
class CustomPostUpdate(PostUpdate):
"""Schema for updating custom post types"""
pass

View File

@@ -0,0 +1,140 @@
"""
Product Pydantic Schemas
Validation schemas for WooCommerce products and related entities.
"""
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
class ProductBase(BaseModel):
"""Base product schema"""
name: str | None = Field(None, description="Product name")
type: str | None = Field("simple", description="Product type")
status: str | None = Field("draft", description="Product status")
featured: bool | None = Field(False, description="Featured product flag")
catalog_visibility: str | None = Field("visible", description="Catalog visibility")
description: str | None = Field(None, description="Product description (HTML)")
short_description: str | None = Field(None, description="Short description (HTML)")
sku: str | None = Field(None, description="Stock Keeping Unit")
regular_price: str | None = Field(None, description="Regular price")
sale_price: str | None = Field(None, description="Sale price")
manage_stock: bool | None = Field(False, description="Manage stock flag")
stock_quantity: int | None = Field(None, description="Stock quantity")
stock_status: str | None = Field("instock", description="Stock status")
weight: str | None = Field(None, description="Product weight")
length: str | None = Field(None, description="Product length")
width: str | None = Field(None, description="Product width")
height: str | None = Field(None, description="Product height")
categories: list[dict[str, Any]] | None = Field(None, description="Product categories")
tags: list[dict[str, Any]] | None = Field(None, description="Product tags")
images: list[dict[str, Any]] | None = Field(None, description="Product images")
attributes: list[dict[str, Any]] | None = Field(None, description="Product attributes")
model_config = ConfigDict(extra="allow")
@classmethod
@field_validator("type")
def validate_type(cls, v):
if v is not None:
allowed = ["simple", "grouped", "external", "variable", "variation"]
if v not in allowed:
raise ValueError(f"Type must be one of: {', '.join(allowed)}")
return v
@classmethod
@field_validator("status")
def validate_status(cls, v):
if v is not None:
allowed = ["draft", "pending", "private", "publish"]
if v not in allowed:
raise ValueError(f"Status must be one of: {', '.join(allowed)}")
return v
@classmethod
@field_validator("catalog_visibility")
def validate_visibility(cls, v):
if v is not None:
allowed = ["visible", "catalog", "search", "hidden"]
if v not in allowed:
raise ValueError(f"Visibility must be one of: {', '.join(allowed)}")
return v
@classmethod
@field_validator("stock_status")
def validate_stock_status(cls, v):
if v is not None:
allowed = ["instock", "outofstock", "onbackorder"]
if v not in allowed:
raise ValueError(f"Stock status must be one of: {', '.join(allowed)}")
return v
class ProductCreate(ProductBase):
"""Schema for creating a new product"""
name: str = Field(..., min_length=1, description="Product name (required)")
type: str = Field("simple", description="Product type")
class ProductUpdate(ProductBase):
"""Schema for updating an existing product"""
# All fields optional for updates
pass
class ProductResponse(BaseModel):
"""Schema for product response data"""
model_config = ConfigDict(extra="allow")
id: int
name: str
slug: str
type: str
status: str
featured: bool
regular_price: str
sale_price: str
price: str
stock_status: str
permalink: str
class ProductCategory(BaseModel):
"""Schema for product category"""
model_config = ConfigDict(extra="allow")
name: str = Field(..., description="Category name")
slug: str | None = Field(None, description="Category slug")
parent: int | None = Field(None, description="Parent category ID")
description: str | None = Field(None, description="Category description")
display: str | None = Field("default", description="Display type")
image: dict[str, Any] | None = Field(None, description="Category image")
class ProductVariation(BaseModel):
"""Schema for product variation"""
model_config = ConfigDict(extra="allow")
regular_price: str | None = Field(None, description="Regular price")
sale_price: str | None = Field(None, description="Sale price")
description: str | None = Field(None, description="Variation description")
sku: str | None = Field(None, description="Stock Keeping Unit")
manage_stock: bool | None = Field(False, description="Manage stock flag")
stock_quantity: int | None = Field(None, description="Stock quantity")
stock_status: str | None = Field("instock", description="Stock status")
weight: str | None = Field(None, description="Variation weight")
attributes: list[dict[str, Any]] | None = Field(None, description="Variation attributes")
image: dict[str, Any] | None = Field(None, description="Variation image")
class ProductAttribute(BaseModel):
"""Schema for product attribute"""
model_config = ConfigDict(extra="allow")
name: str = Field(..., description="Attribute name")
slug: str | None = Field(None, description="Attribute slug")
type: str | None = Field("select", description="Attribute type")
order_by: str | None = Field("menu_order", description="Sort order")
has_archives: bool | None = Field(False, description="Enable archives")

View File

@@ -0,0 +1,80 @@
"""
SEO Pydantic Schemas
Validation schemas for SEO plugin data (Yoast, RankMath, etc.).
"""
from pydantic import BaseModel, ConfigDict, Field, field_validator
class SEOData(BaseModel):
"""Schema for SEO data (read)"""
model_config = ConfigDict(extra="allow")
title: str | None = Field(None, description="SEO title")
description: str | None = Field(None, description="Meta description")
keywords: str | None = Field(None, description="Focus keywords")
canonical: str | None = Field(None, description="Canonical URL")
og_title: str | None = Field(None, description="Open Graph title")
og_description: str | None = Field(None, description="Open Graph description")
og_image: str | None = Field(None, description="Open Graph image URL")
twitter_title: str | None = Field(None, description="Twitter card title")
twitter_description: str | None = Field(None, description="Twitter card description")
twitter_image: str | None = Field(None, description="Twitter card image URL")
robots: list[str] | None = Field(None, description="Robots meta tags")
class SEOUpdate(BaseModel):
"""Schema for SEO data updates"""
title: str | None = Field(
None, max_length=60, description="SEO title (max 60 chars recommended)"
)
description: str | None = Field(
None, max_length=160, description="Meta description (max 160 chars recommended)"
)
keywords: str | None = Field(None, description="Focus keywords (comma-separated)")
canonical: str | None = Field(None, description="Canonical URL")
og_title: str | None = Field(None, description="Open Graph title")
og_description: str | None = Field(None, description="Open Graph description")
og_image: str | None = Field(None, description="Open Graph image URL")
twitter_title: str | None = Field(None, description="Twitter card title")
twitter_description: str | None = Field(None, description="Twitter card description")
twitter_image: str | None = Field(None, description="Twitter card image URL")
robots_index: bool | None = Field(None, description="Allow search engines to index")
robots_follow: bool | None = Field(None, description="Allow search engines to follow links")
model_config = ConfigDict(extra="allow")
@classmethod
@field_validator("title")
def validate_title_length(cls, v):
if v and len(v) > 70:
# Warning, not error - allow but discourage
pass
return v
@classmethod
@field_validator("description")
def validate_description_length(cls, v):
if v and len(v) > 200:
# Warning, not error - allow but discourage
pass
return v
class YoastSEO(SEOData):
"""Yoast SEO specific data"""
model_config = ConfigDict(extra="allow")
yoast_wpseo_focuskw: str | None = Field(None, description="Yoast focus keyword")
yoast_wpseo_metadesc: str | None = Field(None, description="Yoast meta description")
yoast_wpseo_title: str | None = Field(None, description="Yoast SEO title")
class RankMathSEO(SEOData):
"""RankMath SEO specific data"""
model_config = ConfigDict(extra="allow")
rank_math_focus_keyword: str | None = Field(None, description="RankMath focus keyword")
rank_math_description: str | None = Field(None, description="RankMath meta description")
rank_math_title: str | None = Field(None, description="RankMath SEO title")

1263
plugins/wordpress/wp_cli.py Normal file

File diff suppressed because it is too large Load Diff