Skip to main content
Glama
ahnmichael

GitLab Forum MCP

by ahnmichael

Filter Topics

discourse_filter_topics

Filter GitLab forum topics using a query language to find discussions by category, tags, status, dates, metrics, and text. Returns permission-aware results for troubleshooting CI/CD issues and GitLab features.

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 (1-based)
per_pageNoItems per page (max 50)

Implementation Reference

  • The async handler function that implements the core logic of the 'discourse_filter_topics' tool. It makes an API request to the Discourse site using the provided filter query, pagination parameters, processes the topic list, formats a human-readable response with links, and appends a structured JSON footer for easy parsing.
    async ({ filter, page = 1, 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 parameters for the tool: 'filter' (required string query), 'page' (optional integer >=1), 'per_page' (optional integer 1-50). Includes descriptions for each parameter.
    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(1)
          .optional()
          .describe("Page number (1-based)"),
        per_page: z
          .number()
          .int()
          .min(1)
          .max(50)
          .optional()
          .describe("Items per page (max 50)"),
      })
      .strict();
  • The server.registerTool call that registers the 'discourse_filter_topics' tool, specifying the name, title, description, inputSchema, and the handler function. This is invoked by the exported registerFilterTopics function.
    server.registerTool(
      "discourse_filter_topics",
      {
        title: "Filter Topics",
        description,
        inputSchema: schema.shape,
      },
      async ({ filter, page = 1, 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 registerFilterTopics(server, ctx, { allowWrites: false }) within registerAllTools, which triggers the tool 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/ahnmichael/gitlab-forum-mcp'

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