Skip to main content
Glama

create_announcement

Create and schedule course announcements in Canvas LMS by specifying title, message, and optional posting times.

Instructions

Create a new announcement for a course with optional scheduling.

Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID title: The title/subject of the announcement message: The content/body of the announcement delayed_post_at: Optional ISO 8601 datetime to schedule posting (e.g., "2024-01-15T12:00:00Z") lock_at: Optional ISO 8601 datetime to automatically lock the announcement

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
course_identifierYes
titleYes
messageYes
delayed_post_atNo
lock_atNo

Implementation Reference

  • The core handler function for the 'create_announcement' tool. Creates a new announcement by posting a discussion topic to the Canvas API with 'is_announcement': True. Includes parameters for title, message, scheduling (delayed_post_at, lock_at), and uses helpers like get_course_id and make_canvas_request.
    @validate_params async def create_announcement(course_identifier: str | int, title: str, message: str, delayed_post_at: str | None = None, lock_at: str | None = None) -> str: """Create a new announcement for a course with optional scheduling. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID title: The title/subject of the announcement message: The content/body of the announcement delayed_post_at: Optional ISO 8601 datetime to schedule posting (e.g., "2024-01-15T12:00:00Z") lock_at: Optional ISO 8601 datetime to automatically lock the announcement """ course_id = await get_course_id(course_identifier) data = { "title": title, "message": message, "is_announcement": True, "published": True } if delayed_post_at: data["delayed_post_at"] = delayed_post_at if lock_at: data["lock_at"] = lock_at response = await make_canvas_request( "post", f"/courses/{course_id}/discussion_topics", data=data ) if "error" in response: return f"Error creating announcement: {response['error']}" announcement_id = response.get("id") announcement_title = response.get("title", title) created_at = format_date(response.get("created_at")) course_display = await get_course_code(course_id) or course_identifier return f"Announcement created successfully in course {course_display}:\n\n" + \ f"ID: {announcement_id}\n" + \ f"Title: {announcement_title}\n" + \ f"Created: {created_at}"
  • The register_all_tools function which calls register_discussion_tools(mcp) at line 49, thereby registering the create_announcement tool along with others.
    def register_all_tools(mcp: FastMCP) -> None: """Register all MCP tools, resources, and prompts.""" log_info("Registering Canvas MCP tools...") # Register tools by category register_course_tools(mcp) register_assignment_tools(mcp) register_discussion_tools(mcp) register_other_tools(mcp) register_rubric_tools(mcp) register_peer_review_tools(mcp) register_peer_review_comment_tools(mcp) register_messaging_tools(mcp) register_student_tools(mcp) register_accessibility_tools(mcp) register_discovery_tools(mcp) register_code_execution_tools(mcp) # Register resources and prompts register_resources_and_prompts(mcp) log_info("All Canvas MCP tools registered successfully!")
  • Defines AnnouncementInfo TypedDict type, describing the structure of announcement data with fields like id, title, message, posted_at, etc.
    class AnnouncementInfo(TypedDict, total=False): id: int | str title: str message: str posted_at: str | None delayed_post_at: str | None lock_at: str | None published: bool is_announcement: bool
  • The register_discussion_tools function that defines and registers the create_announcement tool via @mcp.tool() decorator.
    def register_discussion_tools(mcp: FastMCP): """Register all discussion and announcement MCP tools.""" # ===== DISCUSSION TOOLS ===== @mcp.tool() @validate_params async def list_discussion_topics(course_identifier: str | int, include_announcements: bool = False) -> str: """List discussion topics for a specific course. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID include_announcements: Whether to include announcements in the list (default: False) """ course_id = await get_course_id(course_identifier) params = {"per_page": 100} if include_announcements: params["include[]"] = ["announcement"] topics = await fetch_all_paginated_results(f"/courses/{course_id}/discussion_topics", params) if isinstance(topics, dict) and "error" in topics: return f"Error fetching discussion topics: {topics['error']}" if not topics: return f"No discussion topics found for course {course_identifier}." topics_info = [] for topic in topics: topic_id = topic.get("id") title = topic.get("title", "Untitled topic") is_announcement = topic.get("is_announcement", False) published = topic.get("published", False) posted_at = format_date(topic.get("posted_at")) topic_type = "Announcement" if is_announcement else "Discussion" status = "Published" if published else "Unpublished" topics_info.append( f"ID: {topic_id}\nType: {topic_type}\nTitle: {title}\nStatus: {status}\nPosted: {posted_at}\n" ) course_display = await get_course_code(course_id) or course_identifier return f"Discussion Topics for Course {course_display}:\n\n" + "\n".join(topics_info) @mcp.tool() @validate_params async def get_discussion_topic_details(course_identifier: str | int, topic_id: str | int) -> str: """Get detailed information about a specific discussion topic. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID topic_id: The Canvas discussion topic ID """ course_id = await get_course_id(course_identifier) response = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{topic_id}" ) if "error" in response: return f"Error fetching discussion topic details: {response['error']}" # Extract topic details title = response.get("title", "Untitled") message = response.get("message", "") is_announcement = response.get("is_announcement", False) author = response.get("author", {}) author_name = author.get("display_name", "Unknown author") author_id = author.get("id", "Unknown") created_at = format_date(response.get("created_at")) posted_at = format_date(response.get("posted_at")) # Discussion statistics discussion_entries_count = response.get("discussion_entries_count", 0) unread_count = response.get("unread_count", 0) read_state = response.get("read_state", "unknown") # Topic settings locked = response.get("locked", False) pinned = response.get("pinned", False) require_initial_post = response.get("require_initial_post", False) # Format the output course_display = await get_course_code(course_id) or course_identifier topic_type = "Announcement" if is_announcement else "Discussion" result = f"{topic_type} Details for Course {course_display}:\n\n" result += f"Title: {title}\n" result += f"ID: {topic_id}\n" result += f"Type: {topic_type}\n" result += f"Author: {author_name} (ID: {author_id})\n" result += f"Created: {created_at}\n" result += f"Posted: {posted_at}\n" if locked: result += "Status: Locked\n" if pinned: result += "Pinned: Yes\n" if require_initial_post: result += "Requires Initial Post: Yes\n" result += f"Total Entries: {discussion_entries_count}\n" if unread_count > 0: result += f"Unread Entries: {unread_count}\n" result += f"Read State: {read_state.title()}\n" if message: result += f"\nContent:\n{message}" return result @mcp.tool() @validate_params async def list_discussion_entries(course_identifier: str | int, topic_id: str | int, include_full_content: bool = False, include_replies: bool = False) -> str: """List discussion entries (posts) for a specific discussion topic with optional full content and replies. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID topic_id: The Canvas discussion topic ID include_full_content: Whether to fetch full content for each entry (default: False) include_replies: Whether to fetch replies for each entry (default: False) """ course_id = await get_course_id(course_identifier) # Get basic entries first entries = await fetch_all_paginated_results( f"/courses/{course_id}/discussion_topics/{topic_id}/entries", {"per_page": 100} ) if isinstance(entries, dict) and "error" in entries: return f"Error fetching discussion entries: {entries['error']}" if not entries: return f"No discussion entries found for topic {topic_id}." # Anonymize entries to protect student privacy try: anonymized_entries = anonymize_response_data(entries, data_type="discussions") # Basic validation: check that anonymization occurred if anonymized_entries and isinstance(anonymized_entries, list) and len(anonymized_entries) > 0: # Verify first entry was anonymized (has anonymous user_name) first_entry = anonymized_entries[0] if first_entry.get("user_name", "").startswith("Student_"): entries = anonymized_entries # Use anonymized data else: log_warning( "Anonymization may not have been applied properly", course_id=course_id, topic_id=topic_id ) else: entries = anonymized_entries # Use result even if validation unclear except Exception as e: # Log error but continue with original data rather than failing completely log_error( "Failed to anonymize discussion entries", exc=e, course_id=course_id, topic_id=topic_id ) # Continue with original data - this maintains functionality while logging the issue # Enhanced content fetching using multiple methods if include_full_content or include_replies: # Method 1: Try to get everything from discussion view (most efficient) full_entries_map = {} try: view_response = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{topic_id}/view" ) if "error" not in view_response and "view" in view_response: for view_entry in view_response.get("view", []): full_entries_map[str(view_entry.get("id"))] = view_entry except Exception as e: log_warning( "Failed to fetch discussion view, falling back to individual calls", exc=e, course_id=course_id, topic_id=topic_id ) # Method 2: For entries not found in view, try entry_list endpoint missing_entry_ids = [] for entry in entries: entry_id = str(entry.get("id")) if entry_id not in full_entries_map: missing_entry_ids.append(entry_id) if missing_entry_ids: try: entry_list_response = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{topic_id}/entry_list", params={"ids[]": missing_entry_ids} ) if "error" not in entry_list_response and isinstance(entry_list_response, list): for full_entry in entry_list_response: full_entries_map[str(full_entry.get("id"))] = full_entry except Exception as e: log_warning( "Failed to fetch entry list", exc=e, course_id=course_id, topic_id=topic_id, missing_count=len(missing_entry_ids) ) # Get topic details for context topic_response = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{topic_id}" ) topic_title = "Unknown Topic" if "error" not in topic_response: topic_title = topic_response.get("title", "Unknown Topic") # Format the output course_display = await get_course_code(course_id) or course_identifier entries_info = [] for entry in entries: entry_id = entry.get("id") entry_id_str = str(entry_id) user_id = entry.get("user_id") user_name = entry.get("user_name", "Unknown user") created_at = format_date(entry.get("created_at")) # Get message content if include_full_content and entry_id_str in full_entries_map: # Use full content from enhanced fetch full_entry = full_entries_map[entry_id_str] message = full_entry.get("message", entry.get("message", "")) else: # Use basic content from original entry message = entry.get("message", "") # Process message content import re if message: if include_full_content: # For full content, clean HTML but keep the full text message_display = re.sub(r'<[^>]+>', '', message) message_display = message_display.strip() if not message_display: message_display = "[Content contains only HTML/formatting]" else: # For preview, truncate as before message_preview = re.sub(r'<[^>]+>', '', message) if len(message_preview) > 300: message_preview = message_preview[:300] + "..." message_display = message_preview.replace("\n", " ").strip() else: message_display = "[No content]" # Handle replies replies_info = "" if include_replies: replies = [] # Try to get replies from enhanced fetch first if entry_id_str in full_entries_map: replies = full_entries_map[entry_id_str].get("replies", []) # If no replies from enhanced fetch, try basic recent_replies if not replies: replies = entry.get("recent_replies", []) # If still no replies or need more, try direct API call has_more_replies = entry.get("has_more_replies", False) if not replies or has_more_replies: try: replies_response = await fetch_all_paginated_results( f"/courses/{course_id}/discussion_topics/{topic_id}/entries/{entry_id}/replies", {"per_page": 100} ) if not isinstance(replies_response, dict) or "error" not in replies_response: replies = replies_response except Exception as e: log_warning( "Failed to fetch entry replies", exc=e, course_id=course_id, topic_id=topic_id, entry_id=entry_id ) if replies: replies_info = f"\n Replies ({len(replies)}):\n" for i, reply in enumerate(replies, 1): reply_user = reply.get("user_name", "Unknown") reply_created = format_date(reply.get("created_at")) reply_msg = reply.get("message", "") # Clean reply message if reply_msg: reply_clean = re.sub(r'<[^>]+>', '', reply_msg) if len(reply_clean) > 200: reply_clean = reply_clean[:200] + "..." reply_clean = reply_clean.replace("\n", " ").strip() else: reply_clean = "[No content]" replies_info += f" {i}. {reply_user} ({reply_created}): {reply_clean}\n" else: replies_info = "\n No replies found.\n" else: # Just show reply count without fetching recent_replies = entry.get("recent_replies", []) has_more_replies = entry.get("has_more_replies", False) total_replies = len(recent_replies) if has_more_replies: total_replies_text = f"{total_replies}+ replies" elif total_replies > 0: total_replies_text = f"{total_replies} replies" else: total_replies_text = "No replies" replies_info = f"\n Replies: {total_replies_text}" # Build entry info entry_info = f"Entry ID: {entry_id}\n" entry_info += f"Author: {user_name} (ID: {user_id})\n" entry_info += f"Posted: {created_at}{replies_info}\n" if include_full_content: entry_info += f"Full Content:\n{message_display}\n" else: entry_info += f"Content Preview: {message_display}\n" entries_info.append(entry_info) # Add helpful footer information footer = "" if not include_full_content: footer += "\nšŸ’” Tip: Use include_full_content=True to get complete post content in one call" if not include_replies: footer += "\nšŸ’” Tip: Use include_replies=True to fetch all replies" return f"Discussion Entries for '{topic_title}' in Course {course_display}:\n\n" + "\n".join(entries_info) + footer @mcp.tool() @validate_params async def get_discussion_entry_details(course_identifier: str | int, topic_id: str | int, entry_id: str | int, include_replies: bool = True) -> str: """Get detailed information about a specific discussion entry including all its replies. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID topic_id: The Canvas discussion topic ID entry_id: The Canvas discussion entry ID include_replies: Whether to fetch and include replies (default: True) """ course_id = await get_course_id(course_identifier) # Method 1: Try to get entry details from the discussion view endpoint entry_response = None replies = [] try: # First try the discussion view endpoint which includes all entries view_response = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{topic_id}/view" ) if "error" not in view_response and "view" in view_response: # Find our specific entry in the view for entry in view_response.get("view", []): if str(entry.get("id")) == str(entry_id): entry_response = entry if include_replies: replies = entry.get("replies", []) break except Exception as e: log_warning( "Failed to fetch discussion view for entry details", exc=e, course_id=course_id, topic_id=topic_id, entry_id=entry_id ) # Method 2: If view method failed, try the entry_list endpoint if not entry_response: try: entry_list_response = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{topic_id}/entry_list", params={"ids[]": entry_id} ) if "error" not in entry_list_response and isinstance(entry_list_response, list): if entry_list_response: entry_response = entry_list_response[0] except Exception as e: log_warning( "Failed to fetch entry from entry_list", exc=e, course_id=course_id, topic_id=topic_id, entry_id=entry_id ) # Method 3: Fallback to getting all entries and finding our target if not entry_response: try: all_entries = await fetch_all_paginated_results( f"/courses/{course_id}/discussion_topics/{topic_id}/entries", {"per_page": 100} ) if not isinstance(all_entries, dict) or "error" not in all_entries: for entry in all_entries: if str(entry.get("id")) == str(entry_id): entry_response = entry # Get recent_replies from this method if include_replies: replies = entry.get("recent_replies", []) break except Exception as e: log_warning( "Failed to fetch all entries as fallback", exc=e, course_id=course_id, topic_id=topic_id, entry_id=entry_id ) # If we still don't have the entry, return error if not entry_response: return f"Error: Could not find discussion entry {entry_id} in topic {topic_id}. The entry may not exist or you may not have permission to view it." # Method 4: If we have the entry but no replies yet, try the replies endpoint if include_replies and not replies: try: replies_response = await fetch_all_paginated_results( f"/courses/{course_id}/discussion_topics/{topic_id}/entries/{entry_id}/replies", {"per_page": 100} ) if not isinstance(replies_response, dict) or "error" not in replies_response: replies = replies_response except Exception as e: log_warning( "Failed to fetch entry replies from replies endpoint", exc=e, course_id=course_id, topic_id=topic_id, entry_id=entry_id ) # Get topic details for context topic_response = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{topic_id}" ) topic_title = "Unknown Topic" if "error" not in topic_response: topic_title = topic_response.get("title", "Unknown Topic") # Format the entry details course_display = await get_course_code(course_id) or course_identifier user_id = entry_response.get("user_id") user_name = entry_response.get("user_name", "Unknown user") message = entry_response.get("message", "") created_at = format_date(entry_response.get("created_at")) updated_at = format_date(entry_response.get("updated_at")) read_state = entry_response.get("read_state", "unknown") result = f"Discussion Entry Details for '{topic_title}' in Course {course_display}:\n\n" result += f"Topic ID: {topic_id}\n" result += f"Entry ID: {entry_id}\n" result += f"Author: {user_name} (ID: {user_id})\n" result += f"Posted: {created_at}\n" if updated_at != "N/A" and updated_at != created_at: result += f"Updated: {updated_at}\n" result += f"Read State: {read_state.title()}\n" result += f"\nContent:\n{message}\n" # Format replies if include_replies: if replies: result += f"\nReplies ({len(replies)}):\n" result += "=" * 50 + "\n" for i, reply in enumerate(replies, 1): reply_id = reply.get("id") reply_user_name = reply.get("user_name", "Unknown user") reply_message = reply.get("message", "") reply_created_at = format_date(reply.get("created_at")) result += f"\nReply #{i}:\n" result += f"Reply ID: {reply_id}\n" result += f"Author: {reply_user_name}\n" result += f"Posted: {reply_created_at}\n" result += f"Content:\n{reply_message}\n" else: result += "\nNo replies found for this entry." else: result += "\n(Replies not included - set include_replies=True to fetch them)" return result @mcp.tool() @validate_params async def get_discussion_with_replies(course_identifier: str | int, topic_id: str | int, include_replies: bool = False) -> str: """Enhanced function to get discussion entries with optional reply fetching. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID topic_id: The Canvas discussion topic ID include_replies: Whether to fetch detailed replies for all entries (default: False) """ course_id = await get_course_id(course_identifier) # Get basic entries first entries = await fetch_all_paginated_results( f"/courses/{course_id}/discussion_topics/{topic_id}/entries", {"per_page": 100} ) if isinstance(entries, dict) and "error" in entries: return f"Error fetching discussion entries: {entries['error']}" if not entries: return f"No discussion entries found for topic {topic_id}." # Get topic details for context topic_response = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{topic_id}" ) topic_title = "Unknown Topic" if "error" not in topic_response: topic_title = topic_response.get("title", "Unknown Topic") course_display = await get_course_code(course_id) or course_identifier result = f"Discussion '{topic_title}' in Course {course_display}:\n\n" # Process each entry for entry in entries: entry_id = entry.get("id") user_name = entry.get("user_name", "Unknown user") message = entry.get("message", "") created_at = format_date(entry.get("created_at")) # Clean up message for display import re if message: message_preview = re.sub(r'<[^>]+>', '', message) if len(message_preview) > 200: message_preview = message_preview[:200] + "..." message_preview = message_preview.replace("\n", " ").strip() else: message_preview = "[No content]" result += f"šŸ“ Entry {entry_id} by {user_name}\n" result += f" Posted: {created_at}\n" result += f" Content: {message_preview}\n" # Handle replies if include_replies: replies = [] # Method 1: Check recent_replies from the entry recent_replies = entry.get("recent_replies", []) if recent_replies: replies = recent_replies # Method 2: If no recent_replies or has_more_replies, try direct API call has_more_replies = entry.get("has_more_replies", False) if not replies or has_more_replies: try: replies_response = await fetch_all_paginated_results( f"/courses/{course_id}/discussion_topics/{topic_id}/entries/{entry_id}/replies", {"per_page": 100} ) if not isinstance(replies_response, dict) or "error" not in replies_response: replies = replies_response except Exception as e: log_warning( "Failed to fetch detailed replies", exc=e, course_id=course_id, topic_id=topic_id, entry_id=entry_id ) # Display replies if replies: result += f" šŸ’¬ Replies ({len(replies)}):\n" for i, reply in enumerate(replies, 1): reply_user = reply.get("user_name", "Unknown") reply_created = format_date(reply.get("created_at")) reply_msg = reply.get("message", "") # Clean reply message if reply_msg: reply_preview = re.sub(r'<[^>]+>', '', reply_msg) if len(reply_preview) > 150: reply_preview = reply_preview[:150] + "..." reply_preview = reply_preview.replace("\n", " ").strip() else: reply_preview = "[No content]" result += f" └─ Reply {i} by {reply_user} ({reply_created}): {reply_preview}\n" else: recent_count = len(entry.get("recent_replies", [])) has_more = entry.get("has_more_replies", False) if recent_count > 0 or has_more: result += f" šŸ’¬ Replies: {recent_count}{'+ (has more)' if has_more else ''} (failed to fetch details)\n" else: result += " šŸ’¬ No replies\n" else: # Just show reply count without fetching recent_count = len(entry.get("recent_replies", [])) has_more = entry.get("has_more_replies", False) if recent_count > 0 or has_more: result += f" šŸ’¬ Replies: {recent_count}{'+ (has more)' if has_more else ''}\n" else: result += " šŸ’¬ No replies\n" result += "\n" if not include_replies: result += "\nšŸ’” Tip: Use include_replies=True to fetch detailed reply content" return result @mcp.tool() @validate_params async def post_discussion_entry(course_identifier: str | int, topic_id: str | int, message: str) -> str: """Post a new top-level entry to a discussion topic. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID topic_id: The Canvas discussion topic ID message: The entry message content """ course_id = await get_course_id(course_identifier) # Prepare the entry data data = { "message": message } # Post the entry response = await make_canvas_request( "post", f"/courses/{course_id}/discussion_topics/{topic_id}/entries", data=data ) if "error" in response: return f"Error posting discussion entry: {response['error']}" # Get context information for confirmation topic_response = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{topic_id}" ) topic_title = "Unknown Topic" if "error" not in topic_response: topic_title = topic_response.get("title", "Unknown Topic") # Extract entry details from response entry_id = response.get("id") entry_created_at = format_date(response.get("created_at")) entry_user_name = response.get("user_name", "You") # Build confirmation message course_display = await get_course_code(course_id) or course_identifier result = "Discussion entry posted successfully!\n\n" result += f"Course: {course_display}\n" result += f"Discussion Topic: {topic_title} (ID: {topic_id})\n" result += f"Entry ID: {entry_id}\n" result += f"Entry Author: {entry_user_name}\n" result += f"Posted: {entry_created_at}\n\n" result += f"Your Entry:\n{message}\n" return result @mcp.tool() @validate_params async def reply_to_discussion_entry(course_identifier: str | int, topic_id: str | int, entry_id: str | int, message: str) -> str: """Reply to a student's discussion entry/comment. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID topic_id: The Canvas discussion topic ID entry_id: The Canvas discussion entry ID to reply to message: The reply message content """ course_id = await get_course_id(course_identifier) # Ensure IDs are strings topic_id_str = str(topic_id) entry_id_str = str(entry_id) data = { "message": message } response = await make_canvas_request( "post", f"/courses/{course_id}/discussion_topics/{topic_id_str}/entries/{entry_id_str}/replies", data=data ) if "error" in response: return f"Error posting reply: {response['error']}" reply_id = response.get("id") course_display = await get_course_code(course_id) or course_identifier return f"Reply posted successfully in course {course_display}:\n" + \ f"Topic ID: {topic_id}\n" + \ f"Original Entry ID: {entry_id}\n" + \ f"Reply ID: {reply_id}\n" + \ f"Message: {truncate_text(message, 200)}" @mcp.tool() @validate_params async def create_discussion_topic(course_identifier: str | int, title: str, message: str, delayed_post_at: str | None = None, lock_at: str | None = None, require_initial_post: bool = False, pinned: bool = False) -> str: """Create a new discussion topic for a course. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID title: The title/subject of the discussion topic message: The content/body of the discussion topic delayed_post_at: Optional ISO 8601 datetime to schedule posting (e.g., "2024-01-15T12:00:00Z") lock_at: Optional ISO 8601 datetime to automatically lock the discussion require_initial_post: Whether students must post before seeing other posts pinned: Whether to pin this discussion topic """ course_id = await get_course_id(course_identifier) data = { "title": title, "message": message, "published": True, "require_initial_post": require_initial_post, "pinned": pinned } if delayed_post_at: data["delayed_post_at"] = delayed_post_at if lock_at: data["lock_at"] = lock_at response = await make_canvas_request( "post", f"/courses/{course_id}/discussion_topics", data=data ) if "error" in response: return f"Error creating discussion topic: {response['error']}" topic_id = response.get("id") topic_title = response.get("title", title) created_at = format_date(response.get("created_at")) course_display = await get_course_code(course_id) or course_identifier return f"Discussion topic created successfully in course {course_display}:\n\n" + \ f"ID: {topic_id}\n" + \ f"Title: {topic_title}\n" + \ f"Created: {created_at}" # ===== ANNOUNCEMENT TOOLS ===== @mcp.tool() async def list_announcements(course_identifier: str) -> str: """List announcements for a specific course. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID """ course_id = await get_course_id(course_identifier) params = { "include[]": ["announcement"], "only_announcements": True, "per_page": 100 } announcements = await fetch_all_paginated_results(f"/courses/{course_id}/discussion_topics", params) if isinstance(announcements, dict) and "error" in announcements: return f"Error fetching announcements: {announcements['error']}" if not announcements: return f"No announcements found for course {course_identifier}." announcements_info = [] for announcement in announcements: announcement_id = announcement.get("id") title = announcement.get("title", "Untitled announcement") posted_at = format_date(announcement.get("posted_at")) announcements_info.append( f"ID: {announcement_id}\nTitle: {title}\nPosted: {posted_at}\n" ) course_display = await get_course_code(course_id) or course_identifier return f"Announcements for Course {course_display}:\n\n" + "\n".join(announcements_info) @mcp.tool() @validate_params async def create_announcement(course_identifier: str | int, title: str, message: str, delayed_post_at: str | None = None, lock_at: str | None = None) -> str: """Create a new announcement for a course with optional scheduling. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID title: The title/subject of the announcement message: The content/body of the announcement delayed_post_at: Optional ISO 8601 datetime to schedule posting (e.g., "2024-01-15T12:00:00Z") lock_at: Optional ISO 8601 datetime to automatically lock the announcement """ course_id = await get_course_id(course_identifier) data = { "title": title, "message": message, "is_announcement": True, "published": True } if delayed_post_at: data["delayed_post_at"] = delayed_post_at if lock_at: data["lock_at"] = lock_at response = await make_canvas_request( "post", f"/courses/{course_id}/discussion_topics", data=data ) if "error" in response: return f"Error creating announcement: {response['error']}" announcement_id = response.get("id") announcement_title = response.get("title", title) created_at = format_date(response.get("created_at")) course_display = await get_course_code(course_id) or course_identifier return f"Announcement created successfully in course {course_display}:\n\n" + \ f"ID: {announcement_id}\n" + \ f"Title: {announcement_title}\n" + \ f"Created: {created_at}" # ===== ANNOUNCEMENT DELETION TOOLS ===== @mcp.tool() @validate_params async def delete_announcement( course_identifier: str | int, announcement_id: str | int ) -> str: """ Delete an announcement from a Canvas course. Announcements are technically discussion topics in Canvas, so this uses the discussion_topics endpoint to delete them. Args: course_identifier: The Canvas course code (e.g., badm_554_120251_246794) or ID announcement_id: The Canvas announcement/discussion topic ID to delete Returns: String describing the deletion result with status and title Raises: HTTPError: - 401: User doesn't have permission to delete the announcement - 404: Announcement not found in the specified course - 403: Editing is restricted for this announcement Example usage: result = delete_announcement("60366", "925355") print(f"Result: {result}") """ course_id = await get_course_id(course_identifier) # First, get the announcement details to return meaningful information announcement = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{announcement_id}" ) if "error" in announcement: return f"Error fetching announcement details: {announcement['error']}" announcement_title = announcement.get("title", "Unknown Title") # Proceed with deletion response = await make_canvas_request( "delete", f"/courses/{course_id}/discussion_topics/{announcement_id}" ) if "error" in response: return f"Error deleting announcement '{announcement_title}': {response['error']}" course_display = await get_course_code(course_id) or course_identifier return f"Announcement deleted successfully from course {course_display}:\n\n" + \ f"ID: {announcement_id}\n" + \ f"Title: {announcement_title}\n" + \ "Status: deleted\n" + \ "Message: Announcement deleted successfully" @mcp.tool() @validate_params async def bulk_delete_announcements( course_identifier: str | int, announcement_ids: list[str | int], stop_on_error: bool = False ) -> str: """ Delete multiple announcements from a Canvas course. Args: course_identifier: The Canvas course code or ID announcement_ids: List of announcement IDs to delete stop_on_error: If True, stop processing on first error; if False, continue with remaining Returns: String with detailed results including successful and failed deletions Example usage: results = bulk_delete_announcements( "60366", ["925355", "925354", "925353"], stop_on_error=False ) """ course_id = await get_course_id(course_identifier) successful = [] failed = [] for announcement_id in announcement_ids: try: # Get announcement details first announcement = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{announcement_id}" ) if "error" in announcement: failed.append({ "id": str(announcement_id), "error": announcement["error"], "message": "Failed to fetch announcement details" }) if stop_on_error: break continue # Proceed with deletion response = await make_canvas_request( "delete", f"/courses/{course_id}/discussion_topics/{announcement_id}" ) if "error" in response: failed.append({ "id": str(announcement_id), "title": announcement.get("title", "Unknown Title"), "error": response["error"], "message": "Failed to delete announcement" }) if stop_on_error: break else: successful.append({ "id": str(announcement_id), "title": announcement.get("title", "Unknown Title") }) except Exception as e: failed.append({ "id": str(announcement_id), "error": str(e), "message": "Unexpected error during deletion" }) if stop_on_error: break # Format results summary = { "total": len(announcement_ids), "successful": len(successful), "failed": len(failed) } course_display = await get_course_code(course_id) or course_identifier result = f"Bulk deletion results for course {course_display}:\n\n" result += f"Summary: {summary['successful']} successful, {summary['failed']} failed out of {summary['total']} total\n\n" if successful: result += "Successfully deleted:\n" for item in successful: result += f" - ID: {item['id']}, Title: {item['title']}\n" result += "\n" if failed: result += "Failed to delete:\n" for item in failed: result += f" - ID: {item['id']}" if 'title' in item: result += f", Title: {item['title']}" result += f", Error: {item['error']}\n" return result @mcp.tool() @validate_params async def delete_announcement_with_confirmation( course_identifier: str | int, announcement_id: str | int, require_title_match: str | None = None, dry_run: bool = False ) -> str: """ Delete an announcement with optional safety checks. Args: course_identifier: The Canvas course code or ID announcement_id: The announcement ID to delete require_title_match: If provided, only delete if the announcement title matches exactly dry_run: If True, verify but don't actually delete (for testing) Returns: String with operation result including status and title match information Raises: ValueError: If require_title_match is provided and doesn't match the actual title Example usage: # Delete only if title matches exactly (safety check) result = delete_announcement_with_confirmation( "60366", "925355", require_title_match="Preparing for the week", dry_run=False ) """ course_id = await get_course_id(course_identifier) # First fetch the announcement details announcement = await make_canvas_request( "get", f"/courses/{course_id}/discussion_topics/{announcement_id}" ) if "error" in announcement: return f"Error fetching announcement details: {announcement['error']}" actual_title = announcement.get("title", "Unknown Title") title_matched = True # Check title match if required if require_title_match is not None: title_matched = actual_title == require_title_match if not title_matched: return f"Title mismatch - Expected: '{require_title_match}', Actual: '{actual_title}'. Deletion aborted for safety." # Handle dry run if dry_run: course_display = await get_course_code(course_id) or course_identifier result = f"DRY RUN - Would delete announcement from course {course_display}:\n\n" result += f"ID: {announcement_id}\n" result += f"Title: {actual_title}\n" result += "Status: dry_run\n" result += "Message: Announcement would be deleted (dry run mode)\n" if require_title_match: result += f"Title matched: {title_matched}\n" return result # Proceed with actual deletion response = await make_canvas_request( "delete", f"/courses/{course_id}/discussion_topics/{announcement_id}" ) if "error" in response: return f"Error deleting announcement '{actual_title}': {response['error']}" course_display = await get_course_code(course_id) or course_identifier result = f"Announcement deleted successfully from course {course_display}:\n\n" result += f"ID: {announcement_id}\n" result += f"Title: {actual_title}\n" result += "Status: deleted\n" result += "Message: Announcement deleted successfully\n" if require_title_match: result += f"Title matched: {title_matched}\n" return result @mcp.tool() @validate_params async def delete_announcements_by_criteria( course_identifier: str | int, criteria: dict, limit: int | None = None, dry_run: bool = True ) -> str: """ Delete announcements matching specific criteria. Args: course_identifier: The Canvas course code or ID criteria: Dict with search criteria: - "title_contains": str - Delete if title contains this text - "older_than": str - Delete if posted before this date (ISO format) - "newer_than": str - Delete if posted after this date (ISO format) - "title_regex": str - Delete if title matches regex pattern limit: Maximum number of announcements to delete (safety limit) dry_run: If True, show what would be deleted without actually deleting Returns: String with operation results showing matched and deleted announcements Example usage: # Delete all announcements older than 30 days from datetime import datetime, timedelta results = delete_announcements_by_criteria( "60366", criteria={ "older_than": (datetime.now() - timedelta(days=30)).isoformat(), "title_contains": "reminder" }, limit=10, dry_run=False ) """ course_id = await get_course_id(course_identifier) # First list all announcements params = { "include[]": ["announcement"], "only_announcements": True, "per_page": 100 } announcements = await fetch_all_paginated_results(f"/courses/{course_id}/discussion_topics", params) if isinstance(announcements, dict) and "error" in announcements: return f"Error fetching announcements: {announcements['error']}" if not announcements: return f"No announcements found for course {course_identifier}." # Filter based on criteria matched = [] for announcement in announcements: match = True announcement_title = announcement.get("title", "") posted_at_str = announcement.get("posted_at") # Check title_contains if "title_contains" in criteria: if criteria["title_contains"].lower() not in announcement_title.lower(): match = False # Check title_regex if "title_regex" in criteria and match: try: if not re.search(criteria["title_regex"], announcement_title, re.IGNORECASE): match = False except re.error: return f"Invalid regex pattern: {criteria['title_regex']}" # Check date criteria if posted_at_str and match: try: posted_at = datetime.fromisoformat(posted_at_str.replace('Z', '+00:00')) if "older_than" in criteria: older_than = datetime.fromisoformat(criteria["older_than"].replace('Z', '+00:00')) if posted_at >= older_than: match = False if "newer_than" in criteria and match: newer_than = datetime.fromisoformat(criteria["newer_than"].replace('Z', '+00:00')) if posted_at <= newer_than: match = False except ValueError as e: return f"Error parsing date: {e}" if match: matched.append(announcement) # Apply limit if specified limit_reached = False if limit and len(matched) > limit: matched = matched[:limit] limit_reached = True course_display = await get_course_code(course_id) or course_identifier result = f"Criteria-based deletion results for course {course_display}:\n\n" result += f"Search criteria: {json.dumps(criteria, indent=2)}\n\n" result += f"Matched {len(matched)} announcements" if limit_reached: result += f" (limited to {limit})" result += "\n\n" if not matched: result += "No announcements matched the specified criteria." return result # Show what was matched result += "Matched announcements:\n" for announcement in matched: result += f" - ID: {announcement.get('id')}, Title: {announcement.get('title', 'Untitled')}, Posted: {format_date(announcement.get('posted_at'))}\n" result += "\n" if dry_run: result += "DRY RUN: No announcements were actually deleted.\n" result += "Set dry_run=False to perform actual deletions." return result # Perform actual deletions deleted = [] failed = [] for announcement in matched: announcement_id = announcement.get("id") try: response = await make_canvas_request( "delete", f"/courses/{course_id}/discussion_topics/{announcement_id}" ) if "error" in response: failed.append({ "id": str(announcement_id), "title": announcement.get("title", "Unknown Title"), "error": response["error"] }) else: deleted.append({ "id": str(announcement_id), "title": announcement.get("title", "Unknown Title") }) except Exception as e: failed.append({ "id": str(announcement_id), "title": announcement.get("title", "Unknown Title"), "error": str(e) }) result += f"Deletion completed: {len(deleted)} successful, {len(failed)} failed\n\n" if deleted: result += "Successfully deleted:\n" for item in deleted: result += f" - ID: {item['id']}, Title: {item['title']}\n" result += "\n" if failed: result += "Failed to delete:\n" for item in failed: result += f" - ID: {item['id']}, Title: {item['title']}, Error: {item['error']}\n" return result
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/vishalsachdev/canvas-mcp'

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