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
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
- Docker
- Configuración
- Conceptos
- Referencia de herramientas
- Formatos de respuesta
- Uso
- Arquitectura
- Desarrollo
- Limitaciones conocidas
- Evaluación
- Contribuciones
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.
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.
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
- Abre GitHub Copilot CLI en modo interactivo.
- Ejecuta
/mcp add. - Introduce Nombre del servidor:
io.github.ehsanghaffar/ganjoor-mcp-server. - Elige Tipo de servidor:
STDIOpara servidores basados en comandosHTTPpara servidores remotos
- Completa la configuración de comando/URL desde la configuración estándar anterior y establece Herramientas en
*. - Pulsa
Ctrl+Spara 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.
| Variable | Predeterminado | Propósito |
|---|---|---|
HTTP | (sin establecer) | Establecer en true para servir HTTP streamable en lugar de stdio. |
PORT | 3000 | Puerto HTTP. |
HOST | 127.0.0.1 | Dirección de vinculación HTTP. |
ALLOWED_ORIGINS | (vacío) | Valores adicionales permitidos de Origin, separados por comas. |
GANJOOR_API_BASE_URL | https://api.ganjoor.net | URL base de la API upstream — apunta a una instancia local de RMuseum para desarrollo. |
GANJOOR_SITE_BASE_URL | https://ganjoor.net | Base 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, Saadi7, Rumi5, Khayyam3, Ferdowsi4.ganjoor_list_poetsresuelve un nombre a un id. - Categoría — un libro o sección dentro de la obra de un poeta (CatType
0general,1poemas,2libro). La categoría raíz de Hafez es9; sus ghazales son24. - 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(versePosition0 y 1).
Los slugs de URL necesitan una barra inicial. La API upstream devuelve 404 para
?url=hafez/ghazal/sh494pero 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
| Herramienta | Descripción |
|---|---|
ganjoor_list_poets | Todos los poetas publicados con biografía, fechas, lugares. Paginado. |
ganjoor_get_poet | Un poeta por id o slug, con biografía y libros/secciones de nivel superior. |
ganjoor_get_poets_by_century | Cada poeta agrupado en medios siglos históricos. |
ganjoor_list_books | Colecciones con nombre, filtrables por poeta o subcadena de nombre. |
Categorías
| Herramienta | Descripción |
|---|---|
ganjoor_get_category | Una categoría con ruta de navegación, subsecciones y poemas contenidos. |
ganjoor_list_category_poems | Poemas paginados en una categoría, con filtro opcional de título. |
ganjoor_get_category_geotags | Lugares geográficos/históricos etiquetados bajo el subárbol de una categoría. |
ganjoor_get_category_people_graph | Red de personas detrás de la pestaña "personajes" de una categoría. |
Poemas
| Herramienta | Descripción |
|---|---|
ganjoor_search_poems | Búsqueda de texto completo en poemas, limitada por poeta o categoría. |
ganjoor_semantic_search_poems | Búsqueda basada en significado ("encuentra un poema sobre…"). |
ganjoor_get_poem | Un poema completo por URL, con versos agrupados en pareados. |
ganjoor_get_poem_by_id | Lo mismo, por id numérico. |
ganjoor_get_poem_verses | Solo los versos — más ligero que obtener el poema completo. |
ganjoor_get_random_poem | Un poema aleatorio, opcionalmente de un poeta. |
ganjoor_get_hafez_faal | Un ghazal aleatorio de Hafez para adivinación persa. |
ganjoor_get_page_url | Resuelve cualquier id de página (poema, categoría, poeta) a su URL web. |
ganjoor_get_redirect_url | Resuelve una URL heredada de Ganjoor a su dirección actual. |
Prosodia, rima y estructura
| Herramienta | Descripción |
|---|---|
ganjoor_list_rhythms | Todos los metros prosódicos persas con recuentos de uso. |
ganjoor_analyze_poem_rhythm | Detecta el metro de un poema. |
ganjoor_analyze_poem_rhyme | Detecta la letra de rima y el patrón de un poema. |
ganjoor_find_similar_poems | Poemas que comparten metro y rima — el grafo de poemas de respuesta. |
ganjoor_get_poem_sections | Las secciones estructurales de un poema (enlaces, bandas de masnavi, tercetos). |
ganjoor_get_section | Una sección con versos y secciones relacionadas. |
ganjoor_get_related_sections | Secciones de otros poemas que coinciden con el metro y la rima de este. |
ganjoor_get_couplet_sections | A qué secciones pertenece un pareado dado. |
ganjoor_get_language_tagged_sections | Estrofas etiquetadas con un código de idioma no persa. |
Recitaciones de audio
| Herramienta | Descripción |
|---|---|
ganjoor_search_recitations | Busca recitaciones por recitador, poeta o categoría. |
ganjoor_get_recitation | Una recitación con su URL de audio. |
ganjoor_get_recitation_sync_info | Sincronización por verso para seguir el audio. |
ganjoor_get_poem_recitations | Cada recitación publicada para un poema. |
ganjoor_get_category_top_recitations | Recitaciones más votadas dentro de una categoría. |
Personas
| Herramienta | Descripción |
|---|---|
ganjoor_list_people | Figuras históricas y literarias en el grafo de conocimiento de Ganjoor. |
ganjoor_get_person | Biografía, fechas y lugares de una persona. |
ganjoor_get_person_poems | Poemas etiquetados con una persona. |
ganjoor_get_person_relations | Los bordes de parentesco y afiliación de una persona. |
ganjoor_get_person_family_tree | Todo el componente de parentesco conectado desde una persona. |
Comentarios, preguntas frecuentes y material complementario
| Herramienta | Descripción |
|---|---|
ganjoor_get_recent_comments | Comentarios recientes publicados de lectores. |
ganjoor_list_faq_categories | Categorías de preguntas frecuentes editoriales de Ganjoor. |
ganjoor_get_faq_items | Todas las entradas de preguntas y respuestas en una categoría de preguntas frecuentes. |
ganjoor_get_faq_item | Una entrada de preguntas frecuentes por id. |
ganjoor_get_poem_images | Imágenes de manuscritos y facsímiles. |
ganjoor_get_poem_quoted_poems | Poemas que este cita, y poemas que lo citan. |
ganjoor_get_poem_effective_corrections | El registro editorial antes/después de las correcciones de texto. |
ganjoor_get_poem_geotags | Lugares 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 comostructuredContentde 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 resultadosisErrorcon mensajes accionables, renderice markdown o JSON de forma consistente, y devuelvastructuredContentjunto 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 eliminanull/falsepara 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_poemsdevuelve un 503. La herramienta muestra un mensaje claro y te señala aganjoor_search_poemscon 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: falseen 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_bookscubre ~168 colecciones nombradas y omite algunos poetas importantes — Hafez no tiene entradas. Usaganjoor_get_poetconinclude_cats: trueen 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
totalse omite cuando es desconocido;has_morese 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.