Skip to main content
Glama

data-room-search — recherche dans une data room de contrats

Un tool qu'un agent LLM appelle pour répondre aux questions d'un avocat sur une data room (« Quels contrats avec le Groupe Brenalis expirent avant fin 2026 ? », « Lesquels sont régis par un droit étranger ? », « Y a-t-il des doublons ? »). Il est exposé en serveur MCP, donc utilisable depuis Claude (Desktop, Code ou claude.ai) ou tout autre client MCP. Un agent de démonstration sur LLM local (Ollama) est aussi fourni.

Le repo est livré avec une data room fictive de 20 contrats (voir Données) ; une vraie en contiendrait des milliers.

Démarrage rapide

Prérequis : Python ≥ 3.10.

python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

pytest                                    # 31 tests, sans LLM (serveur MCP compris)
python -m dataroom.indexer                # 20 contrats, 106 articles indexés
bash scripts/run_query.sh "exclusivité territoriale"          # le tool seul, sortie JSON

claude mcp add dataroom -- "$PWD/.venv/bin/dataroom-mcp"      # le brancher à Claude Code (autres clients : ci-dessous)
python -m dataroom.mcp_server --http      # ou en HTTP : http://127.0.0.1:8002/mcp
uvicorn dataroom.api:app --port 8001      # API REST : GET /tools, POST /tools/search_data_room, /tools/get_contract, /ask

Optionnel : agent local. Avec Ollama et un modèle qui gère l'appel de tools : python -m dataroom.agent "Lesquels sont régis par un droit étranger ?". Testé avec huihui_ai/qwen3.5-abliterated:27b (35 à 90 s par question sur un M1 Pro 32 Go) ; un autre modèle devrait fonctionner mais n'a pas été testé : DATAROOM_MODEL=<modèle> ou --model. Configuration complète dans dataroom/config.py (DATAROOM_DATA, DATAROOM_MODEL, OLLAMA_URL, DATAROOM_MCP_TOKEN).

Related MCP server: mcp-rag-agent

Brancher un client MCP

N'importe quel client MCP (Claude Desktop, Claude Code, Cursor, un agent maison) peut appeler search_data_room et get_contract. C'est le même contrat que l'API REST et l'agent Ollama : les trois s'appuient sur tool_definitions() et run_tool() de dataroom/tools.py.

En local, en stdio : la commande dataroom-mcp, installée par pip install -e ., lance le serveur sans argument.

claude mcp add dataroom -- /chemin/vers/data-room-search/.venv/bin/dataroom-mcp     # Claude Code

Pour Claude Desktop : dans les réglages de l'application, section Développeur, modifier la configuration (claude_desktop_config.json), puis redémarrer l'application.

{
  "mcpServers": {
    "dataroom": {
      "command": "/chemin/vers/data-room-search/.venv/bin/dataroom-mcp"
    }
  }
}

En HTTP (Streamable HTTP, sans état, réponses JSON) : lancer python -m dataroom.mcp_server --http, puis :

claude mcp add --transport http dataroom http://127.0.0.1:8002/mcp     # Claude Code

curl -s -X POST http://127.0.0.1:8002/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "search_data_room",
       "arguments": {"filters": {"governing_law": {"not_in": ["droit français"]}}}}}'

Sécurité. Par défaut, le serveur n'écoute qu'en local et rejette les requêtes dont l'en-tête Host est étranger (protection contre le DNS rebinding). Pour l'ouvrir au réseau, un jeton est obligatoire ; sans jeton, le serveur refuse de démarrer.

DATAROOM_MCP_TOKEN=<secret> python -m dataroom.mcp_server --http --host 0.0.0.0
claude mcp add --transport http dataroom http://<hôte>:8002/mcp --header "Authorization: Bearer <secret>"

En production, il faudrait ajouter HTTPS (reverse proxy) et, pour plusieurs utilisateurs, OAuth, que le SDK MCP prend en charge.

Testé avec :

  • le client MCP officiel en Python (in-process, stdio, HTTP) ;

  • le MCP Inspector, en TypeScript (stdio et HTTP) ;

  • Claude Code avec un vrai LLM (stdio) : il appelle search_data_room avec les bons filtres et cite les contrats ;

  • curl, y compris sur l'URL HTTPS publique du tunnel (liste des tools, appel réel, et 404 sans le chemin secret) ;

  • le connecteur personnalisé de Claude, via le tunnel.

Claude Desktop utilise le même mécanisme stdio que Claude Code.

