io.github.ehsanghaffar/ganjoor-mcp-server·Other

ganjoor-mcp-server

MCP server for the Ganjoor (گنجور) Persian classical poetry API at https://ganjoor.net

Stars
7
Version
0.1.0
License
MIT
Review
Passed
Language
TypeScript
Last commit
2h ago
Forks
1
Open issues
0
Registry update
Oct 14, 2026
ganjoor-mcpmcp-serverpersianpersian-dataset

Connect

npm
Terminal
$claude mcp add ganjoor-mcp-server \  -- npx -y [email protected]

Environment variables

Nothing to configure — it works as is.

Security review

Review passed

Reviewed 1h ago.

metadata
scanned
packages
1 checked

No findings.

Tools

This server runs on your machine, so its tool list is not fetched before install — the README below describes what it offers.

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 — the Model Context Protocol server for the Ganjoor Persian poetry API.

It lets an agent browse poets and collections, read poems with their verses, run prosody and rhyme analysis, trace reply-poems across poets, look up recitations and commentary, and explore the related-people and family-tree graphs.

44 tools, read-only access. No API key required.


Contents


Install

Requires Node.js 18.17 or newer.

npm install ganjoor-mcp-server

VS Code

Just click the badge below to install the Ganjoor MCP server in your VS Code workspace. It will automatically add the server to your .vscode/mcp.json config.

Install in VS Code

Or add to .vscode/mcp.json:

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

VS Code CLI:

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

Claude Desktop / Cursor

Just click the badge below to install the Ganjoor MCP server in your Claude Desktop or Cursor app. It will automatically add the server to your MCP config.

Install in Cursor

Or add to your MCP config file:

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

Other clients

Claude Code

Use the Claude Code CLI to add the io.github.ehsanghaffar/ganjoor-mcp-server MCP server:

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

Follow the MCP install guide, use the standard config above.

Codex

Create or edit the configuration file ~/.codex/config.toml and add:

[mcp_servers.io.github.ehsanghaffar/ganjoor-mcp-server]
command = "npx"
args = ["-y", "ganjoor-mcp-server"]

For more information, see the Codex MCP documentation.

Gemini CLI

Follow the MCP install guide, use the standard config above.

OpenCode

Follow the MCP Servers documentation. For example in ~/.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 supports adding MCP servers interactively and through a config file.

Option 1: Interactive setup with /mcp add

  1. Open GitHub Copilot CLI in interactive mode.
  2. Run /mcp add.
  3. Enter Server Name: io.github.ehsanghaffar/ganjoor-mcp-server.
  4. Choose Server Type:
    • STDIO for command-based servers
    • HTTP for remote servers
  5. Fill in command/url settings from the standard config above and set Tools to *.
  6. Press Ctrl+S to save.

Option 2: Edit ~/.copilot/mcp-config.json

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

For more information, see the GitHub Copilot CLI MCP documentation.

GitHub Copilot Coding Agent

GitHub Copilot Coding Agent can use MCP servers to extend its capabilities. Use the configuration below specifically formatted for the Coding Agent:

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

Add this configuration to your repository settings under Copilot > Coding agent. The "tools": ["*"] setting enables all available tools from the MCP server.

For more information, see the GitHub Copilot Coding Agent MCP documentation.

Install from this source

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

VS Code

Add to .vscode/mcp.json:

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

Claude Desktop / Cursor

Add to your MCP config file:

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

Streamable HTTP (remote / shared deployment)

HTTP=true PORT=3000 npm start

The MCP endpoint is POST /mcp, and GET /health reports server and upstream status. The HTTP transport runs statelessly (a fresh server instance per request), so it scales horizontally. It binds 127.0.0.1 by default and validates the Origin header to block DNS-rebinding attacks; set ALLOWED_ORIGINS (comma-separated) if you need non-loopback origins.

MCP Inspector

npm run inspector

Docker

A multi-stage Dockerfile and a docker-compose.yml are included. The image is built on node:22-alpine and contains only the compiled output and production dependencies.

docker compose up --build

The compose file starts the server in streamable HTTP mode on http://localhost:3000/mcp, with GET /health reporting server and upstream status. HOST is set to 0.0.0.0 so the published port is reachable from the host; every configuration variable from the table below works as an ordinary environment variable. To run a single container without compose:

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

stdio transport over Docker

Clients that launch the server themselves (rather than connecting over HTTP) can use the stdio profile, which opens stdin for the protocol stream and starts no HTTP listener:

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

With Claude Desktop

To run the server through Docker instead of a local Node install:

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

