Skip to main content
Glama
brendan-ch

Canvas Assignment Assistant

by brendan-ch

search_assignments

Search for Canvas assignments across courses using keywords, due dates, and course filters to find relevant tasks and deadlines.

Instructions

Searches for assignments across all courses based on title, description, due dates, and course filters.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryNoSearch term to find in assignment titles or descriptions
dueBeforeNoOnly include assignments due before this date (YYYY-MM-DD)
dueAfterNoOnly include assignments due after this date (YYYY-MM-DD)
includeCompletedNoInclude assignments from completed courses
courseIdNoOptional: Limit search to specific course ID

Implementation Reference

  • The core handler logic for the search_assignments tool. Fetches Canvas courses (filtered by courseId or state), retrieves assignments with date filtering via Canvas API, filters by search query in title/description, sorts by due date, and formats a markdown list of matching assignments.
    async ({ query = "", dueBefore, dueAfter, includeCompleted, courseId }) => {
      try {
        let courses: CanvasCourse[];
    
        // If courseId is provided, only search that course
        if (courseId) {
          courses = [await canvasApiRequest<CanvasCourse>(`/courses/${courseId}`)];
        } else {
          // Otherwise, get all courses based on state
          const courseState = includeCompleted ? 'all' : 'active';
          courses = await canvasApiRequest<CanvasCourse[]>(`/courses?enrollment_state=${courseState}`);
        }
    
        if (courses.length === 0) {
          return {
            content: [{
              type: "text",
              text: "No courses found."
            }]
          };
        }
    
        // Search assignments in each course
        let allResults: AssignmentWithCourse[] = [];
    
        for (const course of courses) {
          try {
            // Build the assignments query
            let assignmentsUrl = `/courses/${course.id}/assignments?per_page=100&order_by=due_at&include[]=submission`;
    
            // Add date filtering parameters if provided
            const params = new URLSearchParams();
    
            // Canvas API uses bucket parameter for broad date filtering
            if (dueAfter && !dueBefore) {
              params.append('bucket', 'future');
            } else if (dueBefore && !dueAfter) {
              params.append('bucket', 'past');
            }
    
            // Add specific date range parameters
            if (dueAfter) {
              const afterDate = parseDate(dueAfter);
              if (afterDate) {
                afterDate.setHours(0, 0, 0, 0);
                params.append('due_after', afterDate.toISOString());
              }
            }
            if (dueBefore) {
              const beforeDate = parseDate(dueBefore);
              if (beforeDate) {
                beforeDate.setHours(23, 59, 59, 999);
                params.append('due_before', beforeDate.toISOString());
              }
            }
    
            if (params.toString()) {
              assignmentsUrl += `&${params.toString()}`;
            }
    
            console.error(`Fetching assignments from URL: ${assignmentsUrl}`); // Debug logging
            const assignments = await canvasApiRequest<CanvasAssignment[]>(assignmentsUrl);
            console.error(`Found ${assignments.length} assignments in course ${course.id}`); // Debug logging
    
            // Filter by search terms if query is provided
            const searchTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 0);
            const matchingAssignments = searchTerms.length > 0 ?
              assignments.filter((assignment) => {
                // Search in title and description
                const titleMatch = searchTerms.some(term =>
                  assignment.name.toLowerCase().includes(term)
                );
    
                const descriptionMatch = assignment.description ?
                  searchTerms.some(term =>
                    htmlToPlainText(assignment.description).toLowerCase().includes(term)
                  ) : false;
    
                return titleMatch || descriptionMatch;
              }) : assignments;
    
            // Double-check date range (in case API filter wasn't exact)
            const dateFilteredAssignments = matchingAssignments.filter(assignment => {
              // Skip local date filtering if the API is already handling it
              if ((dueAfter && !dueBefore && params.has('bucket')) ||
                (dueBefore && !dueAfter && params.has('bucket'))) {
                return true;
              }
              return isDateInRange(assignment.due_at, dueBefore, dueAfter);
            });
    
            // Add course information to each matching assignment
            dateFilteredAssignments.forEach((assignment) => {
              allResults.push({
                ...assignment,
                courseName: course.name,
                courseId: course.id
              });
            });
          } catch (error) {
            console.error(`Error searching in course ${course.id}: ${(error as Error).message}`);
            // Continue with other courses even if one fails
          }
        }
    
        // Sort results by due date
        allResults.sort((a, b) => {
          // Put assignments with no due date at the end
          if (!a.due_at && !b.due_at) return 0;
          if (!a.due_at) return 1;
          if (!b.due_at) return -1;
    
          const dateA = parseDate(a.due_at);
          const dateB = parseDate(b.due_at);
          if (!dateA || !dateB) return 0;
          return dateA.getTime() - dateB.getTime();
        });
    
        if (allResults.length === 0) {
          const dateRange = [];
          if (dueAfter) dateRange.push(`after ${dueAfter}`);
          if (dueBefore) dateRange.push(`before ${dueBefore}`);
          const dateStr = dateRange.length > 0 ? ` due ${dateRange.join(' and ')}` : '';
          const queryStr = query ? ` matching "${query}"` : '';
    
          return {
            content: [{
              type: "text",
              text: `No assignments found${queryStr}${dateStr}.`
            }]
          };
        }
    
        const resultsList = allResults.map((assignment) => {
          const dueDate = formatDate(assignment.due_at);
          const status = assignment.published ? '' : ' (Unpublished)';
          return [
            `- Course: ${assignment.courseName} (ID: ${assignment.courseId})`,
            `  Assignment: ${assignment.name}${status} (ID: ${assignment.id})`,
            `  Due: ${dueDate}`
          ].join('\n');
        }).join('\n\n');
    
        const dateRange = [];
        if (dueAfter) dateRange.push(`after ${dueAfter}`);
        if (dueBefore) dateRange.push(`before ${dueBefore}`);
        const dateStr = dateRange.length > 0 ? ` due ${dateRange.join(' and ')}` : '';
        const queryStr = query ? ` matching "${query}"` : '';
    
        return {
          content: [{
            type: "text",
            text: `Found ${allResults.length} assignments${queryStr}${dateStr}:\n\n${resultsList}`
          }]
        };
      } catch (error) {
        return {
          content: [{
            type: "text",
            text: `Search failed: ${(error as Error).message}`
          }],
          isError: true
        };
      }
    }
  • Zod input schema defining parameters for the tool: query (search term), date range filters (dueBefore, dueAfter), includeCompleted (boolean), and optional courseId.
      query: z.string().optional().default("").describe("Search term to find in assignment titles or descriptions"),
      dueBefore: z.string().optional().describe("Only include assignments due before this date (YYYY-MM-DD)"),
      dueAfter: z.string().optional().describe("Only include assignments due after this date (YYYY-MM-DD)"),
      includeCompleted: z.boolean().default(false).describe("Include assignments from completed courses"),
      courseId: z.string().or(z.number()).optional().describe("Optional: Limit search to specific course ID"),
    },
  • Registers the 'search_assignments' tool on the MCP server with name, description, input schema, and handler function.
    server.tool(
      "search_assignments",
      "Searches for assignments across all courses based on title, description, due dates, and course filters.",
      {
        query: z.string().optional().default("").describe("Search term to find in assignment titles or descriptions"),
        dueBefore: z.string().optional().describe("Only include assignments due before this date (YYYY-MM-DD)"),
        dueAfter: z.string().optional().describe("Only include assignments due after this date (YYYY-MM-DD)"),
        includeCompleted: z.boolean().default(false).describe("Include assignments from completed courses"),
        courseId: z.string().or(z.number()).optional().describe("Optional: Limit search to specific course ID"),
      },
      async ({ query = "", dueBefore, dueAfter, includeCompleted, courseId }) => {
        try {
          let courses: CanvasCourse[];
    
          // If courseId is provided, only search that course
          if (courseId) {
            courses = [await canvasApiRequest<CanvasCourse>(`/courses/${courseId}`)];
          } else {
            // Otherwise, get all courses based on state
            const courseState = includeCompleted ? 'all' : 'active';
            courses = await canvasApiRequest<CanvasCourse[]>(`/courses?enrollment_state=${courseState}`);
          }
    
          if (courses.length === 0) {
            return {
              content: [{
                type: "text",
                text: "No courses found."
              }]
            };
          }
    
          // Search assignments in each course
          let allResults: AssignmentWithCourse[] = [];
    
          for (const course of courses) {
            try {
              // Build the assignments query
              let assignmentsUrl = `/courses/${course.id}/assignments?per_page=100&order_by=due_at&include[]=submission`;
    
              // Add date filtering parameters if provided
              const params = new URLSearchParams();
    
              // Canvas API uses bucket parameter for broad date filtering
              if (dueAfter && !dueBefore) {
                params.append('bucket', 'future');
              } else if (dueBefore && !dueAfter) {
                params.append('bucket', 'past');
              }
    
              // Add specific date range parameters
              if (dueAfter) {
                const afterDate = parseDate(dueAfter);
                if (afterDate) {
                  afterDate.setHours(0, 0, 0, 0);
                  params.append('due_after', afterDate.toISOString());
                }
              }
              if (dueBefore) {
                const beforeDate = parseDate(dueBefore);
                if (beforeDate) {
                  beforeDate.setHours(23, 59, 59, 999);
                  params.append('due_before', beforeDate.toISOString());
                }
              }
    
              if (params.toString()) {
                assignmentsUrl += `&${params.toString()}`;
              }
    
              console.error(`Fetching assignments from URL: ${assignmentsUrl}`); // Debug logging
              const assignments = await canvasApiRequest<CanvasAssignment[]>(assignmentsUrl);
              console.error(`Found ${assignments.length} assignments in course ${course.id}`); // Debug logging
    
              // Filter by search terms if query is provided
              const searchTerms = query.toLowerCase().split(/\s+/).filter(term => term.length > 0);
              const matchingAssignments = searchTerms.length > 0 ?
                assignments.filter((assignment) => {
                  // Search in title and description
                  const titleMatch = searchTerms.some(term =>
                    assignment.name.toLowerCase().includes(term)
                  );
    
                  const descriptionMatch = assignment.description ?
                    searchTerms.some(term =>
                      htmlToPlainText(assignment.description).toLowerCase().includes(term)
                    ) : false;
    
                  return titleMatch || descriptionMatch;
                }) : assignments;
    
              // Double-check date range (in case API filter wasn't exact)
              const dateFilteredAssignments = matchingAssignments.filter(assignment => {
                // Skip local date filtering if the API is already handling it
                if ((dueAfter && !dueBefore && params.has('bucket')) ||
                  (dueBefore && !dueAfter && params.has('bucket'))) {
                  return true;
                }
                return isDateInRange(assignment.due_at, dueBefore, dueAfter);
              });
    
              // Add course information to each matching assignment
              dateFilteredAssignments.forEach((assignment) => {
                allResults.push({
                  ...assignment,
                  courseName: course.name,
                  courseId: course.id
                });
              });
            } catch (error) {
              console.error(`Error searching in course ${course.id}: ${(error as Error).message}`);
              // Continue with other courses even if one fails
            }
          }
    
          // Sort results by due date
          allResults.sort((a, b) => {
            // Put assignments with no due date at the end
            if (!a.due_at && !b.due_at) return 0;
            if (!a.due_at) return 1;
            if (!b.due_at) return -1;
    
            const dateA = parseDate(a.due_at);
            const dateB = parseDate(b.due_at);
            if (!dateA || !dateB) return 0;
            return dateA.getTime() - dateB.getTime();
          });
    
          if (allResults.length === 0) {
            const dateRange = [];
            if (dueAfter) dateRange.push(`after ${dueAfter}`);
            if (dueBefore) dateRange.push(`before ${dueBefore}`);
            const dateStr = dateRange.length > 0 ? ` due ${dateRange.join(' and ')}` : '';
            const queryStr = query ? ` matching "${query}"` : '';
    
            return {
              content: [{
                type: "text",
                text: `No assignments found${queryStr}${dateStr}.`
              }]
            };
          }
    
          const resultsList = allResults.map((assignment) => {
            const dueDate = formatDate(assignment.due_at);
            const status = assignment.published ? '' : ' (Unpublished)';
            return [
              `- Course: ${assignment.courseName} (ID: ${assignment.courseId})`,
              `  Assignment: ${assignment.name}${status} (ID: ${assignment.id})`,
              `  Due: ${dueDate}`
            ].join('\n');
          }).join('\n\n');
    
          const dateRange = [];
          if (dueAfter) dateRange.push(`after ${dueAfter}`);
          if (dueBefore) dateRange.push(`before ${dueBefore}`);
          const dateStr = dateRange.length > 0 ? ` due ${dateRange.join(' and ')}` : '';
          const queryStr = query ? ` matching "${query}"` : '';
    
          return {
            content: [{
              type: "text",
              text: `Found ${allResults.length} assignments${queryStr}${dateStr}:\n\n${resultsList}`
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: "text",
              text: `Search failed: ${(error as Error).message}`
            }],
            isError: true
          };
        }
      }
    );
  • src/index.ts:26-26 (registration)
    Invokes the tool registration function to add search_assignments to the main MCP server.
    registerSearchAssignmentsTool(server);
  • src/index.ts:6-6 (registration)
    Imports the registration function for the search_assignments tool.
    import { registerSearchAssignmentsTool } from "./tools/search-assignments.js";

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/brendan-ch/canvas-mcp'

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