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
};
}
}
);
}