Ganjoor MCP

el servidor MCP para la API de poesía persa de Ganjoor. Permite a un agente explorar poetas y colecciones, leer poemas con sus versos, realizar análisis de prosodia y rima, rastrear poemas de respuesta entre poetas, buscar recitaciones y comentarios, y explorar los gráficos de personas relacionadas y árboles genealógicos.

Documentación

GANJOOR MCP SERVER: MCP server for the Ganjoor (گنجور) Persian classical poetry API

VERSION: 0.1.0 PLATFORM: Node >=18.17 LICENSE: MIT

English | فارسی

Ganjoor MCP Server — el servidor del Protocolo de Contexto de Modelo (MCP) para la API de poesía persa de Ganjoor.

Permite a un agente explorar poetas y colecciones, leer poemas con sus versos, ejecutar análisis de prosodia y rima, rastrear poemas de respuesta entre poetas, buscar recitaciones y comentarios, y explorar los grafos de personas relacionadas y árboles genealógicos.

44 herramientas, acceso de solo lectura. No se requiere clave API.


Contenido


Instalación

Requiere Node.js 18.17 o superior.

npm install ganjoor-mcp-server # for locally installed stdio transport. no need if using VS Code or Claude Desktop / Cursor.

VS Code

No es necesario instalar el paquete npm — el servidor MCP se ejecuta en proceso y es lanzado por VS Code cuando se necesita.

Simplemente haz clic en la insignia a continuación para instalar el servidor Ganjoor MCP en tu espacio de trabajo de VS Code. Se añadirá automáticamente el servidor a tu configuración de .vscode/mcp.json.

Install in VS Code

O añádelo a .vscode/mcp.json:

{
  "servers": {
    "io.github.ehsanghaffar/ganjoor-mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "ganjoor-mcp-server"
      ],
      "env": {}
    }
  }
}

CLI de VS Code:

code --add-mcp '{\"name\":\"io.github.ehsanghaffar/ganjoor-mcp-server\",\"command\":\"npx\",\"args\":[\"-y\",\"ganjoor-mcp-server\"],\"env\":{}}'

Claude Desktop / Cursor

Simplemente haz clic en la insignia a continuación para instalar el servidor Ganjoor MCP en tu aplicación Claude Desktop o Cursor. Se añadirá automáticamente el servidor a tu configuración de MCP.

Install in Cursor

O añádelo a tu archivo de configuración de MCP:

{
  "mcpServers": {
    "io.github.ehsanghaffar/ganjoor-mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "ganjoor-mcp-server"
      ],
      "env": {},
      "type": "local",
      "tools": [
        "*"
      ]
    }
  }
}

Otros clientes

Claude Code

Usa la CLI de Claude Code para añadir el servidor MCP io.github.ehsanghaffar/ganjoor-mcp-server:

claude mcp add io.github.ehsanghaffar/ganjoor-mcp-server npx -y ganjoor-mcp-server
Claude Desktop

Sigue la guía de instalación de MCP, usa la configuración estándar anterior.

Codex

Crea o edita el archivo de configuración ~/.codex/config.toml y añade:

[mcp_servers.ganjoor-mcp-server]
command = "npx"
args = ["-y", "ganjoor-mcp-server"]

Para más información, consulta la documentación de MCP de Codex.

Gemini CLI

Sigue la guía de instalación de MCP, usa la configuración estándar anterior.

OpenCode

Sigue la documentación de Servidores MCP. Por ejemplo, en ~/.config/opencode/opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "io.github.ehsanghaffar/ganjoor-mcp-server": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "ganjoor-mcp-server"
      ],
      "enabled": true
    }
  }
}
GitHub Copilot CLI

GitHub Copilot CLI admite añadir servidores MCP de forma interactiva y mediante un archivo de configuración.

Opción 1: Configuración interactiva con /mcp add

  1. Abre GitHub Copilot CLI en modo interactivo.
  2. Ejecuta /mcp add.
  3. Introduce Nombre del servidor: io.github.ehsanghaffar/ganjoor-mcp-server.
  4. Elige Tipo de servidor:
    • STDIO para servidores basados en comandos
    • HTTP para servidores remotos
  5. Completa la configuración de comando/URL desde la configuración estándar anterior y establece Herramientas en *.
  6. Pulsa Ctrl+S para guardar.

Opción 2: Editar ~/.copilot/mcp-config.json

