discourse_list_user_posts
Retrieve user posts and replies from Discourse forums to analyze contributions or monitor activity. Supports pagination for accessing historical content.
Instructions
Get a list of user posts and replies from a Discourse instance, with the most recent first. Returns 30 posts per page by default. Use the page parameter to paginate (page 0 = offset 0, page 1 = offset 30, etc.).
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| username | Yes | ||
| page | No |
Implementation Reference
- The async handler function that fetches user posts and replies from Discourse using the user_actions API endpoint, paginates with offset, formats post details including topic title, date, URL, category, and excerpt, and returns formatted text content or error.async ({ username, page }, _extra: any) => { try { const { base, client } = ctx.siteState.ensureSelectedSite(); const offset = (page || 0) * 30; // The filter parameter 4,5 corresponds to posts and replies const data = (await client.get( `/user_actions.json?offset=${offset}&username=${encodeURIComponent(username)}&filter=4,5` )) as any; const userActions = data?.user_actions || []; if (userActions.length === 0) { return { content: [{ type: "text", text: page && page > 0 ? `No more posts found for @${username} at page ${page}.` : `No posts found for @${username}.` }] }; } const posts = userActions.map((action: any) => { const excerpt = action.excerpt || ""; const truncated = action.truncated ? "..." : ""; const date = action.created_at || ""; const topicTitle = action.title || ""; const topicSlug = action.slug || ""; const topicId = action.topic_id || ""; const postNumber = action.post_number || ""; const categoryId = action.category_id || ""; const postUrl = `${base}/t/${topicSlug}/${topicId}/${postNumber}`; return [ `**${topicTitle}**`, `Posted: ${date}`, `Topic: ${postUrl}`, categoryId ? `Category ID: ${categoryId}` : undefined, excerpt ? `\n${excerpt}${truncated}` : undefined, ].filter(Boolean).join("\n"); }); const totalShown = userActions.length; const pageInfo = page && page > 0 ? ` (page ${page})` : ""; const header = `Showing ${totalShown} posts for @${username}${pageInfo}:\n\n`; const footer = totalShown === 30 ? `\n\nTo see more posts, use page ${(page || 0) + 1}.` : ""; return { content: [{ type: "text", text: header + posts.join("\n\n---\n\n") + footer }] }; } catch (e: any) { return { content: [{ type: "text", text: `Failed to get posts for ${username}: ${e?.message || String(e)}` }], isError: true }; } } );
- Zod schema for tool input: required 'username' string and optional 'page' non-negative integer for pagination.const schema = z.object({ username: z.string().min(1), page: z.number().int().min(0).optional(), });
- src/tools/builtin/list_user_posts.ts:10-82 (registration)The server.registerTool call that defines and registers the 'discourse_list_user_posts' tool with its metadata, input schema, and handler.server.registerTool( "discourse_list_user_posts", { title: "List User Posts", description: "Get a list of user posts and replies from a Discourse instance, with the most recent first. Returns 30 posts per page by default. Use the page parameter to paginate (page 0 = offset 0, page 1 = offset 30, etc.).", inputSchema: schema.shape, }, async ({ username, page }, _extra: any) => { try { const { base, client } = ctx.siteState.ensureSelectedSite(); const offset = (page || 0) * 30; // The filter parameter 4,5 corresponds to posts and replies const data = (await client.get( `/user_actions.json?offset=${offset}&username=${encodeURIComponent(username)}&filter=4,5` )) as any; const userActions = data?.user_actions || []; if (userActions.length === 0) { return { content: [{ type: "text", text: page && page > 0 ? `No more posts found for @${username} at page ${page}.` : `No posts found for @${username}.` }] }; } const posts = userActions.map((action: any) => { const excerpt = action.excerpt || ""; const truncated = action.truncated ? "..." : ""; const date = action.created_at || ""; const topicTitle = action.title || ""; const topicSlug = action.slug || ""; const topicId = action.topic_id || ""; const postNumber = action.post_number || ""; const categoryId = action.category_id || ""; const postUrl = `${base}/t/${topicSlug}/${topicId}/${postNumber}`; return [ `**${topicTitle}**`, `Posted: ${date}`, `Topic: ${postUrl}`, categoryId ? `Category ID: ${categoryId}` : undefined, excerpt ? `\n${excerpt}${truncated}` : undefined, ].filter(Boolean).join("\n"); }); const totalShown = userActions.length; const pageInfo = page && page > 0 ? ` (page ${page})` : ""; const header = `Showing ${totalShown} posts for @${username}${pageInfo}:\n\n`; const footer = totalShown === 30 ? `\n\nTo see more posts, use page ${(page || 0) + 1}.` : ""; return { content: [{ type: "text", text: header + posts.join("\n\n---\n\n") + footer }] }; } catch (e: any) { return { content: [{ type: "text", text: `Failed to get posts for ${username}: ${e?.message || String(e)}` }], isError: true }; } } );
- src/tools/registry.ts:56-56 (registration)Top-level call to registerListUserPosts during MCP tools initialization in registerAllTools, with read-only permissions.registerListUserPosts(server, ctx, { allowWrites: false });
- Helper function exported as registerListUserPosts that encapsulates the schema, tool registration, and handler for discourse_list_user_posts.export const registerListUserPosts: RegisterFn = (server, ctx) => { const schema = z.object({ username: z.string().min(1), page: z.number().int().min(0).optional(), }); server.registerTool( "discourse_list_user_posts", { title: "List User Posts", description: "Get a list of user posts and replies from a Discourse instance, with the most recent first. Returns 30 posts per page by default. Use the page parameter to paginate (page 0 = offset 0, page 1 = offset 30, etc.).", inputSchema: schema.shape, }, async ({ username, page }, _extra: any) => { try { const { base, client } = ctx.siteState.ensureSelectedSite(); const offset = (page || 0) * 30; // The filter parameter 4,5 corresponds to posts and replies const data = (await client.get( `/user_actions.json?offset=${offset}&username=${encodeURIComponent(username)}&filter=4,5` )) as any; const userActions = data?.user_actions || []; if (userActions.length === 0) { return { content: [{ type: "text", text: page && page > 0 ? `No more posts found for @${username} at page ${page}.` : `No posts found for @${username}.` }] }; } const posts = userActions.map((action: any) => { const excerpt = action.excerpt || ""; const truncated = action.truncated ? "..." : ""; const date = action.created_at || ""; const topicTitle = action.title || ""; const topicSlug = action.slug || ""; const topicId = action.topic_id || ""; const postNumber = action.post_number || ""; const categoryId = action.category_id || ""; const postUrl = `${base}/t/${topicSlug}/${topicId}/${postNumber}`; return [ `**${topicTitle}**`, `Posted: ${date}`, `Topic: ${postUrl}`, categoryId ? `Category ID: ${categoryId}` : undefined, excerpt ? `\n${excerpt}${truncated}` : undefined, ].filter(Boolean).join("\n"); }); const totalShown = userActions.length; const pageInfo = page && page > 0 ? ` (page ${page})` : ""; const header = `Showing ${totalShown} posts for @${username}${pageInfo}:\n\n`; const footer = totalShown === 30 ? `\n\nTo see more posts, use page ${(page || 0) + 1}.` : ""; return { content: [{ type: "text", text: header + posts.join("\n\n---\n\n") + footer }] }; } catch (e: any) { return { content: [{ type: "text", text: `Failed to get posts for ${username}: ${e?.message || String(e)}` }], isError: true }; } } ); };