Skip to main content
Glama

quant-rag

Retrieval over a closed library of quantitative-finance research, where every retrieval choice is settled by a paired measurement and every citation can be checked without a language model.

CI Python Tests License

418 documents (papers, working papers, books; 1971–2026) · 26,120 passages · 10 MCP tools · 85 ms median search, warm · 546 tests that run without the corpus

An LLM agent asks questions about market microstructure, volatility models, backtest statistics or portfolio construction. The server answers with passages that arrive whole (never cut inside a formula, a table row or a word), each with a citable source, a character-level anchor into the document's canonical text, and a trace naming the exact server, corpus and configuration that produced it.

The engine itself is deliberately plain: dense retrieval on Qwen3 embeddings in an embedded Qdrant. The work is in the measurement behind it: two benchmark families built against the known-item bias, an LLM judge whose accuracy and noise are measured on every run, placebo arms, paired bootstrap confidence intervals, and a written protocol before each major experiment. Several popular techniques were measured on this corpus, and most of them were rejected.

Results at a glance

Paired differences in nDCG@10 (same questions in both arms), 95 % bootstrap CI; * = the interval excludes zero. Full protocols and tables: docs/evaluation.md.

Question

What was measured

Decision

Does hybrid BM25 + RRF + cross-encoder beat dense?

Best configuration on a hand-written known-item bench (0.935 vs 0.853), but −0.140 [−0.240, −0.043]* on the first leak-controlled natural-language bench

dense by default; hybrid only on request, for remembered identifiers

Can a rule-based router pick the right path?

+0.021 [+0.002, +0.050]* on the 65 calibration questions, then −0.019 [−0.041, +0.002] on the 130-question bench that followed (−0.027 on its 90 unseen questions); a perfect router would add +0.055

auto = dense; the router's detector now only logs

RRF fusion of dense and BM25 (14 variants)

−0.088 [−0.124, −0.054]*

rejected

bge-reranker-base on the dense pool

−0.092 [−0.148, −0.033]*

reranking off in dense mode

Qwen3-Reranker-0.6B on the dense pool

+0.070 [+0.018, +0.125]*, R@1 0.25 → 0.37, at 15 s median per query

not shipped; reranking only the top 10 keeps half the gain at 3.3 s

Re-embedding every passage with a clean, consolidated title

+0.031 [+0.003, +0.059]*; questions whose gold passage falls outside the top 50: 39 → 26

adopted

Filtering by publication year on dated questions

+0.100 [+0.029, +0.184]*

adopted; a period written in the question ("published before 2010") is parsed into the filter: 15/15 detected, 0 false positives on 160 other questions

Fusing entity-graph hits into the ranking

−0.105 [−0.135, −0.074]*

rejected; the graph stays a separate exploration tool

Served passages cut inside a formula, table row or word

178 of 760 → 0, for +14.1 % context

output contract

Citation check, no language model involved

50/50 real quotes found, 0/50 one-word alterations accepted

verify_citation

Questions whose answer is not in the corpus

20/20 abstentions, 0 fabrications

answer prompt

Related MCP server: Octagon MCP Server

Try it in 30 seconds — no corpus, no model download

git clone https://github.com/Elias-outofsample/quant-rag && cd quant-rag
python3 -m venv .venv && .venv/bin/pip install -e .     # Python 3.12+
.venv/bin/python rag/demo.py

The demo runs the real serving functions on one synthetic document (only the storage layer is swapped for an in-memory text):

1. The output contract: a passage arrives whole, or cleanly interrupted
cap  325 (inside the display formula)
   raw cut      … \kappa (\theta - s_t)\,dt + \sigma\, dW_t,       breaks: an open $$ block
   contract cut …$ is where the spread returns after a shock.      breaks: nothing · 678 characters announced as remaining
