dataroom
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@dataroomWhich contracts with Groupe Brenalis expire before end of 2026?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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, /askOptionnel : 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 CodePour 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_roomavec 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, puisDATAROOM_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" --> S1. 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
query→ tous 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
$refni 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 localesMé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 toolsget_contractB
Renvoie le texte d'un contrat, découpé par article, pour vérifier ou citer une clause précise.
| Name | Required | Description | Default |
|---|---|---|---|
| chunk_ids | No | Limiter à ces articles (ex. « c07-2 »). Vide = tout le contrat. | |
| contract_id | Yes | Identifiant renvoyé par search_data_room, ex. « c07 ». |
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Texte libre recherché dans les articles, ex. « résiliation anticipée », « exclusivité territoriale ». Laisser vide pour une question purement sur les métadonnées. | |
| top_k | No | Nombre max de contrats renvoyés quand `query` est fournie. | |
| filters | No |
TDQS
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.
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.
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.
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.
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.
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.
2 tool updates
v0.1.0- First observed
get_contract - First observed
search_data_room
TDQS
Scored across 2 tools
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.
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.
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.
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
Related MCP Connectors
- ClmentOAuthcom.clment
Contract review that keeps your contracts: cited answers, Word redlines, key-date alerts.
Search U.S. case law, fetch opinions, and ask matter-aware legal questions over your documents.
Query Klaaro datasets, documents, and extracted records from any agent.
Live AI-native web search with citations. One tool for every MCP client. Flat per-request pricing.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables AI agents to query structured knowledge across enterprise domains (legal, HR, compliance) with metadata-driven filtering and TF-IDF ranking.4-
- AlicenseNot gradedqualityBmaintenanceEnables document-based Q&A with multi-modal RAG, hybrid retrieval, knowledge graph reasoning, and multi-agent orchestration via MCP tools.4MIT
- AlicenseBqualityAmaintenanceMCP search and evidence tool for AI agents. Rewrites queries, zooms into source domains, and returns sourced answers with metrics.13MIT
- FlicenseAqualityBmaintenanceEnables LLMs to ingest and analyze legal agreements, compute risk scores, and monitor non-compliant clauses through MCP tools like ingest, fetch_contracts, and run_analysis.4-