Skip to main content
Glama

Filter Topics

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,
          };
        }
      },
    );
Behavior4/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 effectively describes key behavioral traits: the query language syntax, permission-aware results, and pagination context (implied through parameters). However, it doesn't mention rate limits, error conditions, or authentication requirements, leaving some gaps.

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, making it information-rich but somewhat overwhelming. It's front-loaded with the core purpose, but the long list of query options could be better structured. Every sentence earns its place, but readability could be improved.

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?

Given the tool's complexity (rich query language) and lack of annotations/output schema, the description does a good job explaining behavior and parameters. It covers the filter syntax comprehensively and mentions permission-awareness. However, it doesn't describe the return format or error handling, which would be helpful for a tool with 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%, so the schema already documents all three parameters thoroughly. The description adds value by explaining the 'filter' parameter's query language in detail, but doesn't provide additional meaning for 'page' or 'per_page' beyond what the schema states. 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: 'Filter topics with a concise query language.' It specifies the exact resource (topics) and action (filtering), and distinguishes itself from siblings like 'discourse_search' by focusing on structured query-based filtering rather than general search.

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 by detailing its query language capabilities, but it doesn't explicitly mention when not to use it or name specific alternatives. It implies usage for structured filtering of topics, which differentiates it from general search tools in the sibling list.

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

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