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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blackboard-upc",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "CLI no oficial para UPC Aula Virtual (Blackboard Learn) — acceso desde la terminal y MCP para Claude",
5
5
  "main": "run.js",
6
6
  "bin": {
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). Returns base64-encoded content.',
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
- const b64 = Buffer.from(r.data).toString('base64');
178
+
176
179
  const contentDisposition = r.headers['content-disposition'] as string | undefined;
177
- const filename = contentDisposition
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({ filename: filename ?? 'file', mimeType, size: r.data.byteLength, base64: b64 }, null, 2),
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 (for x-bb-document embedded files). Returns base64-encoded content and filename.',
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('Desired filename for saving the file'),
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
- responseType: 'arraybuffer',
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 ?? 'file';
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({ filename: finalName, mimeType, size: r.data.byteLength, base64: b64 }, null, 2),
292
+ text: JSON.stringify({ saved: dest, size: r.data.byteLength, mimeType }, null, 2),
279
293
  }],
280
294
  };
281
295
  }