Content
# mcp-grf-eventhorizon
Servidor [MCP](https://modelcontextprotocol.io/) que permite a Claude (u otro cliente compatible)
leer archivos **GRF de Ragnarok Online en formato v0x300, conocido como "Event Horizon"**.
Este formato es muy reciente (añadido a finales de 2024) y permite GRFs de **más de 4 GB**, algo
que el formato clásico `Master of Magic` (v0x1xx / v0x200) no soporta. Las herramientas y
librerías existentes para GRF (por ejemplo [`grf-loader`](https://www.npmjs.com/package/grf-loader),
usada por servidores MCP clásicos) **no reconocen esta nueva firma** y fallan con un error de tipo
`Not a GRF file (invalid signature)`. Este proyecto implementa el parser desde cero para cubrir ese hueco.
## ¿Por qué existe este proyecto?
Al intentar leer un `data.grf` de un cliente moderno de Ragnarok Online con las herramientas GRF
habituales, estas fallan porque la cabecera no empieza por `Master of Magic\0`, sino por
`Event Horizon\0RL`. Investigando el código fuente de [GRFEditor](https://github.com/Tokeiburu/GRFEditor)
(la herramienta de referencia de la comunidad, que sí soporta esta versión) se pudo extraer y
verificar byte a byte la estructura del nuevo formato, y reimplementarla en JavaScript puro.
## Formato GRF v0x300 ("Event Horizon")
### Cabecera (46 bytes)
| Offset | Tamaño | Campo |
|---|---|---|
| `0x00` | 16 bytes | Firma: `"Event Horizon\0RL"` (en lugar de `"Master of Magic\0"`) |
| `0x10` | 14 bytes | Clave de cifrado (normalmente a cero) |
| `0x1E` (30) | 8 bytes (int64) o 4+4 | `FileTableOffset` (+ `Seed` en el modo "legacy") |
| `0x26` (38) | 4 bytes (int32) | Número de entradas (`RealFilesCount`) |
| `0x2A` (42) | 4 bytes (int32) | Versión: mayor = `version >> 8`, menor = `version & 0xFF` (`0x0300` → v3.0) |
Cuando el GRF es "grande" (modo soportado a partir de esta versión), los bytes en `0x23-0x25`
son cero y entonces:
- `FileTableOffset` se lee como **int64** en `0x1E`
- `Seed` es `0`
- El número de entradas se lee directamente como int32 en `0x26`
En modo "legacy" (compatibilidad con GRFs pequeños):
- `FileTableOffset` es **uint32**
- `Seed` es int32 en `0x22`
- El número real de entradas es `filesCount - Seed - 7`
### Tabla de archivos
Ubicada en la posición absoluta `FileTableOffset + 46` (46 = tamaño de la cabecera):
1. 4 bytes reservados (siempre `0`)
2. `TableSizeCompressed` (int32)
3. `TableSize`, tamaño descomprimido (int32)
4. `TableSizeCompressed` bytes comprimidos con **zlib**, que al descomprimir dan `TableSize` bytes
### Entradas de la tabla
Cada entrada, de forma secuencial dentro de los datos descomprimidos:
| Campo | Tamaño | Notas |
|---|---|---|
| Nombre | variable, terminado en `\0` | Codificado en **CP949** (coreano); `/` se sustituye por `\` |
| `SizeCompressed` | int32 | |
| `SizeCompressedAlignment` | int32 | |
| `SizeDecompressed` | int32 | |
| `Flags` | 1 byte | bit `0x01` = archivo, bit `0x02` = directorio |
| `FileOffset` | int64 | Relativo; el offset absoluto es `valor + 46` |
(En las versiones anteriores, `0x1xx`/`0x200`, esta última parte ocupa 17 bytes con un offset
de 32 bits; en v0x300 ocupa 21 bytes con un offset de 64 bits, lo cual es justo lo que permite
superar el límite de 4 GB).
### Datos de cada archivo
Ubicados en la posición absoluta `FileOffset`, comprimidos con **zlib** (formato estándar
`deflate` con cabecera, `0x78 0x9c` es habitual). Al descomprimir dan `SizeDecompressed` bytes.
> Toda esta estructura se dedujo y verificó comparándola con
> [`GRF/Core/GrfHeader.cs`](https://github.com/Tokeiburu/GRFEditor/blob/main/GRF/Core/GrfHeader.cs)
> y [`GRF/Core/FileTable.cs`](https://github.com/Tokeiburu/GRFEditor/blob/main/GRF/Core/FileTable.cs)
> de GRFEditor, y extrayendo y comprobando archivos reales (incluida la validación de las cabeceras
> resultantes, p. ej. un `.bmp` extraído con cabecera `BM` correcta).
## Instalación
```bash
git clone <url-de-este-repo>
cd mcp-grf-eventhorizon
npm install
```
## Configuración en Claude Code
Añade el servidor a tu configuración de MCP (`~/.claude.json` o `.mcp.json` del proyecto):
```json
{
"mcpServers": {
"grf-eventhorizon": {
"type": "stdio",
"command": "node",
"args": ["RUTA/ABSOLUTA/A/mcp-grf-eventhorizon/index.js"],
"env": {}
}
}
}
```
## Herramientas (tools) expuestas
### `grf_info`
Muestra los metadatos de la cabecera de un GRF: versión, tamaño del archivo, número de
entradas y offset de la tabla de archivos.
**Parámetros**
| Nombre | Tipo | Requerido | Descripción |
|---|---|---|---|
| `path` | string | sí | Ruta del archivo `.grf` |
**Ejemplo de salida**
```
Formato: GRF v3.0 ("Event Horizon")
Tamaño del archivo: 4.483.275.319 bytes
Número de entradas (archivos + directorios): 229.960
Offset de la tabla de archivos (relativo): 4.479.890.688
```
### `list_grf`
Lista los archivos contenidos en el GRF. Permite filtrar por subcadena (case-insensitive)
y limitar el número de resultados — muy útil dado que estos GRFs pueden contener cientos
de miles de entradas.
**Parámetros**
| Nombre | Tipo | Requerido | Descripción |
|---|---|---|---|
| `path` | string | sí | Ruta del archivo `.grf` |
| `filter` | string | no | Subcadena para filtrar rutas internas, p. ej. `.spr` o `sprite\` |
| `limit` | number | no | Máximo de resultados (por defecto 200) |
**Ejemplo**
```
5 resultado(s) que contienen ".spr" (límite 5, total en el GRF: 229.375):
data\sprite\로브\wingofheart\남\화이트스미스_남.spr
data\sprite\로브\wingofheart\남\화이트스미스멧돼지_남.spr
data\sprite\로브\wingofheart\여\cart_summoner_여.spr
data\sprite\로브\wingofheart\여\frog_oboro_여.spr
data\sprite\로브\wingofheart\여\oboro_여.spr
```
### `extract_file`
Extrae y descomprime un archivo del GRF.
- Si se indica `outputPath`, guarda el contenido descomprimido en esa ruta de disco.
- Si no se indica, intenta devolver el contenido como texto UTF-8 (útil para `.lua`, `.txt`,
`.xml`, etc.); si detecta que es binario, devuelve un resumen (tamaño + primeros bytes en hex)
y sugiere usar `outputPath`.
**Parámetros**
| Nombre | Tipo | Requerido | Descripción |
|---|---|---|---|
| `grfPath` | string | sí | Ruta del archivo `.grf` |
| `filePath` | string | sí | Ruta interna del archivo, p. ej. `data\sprite\...spr` |
| `outputPath` | string | no | Ruta absoluta donde guardar el archivo extraído |
**Ejemplo (binario, con `outputPath`)**
```
Extraído "data\texture\유저인터페이스\item\c_medjed_text.bmp" (1.782 bytes) -> C:\ruta\salida.bmp
```
**Ejemplo (binario, sin `outputPath`)**
```
"data\texture\유저인터페이스\item\c_medjed_text.bmp" contiene datos binarios (1.782 bytes).
Indica "outputPath" para guardarlo en disco.
Primeros bytes (hex): 424df60600000000000036000000280000001800000018000000010018000000
```
## Notas
- Los nombres de archivo dentro del GRF están codificados en **CP949** (coreano), típico de
los clientes de Ragnarok Online; este servidor los decodifica con
[`iconv-lite`](https://www.npmjs.com/package/iconv-lite).
- Solo se soportan GRFs con la firma `Event Horizon\0RL` (v0x300). Para GRFs clásicos
(`Master of Magic\0`, v0x1xx/v0x200) usa una herramienta basada en `grf-loader` u otra
compatible con esas versiones.
- Este servidor es de solo lectura (no permite modificar ni reempaquetar GRFs).
## Dependencias
- [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk) — SDK oficial de MCP
- [`iconv-lite`](https://www.npmjs.com/package/iconv-lite) — decodificación de nombres en CP949
## Créditos
La estructura del formato v0x300 se dedujo inspeccionando el código fuente abierto de
[Tokeiburu/GRFEditor](https://github.com/Tokeiburu/GRFEditor), la principal herramienta de la
comunidad con soporte para esta versión.