Via le connecteur personnalisé de Claude (Desktop ou claude.ai), qui exige une URL HTTPS publique : bash scripts/mcp_tunnel.sh ouvre un tunnel Cloudflare gratuit et sans compte (brew install cloudflared), lance le serveur derrière un chemin secret, puis affiche l'URL à coller dans Claude.

  • Le chemin secret sert de clé, car le connecteur ne transmet pas de jeton : ne partage pas l'URL.

  • Seul le domaine du tunnel est accepté, en plus de localhost.

  • L'URL change à chaque lancement et ne fonctionne que tant que le script tourne.

  • Avec des données réelles, ce tunnel les rend accessibles à quiconque a l'URL : à réserver au jeu fictif.

Données

  • data/exemple_data_room.json : data room fictive, écrite à la main. Elle contient 20 contrats d'une due diligence imaginaire sur le « Groupe Brenalis ».

    • On y retrouve les difficultés typiques d'une vraie data room : doublons (fichier téléversé deux fois, traduction de courtoisie), avenant qui reporte le terme d'un contrat, métadonnées manquantes ou contredites par le texte, droit étranger, noms proches désignant des entités différentes.

    • Les noms, les textes, les dates et les montants sont inventés. Les SIREN sont volontairement invalides au sens de la clé de Luhn : ils ne peuvent correspondre à aucune entreprise réelle.

  • Données réelles : à placer dans private/, exclu de git, puis DATAROOM_DATA=private/<fichier>.json. Sans cette variable, seul le jeu fictif est utilisé.

Rien n'est codé en dur pour une data room donnée : les entités, types de contrat et lois possibles sont tirés des données chargées. Les tests tournent sur le jeu fictif.

Architecture

flowchart LR
    subgraph S["Tool"]
        direction TB
        F["Pré-filtrage sur les métadonnées"] --> B["BM25 sur les articles (si query)"] --> G["Regroupement par contrat"]
    end
    Q["Question de l'avocat"] --> A["Agent LLM (Claude via MCP, ou Ollama)"]
    A -- "search_data_room(query, filters)" --> S
    A -- "get_contract(id)" --> S
    S -- "contrats + articles pertinents (JSON)" --> A
    A --> R["Réponse qui cite contrats et articles"]
    D[("Data room JSON")] -- "1 chunk par article, index BM25" --> S

1. Réception de la demande. L'agent reformule la question en paramètres structurés, validés par Pydantic. Les valeurs possibles (entités, types, lois) sont tirées de la data room chargée et proposées au LLM dans le schéma du tool ; une valeur inconnue est rejetée avec la liste des valeurs possibles. Les dates sont absolues : c'est l'agent qui convertit « fin 2026 ».

{
  "query": "texte libre, optionnel",
  "filters": {
    "entity_ids": ["groupe_brenalis"],
    "contract_types": ["bail commercial"],
    "governing_law": {"in": [], "not_in": ["droit français"]},
    "signature_date": {"after": "2023-01-01", "before": null},
    "end_date": {"after": null, "before": "2026-12-31"}
  },
  "top_k": 10
}

2. Indexation. Chaque contrat est découpé par article (ARTICLE n — TITRE). Le titre, les parties et le type du contrat sont indexés avec chaque article, pour qu'un article isolé reste compréhensible. Index BM25 en mémoire (quelques millisecondes à construire).

3. Recherche. Filtrage sur les métadonnées, puis deux cas :

  • sans querytous les contrats qui passent les filtres : une question « lesquels ? » exige une réponse exhaustive ;

  • avec query → classement BM25 des articles ; le score d'un contrat est celui de son meilleur article.

4. Sortie. Pour chaque contrat : les filtres satisfaits et les articles qui justifient le résultat, que l'agent cite. Un contrat dont le champ filtré est vide n'est jamais écarté en silence : il est listé dans excluded_unknown.

{
  "total_candidates": 2,
  "excluded_unknown": ["c08", "c17"],
  "results": [{
    "contract_id": "c04",
    "title": "Lettre de mission d'audit d'acquisition — Cabinet Morand / Groupe Brenalis",
    "end_date": "2026-11-30",
    "governing_law": "droit français",
    "matched_filters": ["parties: groupe_brenalis", "fin 2026-11-30 (<= 2026-12-31)"],
    "relevant_articles": [{
      "chunk_id": "c04-2",
      "heading": "ARTICLE 2 — DURÉE",
      "excerpt": "La mission commence le 1er décembre 2025 et s'achève au plus tard le 30 novembre 2026, date de remise du rapport définitif."
    }]
  }]
}

