@josetra/dutic-mcp 0.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/dist/cli/format.js +69 -0
  4. package/dist/cli/format.js.map +1 -0
  5. package/dist/cli/index.js +426 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli/setup.js +121 -0
  8. package/dist/cli/setup.js.map +1 -0
  9. package/dist/cli/ui.js +111 -0
  10. package/dist/cli/ui.js.map +1 -0
  11. package/dist/core/auth.js +47 -0
  12. package/dist/core/auth.js.map +1 -0
  13. package/dist/core/browser.js +56 -0
  14. package/dist/core/browser.js.map +1 -0
  15. package/dist/core/config.js +34 -0
  16. package/dist/core/config.js.map +1 -0
  17. package/dist/core/dates.js +43 -0
  18. package/dist/core/dates.js.map +1 -0
  19. package/dist/core/errors.js +25 -0
  20. package/dist/core/errors.js.map +1 -0
  21. package/dist/core/http.js +43 -0
  22. package/dist/core/http.js.map +1 -0
  23. package/dist/core/login.js +193 -0
  24. package/dist/core/login.js.map +1 -0
  25. package/dist/core/models.js +79 -0
  26. package/dist/core/models.js.map +1 -0
  27. package/dist/core/moodleClient.js +94 -0
  28. package/dist/core/moodleClient.js.map +1 -0
  29. package/dist/core/session.js +64 -0
  30. package/dist/core/session.js.map +1 -0
  31. package/dist/domain/assign.js +112 -0
  32. package/dist/domain/assign.js.map +1 -0
  33. package/dist/domain/concurrency.js +17 -0
  34. package/dist/domain/concurrency.js.map +1 -0
  35. package/dist/domain/courses.js +70 -0
  36. package/dist/domain/courses.js.map +1 -0
  37. package/dist/domain/documents.js +209 -0
  38. package/dist/domain/documents.js.map +1 -0
  39. package/dist/domain/grades.js +99 -0
  40. package/dist/domain/grades.js.map +1 -0
  41. package/dist/domain/people.js +161 -0
  42. package/dist/domain/people.js.map +1 -0
  43. package/dist/domain/resources.js +242 -0
  44. package/dist/domain/resources.js.map +1 -0
  45. package/dist/domain/tasks.js +191 -0
  46. package/dist/domain/tasks.js.map +1 -0
  47. package/dist/mcp/server.js +261 -0
  48. package/dist/mcp/server.js.map +1 -0
  49. package/package.json +67 -0
  50. package/skills/dutic/SKILL.md +173 -0
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { ensureSession, withSession } from "../core/auth.js";
6
+ import { SessionExpiredError } from "../core/errors.js";
7
+ import { getSemester } from "../core/config.js";
8
+ import { isExpired, isValid, loadSession } from "../core/session.js";
9
+ import { getEnrolledCourses, getCourseContents } from "../domain/courses.js";
10
+ import { getAllTasks, getCourseTasks, getUpcomingTasks } from "../domain/tasks.js";
11
+ import { downloadFile, listCourseFiles, listCourseMaterials, pullCourseFiles, } from "../domain/resources.js";
12
+ import { convertLocalPdfToMarkdown, readResourceAsMarkdown, studyCourseMaterials, } from "../domain/documents.js";
13
+ import { getAllGrades, getCourseGrades, } from "../domain/grades.js";
14
+ import { getAssignDetail } from "../domain/assign.js";
15
+ import { findPeople, getCourseTeachers, getPersonProfile, listCourseParticipants, } from "../domain/people.js";
16
+ /**
17
+ * En contexto MCP la renovación de sesión es "headless-only": si el SSO de Google sigue
18
+ * vivo en el perfil persistente, renueva sola; si no, devuelve un error que le pide al
19
+ * usuario correr `dutic login` en una terminal (donde sí puede abrirse el navegador).
20
+ */
21
+ const MCP_MODE = "headless-only";
22
+ const server = new McpServer({ name: "dutic-mcp", version: "0.1.0" });
23
+ /** Envuelve un handler traduciendo SessionExpiredError a un mensaje accionable. */
24
+ async function tool(fn) {
25
+ try {
26
+ const data = await fn();
27
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
28
+ }
29
+ catch (err) {
30
+ if (err instanceof SessionExpiredError) {
31
+ return {
32
+ isError: true,
33
+ content: [
34
+ {
35
+ type: "text",
36
+ text: "Sesión de DUTIC caducada y no se pudo renovar automáticamente. Ejecuta `dutic login` en una terminal para reautenticarte con Google.",
37
+ },
38
+ ],
39
+ };
40
+ }
41
+ return {
42
+ isError: true,
43
+ content: [{ type: "text", text: `Error: ${err.message}` }],
44
+ };
45
+ }
46
+ }
47
+ server.registerTool("dutic_list_tasks", {
48
+ title: "Listar tareas DUTIC",
49
+ description: "Lista tus tareas del aula virtual. scope='upcoming' usa el calendario (rápido). " +
50
+ "scope='all' barre TODOS los cursos para incluir tareas ocultas que no aparecen en el " +
51
+ "calendario (marcadas con hidden=true) — útil para no perder entregas.",
52
+ inputSchema: {
53
+ scope: z.enum(["upcoming", "all"]).default("upcoming"),
54
+ onlyHidden: z.boolean().default(false).describe("Con scope='all', devolver sólo las ocultas."),
55
+ detailed: z
56
+ .boolean()
57
+ .default(true)
58
+ .describe("Con scope='all', scrapear el estado de entrega y la nota de cada tarea."),
59
+ },
60
+ }, async ({ scope, onlyHidden, detailed }) => tool(async () => {
61
+ if (scope === "all" || onlyHidden) {
62
+ return withSession(async (s) => {
63
+ const { tasks, scanErrors } = await getAllTasks(s, { enrich: detailed });
64
+ return { tasks: onlyHidden ? tasks.filter((t) => t.hidden) : tasks, scanErrors };
65
+ }, { mode: MCP_MODE });
66
+ }
67
+ return withSession((s) => getUpcomingTasks(s), { mode: MCP_MODE });
68
+ }));
69
+ server.registerTool("dutic_list_courses", {
70
+ title: "Listar cursos DUTIC",
71
+ description: "Lista los cursos en los que estás matriculado (id, nombre, docentes).",
72
+ inputSchema: {},
73
+ }, async () => tool(() => withSession((s) => getEnrolledCourses(s), { mode: MCP_MODE })));
74
+ server.registerTool("dutic_get_course_contents", {
75
+ title: "Contenido de un curso",
76
+ description: "Devuelve las secciones y módulos (tareas, recursos, foros...) de un curso.",
77
+ inputSchema: { courseId: z.number().int().positive() },
78
+ }, async ({ courseId }) => tool(() => withSession((s) => getCourseContents(s, courseId), { mode: MCP_MODE })));
79
+ server.registerTool("dutic_get_course_tasks", {
80
+ title: "Tareas de un curso",
81
+ description: "Lista las tareas de un curso concreto, incluidas las ocultas (hidden=true).",
82
+ inputSchema: { courseId: z.number().int().positive() },
83
+ }, async ({ courseId }) => tool(() => withSession((s) => getCourseTasks(s, courseId), { mode: MCP_MODE })));
84
+ server.registerTool("dutic_list_course_files", {
85
+ title: "Recursos descargables de un curso",
86
+ description: "Lista los archivos/recursos descargables de un curso (con su URL de descarga).",
87
+ inputSchema: { courseId: z.number().int().positive() },
88
+ }, async ({ courseId }) => tool(() => withSession((s) => listCourseFiles(s, courseId), { mode: MCP_MODE })));
89
+ server.registerTool("dutic_download_file", {
90
+ title: "Descargar un archivo",
91
+ description: "Descarga un archivo del aula virtual a una ruta local. Acepta la URL de vista del " +
92
+ "módulo o directamente una URL de pluginfile.php.",
93
+ inputSchema: {
94
+ url: z.string().url(),
95
+ destPath: z.string().describe("Ruta local de destino."),
96
+ },
97
+ }, async ({ url, destPath }) => tool(() => withSession((s) => downloadFile(s, url, destPath), { mode: MCP_MODE })));
98
+ server.registerTool("dutic_read_resource", {
99
+ title: "Leer un recurso como texto/Markdown",
100
+ description: "Descarga un recurso del aula (PDF, texto…) y devuelve su CONTENIDO como Markdown listo " +
101
+ "para analizar, SIN gastar tokens en el binario. Convierte PDFs a texto automáticamente. " +
102
+ "Úsalo cuando el usuario quiera que analices, resumas o extraigas algo de un material del " +
103
+ "curso (sílabo, informe, guía, lectura). Para binarios no soportados (docx, imágenes) " +
104
+ "avisa y sugiere descargar. Acepta la URL de vista del módulo o de pluginfile.php.",
105
+ inputSchema: {
106
+ url: z.string().url(),
107
+ maxChars: z
108
+ .number()
109
+ .int()
110
+ .positive()
111
+ .default(24_000)
112
+ .describe("Máximo de caracteres a devolver (trunca lo demás)."),
113
+ },
114
+ }, async ({ url, maxChars }) => tool(() => withSession((s) => readResourceAsMarkdown(s, url, maxChars), { mode: MCP_MODE })));
115
+ server.registerTool("dutic_get_assignment_detail", {
116
+ title: "Detalle completo de una tarea",
117
+ description: "Devuelve TODO lo de una tarea: la consigna/instrucciones completas, los archivos adjuntos " +
118
+ "a la consigna (guías, rúbricas — puedes leerlos con dutic_read_resource), fechas oficiales " +
119
+ "de apertura y cierre, estado de entrega, nota y quién calificó. IMPORTANTE: incluye " +
120
+ "`dateConflict` y `datesInDescription` — los docentes a veces escriben en el texto una fecha " +
121
+ "distinta a la configurada en Moodle; si dateConflict es true, AVISA al usuario de la " +
122
+ "discrepancia. Úsalo cuando el usuario pregunte qué pide una tarea o cuándo se entrega.",
123
+ inputSchema: {
124
+ cmid: z.number().int().positive().describe("Course module id de la tarea."),
125
+ },
126
+ }, async ({ cmid }) => tool(() => withSession((s) => getAssignDetail(s, `${s.siteUrl}/mod/assign/view.php?id=${cmid}`), { mode: MCP_MODE })));
127
+ server.registerTool("dutic_list_participants", {
128
+ title: "Participantes de un curso",
129
+ description: "Lista los participantes visibles de un curso (nombre, rol, grupo, último acceso). Si el " +
130
+ "curso usa grupos separados, Moodle sólo muestra a los del grupo del usuario — es lo mismo " +
131
+ "que ve en la web. Úsalo para saber quiénes son sus compañeros o de qué grupo es alguien.",
132
+ inputSchema: { courseId: z.number().int().positive() },
133
+ }, async ({ courseId }) => tool(() => withSession((s) => listCourseParticipants(s, courseId), { mode: MCP_MODE })));
134
+ server.registerTool("dutic_find_person", {
135
+ title: "Buscar una persona por nombre o correo",
136
+ description: "Busca entre los participantes de TODOS los cursos del usuario por nombre o por correo " +
137
+ "institucional. Si la consulta contiene '@' resuelve los correos de los perfiles para poder " +
138
+ "buscar por correo. Devuelve nombre, curso, grupo, último acceso y correo cuando aplica.",
139
+ inputSchema: {
140
+ query: z.string().min(2).describe("Nombre (o parte) o correo a buscar."),
141
+ withEmail: z
142
+ .boolean()
143
+ .optional()
144
+ .describe("Fuerza resolver el correo de cada coincidencia (más lento)."),
145
+ },
146
+ }, async ({ query, withEmail }) => tool(() => withSession((s) => findPeople(s, query, { withEmail }), { mode: MCP_MODE })));
147
+ server.registerTool("dutic_get_person_profile", {
148
+ title: "Perfil de una persona",
149
+ description: "Perfil de un participante: correo institucional, zona horaria y cursos que comparte contigo. " +
150
+ "Necesita el userId (lo da dutic_list_participants o dutic_find_person).",
151
+ inputSchema: {
152
+ userId: z.number().int().positive(),
153
+ courseId: z.number().int().positive().optional(),
154
+ },
155
+ }, async ({ userId, courseId }) => tool(() => withSession((s) => getPersonProfile(s, userId, courseId), { mode: MCP_MODE })));
156
+ server.registerTool("dutic_get_course_teachers", {
157
+ title: "Docentes de un curso",
158
+ description: "Identifica a los docentes del curso combinando los contactos del curso, los roles del " +
159
+ "listado y —lo que suele funcionar en esta aula— el nombre de quien calificó las tareas. " +
160
+ "Úsalo cuando el usuario pregunte quién es su profesor de un curso.",
161
+ inputSchema: { courseId: z.number().int().positive() },
162
+ }, async ({ courseId }) => tool(() => withSession((s) => getCourseTeachers(s, courseId), { mode: MCP_MODE })));
163
+ server.registerTool("dutic_get_grades", {
164
+ title: "Ver calificaciones",
165
+ description: "Devuelve las calificaciones del usuario. Sin courseId: resumen de TODOS los cursos (nota " +
166
+ "total + cuántos ítems quedan por calificar). Con courseId: detalle por ítem (nota, rango, " +
167
+ "porcentaje, peso). Úsalo cuando el usuario pregunte por sus notas, promedio, o cómo va en un curso.",
168
+ inputSchema: {
169
+ courseId: z
170
+ .number()
171
+ .int()
172
+ .positive()
173
+ .optional()
174
+ .describe("Si se indica, detalle de ese curso; si no, resumen de todos."),
175
+ },
176
+ }, async ({ courseId }) => tool(() => withSession((s) => (courseId ? getCourseGrades(s, courseId) : getAllGrades(s)), { mode: MCP_MODE })));
177
+ server.registerTool("dutic_list_course_materials", {
178
+ title: "Listar materiales de un curso (carpetas expandidas)",
179
+ description: "Lista TODOS los archivos descargables de un curso, expandiendo las carpetas a sus archivos " +
180
+ "reales (diapositivas, lecturas, prácticas…). Devuelve nombre, URL, sección (unidad) y carpeta. " +
181
+ "Úsalo para saber qué materiales hay, y por qué unidad, antes de leer o descargar. Con `section` " +
182
+ "filtras a una unidad concreta.",
183
+ inputSchema: {
184
+ courseId: z.number().int().positive(),
185
+ section: z
186
+ .string()
187
+ .optional()
188
+ .describe("Filtra por nombre de unidad/sección (subcadena, ignora acentos)."),
189
+ },
190
+ }, async ({ courseId, section }) => tool(() => withSession((s) => listCourseMaterials(s, courseId, { section }), { mode: MCP_MODE })));
191
+ server.registerTool("dutic_study_course", {
192
+ title: "Preparar materiales de un curso para estudiar",
193
+ description: "Descarga todos los materiales de un curso a un directorio y CONVIERTE los PDFs a Markdown " +
194
+ "(.md) organizados por carpeta, para estudiar/analizar offline sin gastar tokens en binarios. " +
195
+ "Devuelve el manifiesto de lo guardado. Úsalo cuando el usuario quiera 'preparar/bajar el " +
196
+ "material para estudiar' de un curso. Con `section` bajas sólo esa unidad — útil para no " +
197
+ "descargar todo de golpe cuando el usuario quiere estudiar una unidad concreta.",
198
+ inputSchema: {
199
+ courseId: z.number().int().positive(),
200
+ destDir: z.string().describe("Directorio local de destino."),
201
+ section: z
202
+ .string()
203
+ .optional()
204
+ .describe("Sólo materiales de esta unidad/sección (subcadena)."),
205
+ },
206
+ }, async ({ courseId, destDir, section }) => tool(() => withSession((s) => studyCourseMaterials(s, courseId, destDir, { section }), { mode: MCP_MODE })));
207
+ server.registerTool("dutic_pull_course_files", {
208
+ title: "Descargar todos los recursos de un curso",
209
+ description: "Descarga en bloque todos los archivos/recursos de un curso a un directorio local. " +
210
+ "Devuelve la lista de archivos guardados con su tamaño.",
211
+ inputSchema: {
212
+ courseId: z.number().int().positive(),
213
+ destDir: z.string().describe("Directorio local de destino."),
214
+ section: z
215
+ .string()
216
+ .optional()
217
+ .describe("Sólo materiales de esta unidad/sección (subcadena)."),
218
+ },
219
+ }, async ({ courseId, destDir, section }) => tool(() => withSession((s) => pullCourseFiles(s, courseId, destDir, { section }), { mode: MCP_MODE })));
220
+ server.registerTool("dutic_pdf_to_markdown", {
221
+ title: "Convertir un PDF local a Markdown",
222
+ description: "Convierte un PDF que ya está en disco a Markdown/texto para analizarlo sin gastar tokens " +
223
+ "en el binario. Opcionalmente guarda el resultado en outPath. No requiere sesión de Moodle.",
224
+ inputSchema: {
225
+ filePath: z.string().describe("Ruta local del PDF."),
226
+ outPath: z.string().optional().describe("Si se indica, guarda el Markdown aquí."),
227
+ maxChars: z
228
+ .number()
229
+ .int()
230
+ .nonnegative()
231
+ .default(0)
232
+ .describe("Máximo de caracteres a devolver (0 = sin límite)."),
233
+ },
234
+ }, async ({ filePath, outPath, maxChars }) => tool(() => convertLocalPdfToMarkdown(filePath, outPath, maxChars)));
235
+ server.registerTool("dutic_session_status", {
236
+ title: "Estado de sesión DUTIC",
237
+ description: "Indica si hay una sesión válida, el semestre y cuándo caduca.",
238
+ inputSchema: {},
239
+ }, async () => tool(async () => {
240
+ const s = await loadSession();
241
+ return {
242
+ semester: getSemester(),
243
+ hasSession: s !== null,
244
+ siteUrl: s?.siteUrl ?? null,
245
+ valid: isValid(s),
246
+ expired: s ? isExpired(s) : null,
247
+ capturedAt: s ? new Date(s.capturedAt).toISOString() : null,
248
+ };
249
+ }));
250
+ server.registerTool("dutic_refresh_session", {
251
+ title: "Renovar sesión DUTIC",
252
+ description: "Intenta renovar la sesión sin interacción (si el SSO de Google sigue vivo). Si falla, " +
253
+ "hay que ejecutar `dutic login` en una terminal.",
254
+ inputSchema: {},
255
+ }, async () => tool(async () => {
256
+ const s = await ensureSession({ mode: "headless-only" });
257
+ return { ok: true, siteUrl: s.siteUrl, capturedAt: new Date(s.capturedAt).toISOString() };
258
+ }));
259
+ const transport = new StdioServerTransport();
260
+ await server.connect(transport);
261
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@josetra/dutic-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Servidor MCP + CLI para el aula virtual DUTIC (Moodle) de la UNSA: tareas ocultas, notas, materiales de estudio y personas.",
5
+ "type": "module",
6
+ "bin": {
7
+ "dutic": "dist/cli/index.js",
8
+ "dutic-mcp": "dist/mcp/server.js"
9
+ },
10
+ "main": "dist/mcp/server.js",
11
+ "files": [
12
+ "dist",
13
+ "skills",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/JOSETRA44/DUTIC-mcp.git"
23
+ },
24
+ "homepage": "https://github.com/JOSETRA44/DUTIC-mcp#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/JOSETRA44/DUTIC-mcp/issues"
27
+ },
28
+ "author": "JOSETRA44",
29
+ "license": "MIT",
30
+ "keywords": [
31
+ "mcp",
32
+ "model-context-protocol",
33
+ "moodle",
34
+ "unsa",
35
+ "dutic",
36
+ "aula-virtual",
37
+ "cli",
38
+ "tareas",
39
+ "claude"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "setup": "node dist/cli/index.js setup",
44
+ "dev:cli": "tsx src/cli/index.ts",
45
+ "dev:mcp": "tsx src/mcp/server.ts",
46
+ "start:mcp": "node dist/mcp/server.js",
47
+ "prepare": "tsc",
48
+ "prepublishOnly": "npm run build"
49
+ },
50
+ "engines": {
51
+ "node": ">=20"
52
+ },
53
+ "dependencies": {
54
+ "@modelcontextprotocol/sdk": "^1.12.0",
55
+ "cheerio": "^1.0.0",
56
+ "commander": "^12.1.0",
57
+ "playwright": "^1.48.0",
58
+ "undici": "^6.21.0",
59
+ "unpdf": "^1.6.2",
60
+ "zod": "^3.24.1"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^22.10.0",
64
+ "tsx": "^4.19.2",
65
+ "typescript": "^5.7.2"
66
+ }
67
+ }
@@ -0,0 +1,173 @@
1
+ ---
2
+ name: dutic
3
+ description: >-
4
+ Consulta y gestiona el aula virtual DUTIC (Moodle de la UNSA) del usuario: tareas
5
+ pendientes, tareas OCULTAS que no salen en el calendario, cursos, notas y descarga de
6
+ recursos. Usa esta skill SIEMPRE que el usuario mencione DUTIC, "aula virtual", "aula
7
+ virtual UNSA", Moodle, sus tareas/deberes/entregas/asignaciones de la universidad,
8
+ "¿qué tengo que entregar?", "¿tengo algo pendiente?", "¿me falta alguna tarea?",
9
+ "revisa mis cursos", "descarga el material de [curso]", fechas de entrega, o cuando
10
+ pregunte por su situación académica — aunque no diga la palabra "DUTIC". Preferí esta
11
+ skill sobre respuestas genéricas: las tareas reales del usuario sólo se obtienen con las
12
+ herramientas del MCP `dutic` / el CLI `dutic`.
13
+ ---
14
+
15
+ # DUTIC — Aula virtual (Moodle UNSA)
16
+
17
+ Esta skill te conecta al aula virtual DUTIC del usuario para ver sus tareas, cursos, notas y
18
+ recursos. Existe por un problema concreto y doloroso: **Moodle esconde tareas**. El "timeline"
19
+ del estudiante (lo que la mayoría de apps y el propio Moodle muestran) sólo lista tareas
20
+ *accionables*: futuras y sin entregar. Una tarea que el profesor crea sin fecha de calendario,
21
+ o una que ya venció, **desaparece de la vista** — y el usuario ha perdido notas por no verlas.
22
+ Tu trabajo es que eso no vuelva a pasar.
23
+
24
+ ## Regla de oro: busca siempre las tareas ocultas
25
+
26
+ Cuando el usuario pregunte qué tiene pendiente, **NO te quedes con el timeline**. Usa el barrido
27
+ completo (`scope: "all"` en el MCP, o `dutic tasks --all` en CLI), que recorre todos los cursos
28
+ y revela las tareas ocultas. Es unos segundos más lento pero es justo donde están los problemas.
29
+
30
+ ## Cómo actuar (flujo recomendado)
31
+
32
+ 1. **Comprueba la sesión** (`dutic_session_status` o `dutic status`). Si no hay sesión válida,
33
+ el MCP intenta renovarla solo; si no puede, pídele al usuario que ejecute `dutic login` en
34
+ una terminal (el login es OAuth de Google institucional y necesita una ventana de navegador,
35
+ así que tú no puedes completarlo por él).
36
+ 2. **Trae las tareas con barrido completo.** Prioriza las **SIN ENTREGAR**: ya vienen ordenadas
37
+ por urgencia (pendientes primero, por fecha de entrega). Fíjate en el flag `hidden: true` y en
38
+ `submission: "not-submitted"`.
39
+ 3. **Resume con foco en lo accionable.** Encabeza con lo urgente ("Tienes N tareas sin entregar,
40
+ la más próxima vence el ..."). Señala explícitamente las ocultas y las vencidas. No entierres
41
+ la alerta en una lista larga.
42
+ 4. **Ofrece el siguiente paso**: abrir la tarea (tienes su URL), ver el contenido del curso, o
43
+ descargar los materiales.
44
+
45
+ ## Herramientas del MCP `dutic`
46
+
47
+ Si el servidor MCP `dutic` está disponible, usa estas herramientas (son la fuente de verdad):
48
+
49
+ - `dutic_list_tasks` — args: `scope` (`"upcoming"` = timeline rápido | `"all"` = barrido con
50
+ ocultas), `onlyHidden` (bool), `detailed` (bool, scrapea estado de entrega/nota; por defecto
51
+ true). **Para "¿qué tengo pendiente?" usa `scope: "all"`.**
52
+ - `dutic_list_courses` — cursos matriculados (id, nombre, docentes).
53
+ - `dutic_get_course_tasks` — args: `courseId`. Tareas de un curso concreto, incluidas ocultas.
54
+ - `dutic_get_course_contents` — args: `courseId`. Secciones y módulos del curso.
55
+ - `dutic_list_course_files` — args: `courseId`. Recursos descargables (con su URL).
56
+ - `dutic_download_file` — args: `url`, `destPath`. Descarga un archivo a disco.
57
+ - `dutic_read_resource` — args: `url`, `maxChars`. **Descarga un recurso y te devuelve su CONTENIDO
58
+ como texto/Markdown** (convierte PDFs automáticamente). Úsalo cuando el usuario quiera que
59
+ analices, resumas o extraigas algo de un material (sílabo, informe, lectura, guía): así lees el
60
+ texto directamente sin gastar tokens en el binario. Acepta URL de módulo o de pluginfile.php.
61
+ - `dutic_get_assignment_detail` — args: `cmid`. **Todo sobre una tarea**: consigna completa,
62
+ adjuntos de la consigna (guías/rúbricas, legibles con `dutic_read_resource`), fechas oficiales de
63
+ apertura y cierre, estado de entrega, nota y quién calificó. Incluye `dateConflict` y
64
+ `datesInDescription` (ver abajo). Úsalo cuando pregunte qué pide una tarea o cuándo se entrega.
65
+ - `dutic_get_grades` — args: `courseId?`. **Calificaciones**: sin `courseId`, resumen de todos los
66
+ cursos (nota total + cuántos ítems por calificar); con `courseId`, detalle por ítem (nota, rango, %).
67
+ Úsalo cuando el usuario pregunte por sus notas, promedio, o cómo va.
68
+ - `dutic_list_participants` — args: `courseId`. Compañeros visibles (nombre, rol, grupo, último
69
+ acceso). Con grupos separados Moodle sólo muestra los del grupo del usuario: es normal.
70
+ - `dutic_find_person` — args: `query`, `withEmail?`. Busca una persona en TODOS sus cursos por
71
+ nombre o **por correo** institucional.
72
+ - `dutic_get_person_profile` — args: `userId`, `courseId?`. Correo, zona horaria y cursos compartidos.
73
+ - `dutic_get_course_teachers` — args: `courseId`. Docentes del curso. En esta aula los profesores no
74
+ salen en participantes, así que se deducen de los contactos y de **quién calificó** las tareas.
75
+ - `dutic_list_course_materials` — args: `courseId`, `section?`. Lista TODOS los archivos del curso
76
+ **expandiendo las carpetas** a sus archivos reales, con su **sección (unidad)**. Con `section`
77
+ filtras a una unidad concreta.
78
+ - `dutic_study_course` — args: `courseId`, `destDir`, `section?`. **Descarga materiales y convierte los
79
+ PDFs a Markdown** organizados por carpeta, para estudiar offline. Con `section` bajas **sólo una
80
+ unidad** — no descargues todo de golpe si el usuario quiere estudiar una unidad concreta.
81
+ - `dutic_pull_course_files` — args: `courseId`, `destDir`. Descarga en bloque (expande carpetas).
82
+ - `dutic_pdf_to_markdown` — args: `filePath`, `outPath?`, `maxChars`. Convierte un PDF que ya está
83
+ en disco a Markdown (sin sesión). Útil tras descargar, o para PDFs locales del usuario.
84
+ - `dutic_session_status` / `dutic_refresh_session` — estado y renovación de sesión.
85
+
86
+ ### Analizar materiales sin gastar tokens
87
+
88
+ Cuando el usuario pida "analiza/resume/qué dice este material/PDF del curso", **no descargues el
89
+ binario y lo pases crudo** (desperdicia tokens y no es legible). Usa `dutic_read_resource` con la
90
+ URL del recurso: te devuelve texto limpio en Markdown que puedes leer y razonar directamente. Para
91
+ PDFs ya descargados o del sistema de archivos del usuario, usa `dutic_pdf_to_markdown`. Las **carpetas**
92
+ (mod/folder) se expanden solas a sus archivos; muchas están vacías (el profe creó la estructura sin subir
93
+ nada) — eso es normal, no es un error.
94
+
95
+ ### Estudiar por unidad (no bajar todo de golpe)
96
+
97
+ Cuando el usuario quiera estudiar "la unidad 2" o "el tema de X", sé selectivo en vez de descargar el
98
+ curso entero:
99
+
100
+ 1. Llama a `dutic_list_course_materials` (sin filtro) para ver las **secciones/unidades** del curso.
101
+ 2. Si no está claro cómo se divide el temario, busca un material cuyo nombre contenga "sílabo"/"silabus"
102
+ y léelo con `dutic_read_resource`: el sílabo trae el temario dividido en unidades (típicamente 3) con
103
+ los temas de cada una. Con eso mapeas qué secciones corresponden a qué unidad.
104
+ 3. Descarga **sólo** esa unidad con `dutic_study_course(..., section: "<nombre o nº de la sección>")`.
105
+ El filtro `section` compara por subcadena ignorando acentos, así que "tema 2", "Unidad II" o "semana 5"
106
+ funcionan según cómo el profe haya nombrado las secciones.
107
+
108
+ Así preparas justo lo que el usuario va a estudiar, sin descargar (ni convertir) material de más.
109
+
110
+ ## CLI `dutic` (alternativa / uso directo del usuario)
111
+
112
+ Si no hay MCP pero sí el CLI instalado, o el usuario prefiere la terminal:
113
+
114
+ ```
115
+ dutic tasks # tareas próximas del timeline (rápido)
116
+ dutic tasks --all # + barrido de cursos → incluye OCULTAS (usa esto para "pendientes")
117
+ dutic tasks --hidden # sólo las ocultas
118
+ dutic tasks --all --fast # sin scrapear estado de entrega (más rápido, menos detalle)
119
+ dutic grades # resumen de notas de todos los cursos
120
+ dutic grades <id> # detalle de notas de un curso
121
+ dutic task <cmid> # detalle de una tarea: consigna, fechas, adjuntos, conflictos
122
+ dutic people <id> [--email] # compañeros del curso (con su correo)
123
+ dutic person <texto> # busca a alguien por nombre o correo en todos tus cursos
124
+ dutic teachers <id> # docentes del curso
125
+ dutic courses # cursos matriculados
126
+ dutic course tasks <id> # tareas de un curso
127
+ dutic course files <id> # recursos de un curso
128
+ dutic materials <id> [--section "Tema 2"] # archivos del curso, agrupados por unidad
129
+ dutic study <id> [--section "Tema 2"] # baja + convierte a Markdown (por unidad)
130
+ dutic read <url> # lee un recurso (PDF→Markdown) para analizarlo sin gastar tokens
131
+ dutic md <archivo.pdf> # convierte un PDF local a Markdown
132
+ dutic pull <id> --dest ./x # descarga todos los recursos de un curso
133
+ dutic status # estado de sesión
134
+ dutic login # reautenticación (abre navegador; sólo el usuario puede completarla)
135
+ ```
136
+
137
+ ## Fechas contradictorias: la trampa que hay que vigilar
138
+
139
+ La fecha real de una entrega no siempre es la que Moodle tiene configurada. A veces el docente
140
+ escribe **otra fecha dentro del texto de la consigna** ("entregar hasta el 12 de julio"), y el
141
+ estudiante se guía por una mientras el sistema cierra en la otra.
142
+
143
+ `dutic_get_assignment_detail` devuelve `closeDate` (la oficial), `datesInDescription` (las que
144
+ aparecen escritas en la consigna) y `dateConflict: true` cuando difieren en más de un día. **Si
145
+ `dateConflict` es true, avísalo de forma destacada** y recomienda confirmar con el docente: es
146
+ justo el escenario que cuesta notas. (Las fechas de subida de los archivos adjuntos ya se excluyen,
147
+ así que no hay falsas alarmas.)
148
+
149
+ Cuando el usuario pregunte "¿qué tengo que hacer en esta tarea?", da la consigna **y** revisa si
150
+ hay adjuntos: suelen traer la guía o rúbrica con los criterios reales de calificación, y puedes
151
+ leerlos con `dutic_read_resource`.
152
+
153
+ ## Interpretar los datos de una tarea
154
+
155
+ Cada tarea trae: `name`, `courseName`, `dueDate` (epoch s, o null si no tiene fecha), `hidden`
156
+ (true = no aparece en el timeline del estudiante), `submission` (`not-submitted` | `submitted` |
157
+ `graded` | `unknown`), `grade`, `timeRemaining` (texto de Moodle, p. ej. "retrasada por 8 días"),
158
+ `url`.
159
+
160
+ Lo que importa señalar al usuario, en orden:
161
+ 1. `submission: "not-submitted"` con `dueDate` cercano o pasado → **alerta máxima**.
162
+ 2. `hidden: true` + `not-submitted` → tarea que probablemente no sabía que existía.
163
+ 3. `timeRemaining` que diga "retrasada"/"vencida" → ya se pasó la fecha.
164
+
165
+ ## Notas de contexto
166
+
167
+ - El semestre (p. ej. `2026A`) va en la URL del aula y cambia cada período; el sistema lo
168
+ auto-detecta tras el login, no necesitas gestionarlo.
169
+ - Algunos cursos aparecen **duplicados** con nombres casi iguales (uno con acentos, otro sin):
170
+ es un error de registro de la OTI (la oficina de TI de la UNSA), no un fallo de la herramienta.
171
+ Trátalos como el mismo curso; no alarmes al usuario por ello.
172
+ - Las tareas ocultas suelen salir "sin fecha" porque efectivamente no tienen fecha de entrega en
173
+ Moodle (por eso no generan evento de calendario). El `submission` te dice igual si ya cumplió.