blackboard-upc 1.0.3 → 1.0.4
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 +8 -0
- package/package.json +1 -1
- package/src/mcp/server.ts +30 -16
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to `blackboard-upc` will be documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [1.0.3] — 2026-03-30
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Todos los tools MCP migrados de `server.tool()` a `server.registerTool()` (API nueva del SDK v1.28+)
|
|
11
|
+
- Elimina todos los warnings de TypeScript por uso de API deprecada
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
7
15
|
## [1.0.2] — 2026-03-30
|
|
8
16
|
|
|
9
17
|
### Fixed
|
package/package.json
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
4
6
|
import { loadSession, isSessionValid } from '../auth/session.js';
|
|
5
7
|
import { createClient } from '../api/client.js';
|
|
6
8
|
import {
|
|
@@ -156,32 +158,40 @@ export async function startMcpServer() {
|
|
|
156
158
|
server.registerTool(
|
|
157
159
|
'download_attachment',
|
|
158
160
|
{
|
|
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).
|
|
161
|
+
description: 'Download a file from a course content item and save it to disk. attachmentId can be a Blackboard attachment ID (for x-bb-file) or a full bbcswebdav URL (for x-bb-document embedded files). Saves to outputDir (default: current working directory).',
|
|
160
162
|
inputSchema: {
|
|
161
163
|
courseId: z.string().describe('Blackboard course ID'),
|
|
162
164
|
contentId: z.string().describe('Content item ID'),
|
|
163
165
|
attachmentId: z.string().describe('Attachment ID from list_attachments, or a full bbcswebdav URL for embedded files'),
|
|
166
|
+
filename: z.string().optional().describe('Filename to save as (e.g. displayName from list_attachments). Falls back to Content-Disposition header.'),
|
|
167
|
+
outputDir: z.string().optional().describe('Directory to save the file (default: current working directory)'),
|
|
164
168
|
},
|
|
165
169
|
},
|
|
166
|
-
async ({ courseId, contentId, attachmentId }) => {
|
|
170
|
+
async ({ courseId, contentId, attachmentId, filename, outputDir }) => {
|
|
167
171
|
const { client } = getClient();
|
|
168
172
|
|
|
169
|
-
// If attachmentId is a full URL (embedded file), download directly
|
|
170
173
|
const url = attachmentId.startsWith('http')
|
|
171
174
|
? attachmentId
|
|
172
175
|
: `/learn/api/public/v1/courses/${courseId}/contents/${contentId}/attachments/${attachmentId}/download`;
|
|
173
176
|
|
|
174
177
|
const r = await client.get(url, { responseType: 'arraybuffer', headers: { Accept: '*/*' } });
|
|
175
|
-
|
|
178
|
+
|
|
176
179
|
const contentDisposition = r.headers['content-disposition'] as string | undefined;
|
|
177
|
-
const
|
|
180
|
+
const detectedName = contentDisposition
|
|
178
181
|
? (contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/))?.[1]?.replace(/['"]/g, '').trim()
|
|
179
182
|
: undefined;
|
|
183
|
+
const finalName = filename ?? detectedName ?? 'download';
|
|
184
|
+
|
|
185
|
+
const dir = path.resolve(outputDir ?? process.cwd());
|
|
186
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
187
|
+
const dest = path.join(dir, finalName);
|
|
188
|
+
fs.writeFileSync(dest, Buffer.from(r.data));
|
|
189
|
+
|
|
180
190
|
const mimeType = (r.headers['content-type'] as string | undefined) ?? 'application/octet-stream';
|
|
181
191
|
return {
|
|
182
192
|
content: [{
|
|
183
193
|
type: 'text',
|
|
184
|
-
text: JSON.stringify({
|
|
194
|
+
text: JSON.stringify({ saved: dest, size: r.data.byteLength, mimeType }, null, 2),
|
|
185
195
|
}],
|
|
186
196
|
};
|
|
187
197
|
}
|
|
@@ -253,29 +263,33 @@ export async function startMcpServer() {
|
|
|
253
263
|
server.registerTool(
|
|
254
264
|
'download_file_url',
|
|
255
265
|
{
|
|
256
|
-
description: 'Download a file directly from a Blackboard bbcswebdav URL
|
|
266
|
+
description: 'Download a file directly from a Blackboard bbcswebdav URL and save it to disk. Saves to outputDir (default: current working directory).',
|
|
257
267
|
inputSchema: {
|
|
258
268
|
url: z.string().describe('Direct file URL from bbcswebdav (downloadUrl from list_attachments)'),
|
|
259
|
-
filename: z.string().optional().describe('
|
|
269
|
+
filename: z.string().optional().describe('Filename to save as (e.g. displayName from list_attachments)'),
|
|
270
|
+
outputDir: z.string().optional().describe('Directory to save the file (default: current working directory)'),
|
|
260
271
|
},
|
|
261
272
|
},
|
|
262
|
-
async ({ url, filename }) => {
|
|
273
|
+
async ({ url, filename, outputDir }) => {
|
|
263
274
|
const { client } = getClient();
|
|
264
|
-
const r = await client.get(url, {
|
|
265
|
-
|
|
266
|
-
headers: { Accept: '*/*' },
|
|
267
|
-
});
|
|
268
|
-
const b64 = Buffer.from(r.data).toString('base64');
|
|
275
|
+
const r = await client.get(url, { responseType: 'arraybuffer', headers: { Accept: '*/*' } });
|
|
276
|
+
|
|
269
277
|
const contentDisposition = r.headers['content-disposition'] as string | undefined;
|
|
270
278
|
const detectedName = contentDisposition
|
|
271
279
|
? (contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/))?.[1]?.replace(/['"]/g, '').trim()
|
|
272
280
|
: undefined;
|
|
273
|
-
const finalName = filename ?? detectedName ?? '
|
|
281
|
+
const finalName = filename ?? detectedName ?? 'download';
|
|
282
|
+
|
|
283
|
+
const dir = path.resolve(outputDir ?? process.cwd());
|
|
284
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
285
|
+
const dest = path.join(dir, finalName);
|
|
286
|
+
fs.writeFileSync(dest, Buffer.from(r.data));
|
|
287
|
+
|
|
274
288
|
const mimeType = (r.headers['content-type'] as string | undefined) ?? 'application/octet-stream';
|
|
275
289
|
return {
|
|
276
290
|
content: [{
|
|
277
291
|
type: 'text',
|
|
278
|
-
text: JSON.stringify({
|
|
292
|
+
text: JSON.stringify({ saved: dest, size: r.data.byteLength, mimeType }, null, 2),
|
|
279
293
|
}],
|
|
280
294
|
};
|
|
281
295
|
}
|