@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/README.md +117 -0
- package/dist/common/config-manager.js +28 -0
- package/dist/common/helpers.js +46 -0
- package/dist/common/tool-model.js +1 -0
- package/dist/common/types.js +1 -0
- package/dist/http-server.js +760 -0
- package/dist/index.js +168 -0
- package/dist/prompts/canvas-prompts.js +64 -0
- package/dist/resources/canvas-resources.js +55 -0
- package/dist/services/canvas-client.js +459 -0
- package/dist/tools/assignment-tools.js +626 -0
- package/dist/tools/calendar-tools.js +240 -0
- package/dist/tools/communication-tools.js +130 -0
- package/dist/tools/config-tools.js +39 -0
- package/dist/tools/course-tools.js +123 -0
- package/dist/tools/create-tools.js +76 -0
- package/dist/tools/file-tools.js +229 -0
- package/dist/tools/grading-tools.js +187 -0
- package/dist/tools/group-tools.js +192 -0
- package/dist/tools/module-tools.js +269 -0
- package/dist/tools/question-bank-tools.js +238 -0
- package/dist/tools/quiz-question-tools.js +303 -0
- package/dist/tools/quiz-tools.js +184 -0
- package/dist/tools/rubric-tools.js +145 -0
- package/dist/tools/student-tools.js +172 -0
- package/package.json +65 -0
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
|
+
};
|