cap  752 (inside a table row)
   raw cut      …A | 0.35 | 0.00 | 0.12 | 1.98 |⏎| B | 0.08 |      breaks: half a table row
   contract cut …-|---|---|⏎| A | 0.35 | 0.00 | 0.12 | 1.98 |      breaks: nothing · 209 characters announced as remaining

2. Quote verification, without a language model
   copied from the passage              found · offsets [376, 467] · exacte
   retyped: spacing, hyphen, capitals   found · offsets [376, 467] · exacte après normalisation
   one word changed (more -> less)      NOT found · closest passage: similarity 0.933, 2 word edits
   stitched from two places             NOT found · closest passage: similarity 0.833, 4 word edits

3. The query side: publication periods and exact tokens
   'volatility targeting, according to papers published between 2015 and 2020'
      -> year_min=2015 year_max=2020, embedded query: 'volatility targeting'

How it works

flowchart TB
  subgraph ingest["1 · Ingestion, offline"]
    direction LR
    PDF["PDF"] --> P["MinerU parse"] --> K["passages with<br/>section path"] --> M["metadata with provenance;<br/>tables to Markdown"] --> E["Qwen3-Embedding-0.6B"]
  end
  subgraph store["2 · Corpus state, named by its signature"]
    direction LR
    Q[("Qdrant, embedded")]
    B[("BM25 index")]
    G[("GLiNER2 entity graph")]
    Q ~~~ B ~~~ G
  end
  subgraph serve["3 · Serving"]
    direction LR
    U["question"] --> D["period clause<br/>to year filter"] --> R{"mode"}
    R -->|"auto"| DN["dense top-k"]
    R -->|"hybrid, on request"| H["BM25 + RRF<br/>+ cross-encoder"]
    DN --> S["drop headings and near-duplicates;<br/>max 2 passages per document"]
    H --> S
    S --> O["output contract:<br/>whole passage, anchor, trace"]
  end
  ingest --> store --> serve --> MCP["MCP server, 10 tools"] --> A["LLM agent"]

Every step is described, with its measured cost, in docs/architecture.md. The promises made on what is served are in docs/output-contract.md.

What a served passage looks like