{
  "mcpServers": {
    "io.github.ehsanghaffar/ganjoor-mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "ganjoor-mcp-server"
      ],
      "env": {},
      "type": "local",
      "tools": [
        "*"
      ]
    }
  }
}

Para más información, consulta la documentación de MCP de GitHub Copilot CLI.

GitHub Copilot Coding Agent

GitHub Copilot Coding Agent puede usar servidores MCP para ampliar sus capacidades. Usa la configuración a continuación, formateada específicamente para el Coding Agent:

{
  "mcpServers": {
    "ganjoor-mcp-server": {
      "command": "npx",
      "args": [
        "-y",
        "ganjoor-mcp-server"
      ],
      "env": {},
      "type": "local",
      "tools": [
        "*"
      ]
    }
  }
}

Añade esta configuración a la configuración de tu repositorio en Copilot > Coding agent. El ajuste "tools": ["*"] habilita todas las herramientas disponibles del servidor MCP.

Para más información, consulta la documentación de MCP de GitHub Copilot Coding Agent.

Instalar desde esta fuente

npm install
npm run build
npm start            # stdio transport (default)

VS Code

Añade a .vscode/mcp.json:

{
  "servers": {
    "ganjoor": {
      "type": "stdio",
      "command": "node",
      "args": ["${workspaceFolder}/dist/index.js"]
    }
  }
}

Claude Desktop / Cursor

Añade a tu archivo de configuración de MCP:

{
  "mcpServers": {
    "ganjoor": {
      "command": "node",
      "args": ["/absolute/path/to/ganjoor-mcp/dist/index.js"]
    }
  }
}

HTTP Streamable (implementación remota / compartida)

HTTP=true PORT=3000 npm start

El endpoint de MCP es POST /mcp, y GET /health informa el estado del servidor y del upstream. El transporte HTTP se ejecuta sin estado (una instancia de servidor nueva por solicitud), por lo que escala horizontalmente. Se vincula a 127.0.0.1 de forma predeterminada y valida el encabezado Origin para bloquear ataques de re-vinculación de DNS; establece ALLOWED_ORIGINS (separado por comas) si necesitas orígenes que no sean de bucle local.

MCP Inspector

npm run inspector

Docker

Se incluyen un Dockerfile de múltiples etapas y un docker-compose.yml. La imagen se basa en node:22-alpine y contiene solo la salida compilada y las dependencias de producción.

docker compose up --build

El archivo de compose inicia el servidor en modo HTTP streamable en http://localhost:3000/mcp, con GET /health informando el estado del servidor y del upstream. HOST está establecido en 0.0.0.0 para que el puerto publicado sea accesible desde el host; cada variable de configuración de la tabla a continuación funciona como una variable de entorno ordinaria. Para ejecutar un solo contenedor sin compose:

docker build -t ganjoor-mcp .
docker run --rm -p 3000:3000 -e HTTP=true -e HOST=0.0.0.0 ganjoor-mcp

Transporte stdio sobre Docker

Los clientes que inician el servidor ellos mismos (en lugar de conectarse por HTTP) pueden usar el perfil stdio, que abre stdin para el flujo del protocolo y no inicia ningún listener HTTP:

docker compose --profile stdio run --rm ganjoor-mcp-stdio

Con Claude Desktop

Para ejecutar el servidor a través de Docker en lugar de una instalación local de Node:

{
  "mcpServers": {
    "ganjoor": {
      "command": "docker",
      "args": ["run", "--rm", "-i", "ganjoor-mcp"]
    }
  }
}

Configuración

Todos los ajustes son variables de entorno; ninguna es obligatoria.

VariablePredeterminadoPropósito
HTTP(sin establecer)Establecer en true para servir HTTP streamable en lugar de stdio.
PORT3000Puerto HTTP.
HOST127.0.0.1Dirección de vinculación HTTP.
ALLOWED_ORIGINS(vacío)Valores adicionales permitidos de Origin, separados por comas.
GANJOOR_API_BASE_URLhttps://api.ganjoor.netURL base de la API upstream — apunta a una instancia local de RMuseum para desarrollo.
GANJOOR_SITE_BASE_URLhttps://ganjoor.netBase del front-end utilizada para construir enlaces clicables.

Conceptos

El modelo de datos de Ganjoor tiene tres niveles anidados. Entender esto es la diferencia entre una sesión de agente productiva y una infructuosa.

