- Add complete Library RAG application (Flask + MCP server) - PDF processing pipeline with OCR and LLM extraction - Weaviate vector database integration (BGE-M3 embeddings) - Flask web interface with search and document management - MCP server for Claude Desktop integration - Comprehensive test suite (134 tests) - Clean up root directory - Remove obsolete documentation files - Remove backup and temporary files - Update autonomous agent configuration - Update prompts - Enhance initializer bis prompt with better instructions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
360 lines
11 KiB
Python
360 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
MCP Client pour Library RAG avec Claude (Anthropic).
|
|
|
|
Implémentation d'un client MCP qui permet à Claude d'utiliser
|
|
les outils de Library RAG via tool calling.
|
|
|
|
Usage:
|
|
python mcp_client_claude.py
|
|
|
|
Requirements:
|
|
pip install anthropic python-dotenv
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
# Charger les variables d'environnement depuis .env
|
|
try:
|
|
from dotenv import load_dotenv
|
|
# Charger depuis le .env du projet parent
|
|
env_path = Path(__file__).parent.parent / ".env"
|
|
load_dotenv(env_path)
|
|
print(f"[ENV] Loaded environment from {env_path}")
|
|
except ImportError:
|
|
print("[ENV] python-dotenv not installed, using system environment variables")
|
|
print(" Install with: pip install python-dotenv")
|
|
|
|
|
|
@dataclass
|
|
class ToolDefinition:
|
|
"""Définition d'un outil MCP."""
|
|
|
|
name: str
|
|
description: str
|
|
input_schema: dict[str, Any]
|
|
|
|
|
|
class MCPClient:
|
|
"""Client pour communiquer avec le MCP server de Library RAG."""
|
|
|
|
def __init__(self, server_path: str, env: dict[str, str] | None = None):
|
|
"""
|
|
Args:
|
|
server_path: Chemin vers mcp_server.py
|
|
env: Variables d'environnement additionnelles
|
|
"""
|
|
self.server_path = server_path
|
|
self.env = env or {}
|
|
self.process = None
|
|
self.request_id = 0
|
|
|
|
async def start(self) -> None:
|
|
"""Démarrer le MCP server subprocess."""
|
|
print(f"[MCP] Starting server: {self.server_path}")
|
|
|
|
# Préparer l'environnement
|
|
full_env = {**os.environ, **self.env}
|
|
|
|
# Démarrer le subprocess
|
|
self.process = await asyncio.create_subprocess_exec(
|
|
sys.executable,
|
|
self.server_path,
|
|
stdin=asyncio.subprocess.PIPE,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
env=full_env,
|
|
)
|
|
|
|
# Phase 1: Initialize
|
|
init_result = await self._send_request(
|
|
"initialize",
|
|
{
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {"tools": {}},
|
|
"clientInfo": {"name": "library-rag-client-claude", "version": "1.0.0"},
|
|
},
|
|
)
|
|
|
|
print(f"[MCP] Server initialized: {init_result.get('serverInfo', {}).get('name')}")
|
|
|
|
# Phase 2: Initialized notification
|
|
await self._send_notification("notifications/initialized", {})
|
|
|
|
print("[MCP] Client ready")
|
|
|
|
async def _send_request(self, method: str, params: dict) -> dict:
|
|
"""Envoyer une requête JSON-RPC et attendre la réponse."""
|
|
self.request_id += 1
|
|
request = {
|
|
"jsonrpc": "2.0",
|
|
"id": self.request_id,
|
|
"method": method,
|
|
"params": params,
|
|
}
|
|
|
|
# Envoyer
|
|
request_json = json.dumps(request) + "\n"
|
|
self.process.stdin.write(request_json.encode())
|
|
await self.process.stdin.drain()
|
|
|
|
# Recevoir
|
|
response_line = await self.process.stdout.readline()
|
|
if not response_line:
|
|
raise RuntimeError("MCP server closed connection")
|
|
|
|
response = json.loads(response_line.decode())
|
|
|
|
# Vérifier erreurs
|
|
if "error" in response:
|
|
raise RuntimeError(f"MCP error: {response['error']}")
|
|
|
|
return response.get("result", {})
|
|
|
|
async def _send_notification(self, method: str, params: dict) -> None:
|
|
"""Envoyer une notification (pas de réponse)."""
|
|
notification = {"jsonrpc": "2.0", "method": method, "params": params}
|
|
|
|
notification_json = json.dumps(notification) + "\n"
|
|
self.process.stdin.write(notification_json.encode())
|
|
await self.process.stdin.drain()
|
|
|
|
async def list_tools(self) -> list[ToolDefinition]:
|
|
"""Obtenir la liste des outils disponibles."""
|
|
result = await self._send_request("tools/list", {})
|
|
tools = result.get("tools", [])
|
|
|
|
tool_defs = [
|
|
ToolDefinition(
|
|
name=tool["name"],
|
|
description=tool["description"],
|
|
input_schema=tool["inputSchema"],
|
|
)
|
|
for tool in tools
|
|
]
|
|
|
|
print(f"[MCP] Found {len(tool_defs)} tools")
|
|
return tool_defs
|
|
|
|
async def call_tool(self, tool_name: str, arguments: dict) -> Any:
|
|
"""Appeler un outil MCP."""
|
|
print(f"[MCP] Calling tool: {tool_name}")
|
|
print(f" Arguments: {json.dumps(arguments, indent=2)[:200]}...")
|
|
|
|
result = await self._send_request(
|
|
"tools/call", {"name": tool_name, "arguments": arguments}
|
|
)
|
|
|
|
# Extraire le contenu
|
|
content = result.get("content", [])
|
|
if content and content[0].get("type") == "text":
|
|
text_content = content[0]["text"]
|
|
try:
|
|
return json.loads(text_content)
|
|
except json.JSONDecodeError:
|
|
return text_content
|
|
|
|
return result
|
|
|
|
async def stop(self) -> None:
|
|
"""Arrêter le MCP server."""
|
|
if self.process:
|
|
print("[MCP] Stopping server...")
|
|
self.process.terminate()
|
|
await self.process.wait()
|
|
print("[MCP] Server stopped")
|
|
|
|
|
|
class ClaudeWithMCP:
|
|
"""Claude avec capacité d'utiliser les outils MCP."""
|
|
|
|
def __init__(self, mcp_client: MCPClient, anthropic_api_key: str):
|
|
"""
|
|
Args:
|
|
mcp_client: Client MCP initialisé
|
|
anthropic_api_key: Clé API Anthropic
|
|
"""
|
|
self.mcp_client = mcp_client
|
|
self.anthropic_api_key = anthropic_api_key
|
|
self.tools = None
|
|
self.messages = []
|
|
|
|
# Import Claude
|
|
try:
|
|
from anthropic import Anthropic
|
|
|
|
self.client = Anthropic(api_key=anthropic_api_key)
|
|
except ImportError:
|
|
raise ImportError("Install anthropic: pip install anthropic")
|
|
|
|
async def initialize(self) -> None:
|
|
"""Charger les outils MCP et les convertir pour Claude."""
|
|
mcp_tools = await self.mcp_client.list_tools()
|
|
|
|
# Convertir au format Claude (identique au format MCP)
|
|
self.tools = [
|
|
{
|
|
"name": tool.name,
|
|
"description": tool.description,
|
|
"input_schema": tool.input_schema,
|
|
}
|
|
for tool in mcp_tools
|
|
]
|
|
|
|
print(f"[Claude] Loaded {len(self.tools)} tools")
|
|
|
|
async def chat(self, user_message: str, max_iterations: int = 10) -> str:
|
|
"""
|
|
Converser avec Claude qui peut utiliser les outils MCP.
|
|
|
|
Args:
|
|
user_message: Message de l'utilisateur
|
|
max_iterations: Limite de tool calls
|
|
|
|
Returns:
|
|
Réponse finale de Claude
|
|
"""
|
|
print(f"\n[USER] {user_message}\n")
|
|
|
|
self.messages.append({"role": "user", "content": user_message})
|
|
|
|
for iteration in range(max_iterations):
|
|
print(f"[Claude] Iteration {iteration + 1}/{max_iterations}")
|
|
|
|
# Appel Claude avec tools
|
|
response = self.client.messages.create(
|
|
model="claude-sonnet-4-5-20250929", # Claude Sonnet 4.5
|
|
max_tokens=4096,
|
|
messages=self.messages,
|
|
tools=self.tools,
|
|
)
|
|
|
|
# Ajouter la réponse de Claude
|
|
assistant_message = {
|
|
"role": "assistant",
|
|
"content": response.content,
|
|
}
|
|
self.messages.append(assistant_message)
|
|
|
|
# Vérifier si Claude veut utiliser des outils
|
|
tool_uses = [block for block in response.content if block.type == "tool_use"]
|
|
|
|
# Si pas de tool use → réponse finale
|
|
if not tool_uses:
|
|
# Extraire le texte de la réponse
|
|
text_blocks = [block for block in response.content if block.type == "text"]
|
|
if text_blocks:
|
|
print(f"[Claude] Final response")
|
|
return text_blocks[0].text
|
|
return ""
|
|
|
|
# Exécuter les tool uses
|
|
print(f"[Claude] Tool uses: {len(tool_uses)}")
|
|
|
|
tool_results = []
|
|
|
|
for tool_use in tool_uses:
|
|
tool_name = tool_use.name
|
|
arguments = tool_use.input
|
|
|
|
# Appeler via MCP
|
|
try:
|
|
result = await self.mcp_client.call_tool(tool_name, arguments)
|
|
result_str = json.dumps(result) if isinstance(result, dict) else str(result)
|
|
print(f"[MCP] Result: {result_str[:200]}...")
|
|
|
|
tool_results.append({
|
|
"type": "tool_result",
|
|
"tool_use_id": tool_use.id,
|
|
"content": result_str,
|
|
})
|
|
|
|
except Exception as e:
|
|
print(f"[MCP] Error: {e}")
|
|
tool_results.append({
|
|
"type": "tool_result",
|
|
"tool_use_id": tool_use.id,
|
|
"content": json.dumps({"error": str(e)}),
|
|
"is_error": True,
|
|
})
|
|
|
|
# Ajouter les résultats des outils
|
|
self.messages.append({
|
|
"role": "user",
|
|
"content": tool_results,
|
|
})
|
|
|
|
return "Max iterations atteintes"
|
|
|
|
|
|
async def main():
|
|
"""Exemple d'utilisation du client MCP avec Claude."""
|
|
|
|
# Configuration
|
|
library_rag_path = Path(__file__).parent.parent
|
|
server_path = library_rag_path / "mcp_server.py"
|
|
|
|
anthropic_api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
if not anthropic_api_key:
|
|
print("ERROR: ANTHROPIC_API_KEY not found in .env file")
|
|
print("Please add to .env: ANTHROPIC_API_KEY=your_key")
|
|
return
|
|
|
|
mistral_api_key = os.getenv("MISTRAL_API_KEY")
|
|
if not mistral_api_key:
|
|
print("ERROR: MISTRAL_API_KEY not found in .env file")
|
|
print("The MCP server needs Mistral API for OCR functionality")
|
|
return
|
|
|
|
# 1. Créer et démarrer le client MCP
|
|
mcp_client = MCPClient(
|
|
server_path=str(server_path),
|
|
env={
|
|
"MISTRAL_API_KEY": mistral_api_key or "",
|
|
},
|
|
)
|
|
|
|
try:
|
|
await mcp_client.start()
|
|
|
|
# 2. Créer l'agent Claude
|
|
agent = ClaudeWithMCP(mcp_client, anthropic_api_key)
|
|
await agent.initialize()
|
|
|
|
# 3. Exemples de conversations
|
|
print("\n" + "=" * 80)
|
|
print("EXAMPLE 1: Search in Peirce")
|
|
print("=" * 80)
|
|
|
|
response = await agent.chat(
|
|
"What did Charles Sanders Peirce say about the philosophical debate "
|
|
"between nominalism and realism? Search the database and provide "
|
|
"a detailed summary with specific quotes."
|
|
)
|
|
|
|
print(f"\n[CLAUDE]\n{response}\n")
|
|
|
|
print("\n" + "=" * 80)
|
|
print("EXAMPLE 2: Explore database")
|
|
print("=" * 80)
|
|
|
|
response = await agent.chat(
|
|
"What documents are available in the database? "
|
|
"Give me an overview of the authors and topics covered."
|
|
)
|
|
|
|
print(f"\n[CLAUDE]\n{response}\n")
|
|
|
|
finally:
|
|
await mcp_client.stop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|