Skip to main content
Glama
README.md
# upv-mcp

[![CI](https://github.com/pgomsan/upv-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/pgomsan/upv-mcp/actions/workflows/ci.yml)

Servidor [MCP](https://modelcontextprotocol.io) local que expone tu calendario
academico de la **UPV** a cualquier cliente MCP: Claude Desktop, Claude Code o
Cursor. Preguntale por tu horario en lenguaje natural y responde con tus datos
reales.

> «¿cual es mi proxima clase?»
> → *Modelado y Control de Robots, lunes 7 de septiembre a las 15:00, AULA 1E 0.3
> (Edificio 1E).*

Se alimenta del export iCal de tu horario y, opcionalmente, de PoliformaT a traves
de su **API REST oficial**. **No hace scraping y no manda tus datos a ningun sitio**:
todo ocurre en tu ordenador, en solo lectura y solo sobre tu cuenta.

## Tools

| Tool | Para que sirve |
|---|---|
| `get_schedule(start_date, end_date, course)` | Clases en un rango de fechas |
| `get_next_class()` | La siguiente sesion desde ahora, con aula y hora |
| `list_upcoming_deadlines(days_ahead, days_back, pending_only, course)` | Entregas, con estado y nota ([ver limitacion](#limitaciones)) |
| `list_announcements(days_back)` | Avisos publicados por los profesores |
| `read_material(course, file)` | **Lee** un apunte o transparencia y devuelve su texto |

## Resources

| Resource | Contenido |
|---|---|
| `upv://materiales` | Indice de asignaturas con apuntes y enunciados |
| `upv://materiales/{codigo}` | Materiales de una asignatura (metadatos y enlaces) |

Los *resources* sirven para **navegar**: listan nombre, tipo, tamano y URL sin
descargar nada, y solo de la asignatura que abras. Para **leer** el contenido de un
fichero concreto esta la tool `read_material`, que lo descarga y extrae su texto:
asi puedes pedir "resumeme el tema 3" o "que dice el enunciado de la practica 2".

Lee PDF, PowerPoint (.pptx), Word (.docx) y texto plano. Los ZIP, imagenes y videos
no se pueden leer, y un PDF escaneado tampoco tiene texto que extraer: en esos casos
el servidor lo dice en vez de inventarse el contenido.

El filtro `course` de las tools acepta el codigo (`14537`), el nombre o parte de el
(`vision`) o las siglas (`VC`).

## Instalacion

Necesitas [uv](https://docs.astral.sh/uv/) y Python 3.12+.

```bash
git clone https://github.com/pgomsan/upv-mcp.git
cd upv-mcp
uv sync
```

### 1. Consigue tu URL iCal

En la [intranet de la UPV](https://intranet.upv.es): **Horarios → Compartir
horarios**, y genera el enlace iCal de tu horario.

> Ese enlace lleva un token y **es equivalente a una contrasena**: cualquiera que lo
> tenga puede ver tu horario completo con tu nombre. No lo pegues en repositorios,
> issues ni capturas de pantalla.

### 2. Guardala en el llavero

```bash
uv run upv-mcp-config set schedule    # se pide sin eco, no se escribe en ningun fichero
uv run upv-mcp-config show            # comprobar (enmascarada)
```

Se guarda en el llavero del sistema (Keychain en macOS).

Si lo lanzas donde no hay terminal interactiva (un script, CI, o el `!` de Claude
Code), la URL se lee de stdin. Pasala desde un fichero para que no quede en el
historial del shell:

```bash
cat url.txt | uv run upv-mcp-config set schedule && rm url.txt
```

### 2b. Conecta PoliformaT (opcional)

Para ver tus entregas, materiales y avisos:

```bash
uv run upv-mcp-config set poliformat    # usuario y contrasena de la UPV
```

Van al llavero, igual que la URL. El servidor entra por el SSO de la UPV
(`cas.upv.es`) y lee la API REST oficial de Sakai: **solo lectura, solo tu cuenta**,
con un limite de 0.5 peticiones por segundo.

> Si te equivocas de contrasena, el servidor **no reintenta**: te lo dice y para.
> Reintentar contra el SSO de la universidad bloquea la cuenta.

Sin esto el servidor funciona igual, pero solo con el horario.

### 3. Registra el servidor en tu cliente

**Claude Desktop** — edita `claude_desktop_config.json`:

- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "upv": {
      "command": "/Users/TU_USUARIO/.local/bin/uv",
      "args": ["--directory", "/ruta/absoluta/a/upv-mcp", "run", "upv-mcp"]
    }
  }
}
```

> **Usa la ruta absoluta de `uv`** (`which uv` te la dice). Las apps con interfaz
> grafica de macOS no heredan el `PATH` de tu terminal, asi que poner `"uv"` a secas
> hace que el servidor no arranque y sin mensaje de error visible. Es el fallo mas
> comun al instalar esto.

Reinicia Claude Desktop **por completo** (`Cmd+Q`, no solo cerrar la ventana): el
fichero de configuracion solo se lee al arrancar. Deberias ver las cinco tools.

Si no aparecen, el log esta en `~/Library/Logs/Claude/mcp-server-upv.log`.

**Claude Code**:

```bash
claude mcp add upv -- uv --directory /ruta/absoluta/a/upv-mcp run upv-mcp
```

## Limitaciones

**Los examenes necesitan su propio calendario.** El iCal de horarios contiene
clases y **ninguna** examen. Los examenes se generan aparte, desde la intranet en
**Horarios → Compartir horarios**: crea una consulta que incluya examenes, marcala
como **publica** (la suscripcion iCal solo se ofrece en las publicas) y usa
«Suscribirse por URL». Despues:

```bash
uv run upv-mcp-config set exams
```

Sin ese calendario, el servidor **avisa en cada respuesta** de que no puede ver tus
examenes, y las descripciones de las tools obligan al modelo a decir *«no puedo
verlos»* y nunca *«no tienes examenes»*. Es deliberado: una respuesta falsa y
creible es peor que una incompleta.

Las tareas de PoliformaT nunca se cuentan como examenes aunque se titulen «Examen
parcial»: ni todo examen es una tarea ni toda tarea es un examen, y clasificar por
el titulo daria falsos positivos.

**Solo el curso academico en marcha.** PoliformaT acumula todas las asignaturas que
has cursado; se sincronizan unicamente las del ultimo curso, para no llenar las
respuestas de ruido. Para consultar entregas ya pasadas, `list_upcoming_deadlines`
acepta `days_back`.

**El estado de entrega puede ser desconocido.** Cada entrega dice si esta
`submitted`, `not_submitted` o `unknown`, y si ya esta corregida trae la nota y el
comentario del profesor. Las que llegan del calendario (y no de la herramienta de
Tareas) no tienen estado. El filtro `pending_only` deja fuera lo desconocido a
proposito: decirte que te falta algo que quiza ya entregaste es peor que callarse.

## Desarrollo

```bash
uv run pytest -q         # 150 tests
uv run mypy              # strict, limpio
uv run ruff check .
```

Inspeccionar el servidor a mano:

```bash
npx @modelcontextprotocol/inspector uv run upv-mcp
```

### Arquitectura

```
sources/   red + parseo -> modelos          (no importa mcp)
cache/     SQLite, migraciones versionadas  (no importa mcp)
tools/     filtrado y forma de la respuesta (no importa mcp, httpx2 ni icalendar)
server.py  registro de tools y resources    (unico fichero que toca el SDK)
```

La separacion se puso a prueba al integrar PoliformaT: entregas, materiales y
avisos entraron **sin tocar una linea de `tools/`** para las tools que ya existian.
`tests/test_architecture.py` verifica los invariantes analizando los imports con
`ast`, asi que una violacion rompe la build.

Decisiones razonadas en [`PLAN.md`](PLAN.md); convenciones para contribuir en
[`CLAUDE.md`](CLAUDE.md).

Los cambios se validan con el guion de [`docs/manual-testing.md`](docs/manual-testing.md):
casos que cubren ambiguedad entre tools, fechas relativas, rangos vacios, truncado y
preguntas que no deben disparar ninguna tool.

## Privacidad

- Tu `.ics` y tu base de datos local nunca se commitean (`.gitignore`).
- La URL iCal vive en el llavero del sistema, no en un fichero.
- El unico trafico de red es la descarga de tu propio calendario desde `upv.es`.

## Licencia

MIT

Maintenance

ActivitySlowing
ResponsivenessNo issues