Configuration

All settings are environment variables; none are required.

VariableDefaultPurpose
HTTP(unset)Set to true to serve streamable HTTP instead of stdio.
PORT3000HTTP port.
HOST127.0.0.1HTTP bind address.
ALLOWED_ORIGINS(empty)Extra allowed Origin values, comma-separated.
GANJOOR_API_BASE_URLhttps://api.ganjoor.netUpstream API base URL — point at a local RMuseum instance for development.
GANJOOR_SITE_BASE_URLhttps://ganjoor.netFront-end base used to build clickable links.

Concepts

Ganjoor's data model has three nested levels. Getting these straight is the difference between a productive and a fruitless agent session.

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"]
  • Poet — an author. Hafez is poet id 2, Saadi 7, Rumi 5, Khayyam 3, Ferdowsi 4. ganjoor_list_poets resolves a name to an id.
  • Category — a book or section inside a poet's work (CatType 0 general, 1 poems, 2 book). Hafez's root category is 9; his ghazals are 24.
  • Poem — one text, identified by a numeric id and a URL slug such as hafez/ghazal/sh494.
  • Verse — a hemistich. Two consecutive verses form a couplet (versePosition 0 and 1).

URL slugs need a leading slash. The upstream API returns 404 for ?url=hafez/ghazal/sh494 but resolves ?url=/hafez/ghazal/sh494. This server normalizes for you, so either form works.

Persian text is returned verbatim. It is right-to-left with Arabic-script diacritics used for prosody. Do not transliterate or translate it unless the user explicitly asks.


Tool reference

Poets and collections

ToolDescription
ganjoor_list_poetsAll published poets with biography, dates, places. Paginated.
ganjoor_get_poetOne poet by id or slug, with biography and top-level books/sections.
ganjoor_get_poets_by_centuryEvery poet grouped into historical half-centuries.
ganjoor_list_booksNamed collections, filterable by poet or name substring.

Categories

ToolDescription
ganjoor_get_categoryA category with breadcrumb, sub-sections, and contained poems.
ganjoor_list_category_poemsPaginated poems in a category, with optional title filter.
ganjoor_get_category_geotagsGeographic/historical places tagged under a category's subtree.
ganjoor_get_category_people_graphPeople network behind a category's "characters" tab.

Poems

ToolDescription
ganjoor_search_poemsFull-text search across poems, scoped by poet or category.
ganjoor_semantic_search_poemsMeaning-based search ("find a poem about…").
ganjoor_get_poemA complete poem by URL, with verses grouped into couplets.
ganjoor_get_poem_by_idThe same, by numeric id.
ganjoor_get_poem_versesJust the verses — lighter than fetching the whole poem.
ganjoor_get_random_poemA random poem, optionally from one poet.
ganjoor_get_hafez_faalA random Hafez ghazal for Persian divination.
ganjoor_get_page_urlResolve any page id (poem, category, poet) to its web URL.
ganjoor_get_redirect_urlResolve a legacy Ganjoor URL to its current address.

Prosody, rhyme, and structure

ToolDescription
ganjoor_list_rhythmsAll Persian prosodic metres with usage counts.
ganjoor_analyze_poem_rhythmDetect a poem's metre.
ganjoor_analyze_poem_rhymeDetect a poem's rhyme letter and pattern.
ganjoor_find_similar_poemsPoems sharing a metre and rhyme — the reply-poem graph.
ganjoor_get_poem_sectionsA poem's structural sections (bonds, masnavi bands, tercets).
ganjoor_get_sectionOne section with verses and related sections.
ganjoor_get_related_sectionsSections from other poems matching this one's metre and rhyme.
ganjoor_get_couplet_sectionsWhich sections a given couplet belongs to.
ganjoor_get_language_tagged_sectionsStanzas tagged with a non-Persian language code.

Audio recitations

ToolDescription
ganjoor_search_recitationsSearch recitations by reciter, poet, or category.
ganjoor_get_recitationOne recitation with its audio URL.
ganjoor_get_recitation_sync_infoPer-verse timing for following along with audio.
ganjoor_get_poem_recitationsEvery recitation published for one poem.
ganjoor_get_category_top_recitationsMost-upvoted recitations within a category.

People

ToolDescription
ganjoor_list_peopleHistorical and literary figures in Ganjoor's knowledge graph.
ganjoor_get_personOne person's biography, dates, and places.
ganjoor_get_person_poemsPoems tagged with a person.
ganjoor_get_person_relationsA person's kinship and affiliation edges.
ganjoor_get_person_family_treeThe whole connected kinship component from one person.

