blackboard-upc 1.0.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/CHANGELOG.md +60 -0
- package/CLAUDE.md +70 -0
- package/README.md +222 -0
- package/package.json +48 -0
- package/run.js +8 -0
- package/src/api/assignments.ts +135 -0
- package/src/api/client.ts +41 -0
- package/src/api/courses.ts +110 -0
- package/src/auth/login.ts +139 -0
- package/src/auth/session.ts +44 -0
- package/src/commands/api-docs.ts +100 -0
- package/src/commands/assignments.ts +237 -0
- package/src/commands/courses.ts +284 -0
- package/src/commands/download.ts +149 -0
- package/src/commands/login.ts +68 -0
- package/src/index.ts +109 -0
- package/src/mcp/server.ts +222 -0
- package/src/types/index.ts +104 -0
- package/src/ui/theme.ts +34 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { loadSession, isSessionValid } from '../auth/session.js';
|
|
5
|
+
import { createClient } from '../api/client.js';
|
|
6
|
+
import {
|
|
7
|
+
getMe,
|
|
8
|
+
getMyCourses,
|
|
9
|
+
getCourse,
|
|
10
|
+
getCourseContents,
|
|
11
|
+
getCourseAnnouncements,
|
|
12
|
+
getGrades,
|
|
13
|
+
getGradeColumns,
|
|
14
|
+
getSystemVersion,
|
|
15
|
+
} from '../api/courses.js';
|
|
16
|
+
import { listAssignments, listAttempts, submitAttempt, uploadFile } from '../api/assignments.js';
|
|
17
|
+
|
|
18
|
+
function getClient() {
|
|
19
|
+
const session = loadSession();
|
|
20
|
+
if (!isSessionValid(session)) {
|
|
21
|
+
throw new Error('Not authenticated. Ask the user to run: blackboard login');
|
|
22
|
+
}
|
|
23
|
+
return { client: createClient(session!), session: session! };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function startMcpServer() {
|
|
27
|
+
const server = new McpServer({
|
|
28
|
+
name: 'blackboard-upc',
|
|
29
|
+
version: '1.0.0',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ── whoami ─────────────────────────────────────────────────────────────────
|
|
33
|
+
server.tool('whoami', 'Get the currently authenticated UPC student info', {}, async () => {
|
|
34
|
+
const { client } = getClient();
|
|
35
|
+
const me = await getMe(client);
|
|
36
|
+
return { content: [{ type: 'text', text: JSON.stringify(me, null, 2) }] };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ── system_version ─────────────────────────────────────────────────────────
|
|
40
|
+
server.tool('system_version', 'Get Blackboard Learn server version', {}, async () => {
|
|
41
|
+
const { client } = getClient();
|
|
42
|
+
const v = await getSystemVersion(client);
|
|
43
|
+
return { content: [{ type: 'text', text: JSON.stringify(v, null, 2) }] };
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── list_courses ────────────────────────────────────────────────────────────
|
|
47
|
+
server.tool('list_courses', 'List all enrolled courses for the current student', {}, async () => {
|
|
48
|
+
const { client, session } = getClient();
|
|
49
|
+
let userId = session.userId;
|
|
50
|
+
if (!userId) { const me = await getMe(client); userId = me.id; }
|
|
51
|
+
const data = await getMyCourses(client, userId!, { limit: 50 });
|
|
52
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ── get_course ──────────────────────────────────────────────────────────────
|
|
56
|
+
server.tool(
|
|
57
|
+
'get_course',
|
|
58
|
+
'Get details of a specific course by its Blackboard ID (e.g. _529580_1)',
|
|
59
|
+
{ courseId: z.string().describe('Blackboard course ID like _529580_1') },
|
|
60
|
+
async ({ courseId }) => {
|
|
61
|
+
const { client } = getClient();
|
|
62
|
+
const data = await getCourse(client, courseId);
|
|
63
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// ── list_contents ───────────────────────────────────────────────────────────
|
|
68
|
+
server.tool(
|
|
69
|
+
'list_contents',
|
|
70
|
+
'List content items inside a course or folder. Use parentId to navigate into subfolders.',
|
|
71
|
+
{
|
|
72
|
+
courseId: z.string().describe('Blackboard course ID'),
|
|
73
|
+
parentId: z.string().optional().describe('Parent folder content ID (omit for root level)'),
|
|
74
|
+
},
|
|
75
|
+
async ({ courseId, parentId }) => {
|
|
76
|
+
const { client } = getClient();
|
|
77
|
+
const data = await getCourseContents(client, courseId, parentId);
|
|
78
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// ── list_announcements ──────────────────────────────────────────────────────
|
|
83
|
+
server.tool(
|
|
84
|
+
'list_announcements',
|
|
85
|
+
'List recent announcements for a course',
|
|
86
|
+
{ courseId: z.string().describe('Blackboard course ID') },
|
|
87
|
+
async ({ courseId }) => {
|
|
88
|
+
const { client } = getClient();
|
|
89
|
+
const data = await getCourseAnnouncements(client, courseId);
|
|
90
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// ── list_assignments ────────────────────────────────────────────────────────
|
|
95
|
+
server.tool(
|
|
96
|
+
'list_assignments',
|
|
97
|
+
'List assignments and tasks in a course with due dates, scores and submission status',
|
|
98
|
+
{ courseId: z.string().describe('Blackboard course ID') },
|
|
99
|
+
async ({ courseId }) => {
|
|
100
|
+
const { client } = getClient();
|
|
101
|
+
const data = await listAssignments(client, courseId);
|
|
102
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// ── list_attempts ───────────────────────────────────────────────────────────
|
|
107
|
+
server.tool(
|
|
108
|
+
'list_attempts',
|
|
109
|
+
'List submission attempts for a specific assignment (gradebook column)',
|
|
110
|
+
{
|
|
111
|
+
courseId: z.string().describe('Blackboard course ID'),
|
|
112
|
+
columnId: z.string().describe('Gradebook column ID (assignment ID)'),
|
|
113
|
+
},
|
|
114
|
+
async ({ courseId, columnId }) => {
|
|
115
|
+
const { client } = getClient();
|
|
116
|
+
const data = await listAttempts(client, courseId, columnId);
|
|
117
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// ── get_grades ──────────────────────────────────────────────────────────────
|
|
122
|
+
server.tool(
|
|
123
|
+
'get_grades',
|
|
124
|
+
'Get all grades for the current student in a course',
|
|
125
|
+
{ courseId: z.string().describe('Blackboard course ID') },
|
|
126
|
+
async ({ courseId }) => {
|
|
127
|
+
const { client, session } = getClient();
|
|
128
|
+
let userId = session.userId;
|
|
129
|
+
if (!userId) { const me = await getMe(client); userId = me.id; }
|
|
130
|
+
const [columns, grades] = await Promise.all([
|
|
131
|
+
getGradeColumns(client, courseId),
|
|
132
|
+
getGrades(client, courseId, userId!),
|
|
133
|
+
]);
|
|
134
|
+
return {
|
|
135
|
+
content: [{
|
|
136
|
+
type: 'text',
|
|
137
|
+
text: JSON.stringify({ columns: columns.results, grades: grades.results }, null, 2),
|
|
138
|
+
}],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// ── download_attachment ─────────────────────────────────────────────────────
|
|
144
|
+
server.tool(
|
|
145
|
+
'download_attachment',
|
|
146
|
+
'Download a file from a course content item. Returns base64-encoded content.',
|
|
147
|
+
{
|
|
148
|
+
courseId: z.string().describe('Blackboard course ID'),
|
|
149
|
+
contentId: z.string().describe('Content item ID'),
|
|
150
|
+
attachmentId: z.string().describe('Attachment ID from list_attachments'),
|
|
151
|
+
},
|
|
152
|
+
async ({ courseId, contentId, attachmentId }) => {
|
|
153
|
+
const { client } = getClient();
|
|
154
|
+
const r = await client.get(
|
|
155
|
+
`/learn/api/public/v1/courses/${courseId}/contents/${contentId}/attachments/${attachmentId}/download`,
|
|
156
|
+
{ responseType: 'arraybuffer' }
|
|
157
|
+
);
|
|
158
|
+
const b64 = Buffer.from(r.data).toString('base64');
|
|
159
|
+
return { content: [{ type: 'text', text: b64 }] };
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// ── list_attachments ────────────────────────────────────────────────────────
|
|
164
|
+
server.tool(
|
|
165
|
+
'list_attachments',
|
|
166
|
+
'List file attachments for a course content item',
|
|
167
|
+
{
|
|
168
|
+
courseId: z.string().describe('Blackboard course ID'),
|
|
169
|
+
contentId: z.string().describe('Content item ID'),
|
|
170
|
+
},
|
|
171
|
+
async ({ courseId, contentId }) => {
|
|
172
|
+
const { client } = getClient();
|
|
173
|
+
const r = await client.get(
|
|
174
|
+
`/learn/api/public/v1/courses/${courseId}/contents/${contentId}/attachments`
|
|
175
|
+
);
|
|
176
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data, null, 2) }] };
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ── submit_attempt ──────────────────────────────────────────────────────────
|
|
181
|
+
server.tool(
|
|
182
|
+
'submit_attempt',
|
|
183
|
+
'Submit an assignment attempt. ALWAYS confirm with the user before submitting.',
|
|
184
|
+
{
|
|
185
|
+
courseId: z.string().describe('Blackboard course ID'),
|
|
186
|
+
columnId: z.string().describe('Assignment (gradebook column) ID'),
|
|
187
|
+
studentComments: z.string().optional().describe('Comment to the instructor'),
|
|
188
|
+
studentSubmission: z.string().optional().describe('Text body of the submission'),
|
|
189
|
+
},
|
|
190
|
+
async ({ courseId, columnId, studentComments, studentSubmission }) => {
|
|
191
|
+
const { client } = getClient();
|
|
192
|
+
const attempt = await submitAttempt(client, courseId, columnId, {
|
|
193
|
+
studentComments,
|
|
194
|
+
studentSubmission,
|
|
195
|
+
status: 'NeedsGrading',
|
|
196
|
+
});
|
|
197
|
+
return { content: [{ type: 'text', text: JSON.stringify(attempt, null, 2) }] };
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// ── raw_api ─────────────────────────────────────────────────────────────────
|
|
202
|
+
server.tool(
|
|
203
|
+
'raw_api',
|
|
204
|
+
'Make a raw REST API call to Blackboard Learn. Use for any endpoint not covered by other tools.',
|
|
205
|
+
{
|
|
206
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'),
|
|
207
|
+
path: z.string().describe('API path, e.g. /learn/api/public/v1/users/me'),
|
|
208
|
+
query: z.string().optional().describe('Query string, e.g. limit=10&offset=0'),
|
|
209
|
+
body: z.string().optional().describe('JSON body string for POST/PUT/PATCH'),
|
|
210
|
+
},
|
|
211
|
+
async ({ method, path, query, body }) => {
|
|
212
|
+
const { client } = getClient();
|
|
213
|
+
const params = query ? Object.fromEntries(new URLSearchParams(query)) : undefined;
|
|
214
|
+
const data = body ? JSON.parse(body) : undefined;
|
|
215
|
+
const r = await client.request({ method: method.toLowerCase() as any, url: path, params, data });
|
|
216
|
+
return { content: [{ type: 'text', text: JSON.stringify(r.data, null, 2) }] };
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const transport = new StdioServerTransport();
|
|
221
|
+
await server.connect(transport);
|
|
222
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export interface Session {
|
|
2
|
+
cookies: Cookie[];
|
|
3
|
+
xsrfToken: string;
|
|
4
|
+
userId?: string;
|
|
5
|
+
userName?: string;
|
|
6
|
+
expiresAt: number; // unix ms
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Cookie {
|
|
10
|
+
name: string;
|
|
11
|
+
value: string;
|
|
12
|
+
domain: string;
|
|
13
|
+
path: string;
|
|
14
|
+
expires?: number;
|
|
15
|
+
httpOnly?: boolean;
|
|
16
|
+
secure?: boolean;
|
|
17
|
+
sameSite?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Course {
|
|
21
|
+
id: string;
|
|
22
|
+
courseId: string;
|
|
23
|
+
name: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
externalId?: string;
|
|
26
|
+
created?: string;
|
|
27
|
+
modified?: string;
|
|
28
|
+
term?: { id: string; name: string };
|
|
29
|
+
availability?: { available: string };
|
|
30
|
+
enrollment?: { type: string };
|
|
31
|
+
ultraStatus?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UserCourse {
|
|
35
|
+
userId: string;
|
|
36
|
+
courseId: string;
|
|
37
|
+
dataSourceId?: string;
|
|
38
|
+
created?: string;
|
|
39
|
+
modified?: string;
|
|
40
|
+
availability?: { available: string };
|
|
41
|
+
courseRoleId?: string;
|
|
42
|
+
lastAccessDate?: string;
|
|
43
|
+
childCourseId?: string;
|
|
44
|
+
course?: Course;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CourseContent {
|
|
48
|
+
id: string;
|
|
49
|
+
parentId?: string;
|
|
50
|
+
title: string;
|
|
51
|
+
body?: string;
|
|
52
|
+
created?: string;
|
|
53
|
+
modified?: string;
|
|
54
|
+
position?: number;
|
|
55
|
+
hasChildren?: boolean;
|
|
56
|
+
launchInNewWindow?: boolean;
|
|
57
|
+
availability?: { available: string; adaptiveRelease?: object };
|
|
58
|
+
contentHandler?: {
|
|
59
|
+
id: string;
|
|
60
|
+
url?: string;
|
|
61
|
+
file?: { uploadId: string; fileName: string; mimeType: string; size: number };
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface Announcement {
|
|
66
|
+
id: string;
|
|
67
|
+
title: string;
|
|
68
|
+
body: string;
|
|
69
|
+
creator?: string;
|
|
70
|
+
created?: string;
|
|
71
|
+
modified?: string;
|
|
72
|
+
availability?: { available: string; duration?: { type: string; start?: string; end?: string } };
|
|
73
|
+
showReorder?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface GradeColumn {
|
|
77
|
+
id: string;
|
|
78
|
+
externalId?: string;
|
|
79
|
+
name: string;
|
|
80
|
+
displayName?: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
externalGrade?: boolean;
|
|
83
|
+
created?: string;
|
|
84
|
+
score?: { possible: number; decimalPlaces: number };
|
|
85
|
+
availability?: { available: string };
|
|
86
|
+
gradingPeriodId?: string;
|
|
87
|
+
contentId?: string;
|
|
88
|
+
formula?: { formulaType: string };
|
|
89
|
+
includeInCalculations?: boolean;
|
|
90
|
+
showStatisticsToStudents?: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface PaginatedResponse<T> {
|
|
94
|
+
results: T[];
|
|
95
|
+
paging?: {
|
|
96
|
+
nextPage?: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface BBError {
|
|
101
|
+
status: number;
|
|
102
|
+
message: string;
|
|
103
|
+
extraInfo?: string;
|
|
104
|
+
}
|
package/src/ui/theme.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
// UPC brand red
|
|
4
|
+
export const upcRed = chalk.hex('#E31837');
|
|
5
|
+
export const upcRedBold = chalk.hex('#E31837').bold;
|
|
6
|
+
export const dim = chalk.dim;
|
|
7
|
+
|
|
8
|
+
export const ok = (s: string) => chalk.green(`✓ ${s}`);
|
|
9
|
+
export const fail = (s: string) => chalk.red(`✗ ${s}`);
|
|
10
|
+
export const warn = (s: string) => chalk.yellow(`⚠ ${s}`);
|
|
11
|
+
export const hint = (s: string) => chalk.cyan(s);
|
|
12
|
+
export const bold = (s: string) => chalk.bold(s);
|
|
13
|
+
export const gray = (s: string) => chalk.gray(s);
|
|
14
|
+
|
|
15
|
+
export const BANNER = `
|
|
16
|
+
${upcRed(' ██████ ██ █████ ██████ ██ ██ ██████ ██████ █████ ██████ ██████ ')}
|
|
17
|
+
${upcRed(' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ')}
|
|
18
|
+
${upcRed(' ██████ ██ ███████ ██ █████ ██████ ██ ██ ███████ ██████ ██ ██ ')}
|
|
19
|
+
${upcRed(' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ')}
|
|
20
|
+
${upcRed(' ██████ ███████ ██ ██ ██████ ██ ██ ██████ ██████ ██ ██ ██ ██ ██████ ')}
|
|
21
|
+
${chalk.dim('CLI no oficial para UPC Aula Virtual · Blackboard Learn')}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export function whatNext() {
|
|
25
|
+
console.log(`
|
|
26
|
+
${chalk.bold('¿Qué puedo hacer ahora?')}
|
|
27
|
+
|
|
28
|
+
${hint('blackboard courses list')} ver tus cursos del ciclo
|
|
29
|
+
${hint('blackboard assignments list <courseId>')} ver tareas pendientes y notas
|
|
30
|
+
${hint('blackboard courses contents <courseId>')} explorar materiales de un curso
|
|
31
|
+
${hint('blackboard download-folder <id> <fid>')} descargar toda una carpeta
|
|
32
|
+
${hint('blackboard status')} estado de sesión y servidor
|
|
33
|
+
`);
|
|
34
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules"]
|
|
15
|
+
}
|