Choix et compromis

  • Pas un RAG classique. Un RAG qui renvoie les k passages les plus proches échoue sur ces questions : « lesquels ? » demande l'exhaustivité, « droit étranger » demande un filtre (« droit français » et « droit suisse » sont presque identiques pour un moteur de similarité). Le tool est donc hybride : requête structurée pour les listes, recherche plein texte pour le contenu des clauses.

  • L'agent interprète, le tool reste déterministe. Le LLM traduit le langage naturel (dates relatives, « droit étranger » = not_in: ["droit français"]). Le tool valide strictement ses entrées et renvoie les erreurs au modèle pour qu'il corrige son appel.

  • BM25 d'abord. Simple, explicable, sans dépendance. Les embeddings pourront être ajoutés dans search.py (fusion par rang) sans changer les formats d'entrée et de sortie.

  • LLM local possible. Une data room est confidentielle : un modèle local évite d'envoyer les contrats à un tiers. Pour un petit modèle, le schéma des tools est aplati (pas de $ref ni d'anyOf).

  • Aucune data room codée en dur. Les valeurs possibles des filtres viennent des données chargées : le même code sert n'importe quelle data room.

Résultats

Question

Réponse du tool

Signalés à part

Contrats Brenalis qui expirent avant fin 2026

c04, c09 (article DURÉE)

c08, c17 : pas de date de fin ; c12 (Brénalys Advisory) exclu

Contrats régis par un droit étranger

c05 (new-yorkais) et c20, sa traduction de courtoisie ; c16 (suisse)

c10 : loi non renseignée

« exclusivité territoriale »

c03, ARTICLE 4 — EXCLUSIVITÉ

« franchise »

c13, alors que ses métadonnées le typent « contrat de licence »

Trace complète de l'agent sur les questions types : docs/demo.md (régénérable avec python scripts/demo.py).

