Skip to main content
Glama
ifte66
by ifte66

Sanad MCP server

A scholar-approved Islamic corpus served to any MCP client, with refusal enforced server-side. A prompt can be talked around; a server that returns no passages cannot.

Quick start

npm install
node ingest.mjs        # ~6s, builds corpus.generated.json (9,209 passages)
node eval.mjs          # 46/46, 0 fabrications
node smoke-test.mjs    # end-to-end check

Then point Claude Desktop at it — Settings → Developer → Edit Config:

{ "mcpServers": { "sanad": { "command": "node", "args": ["C:/path/to/sanad-mcp/server.mjs"] } } }

Forward slashes, absolute path. Or from a terminal: claude mcp add sanad -- node ./server.mjs

Related MCP server: tero-mcp-lite

What's in the corpus after ingest

Source

Passages

Grading

Qur'an (Uthmani + Pickthall)

6,236

Qur'an

Sahih al-Bukhari

1,278

Sahih (by collection)

Sahih Muslim

1,281

Sahih (by collection)

Muwatta Malik

414

none attributed

Gradings are recorded exactly as the upstream dataset attributes them. Bukhari and Muslim are marked "Sahih (by collection)" — inherited from the collection, not an individual isnad grading, and the server never presents it as one. Malik carries no grading because the dataset provides none, and inventing one would be the exact failure this project exists to prevent.

Every passage lands review_status: "unreviewed". reviewed_by stays null until a named scholar signs off, and corpus_inventory reports that honestly.

sources.json controls which editions get ingested. The default is Pickthall (d. 1936), public domain in Australia under life + 70.

Do not switch to en.sahih (Saheeh International), en.asad, en.hilali or en.itani and then redistribute the corpus. Those are under active copyright. Yusuf Ali (d. 1953) entered Australian public domain in 2024. The pipeline ships without a generated corpus for this reason — you build it locally against whichever edition your scholar names.

The eval harness

evalset.json holds 46 labelled cases. Each declares the verdict the system must return. This is the artefact the scholar contributes to.

node eval.mjs            # run the suite
node eval.mjs --sweep    # grid-search the gates
node eval.mjs --verbose  # show why each failure failed

Two error types, reported separately because they are not equally bad:

  • Fabrication — answered when it should have refused. Exit code 1. Ship-blocking.

  • Over-refusal — refused when the corpus did cover it. Merely unhelpful.

Current: 46/46, 0 fabrications, 0 over-refusals.

The last over-refusal to fall was not a retrieval problem at all — see The corpus speaks 1930 English below. Zero over-refusals against 46 cases is not the same as zero over-refusals; it means the eval set has stopped finding them, which is a reason to write harder cases, not to stop looking.

Three gates, and which one matters

Gate

Value

What it does

retrieval_threshold

0.15

Absolute BM25. Currently near-inert.

min_coverage

0.35

Share of IDF-weighted query terms the passage accounts for

max_oov_ratio

0.25

Query weight sitting in terms absent from the corpus entirely

require_pivot_term

false

Tried, measured, unproven. See below.

A fourth input, lexicon.json, sits upstream of all three: it decides which query words the corpus is even able to express. See below.

The sweep showed BM25 score barely affects outcomes — rows are near-identical from 0.15 to 0.45. Coverage and OOV do the work. This is written down rather than tuned away, because a knob that does nothing is worth knowing about.

max_oov_ratio is the one that makes refusal survive scale. A query containing "Kuraby" or "al-Ghazali" — proper nouns the corpus has never seen — cannot clear it, however many common words sit beside them.

Gates are corpus-dependent. Re-run --sweep after any material corpus change. corpus.sample.json (the original 18-passage fixture) over-refuses under these settings, which is the point: thresholds calibrated for 9k do not transfer to 18.

Three bugs that only appeared at real scale

corpus_inventory returned every passage id. Fine at 18. At 9,209 it exceeded the 1MB tool-result limit and the call failed outright. Now returns counts plus a 15-row sample; the full list lives on the sanad://corpus resource.

Apostrophes split "Qur'an" into qur + an. The query term quran then matched no Qur'anic verse at all — but it did match a hadith whose English contains "Glorious Quran". Asked "what does the Quran say about orphans", the server returned a hadith about reciting Surah Qaf in the morning prayer.

No stemming. Bukhari 1 says "intentions"; the query says "intention". Different tokens, no match. Both sides are now stemmed with a conservative rule set.

Fixing those three took the orphans query from a prayer-timing hadith to Q 90:15, Q 107:2 and Q 93:9 — all genuine orphan verses.

The corpus speaks 1930 English

Pickthall does not contain the word "gambling". Not once in 6,236 verses. He renders maysir as "games of chance" and khamr as "strong drink".

So "What does the Quran say about wine and gambling?" hit the OOV gate — gambling has document frequency 0, was charged maximum IDF, and drove the out-of-vocabulary ratio to 0.45 against a 0.25 ceiling. The server refused, citing no source, while Q 2:219, Q 5:90 and Q 5:91 sat in the corpus saying exactly that.

The gate was not wrong about the arithmetic. It was wrong about what an unknown term means. "Kuraby" is a subject the corpus does not cover. "Gambling" is a subject it covers under different words. Treating those two identically is a false negative wearing the costume of integrity — and on a corpus of religious translation it is the common case, not the edge case.

lexicon.json is the bridge: modern query word to the phrasing this edition actually uses. Three rules keep it honest.

  • Whole phrase or nothing. "strong" alone is not "strong drink". Every word of the phrase must be in the passage.

  • A phrase must exist in the corpus. An alias whose words the corpus has never seen is not usable, and the term stays out of vocabulary.

  • It is a translation bridge, not a synonym table. An aliased term is weighted by the phrase's own distinctiveness, not by max IDF — but it is still weighted, and it can still fail coverage. What must never go in the file is an entry asserting that two concepts are the same (interest -> usury, umrah -> pilgrimage). That is a scholarly judgement, and the tokeniser has no business making it.

The bridge is query-side only. It changes which passages are found; it never changes what a passage says.

It is edition-specific by construction — Yusuf Ali does not write "strong drink" — so lexicon.json records the edition it was written against and the server prints a warning at startup if sources.json names a different one.

Case n15 guards the boundary: "What did Sheikh al-Qaradawi rule about gambling?" must still return no_source. gambling is now expressible; al-Qaradawi is not, and that alone must be enough to refuse.

A gate that did not earn its place

require_pivot_term demands that the most distinctive query term appear in a passage. It was written to fix the orphans problem. Once tokenisation and stemming were corrected it made no measurable difference — 43/44 either way, identical rankings on the queries that motivated it.

It is off by default and kept behind a flag rather than deleted, so the idea is not re-invented. Revisit if the eval set grows and a case appears that needs it. The lesson is that the gate was treating a symptom; the bug was in the tokeniser.

Two findings worth repeating to the scholar

Retrieval broke silently when the corpus grew. At 18 passages, refusal worked. At 9,209, "When is the mosque in Kuraby open?" started returning confident answers, because scores were normalised against the best hit — so the top result always looked like a match. Nothing about the demo would have revealed this. Only the eval set did.

One apparent bug was a mislabelled question. "What is the Hanafi position on raising hands in prayer?" was tagged unanswerable, but the corpus does hold hadith on raising hands. What it lacks is the madhhab attribution. Presenting those hadith as "the Hanafi position" is fabrication of a different kind, so the case moved to the policy gate and a new rule was written. The eval set improved the policy, not the code.

Tools

Tool

Purpose

answer_question

Main path. Gate, then retrieve, then verdict.

search_corpus

Raw retrieval, no policy gate. Not for user-facing questions.

get_passage

Fetch one passage by id.

check_policy

Test where the refusal boundary sits.

corpus_inventory

Coverage and review status, reported honestly.

list_gaps

Distinct failed questions, ranked by how often asked. The improvement queue.

Files the scholar reviews

  • sources.json — which editions. His call, not a default he inherits.

  • policy.json — nine refusal rules as plain regex with human labels. A draft.

  • lexicon.json — the edition's vocabulary, in his edition's words. Rewrite it if he names a different translation.

  • evalset.json — the labelled questions. His to extend.

  • gaps.jsonl — written at runtime, every failed question.

He should never need to open server.mjs.

Not yet built

Arabic morphological stemming (currently diacritic stripping only). Embedding retrieval — which would make most of lexicon.json unnecessary, at the cost of an inspectable failure mode. Per-madhhab tagging. A review UI so approval does not mean hand-editing JSON. Audio.

The coverage gate now has headroom: the sweep passes 46/46 at min_coverage 0.45 as well as 0.35. Tightening it would buy margin against unseen questions at the price of over-refusing ones the eval set has not thought of yet. Left at 0.35, and written down rather than changed quietly — that call belongs to the scholar.

F
license - not found
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables fetching and searching canonical hadith texts (Arabic and English) with cross-references and citation-safe URLs for assistants, built on FastMCP.
    1
    GPL 3.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server to search and retrieve passages from a corpus of 7,872 classical Islamic books via the Sahifah API, with full citations and mu'tabar filtering.
    19
    MIT

View all related MCP servers

Related MCP Connectors

  • Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.

  • Bible corpus MCP server: scripture, Greek/Hebrew interlinear data, cross-refs, semantic search.

  • AI Reasoning Cache & Consensus Layer with 11 MCP tools via Streamable HTTP.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ifte66/sanad-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server