wp_seo_bulk_update_metadata
Update SEO metadata for multiple WordPress posts simultaneously with progress tracking and error handling.
Instructions
Update SEO metadata for multiple posts with progress tracking and error handling
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| site | No | Site identifier for multi-site setups | |
| postIds | Yes | Array of WordPress post IDs to update | |
| updates | Yes | Metadata fields to update for all posts | |
| dryRun | No | Perform a dry run without making actual changes |
Implementation Reference
- src/tools/seo/SEOHandlers.ts:82-105 (handler)The main MCP tool handler function for 'wp_seo_bulk_update_metadata'. It parses input arguments, creates parameters, and delegates execution to the SEOTools.bulkUpdateMetadata method.
/** * Handle bulk metadata update request */ export async function handleBulkUpdateMetadata( client: WordPressClient, args: Record<string, unknown>, ): Promise<unknown> { const logger = LoggerFactory.tool("wp_seo_bulk_update_metadata"); try { const seoTools = getSEOToolsInstance(); const params: SEOToolParams = { postIds: args.postIds as number[], updates: args.updates as Partial<SEOMetadata>, dryRun: args.dryRun as boolean, site: args.site as string, }; return await seoTools.bulkUpdateMetadata(params); } catch (error) { logger.error("Failed to bulk update metadata", { error, args }); throw error; } } - Defines the tool schema including name, description, and input validation schema for the wp_seo_bulk_update_metadata tool.
/** * Bulk update metadata for multiple posts */ export const bulkUpdateMetadataTool: Tool = { name: "wp_seo_bulk_update_metadata", description: "Update SEO metadata for multiple posts with progress tracking and error handling", inputSchema: { type: "object", properties: { postIds: { type: "array", items: { type: "number" }, description: "Array of WordPress post IDs to update", }, updates: { type: "object", properties: { title: { type: "string" }, description: { type: "string" }, focusKeyword: { type: "string" }, canonical: { type: "string" }, }, description: "Metadata fields to update for all posts", }, dryRun: { type: "boolean", description: "Perform a dry run without making actual changes", }, site: { type: "string", description: "Site identifier for multi-site setups", }, }, required: ["postIds", "updates"], }, }; - src/tools/seo/SEOTools.ts:378-399 (registration)Maps the tool name 'wp_seo_bulk_update_metadata' to its handler function handleBulkUpdateMetadata. Used by getTools() to register all SEO tools with handlers.
private getHandlerForTool(toolName: string): unknown { const handlers: Record<string, unknown> = { wp_seo_analyze_content: handleAnalyzeContent, wp_seo_generate_metadata: handleGenerateMetadata, wp_seo_bulk_update_metadata: handleBulkUpdateMetadata, wp_seo_generate_schema: handleGenerateSchema, wp_seo_validate_schema: handleValidateSchema, wp_seo_suggest_internal_links: handleSuggestInternalLinks, wp_seo_site_audit: handlePerformSiteAudit, wp_seo_track_serp: handleTrackSERPPositions, wp_seo_keyword_research: handleKeywordResearch, wp_seo_test_integration: handleTestSEOIntegration, wp_seo_get_live_data: handleGetLiveSEOData, }; return ( handlers[toolName] || (() => { throw new Error(`Unknown SEO tool: ${toolName}`); }) ); } - Core bulk processing logic delegated from SEOTools. Handles batching, individual post metadata updates, retries, progress reporting, and error collection.
async bulkUpdateMetadata(params: SEOToolParams, progressCallback?: ProgressCallback): Promise<BulkOperationResult> { const startTime = Date.now(); this.logger.info("Starting bulk metadata update", { postIds: params.postIds?.length, dryRun: params.dryRun, batchSize: this.config.batchSize, }); if (!params.postIds?.length) { throw new Error("No post IDs provided for bulk operation"); } const progress: BulkProgress = { total: params.postIds.length, processed: 0, completed: 0, failed: 0, skipped: 0, currentBatch: 0, totalBatches: Math.ceil(params.postIds.length / this.config.batchSize), avgProcessingTime: 0, }; const errors: BulkOperationError[] = []; const batches = this.createBatches(params.postIds, this.config.batchSize); // Process each batch for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) { const batch = batches[batchIndex]; progress.currentBatch = batchIndex + 1; this.logger.debug(`Processing batch ${progress.currentBatch}/${progress.totalBatches}`, { batchSize: batch.length, postIds: batch, }); // Process batch items await Promise.all( batch.map(async (postId) => { const itemStartTime = Date.now(); try { await this.processMetadataUpdate(postId, params); progress.completed++; // Update average processing time const processingTime = Date.now() - itemStartTime; progress.avgProcessingTime = (progress.avgProcessingTime * progress.processed + processingTime) / (progress.processed + 1); } catch (_error) { const bulkError: BulkOperationError = { postId, error: _error instanceof Error ? _error.message : String(_error), attempts: 1, retryable: this.isRetryableError(_error), }; // Attempt retries if (bulkError.retryable) { const retryResult = await this.retryOperation( () => this.processMetadataUpdate(postId, params), bulkError, ); if (retryResult.success) { progress.completed++; } else { progress.failed++; errors.push(retryResult.error); } } else { progress.failed++; errors.push(bulkError); } } progress.processed++; }), ); // Calculate ETA if (progress.avgProcessingTime > 0 && progress.processed < progress.total) { const remainingItems = progress.total - progress.processed; const etaMs = remainingItems * progress.avgProcessingTime; progress.eta = new Date(Date.now() + etaMs); } else if (progress.processed > 0 && progress.processed < progress.total) { // Fallback ETA calculation even with minimal processing time const remainingItems = progress.total - progress.processed; const averageTime = progress.avgProcessingTime || 100; // Fallback to 100ms const etaMs = remainingItems * averageTime; progress.eta = new Date(Date.now() + etaMs); } // Call progress callback if (progressCallback && this.config.enableProgress) { progressCallback(progress); } // Small delay between batches to avoid overwhelming the server if (batchIndex < batches.length - 1) { await this.delay(100); } } const result: BulkOperationResult = { total: params.postIds.length, success: progress.completed, failed: progress.failed, skipped: progress.skipped, errors: errors.map((e) => ({ postId: e.postId, error: e.error })), processingTime: Date.now() - startTime, dryRun: params.dryRun || false, }; this.logger.info("Bulk metadata update completed", { ...result, successRate: ((result.success / result.total) * 100).toFixed(1) + "%", }); return result; }