Get Discussions
get_discussionsRetrieve discussion board content for a course, including forums, topics, and posts. Use courseId to list all forums, or add forumId and topicId to drill into specific topics and posts.
Instructions
Fetch discussion board content for a course including forums, topics, and posts. Use this when the user asks about discussion boards, forum posts, class discussions, or wants to see what's been posted. Provide just courseId to list all forums and their topics. Add forumId to get topics and posts for a specific forum. Add both forumId and topicId to get all posts in a specific discussion topic.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| courseId | Yes | Course ID to get discussion boards for. | |
| forumId | No | Specific forum ID to get topics and posts for. If omitted, returns all forums. | |
| topicId | No | Specific topic ID to get posts for. Requires forumId. |
Implementation Reference
- src/tools/get-discussions.ts:66-332 (handler)The main handler registration function (registerGetDiscussions) and all helper functions (getForumsOverview, getForumDetail, getTopicPosts, formatPosts) that execute the get_discussions tool logic. It fetches discussion board data from Brightspace D2L API — forums, topics, and posts — with three levels of detail based on provided parameters.
export function registerGetDiscussions( server: McpServer, apiClient: D2LApiClient ): void { server.registerTool( "get_discussions", { title: "Get Discussions", description: "Fetch discussion board content for a course including forums, topics, and posts. Use this when the user asks about discussion boards, forum posts, class discussions, or wants to see what's been posted. Provide just courseId to list all forums and their topics. Add forumId to get topics and posts for a specific forum. Add both forumId and topicId to get all posts in a specific discussion topic.", inputSchema: GetDiscussionsSchema, }, async (args: any) => { try { log("DEBUG", "get_discussions tool called", { args }); const { courseId, forumId, topicId } = GetDiscussionsSchema.parse(args); // topicId requires forumId if (topicId !== undefined && forumId === undefined) { return errorResponse( "topicId requires forumId. Provide both forumId and topicId to get posts for a specific topic." ); } // Specific topic — get posts if (forumId !== undefined && topicId !== undefined) { return await getTopicPosts(apiClient, courseId, forumId, topicId); } // Specific forum — get its topics + posts if (forumId !== undefined) { return await getForumDetail(apiClient, courseId, forumId); } // All forums overview return await getForumsOverview(apiClient, courseId); } catch (error) { return sanitizeError(error); } } ); } /** * Get all forums for a course with their topics (no posts). */ async function getForumsOverview( apiClient: D2LApiClient, courseId: number ): Promise<any> { const forumsPath = apiClient.le(courseId, "/discussions/forums/"); const forums = await apiClient.get<D2LForum[]>(forumsPath, { ttl: DEFAULT_CACHE_TTLS.courseContent, }); const result = []; for (const forum of forums) { // Fetch topics for each forum let topics: D2LTopic[] = []; try { const topicsPath = apiClient.le( courseId, `/discussions/forums/${forum.ForumId}/topics/` ); topics = await apiClient.get<D2LTopic[]>(topicsPath, { ttl: DEFAULT_CACHE_TTLS.courseContent, }); } catch (error: any) { if (error?.status === 403) { log("DEBUG", `No access to topics for forum ${forum.ForumId}, skipping`); } else { log("DEBUG", `Failed to fetch topics for forum ${forum.ForumId}`, error); } } result.push({ forumId: forum.ForumId, name: forum.Name, description: forum.Description?.Text ?? null, isLocked: forum.IsLocked, isHidden: forum.IsHidden, topics: topics.map((t) => ({ topicId: t.TopicId, forumId: t.ForumId, name: t.Name, description: t.Description?.Text ?? null, dueDate: t.DueDate, isLocked: t.IsLocked, isHidden: t.IsHidden, mustPostToParticipate: t.MustPostToParticipate, scoreOutOf: t.ScoreOutOf, })), }); } log( "INFO", `get_discussions: Retrieved ${forums.length} forums for course ${courseId}` ); return toolResponse({ courseId, forumCount: result.length, forums: result, }); } /** * Get a specific forum with its topics and posts. */ async function getForumDetail( apiClient: D2LApiClient, courseId: number, forumId: number ): Promise<any> { // Fetch forum info const forumPath = apiClient.le( courseId, `/discussions/forums/${forumId}` ); const forum = await apiClient.get<D2LForum>(forumPath, { ttl: DEFAULT_CACHE_TTLS.courseContent, }); // Fetch topics const topicsPath = apiClient.le( courseId, `/discussions/forums/${forumId}/topics/` ); const topics = await apiClient.get<D2LTopic[]>(topicsPath, { ttl: DEFAULT_CACHE_TTLS.courseContent, }); // Fetch posts for each topic const topicsWithPosts = []; for (const topic of topics) { let posts: D2LPost[] = []; try { const postsPath = apiClient.le( courseId, `/discussions/forums/${forumId}/topics/${topic.TopicId}/posts/` ); posts = await apiClient.get<D2LPost[]>(postsPath, { ttl: DEFAULT_CACHE_TTLS.announcements, }); } catch (error: any) { if (error?.status === 403) { log("DEBUG", `No access to posts for topic ${topic.TopicId}, skipping`); } else { log("DEBUG", `Failed to fetch posts for topic ${topic.TopicId}`, error); } } topicsWithPosts.push({ topicId: topic.TopicId, name: topic.Name, description: topic.Description?.Html ? convertHtmlToMarkdown(topic.Description.Html).markdown : topic.Description?.Text ?? null, dueDate: topic.DueDate, isLocked: topic.IsLocked, mustPostToParticipate: topic.MustPostToParticipate, scoreOutOf: topic.ScoreOutOf, postCount: posts.length, posts: formatPosts(posts), }); } log( "INFO", `get_discussions: Retrieved forum ${forumId} with ${topics.length} topics for course ${courseId}` ); return toolResponse({ courseId, forum: { forumId: forum.ForumId, name: forum.Name, description: forum.Description?.Text ?? null, isLocked: forum.IsLocked, isHidden: forum.IsHidden, }, topicCount: topicsWithPosts.length, topics: topicsWithPosts, }); } /** * Get all posts for a specific topic. */ async function getTopicPosts( apiClient: D2LApiClient, courseId: number, forumId: number, topicId: number ): Promise<any> { // Fetch topic info const topicPath = apiClient.le( courseId, `/discussions/forums/${forumId}/topics/${topicId}` ); const topic = await apiClient.get<D2LTopic>(topicPath, { ttl: DEFAULT_CACHE_TTLS.courseContent, }); // Fetch posts const postsPath = apiClient.le( courseId, `/discussions/forums/${forumId}/topics/${topicId}/posts/` ); const posts = await apiClient.get<D2LPost[]>(postsPath, { ttl: DEFAULT_CACHE_TTLS.announcements, }); log( "INFO", `get_discussions: Retrieved ${posts.length} posts for topic ${topicId} in forum ${forumId}` ); return toolResponse({ courseId, forumId, topic: { topicId: topic.TopicId, name: topic.Name, description: topic.Description?.Html ? convertHtmlToMarkdown(topic.Description.Html).markdown : topic.Description?.Text ?? null, dueDate: topic.DueDate, isLocked: topic.IsLocked, mustPostToParticipate: topic.MustPostToParticipate, scoreOutOf: topic.ScoreOutOf, }, postCount: posts.length, posts: formatPosts(posts), }); } /** * Format posts into a clean thread structure. */ function formatPosts(posts: D2LPost[]): any[] { return posts .filter((p) => !p.IsDeleted) .map((p) => ({ postId: p.PostId, threadId: p.ThreadId, parentPostId: p.ParentPostId, subject: p.Subject, message: p.Message?.Html ? convertHtmlToMarkdown(p.Message.Html).markdown : p.Message?.Text ?? "", author: p.IsAnonymous ? "Anonymous" : p.PostingUserDisplayName, datePosted: p.DatePosted, lastEditedDate: p.LastEditedDate, replyCount: p.ReplyPostIds?.length ?? 0, wordCount: p.WordCount, attachmentCount: p.AttachmentCount, isRead: p.IsRead, })) .sort( (a, b) => new Date(a.datePosted).getTime() - new Date(b.datePosted).getTime() ); } - src/tools/schemas.ts:76-83 (schema)Zod schema (GetDiscussionsSchema) defining input validation: courseId (required), forumId (optional), topicId (optional, requires forumId).
export const GetDiscussionsSchema = z.object({ courseId: z.coerce.number().int().positive() .describe("Course ID to get discussion boards for."), forumId: z.coerce.number().int().positive().optional() .describe("Specific forum ID to get topics and posts for. If omitted, returns all forums."), topicId: z.coerce.number().int().positive().optional() .describe("Specific topic ID to get posts for. Requires forumId."), }); - src/index.ts:31-32 (registration)Import of registerGetDiscussions from tools/index.ts.
registerGetDiscussions, } from "./tools/index.js"; - src/index.ts:189-189 (registration)Registration call: registerGetDiscussions(server, apiClient) invoked during server startup.
registerGetDiscussions(server, apiClient); - src/tools/index.ts:18-22 (helper)Barrel re-export of registerGetDiscussions from the tools module.
export { registerGetDiscussions } from "./get-discussions.js"; // Re-export shared helpers and schemas for convenience export { toolResponse, errorResponse, sanitizeError } from "./tool-helpers.js"; export * from "./schemas.js";