File size: 5,623 Bytes
2e18bd3 dd812c4 420c9af 2e18bd3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | """Serveur MCP exposant les outils JeuxDeMots à n'importe quel client MCP
(Claude Desktop, Claude Code, Cursor, etc.).
Réutilise les mêmes fonctions Python que la couche LangChain — pas de
duplication. Chaque `StructuredTool` LangChain expose la fonction d'origine
via `.func`, qu'on enregistre ensuite comme outil MCP.
Lancement :
python -m jdm_agent.mcp.server # stdio (par défaut, pour Claude Desktop/Code)
jdm-mcp # même chose via le script installé
Configuration côté Claude Desktop/Code (`claude_desktop_config.json` ou
`~/.claude.json` selon l'outil) :
{
"mcpServers": {
"jdm": {
"command": "python",
"args": ["-m", "jdm_agent.mcp.server"]
}
}
}
"""
from __future__ import annotations
import argparse
import logging
from fastmcp import FastMCP
from jdm_agent.client import JDMClient
from jdm_agent.tools.jdm_tools import ALL_TOOLS, set_default_client
logger = logging.getLogger("jdm-mcp")
def build_server(client: JDMClient | None = None) -> FastMCP:
"""Construit l'instance FastMCP et enregistre tous les outils JDM."""
if client is not None:
set_default_client(client)
else:
# Force la création paresseuse pour pré-charger les types relations
# (un seul HTTP au démarrage).
set_default_client(JDMClient())
mcp = FastMCP(
name="jdm-agent",
instructions=(
"Outils JeuxDeMots — graphe lexico-sémantique du français (LIRMM/CNRS).\n\n"
"RÈGLE PRIORITAIRE : tout ce que tu énonces (relation, triplet, verdict, "
"gap) provient d'un appel d'outil JDM réel de ce tour. Pas de simulation, "
"pas de relations hallucinées. Si les outils ne sont pas disponibles dans "
"la session, dis-le et arrête.\n\n"
"CITATION : format `source | relation | target (w=...)`. Les noms reçus "
"sont déjà décodés — cite-les tels quels. `|w|` = intensité du consensus.\n\n"
"POLARITÉ : `polarity ∈ {affirmation, négation}`. Préface clairement "
"les négations (« JDM affirme que X N'EST PAS Y »), NE les mêle JAMAIS "
"aux affirmations.\n\n"
"POLYSÉMIE : pour les termes à plusieurs sens (avocat, souris, police, "
"chat, livre, sens, vol, glace…), liste d'abord les sens via l'outil "
"dédié puis requête sur le sens visé avec son `sense_id` raffiné.\n\n"
"CONTENANCE vs INFÉRENCE : « JDM contient-il X ? » → vérification "
"stricte (effort 0), si absent dis « pas dans JDM ». « X est-il vrai / "
"déductible ? » → autorise l'inférence (effort ≥ 1). Un résultat "
"inféré se présente comme déduction (« on peut déduire que… parce "
"que… »), JAMAIS comme contenu direct.\n\n"
"ANNOTATIONS : opt-in (perf) ; outil dédié pour récupérer "
"constitutif / contrastif / exception sur un triplet précis.\n\n"
"PRÉSENTATION : n'écris JAMAIS les noms d'outils internes — parle en "
"français (« je regarde les sens », « je cherche les synonymes »). "
"Les noms de relations JDM (r_isa, r_anto, r_has_part…) sont AUTORISÉS "
"— terminologie linguistique légitime.\n\n"
"PERSPECTIVES MULTIPLES (suggestion) : question ouverte sur un terme → "
"explorer plusieurs angles complémentaires (catégorisation / parties / "
"caractéristiques ; fonction / usages / actions subies ; …) enrichit "
"la réponse. Utilise ton jugement.\n\n"
"ENRICHISSEMENT : dès qu'on te demande de proposer / suggérer / "
"ajouter / enrichir des triplets dans JDM, ton TOUT PREMIER appel "
"est `enrichment_workflow()` — il renvoie le flux canonique à "
"suivre étape par étape (pré-fetch, désambiguïsation, proposition, "
"validation + consolidation, écriture soumission) et les règles "
"transversales. C'est la source de vérité du flux.\n\n"
"DÉTECTION DE GAPS SANS TERME : règle détaillée dans la docstring "
"de `detect_gaps` (rechargée à chaque appel d'outil)."
),
)
# Chaque LangChain @tool a un attribut .func = la fonction Python d'origine
# (avec ses annotations + docstring). FastMCP en infère le schéma.
for t in ALL_TOOLS:
fn = getattr(t, "func", None) or t # fallback si l'attribut change
mcp.tool(fn, name=t.name, description=t.description)
return mcp
def main() -> int:
parser = argparse.ArgumentParser(description="Serveur MCP JeuxDeMots.")
parser.add_argument(
"--transport",
choices=("stdio", "sse", "streamable-http"),
default="stdio",
help="Transport MCP (stdio = défaut pour clients desktop).",
)
parser.add_argument(
"--host", default="127.0.0.1",
help="Hôte pour les transports HTTP/SSE.",
)
parser.add_argument("--port", type=int, default=8765)
parser.add_argument("--log-level", default="WARNING")
args = parser.parse_args()
logging.basicConfig(level=args.log_level.upper(), format="[%(name)s] %(message)s")
server = build_server()
if args.transport == "stdio":
server.run() # transport par défaut stdio
else:
server.run(transport=args.transport, host=args.host, port=args.port)
return 0
if __name__ == "__main__":
raise SystemExit(main())
|