Skip to main content
Glama

discourse_filter_topics

Filter Discourse forum topics using a query language for categories, tags, status, dates, metrics, and text search with permission-aware results.

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

  • The asynchronous handler function that implements the tool logic: constructs API params from input (filter, page, per_page), fetches from Discourse /filter.json endpoint, extracts topics, formats into a numbered list with full URLs, appends a JSON object with structured results (including next_url if available), and handles errors by returning error content.
    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 schema defining the input shape: filter (required non-empty string), page (optional non-negative integer, default 0), per_page (optional integer between 1 and 50). Includes descriptions for parameters.
    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();
  • The server.registerTool invocation that registers the tool named 'discourse_filter_topics' with title 'Filter Topics', multi-line description of query syntax, the defined inputSchema, and the handler function.
    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,
          };
        }
      },
    );

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

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