Real output of search_documents (the server's labels are French: citer = cite as, ancre = anchor, interne = internal keys, qualité = known defects of the passage):

routage: dense — dense par défaut (calibration v3 : aucune règle ne bat le dense) · rerank: non · dense_top1=0.719 · 12406 ms
trace: request_id=8a7bdb351ec64fbb · serveur 1.4.0 · contrat 1.0.0 · corpus e1bdf36e2e · config aa5531c56ba9 · fenêtre 10000 c.

[1] cosine=0.719
    citer   : Ding et al. (2025) — Deep Learning Option Pricing with Market Implied Volatility Surfaces, p. 3
    section : III. RESULTS
    interne : chunk_id=chunk-169b5be73892b3f3 · document_id=doc-96437e7c996bb218 · mixed (clés de travail, ne pas citer)
    ancre   : doc-96437e7c996bb218 · caractères 10861–13884 (exacte) · sha256:c41346f98382
    qualité : has_html_tags

The 12.4 s of this first call include loading the embedding model; warm searches take 75–89 ms. The ancre line is what verify_citation(document_id, quote) checks a quote against: character offsets in the canonical text of the document and the SHA-256 of that text, so an anchor rebuilt on a different text is detected.

The ten MCP tools

Measured on the running server over JSON-RPC.

Tool

What it returns

Latency

search_documents

ranked passages, with source, anchor and trace; filters by year and author

84 ms

get_passage

one passage with its neighbours, never truncated mid-structure

363 ms

list_documents

bibliography, filtered by author, period or text

1.8 ms

timeline

how a topic evolves, grouped by publication year

454 ms

verify_citation

found / not found, offsets, and the closest match when not found

83 ms

verify_citations

the same, in batch

156 ms

corpus_status

collection, counts, backend, device, contract, provenance

2.7 s

search_graph

passages mentioning the entities of a query

288 ms

expand_entity

typed neighbours of an entity (depends_on, uses_method, …) and co-mentions

17 ms

connect_entities

passages and documents where two entities meet

181 ms

How the evaluation is built

The first benchmark was the usual one: 25 questions written by hand while looking at the passage that answers them. It measured an IDF-weighted lexical leak of the question into its answer of 0.533 at the median and 0.916 at worst: retrieval on those questions is string matching. It crowned the hybrid pipeline, and that verdict reversed on questions worded the way a practitioner asks them. The benchmarks that followed are built against that bias:

  • Question factory. An LLM drafts a question from a passage as a practitioner would ask it; a second call rewrites it without seeing the passage; a leak score above 0.42 (the first quartile of the hand-written set) sends it back with the offending terms named; a verifier rejects questions that are unanswerable, not self-contained, or answered equally well by many other passages. The rejection rate is itself a finding: 12 % of table questions survive, against about a third of text questions.

  • Negative questions name a source that is provably absent: every spelling of it is checked absent from the corpus text. The retriever is only ever used to refute a negative question, never to certify one.

  • Two score families, never merged. Retrieval (nDCG@10 with graded gains, recall, MRR) needs no judge and stays valid when the judge model changes. Generation (groundedness, coverage of reference facts extracted before any answer existed) is judged; abstention is an exact string match.

  • The judge is measured on every run: sentinel answers with a known correct grade (verbatim gold, off-topic, fluent but unsupported, wrongful refusal) give its accuracy (0.889 on the v3 baseline), and a double-graded sample gives its noise floor. Generator and judge are different models.

  • Placebo arms. Regenerating the same configuration left only 55 % of answers identical and produced a "significant" +0.118 [+0.029, +0.235] change in fabrications on one family, with no variable changed. Since then both arms of a comparison are drawn in the same run, and a difference whose interval overlaps the placebo's is not reported as a result.

  • Instruments are proven before use. The in-memory dense matrix used to run experiments in parallel reproduces Qdrant's top 50, in the same order, on 155/155 questions; replaying the old bench in the new code reproduces the published numbers with a maximum deviation of 0.00.

  • Written protocol first. The larger experiments start from a dated pre-registration (question, metric, decision rule) and end with a report and a verdict: keep, reject, or keep as experimental.

Engineering

  • Explicit corpus state. The document set, the overlays (deduplication, Markdown tables) and the consolidated titles hash to a corpus signature. The corpus is frozen between batches; unfreezing requires a written reason, kept in the history. The BM25 index file is named by that signature, and a check proves that production and benchmark serve the same index.

  • Ingestion without an operator. A batch driver runs eight steps per PDF (parse, survey, diagnosis, import, registry and index updates, entity graph, retrieval probes, commit), holds an exclusive lock, survives a kill -9, and rolls an import back symmetrically. On a real 6-page paper: import in 25.9 s, zero LLM calls.

  • One home per decision. The Qdrant backend (embedded or server), the served window, the model prices used for cost accounting: each lives in one module, and tests fail if a literal copy reappears elsewhere.

  • Installation check. verifier_installation.py names every artefact the served path depends on, checks coherence rather than presence (index and collection counts against the registry, unresolved LFS pointers), says how to rebuild each one, and exits non-zero if one is missing.

  • Tests. 674 tests; 546 run in CI without the corpus; those that read the private corpus are skipped with the missing path (rag/conftest.py).

Repository layout

rag/
  quant_rag.py              retrieval: period parsing, routing, dense / hybrid, filters, CLI
  mcp_server.py             MCP server, ten tools
  contrat.py                output contract: safe cut, quality flags, anchor and trace lines
  citation.py  ancrage.py   quote verification; passage anchoring in the canonical text
  graph_search.py           entity-graph queries: mentions, neighbours, meeting points
  reranking.py              cross-encoder and Qwen3 rerankers
  qdrant_backend.py         embedded or server Qdrant, one home for the choice
  build_index.py            rebuild the collection from the export and the overlays
  corpus_overlay.py         deduplication, tables and titles overlays, applied wherever the corpus is read
  gel_corpus.py             corpus freeze, with its history
  verifier_installation.py  what the served path needs, and how to rebuild it
  demo.py                   corpus-free tour of the served surface
  ingestion/                parse, inspect, import, roll back; batch driver; registry
  metadata/ titles/ tables/ graph/    corpus curation passes
  benchmark/                about 100 experiment scripts and their tests (see its README)
  agents/rag-scout.md       minimal system prompt for the agent consuming the MCP tools
src/                        parsing and lexical primitives (see NOTICE.md)
tests/                      tests of the two original parsing modules in src/, and of the demo
docs/                       architecture, evaluation, output contract

Running it

uv venv --python 3.12 .venv
VIRTUAL_ENV=.venv uv pip install -e ".[dev]"
.venv/bin/python -m pytest -n auto -rs     # no corpus needed; corpus tests report what is missing

Serving queries needs the model extras (.[models]: torch, sentence-transformers) and a corpus state on disk: parsed documents, the vector export and the overlays under data/ and rag/. None of it is distributed (see below). With a corpus in place:

.venv/bin/python rag/build_index.py                                    # embedded Qdrant collection
.venv/bin/python rag/quant_rag.py "square-root law of market impact"
.venv/bin/python rag/quant_rag.py "cointegration test" --year-max 2000
.venv/bin/python rag/quant_rag.py --timeline "rough volatility"
claude mcp add quant-rag -- "$PWD/.venv/bin/python" "$PWD/rag/mcp_server.py"

Scope

  • Not distributed: the corpus (third-party copyrighted documents) and everything derived from it: vectors, BM25 index, entity graph, consolidated metadata, benchmark questions and per-run results. Test fixtures that came from the corpus were replaced by structure-preserving scrambles; the three short quotes kept for the citation tests are bibliographic-length.

  • Research log kept private: about forty dated pre-registrations and reports, in French, quote the corpus. Code comments that point to docs/… or RAPPORT-… files refer to it.

  • Language: identifiers, comments and test names are mostly in French, the working language of the project; the documentation is in English.

  • LLMs: none on the served path. Language models only draft, launder and verify benchmark questions and judge answers (Mistral small and medium; Gemini once, as a separate instrument).

  • Hardware: developed and measured on an Apple M4 with 16 GB (MPS); CPU works, slower.

Stack: Python 3.12 · Qdrant · Qwen3-Embedding-0.6B · BM25 · bge / Qwen3 rerankers · GLiNER2 · MinerU · Model Context Protocol · NumPy.

License

All rights reserved. The code is published for reading and review; see LICENSE and, for the provenance of src/, NOTICE.md.

Available Tools

10 tools
connect_entitiesA

Passages et documents qui mentionnent LES DEUX entités (« SVI » et « rough volatility »).

Pour trouver où deux idées se rencontrent dans le corpus. Liste les documents par nombre de passages communs, puis les meilleurs passages avec chunk_id. Aucun passage commun est une information en soi : les deux entités ne sont jamais citées ensemble dans un même passage.

Args: entity_a: première entité (nom ou fragment, variantes réunies). entity_b: seconde entité. top_k: nombre de passages (défaut 10, au plus 2 par document).

Inputs: entity_a (required), entity_b (required), top_k. Returns: result

corpus_statusA

Décrit l'index : documents, passages, couverture des métadonnées (années, auteurs), backend et device, et l'état du graphe d'entités (nombre d'entités, relations, état du corpus sur lequel il a été bâti).

Inputs: no named parameters. Returns: result

expand_entityA

Entités reliées à entity_name dans le graphe, par relation, avec les passages qui l'attestent.

Répond à « quelles méthodes dépendent de la rough volatility ? », « à quoi SVI est-il comparé ? ». Chaque voisin porte la relation et son sens (→ : l'entité est la tête, ← : la queue), le nombre de passages qui soutiennent le lien et jusqu'à trois chunk_id à lire avec get_passage. Les relations viennent de GLiNER2 (seuil 0,5) et sont clairsemées : un lien attesté par un seul passage est une piste, pas un fait ; lire le passage avant de l'affirmer. Sans relation, la réponse liste aussi les CO-MENTIONS : les entités citées dans les mêmes passages, pondérées par leur rareté — la vue la plus utile pour explorer un sujet (auteurs, modèles, mesures qui vont avec).

Args: entity_name: nom de l'entité (« rough volatility », « Heston model », « Gatheral »). L'entité exacte et ses variantes de nom (« rough volatility models ») sont réunies. relation: une seule relation (measures, predicts, causes, depends_on, correlates_with, applies_to, uses_method, compares_with, is_a, part_of) ; vide = toutes. entity_type: type de l'entité de départ, si le nom est ambigu (person, market_concept…). limit: nombre de voisins (défaut 20), classés par nombre de passages qui les attestent.

Inputs: entity_name (required), limit, relation, entity_type. Returns: result

get_passageA

Retourne le passage complet d'un chunk_id, avec ses voisins immédiats, pour citation.

Args: chunk_id: la clé interne rendue par search_documents ou search_graph. max_characters: fenêtre servie. Vide = 15 000, la fenêtre du contrat de sortie. Le défaut historique était 6 000 et tronquait 30,2 % des fenêtres du corpus (médiane 4 520 c., max 13 982) : un outil dont le nom promet le passage entier en rendait moins d'un tiers du temps. neighbours: nombre de voisins de chaque côté (défaut 1, 0 pour le seul chunk demandé). Les voisins ne sont pas filtrés comme les résultats de recherche — un voisin en-tête est un intitulé de section, donc du contexte — mais ils sont déclarés dans la ligne « voisins : ».

Inputs: chunk_id (required), neighbours, max_characters. Returns: result

list_documentsA

Liste bibliographique des documents du corpus, du plus récent au plus ancien, sans recherche vectorielle.

Args: author: sous-chaîne du nom d'un auteur (insensible aux accents et à la casse). year_min: année de publication minimale (inclus). year_max: année de publication maximale (inclus). text: sous-chaîne du titre. limit: nombre maximal de lignes (défaut 50).

Inputs: text, limit, author, year_max, +1 more. Returns: result

search_documentsA

Cherche des passages dans le corpus de recherche quantitative (corpus dont la taille n'a pas pu être lue).

Couvre la volatilité et les options, la microstructure et l'exécution, les facteurs et l'alpha, la construction de portefeuille et le ML appliqué à la finance (1971–2026). Retourne des extraits classés, précédés de la décision de routage. Chaque extrait porte une ligne « citer : » — auteurs, année, titre, page — et c'est la seule à recopier dans une réponse : l'année fait partie de l'information, le corpus mélangeant des travaux de 1987 et des preprints de 2026. La ligne « interne : » porte chunk_id et document_id, qui servent à get_passage et ne sont pas des citations — ils ne survivent pas à un re-découpage du corpus et ne disent rien à un lecteur.

Les pages affichées sont celles d'un lecteur, et une plage quand le passage en couvre plusieurs : le système ne dispose pas d'un pointeur de phrase, et annoncer une page exacte pour un passage à cheval serait une précision qu'il n'a pas.

Protocole d'usage, mesuré sur 155 questions (benchmark/eval_protocol.py) :

  • Une période explicite (« sources published in 2022 or earlier », « papers before 2010 », « depuis 2020 ») est détectée dans la question et transformée en year_min/year_max, la clause retirée du texte : rien à faire, sauf quand la période est implicite (« récent », « d'avant la crise ») — alors la traduire soi-même en year_min/year_max (+0,10 nDCG@10).

  • Question ouverte, conceptuelle : une seule requête en langue naturelle, en anglais, qui décrit le problème ; la reformuler pour l'index n'a pas aidé (chantier C).

  • N'utiliser mode="hybrid" que pour une requête faite d'identifiants mémorisés (auteur, acronyme, numéro, année : « Fukasawa SVI B2 B3 ») ; sur toute autre question il perd (−0,11 nDCG@10 sur 130 questions, −0,19 sur les tableaux). Depuis le 5 septembre 2026 on sait pourquoi : ce chemin empile deux composants mesurés négatifs — la fusion (−0,088 pooled, 14 variantes essayées, zéro en GO) et le reranker bge-base sur pool dense (−0,076 pooled, mesuré sur ce corpus). Il reste utile pour le cas des identifiants exacts ; ailleurs, le choisir est une erreur documentée.

  • Question multi-documents ou exploratoire : compléter par search_graph / expand_entity / connect_entities sur les entités nommées de la question, puis lire les passages des deux sources ; ne pas fusionner aveuglément (mesuré : la fusion automatique perd).

Args: query: la question, en anglais de préférence (le corpus est anglophone). limit: nombre de passages à retourner (défaut 5). document_id: restreindre à un seul document. mode: "auto" (défaut) = recherche sémantique seule (dense) — depuis le banc de 150 questions, aucune règle automatique ne fait mieux. "hybrid" ajoute BM25+fusion+rerank : à demander explicitement quand la question est faite d'identifiants exacts dont l'utilisateur se souvient (Fukasawa, SVI, ITRAXX 2007, arXiv 1206.0682), et seulement dans ce cas — sur une question en langue naturelle, et sur les tableaux, l'hybride fait moins bien que le dense. rerank: laisser vide pour suivre le mode (activé en hybrid, désactivé en dense). True/False force. Mesuré : bge-reranker-base aide sur un pool hybride et nuit sur un pool dense (−0,076 nDCG@10 pooled, −0,147 sur les questions simples). Ne pas forcer rerank=True en mode dense. year_min: ne garder que les documents publiés à partir de cette année (inclus). year_max: ne garder que les documents publiés jusqu'à cette année (inclus). Une borne d'année exclut les documents dont l'année est inconnue (un nombre non lu de documents). Laissées vides, une période explicite dans la question est détectée et appliquée (la ligne « période: » de la réponse le dit) ; une borne donnée prime toujours. author: ne garder que les documents d'un auteur (sous-chaîne du nom, insensible aux accents et à la casse : "lopez de prado", "Gatheral", "Jacquier"). reclassement: "selectif" reclasse les dix premiers candidats par Qwen3-Reranker-0.6B en protégeant le rang 1 dense. Laisser vide (défaut) dans l'immense majorité des appels.

    **Ce que ça coûte** : ~3,2 s par requête contre 59 ms sans — un facteur **54** —
    et 2,4 Go de mémoire résidente tant que le modèle est chargé, sur une machine de
    16 Go qui tourne déjà avec 2,4 Go de swap. Ce n'est pas un réglage anodin.

    **Ce que ça rapporte** (155 questions, corpus 5530cba145) : l'or entre dans les
    cinq passages servis pour 19 questions et en sort pour 5. Net +14, et les cinq
    pertes sont un sous-ensemble strict des sept du reclassement plein.

    **Ce que ces 19 et ces 5 valent en RÉPONSE — mesuré le 8 septembre 2026** sur les
    24 questions concernées, jugées dans les deux bras : sur les 5 « pertes », **une
    seule** perd vraiment sa réponse ; les trois autres mesurables avaient une
    couverture nulle **dans les deux bras** — elles n'avaient rien à perdre. Sur les
    19 « gains », **12** gagnent vraiment une réponse. Le rapport réel est **12 pour
    1**, non 19 pour 5, et l'échange net vaut **+11 réponses**.

    **Ce que ça change pour toi** : le risque de dégrader une réponse en l'activant est
    **cinq fois plus petit** que ce que le paragraphe précédent laisse croire. Ce qui
    reste vrai, et qui est désormais la seule raison de ne pas l'activer partout, c'est
    le coût — 3,2 s et 2,4 Go sur une machine de 16 Go. Active-le sans hésiter quand la
    qualité de la réponse compte plus que trois secondes ; ne l'active pas en rafale
    sur une exploration où tu enchaînes dix recherches.

    **Quand le mettre** : quand un humain demande explicitement une recherche plus
    soignée, ou après avoir constaté qu'une première recherche a rendu des passages
    hors sujet sur une question dont on a de bonnes raisons de croire que le corpus
    porte la réponse.

    **Quand ne pas le mettre — et c'est le point important** : ne l'active pas « au
    cas où », ni systématiquement, ni selon une règle que tu te donnerais toi-même
    (longueur de la question, présence de chiffres, famille supposée). Une règle
    d'activation inventée par l'appelant est une **politique de routage non mesurée**,
    et ce dépôt a mesuré six fois que ses politiques de routage perdent. Le gain
    ci-dessus n'est valable que sur un usage à la demande ; il ne dit rien de ce que
    vaudrait une heuristique automatique, qui n'a jamais été évaluée.

    Un échec de reclassement ne casse jamais la recherche : l'ordre dense est rendu.

Inputs: query (required), mode, limit, author, +5 more. Returns: result

search_graphA

Cherche dans le graphe d'entités : les passages qui MENTIONNENT une entité dont le nom contient query.

Complémentaire de search_documents (sémantique) : ici la correspondance est nominale et exacte — « Gatheral » retourne les passages où Gatheral est cité, « SVI » ceux où SVI apparaît, même quand la question sémantique ne les ferait pas remonter. Les entités ont été extraites par GLiNER2 sur chaque passage (personnes, organisations, instruments, concepts de marché, mesures, méthodes, jeux de données). Même format de sortie que search_documents : source citable, pages, chunk_id (pour get_passage), plus les entités reconnues dans le passage. Croiser avec search_documents : le graphe ne connaît que les noms, pas le sens.

Args: query: nom d'entité ou fragment (« Gatheral », « rough volatility », « SVI », « S&P 500 »). Casse et accents ignorés ; « Lopez de Prado » trouve « López de Prado ». entity_type: restreindre à un type : person, organization, financial_instrument, market_concept, measure, method, dataset. relation: ne garder que les passages où l'entité est tête ou queue d'une relation de ce type : measures, predicts, causes, depends_on, correlates_with, applies_to, uses_method, compares_with, is_a, part_of. top_k: nombre de passages (défaut 10, au plus 2 par document).

Inputs: query (required), top_k, relation, entity_type. Returns: result

timelineA

Chronologie d'un sujet : le meilleur passage de chaque document, groupé par année de publication.

Utile pour voir comment une idée a évolué (rough volatility 2014 → 2026) ou pour distinguer les travaux fondateurs des preprints récents.

Args: topic: le sujet, formulé comme une question ou un intitulé. year_min: borne basse (inclus). year_max: borne haute (inclus). limit: nombre de documents (défaut 20).

Inputs: topic (required), limit, year_max, year_min. Returns: result

verify_citationA

Vérifie qu'une citation se trouve vraiment dans le document qu'elle nomme.

À utiliser avant d'affirmer qu'un document dit quelque chose, et surtout quand la citation vient d'ailleurs que d'un passage servi à l'instant : d'une note prise plus tôt, d'un résumé, d'une réponse antérieure. Une citation recopiée de travers, recollée à partir de deux endroits ou reformulée reste plausible à la lecture — c'est précisément ce que cet outil détecte, et rien d'autre ne le détecte.

La vérification est déterministe : aucune génération, aucun modèle de langue. La citation est normalisée (ligatures, <sup>/<sub>, tirets, guillemets, blancs, casse) puis cherchée dans le texte canonique du document, celui dont le sha256 est servi par la ligne « ancre : ».

Ce que rend l'outil :

  • trouve — vrai seulement pour une correspondance exacte après normalisation. Une citation dont un seul mot diffère rend false. Mesuré : 50 extraits réels sur 50 retrouvés, 0 accepté sur 50 extraits altérés d'un mot.

  • offsets — la position [début, fin] dans le texte canonique, opposable.

  • plus_proche — quand c'est faux : le passage le plus ressemblant, sa ressemblance et le nombre de mots qui diffèrent. C'est ce qui dit où la citation a dérivé.

  • source — texte canonique, ou texte servi pour les 5 914 tableaux dont le rendu Markdown n'est pas une sous-chaîne du document.

Args: document_id: la clé interne du document, servie par la ligne « interne : ». quote: le texte cité, tel qu'on s'apprête à l'affirmer. Une phrase ou deux suffisent ; au-delà d'un paragraphe, une reformulation invisible fera échouer la vérification pour une raison sans rapport avec l'honnêteté de la citation. chunk_id: facultatif, restreint la recherche du repli « texte servi » à ce passage.

Inputs: quote (required), document_id (required), chunk_id. Returns: result

verify_citationsA

Vérifie plusieurs citations d'un coup — même contrat que verify_citation.

Args: citations: liste d'objets {"document_id": …, "quote": …, "chunk_id": … (facultatif)}. Vérifier ensemble les citations d'une même réponse coûte à peine plus qu'une seule : le texte canonique d'un document n'est lu qu'une fois.

Inputs: citations (required). Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.3/5.0

    Scored across 10 tools

    Disambiguation4/5

    Most tools have clearly distinct purposes: search_documents (semantic search), search_graph (entity mention search), expand_entity (entity neighbors), connect_entities (co-mentions), get_passage (retrieve chunk), verify_citation/verify_citations (verification), list_documents (bibliographic listing), timeline (chronological overview), corpus_status (index status). The only potential confusion is between search_documents and search_graph, but their descriptions explicitly contrast semantic vs. nominal matching. verify_citation and verify_citations are clearly the same operation with batch vs. single, which is acceptable.

    Naming Consistency4/5

    Tool names mostly follow a verb_noun pattern: search_documents, search_graph, get_passage, list_documents, expand_entity, connect_entities, verify_citation, verify_citations, corpus_status, timeline. The pattern is consistent (imperative verb + object), though corpus_status and timeline are noun-only rather than verb_noun, which is a minor deviation. verify_citation vs verify_citations is a clear plural variant.

    Tool Count5/5

    10 tools is well within the ideal 3-15 range for a RAG/quant-research server. Each tool serves a distinct function: corpus status, search (semantic, graph, timeline), retrieval (get_passage), citation verification (single and batch), document listing, and entity graph exploration (expand, connect). No tool feels redundant or superfluous.

    Completeness5/5

    The tool surface covers the full RAG workflow: search (semantic, entity-based, timeline), passage retrieval, citation verification, document listing, corpus status, and entity graph exploration. The domain is a read-only research corpus, so CRUD operations are not expected. The only minor gap might be a tool to get full document text, but get_passage with neighbors and list_documents cover most needs, and the corpus is described as passage-based.

    Maintenance

    ActivitySlowing
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables natural-language research and structured, source-backed answers across SEC filings, earnings transcripts, company financials, stock and crypto market data, and prediction markets news and research.
      -
    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables natural-language questions that return structured, source-backed answers across SEC filings, earnings call transcripts, company financials, stock and crypto market data, and prediction markets news and research. It also exposes live documentation search, read, list, and refresh tools plus browsable docs resources for setup and discovery.
      -
    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables natural-language financial research across SEC filings, earnings transcripts, financial metrics, stock and crypto market data, stock news, and prediction markets, returning structured, source-backed answers within MCP clients.
      -
    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables natural-language financial research and analysis across SEC filings, earnings transcripts, company financials, stock and crypto market data, and prediction markets, returning structured, source-backed answers.
      -