Skip to main content
Glama
search-assignments.ts7.53 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { canvasApiRequest } from "../api/canvas-client.js"; import { CanvasCourse, CanvasAssignment, AssignmentWithCourse } from "../types/canvas.js"; import { htmlToPlainText } from "../utils/html.js"; import { formatDate, parseDate, isDateInRange } from "../utils/date.js"; export function registerSearchAssignmentsTool(server: McpServer) { 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 }; } } ); }

Implementation Reference

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