graph TD
    P["Poet — e.g. Hafez, id 2, slug /hafez"] --> C1["Category — e.g. Ghazals, id 24"]
    P --> C2["Category — e.g. Qeta, id 25"]
    C1 --> M1["Poem — e.g. Ghazal 494, id 2623, /hafez/ghazal/sh494"]
    C2 --> M2["Poem"]
    M1 --> V["Verses (hemistichs, paired into couplets)"]
    M1 --> S["Sections (nested stanzas with metre + rhyme)"]
    M1 --> R["Recitations (audio)"]
    M1 --> CM["Comments (reader commentary)"]
    M1 --> Q["Quoted / related poems"]
  • Poeta — un autor. Hafez es el id de poeta 2, Saadi 7, Rumi 5, Khayyam 3, Ferdowsi 4. ganjoor_list_poets resuelve un nombre a un id.
  • Categoría — un libro o sección dentro de la obra de un poeta (CatType 0 general, 1 poemas, 2 libro). La categoría raíz de Hafez es 9; sus ghazales son 24.
  • Poema — un texto, identificado por un id numérico y un slug de URL como hafez/ghazal/sh494.
  • Verso — un hemistiquio. Dos versos consecutivos forman un couplet (versePosition 0 y 1).

Los slugs de URL necesitan una barra inicial. La API upstream devuelve 404 para ?url=hafez/ghazal/sh494 pero resuelve ?url=/hafez/ghazal/sh494. Este servidor lo normaliza por ti, por lo que cualquiera de las dos formas funciona.

El texto persa se devuelve tal cual. Es de derecha a izquierda con diacríticos de escritura árabe utilizados para la prosodia. No lo transcribas ni lo traduzcas a menos que el usuario lo pida explícitamente.


Referencia de herramientas

Poetas y colecciones

HerramientaDescripción
ganjoor_list_poetsTodos los poetas publicados con biografía, fechas, lugares. Paginado.
ganjoor_get_poetUn poeta por id o slug, con biografía y libros/secciones de nivel superior.
ganjoor_get_poets_by_centuryCada poeta agrupado en medios siglos históricos.
ganjoor_list_booksColecciones con nombre, filtrables por poeta o subcadena de nombre.

Categorías

HerramientaDescripción
ganjoor_get_categoryUna categoría con ruta de navegación, subsecciones y poemas contenidos.
ganjoor_list_category_poemsPoemas paginados en una categoría, con filtro opcional de título.
ganjoor_get_category_geotagsLugares geográficos/históricos etiquetados bajo el subárbol de una categoría.
ganjoor_get_category_people_graphRed de personas detrás de la pestaña "personajes" de una categoría.

Poemas

HerramientaDescripción
ganjoor_search_poemsBúsqueda de texto completo en poemas, limitada por poeta o categoría.
ganjoor_semantic_search_poemsBúsqueda basada en significado ("encuentra un poema sobre…").
ganjoor_get_poemUn poema completo por URL, con versos agrupados en pareados.
ganjoor_get_poem_by_idLo mismo, por id numérico.
ganjoor_get_poem_versesSolo los versos — más ligero que obtener el poema completo.
ganjoor_get_random_poemUn poema aleatorio, opcionalmente de un poeta.
ganjoor_get_hafez_faalUn ghazal aleatorio de Hafez para adivinación persa.
ganjoor_get_page_urlResuelve cualquier id de página (poema, categoría, poeta) a su URL web.
ganjoor_get_redirect_urlResuelve una URL heredada de Ganjoor a su dirección actual.

Prosodia, rima y estructura

HerramientaDescripción
ganjoor_list_rhythmsTodos los metros prosódicos persas con recuentos de uso.
ganjoor_analyze_poem_rhythmDetecta el metro de un poema.
ganjoor_analyze_poem_rhymeDetecta la letra de rima y el patrón de un poema.
ganjoor_find_similar_poemsPoemas que comparten metro y rima — el grafo de poemas de respuesta.
ganjoor_get_poem_sectionsLas secciones estructurales de un poema (enlaces, bandas de masnavi, tercetos).
ganjoor_get_sectionUna sección con versos y secciones relacionadas.
ganjoor_get_related_sectionsSecciones de otros poemas que coinciden con el metro y la rima de este.
ganjoor_get_couplet_sectionsA qué secciones pertenece un pareado dado.
ganjoor_get_language_tagged_sectionsEstrofas etiquetadas con un código de idioma no persa.

