Skip to main content
Glama
pylegifrance

MCP Server Legifrance

by pylegifrance
server.py15.9 kB
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Created on Sat Apr 19 16:27:37 2025 @author: legalchain """ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Serveur MCP pour l'accès à Legifrance ------------------------------------- Facilite l'accès aux ressources juridiques françaises via l'API Legifrance en utilisant le protocole Model Context (MCP). Auteur: Raphaël d'Assignies (dassignies.law) Date de création: Avril 2025 """ import os import json import logging import asyncio from typing import Any, Dict, Optional, List, Sequence from functools import wraps from datetime import datetime import requests from dotenv import load_dotenv from mcp.server import Server from mcp.types import Tool, TextContent # Configuration du logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger("legifrance_mcp") # Chargement des variables d'environnement load_dotenv() # Constantes et configuration API_KEY = os.getenv('DASSIGNIES_API_KEY') API_URL = os.getenv('DASSIGNIES_API_URL') if not API_KEY or not API_URL: raise ValueError("Les variables d'environnement LAB_DASSIGNIES_API_KEY et LEGAL_API_URL doivent être définies") HEADERS = { "accept": "*/*", "Content-Type": "application/json" } # Création du serveur MCP server = Server("legifrance") # Utilitaires def clean_dict(d: dict) -> dict: """ Supprime les clés dont la valeur est None pour optimiser les requêtes API. Args: d (dict): Dictionnaire à nettoyer Returns: dict: Dictionnaire sans les valeurs None """ return {k: v for k, v in d.items() if v is not None} def rate_limit(calls: int, period: float): """ Décorateur pour limiter le nombre d'appels API dans une période donnée. Args: calls (int): Nombre maximum d'appels autorisés period (float): Période en secondes """ def decorator(func): last_reset = datetime.now() calls_made = 0 @wraps(func) async def wrapper(*args, **kwargs): nonlocal last_reset, calls_made now = datetime.now() # Réinitialisation du compteur si la période est écoulée if (now - last_reset).total_seconds() > period: calls_made = 0 last_reset = now # Si la limite est atteinte, attendre la fin de la période if calls_made >= calls: wait_time = period - (now - last_reset).total_seconds() if wait_time > 0: await asyncio.sleep(wait_time) last_reset = datetime.now() calls_made = 0 calls_made += 1 return await func(*args, **kwargs) return wrapper return decorator async def make_api_request(endpoint: str, data: Dict) -> Dict: """ Fonction générique pour effectuer des requêtes API avec gestion d'erreurs. Args: endpoint (str): Point de terminaison de l'API (sans le domaine) data (Dict): Données à envoyer dans la requête Returns: Dict: Résultat de la requête ou message d'erreur """ try: url = f"{API_URL}{endpoint}" clean_data = clean_dict(data) logger.info(f"Envoi de requête à {endpoint} avec les données: {json.dumps(clean_data)}") res = requests.post( url, headers=HEADERS, params={"api_key": API_KEY}, json=clean_data, timeout=30 # Timeout explicite pour éviter les blocages ) content_type = res.headers.get("Content-Type", "") response_body = res.text if res.ok: try: result = res.json() except requests.exceptions.JSONDecodeError: result = response_body # fallback sur le texte brut if isinstance(result, str): result += "\n\n🔗 Mentionne systématiquement et impérativement le lien officiel dans ta réponse pour pouvoir y accéder." return result if res.status_code == 422 or res.status_code == 404: if "text/plain" in content_type: return {"error": response_body} if "application/json" in content_type: try: return {"error": res.json()} except requests.exceptions.JSONDecodeError: return {"error": response_body} return {"error": f"Erreur {res.status_code} : {response_body}"} except requests.exceptions.RequestException as e: logger.error("Erreur de connexion à l'API", exc_info=True) return {"error": f"Erreur de connexion : {e}"} except Exception as e: # Uniquement pour les erreurs de connexion ou autres problèmes graves logger.error(f"Erreur de connexion: {str(e)}") return {"error": f"Erreur de connexion: {str(e)}"} @server.list_tools() async def list_tools() -> List[Tool]: """Liste tous les outils disponibles dans ce serveur MCP.""" return [ Tool( name="rechercher_dans_texte_legal", description=""" Recherche un article dans un texte légal (loi, ordonnance, décret, arrêté) par le numéro du texte et le numéro de l'article. On peut également rechercher des mots clés ("mots clés" séparés par des espaces) dans une loi précise (n° de loi) Paramètres: - text_id: Le numéro du texte (format AAAA-NUMERO) - search: Mots-clés de recherche ou numéro d'article - champ: Champ de recherche ("ALL", "TITLE", "TABLE", "NUM_ARTICLE", "ARTICLE") - type_recherche: Type de recherche ("TOUS_LES_MOTS_DANS_UN_CHAMP", "EXPRESSION_EXACTE", "AU_MOINS_UN_MOT") - page_size: Nombre de résultats (max 100) Exemples: - Pour l'article 7 de la loi 78-17: {text_id="78-17", search="7", champ="NUM_ARTICLE"} - On cherche les conditions de validité de la signature électronique : {search="signature électronique validité conditions"} """, inputSchema={ "type": "object", "properties": { "search": {"type": "string"}, "text_id": {"type": "string"}, "champ": {"type": "string", "enum": ["ALL", "TITLE", "TABLE", "NUM_ARTICLE", "ARTICLE"]}, "type_recherche": {"type": "string", "enum": ["TOUS_LES_MOTS_DANS_UN_CHAMP", "EXPRESSION_EXACTE", "AU_MOINS_UN_MOT"]}, "page_size": {"type": "integer", "maximum": 100} } } ), Tool( name="rechercher_code", description=""" Recherche des articles juridiques dans les codes de loi français. Paramètres: - search: Termes de recherche (ex: "contrat de travail", "légitime défense") - code_name: Nom du code juridique (ex: "Code civil", "Code du travail") - champ: Champ de recherche ("ALL", "TITLE", "TABLE", "NUM_ARTICLE", "ARTICLE") - sort: Tri des résultats ("PERTINENCE", "DATE_ASC", "DATE_DESC") - type_recherche: Type de recherche - page_size: Nombre de résultats (max 100) - fetch_all: Récupérer tous les résultats Exemples: - Pour le PACS dans le Code civil: {search="pacte civil de solidarité", code_name="Code civil"} """, inputSchema={ "type": "object", "properties": { "search": {"type": "string"}, "code_name": {"type": "string"}, "champ": {"type": "string"}, "sort": {"type": "string", "enum": ["PERTINENCE", "DATE_ASC", "DATE_DESC"]}, "type_recherche": {"type": "string"}, "page_size": {"type": "integer", "maximum": 100}, "fetch_all": {"type": "boolean"} }, "required": ["search", "code_name"] } ), Tool( name="rechercher_jurisprudence_judiciaire", description=""" Recherche des jurisprudences judiciaires dans la base JURI de Legifrance. Paramètres: - search: Termes ou numéros d'affaires à rechercher - publication_bulletin: Si publiée au bulletin ['T'] sinon ['F'] - sort: Tri des résultats ("PERTINENCE", "DATE_DESC", "DATE_ASC") - champ: Champ de recherche ("ALL", "TITLE", "ABSTRATS", "TEXTE", "RESUMES", "NUM_AFFAIRE") - type_recherche: Type de recherche - page_size: Nombre de résultats (max. 100) - fetch_all: Récupérer tous les résultats - juri_keys: Mots-clés pour extraire des champs comme 'titre'. Par défaut, le titre, le texte et les résumés sont extraits - juridiction_judiciaire: Liste des juridictions à inclure parmi ['Cour de cassation', 'Juridictions d'appel', ] Exemples : - Obtenir un panorama de la jurisprudence par mots clés : search = "tierce opposition salarié société liquidation", page_size=100, juri_keys=['titre'] - Obtenir toutes les jurisprudences sur la signature électronique : search = "signature électronique", fetch_all=True, juri_keys=['titre', 'sommaire'] - Obtenir les 20 dernières jurisprudences sur la signature électronique des juridictions d'appel search = "signature électronique", page_size, sort='DATE_DESC', juridiction_judiciaire=['Juridictions d'appel']] """, inputSchema={ "type": "object", "properties": { "search": {"type": "string"}, "publication_bulletin": {"type": "array", "items": {"type": "string", "enum": ["T", "F"]}}, "sort": {"type": "string", "enum": ["PERTINENCE", "DATE_DESC", "DATE_ASC"]}, "champ": {"type": "string"}, "type_recherche": {"type": "string"}, "page_size": {"type": "integer", "maximum": 100}, "fetch_all": {"type": "boolean"}, "juri_keys": {"type": "array", "items": {"type": "string"}}, "juridiction_judiciaire": {"type": "array", "items": {"type": "string"}} }, "required": ["search"] } ) ] @server.call_tool() @rate_limit(calls=5, period=1.0) # Limite à 5 appels par seconde async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]: """ Gère les appels aux outils juridiques. Args: name (str): Nom de l'outil à appeler arguments (Any): Arguments à passer à l'outil Returns: Sequence[TextContent]: Résultat de l'appel """ try: logger.info(f"Appel de l'outil: {name} avec arguments: {json.dumps(arguments)}") if name == "rechercher_dans_texte_legal": result = await make_api_request("loda", arguments) elif name == "rechercher_code": result = await make_api_request("code", arguments) elif name == "rechercher_jurisprudence_judiciaire": result = await make_api_request("juri", arguments) else: raise ValueError(f"Outil inconnu: {name}") # Détection et traitement des erreurs if isinstance(result, dict) and "error" in result: return [TextContent(type="text", text=result["error"])] # Formatage du résultat if isinstance(result, str): return [TextContent(type="text", text=result)] else: return [TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))] except Exception as e: error_message = f"Erreur lors de l'exécution de {name}: {str(e)}" logger.error(error_message) return [TextContent(type="text", text=error_message)] @server.list_prompts() async def list_prompts(): """Liste tous les prompts disponibles dans ce serveur MCP.""" from mcp.types import Prompt, PromptArgument return [ Prompt( name="agent_juridique_expert", description="Utilise un agent juridique expert pour répondre à des questions de droit français", arguments=[ PromptArgument( name="question", description="La question juridique", required=True ) ] ) ] @server.get_prompt() async def get_prompt(prompt_name: str, inputs: dict) -> Dict: """ Retourne un prompt prédéfini pour une utilisation spécifique. Args: prompt_name (str): Nom du prompt à récupérer inputs (dict): Entrées pour le prompt Returns: Dict: Structure du prompt """ if prompt_name == "agent_juridique_expert": question = inputs.get("question", "") return { "messages": [ { "role": "assistant", "content": [ { "type": "text", "text": ( "Tu es un agent juridique expert qui cite toujours ses sources dans le corps du texte.\n" "Lorsque tu effectues une recherche et que des références sont citées (article d'un code, numéro de décision de justice), " "tu dois systématiquement utiliser les outils à ta disposition pour aller chercher leur contenu et l'analyser. " "Tu peux utiliser tous les outils disponibles pour rechercher des informations dans les textes de loi français ou la jurisprudence.\n" "Tu dois :\n" "- Expliquer ton raisonnement étape par étape\n" "- Utiliser les outils pertinents\n" "- Fournir une synthèse claire, sourcée, avec des liens vers les articles. " "Tu dois impérativement récupérer les liens officiels et les citer." ) } ] }, { "role": "user", "content": [ { "type": "text", "text": f"Voici ma question juridique : {question}" } ] } ] } else: raise ValueError(f"Prompt inconnu: {prompt_name}") async def main(): """Point d'entrée principal du serveur MCP.""" import mcp.server.stdio try: logger.info("Démarrage du serveur MCP Legifrance...") async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, server.create_initialization_options(), ) except Exception as e: logger.error(f"Erreur fatale lors de l'exécution du serveur: {str(e)}") raise if __name__ == "__main__": asyncio.run(main())

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/pylegifrance/mcp-server-legifrance'

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