utn-mcp 0.6.9 → 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 +51 -8
- package/build/aula-virtual/client/client.js +9 -0
- package/build/aula-virtual/client/link.js +33 -0
- 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 +36 -22
- package/build/aula-virtual/format/actions.js +1 -2
- package/build/aula-virtual/format/files.js +30 -11
- package/build/aula-virtual/login.js +25 -18
- package/build/aula-virtual/tools.js +11 -5
- package/build/http.js +90 -29
- package/build/index.js +28 -5
- package/build/oauth/access.js +52 -0
- package/build/oauth/admin.js +110 -0
- package/build/oauth/expiring.js +57 -0
- package/build/oauth/redirects.js +60 -0
- package/build/oauth/server.js +341 -0
- 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 +66 -0
- package/build/shared/paths.js +1 -1
- package/build/shared/store/sqlite-store.js +135 -8
- package/build/shared/uploads.js +9 -1
- package/package.json +58 -58
- package/build/aula-virtual/client/constants.js +0 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Los colores del sitio del proyecto: papel milimetrado, tinta y un azul de acento. Oscuro salvo que
|
|
3
|
+
* el sistema pida claro. Sin fuentes externas: la página de login recibe contraseñas.
|
|
4
|
+
*/
|
|
5
|
+
const STYLE = `
|
|
6
|
+
:root{--paper:#0a0b0d;--ink:#e9e7e0;--muted:#8b8a84;--line:rgb(233 231 224/.055);--line-faint:rgb(233 231 224/.025);
|
|
7
|
+
--line-strong:rgb(233 231 224/.16);--accent:#6b93ff;--err:#f87171;--panel:rgb(20 21 24/.6);color-scheme:dark}
|
|
8
|
+
@media(prefers-color-scheme:light){:root{--paper:#f3f2ec;--ink:#121212;--muted:#6d6c66;--line:rgb(18 18 18/.07);
|
|
9
|
+
--line-faint:rgb(18 18 18/.035);--line-strong:rgb(18 18 18/.2);--accent:#1f4fd6;--err:#b91c1c;--panel:rgb(255 255 255/.55);color-scheme:light}}
|
|
10
|
+
*{box-sizing:border-box}
|
|
11
|
+
body{margin:0;min-height:100vh;color:var(--ink);font:15px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
12
|
+
background-color:var(--paper);background-image:linear-gradient(var(--line) 1px,transparent 1px),
|
|
13
|
+
linear-gradient(90deg,var(--line) 1px,transparent 1px),linear-gradient(var(--line-faint) 1px,transparent 1px),
|
|
14
|
+
linear-gradient(90deg,var(--line-faint) 1px,transparent 1px);background-size:80px 80px,80px 80px,16px 16px,16px 16px;
|
|
15
|
+
background-position:-1px -1px}
|
|
16
|
+
.mono,label,button,.kicker,header{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
17
|
+
header{display:flex;align-items:center;gap:.5rem;height:3.5rem;padding:0 1rem;border-bottom:1px solid var(--line);
|
|
18
|
+
font-size:13px;letter-spacing:-.01em;background:color-mix(in srgb,var(--paper) 75%,transparent)}
|
|
19
|
+
header svg{height:1.5rem;width:auto}
|
|
20
|
+
main{max-width:28rem;margin:3rem auto;padding:0 1rem}
|
|
21
|
+
.card{position:relative;border:1px solid var(--line-strong);background:var(--panel);padding:1.5rem}
|
|
22
|
+
.card::before,.card::after{content:"";position:absolute;width:9px;height:9px;border-color:var(--ink);opacity:.55}
|
|
23
|
+
.card::before{top:-1px;left:-1px;border-top:1px solid;border-left:1px solid}
|
|
24
|
+
.card::after{bottom:-1px;right:-1px;border-bottom:1px solid;border-right:1px solid}
|
|
25
|
+
.kicker{margin:0 0 .5rem;font-size:10px;text-transform:uppercase;letter-spacing:.16em;color:var(--muted)}
|
|
26
|
+
h1{margin:0 0 1rem;font-size:1.5rem;font-weight:600;letter-spacing:-.02em;line-height:1.2}
|
|
27
|
+
p{margin:.75rem 0}
|
|
28
|
+
a{color:var(--accent);text-underline-offset:4px}
|
|
29
|
+
form{display:flex;flex-direction:column;gap:1rem;margin:1.5rem 0}
|
|
30
|
+
label{display:flex;flex-direction:column;gap:.4rem;font-size:10px;text-transform:uppercase;letter-spacing:.16em;color:var(--muted)}
|
|
31
|
+
input,select{font:inherit;font-size:15px;letter-spacing:normal;text-transform:none;color:var(--ink);background:transparent;
|
|
32
|
+
border:1px solid var(--line-strong);border-radius:0;padding:.65rem .75rem}
|
|
33
|
+
input:focus,select:focus{outline:none;border-color:var(--accent)}
|
|
34
|
+
select option{background:var(--paper);color:var(--ink)}
|
|
35
|
+
input[type=file]{padding:.5rem}
|
|
36
|
+
button{font-size:12px;text-transform:uppercase;letter-spacing:.14em;border:0;border-radius:0;padding:.8rem 1rem;
|
|
37
|
+
background:var(--ink);color:var(--paper);cursor:pointer;transition:background .15s}
|
|
38
|
+
button:hover{background:var(--accent)}
|
|
39
|
+
.nota{font-size:13px;color:var(--muted)}
|
|
40
|
+
.error{border-left:2px solid var(--err);padding-left:.75rem;color:var(--err)}
|
|
41
|
+
.aviso{border:1px solid var(--err);padding:0 1rem;margin:1rem 0}
|
|
42
|
+
label.acepto{flex-direction:row;align-items:flex-start;gap:.6rem;font:13px/1.5 ui-sans-serif,system-ui,sans-serif;
|
|
43
|
+
text-transform:none;letter-spacing:normal;color:var(--ink)}
|
|
44
|
+
input[type=checkbox]{padding:0;margin:.2rem 0 0;accent-color:var(--accent)}
|
|
45
|
+
ul{padding-left:1.2rem}
|
|
46
|
+
::selection{background:var(--accent);color:var(--paper)}
|
|
47
|
+
`.replace(/\n/g, "");
|
|
48
|
+
/** El emblema del proyecto: el de la UTN con "MCP" en lugar de "UTN". */
|
|
49
|
+
const MARK = '<svg viewBox="0 0 100 132" fill="none" aria-hidden="true"><g stroke="currentColor" stroke-width="11">' +
|
|
50
|
+
'<path d="M50 0v100M0 50h100"/><path d="M19 0v19a11 11 0 0 0 11 11h40a11 11 0 0 0 11-11V0"/>' +
|
|
51
|
+
'<path d="M19 100V81a11 11 0 0 1 11-11h40a11 11 0 0 1 11 11v19"/></g>' +
|
|
52
|
+
'<text x="50" y="131" text-anchor="middle" textLength="72" lengthAdjust="spacingAndGlyphs" ' +
|
|
53
|
+
'font-family="Arial,Helvetica,sans-serif" font-weight="900" font-size="29" fill="currentColor">MCP</text></svg>';
|
|
54
|
+
/**
|
|
55
|
+
* Página mínima con el estilo del sitio del proyecto: se abre desde el celu, así que nada más que lo necesario.
|
|
56
|
+
* `kicker` es la etiqueta chica arriba del título.
|
|
57
|
+
*/
|
|
58
|
+
export function page(title, body, kicker = "UTN-MCP") {
|
|
59
|
+
return (`<!doctype html><html lang="es"><head><meta charset="utf-8">` +
|
|
60
|
+
`<meta name="viewport" content="width=device-width, initial-scale=1"><title>${escapeHtml(title)} · UTN-MCP</title>` +
|
|
61
|
+
`<style>${STYLE}</style></head><body>` +
|
|
62
|
+
`<header>${MARK}utn·mcp</header>` +
|
|
63
|
+
`<main><div class="card"><p class="kicker">${escapeHtml(kicker)}</p><h1>${escapeHtml(title)}</h1>${body}</div></main>` +
|
|
64
|
+
`</body></html>`);
|
|
65
|
+
}
|
|
66
|
+
export const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
package/build/shared/paths.js
CHANGED
|
@@ -9,7 +9,7 @@ export function configDir() {
|
|
|
9
9
|
}
|
|
10
10
|
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "utn-mcp");
|
|
11
11
|
}
|
|
12
|
-
/** Token de Moodle
|
|
12
|
+
/** Token de Moodle del modo local. */
|
|
13
13
|
export const sessionFile = () => join(configDir(), "session.json");
|
|
14
14
|
/** Base de usuarios, tokens y sesiones del modo HTTP: UTN_MCP_DB o un SQLite en la carpeta de configuración. */
|
|
15
15
|
export const storeFile = () => process.env.UTN_MCP_DB ?? join(configDir(), "utn-mcp.db");
|
|
@@ -5,8 +5,11 @@ import { storeFile, storeKeyFile } from "../paths.js";
|
|
|
5
5
|
const SCHEMA = `
|
|
6
6
|
CREATE TABLE IF NOT EXISTS users (
|
|
7
7
|
id INTEGER PRIMARY KEY,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
regional TEXT NOT NULL,
|
|
9
|
+
utn_username TEXT NOT NULL,
|
|
10
|
+
email TEXT,
|
|
11
|
+
created_at INTEGER NOT NULL,
|
|
12
|
+
UNIQUE (regional, utn_username)
|
|
10
13
|
);
|
|
11
14
|
CREATE TABLE IF NOT EXISTS tokens (
|
|
12
15
|
hash TEXT PRIMARY KEY,
|
|
@@ -24,6 +27,16 @@ const SCHEMA = `
|
|
|
24
27
|
updated_at INTEGER NOT NULL,
|
|
25
28
|
PRIMARY KEY (user_id, platform)
|
|
26
29
|
);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS verified_regionals (
|
|
31
|
+
regional TEXT PRIMARY KEY,
|
|
32
|
+
verified_at INTEGER NOT NULL
|
|
33
|
+
);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS oauth_clients (
|
|
35
|
+
client_id TEXT PRIMARY KEY,
|
|
36
|
+
client_name TEXT,
|
|
37
|
+
redirect_uris TEXT NOT NULL,
|
|
38
|
+
created_at INTEGER NOT NULL
|
|
39
|
+
);
|
|
27
40
|
`;
|
|
28
41
|
/** Bytes de la clave AES-256. */
|
|
29
42
|
const KEY_BYTES = 32;
|
|
@@ -34,8 +47,8 @@ const TAG_BYTES = 16;
|
|
|
34
47
|
const now = () => Math.floor(Date.now() / 1000);
|
|
35
48
|
const hashToken = (token) => createHash("sha256").update(token).digest("hex");
|
|
36
49
|
/**
|
|
37
|
-
* El SessionStore sobre SQLite. Las sesiones van cifradas con AES-256-GCM: tienen el token de Moodle
|
|
38
|
-
*
|
|
50
|
+
* El SessionStore sobre SQLite. Las sesiones van cifradas con AES-256-GCM: tienen el token de Moodle,
|
|
51
|
+
* que alcanza para entrar al aula como el usuario. Los tokens se guardan
|
|
39
52
|
* hasheados, así que una copia de la base no sirve para usar el servidor.
|
|
40
53
|
*/
|
|
41
54
|
export class SqliteStore {
|
|
@@ -59,16 +72,42 @@ export class SqliteStore {
|
|
|
59
72
|
// WAL: el login escribe mientras el servidor lee, desde otro proceso.
|
|
60
73
|
db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 5000;");
|
|
61
74
|
db.exec(SCHEMA);
|
|
75
|
+
migrate(db);
|
|
62
76
|
return new SqliteStore(db, await loadKey());
|
|
63
77
|
}
|
|
64
|
-
async findOrCreateUser(utnUsername) {
|
|
78
|
+
async findOrCreateUser(regional, utnUsername, email) {
|
|
65
79
|
const username = utnUsername.trim().toLowerCase();
|
|
66
80
|
this.db
|
|
67
|
-
.prepare("INSERT INTO users (utn_username, created_at) VALUES (?, ?) ON CONFLICT DO NOTHING")
|
|
68
|
-
.run(username, now());
|
|
69
|
-
|
|
81
|
+
.prepare("INSERT INTO users (regional, utn_username, created_at) VALUES (?, ?, ?) ON CONFLICT DO NOTHING")
|
|
82
|
+
.run(regional, username, now());
|
|
83
|
+
if (email) {
|
|
84
|
+
this.db
|
|
85
|
+
.prepare("UPDATE users SET email = ? WHERE regional = ? AND utn_username = ?")
|
|
86
|
+
.run(email.trim().toLowerCase(), regional, username);
|
|
87
|
+
}
|
|
88
|
+
const row = this.db
|
|
89
|
+
.prepare("SELECT id FROM users WHERE regional = ? AND utn_username = ?")
|
|
90
|
+
.get(regional, username);
|
|
70
91
|
return row.id;
|
|
71
92
|
}
|
|
93
|
+
async findUserByEmail(email) {
|
|
94
|
+
const row = this.db
|
|
95
|
+
.prepare("SELECT id, regional, utn_username, email FROM users WHERE email = ?")
|
|
96
|
+
.get(email.trim().toLowerCase());
|
|
97
|
+
return row ? toUser(row) : null;
|
|
98
|
+
}
|
|
99
|
+
async listUsers() {
|
|
100
|
+
const rows = this.db
|
|
101
|
+
.prepare("SELECT id, regional, utn_username, email, created_at FROM users ORDER BY created_at DESC")
|
|
102
|
+
.all();
|
|
103
|
+
return rows.map((r) => ({ ...toUser(r), createdAt: r.created_at }));
|
|
104
|
+
}
|
|
105
|
+
async findUser(userId) {
|
|
106
|
+
const row = this.db
|
|
107
|
+
.prepare("SELECT id, regional, utn_username, email FROM users WHERE id = ?")
|
|
108
|
+
.get(userId);
|
|
109
|
+
return row ? toUser(row) : null;
|
|
110
|
+
}
|
|
72
111
|
async issueToken(userId, options) {
|
|
73
112
|
const token = randomBytes(32).toString("base64url");
|
|
74
113
|
const createdAt = now();
|
|
@@ -99,6 +138,46 @@ export class SqliteStore {
|
|
|
99
138
|
"ON CONFLICT (user_id, platform) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at")
|
|
100
139
|
.run(userId, platform, this.encrypt(data), now());
|
|
101
140
|
}
|
|
141
|
+
async listVerifiedRegionals() {
|
|
142
|
+
const rows = this.db.prepare("SELECT regional FROM verified_regionals").all();
|
|
143
|
+
return rows.map((r) => r.regional);
|
|
144
|
+
}
|
|
145
|
+
async markRegionalVerified(regional) {
|
|
146
|
+
this.db
|
|
147
|
+
.prepare("INSERT INTO verified_regionals (regional, verified_at) VALUES (?, ?) ON CONFLICT DO NOTHING")
|
|
148
|
+
.run(regional, now());
|
|
149
|
+
}
|
|
150
|
+
async saveClient(client) {
|
|
151
|
+
this.db
|
|
152
|
+
.prepare("INSERT INTO oauth_clients (client_id, client_name, redirect_uris, created_at) VALUES (?, ?, ?, ?)")
|
|
153
|
+
.run(client.clientId, client.clientName, JSON.stringify(client.redirectUris), now());
|
|
154
|
+
}
|
|
155
|
+
async findClient(clientId) {
|
|
156
|
+
const row = this.db
|
|
157
|
+
.prepare("SELECT client_id, client_name, redirect_uris FROM oauth_clients WHERE client_id = ?")
|
|
158
|
+
.get(clientId);
|
|
159
|
+
if (!row)
|
|
160
|
+
return null;
|
|
161
|
+
return { clientId: row.client_id, clientName: row.client_name, redirectUris: JSON.parse(row.redirect_uris) };
|
|
162
|
+
}
|
|
163
|
+
async sessionUpdatedAt(userId, platform) {
|
|
164
|
+
const row = this.db
|
|
165
|
+
.prepare("SELECT updated_at FROM platform_sessions WHERE user_id = ? AND platform = ?")
|
|
166
|
+
.get(userId, platform);
|
|
167
|
+
return row?.updated_at ?? null;
|
|
168
|
+
}
|
|
169
|
+
async listConnectedClients(userId) {
|
|
170
|
+
const rows = this.db
|
|
171
|
+
.prepare("SELECT t.client_id, c.client_name, MAX(t.created_at) AS last_token_at FROM tokens t " +
|
|
172
|
+
"LEFT JOIN oauth_clients c ON c.client_id = t.client_id " +
|
|
173
|
+
"WHERE t.user_id = ? AND t.client_id IS NOT NULL AND (t.expires_at IS NULL OR t.expires_at > ?) " +
|
|
174
|
+
"GROUP BY t.client_id ORDER BY last_token_at DESC")
|
|
175
|
+
.all(userId, now());
|
|
176
|
+
return rows.map((r) => ({ clientId: r.client_id, clientName: r.client_name, lastTokenAt: r.last_token_at }));
|
|
177
|
+
}
|
|
178
|
+
async revokeClient(userId, clientId) {
|
|
179
|
+
this.db.prepare("DELETE FROM tokens WHERE user_id = ? AND client_id = ?").run(userId, clientId);
|
|
180
|
+
}
|
|
102
181
|
/** iv + tag + texto cifrado, en un solo blob. */
|
|
103
182
|
encrypt(plain) {
|
|
104
183
|
const iv = randomBytes(IV_BYTES);
|
|
@@ -118,6 +197,54 @@ export class SqliteStore {
|
|
|
118
197
|
}
|
|
119
198
|
}
|
|
120
199
|
}
|
|
200
|
+
const toUser = (row) => ({
|
|
201
|
+
id: row.id,
|
|
202
|
+
regional: row.regional,
|
|
203
|
+
utnUsername: row.utn_username,
|
|
204
|
+
email: row.email,
|
|
205
|
+
});
|
|
206
|
+
/** Lo que `CREATE TABLE IF NOT EXISTS` no agrega a una base que ya existía. */
|
|
207
|
+
function migrate(db) {
|
|
208
|
+
const columns = db.prepare("PRAGMA table_info(users)").all();
|
|
209
|
+
if (!columns.some((c) => c.name === "email"))
|
|
210
|
+
db.exec("ALTER TABLE users ADD COLUMN email TEXT");
|
|
211
|
+
if (!columns.some((c) => c.name === "regional"))
|
|
212
|
+
addRegional(db);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Antes de elegir la regional al entrar, todos los usuarios de un servidor eran de la que tenía
|
|
216
|
+
* configurada (UTN_REGIONAL, o FRBA). La unicidad pasa de `utn_username` a regional + usuario, y
|
|
217
|
+
* SQLite no cambia restricciones con ALTER TABLE: se rehace la tabla, conservando los ids.
|
|
218
|
+
*/
|
|
219
|
+
function addRegional(db) {
|
|
220
|
+
const legacy = process.env.UTN_REGIONAL?.trim().toLowerCase() || "frba";
|
|
221
|
+
// Sin esto, borrar `users` borraría en cascada los tokens y las sesiones.
|
|
222
|
+
db.exec("PRAGMA foreign_keys = OFF");
|
|
223
|
+
try {
|
|
224
|
+
db.exec("BEGIN");
|
|
225
|
+
db.exec(`
|
|
226
|
+
CREATE TABLE users_new (
|
|
227
|
+
id INTEGER PRIMARY KEY,
|
|
228
|
+
regional TEXT NOT NULL,
|
|
229
|
+
utn_username TEXT NOT NULL,
|
|
230
|
+
email TEXT,
|
|
231
|
+
created_at INTEGER NOT NULL,
|
|
232
|
+
UNIQUE (regional, utn_username)
|
|
233
|
+
);
|
|
234
|
+
`);
|
|
235
|
+
db.prepare("INSERT INTO users_new (id, regional, utn_username, email, created_at) " +
|
|
236
|
+
"SELECT id, ?, utn_username, email, created_at FROM users").run(legacy);
|
|
237
|
+
db.exec("DROP TABLE users; ALTER TABLE users_new RENAME TO users;");
|
|
238
|
+
db.exec("COMMIT");
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
db.exec("ROLLBACK");
|
|
242
|
+
throw e;
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
121
248
|
/**
|
|
122
249
|
* UTN_MCP_KEY (32 bytes en base64) en el servidor hosteado, donde la clave no tiene que estar en el
|
|
123
250
|
* mismo disco que la base. En local, si no está, se genera una en la carpeta de configuración.
|
package/build/shared/uploads.js
CHANGED
|
@@ -4,6 +4,8 @@ import { extname } from "node:path";
|
|
|
4
4
|
const UPLOAD_TTL_MS = 30 * 60 * 1000;
|
|
5
5
|
/** Tope de una subida entera, en bytes: los archivos quedan en memoria hasta que se entregan. */
|
|
6
6
|
export const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
7
|
+
/** Links abiertos por usuario: con más, se cierra el más viejo. */
|
|
8
|
+
const MAX_OPEN_PER_OWNER = 5;
|
|
7
9
|
/**
|
|
8
10
|
* Links de un solo uso para recibir archivos por HTTP: los sube el agente con `curl` si tiene
|
|
9
11
|
* terminal, o el usuario desde el navegador. Los archivos quedan en memoria hasta que una tool los
|
|
@@ -17,6 +19,10 @@ export class UploadInbox {
|
|
|
17
19
|
}
|
|
18
20
|
open(owner, title, limits) {
|
|
19
21
|
this.sweep();
|
|
22
|
+
// El Map conserva el orden de inserción: el primero que aparece es el más viejo.
|
|
23
|
+
const own = [...this.uploads].filter(([, u]) => u.owner === owner);
|
|
24
|
+
for (const [oldId] of own.slice(0, Math.max(0, own.length - MAX_OPEN_PER_OWNER + 1)))
|
|
25
|
+
this.uploads.delete(oldId);
|
|
20
26
|
const id = randomBytes(24).toString("base64url");
|
|
21
27
|
const expiresAt = Date.now() + UPLOAD_TTL_MS;
|
|
22
28
|
this.uploads.set(id, { owner, title, limits, files: null, expiresAt });
|
|
@@ -27,11 +33,13 @@ export class UploadInbox {
|
|
|
27
33
|
this.sweep();
|
|
28
34
|
return this.uploads.get(id);
|
|
29
35
|
}
|
|
30
|
-
/** Guarda los archivos de una subida;
|
|
36
|
+
/** Guarda los archivos de una subida; cada link acepta una sola. */
|
|
31
37
|
receive(id, files) {
|
|
32
38
|
const upload = this.uploads.get(id);
|
|
33
39
|
if (!upload || upload.expiresAt < Date.now())
|
|
34
40
|
throw new Error("El link de subida venció o no existe.");
|
|
41
|
+
if (upload.files)
|
|
42
|
+
throw new Error("Este link ya se usó. Pedile al agente uno nuevo.");
|
|
35
43
|
if (!files.length)
|
|
36
44
|
throw new Error('No llegó ningún archivo: van en el campo "archivos".');
|
|
37
45
|
checkFiles(files, upload.limits);
|
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "utn-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Este es el Model Context Protocol server NO OFICIAL de la UTN para buscar informacion o archivos en el aula virtual.",
|
|
5
|
-
"main": "build/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"utn-mcp": "build/index.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"build",
|
|
11
|
-
"vendor"
|
|
12
|
-
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"prebuild": "node -e \"require('fs').rmSync('build',{recursive:true,force:true})\"",
|
|
15
|
-
"build": "tsc",
|
|
16
|
-
"prepublishOnly": "npm run build",
|
|
17
|
-
"prepare": "node scripts/vendor-pdfjs.mjs",
|
|
18
|
-
"start": "node build/index.js",
|
|
19
|
-
"dev": "tsx src/index.ts",
|
|
20
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
21
|
-
"login": "tsx src/index.ts login"
|
|
22
|
-
},
|
|
23
|
-
"repository": {
|
|
24
|
-
"type": "git",
|
|
25
|
-
"url": "git+https://github.com/nialvape/utn-mcp.git"
|
|
26
|
-
},
|
|
27
|
-
"keywords": [
|
|
28
|
-
"mcp",
|
|
29
|
-
"model-context-protocol",
|
|
30
|
-
"utn",
|
|
31
|
-
"aula-virtual"
|
|
32
|
-
],
|
|
33
|
-
"author": "Joaquín Peñalva (https://github.com/nialvape)",
|
|
34
|
-
"license": "ISC",
|
|
35
|
-
"type": "module",
|
|
36
|
-
"bugs": {
|
|
37
|
-
"url": "https://github.com/nialvape/utn-mcp/issues"
|
|
38
|
-
},
|
|
39
|
-
"homepage": "https://utnmcp.com.ar",
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"@modelcontextprotocol/node": "^2.1.1",
|
|
42
|
-
"@modelcontextprotocol/server": "^2.3.0",
|
|
43
|
-
"@napi-rs/keyring": "^2.1.0",
|
|
44
|
-
"express": "^5.2.1",
|
|
45
|
-
"tough-cookie": "^6.0.2",
|
|
46
|
-
"zod": "^4.6.5"
|
|
47
|
-
},
|
|
48
|
-
"devDependencies": {
|
|
49
|
-
"@types/express": "^5.0.6",
|
|
50
|
-
"@types/node": "^26.6.4",
|
|
51
|
-
"pdfjs-dist": "^6.4.299",
|
|
52
|
-
"tsx": "^4.23.15",
|
|
53
|
-
"typescript": "^7.0.2"
|
|
54
|
-
},
|
|
55
|
-
"engines": {
|
|
56
|
-
"node": ">=20"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "utn-mcp",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Este es el Model Context Protocol server NO OFICIAL de la UTN para buscar informacion o archivos en el aula virtual.",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"utn-mcp": "build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build",
|
|
11
|
+
"vendor"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"prebuild": "node -e \"require('fs').rmSync('build',{recursive:true,force:true})\"",
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"prepare": "node scripts/vendor-pdfjs.mjs",
|
|
18
|
+
"start": "node build/index.js",
|
|
19
|
+
"dev": "tsx src/index.ts",
|
|
20
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
21
|
+
"login": "tsx src/index.ts login"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/nialvape/utn-mcp.git"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"utn",
|
|
31
|
+
"aula-virtual"
|
|
32
|
+
],
|
|
33
|
+
"author": "Joaquín Peñalva (https://github.com/nialvape)",
|
|
34
|
+
"license": "ISC",
|
|
35
|
+
"type": "module",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/nialvape/utn-mcp/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://utnmcp.com.ar",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/node": "^2.1.1",
|
|
42
|
+
"@modelcontextprotocol/server": "^2.3.0",
|
|
43
|
+
"@napi-rs/keyring": "^2.1.0",
|
|
44
|
+
"express": "^5.2.1",
|
|
45
|
+
"tough-cookie": "^6.0.2",
|
|
46
|
+
"zod": "^4.6.5"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/express": "^5.0.6",
|
|
50
|
+
"@types/node": "^26.6.4",
|
|
51
|
+
"pdfjs-dist": "^6.4.299",
|
|
52
|
+
"tsx": "^4.23.15",
|
|
53
|
+
"typescript": "^7.0.2"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=20"
|
|
57
|
+
}
|
|
58
|
+
}
|