Recitaciones de audio

HerramientaDescripción
ganjoor_search_recitationsBusca recitaciones por recitador, poeta o categoría.
ganjoor_get_recitationUna recitación con su URL de audio.
ganjoor_get_recitation_sync_infoSincronización por verso para seguir el audio.
ganjoor_get_poem_recitationsCada recitación publicada para un poema.
ganjoor_get_category_top_recitationsRecitaciones más votadas dentro de una categoría.

Personas

HerramientaDescripción
ganjoor_list_peopleFiguras históricas y literarias en el grafo de conocimiento de Ganjoor.
ganjoor_get_personBiografía, fechas y lugares de una persona.
ganjoor_get_person_poemsPoemas etiquetados con una persona.
ganjoor_get_person_relationsLos bordes de parentesco y afiliación de una persona.
ganjoor_get_person_family_treeTodo el componente de parentesco conectado desde una persona.

Comentarios, preguntas frecuentes y material complementario

HerramientaDescripción
ganjoor_get_recent_commentsComentarios recientes publicados de lectores.
ganjoor_list_faq_categoriesCategorías de preguntas frecuentes editoriales de Ganjoor.
ganjoor_get_faq_itemsTodas las entradas de preguntas y respuestas en una categoría de preguntas frecuentes.
ganjoor_get_faq_itemUna entrada de preguntas frecuentes por id.
ganjoor_get_poem_imagesImágenes de manuscritos y facsímiles.
ganjoor_get_poem_quoted_poemsPoemas que este cita, y poemas que lo citan.
ganjoor_get_poem_effective_correctionsEl registro editorial antes/después de las correcciones de texto.
ganjoor_get_poem_geotagsLugares y fechas conmemorativas etiquetadas en un poema.

Cada herramienta está anotada con readOnlyHint, idempotentHint y openWorldHint, y ninguna de ellas modifica el estado en Ganjoor.


Formatos de respuesta

Cada herramienta acepta un argumento response_format:

  • markdown (predeterminado) — salida legible para humanos con encabezados, listas y enlaces clicables.
  • json — los mismos datos como un objeto estructurado, también devueltos como structuredContent de MCP para que los clientes que lo admitan puedan consumir los campos directamente sin analizar texto.

Las herramientas de lista comparten un sobre de paginación consistente:

{
  "total": 842,
  "count": 20,
  "page": 1,
  "page_size": 20,
  "has_more": true,
  "next_page": 2,
  "poems": [{ "id": 2623, "title": "غزل شمارهٔ ۴۹۴", "full_url": "/hafez/ghazal/sh494" }]
}

page y page_size tienen base 1; page_size tiene un límite de 100. Las respuestas de más de 25,000 caracteres se truncan con un indicador explícito truncated en lugar de cortarse silenciosamente. Dos endpoints ascendentes (/api/ganjoor/poets y /api/ganjoor/rhythms) ignoran sus parámetros de paginación y siempre devuelven la colección completa. El servidor detecta eso y recorta la ventana solicitada localmente, por lo que page y has_more se mantienen correctos y consistentes en todas las herramientas de listado.

Los errores se devuelven como resultados de herramientas (no errores de protocolo) con un mensaje que nombra el endpoint y sugiere un siguiente paso — por ejemplo, un 404 te señala a ganjoor_list_poets para encontrar un id válido.


Uso

"¿Quién es Hafez?"

ganjoor_get_poet { "url": "hafez" }

"Muéstrame el ghazal 494 de Hafez"

ganjoor_get_poem { "url": "hafez/ghazal/sh494", "include_recitations": true }

"¿Qué poemas de Hafez contienen la palabra صبر?"

ganjoor_search_poems { "term": "صبر", "poet_id": 2, "page_size": 10 }

"¿Qué métrica tiene este poema, y quién más escribió en esa métrica?"

ganjoor_analyze_poem_rhythm { "poem_id": 2623 }
ganjoor_list_rhythms { "page_size": 100 }
ganjoor_find_similar_poems { "metre": "<rhythm from the previous step>", "page_size": 10 }

"¿Quiénes son los antepasados de esta figura?"

ganjoor_get_person { "person_id": 22 }
ganjoor_get_person_family_tree { "person_id": 22 }

"¿Qué dice Hafez sobre mi pregunta?"

ganjoor_get_hafez_faal { }

Arquitectura

