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";
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It states the search action but doesn't describe key behaviors: whether results are paginated, sorted, or limited; if it requires authentication; potential rate limits; or what the output format looks like (e.g., list of assignments with details). For a search tool with zero annotation coverage, this leaves significant gaps in understanding how it operates.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core purpose ('Searches for assignments across all courses') and lists key filters. There's no wasted text, and it's appropriately sized for the tool's complexity. However, it could be slightly more structured by separating search criteria from scope for clarity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (5 parameters, search functionality) and lack of annotations and output schema, the description is incomplete. It doesn't address behavioral aspects like result handling, authentication needs, or error cases, which are critical for an AI agent to use it correctly. The description alone is insufficient for safe and effective tool invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description mentions searchable fields (title, description, due dates, course filters), which aligns with parameters like 'query', 'dueBefore', 'dueAfter', and 'courseId'. However, with 100% schema description coverage, the input schema already documents all 5 parameters thoroughly (e.g., date formats, defaults). The description adds minimal value beyond reinforcing the schema, meeting the baseline for high coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the verb ('Searches for') and resource ('assignments across all courses'), specifying what the tool does. It mentions searchable fields (title, description, due dates, course filters), which helps distinguish it from siblings like 'get_assignment' (singular retrieval) and 'list_courses' (different resource). However, it doesn't explicitly differentiate from 'canvas_list_active_courses' in terms of scope or output type.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites, compare with sibling tools (e.g., use 'get_assignment' for a specific assignment, 'list_courses' for course listing), or specify scenarios where this search is preferred. Usage is implied by the search functionality but lacks explicit context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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

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