Commentary, FAQ, and supplementary material

ToolDescription
ganjoor_get_recent_commentsRecent published reader commentary.
ganjoor_list_faq_categoriesGanjoor's editorial FAQ categories.
ganjoor_get_faq_itemsAll Q&A entries in one FAQ category.
ganjoor_get_faq_itemA single FAQ entry by id.
ganjoor_get_poem_imagesManuscript and facsimile images.
ganjoor_get_poem_quoted_poemsPoems this one quotes, and poems quoting it.
ganjoor_get_poem_effective_correctionsThe editorial before/after record of text corrections.
ganjoor_get_poem_geotagsPlaces and commemorative dates tagged to one poem.

Every tool is annotated readOnlyHint, idempotentHint, and openWorldHint, and none of them modify state on Ganjoor.


Response formats

Every tool takes a response_format argument:

  • markdown (default) — human-readable output with headers, lists, and clickable links.
  • json — the same data as a structured object, also returned as MCP structuredContent so clients that support it can consume fields directly without parsing text.

List tools share a consistent pagination envelope:

{
  "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 and page_size are 1-based; page_size caps at 100. Responses over 25,000 characters are truncated with an explicit truncated flag rather than silently cut.

Two upstream endpoints (/api/ganjoor/poets and /api/ganjoor/rhythms) ignore their paging parameters and always return the whole collection. The server detects that and slices the requested window locally, so page and has_more stay correct and consistent across every list tool.

Errors are returned as tool results (not protocol errors) with a message that names the endpoint and suggests a next step — for example, a 404 points you at ganjoor_list_poets to find a valid id.


Usage

"Who is Hafez?"

ganjoor_get_poet { "url": "hafez" }

"Show me Hafez's ghazal 494"

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

"Which Hafez poems contain the word صبر?"

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

"What metre is this poem in, and who else wrote in that metre?"

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 }

"Who are this figure's ancestors?"

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

"What does Hafez say about my question?"

ganjoor_get_hafez_faal { }

Architecture

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

Two conventions run through the codebase:

  • defineTool() wraps every registration. It guarantees each tool reports failures as isError results with actionable messages, renders markdown or JSON consistently, and returns structuredContent alongside text.
  • md() builds documents. It preserves explicit blank lines so markdown headings and lists are separated properly, while dropping null/false so conditional fragments disappear cleanly.

Upstream quirks are normalized in client.ts and the tool modules rather than leaking to callers — for example the { poet, cat } envelope on category endpoints, the nested category.cat on poem payloads, and the leading-slash requirement on URL parameters.


Development

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 boots the server over an in-memory transport, asserts every tool has a substantial description, and invokes all 44 tools against the live API, failing on any error response.


Known limitations

  • Semantic search may be disabled. Ganjoor's embedding backend is optional; when a deployment has it turned off, ganjoor_semantic_search_poems returns a 503. The tool surfaces a clear message and points you at ganjoor_search_poems with a Persian keyword instead.
  • Redirect resolution is sparse. Ganjoor only stores redirects for paths it actually renamed, so most valid URLs return has_redirect: false rather than a target.
  • Some tools always need authentication. Endpoints behind a Ganjoor account (bookmarks, posting comments, uploading corrections) are out of scope by design — this server is read-only and anonymous.
  • The book catalog is curated. ganjoor_list_books covers ~168 named collections and omits some major poets — Hafez has no entries. Use ganjoor_get_poet with include_cats: true instead, which reads each poet's sections straight from their root category.
  • No total counts on paged endpoints. The upstream API pages without reporting totals, so total is omitted where unknown; has_more is inferred by fetching one row beyond the page.
  • Poetry text coverage is curated. Person tagging, geo tags, and language tags are incomplete by nature — an empty result is usually a coverage gap, not proof of absence.
  • Date fields are sometimes approximate. Ganjoor flags dates it is unsure about (validBirthDate / validDeathDate); the tools expose those flags so callers can filter.

Evaluation

evaluation/evaluation.xml holds 10 read-only questions with verified answers. Each one requires several tool calls to answer and was checked against the live API before being written down:

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

The questions deliberately avoid single-lookup trivia. They cross-reference poets against centuries, walk the kinship graph, compare the prosody of a poem against the corpus-wide metre list, and resolve a reply-poem chain through quotation records.


Contributing

See CONTRIBUTING.md.

From the server's repository — third-party content, read it before running anything it suggests.

More in Other

Listing from the official MCP Registry · repository facts from GitHub, 1h ago.