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:
418
plugins/wordpress/handlers/media.py
Normal file
418
plugins/wordpress/handlers/media.py
Normal 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,
|
||||
)
|
||||
Reference in New Issue
Block a user