@charlie.act7/canvas-mcp-server 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import * as dotenv from "dotenv";
6
+ import { Command } from "commander";
7
+ import inquirer from "inquirer";
8
+ import chalk from "chalk";
9
+ import { CanvasClient } from "./services/canvas-client.js";
10
+ import { ConfigManager } from "./common/config-manager.js";
11
+ // Import Modular Components
12
+ import { courseTools } from "./tools/course-tools.js";
13
+ import { assignmentTools } from "./tools/assignment-tools.js";
14
+ import { quizTools } from "./tools/quiz-tools.js";
15
+ import { gradingTools } from "./tools/grading-tools.js";
16
+ import { communicationTools } from "./tools/communication-tools.js";
17
+ import { studentTools } from "./tools/student-tools.js";
18
+ import { quizQuestionTools } from "./tools/quiz-question-tools.js";
19
+ import { createTools } from "./tools/create-tools.js";
20
+ import { moduleTools } from "./tools/module-tools.js";
21
+ import { fileTools } from "./tools/file-tools.js";
22
+ import { configTools } from "./tools/config-tools.js";
23
+ import { rubricTools } from "./tools/rubric-tools.js";
24
+ import { calendarTools } from "./tools/calendar-tools.js";
25
+ import { groupTools } from "./tools/group-tools.js";
26
+ import { canvasResources } from "./resources/canvas-resources.js";
27
+ import { canvasPrompts } from "./prompts/canvas-prompts.js";
28
+ import { startHttpServer } from "./http-server.js";
29
+ // Load env vars if present
30
+ dotenv.config();
31
+ const configManager = new ConfigManager();
32
+ const program = new Command();
33
+ program
34
+ .name("canvas-mcp")
35
+ .description("MCP Server for Canvas LMS (Refactored)")
36
+ .version("1.1.0");
37
+ // --- CLI Commands ---
38
+ program
39
+ .command("config")
40
+ .description("Configure Canvas credentials interactively")
41
+ .action(async () => {
42
+ console.log(chalk.blue.bold("\nCanvas MCP Server Configuration\n"));
43
+ const answers = await inquirer.prompt([
44
+ {
45
+ type: "input",
46
+ name: "domain",
47
+ message: "Canvas Domain (e.g., uide.instructure.com):",
48
+ default: configManager.get("CANVAS_API_DOMAIN"),
49
+ validate: (input) => input.trim().length > 0
50
+ },
51
+ {
52
+ type: "password",
53
+ name: "token",
54
+ message: "Canvas API Token:",
55
+ mask: "*",
56
+ validate: (input) => input.trim().length > 0
57
+ }
58
+ ]);
59
+ configManager.set("CANVAS_API_DOMAIN", answers.domain);
60
+ configManager.set("CANVAS_API_TOKEN", answers.token);
61
+ console.log(chalk.green("\n✅ Configuration saved successfully!"));
62
+ console.log(chalk.gray(`Saved to: ${configManager.path}`));
63
+ });
64
+ // Helper to get authenticated client
65
+ function getClient() {
66
+ const token = process.env.CANVAS_API_TOKEN || configManager.get("CANVAS_API_TOKEN");
67
+ const domain = process.env.CANVAS_API_DOMAIN || configManager.get("CANVAS_API_DOMAIN");
68
+ if (!token || !domain) {
69
+ console.error(chalk.red("Error: Missing configuration."));
70
+ console.error(`Please run ${chalk.cyan("canvas-mcp config")} or set env vars.`);
71
+ process.exit(1);
72
+ }
73
+ return new CanvasClient(token, domain);
74
+ }
75
+ // NOTE: We could keep CLI commands for grading/auditing here calling the client directly,
76
+ // but for the sake of the MCP Server refactor, we are focusing on the 'start' command.
77
+ // I'll leave the 'start' command as the default.
78
+ program
79
+ .command("start", { isDefault: true })
80
+ .description("Start the MCP server (stdio mode)")
81
+ .action(async () => {
82
+ const client = getClient();
83
+ const server = new Server({
84
+ name: "canvas-lms-server",
85
+ version: "1.0.0",
86
+ }, {
87
+ capabilities: {
88
+ tools: {},
89
+ prompts: {},
90
+ resources: {},
91
+ },
92
+ });
93
+ // --- Aggregation ---
94
+ const allTools = [
95
+ ...courseTools,
96
+ ...assignmentTools,
97
+ ...quizTools,
98
+ ...gradingTools,
99
+ ...communicationTools,
100
+ ...studentTools,
101
+ ...quizQuestionTools,
102
+ ...createTools,
103
+ ...moduleTools,
104
+ ...fileTools,
105
+ ...configTools,
106
+ ...rubricTools,
107
+ ...calendarTools,
108
+ ...groupTools
109
+ ];
110
+ // --- Tool Handlers ---
111
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
112
+ return {
113
+ tools: allTools.map(t => t.tool)
114
+ };
115
+ });
116
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
117
+ const toolDef = allTools.find(t => t.name === request.params.name);
118
+ if (!toolDef) {
119
+ throw new Error(`Tool ${request.params.name} not found`);
120
+ }
121
+ try {
122
+ return await toolDef.handler(client, request.params.arguments);
123
+ }
124
+ catch (error) {
125
+ return {
126
+ content: [{ type: "text", text: `Error: ${error.message}` }],
127
+ isError: true
128
+ };
129
+ }
130
+ });
131
+ // --- Resource Handlers ---
132
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
133
+ return {
134
+ resources: canvasResources.list
135
+ };
136
+ });
137
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
138
+ const uri = new URL(request.params.uri);
139
+ return await canvasResources.read(uri, client);
140
+ });
141
+ // --- Prompt Handlers ---
142
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
143
+ return {
144
+ prompts: canvasPrompts.map(p => p.prompt)
145
+ };
146
+ });
147
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
148
+ const promptDef = canvasPrompts.find(p => p.name === request.params.name);
149
+ if (!promptDef) {
150
+ throw new Error("Prompt not found");
151
+ }
152
+ return await promptDef.handler(request.params.arguments);
153
+ });
154
+ const transport = new StdioServerTransport();
155
+ await server.connect(transport);
156
+ console.error("Canvas MCP Server running on stdio");
157
+ });
158
+ program
159
+ .command("serve-http")
160
+ .description("Start HTTP API (Fastify) for GPT Builder Actions")
161
+ .option("--host <host>", "Host to bind", process.env.HTTP_HOST || "0.0.0.0")
162
+ .option("--port <port>", "Port to bind", process.env.PORT || process.env.HTTP_PORT || "3000")
163
+ .action(async (options) => {
164
+ const client = getClient();
165
+ const port = Number.parseInt(options.port, 10);
166
+ await startHttpServer(client, options.host, port);
167
+ });
168
+ program.parse(process.argv);
@@ -0,0 +1,64 @@
1
+ export const canvasPrompts = [
2
+ {
3
+ name: "audit_course",
4
+ prompt: {
5
+ name: "audit_course",
6
+ description: "Audit a course to find students who are missing assignments.",
7
+ arguments: [
8
+ {
9
+ name: "course_id",
10
+ description: "The ID of the course to audit",
11
+ required: true
12
+ }
13
+ ]
14
+ },
15
+ handler: async (args) => {
16
+ const courseId = args?.course_id;
17
+ if (!courseId)
18
+ throw new Error("course_id is required");
19
+ return {
20
+ description: "Audit course for missing submissions",
21
+ messages: [
22
+ {
23
+ role: "user",
24
+ content: {
25
+ type: "text",
26
+ text: `Please audit course ${courseId} to find any students who have not submitted assignments that are due soon or past due. Use the canvas_audit_course tool.`
27
+ }
28
+ }
29
+ ]
30
+ };
31
+ }
32
+ },
33
+ {
34
+ name: "summarize_course",
35
+ prompt: {
36
+ name: "summarize_course",
37
+ description: "Summarize the content and structure of a course.",
38
+ arguments: [
39
+ {
40
+ name: "course_id",
41
+ description: "The ID of the course",
42
+ required: true
43
+ }
44
+ ]
45
+ },
46
+ handler: async (args) => {
47
+ const courseId = args?.course_id;
48
+ if (!courseId)
49
+ throw new Error("course_id is required");
50
+ return {
51
+ description: "Summarize course content",
52
+ messages: [
53
+ {
54
+ role: "user",
55
+ content: {
56
+ type: "text",
57
+ text: `Please provide a summary of the course ${courseId}. List the modules, pages, and files available to understand the course structure.`
58
+ }
59
+ }
60
+ ]
61
+ };
62
+ }
63
+ }
64
+ ];
@@ -0,0 +1,55 @@
1
+ const resources = [
2
+ {
3
+ uri: "canvas://courses/{course_id}/readme",
4
+ name: "Course Readme/Summary",
5
+ mimeType: "text/markdown",
6
+ description: "A summary of the course structure"
7
+ }
8
+ ];
9
+ // We'll manual dispatch for now to keep it simple as per original index.ts logic
10
+ async function readResource(uri, client) {
11
+ if (uri.protocol !== "canvas:") {
12
+ throw new Error("Invalid protocol");
13
+ }
14
+ const pathParts = uri.pathname.split("/").filter(Boolean);
15
+ // Expected patterns:
16
+ // courses/{id}/pages/{page_id}
17
+ // courses/{id}/files/{file_id}
18
+ // courses/{id}/readme
19
+ if (pathParts[0] !== "courses") {
20
+ throw new Error("Unknown resource type");
21
+ }
22
+ const courseId = parseInt(pathParts[1]);
23
+ const subResource = pathParts[2];
24
+ if (subResource === "readme") {
25
+ const modules = await client.getModules(courseId);
26
+ const assignments = await client.getAssignments(courseId);
27
+ const summary = `# Course ${courseId} Summary\n\n## Modules\n${modules.map((m) => `- ${m.name} (${m.items_count} items)`).join("\n")}\n\n## Assignments\n${assignments.map((a) => `- ${a.name} (Due: ${a.due_at})`).join("\n")}`;
28
+ return {
29
+ contents: [{
30
+ uri: uri.toString(),
31
+ mimeType: "text/markdown",
32
+ text: summary
33
+ }]
34
+ };
35
+ }
36
+ if (subResource === "pages") {
37
+ const pageId = pathParts[3];
38
+ const page = await client.getPage(courseId, pageId);
39
+ return {
40
+ contents: [{
41
+ uri: uri.toString(),
42
+ mimeType: "text/html",
43
+ text: page.body || ""
44
+ }]
45
+ };
46
+ }
47
+ // Note: Files are tricky because we need to stream content or return text.
48
+ // For now, we won't implement file binary reading in this text-based resource handler,
49
+ // but we could return the metadata or download URL.
50
+ throw new Error(`Resource type ${subResource} not supported yet`);
51
+ }
52
+ export const canvasResources = {
53
+ list: resources,
54
+ read: readResource
55
+ };