wp_list_posts
Retrieve and filter WordPress posts using search terms, status, categories, or tags to manage and organize site content efficiently.
Instructions
Lists posts from a WordPress site with comprehensive filtering options. Supports search, status filtering, and category/tag filtering with enhanced metadata display.
Usage Examples:
• Basic listing: wp_list_posts
• Search posts: wp_list_posts --search="AI trends"
• Filter by status: wp_list_posts --status="draft"
• Category filtering: wp_list_posts --categories=[1,2,3]
• Paginated results: wp_list_posts --per_page=20 --page=2
• Combined filters: wp_list_posts --search="WordPress" --status="publish" --per_page=10
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| site | No | The ID of the WordPress site to target (from mcp-wordpress.config.json). Required if multiple sites are configured. | |
| per_page | No | Number of items to return per page (max 100). | |
| search | No | Limit results to those matching a search term. | |
| status | No | Filter by post status. | |
| categories | No | Limit results to posts in specific category IDs. | |
| tags | No | Limit results to posts with specific tag IDs. |
Implementation Reference
- src/tools/posts/PostHandlers.ts:20-220 (handler)Core handler function that validates input parameters, sanitizes data, fetches posts using WordPressClient.getPosts(), enriches with author/category/tag info, handles streaming for large sets, and formats a rich markdown response with metadata and pagination tips.export async function handleListPosts( client: WordPressClient, params: PostQueryParams, ): Promise<WordPressPost[] | string> { try { // Handle null/undefined parameters if (!params || typeof params !== "object") { throw ErrorHandlers.validationError("params", params, "valid object"); } // Enhanced input validation and sanitization const paginationValidated = validatePaginationParams(params); const sanitizedParams = { ...params, ...paginationValidated, }; // Validate and sanitize search term if (sanitizedParams.search) { sanitizedParams.search = sanitizedParams.search.trim(); if (sanitizedParams.search.length === 0) { delete sanitizedParams.search; } } // Validate category and tag IDs if provided if (sanitizedParams.categories) { sanitizedParams.categories = sanitizedParams.categories.map((id) => validateId(id, "category ID")); } if (sanitizedParams.tags) { sanitizedParams.tags = sanitizedParams.tags.map((id) => validateId(id, "tag ID")); } // Validate and normalize status parameter to array (WordPress REST API expects array) if (sanitizedParams.status) { const validStatuses = ["publish", "future", "draft", "pending", "private"]; const statusesToCheck = Array.isArray(sanitizedParams.status) ? sanitizedParams.status : [sanitizedParams.status]; for (const statusToCheck of statusesToCheck) { if (!validStatuses.includes(statusToCheck)) { throw ErrorHandlers.validationError("status", statusToCheck, "one of: " + validStatuses.join(", ")); } } // Normalize to array format as expected by WordPress REST API sanitizedParams.status = statusesToCheck as PostStatus[]; } // Performance optimization: set reasonable defaults if (!sanitizedParams.per_page) { sanitizedParams.per_page = 10; // Default to 10 posts for better performance } const posts = await client.getPosts(sanitizedParams); if (posts.length === 0) { const searchInfo = sanitizedParams.search ? ` matching "${sanitizedParams.search}"` : ""; const statusInfo = sanitizedParams.status ? ` with status "${sanitizedParams.status}"` : ""; return `No posts found${searchInfo}${statusInfo}. Try adjusting your search criteria or check if posts exist.`; } // Use streaming for large result sets (>50 posts) if (posts.length > 50) { const streamResults: StreamingResult<unknown>[] = []; for await (const result of WordPressDataStreamer.streamPosts(posts, { includeAuthor: true, includeCategories: true, includeTags: true, batchSize: 20, })) { streamResults.push(result); } return StreamingUtils.formatStreamingResponse(streamResults, "posts"); } // Add comprehensive site context information const siteUrl = client.getSiteUrl ? client.getSiteUrl() : "Unknown site"; const totalPosts = posts.length; const statusCounts = posts.reduce( (acc, p) => { acc[p.status] = (acc[p.status] || 0) + 1; return acc; }, {} as Record<string, number>, ); // Enhanced metadata const metadata = [ `📊 **Posts Summary**: ${totalPosts} total`, `📝 **Status Breakdown**: ${Object.entries(statusCounts) .map(([status, count]) => `${status}: ${count}`) .join(", ")}`, `🌐 **Source**: ${siteUrl}`, `📅 **Retrieved**: ${new Date().toLocaleString()}`, ...(params.search ? [`🔍 **Search Term**: "${params.search}"`] : []), ...(params.categories ? [`📁 **Categories**: ${params.categories.join(", ")}`] : []), ...(params.tags ? [`🏷️ **Tags**: ${params.tags.join(", ")}`] : []), ]; // Fetch additional metadata for enhanced responses const authorIds = [...new Set(posts.map((p) => p.author).filter(Boolean))]; const categoryIds = [...new Set(posts.flatMap((p) => p.categories || []))]; const tagIds = [...new Set(posts.flatMap((p) => p.tags || []))]; // Fetch authors, categories, and tags in parallel for better performance const [authors, categories, tags] = await Promise.all([ authorIds.length > 0 ? Promise.all( authorIds.map(async (id) => { try { const user = await client.getUser(id); return { id, name: user.name || user.username || `User ${id}` }; } catch { return { id, name: `User ${id}` }; } }), ) : [], categoryIds.length > 0 ? Promise.all( categoryIds.map(async (id) => { try { const category = await client.getCategory(id); return { id, name: category.name || `Category ${id}` }; } catch { return { id, name: `Category ${id}` }; } }), ) : [], tagIds.length > 0 ? Promise.all( tagIds.map(async (id) => { try { const tag = await client.getTag(id); return { id, name: tag.name || `Tag ${id}` }; } catch { return { id, name: `Tag ${id}` }; } }), ) : [], ]); // Create lookup maps for performance const authorMap = new Map(authors.map((a) => [a.id, a.name])); const categoryMap = new Map(categories.map((c) => [c.id, c.name])); const tagMap = new Map(tags.map((t) => [t.id, t.name])); const content = metadata.join("\n") + "\n\n" + posts .map((p) => { const date = new Date(p.date); const formattedDate = date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }); const excerpt = p.excerpt?.rendered ? sanitizeHtml(p.excerpt.rendered).substring(0, 80) + "..." : ""; // Enhanced metadata const authorName = authorMap.get(p.author) || `User ${p.author}`; const postCategories = (p.categories || []).map((id) => categoryMap.get(id) || `Category ${id}`); const postTags = (p.tags || []).map((id) => tagMap.get(id) || `Tag ${id}`); let postInfo = `- ID ${p.id}: **${p.title.rendered}** (${p.status})\n`; postInfo += ` 👤 Author: ${authorName}\n`; postInfo += ` 📅 Published: ${formattedDate}\n`; if (postCategories.length > 0) { postInfo += ` 📁 Categories: ${postCategories.join(", ")}\n`; } if (postTags.length > 0) { postInfo += ` 🏷️ Tags: ${postTags.join(", ")}\n`; } if (excerpt) { postInfo += ` 📝 Excerpt: ${excerpt}\n`; } postInfo += ` 🔗 Link: ${p.link}`; return postInfo; }) .join("\n\n"); // Add pagination guidance for large result sets let finalContent = content; if (posts.length >= (sanitizedParams.per_page || 10)) { finalContent += `\n\n📄 **Pagination Tip**: Use \`per_page\` parameter to control results (max 100). Current: ${ sanitizedParams.per_page || 10 }`; } return finalContent; } catch (_error) { throw new Error(`Failed to list posts: ${getErrorMessage(_error)}`); } }
- MCP tool schema definition for wp_list_posts including name, detailed description with usage examples, and inputSchema with properties for per_page, search, status, categories, tags.export const listPostsTool: MCPTool = { name: "wp_list_posts", description: "Lists posts from a WordPress site with comprehensive filtering options. Supports search, status filtering, and category/tag filtering with enhanced metadata display.\n\n" + "**Usage Examples:**\n" + "• Basic listing: `wp_list_posts`\n" + '• Search posts: `wp_list_posts --search="AI trends"`\n' + '• Filter by status: `wp_list_posts --status="draft"`\n' + "• Category filtering: `wp_list_posts --categories=[1,2,3]`\n" + "• Paginated results: `wp_list_posts --per_page=20 --page=2`\n" + '• Combined filters: `wp_list_posts --search="WordPress" --status="publish" --per_page=10`', inputSchema: { type: "object", properties: { per_page: { type: "number", description: "Number of items to return per page (max 100).", }, search: { type: "string", description: "Limit results to those matching a search term.", }, status: { type: "string", description: "Filter by post status.", enum: ["publish", "future", "draft", "pending", "private"], }, categories: { type: "array", items: { type: "number" }, description: "Limit results to posts in specific category IDs.", }, tags: { type: "array", items: { type: "number" }, description: "Limit results to posts with specific tag IDs.", }, }, }, };
- src/tools/posts/index.ts:73-107 (registration)PostTools.getTools() maps tool definitions to include bound handlers via getHandlerForTool switch which binds wp_list_posts to this.handleListPosts.public getTools(): unknown[] { return postToolDefinitions.map((toolDef) => ({ ...toolDef, handler: this.getHandlerForTool(toolDef.name), })); } /** * Maps tool names to their corresponding handler methods. * * This method provides the binding between tool definitions and their * implementations, ensuring proper context and error handling. * * @param toolName - The name of the tool to get a handler for * @returns The bound handler method for the specified tool * @private */ private getHandlerForTool(toolName: string) { switch (toolName) { case "wp_list_posts": return this.handleListPosts.bind(this); case "wp_get_post": return this.handleGetPost.bind(this); case "wp_create_post": return this.handleCreatePost.bind(this); case "wp_update_post": return this.handleUpdatePost.bind(this); case "wp_delete_post": return this.handleDeletePost.bind(this); case "wp_get_post_revisions": return this.handleGetPostRevisions.bind(this); default: throw new Error(`Unknown tool: ${toolName}`); } }
- src/tools/posts/index.ts:118-149 (helper)Wrapper handler in PostTools class that extracts and normalizes MCP input parameters before delegating to core handleListPosts from PostHandlers.public async handleListPosts( client: WordPressClient, params: PostQueryParams | Record<string, unknown>, ): Promise<WordPressPost[] | string> { // Handle null/undefined params if (!params) { params = {}; } // Extract only the relevant query parameters, excluding MCP-specific fields const queryParams: PostQueryParams = {}; if (params.page !== undefined) queryParams.page = params.page as number; if (params.per_page !== undefined) queryParams.per_page = params.per_page as number; if (params.search !== undefined) queryParams.search = params.search as string; if (params.orderby !== undefined) queryParams.orderby = params.orderby as string; if (params.order !== undefined) queryParams.order = params.order as "asc" | "desc"; if (params.status !== undefined) { // Handle both string and array forms const statusValue = params.status; if (Array.isArray(statusValue)) { queryParams.status = statusValue as PostStatus[]; } else { queryParams.status = [statusValue as PostStatus]; } } if (params.categories !== undefined) queryParams.categories = params.categories as number[]; if (params.tags !== undefined) queryParams.tags = params.tags as number[]; if (params.offset !== undefined) queryParams.offset = params.offset as number; return handleListPosts(client, queryParams); }