'object', 'properties' => $props]; if ($required) $schema['required'] = $required; return ['name' => $name, 'description' => $desc, 'scope' => $scope, 'handler' => 'AskTamMCP\\Extensions\\Batch5::' . $method, 'inputSchema' => $schema]; }; $registry->register($def('set_image_alt', 'Set (or clear) the alt text of a media-library image attachment.', ['attachment_id' => ['type' => 'integer'], 'alt' => ['type' => 'string']], ['attachment_id', 'alt'], 'write', 'setAlt')); $registry->register($def('bulk_set_alt_from_title', 'Backfill alt text for image attachments from their title (or a humanized filename). By default only fills images that have no alt; set overwrite=true to replace existing alts.', ['per_page' => ['type' => 'integer', 'default' => 100], 'overwrite' => ['type' => 'boolean', 'default' => false]], [], 'write', 'bulkAlt')); $registry->register($def('find_broken_internal_links', 'Scan published posts/pages for internal links whose target slug does not resolve to a post/page (candidate broken links). Heuristic: term/archive URLs may appear as false positives.', ['post_types' => ['type' => 'array', 'items' => ['type' => 'string'], 'default' => ['post', 'page']], 'per_page' => ['type' => 'integer', 'default' => 100]], [], 'read', 'brokenLinks')); $registry->register($def('list_large_images', 'List media-library images whose file size exceeds a threshold (KB), largest first.', ['min_kb' => ['type' => 'integer', 'default' => 500], 'per_page' => ['type' => 'integer', 'default' => 200]], [], 'read', 'largeImages')); $registry->register($def('duplicate_title_finder', 'Find published posts/pages that share an identical title.', ['post_types' => ['type' => 'array', 'items' => ['type' => 'string'], 'default' => ['post', 'page']], 'per_page' => ['type' => 'integer', 'default' => 500]], [], 'read', 'dupTitles')); } public static function setAlt(array $args): array { $id = (int) ($args['attachment_id'] ?? 0); $post = $id ? get_post($id) : null; if (!$post || $post->post_type !== 'attachment' || strpos((string) get_post_mime_type($id), 'image/') !== 0) { throw new \RuntimeException("Not an image attachment: {$id}"); } $alt = sanitize_text_field((string) ($args['alt'] ?? '')); update_post_meta($id, '_wp_attachment_image_alt', $alt); return ['success' => true, 'attachment_id' => $id, 'alt' => $alt]; } private static function humanizeFilename(int $id): string { $file = get_attached_file($id); if (!$file) return ''; $name = pathinfo($file, PATHINFO_FILENAME); $name = preg_replace('/[-_]+/', ' ', $name); $name = preg_replace('/\b\d{2,4}x\d{2,4}\b/', '', $name); // drop size suffixes return ucwords(trim(preg_replace('/\s+/', ' ', $name))); } public static function bulkAlt(array $args): array { $per = (int) ($args['per_page'] ?? 100); $overwrite = !empty($args['overwrite']); $atts = get_posts(['post_type' => 'attachment', 'post_mime_type' => 'image', 'post_status' => 'inherit', 'posts_per_page' => $per]); $changed = []; foreach ($atts as $a) { $cur = trim((string) get_post_meta($a->ID, '_wp_attachment_image_alt', true)); if ($cur !== '' && !$overwrite) continue; $alt = trim((string) $a->post_title); if ($alt === '') $alt = self::humanizeFilename($a->ID); if ($alt === '') continue; update_post_meta($a->ID, '_wp_attachment_image_alt', sanitize_text_field($alt)); $changed[] = ['id' => $a->ID, 'alt' => $alt]; } return ['scanned' => count($atts), 'updated' => count($changed), 'overwrite' => $overwrite, 'samples' => array_slice($changed, 0, 20)]; } public static function brokenLinks(array $args): array { $types = $args['post_types'] ?? ['post', 'page']; $per = (int) ($args['per_page'] ?? 100); $home = home_url(); $host = parse_url($home, PHP_URL_HOST); $posts = get_posts(['post_type' => $types, 'post_status' => 'publish', 'posts_per_page' => $per, 'fields' => 'ids']); $issues = []; if (!class_exists('DOMDocument')) return ['note' => 'DOMDocument unavailable', 'issues' => []]; foreach ($posts as $pid) { $pid = (int) $pid; $html = (string) get_post_field('post_content', $pid); if (trim($html) === '') continue; $dom = new \DOMDocument(); libxml_use_internal_errors(true); $dom->loadHTML('' . $html); libxml_clear_errors(); foreach ($dom->getElementsByTagName('a') as $a) { $href = trim($a->getAttribute('href')); if ($href === '' || preg_match('#^(mailto:|tel:|#|javascript:)#i', $href)) continue; $h = parse_url($href, PHP_URL_HOST); if ($h && $h !== $host) continue; // external $pathUrl = $h ? $href : rtrim($home, '/') . '/' . ltrim($href, '/'); $resolved = url_to_postid($pathUrl); if (!$resolved) { $issues[] = ['post_id' => $pid, 'href' => $href, 'anchor' => trim($a->textContent)]; } } } return ['scanned' => count($posts), 'candidate_broken' => $issues, 'count' => count($issues), 'note' => 'Heuristic: links to category/tag/author archives or custom routes can show as false positives.']; } public static function largeImages(array $args): array { $minKb = max(1, (int) ($args['min_kb'] ?? 500)); $per = (int) ($args['per_page'] ?? 200); $atts = get_posts(['post_type' => 'attachment', 'post_mime_type' => 'image', 'post_status' => 'inherit', 'posts_per_page' => $per]); $big = []; foreach ($atts as $a) { $file = get_attached_file($a->ID); if (!$file || !file_exists($file)) continue; $kb = (int) round(filesize($file) / 1024); if ($kb >= $minKb) { $big[] = ['id' => $a->ID, 'title' => $a->post_title, 'kb' => $kb, 'url' => wp_get_attachment_url($a->ID)]; } } usort($big, fn($x, $y) => $y['kb'] <=> $x['kb']); return ['scanned' => count($atts), 'threshold_kb' => $minKb, 'large_images' => $big, 'count' => count($big)]; } public static function dupTitles(array $args): array { $types = $args['post_types'] ?? ['post', 'page']; $per = (int) ($args['per_page'] ?? 500); $posts = get_posts(['post_type' => $types, 'post_status' => 'publish', 'posts_per_page' => $per]); $byTitle = []; foreach ($posts as $p) { $key = mb_strtolower(trim($p->post_title)); if ($key === '') continue; $byTitle[$key][] = ['id' => $p->ID, 'type' => $p->post_type]; } $dups = []; foreach ($byTitle as $title => $group) { if (count($group) > 1) $dups[] = ['title' => $title, 'posts' => $group, 'count' => count($group)]; } return ['scanned' => count($posts), 'duplicates' => $dups, 'duplicate_sets' => count($dups)]; } }