blackboard-upc 1.0.2 → 1.0.3
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 +12 -0
- package/package.json +1 -1
- package/src/mcp/server.ts +82 -53
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `blackboard-upc` will be documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [1.0.2] — 2026-03-30
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `list_attachments` — fallback automático a parseo del HTML del `body` para contenido tipo `x-bb-document` y `x-bb-lesson` (antes retornaba 400 en estos casos)
|
|
11
|
+
- `download_attachment` — ahora acepta URLs directas de `bbcswebdav` además de IDs estándar de Blackboard
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `download_file_url` (MCP) — nueva herramienta para descargar archivos embebidos directamente desde URLs de `bbcswebdav` con las cookies de sesión autenticadas
|
|
15
|
+
- Todos los tools de descarga ahora retornan `filename`, `mimeType` y `size` junto al contenido `base64`
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
7
19
|
## [1.0.1] — 2026-03-30
|
|
8
20
|
|
|
9
21
|
### Added
|
package/package.json
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -30,21 +30,21 @@ export async function startMcpServer() {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
// ── whoami ─────────────────────────────────────────────────────────────────
|
|
33
|
-
server.
|
|
33
|
+
server.registerTool('whoami', { description: 'Get the currently authenticated UPC student info' }, async () => {
|
|
34
34
|
const { client } = getClient();
|
|
35
35
|
const me = await getMe(client);
|
|
36
36
|
return { content: [{ type: 'text', text: JSON.stringify(me, null, 2) }] };
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
// ── system_version ─────────────────────────────────────────────────────────
|
|
40
|
-
server.
|
|
40
|
+
server.registerTool('system_version', { description: 'Get Blackboard Learn server version' }, async () => {
|
|
41
41
|
const { client } = getClient();
|
|
42
42
|
const v = await getSystemVersion(client);
|
|
43
43
|
return { content: [{ type: 'text', text: JSON.stringify(v, null, 2) }] };
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
// ── list_courses ────────────────────────────────────────────────────────────
|
|
47
|
-
server.
|
|
47
|
+
server.registerTool('list_courses', { description: 'List all enrolled courses for the current student' }, async () => {
|
|
48
48
|
const { client, session } = getClient();
|
|
49
49
|
let userId = session.userId;
|
|
50
50
|
if (!userId) { const me = await getMe(client); userId = me.id; }
|
|
@@ -53,10 +53,12 @@ export async function startMcpServer() {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
// ── get_course ──────────────────────────────────────────────────────────────
|
|
56
|
-
server.
|
|
56
|
+
server.registerTool(
|
|
57
57
|
'get_course',
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
{
|
|
59
|
+
description: 'Get details of a specific course by its Blackboard ID (e.g. _529580_1)',
|
|
60
|
+
inputSchema: { courseId: z.string().describe('Blackboard course ID like _529580_1') },
|
|
61
|
+
},
|
|
60
62
|
async ({ courseId }) => {
|
|
61
63
|
const { client } = getClient();
|
|
62
64
|
const data = await getCourse(client, courseId);
|
|
@@ -65,12 +67,14 @@ export async function startMcpServer() {
|
|
|
65
67
|
);
|
|
66
68
|
|
|
67
69
|
// ── list_contents ───────────────────────────────────────────────────────────
|
|
68
|
-
server.
|
|
70
|
+
server.registerTool(
|
|
69
71
|
'list_contents',
|
|
70
|
-
'List content items inside a course or folder. Use parentId to navigate into subfolders.',
|
|
71
72
|
{
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
description: 'List content items inside a course or folder. Use parentId to navigate into subfolders.',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
courseId: z.string().describe('Blackboard course ID'),
|
|
76
|
+
parentId: z.string().optional().describe('Parent folder content ID (omit for root level)'),
|
|
77
|
+
},
|
|
74
78
|
},
|
|
75
79
|
async ({ courseId, parentId }) => {
|
|
76
80
|
const { client } = getClient();
|
|
@@ -80,10 +84,12 @@ export async function startMcpServer() {
|
|
|
80
84
|
);
|
|
81
85
|
|
|
82
86
|
// ── list_announcements ──────────────────────────────────────────────────────
|
|
83
|
-
server.
|
|
87
|
+
server.registerTool(
|
|
84
88
|
'list_announcements',
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
{
|
|
90
|
+
description: 'List recent announcements for a course',
|
|
91
|
+
inputSchema: { courseId: z.string().describe('Blackboard course ID') },
|
|
92
|
+
},
|
|
87
93
|
async ({ courseId }) => {
|
|
88
94
|
const { client } = getClient();
|
|
89
95
|
const data = await getCourseAnnouncements(client, courseId);
|
|
@@ -92,10 +98,12 @@ export async function startMcpServer() {
|
|
|
92
98
|
);
|
|
93
99
|
|
|
94
100
|
// ── list_assignments ────────────────────────────────────────────────────────
|
|
95
|
-
server.
|
|
101
|
+
server.registerTool(
|
|
96
102
|
'list_assignments',
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
{
|
|
104
|
+
description: 'List assignments and tasks in a course with due dates, scores and submission status',
|
|
105
|
+
inputSchema: { courseId: z.string().describe('Blackboard course ID') },
|
|
106
|
+
},
|
|
99
107
|
async ({ courseId }) => {
|
|
100
108
|
const { client } = getClient();
|
|
101
109
|
const data = await listAssignments(client, courseId);
|
|
@@ -104,12 +112,14 @@ export async function startMcpServer() {
|
|
|
104
112
|
);
|
|
105
113
|
|
|
106
114
|
// ── list_attempts ───────────────────────────────────────────────────────────
|
|
107
|
-
server.
|
|
115
|
+
server.registerTool(
|
|
108
116
|
'list_attempts',
|
|
109
|
-
'List submission attempts for a specific assignment (gradebook column)',
|
|
110
117
|
{
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
description: 'List submission attempts for a specific assignment (gradebook column)',
|
|
119
|
+
inputSchema: {
|
|
120
|
+
courseId: z.string().describe('Blackboard course ID'),
|
|
121
|
+
columnId: z.string().describe('Gradebook column ID (assignment ID)'),
|
|
122
|
+
},
|
|
113
123
|
},
|
|
114
124
|
async ({ courseId, columnId }) => {
|
|
115
125
|
const { client } = getClient();
|
|
@@ -119,10 +129,12 @@ export async function startMcpServer() {
|
|
|
119
129
|
);
|
|
120
130
|
|
|
121
131
|
// ── get_grades ──────────────────────────────────────────────────────────────
|
|
122
|
-
server.
|
|
132
|
+
server.registerTool(
|
|
123
133
|
'get_grades',
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
{
|
|
135
|
+
description: 'Get all grades for the current student in a course',
|
|
136
|
+
inputSchema: { courseId: z.string().describe('Blackboard course ID') },
|
|
137
|
+
},
|
|
126
138
|
async ({ courseId }) => {
|
|
127
139
|
const { client, session } = getClient();
|
|
128
140
|
let userId = session.userId;
|
|
@@ -141,13 +153,15 @@ export async function startMcpServer() {
|
|
|
141
153
|
);
|
|
142
154
|
|
|
143
155
|
// ── download_attachment ─────────────────────────────────────────────────────
|
|
144
|
-
server.
|
|
156
|
+
server.registerTool(
|
|
145
157
|
'download_attachment',
|
|
146
|
-
'Download a file from a course content item. attachmentId can be a Blackboard attachment ID (for x-bb-file) or a full bbcswebdav URL (for x-bb-document embedded files). Returns base64-encoded content.',
|
|
147
158
|
{
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
description: 'Download a file from a course content item. attachmentId can be a Blackboard attachment ID (for x-bb-file) or a full bbcswebdav URL (for x-bb-document embedded files). Returns base64-encoded content.',
|
|
160
|
+
inputSchema: {
|
|
161
|
+
courseId: z.string().describe('Blackboard course ID'),
|
|
162
|
+
contentId: z.string().describe('Content item ID'),
|
|
163
|
+
attachmentId: z.string().describe('Attachment ID from list_attachments, or a full bbcswebdav URL for embedded files'),
|
|
164
|
+
},
|
|
151
165
|
},
|
|
152
166
|
async ({ courseId, contentId, attachmentId }) => {
|
|
153
167
|
const { client } = getClient();
|
|
@@ -174,12 +188,14 @@ export async function startMcpServer() {
|
|
|
174
188
|
);
|
|
175
189
|
|
|
176
190
|
// ── list_attachments ────────────────────────────────────────────────────────
|
|
177
|
-
server.
|
|
191
|
+
server.registerTool(
|
|
178
192
|
'list_attachments',
|
|
179
|
-
'List file attachments for a course content item. Works for x-bb-file (REST API) and x-bb-document (embedded files in body HTML).',
|
|
180
193
|
{
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
description: 'List file attachments for a course content item. Works for x-bb-file (REST API) and x-bb-document (embedded files in body HTML).',
|
|
195
|
+
inputSchema: {
|
|
196
|
+
courseId: z.string().describe('Blackboard course ID'),
|
|
197
|
+
contentId: z.string().describe('Content item ID'),
|
|
198
|
+
},
|
|
183
199
|
},
|
|
184
200
|
async ({ courseId, contentId }) => {
|
|
185
201
|
const { client } = getClient();
|
|
@@ -199,15 +215,22 @@ export async function startMcpServer() {
|
|
|
199
215
|
`/learn/api/public/v1/courses/${courseId}/contents/${contentId}`
|
|
200
216
|
);
|
|
201
217
|
const body: string = r.data?.body ?? '';
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
|
|
219
|
+
// Extract <a> tags with data-bbfile — capture both the JSON metadata and the href (signed download URL)
|
|
220
|
+
// Handle both attribute orderings: data-bbfile...href and href...data-bbfile
|
|
221
|
+
const filePattern = /data-bbfile="([^"]+)"[^<]*?href="([^"]+)"|href="([^"]+)"[^<]*?data-bbfile="([^"]+)"/g;
|
|
222
|
+
const anchorMatches = [...body.matchAll(filePattern)];
|
|
223
|
+
const files = anchorMatches.map((m) => {
|
|
224
|
+
const bbfileRaw = m[1] ?? m[4];
|
|
225
|
+
const hrefRaw = m[2] ?? m[3];
|
|
204
226
|
try {
|
|
205
|
-
const meta = JSON.parse(
|
|
227
|
+
const meta = JSON.parse(bbfileRaw.replace(/"/g, '"'));
|
|
228
|
+
const downloadUrl = hrefRaw ? hrefRaw.replace(/&/g, '&') : (meta.resourceUrl ?? null);
|
|
206
229
|
return {
|
|
207
230
|
type: 'embedded',
|
|
208
231
|
displayName: meta.displayName ?? meta.linkName ?? 'unknown',
|
|
209
232
|
mimeType: meta.mimeType ?? 'application/octet-stream',
|
|
210
|
-
|
|
233
|
+
downloadUrl,
|
|
211
234
|
};
|
|
212
235
|
} catch {
|
|
213
236
|
return null;
|
|
@@ -218,7 +241,7 @@ export async function startMcpServer() {
|
|
|
218
241
|
content: [{
|
|
219
242
|
type: 'text',
|
|
220
243
|
text: JSON.stringify(
|
|
221
|
-
{ type: 'embedded_files', note: '
|
|
244
|
+
{ type: 'embedded_files', note: 'Pass downloadUrl as attachmentId to download_attachment', results: files },
|
|
222
245
|
null, 2
|
|
223
246
|
),
|
|
224
247
|
}],
|
|
@@ -227,12 +250,14 @@ export async function startMcpServer() {
|
|
|
227
250
|
);
|
|
228
251
|
|
|
229
252
|
// ── download_file_url ───────────────────────────────────────────────────────
|
|
230
|
-
server.
|
|
253
|
+
server.registerTool(
|
|
231
254
|
'download_file_url',
|
|
232
|
-
'Download a file directly from a Blackboard bbcswebdav URL (for x-bb-document embedded files). Returns base64-encoded content and filename.',
|
|
233
255
|
{
|
|
234
|
-
|
|
235
|
-
|
|
256
|
+
description: 'Download a file directly from a Blackboard bbcswebdav URL (for x-bb-document embedded files). Returns base64-encoded content and filename.',
|
|
257
|
+
inputSchema: {
|
|
258
|
+
url: z.string().describe('Direct file URL from bbcswebdav (downloadUrl from list_attachments)'),
|
|
259
|
+
filename: z.string().optional().describe('Desired filename for saving the file'),
|
|
260
|
+
},
|
|
236
261
|
},
|
|
237
262
|
async ({ url, filename }) => {
|
|
238
263
|
const { client } = getClient();
|
|
@@ -257,14 +282,16 @@ export async function startMcpServer() {
|
|
|
257
282
|
);
|
|
258
283
|
|
|
259
284
|
// ── submit_attempt ──────────────────────────────────────────────────────────
|
|
260
|
-
server.
|
|
285
|
+
server.registerTool(
|
|
261
286
|
'submit_attempt',
|
|
262
|
-
'Submit an assignment attempt. ALWAYS confirm with the user before submitting.',
|
|
263
287
|
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
288
|
+
description: 'Submit an assignment attempt. ALWAYS confirm with the user before submitting.',
|
|
289
|
+
inputSchema: {
|
|
290
|
+
courseId: z.string().describe('Blackboard course ID'),
|
|
291
|
+
columnId: z.string().describe('Assignment (gradebook column) ID'),
|
|
292
|
+
studentComments: z.string().optional().describe('Comment to the instructor'),
|
|
293
|
+
studentSubmission: z.string().optional().describe('Text body of the submission'),
|
|
294
|
+
},
|
|
268
295
|
},
|
|
269
296
|
async ({ courseId, columnId, studentComments, studentSubmission }) => {
|
|
270
297
|
const { client } = getClient();
|
|
@@ -278,14 +305,16 @@ export async function startMcpServer() {
|
|
|
278
305
|
);
|
|
279
306
|
|
|
280
307
|
// ── raw_api ─────────────────────────────────────────────────────────────────
|
|
281
|
-
server.
|
|
308
|
+
server.registerTool(
|
|
282
309
|
'raw_api',
|
|
283
|
-
'Make a raw REST API call to Blackboard Learn. Use for any endpoint not covered by other tools.',
|
|
284
310
|
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
311
|
+
description: 'Make a raw REST API call to Blackboard Learn. Use for any endpoint not covered by other tools.',
|
|
312
|
+
inputSchema: {
|
|
313
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).describe('HTTP method'),
|
|
314
|
+
path: z.string().describe('API path, e.g. /learn/api/public/v1/users/me'),
|
|
315
|
+
query: z.string().optional().describe('Query string, e.g. limit=10&offset=0'),
|
|
316
|
+
body: z.string().optional().describe('JSON body string for POST/PUT/PATCH'),
|
|
317
|
+
},
|
|
289
318
|
},
|
|
290
319
|
async ({ method, path, query, body }) => {
|
|
291
320
|
const { client } = getClient();
|