Limites connues

  • c09 est un faux positif pour Brenalis : l'avenant c18 reporte son terme au 30/06/2028. Le lien avenant → contrat n'est pas modélisé.

  • Pas de tool dédié aux doublons ({c11, c19} : la même convention téléversée deux fois ; {c05, c20} : un contrat et sa traduction de courtoisie). L'agent peut les repérer en demandant la liste complète et en comparant parties, types et dates, mais avec des milliers de contrats cette liste ne tiendrait pas dans son contexte : il faut un traitement dédié.

  • Qualité des données laissée de côté volontairement : normalisation minimale (dates, SIREN, noms d'entités), pas de détection des contradictions entre métadonnées et texte (c15 : fin au 31/03/2027 dans les métadonnées, au 30/09/2026 dans le texte).

  • Bruit sur les requêtes texte : aucun seuil de pertinence, et le préfixe titre/type fait remonter des contrats dont seul le titre correspond.

  • Identifiants d'entités tirés du nom normalisé. Deux écritures vraiment différentes d'une même société (sigle, faute de frappe) ne sont pas rapprochées. C'est voulu, pour éviter les fusions approximatives, mais à grande échelle il faudrait un tool de résolution des parties.

Prochaines étapes

Relier les avenants à leur contrat et ajouter un tool de doublons ; ajouter un seuil de pertinence et les embeddings ; mesurer chaque changement sur un jeu de questions annoté par des juristes.

Structure

├── data/exemple_data_room.json   jeu fictif
├── dataroom/
│   ├── config.py      configuration (variables d'environnement)
│   ├── models.py      schémas d'entrée et de sortie du tool
│   ├── loader.py      chargement et normalisation du JSON
│   ├── indexer.py     découpage par article, BM25
│   ├── search.py      filtres, classement, articles pertinents
│   ├── tools.py       définitions des tools et exécution
│   ├── agent.py       boucle agent avec Ollama, CLI
│   ├── api.py         API FastAPI
│   └── mcp_server.py  serveur MCP (stdio et HTTP)
├── scripts/           run_query.sh (tool seul), demo.py (agent sur des questions types), mcp_tunnel.sh (URL HTTPS pour Claude)
├── docs/demo.md       trace de la démo, sur le jeu fictif
├── tests/             un fichier par module, sur le jeu fictif ; l'agent est testé avec un faux LLM
├── .github/workflows/ CI : les tests sur Python 3.10 et 3.14
└── private/           non versionné : données réelles et notes locales

Méthode de travail

Développé avec Claude Code. Les choix d'architecture ont été discutés et arbitrés au fil de la session : mettre la qualité des données de côté, construire le format de la demande et la sortie avant le reste, BM25 avant les embeddings, LLM local. CLAUDE.md en garde la trace.

Available Tools

2 tools
get_contractB

Renvoie le texte d'un contrat, découpé par article, pour vérifier ou citer une clause précise.

ParametersJSON Schema
NameRequiredDescriptionDefault
chunk_idsNoLimiter à ces articles (ex. « c07-2 »). Vide = tout le contrat.
contract_idYesIdentifiant renvoyé par search_data_room, ex. « c07 ».

TDQS

B3.4/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full behavioral burden. It does disclose that output is chunked by article, which is genuinely useful, but says nothing about permissions, whether the call is safe/read-only, rate limits, or how a full-contract dump behaves for very long documents.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single sentence that front-loads the action and the output structure, then the purpose. No filler, no restatement of the tool name.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple two-parameter getter with full schema coverage and a named sibling, one sentence plus the schema is close to sufficient — the article-level return shape is stated. The main gap is the lack of any workflow note about obtaining contract_id first, and there is no output schema to fall back on.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so both parameters are already documented in the schema with format examples ("c07", "c07-2") and the empty-array default. The description adds no parameter-level detail, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb+resource: it returns contract text split by article. The clause-verification framing sharpens what the resource is for. It doesn't explicitly contrast itself with search_data_room, which is the natural sibling, but an agent can distinguish a full-text fetch from a search without much trouble.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

"pour vérifier ou citer une clause précise" implies the use case (deep-read a known contract for a specific article) but gives no explicit when-not or routing rule versus search_data_room. The dependency on a contract_id from search_data_room is only hinted at in the schema, not stated as a workflow step.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_data_roomA

Recherche dans la data room : filtres sur les métadonnées et/ou recherche plein texte dans les articles.

Sans query, renvoie TOUS les contrats qui passent les filtres (réponse exhaustive). Avec query, classe les contrats par pertinence de leurs articles et renvoie les top_k premiers. Chaque résultat contient les articles pertinents à citer dans la réponse.

ParametersJSON Schema
NameRequiredDescriptionDefault
queryNoTexte libre recherché dans les articles, ex. « résiliation anticipée », « exclusivité territoriale ». Laisser vide pour une question purement sur les métadonnées.
top_kNoNombre max de contrats renvoyés quand `query` est fournie.
filtersNo

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden, and it does disclose meaningful behavior: the exhaustive-vs-ranked switch on `query`, the top_k cap, and that each result carries citable articles. It omits read-only status, auth requirements, and pagination behavior, but the mode-dependent output semantics are well surfaced.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four tight sentences with the key behavioral distinction front-loaded, no repetition of the schema, and no filler. Every sentence adds a distinct fact.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a search tool with no annotations and no output schema, the description covers the essential invocation semantics (mode switching, top_k, citable articles). It stops short of describing result shape, pagination, or limits, which for an exhaustive-mode tool is a minor but real gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 67%, below the 80% baseline, so the description should compensate. It clarifies that top_k only applies when `query` is supplied, which is useful, but the schema already documents that dependency, and the nested filter fields receive no additional explanation from the description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ("Recherche dans la data room") plus the two operating modes, so the agent immediately knows this is a filtered/full-text search over contracts. It does not, however, differentiate itself from the sibling get_contract, which also retrieves contracts.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explains the two modes (no `query` = exhaustive list of everything passing filters; with `query` = ranked top_k), which implies how to invoke it for a given intent. But it never says when to prefer this over get_contract or when the exhaustive mode is appropriate versus the ranked one.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

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

  1. 2 tool updatesv0.1.0
    • First observedget_contract
    • First observedsearch_data_room

TDQS

A3.7/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have clearly distinct purposes: search_data_room locates contracts or articles via filters/full-text search, while get_contract retrieves the complete text of a specific contract. There is no overlap in functionality, so an agent can easily select the right tool.

Naming Consistency5/5

Both names follow a consistent verb_noun snake_case pattern: search_data_room and get_contract. The verbs (search, get) clearly indicate the action, and the nouns refer to the domain resources in a predictable way.

Tool Count3/5

With only two tools, the set is on the thin side for a data room server that might involve contract management, metadata browsing, and article-level access. While the two tools cover the core read operations, the count feels minimal and borderline for the apparent scope.

Completeness4/5

The tools cover the essential read lifecycle: search/filter contracts and retrieve full contract text with articles. Minor gaps exist, such as direct article-level retrieval or explicit metadata-only queries, but agents can work around these using the provided tools.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers