Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
siteNoThe ID of the WordPress site to target (from mcp-wordpress.config.json). Required if multiple sites are configured.
per_pageNoNumber of items to return per page (max 100).
searchNoLimit results to those matching a search term.
statusNoFilter by post status.
categoriesNoLimit results to posts in specific category IDs.
tagsNoLimit results to posts with specific tag IDs.

Implementation Reference

  • 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.",
          },
        },
      },
    };
  • 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}`);
      }
    }
  • 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);
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It does mention 'enhanced metadata display' which adds useful context about what information is returned. However, it doesn't disclose important behavioral aspects like pagination behavior (beyond parameter examples), rate limits, authentication requirements, or error handling for a tool with multiple filtering options.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with a clear purpose statement followed by practical usage examples. Each example earns its place by demonstrating different parameter combinations. However, the examples section is quite lengthy and could potentially be more concise while still being helpful.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only list tool with comprehensive filtering (6 parameters) and no output schema, the description provides adequate but not complete context. The examples help illustrate usage, but without annotations or output schema, it doesn't fully describe what the return format looks like (structure of posts, what 'enhanced metadata' includes), error conditions, or performance characteristics.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema description coverage is 100%, so the schema already documents all 6 parameters thoroughly. The description's usage examples provide practical illustrations of parameter combinations but don't add significant semantic meaning beyond what's in the schema descriptions. This meets the baseline for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with a specific verb ('Lists posts') and resource ('from a WordPress site'), and distinguishes it from siblings by mentioning 'comprehensive filtering options' and 'enhanced metadata display' - differentiating it from simpler list tools like wp_list_pages or wp_list_users.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context for when to use this tool through multiple usage examples showing different filtering scenarios. However, it doesn't explicitly state when NOT to use it or mention specific alternatives among the many sibling tools, such as wp_search_site for broader searches.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/docdyhr/mcp-wordpress'

If you have feedback or need assistance with the MCP directory API, please join our Discord server