Skip to main content
Glama
discourse

Discourse MCP

Official
by discourse

Filter Topics

discourse_filter_topics

Filter Discourse forum topics using a query language to find specific discussions by category, tags, status, dates, engagement metrics, and text content.

Instructions

Filter topics with a concise query language: use key:value tokens separated by spaces; category/categories for categories (comma = OR, '=category' = without subcats, '-' prefix = exclude), tag/tags (comma = OR, '+' = AND) and tag_group; status:(open|closed|archived|listed|unlisted|public) and personal in:(bookmarked|watching|tracking|muted|pinned); dates: created/activity/latest-post-(before|after) with YYYY-MM-DD or N (days); numeric: likes[-op]-(min|max), posts-(min|max), posters-(min|max), views-(min|max); order: activity|created|latest-post|likes|likes-op|posters|title|views|category with optional -asc; free text terms are matched full-text. Results are permission-aware.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
filterYesFilter query, e.g. 'category:support status:open created-after:30 order:activity'
pageNoPage number (0-based, default: 0)
per_pageNoItems per page (max 50)

Implementation Reference

  • Handler function that queries the Discourse filter endpoint, processes the topic list, formats a human-readable response with links, and includes structured JSON data.
    async ({ filter, page = 0, per_page }, _extra: any) => {
      try {
        const { base, client } = ctx.siteState.ensureSelectedSite();
        const params = new URLSearchParams();
        params.set("q", filter);
        params.set("page", String(page));
        if (per_page) params.set("per_page", String(per_page));
    
        const data = (await client.get(
          `/filter.json?${params.toString()}`,
        )) as any;
        const list = data?.topic_list ?? data;
        const topics: any[] = Array.isArray(list?.topics) ? list.topics : [];
        const perPage = per_page ?? list?.per_page ?? undefined;
        const moreUrl: string | undefined =
          list?.more_topics_url || list?.more_url || undefined;
    
        const items = topics.map((t) => ({
          id: t.id,
          title: t.title || t.fancy_title || `Topic ${t.id}`,
          slug: t.slug || String(t.id),
        }));
    
        const lines: string[] = [];
        lines.push(`Filter: "${filter}" — Page ${page}`);
        if (items.length === 0) {
          lines.push("No topics matched.");
        } else {
          let i = 1;
          for (const it of items) {
            const url = `${base}/t/${it.slug}/${it.id}`;
            lines.push(`${i}. ${it.title} – ${url}`);
            i++;
          }
        }
    
        // Build a compact JSON footer for structured extraction
        const jsonFooter: any = {
          page,
          per_page: perPage,
          results: items.map((it) => ({
            id: it.id,
            title: it.title,
            url: `${base}/t/${it.slug}/${it.id}`,
          })),
        };
        if (moreUrl) {
          const abs = moreUrl.startsWith("http")
            ? moreUrl
            : `${base}${moreUrl.startsWith("/") ? "" : "/"}${moreUrl}`;
          jsonFooter.next_url = abs;
        }
    
        const text =
          lines.join("\n") +
          "\n\n```json\n" +
          JSON.stringify(jsonFooter) +
          "\n```\n";
        return { content: [{ type: "text", text }] };
      } catch (e: any) {
        return {
          content: [
            {
              type: "text",
              text: `Failed to filter topics: ${e?.message || String(e)}`,
            },
          ],
          isError: true,
        };
      }
  • Zod input schema for the tool parameters: filter (required string), page and per_page (optional numbers with constraints).
    const schema = z
      .object({
        filter: z
          .string()
          .min(1)
          .describe(
            "Filter query, e.g. 'category:support status:open created-after:30 order:activity'",
          ),
        page: z
          .number()
          .int()
          .min(0)
          .optional()
          .describe("Page number (0-based, default: 0)"),
        per_page: z
          .number()
          .int()
          .min(1)
          .max(50)
          .optional()
          .describe("Items per page (max 50)"),
      })
      .strict();
  • Registration of the 'discourse_filter_topics' tool with server.registerTool, including title, description, inputSchema, and handler.
    server.registerTool(
      "discourse_filter_topics",
      {
        title: "Filter Topics",
        description,
        inputSchema: schema.shape,
      },
      async ({ filter, page = 0, per_page }, _extra: any) => {
        try {
          const { base, client } = ctx.siteState.ensureSelectedSite();
          const params = new URLSearchParams();
          params.set("q", filter);
          params.set("page", String(page));
          if (per_page) params.set("per_page", String(per_page));
    
          const data = (await client.get(
            `/filter.json?${params.toString()}`,
          )) as any;
          const list = data?.topic_list ?? data;
          const topics: any[] = Array.isArray(list?.topics) ? list.topics : [];
          const perPage = per_page ?? list?.per_page ?? undefined;
          const moreUrl: string | undefined =
            list?.more_topics_url || list?.more_url || undefined;
    
          const items = topics.map((t) => ({
            id: t.id,
            title: t.title || t.fancy_title || `Topic ${t.id}`,
            slug: t.slug || String(t.id),
          }));
    
          const lines: string[] = [];
          lines.push(`Filter: "${filter}" — Page ${page}`);
          if (items.length === 0) {
            lines.push("No topics matched.");
          } else {
            let i = 1;
            for (const it of items) {
              const url = `${base}/t/${it.slug}/${it.id}`;
              lines.push(`${i}. ${it.title} – ${url}`);
              i++;
            }
          }
    
          // Build a compact JSON footer for structured extraction
          const jsonFooter: any = {
            page,
            per_page: perPage,
            results: items.map((it) => ({
              id: it.id,
              title: it.title,
              url: `${base}/t/${it.slug}/${it.id}`,
            })),
          };
          if (moreUrl) {
            const abs = moreUrl.startsWith("http")
              ? moreUrl
              : `${base}${moreUrl.startsWith("/") ? "" : "/"}${moreUrl}`;
            jsonFooter.next_url = abs;
          }
    
          const text =
            lines.join("\n") +
            "\n\n```json\n" +
            JSON.stringify(jsonFooter) +
            "\n```\n";
          return { content: [{ type: "text", text }] };
        } catch (e: any) {
          return {
            content: [
              {
                type: "text",
                text: `Failed to filter topics: ${e?.message || String(e)}`,
              },
            ],
            isError: true,
          };
        }
      },
    );
  • Invocation of the registerFilterTopics function to register the tool during overall tools registration.
    registerFilterTopics(server, ctx, { allowWrites: false });
Behavior4/5

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

With no annotations provided, the description carries full burden for behavioral disclosure. It effectively describes key behavioral traits: the query language syntax, permission-aware results (important for access control), and pagination behavior (implied through page/per_page parameters). It doesn't mention rate limits, error conditions, or authentication requirements, but provides substantial operational context.

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

Conciseness3/5

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

The description is densely packed with query syntax details but remains a single paragraph. While information-dense, it could benefit from better structure (e.g., bullet points for different filter types). Every sentence earns its place by explaining the query language, but the presentation could be more scannable for an AI agent.

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

Completeness4/5

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

For a filtering tool with 3 parameters (one complex), no annotations, and no output schema, the description provides substantial context about the query language and permission-aware results. It adequately covers the tool's purpose and usage, though it could benefit from explicit examples of complete queries and more detail about result format since there's no output schema.

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?

Schema description coverage is 100%, providing good documentation for all three parameters. The description adds value by explaining the complex 'filter' parameter syntax in detail (key:value tokens, operators, date formats, etc.), but doesn't add meaningful context for 'page' and 'per_page' beyond what the schema already states. Baseline 3 is appropriate given the 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 explicitly states 'Filter topics with a concise query language', providing a specific verb ('filter') and resource ('topics'). It clearly distinguishes this tool from siblings like 'discourse_search' by focusing on structured filtering rather than general search, and from 'discourse_list_categories/tags' by targeting topics specifically.

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 implies usage context through the detailed query language explanation, suggesting this tool is for structured filtering of topics. However, it doesn't explicitly state when to use this versus alternatives like 'discourse_search' or 'discourse_list_categories', nor does it provide exclusion criteria or prerequisites for use.

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/discourse/discourse-mcp'

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