utn-mcp 0.7.0 → 0.8.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/README.md +199 -180
- package/build/aula-virtual/client/client.js +2 -2
- package/build/aula-virtual/client/link.js +17 -11
- package/build/aula-virtual/client/local-session.js +21 -13
- package/build/aula-virtual/client/moodle-api.js +6 -5
- package/build/aula-virtual/client/regionals.js +103 -0
- package/build/aula-virtual/client/sso-login.js +123 -24
- package/build/aula-virtual/client/stored-session.js +24 -20
- package/build/aula-virtual/format/actions.js +1 -2
- package/build/aula-virtual/format/files.js +30 -11
- package/build/aula-virtual/login.js +24 -10
- package/build/aula-virtual/tools.js +11 -5
- package/build/http.js +57 -9
- package/build/index.js +12 -13
- package/build/oauth/access.js +3 -3
- package/build/oauth/admin.js +75 -13
- package/build/oauth/redirects.js +60 -0
- package/build/oauth/server.js +106 -37
- package/build/server.js +1 -1
- package/build/shared/input.js +60 -0
- package/build/shared/load-env.js +5 -0
- package/build/shared/pages.js +42 -37
- package/build/shared/paths.js +1 -1
- package/build/shared/store/sqlite-store.js +111 -40
- package/build/shared/uploads.js +9 -1
- package/package.json +58 -58
- package/build/aula-virtual/client/constants.js +0 -2
package/README.md
CHANGED
|
@@ -1,180 +1,199 @@
|
|
|
1
|
-
# UTN-MCP — El puente entre tu agente y los sistemas de la Universidad.
|
|
2
|
-
Este es el Model Context Protocol server NO OFICIAL de la UTN. Con él
|
|
3
|
-
|
|
4
|
-
## Qué podés pedirle
|
|
5
|
-
- *"¿Cuándo es el recuperatorio del primer parcial de Álgebra?"*
|
|
6
|
-
- *"¿Qué avisó la profesora de Física esta semana?"*
|
|
7
|
-
- *"Resumime la guía de la unidad 3 de Sistemas Operativos."*
|
|
8
|
-
- *"Pasame el link para bajar el apunte de calorimetría."*
|
|
9
|
-
- *"¿Qué nota me pusieron en el TP1 y qué me corrigieron?"*
|
|
10
|
-
- *"Preguntá en el foro de consultas si el parcial incluye la unidad 4."*
|
|
11
|
-
- *"Anotame en el grupo 6."*
|
|
12
|
-
- *"Agendame el recuperatorio del 14 a las 18."*
|
|
13
|
-
|
|
14
|
-
El agente recorre el aula como lo harías vos: entra al curso, mira los foros y abre los archivos que hagan falta.
|
|
15
|
-
|
|
16
|
-
## Tools
|
|
17
|
-
| Tool | Qué hace |
|
|
18
|
-
|---|---|
|
|
19
|
-
| `aula_list_courses` | Lista tus cursos. Es el punto de partida. |
|
|
20
|
-
| `aula_open` | Entra a un curso, un módulo, un foro o una discusión. Cada respuesta trae las direcciones de lo que hay adentro y de un nivel más arriba, así el agente navega sin perderse. |
|
|
21
|
-
| `aula_read_file` | Lee un archivo del aula, por partes cuando es largo. |
|
|
22
|
-
| `aula_get_site_info` | Datos de tu usuario en el aula virtual. |
|
|
23
|
-
| `aula_forum_post` | Abre un hilo en un foro o responde en una discusión. |
|
|
24
|
-
| `aula_forum_edit_post` | Edita una publicación tuya. |
|
|
25
|
-
| `aula_forum_delete_post` | Borra una publicación tuya. |
|
|
26
|
-
| `aula_assign_submit` | Entrega una tarea: sube archivos de tu compu, escribe el texto en línea y confirma el envío. |
|
|
27
|
-
| `aula_assign_upload_link` | Solo en modo HTTP: un link para subir los archivos de una entrega, que usa el agente con `curl` o vos desde el navegador. |
|
|
28
|
-
| `aula_choice_answer` | Responde una elección o te anota en un grupo. |
|
|
29
|
-
| `aula_send_message` | Manda un mensaje privado a un docente o compañero. |
|
|
30
|
-
| `aula_calendar_add_event` | Agrega un recordatorio a tu calendario del aula. |
|
|
31
|
-
|
|
32
|
-
Al abrir una tarea, `aula_open` muestra si la entregaste, cuándo vence y la devolución con la nota. Las tools que escriben le piden al agente que te muestre lo que va a hacer y espere tu confirmación.
|
|
33
|
-
|
|
34
|
-
### Archivos que puede leer
|
|
35
|
-
- **PDF**: el texto, página por página. Los **escaneados** se mandan como imagen de cada página, y el agente los lee igual.
|
|
36
|
-
- **Word (.docx), PowerPoint (.pptx) y Excel (.xlsx)**: el texto, con las tablas fila por fila.
|
|
37
|
-
- **Imágenes** (PNG, JPG, GIF) y texto plano.
|
|
38
|
-
|
|
39
|
-
Los formatos viejos de Office (.doc, .ppt, .pps), los comprimidos (.zip, .rar) y los videos no se leen. En ese caso el agente te pasa el link para que lo bajes.
|
|
40
|
-
|
|
41
|
-
### Links de descarga
|
|
42
|
-
Cada archivo viene con un link del aula virtual que podés abrir en tu navegador. Funciona con tu sesión del aula (si no estás logueado te manda al login) y **nunca incluye tu token**: ese queda siempre en tu computadora.
|
|
43
|
-
|
|
44
|
-
## Cómo instalar
|
|
45
|
-
Necesitás [Node.js](https://nodejs.org) 20 o superior. No hace falta clonar el repo ni instalar nada a mano: `npx` descarga el paquete solo.
|
|
46
|
-
|
|
47
|
-
### 1. Iniciar sesión
|
|
48
|
-
```bash
|
|
49
|
-
npx -y utn-mcp login
|
|
50
|
-
```
|
|
51
|
-
Te pide tu usuario
|
|
52
|
-
|
|
53
|
-
### 2. Agregarlo a tu cliente
|
|
54
|
-
|
|
55
|
-
**Claude Code**
|
|
56
|
-
```bash
|
|
57
|
-
claude mcp add utn -- npx -y utn-mcp
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
**Claude Desktop**: en *Settings → Developer → Edit Config*, agregá dentro de `mcpServers`:
|
|
61
|
-
```json
|
|
62
|
-
"utn": {
|
|
63
|
-
"command": "npx",
|
|
64
|
-
"args": ["-y", "utn-mcp"]
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
En Windows, si no arranca, usá `"command": "cmd"` y `"args": ["/c", "npx", "-y", "utn-mcp"]`. Después reiniciá Claude Desktop por completo.
|
|
68
|
-
|
|
69
|
-
**Codex**
|
|
70
|
-
```bash
|
|
71
|
-
codex mcp add utn -- npx -y utn-mcp
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**Gemini CLI**
|
|
75
|
-
```bash
|
|
76
|
-
gemini mcp add utn npx -y utn-mcp
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**OpenClaw**
|
|
80
|
-
```bash
|
|
81
|
-
openclaw mcp add utn --command npx --arg -y --arg utn-mcp
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**Hermes**: en `~/.hermes/config.yaml`:
|
|
85
|
-
```yaml
|
|
86
|
-
mcp_servers:
|
|
87
|
-
utn:
|
|
88
|
-
command: "npx"
|
|
89
|
-
args: ["-y", "utn-mcp"]
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**Cursor**: en `~/.cursor/mcp.json`:
|
|
93
|
-
```json
|
|
94
|
-
{
|
|
95
|
-
"mcpServers": {
|
|
96
|
-
"utn": { "command": "npx", "args": ["-y", "utn-mcp"] }
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
**VS Code**: en `.vscode/mcp.json` (o con *MCP: Add Server* desde la paleta de comandos):
|
|
102
|
-
```json
|
|
103
|
-
{
|
|
104
|
-
"servers": {
|
|
105
|
-
"utn": { "type": "stdio", "command": "npx", "args": ["-y", "utn-mcp"] }
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**ChatGPT**:
|
|
111
|
-
|
|
112
|
-
**Otros clientes (Windsurf, opencode, etc.)**: cualquier cliente MCP que lance servidores por STDIO sirve con el comando `npx -y utn-mcp`.
|
|
113
|
-
|
|
114
|
-
### Instalación global (opcional)
|
|
115
|
-
Si preferís tener el comando `utn-mcp` siempre disponible:
|
|
116
|
-
```bash
|
|
117
|
-
npm i -g utn-mcp
|
|
118
|
-
utn-mcp login
|
|
119
|
-
```
|
|
120
|
-
y en la config del cliente usás `"command": "utn-mcp"` sin argumentos.
|
|
121
|
-
|
|
122
|
-
###
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
1
|
+
# UTN-MCP — El puente entre tu agente y los sistemas de la Universidad.
|
|
2
|
+
Este es el Model Context Protocol server NO OFICIAL de la UTN. Con él podés pedirle a tu agente de preferencia (Claude, ChatGPT, etc.) que busque información y lea los apuntes del aula virtual y, con tu confirmación, que publique en los foros o entregue tareas. Corre en tu computadora o como servidor HTTP con OAuth, para usarlo desde cualquier lado.
|
|
3
|
+
|
|
4
|
+
## Qué podés pedirle
|
|
5
|
+
- *"¿Cuándo es el recuperatorio del primer parcial de Álgebra?"*
|
|
6
|
+
- *"¿Qué avisó la profesora de Física esta semana?"*
|
|
7
|
+
- *"Resumime la guía de la unidad 3 de Sistemas Operativos."*
|
|
8
|
+
- *"Pasame el link para bajar el apunte de calorimetría."*
|
|
9
|
+
- *"¿Qué nota me pusieron en el TP1 y qué me corrigieron?"*
|
|
10
|
+
- *"Preguntá en el foro de consultas si el parcial incluye la unidad 4."*
|
|
11
|
+
- *"Anotame en el grupo 6."*
|
|
12
|
+
- *"Agendame el recuperatorio del 14 a las 18."*
|
|
13
|
+
|
|
14
|
+
El agente recorre el aula como lo harías vos: entra al curso, mira los foros y abre los archivos que hagan falta.
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
| Tool | Qué hace |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `aula_list_courses` | Lista tus cursos. Es el punto de partida. |
|
|
20
|
+
| `aula_open` | Entra a un curso, un módulo, un foro o una discusión. Cada respuesta trae las direcciones de lo que hay adentro y de un nivel más arriba, así el agente navega sin perderse. |
|
|
21
|
+
| `aula_read_file` | Lee un archivo del aula, por partes cuando es largo. |
|
|
22
|
+
| `aula_get_site_info` | Datos de tu usuario y de tu regional en el aula virtual. |
|
|
23
|
+
| `aula_forum_post` | Abre un hilo en un foro o responde en una discusión. |
|
|
24
|
+
| `aula_forum_edit_post` | Edita una publicación tuya. |
|
|
25
|
+
| `aula_forum_delete_post` | Borra una publicación tuya. |
|
|
26
|
+
| `aula_assign_submit` | Entrega una tarea: sube archivos de tu compu, escribe el texto en línea y confirma el envío. |
|
|
27
|
+
| `aula_assign_upload_link` | Solo en modo HTTP: un link para subir los archivos de una entrega, que usa el agente con `curl` o vos desde el navegador. |
|
|
28
|
+
| `aula_choice_answer` | Responde una elección o te anota en un grupo. |
|
|
29
|
+
| `aula_send_message` | Manda un mensaje privado a un docente o compañero. |
|
|
30
|
+
| `aula_calendar_add_event` | Agrega un recordatorio a tu calendario del aula. |
|
|
31
|
+
|
|
32
|
+
Al abrir una tarea, `aula_open` muestra si la entregaste, cuándo vence y la devolución con la nota. Las tools que escriben le piden al agente que te muestre lo que va a hacer y espere tu confirmación.
|
|
33
|
+
|
|
34
|
+
### Archivos que puede leer
|
|
35
|
+
- **PDF**: el texto, página por página. Los **escaneados** se mandan como imagen de cada página, y el agente los lee igual.
|
|
36
|
+
- **Word (.docx), PowerPoint (.pptx) y Excel (.xlsx)**: el texto, con las tablas fila por fila.
|
|
37
|
+
- **Imágenes** (PNG, JPG, GIF) y texto plano.
|
|
38
|
+
|
|
39
|
+
Los formatos viejos de Office (.doc, .ppt, .pps), los comprimidos (.zip, .rar) y los videos no se leen. En ese caso el agente te pasa el link para que lo bajes.
|
|
40
|
+
|
|
41
|
+
### Links de descarga
|
|
42
|
+
Cada archivo viene con un link del aula virtual que podés abrir en tu navegador. Funciona con tu sesión del aula (si no estás logueado te manda al login) y **nunca incluye tu token**: ese queda siempre en tu computadora.
|
|
43
|
+
|
|
44
|
+
## Cómo instalar
|
|
45
|
+
Necesitás [Node.js](https://nodejs.org) 20 o superior. No hace falta clonar el repo ni instalar nada a mano: `npx` descarga el paquete solo.
|
|
46
|
+
|
|
47
|
+
### 1. Iniciar sesión
|
|
48
|
+
```bash
|
|
49
|
+
npx -y utn-mcp login
|
|
50
|
+
```
|
|
51
|
+
Te pide tu regional, tu usuario y tu contraseña: los mismos con los que entrás al aula virtual. Si no tenés usuario institucional (por ejemplo, en el curso de ingreso), usá el que te dieron para el aula. Se hace una sola vez: después el servidor renueva la sesión solo.
|
|
52
|
+
|
|
53
|
+
### 2. Agregarlo a tu cliente
|
|
54
|
+
|
|
55
|
+
**Claude Code**
|
|
56
|
+
```bash
|
|
57
|
+
claude mcp add utn -- npx -y utn-mcp
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Claude Desktop**: en *Settings → Developer → Edit Config*, agregá dentro de `mcpServers`:
|
|
61
|
+
```json
|
|
62
|
+
"utn": {
|
|
63
|
+
"command": "npx",
|
|
64
|
+
"args": ["-y", "utn-mcp"]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
En Windows, si no arranca, usá `"command": "cmd"` y `"args": ["/c", "npx", "-y", "utn-mcp"]`. Después reiniciá Claude Desktop por completo.
|
|
68
|
+
|
|
69
|
+
**Codex**
|
|
70
|
+
```bash
|
|
71
|
+
codex mcp add utn -- npx -y utn-mcp
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Gemini CLI**
|
|
75
|
+
```bash
|
|
76
|
+
gemini mcp add utn npx -y utn-mcp
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**OpenClaw**
|
|
80
|
+
```bash
|
|
81
|
+
openclaw mcp add utn --command npx --arg -y --arg utn-mcp
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Hermes**: en `~/.hermes/config.yaml`:
|
|
85
|
+
```yaml
|
|
86
|
+
mcp_servers:
|
|
87
|
+
utn:
|
|
88
|
+
command: "npx"
|
|
89
|
+
args: ["-y", "utn-mcp"]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Cursor**: en `~/.cursor/mcp.json`:
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"utn": { "command": "npx", "args": ["-y", "utn-mcp"] }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**VS Code**: en `.vscode/mcp.json` (o con *MCP: Add Server* desde la paleta de comandos):
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"servers": {
|
|
105
|
+
"utn": { "type": "stdio", "command": "npx", "args": ["-y", "utn-mcp"] }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**ChatGPT**: solo se conecta a servidores MCP remotos por HTTPS, así que necesita un servidor en modo HTTP publicado en internet (ver [Hostearlo para otros](#hostearlo-para-otros)). Se agrega como conector desde *Configuración → Apps y conectores*, con el modo desarrollador activado. Los conectores propios están desde el plan Pro, y ahí solo con las herramientas de lectura; para usar también las de escritura (foros, mensajes, entregas) hace falta Business o superior.
|
|
111
|
+
|
|
112
|
+
**Otros clientes (Windsurf, opencode, etc.)**: cualquier cliente MCP que lance servidores por STDIO sirve con el comando `npx -y utn-mcp`.
|
|
113
|
+
|
|
114
|
+
### Instalación global (opcional)
|
|
115
|
+
Si preferís tener el comando `utn-mcp` siempre disponible:
|
|
116
|
+
```bash
|
|
117
|
+
npm i -g utn-mcp
|
|
118
|
+
utn-mcp login
|
|
119
|
+
```
|
|
120
|
+
y en la config del cliente usás `"command": "utn-mcp"` sin argumentos.
|
|
121
|
+
|
|
122
|
+
### Regionales
|
|
123
|
+
La regional se elige al iniciar sesión, de una lista con las regionales de la UTN. Buenos Aires (FRBA), Resistencia (FRRE) y Tucumán (FRT) ya están probadas. El login se adapta solo: si el aula entra por SSO (Keycloak), como FRBA, usa el SSO; si no, el login directo de Moodle.
|
|
124
|
+
|
|
125
|
+
La lista puede traer regionales que nadie probó todavía: su aula tiene habilitado el servicio que usa UTN-MCP, así que casi seguro anda. En un servidor hosteado, la primera persona que entra con una de esas la deja verificada para el resto. Las que no tienen el servicio habilitado aparecen como "todavía sin soporte": se pueden elegir, pero el login avisa que por ahora no se puede usar y no manda la contraseña.
|
|
126
|
+
|
|
127
|
+
Si tu campus no está en la lista, abrí un [issue](https://github.com/nialvape/utn-mcp/issues) con la URL de su aula virtual para sumarlo. Mientras tanto, podés usarlo definiendo en un `.env` en la carpeta desde donde corrés `utn-mcp` (o en el entorno):
|
|
128
|
+
```bash
|
|
129
|
+
UTN_AULA_BASE_URL=https://tu-campus.utn.edu.ar
|
|
130
|
+
# UTN_SSO_HOST=auth.tu-campus.utn.edu.ar # solo si usa Keycloak
|
|
131
|
+
```
|
|
132
|
+
Aparece como una opción más al iniciar sesión.
|
|
133
|
+
|
|
134
|
+
`UTN_REGIONAL` (por ejemplo, `UTN_REGIONAL=frt`) elige la que viene marcada por defecto, y es la que se usa si en lugar de `login` definís `UTN_USER` y `UTN_PASS`.
|
|
135
|
+
|
|
136
|
+
### Modo HTTP (opcional)
|
|
137
|
+
Necesita Node 22.13 o más nuevo. Primero, una vez:
|
|
138
|
+
```bash
|
|
139
|
+
npx -y utn-mcp login --http
|
|
140
|
+
```
|
|
141
|
+
Te muestra un token: configurá tu cliente MCP para que mande el header `Authorization: Bearer <token>`. Después:
|
|
142
|
+
```bash
|
|
143
|
+
npx -y utn-mcp --http --port 3000
|
|
144
|
+
```
|
|
145
|
+
Levanta el servidor en `http://127.0.0.1:3000/mcp`, solo accesible desde tu propia máquina. Tenés que dejar la terminal abierta mientras lo uses.
|
|
146
|
+
|
|
147
|
+
En este modo el servidor no lee archivos de tu disco: para entregar, el agente pide un link con `aula_assign_upload_link` y sube ahí los archivos con `curl` si tiene terminal, o te pasa el link para que los elijas desde el navegador.
|
|
148
|
+
|
|
149
|
+
### Hostearlo para otros
|
|
150
|
+
En modo HTTP el servidor también es servidor de autorización OAuth: un cliente MCP (Claude, ChatGPT) que se conecta a `https://tu-dominio/mcp` recibe un 401, descubre el OAuth y abre una página donde el alumno elige su regional y entra con su usuario de la UTN. Cada alumno usa el aula de su regional, aunque sean de regionales distintas. Ese login vincula su aula: la contraseña se usa una sola vez para sacar el token de Moodle y **no se guarda**. Cuando la sesión del aula vence, el agente le pasa el link de `/login` para renovarla, sin reconectar el cliente.
|
|
151
|
+
|
|
152
|
+
Quién puede entrar lo decidís vos: con `UTN_MCP_ALLOWED_EMAILS` (una lista), o con `UTN_MCP_ACCESS_URL`, que apunta a un servicio tuyo. El servidor le hace `POST` con `{ "email", "usuario", "regional" }` y el header `Authorization: Bearer <UTN_MCP_ACCESS_SECRET>`, y espera `{ "permitido": true }` o `{ "permitido": false, "motivo": "...", "url": "..." }`. Se consulta al conectar y en cada renovación del token (cada hora), así que una baja se aplica sola.
|
|
153
|
+
|
|
154
|
+
La configuración va en variables de entorno o en un `.env` en la carpeta desde donde lo corrés (copiá [`.env.example`](https://github.com/nialvape/utn-mcp/blob/main/.env.example)):
|
|
155
|
+
|
|
156
|
+
| Variable | Para qué | Por defecto |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `HOST`, `PORT` | Dónde escucha. | `127.0.0.1`, `3000` |
|
|
159
|
+
| `UTN_MCP_PUBLIC_URL` | Dirección pública (`https://mcp.tu-dominio.com`). Es el issuer del OAuth, la base de los links de subida y el único host, además de localhost, que se acepta en `Host` y `Origin`. | `http://127.0.0.1:<PORT>` |
|
|
160
|
+
| `UTN_MCP_ACCESS_URL`, `UTN_MCP_ACCESS_SECRET` | Servicio que decide quién puede entrar. | — |
|
|
161
|
+
| `UTN_MCP_ALLOWED_EMAILS` | Emails o dominios (`@frba.utn.edu.ar`) habilitados, separados por coma, si no hay `UTN_MCP_ACCESS_URL`. | Entra cualquiera con usuario de la UTN |
|
|
162
|
+
| `UTN_MCP_REDIRECT_ALLOW` | Hosts (`app.ejemplo.com`) o esquemas (`miapp:`) extra, separados por coma, a los que puede volver el login de OAuth. Loopback, Claude, ChatGPT, VS Code y Cursor pasan siempre. Cualquier otro host HTTPS (un agente propio, por ejemplo) se acepta, pero el login avisa a dónde vuelve y el alumno tiene que marcar que lo conecta bajo su responsabilidad: es la defensa contra alguien que use tu página de login para quedarse con su sesión. Los hosts de esta lista pasan sin aviso. | — |
|
|
163
|
+
| `UTN_MCP_ADMIN_SECRET` | Habilita la API de `/admin` para un panel de cuentas tuyo: `POST /admin/usuarios` (todos los usuarios con sus apps), `/admin/clientes` y `/admin/clientes/revocar` (las apps de un usuario, por email), `/admin/regionales` (las regionales con su soporte) y `/admin/verificar` (entra al aula con usuario y contraseña, como el login de OAuth, y le guarda la sesión). | — |
|
|
164
|
+
| `UTN_MCP_LOGIN_URL` | Página donde el usuario renueva su sesión del aula; el agente se la pasa. | `<UTN_MCP_PUBLIC_URL>/login` |
|
|
165
|
+
| `UTN_MCP_DB`, `UTN_MCP_KEY` | Base de sesiones y su clave de cifrado (32 bytes en base64). | En la carpeta de config |
|
|
166
|
+
|
|
167
|
+
Con Docker, detrás de un reverse proxy con HTTPS en la misma máquina (red host, para que el rate limit del login vea la IP real):
|
|
168
|
+
```bash
|
|
169
|
+
docker build -t utn-mcp .
|
|
170
|
+
docker run -d --name utn-mcp --network host --restart unless-stopped \
|
|
171
|
+
-e HOST=127.0.0.1 -e PORT=8100 -e UTN_MCP_PUBLIC_URL=https://mcp.tu-dominio.com \
|
|
172
|
+
-e UTN_MCP_KEY=$(openssl rand -base64 32) -v utn-mcp-data:/data utn-mcp
|
|
173
|
+
```
|
|
174
|
+
Guardá esa `UTN_MCP_KEY`: sin ella no se pueden descifrar las sesiones guardadas.
|
|
175
|
+
|
|
176
|
+
## Dónde se guardan tus datos
|
|
177
|
+
Instalado en tu computadora, todo queda ahí:
|
|
178
|
+
- **Usuario y contraseña**: en el gestor de credenciales del sistema (Windows Credential Manager, Keychain en macOS, Secret Service en Linux).
|
|
179
|
+
- **Token del aula virtual**: en `%APPDATA%\utn-mcp\session.json` (Windows) o `~/.config/utn-mcp/session.json` (Linux y macOS).
|
|
180
|
+
- **En modo HTTP**: la contraseña no se guarda; la sesión va cifrada en `utn-mcp.db`, en la misma carpeta, con la clave de `store.key` (o la de `UTN_MCP_KEY`). Del token del cliente solo se guarda el hash.
|
|
181
|
+
|
|
182
|
+
En Linux sin gestor de credenciales (servidores, Docker, WSL) podés definir las variables de entorno `UTN_USER` y `UTN_PASS` en lugar de usar `login`.
|
|
183
|
+
|
|
184
|
+
Si usás un servidor hosteado por otra persona, tu sesión del aula la guarda ese servidor (cifrada, y sin tu contraseña): revisá su política de privacidad.
|
|
185
|
+
|
|
186
|
+
## Desarrollo
|
|
187
|
+
```bash
|
|
188
|
+
git clone https://github.com/nialvape/utn-mcp.git
|
|
189
|
+
cd utn-mcp
|
|
190
|
+
npm install
|
|
191
|
+
npm run login # una vez
|
|
192
|
+
npm run dev # servidor por STDIO desde src/, con tsx
|
|
193
|
+
npm run build # compila a build/
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
pdf.js no es una dependencia del paquete publicado. Está como `devDependency`, y `npm install` corre el script `prepare`, que copia a `vendor/pdfjs/` solo los archivos que usa el servidor (~4 MB). Así, quien instala `utn-mcp` no baja pdf.js entero ni su canvas nativo opcional (~37 MB). Si instalaste con `--ignore-scripts`, generalo a mano con `npm run prepare`.
|
|
197
|
+
|
|
198
|
+
## Aviso
|
|
199
|
+
Este proyecto no está afiliado ni avalado por la UTN. Usalo bajo tu responsabilidad.
|
|
@@ -4,12 +4,12 @@ export function getSiteInfo(session) {
|
|
|
4
4
|
}
|
|
5
5
|
/** El usuario dueño del token, con su email institucional (site_info no lo trae). */
|
|
6
6
|
export async function getOwnUser(session) {
|
|
7
|
-
const { userid, username } = await getSiteInfo(session);
|
|
7
|
+
const { userid, username, fullname } = await getSiteInfo(session);
|
|
8
8
|
const [user] = await callWs(session, "core_user_get_users_by_field", {
|
|
9
9
|
field: "id",
|
|
10
10
|
"values[0]": userid,
|
|
11
11
|
});
|
|
12
|
-
return { username, email: user?.email || null };
|
|
12
|
+
return { username, fullname, email: user?.email || null };
|
|
13
13
|
}
|
|
14
14
|
export async function getUserCourses(session) {
|
|
15
15
|
const { userid } = await getSiteInfo(session);
|
|
@@ -1,27 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getOwnUser } from "./client.js";
|
|
1
|
+
import { getOwnUser, getUserCourses } from "./client.js";
|
|
3
2
|
import { fetchToken } from "./sso-login.js";
|
|
4
3
|
import { AULA_PLATFORM } from "./stored-session.js";
|
|
5
4
|
/**
|
|
6
|
-
* Entra al aula con usuario y contraseña, crea el usuario del store si no existe y le
|
|
7
|
-
* sesión (token
|
|
8
|
-
* Lo usan `utn-mcp login --http
|
|
5
|
+
* Entra al aula de la regional con usuario y contraseña, crea el usuario del store si no existe y le
|
|
6
|
+
* guarda la sesión (el token). La contraseña se usa solo acá: no se guarda en ningún lado.
|
|
7
|
+
* Lo usan `utn-mcp login --http`, el login por OAuth de la versión hosteada y `/admin/verificar`.
|
|
8
|
+
* `regional` tiene que venir con su soporte en este servidor (`availableRegionals` con las verificadas).
|
|
9
9
|
*/
|
|
10
|
-
export async function linkAulaAccount(store, credentials) {
|
|
11
|
-
const
|
|
12
|
-
const token = await fetchToken(jar, credentials);
|
|
10
|
+
export async function linkAulaAccount(store, regional, credentials) {
|
|
11
|
+
const token = await fetchToken(regional, credentials);
|
|
13
12
|
// El token recién emitido: si falla, no hay con qué renovarlo.
|
|
14
13
|
const fresh = {
|
|
14
|
+
getRegional: async () => regional,
|
|
15
15
|
getToken: async () => token,
|
|
16
16
|
refreshToken: async () => {
|
|
17
17
|
throw new Error("El aula rechazó el token recién emitido.");
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
const own = await getOwnUser(fresh);
|
|
21
|
+
// La primera vez que alguien entra a una regional sin verificar se prueban también los cursos: si
|
|
22
|
+
// anda, queda verificada para todos los de este servidor.
|
|
23
|
+
if (regional.support === "candidate") {
|
|
24
|
+
await getUserCourses(fresh);
|
|
25
|
+
await store.markRegionalVerified(regional.id);
|
|
26
|
+
}
|
|
21
27
|
// El de Moodle, en minúscula, igual que en `users`: es con lo que se compara al renovar.
|
|
22
28
|
const username = own.username.toLowerCase();
|
|
23
|
-
const userId = await store.findOrCreateUser(username, own.email ?? undefined);
|
|
24
|
-
const data = { username, token
|
|
29
|
+
const userId = await store.findOrCreateUser(regional.id, username, own.email ?? undefined);
|
|
30
|
+
const data = { username, token };
|
|
25
31
|
await store.saveSession(userId, AULA_PLATFORM, JSON.stringify(data));
|
|
26
|
-
return { userId, username, email: own.email };
|
|
32
|
+
return { userId, regional, username, fullname: own.fullname, email: own.email };
|
|
27
33
|
}
|
|
@@ -1,35 +1,43 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { CookieJar } from "tough-cookie";
|
|
4
3
|
import { sessionFile } from "../../shared/paths.js";
|
|
5
4
|
import { logger } from "../../shared/logger.js";
|
|
6
5
|
import { loadCredentials } from "./credentials.js";
|
|
7
|
-
import {
|
|
6
|
+
import { defaultRegional, findRegional } from "./regionals.js";
|
|
7
|
+
import { fetchToken, LoginRequiredError } from "./sso-login.js";
|
|
8
8
|
/**
|
|
9
9
|
* Sesión del modo local: un solo usuario, guardada en disco.
|
|
10
|
-
* Credenciales en el keychain (o UTN_USER/UTN_PASS); token y
|
|
10
|
+
* Credenciales en el keychain (o UTN_USER/UTN_PASS); el token y la regional en session.json.
|
|
11
11
|
*/
|
|
12
12
|
export class LocalSession {
|
|
13
13
|
refreshing = null;
|
|
14
|
+
/**
|
|
15
|
+
* La elegida en `utn-mcp login`: la de session.json o, si se borró, la guardada con las credenciales.
|
|
16
|
+
* Sin login (UTN_USER/UTN_PASS) o de antes de elegirla, la de UTN_REGIONAL.
|
|
17
|
+
*/
|
|
18
|
+
async getRegional() {
|
|
19
|
+
const regional = (await this.read()).regional ?? (await loadCredentials())?.regional;
|
|
20
|
+
return regional ? findRegional(regional) : defaultRegional();
|
|
21
|
+
}
|
|
14
22
|
async getToken() {
|
|
15
23
|
return (await this.read()).token ?? this.refreshToken();
|
|
16
24
|
}
|
|
17
25
|
/** Una sola renovación en curso aunque varias tools fallen a la vez. */
|
|
18
26
|
refreshToken() {
|
|
19
|
-
this.refreshing ??= loadCredentials()
|
|
20
|
-
.then((creds) => this.login(creds))
|
|
27
|
+
this.refreshing ??= Promise.all([this.getRegional(), loadCredentials()])
|
|
28
|
+
.then(([regional, creds]) => this.login(regional, creds))
|
|
21
29
|
.finally(() => {
|
|
22
30
|
this.refreshing = null;
|
|
23
31
|
});
|
|
24
32
|
return this.refreshing;
|
|
25
33
|
}
|
|
26
|
-
/** Obtiene un token nuevo y lo guarda
|
|
27
|
-
async login(credentials) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const token = await fetchToken(
|
|
32
|
-
await this.write({ token, obtainedAt: new Date().toISOString(),
|
|
34
|
+
/** Obtiene un token nuevo y lo guarda. */
|
|
35
|
+
async login(regional, credentials) {
|
|
36
|
+
if (!credentials)
|
|
37
|
+
throw new LoginRequiredError();
|
|
38
|
+
logger.info(`Obteniendo token del aula virtual de ${regional.name}...`);
|
|
39
|
+
const token = await fetchToken(regional, credentials);
|
|
40
|
+
await this.write({ token, obtainedAt: new Date().toISOString(), regional: regional.id });
|
|
33
41
|
return token;
|
|
34
42
|
}
|
|
35
43
|
async read() {
|
|
@@ -37,7 +45,7 @@ export class LocalSession {
|
|
|
37
45
|
return JSON.parse(await readFile(sessionFile(), "utf8"));
|
|
38
46
|
}
|
|
39
47
|
catch {
|
|
40
|
-
return { token: null, obtainedAt: null
|
|
48
|
+
return { token: null, obtainedAt: null };
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
51
|
async write(data) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { postForm } from "../../shared/http.js";
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const UPLOAD_URL = `${AULA_BASE_URL}/webservice/upload.php`;
|
|
2
|
+
const WS_PATH = "/webservice/rest/server.php";
|
|
3
|
+
const UPLOAD_PATH = "/webservice/upload.php";
|
|
5
4
|
const TOKEN_ERRORS = new Set(["invalidtoken", "expiredtoken"]);
|
|
6
5
|
export class MoodleError extends Error {
|
|
7
6
|
errorcode;
|
|
@@ -14,9 +13,10 @@ export class MoodleError extends Error {
|
|
|
14
13
|
const isException = (data) => typeof data === "object" && data !== null && "exception" in data;
|
|
15
14
|
/** Llama a una función del web service de Moodle; renueva el token una vez si venció. */
|
|
16
15
|
export async function callWs(session, wsfunction, params = {}) {
|
|
16
|
+
const { baseUrl } = await session.getRegional();
|
|
17
17
|
let token = await session.getToken();
|
|
18
18
|
for (let attempt = 0;; attempt++) {
|
|
19
|
-
const data = await postForm(
|
|
19
|
+
const data = await postForm(baseUrl + WS_PATH, {
|
|
20
20
|
...params,
|
|
21
21
|
wstoken: token,
|
|
22
22
|
wsfunction,
|
|
@@ -66,6 +66,7 @@ export async function fetchFile(session, url, maxBytes) {
|
|
|
66
66
|
* que es lo que piden las funciones que guardan archivos. Todos quedan en el mismo borrador.
|
|
67
67
|
*/
|
|
68
68
|
export async function uploadDraftFiles(session, files) {
|
|
69
|
+
const { baseUrl } = await session.getRegional();
|
|
69
70
|
let token = await session.getToken();
|
|
70
71
|
for (let attempt = 0;; attempt++) {
|
|
71
72
|
const form = new FormData();
|
|
@@ -75,7 +76,7 @@ export async function uploadDraftFiles(session, files) {
|
|
|
75
76
|
files.forEach((f, i) => form.set(`file_${i + 1}`, new Blob([new Uint8Array(f.bytes)]), f.name));
|
|
76
77
|
let res;
|
|
77
78
|
try {
|
|
78
|
-
res = await fetch(
|
|
79
|
+
res = await fetch(baseUrl + UPLOAD_PATH, { method: "POST", body: form, signal: AbortSignal.timeout(120_000) });
|
|
79
80
|
}
|
|
80
81
|
catch (e) {
|
|
81
82
|
throw new MoodleError(`No se pudo subir el archivo: ${e.message}`);
|