src/
├── index.ts            # Process entry: stdio or streamable HTTP transport
├── server.ts           # createServer() — registers every tool
├── client.ts           # fetch wrapper, error mapping, pagination helpers
├── types.ts            # TypeScript interfaces for Ganjoor's payloads
├── constants.ts        # Base URLs, limits, server identity
├── schemas.ts          # Shared Zod schemas (pagination, URL slugs, ids)
├── format.ts           # Markdown rendering and formatting helpers
└── tools/
    ├── registry.ts     # defineTool() wrapper: consistent errors + both formats
    ├── poets.ts        # Poet discovery
    ├── categories.ts   # Category browsing
    ├── poems.ts        # Poem retrieval, search, URL resolution
    ├── analysis.ts     # Prosody, rhyme, sections
    ├── audio.ts        # Recitations
    ├── content.ts      # Images, quotations, corrections, geo tags
    └── people.ts       # People graph, comments, FAQ

Dos convenciones recorren el código base:

  • defineTool() envuelve cada registro. Garantiza que cada herramienta reporte fallos como resultados isError con mensajes accionables, renderice markdown o JSON de forma consistente, y devuelva structuredContent junto con el texto.
  • md() construye documentos. Preserva líneas en blanco explícitas para que los encabezados y listas de markdown estén separados correctamente, mientras elimina null/false para que los fragmentos condicionales desaparezcan limpiamente.

Las peculiaridades del upstream se normalizan en client.ts y en los módulos de herramientas en lugar de filtrarse a los llamadores — por ejemplo, el envoltorio { poet, cat } en los endpoints de categorías, el category.cat anidado en los payloads de poemas, y el requisito de barra inicial en los parámetros de URL.


Desarrollo

npm run build       # compile to dist/
npm run typecheck   # type-check without emitting
npm test            # live smoke test against api.ganjoor.net
npm run inspect     # print real tool output for review
npm run clean

npm test arranca el servidor sobre un transporte en memoria, verifica que cada herramienta tenga una descripción sustancial, e invoca las 44 herramientas contra la API en vivo, fallando ante cualquier respuesta de error.


Limitaciones conocidas

  • La búsqueda semántica puede estar deshabilitada. El backend de embeddings de Ganjoor es opcional; cuando una implementación lo tiene desactivado, ganjoor_semantic_search_poems devuelve un 503. La herramienta muestra un mensaje claro y te señala a ganjoor_search_poems con una palabra clave en persa en su lugar.
  • La resolución de redirecciones es escasa. Ganjoor solo almacena redirecciones para rutas que realmente renombró, por lo que la mayoría de las URLs válidas devuelven has_redirect: false en lugar de un destino.
  • Algunas herramientas siempre requieren autenticación. Los endpoints detrás de una cuenta de Ganjoor (marcadores, publicación de comentarios, subida de correcciones) están fuera de alcance por diseño — este servidor es de solo lectura y anónimo.
  • El catálogo de libros está curado. ganjoor_list_books cubre ~168 colecciones nombradas y omite algunos poetas importantes — Hafez no tiene entradas. Usa ganjoor_get_poet con include_cats: true en su lugar, que lee las secciones de cada poeta directamente desde su categoría raíz.
  • Sin totales en endpoints paginados. La API upstream pagina sin reportar totales, por lo que total se omite cuando es desconocido; has_more se infiere obteniendo una fila más allá de la página.
  • La cobertura del texto poético está curada. El etiquetado de personas, etiquetas geográficas y etiquetas de idioma son incompletos por naturaleza — un resultado vacío suele ser una brecha de cobertura, no una prueba de ausencia.
  • Los campos de fecha a veces son aproximados. Ganjoor marca las fechas de las que no está seguro (validBirthDate / validDeathDate); las herramientas exponen esas marcas para que los llamadores puedan filtrar.

Evaluación

evaluation/evaluation.xml contiene 10 preguntas de solo lectura con respuestas verificadas. Cada una requiere varias llamadas a herramientas para responder y fue comprobada contra la API en vivo antes de escribirse:

npx tsx tests/verify-evaluation.ts   # replays every question through the MCP tools

Las preguntas evitan deliberadamente trivialidades de una sola consulta. Cruzan poetas contra siglos, recorren el grafo de parentesco, comparan la prosodia de un poema contra la lista de métricas de todo el corpus, y resuelven una cadena de poemas de respuesta a través de registros de citas.


Contribuciones

Ver CONTRIBUTING.md.