Files
linear-coding-agent/generations/library_rag/examples/mcp_client_claude.py
David Blanc Brioir d2f7165120 Add Library RAG project and cleanup root directory
- 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>
2025-12-30 11:57:12 +01:00

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())