Skip to main content
Glama

Canvas MCP Server V2.0

// tests/client.test.ts import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; import { CanvasClient } from '../src/client.js'; import { CanvasAPIError } from '../src/types.js'; import axios from 'axios'; // Mock axios vi.mock('axios'); const mockedAxios = vi.mocked(axios); describe('CanvasClient', () => { let client: CanvasClient; const mockToken = 'test-token'; const mockDomain = 'test.instructure.com'; beforeEach(() => { // Reset all mocks vi.clearAllMocks(); // Mock axios.create const mockAxiosInstance = { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } } }; mockedAxios.create.mockReturnValue(mockAxiosInstance as any); client = new CanvasClient(mockToken, mockDomain); }); afterEach(() => { vi.restoreAllMocks(); }); describe('constructor', () => { test('should create client with correct base URL', () => { expect(mockedAxios.create).toHaveBeenCalledWith({ baseURL: `https://${mockDomain}/api/v1`, headers: { 'Authorization': `Bearer ${mockToken}`, 'Content-Type': 'application/json' }, timeout: 30000 }); }); test('should setup interceptors', () => { const mockInstance = mockedAxios.create.mock.results[0].value; expect(mockInstance.interceptors.request.use).toHaveBeenCalled(); expect(mockInstance.interceptors.response.use).toHaveBeenCalled(); }); }); describe('healthCheck', () => { test('should return ok status when API is accessible', async () => { const mockProfile = { id: 1, name: 'Test User' }; const mockInstance = mockedAxios.create.mock.results[0].value; mockInstance.get.mockResolvedValue({ data: mockProfile }); const result = await client.healthCheck(); expect(result.status).toBe('ok'); expect(result.user).toEqual({ id: 1, name: 'Test User' }); expect(result.timestamp).toBeDefined(); }); test('should return error status when API is not accessible', async () => { const mockInstance = mockedAxios.create.mock.results[0].value; mockInstance.get.mockRejectedValue(new Error('Network error')); const result = await client.healthCheck(); expect(result.status).toBe('error'); expect(result.timestamp).toBeDefined(); }); }); describe('listCourses', () => { test('should fetch courses with default parameters', async () => { const mockCourses = [ { id: 1, name: 'Course 1', course_code: 'CS101' }, { id: 2, name: 'Course 2', course_code: 'CS102' } ]; const mockInstance = mockedAxios.create.mock.results[0].value; mockInstance.get.mockResolvedValue({ data: mockCourses }); const result = await client.listCourses(); expect(mockInstance.get).toHaveBeenCalledWith('/courses', { params: { include: ['total_students', 'teachers', 'term', 'course_progress'], state: ['available', 'completed'] } }); expect(result).toEqual(mockCourses); }); test('should include ended courses when requested', async () => { const mockInstance = mockedAxios.create.mock.results[0].value; mockInstance.get.mockResolvedValue({ data: [] }); await client.listCourses(true); expect(mockInstance.get).toHaveBeenCalledWith('/courses', { params: { include: ['total_students', 'teachers', 'term', 'course_progress'] } }); }); }); describe('submitAssignment', () => { test('should submit text assignment', async () => { const mockSubmission = { id: 1, assignment_id: 1, user_id: 1 }; const mockInstance = mockedAxios.create.mock.results[0].value; mockInstance.post.mockResolvedValue({ data: mockSubmission }); const args = { course_id: 1, assignment_id: 1, submission_type: 'online_text_entry' as const, body: 'Test submission content' }; const result = await client.submitAssignment(args); expect(mockInstance.post).toHaveBeenCalledWith( '/courses/1/assignments/1/submissions', { submission: { submission_type: 'online_text_entry', body: 'Test submission content' } } ); expect(result).toEqual(mockSubmission); }); test('should submit URL assignment', async () => { const mockInstance = mockedAxios.create.mock.results[0].value; mockInstance.post.mockResolvedValue({ data: {} }); const args = { course_id: 1, assignment_id: 1, submission_type: 'online_url' as const, url: 'https://example.com' }; await client.submitAssignment(args); expect(mockInstance.post).toHaveBeenCalledWith( '/courses/1/assignments/1/submissions', { submission: { submission_type: 'online_url', url: 'https://example.com' } } ); }); }); describe('error handling', () => { test('should throw CanvasAPIError for API errors', async () => { const mockInstance = mockedAxios.create.mock.results[0].value; const mockError = { response: { status: 401, data: { message: 'Unauthorized' } } }; mockInstance.get.mockRejectedValue(mockError); await expect(client.getUserProfile()).rejects.toThrow(CanvasAPIError); }); }); }); // tests/integration.test.ts import { describe, test, expect, beforeAll, afterAll } from 'vitest'; import { CanvasClient } from '../src/client.js'; import { CanvasMCPServer } from '../src/index.js'; describe('Integration Tests', () => { let client: CanvasClient; let server: any; // CanvasMCPServer type beforeAll(async () => { // Only run integration tests if we have real credentials const token = process.env.CANVAS_TEST_TOKEN; const domain = process.env.CANVAS_TEST_DOMAIN; if (!token || !domain) { console.log('Skipping integration tests - no test credentials provided'); return; } client = new CanvasClient(token, domain); }); afterAll(async () => { // Cleanup if needed }); test('should connect to Canvas API', async () => { if (!client) return; const health = await client.healthCheck(); expect(health.status).toBe('ok'); expect(health.user).toBeDefined(); }); test('should list user courses', async () => { if (!client) return; const courses = await client.listCourses(); expect(Array.isArray(courses)).toBe(true); }); test('should get user profile', async () => { if (!client) return; const profile = await client.getUserProfile(); expect(profile).toBeDefined(); expect(profile.id).toBeDefined(); expect(profile.name).toBeDefined(); }); }); // tests/mcp-server.test.ts import { describe, test, expect, beforeEach, vi } from 'vitest'; import { MCPServerConfig } from '../src/types.js'; describe('MCP Server', () => { let config: MCPServerConfig; beforeEach(() => { config = { name: 'test-canvas-mcp', version: '1.0.0', canvas: { token: 'test-token', domain: 'test.instructure.com' } }; }); test('should validate configuration', () => { expect(config.name).toBe('test-canvas-mcp'); expect(config.canvas.token).toBe('test-token'); expect(config.canvas.domain).toBe('test.instructure.com'); }); test('should have default retry settings', () => { expect(config.canvas.maxRetries).toBeUndefined(); // Will use default }); }); // tests/setup.ts import { beforeAll, afterAll } from 'vitest'; import dotenv from 'dotenv'; beforeAll(async () => { // Load test environment variables dotenv.config({ path: '.env.test' }); }); afterAll(async () => { // Global cleanup }); // tests/utils/canvas-mock.ts export const mockCanvasResponse = (data: any, headers: Record<string, string> = {}) => ({ data, status: 200, statusText: 'OK', headers, config: {} }); export const mockPaginatedResponse = (data: any[], hasNext: boolean = false) => { const headers: Record<string, string> = {}; if (hasNext) { headers.link = '</api/v1/courses?page=2>; rel="next"'; } return mockCanvasResponse(data, headers); }; export const mockCourse = (id: number = 1) => ({ id, name: `Test Course ${id}`, course_code: `TEST${id}`, workflow_state: 'available', account_id: 1, start_at: '2024-01-01T00:00:00Z', end_at: '2024-12-31T23:59:59Z' }); export const mockAssignment = (id: number = 1, courseId: number = 1) => ({ id, course_id: courseId, name: `Test Assignment ${id}`, description: 'Test assignment description', due_at: '2024-06-15T23:59:59Z', points_possible: 100, position: 1, submission_types: ['online_text_entry'], html_url: `https://test.instructure.com/courses/${courseId}/assignments/${id}`, published: true, grading_type: 'points' }); export const mockSubmission = (id: number = 1, assignmentId: number = 1, userId: number = 1) => ({ id, assignment_id: assignmentId, user_id: userId, submitted_at: '2024-06-10T10:00:00Z', score: 85, grade: '85', attempt: 1, workflow_state: 'graded', late: false, missing: false }); // vitest.config.ts import { defineConfig } from 'vitest/config'; import path from 'path'; export default defineConfig({ test: { globals: true, environment: 'node', setupFiles: ['./tests/setup.ts'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/**', 'build/**', 'tests/**', '**/*.d.ts' ] }, testTimeout: 10000, hookTimeout: 10000 }, resolve: { alias: { '@': path.resolve(__dirname, './src') } } });

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/DMontgomery40/mcp-canvas-lms'

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