Skip to main content
Glama

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 });

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