First commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/.idea
|
||||||
99
README.md
Normal file
99
README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# TP : création d'un serveur d'outils avec MCP pour un Agent IA
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Ouvrir un terminal dans le dossier racine du projet :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry install # installer les dépendances nécessaires
|
||||||
|
```
|
||||||
|
|
||||||
|
Attention, si derrière un proxy, vous devez ajouter en haut du fichier `run_agent.py` les lignes suivantes :
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
os.environ["NO_PROXY"] = "127.0.0.1,localhost"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objectif du TP
|
||||||
|
|
||||||
|
L'objectif de ce travail pratique est de développer un petit serveur d'outils en utilisant le protocole **MCP (Model Context Protocol)**.
|
||||||
|
Ce serveur exposera des fonctionnalités Python spécifiques (nos "outils") que des agents d'intelligence artificielle, comme ceux basés sur de grands modèles de langage (LLM), pourront découvrir et utiliser pour accomplir des tâches.
|
||||||
|
|
||||||
|
Dans ce scénario, nous allons créer un serveur qui donne accès à une base de données de films. Un agent LangChain, dont le code vous est entièrement fourni (`run_agent.py`), se connectera à votre serveur pour récupérer des informations sur les films et répondre aux questions d'un utilisateur.
|
||||||
|
|
||||||
|
**Votre mission est de compléter le squelette du fichier `mcp_tool_server.py` pour le rendre fonctionnel.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Contexte : qu'est-ce qu'un serveur MCP ?
|
||||||
|
|
||||||
|
Avant de commencer à coder, il est essentiel de comprendre le rôle d'un serveur MCP, qui est au centre de l'IA Agentique. Prenez un moment pour effectuer des recherches sur les questions suivantes :
|
||||||
|
|
||||||
|
* **Qu'est-ce que le protocole MCP (Model Context Protocol) ?**
|
||||||
|
* À quel besoin répond-il dans une architecture basée sur des agents IA ?
|
||||||
|
* Quelle est la différence entre exposer une fonction via une API REST classique et via un serveur d'outils ?
|
||||||
|
|
||||||
|
Les diagrammes suivants illustrent la différence fondamentale d'approche :
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "Architecture API REST classique"
|
||||||
|
Client[Application Client] -- "Requête HTTP GET /films" --> APIRest["Serveur API REST"]
|
||||||
|
APIRest -- "Logique métier" --> Endpoint1["Endpoint /films"]
|
||||||
|
APIRest -- "Logique métier" --> Endpoint2["Endpoint /acteur"]
|
||||||
|
Client -- "Requête HTTP POST /acteur" --> APIRest
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "Architecture Agent MCP"
|
||||||
|
Agent["Agent IA / LLM"] -- "1. Découverte: 'Quels outils as-tu?'" --> MCPServeur["Serveur d'outils MCP"]
|
||||||
|
MCPServeur -- "Expose 'get_film_details'" --> Outil1[Fonction Python: get_film_details]
|
||||||
|
MCPServeur -- "Expose 'search_by_actor'" --> Outil2[Fonction Python: search_by_actor]
|
||||||
|
Agent -- "2. Appel: 'get_film_details NomDuFilm'" --> MCPServeur
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette recherche vous aidera à mieux saisir l'importance de chaque ligne de code que vous écrirez.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Fichiers fournis
|
||||||
|
|
||||||
|
Vous disposez des fichiers suivants :
|
||||||
|
|
||||||
|
1. **`mcp_tool_server.py` (À COMPLÉTER)** : Le squelette du serveur que vous devez développer.
|
||||||
|
2. **`run_agent.py` (COMPLET)** : Le client qui exécute l'agent LangChain. Vous n'avez pas besoin de le modifier, mais n'hésitez pas à l'étudier pour comprendre comment il découvre et appelle les outils de votre serveur.
|
||||||
|
3. Un répertoire `resources` contenant `servers.json`, utilisé par `run_agent.py` pour connaître l'adresse de votre serveur.
|
||||||
|
|
||||||
|
Voici comment ces éléments interagissent dans ce TP :
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
Utilisateur -- "Pose une question" --> Agent["Agent LangChain (run_agent.py)"]
|
||||||
|
|
||||||
|
Agent -- "Lit la config" --> Config["servers.json"]
|
||||||
|
Agent -- "Se connecte et découvre les outils (MCP)" --> Serveur["Votre Serveur (mcp_tool_server.py)"]
|
||||||
|
|
||||||
|
Serveur -- "Implémente les outils" --> LogiqueMetier["Logique des outils (ex: recherche film)"]
|
||||||
|
LogiqueMetier -- "Accède" --> DB["Base de données Films (simulée)"]
|
||||||
|
|
||||||
|
Agent -- "Appelle l'outil nécessaire" --> Serveur
|
||||||
|
Serveur -- "Retourne le résultat" --> Agent
|
||||||
|
Agent -- "Formule la réponse" --> Utilisateur
|
||||||
|
```
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## 3\. Travail à réaliser : Compléter le serveur d'outils
|
||||||
|
|
||||||
|
Ouvrez le fichier `mcp_tool_server.py` et suivez les étapes ci-dessous pour le compléter. Les sections à modifier sont indiquées par des commentaires `# TODO`.
|
||||||
|
|
||||||
|
## 4. Facultatif : aller plus loin
|
||||||
|
|
||||||
|
Une fois que vous avez un serveur fonctionnel, vous pouvez envisager d'ajouter des fonctionnalités supplémentaires :
|
||||||
|
* Rendre l'agent accessible depuis une API REST (ou au choix GraphQL), permettant à un utilisateur final de la Filmothèque (via un ChatBot par exemple) de poser des questions sur les films via des requêtes HTTP.
|
||||||
|
* Ajouter des outils supplémentaires, comme la recherche de films par titre, la réservation de billets de cinéma, l'achat du film sur Amazon directement par l'Agent IA, etc. Donnez libre cours à votre imagination !
|
||||||
|
|
||||||
3465
poetry.lock
generated
Normal file
3465
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
pyproject.toml
Normal file
31
pyproject.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[project]
|
||||||
|
name = "fast-mcp"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = [
|
||||||
|
{name = "Your Name",email = "you@example.com"}
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
]
|
||||||
|
keywords = ["mcp", "ia", "llm"]
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.13"
|
||||||
|
langchain = "^0.3.27"
|
||||||
|
langchain-openai = "^0.3.35"
|
||||||
|
mcp-use = "^1.3.10"
|
||||||
|
fastapi = "^0.116.1"
|
||||||
|
uvicorn = { version = "^0.35.0", extras = [ "standard" ] }
|
||||||
|
gunicorn = "^23.0.0"
|
||||||
|
pydantic = {extras = ["email"], version = "^2.11.7"}
|
||||||
|
strawberry-graphql = "^0.281.0"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
8
resources/servers.json
Normal file
8
resources/servers.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"movies-mcp-server": {
|
||||||
|
"type": "streamable_http",
|
||||||
|
"url": "http://localhost:12345/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/mcp_tool_server.py
Normal file
43
src/mcp_tool_server.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import asyncio
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
# --- 1. La base de données locale du serveur ---
|
||||||
|
# C'est la seule source de vérité pour les données des films.
|
||||||
|
FILM_DATABASE = {
|
||||||
|
1: {
|
||||||
|
"title": "Matrix",
|
||||||
|
"synopsis": """
|
||||||
|
Programmeur anonyme dans un service administratif le jour, Thomas Anderson devient Neo la nuit venue.
|
||||||
|
Sous ce pseudonyme, il est l'un des pirates les plus recherchés du cyber-espace. A cheval entre deux mondes,
|
||||||
|
Neo est assailli par d'étranges songes et des messages cryptés provenant d'un certain Morpheus.
|
||||||
|
Celui-ci l'exhorte à aller au-delà des apparences et à trouver la réponse à la question qui hante
|
||||||
|
constamment ses pensées : qu'est-ce que la Matrice ?
|
||||||
|
"""
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
"title": "Inception",
|
||||||
|
"synopsis": """
|
||||||
|
Dom Cobb est un voleur expérimenté – le meilleur qui soit dans l'art périlleux de l'extraction :
|
||||||
|
sa spécialité consiste à s'approprier les secrets les plus précieux d'un individu, enfouis au plus
|
||||||
|
profond de son subconscient, pendant qu'il rêve et que son esprit est particulièrement vulnérable.
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 2. Création du serveur MCP ---
|
||||||
|
async def main():
|
||||||
|
mcp = FastMCP("movies-mcp-server", json_response=True, port=12345)
|
||||||
|
|
||||||
|
# --- 3. Définition de l'unique outil ---
|
||||||
|
# Cet outil ne fait aucune analyse, il retourne juste le synopsis d'un film, donné son ID.
|
||||||
|
# TODO : implémentez l'outil mcp_get_film_synopsis
|
||||||
|
# ... votre code ici ...
|
||||||
|
|
||||||
|
# --- 4. Démarrage du serveur ---
|
||||||
|
print("Serveur de données MCP démarré sur le port 12345...")
|
||||||
|
print("Ce terminal est maintenant dédié au serveur. Laissez-le tourner.")
|
||||||
|
# --- TODO : démarrez le serveur ---
|
||||||
|
# ... votre code ici ...
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
112
src/run_agent.py
Normal file
112
src/run_agent.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import List, Dict, Any, Type
|
||||||
|
|
||||||
|
from langchain.globals import set_debug
|
||||||
|
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||||
|
from langchain_core.tools import StructuredTool
|
||||||
|
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
from mcp_use import MCPClient
|
||||||
|
from pydantic import BaseModel, Field, create_model
|
||||||
|
|
||||||
|
set_debug(True)
|
||||||
|
|
||||||
|
# --- Configuration pour LM Studio Server ---
|
||||||
|
LLM_CHAT_SERVER_BASE_URL = "http://127.0.0.1:1234/v1"
|
||||||
|
LLM_CHAT_MODEL = "meta-llama-3.1-8b-instruct"
|
||||||
|
LLM_CHAT_TEMPERATURE = 0.3
|
||||||
|
LLM_CHAT_API_KEY = "not-needed"
|
||||||
|
|
||||||
|
agent_executor: AgentExecutor | None = None
|
||||||
|
mcp_client: MCPClient | None = None
|
||||||
|
|
||||||
|
async def build_agent() -> AgentExecutor:
|
||||||
|
print("--- 1. Prêt à analyser un film via son ID ---")
|
||||||
|
|
||||||
|
print("\n--- 2. Initialisation du LLM et du client MCP ---")
|
||||||
|
llm = ChatOpenAI(
|
||||||
|
model=LLM_CHAT_MODEL,
|
||||||
|
base_url=LLM_CHAT_SERVER_BASE_URL,
|
||||||
|
temperature=LLM_CHAT_TEMPERATURE,
|
||||||
|
api_key=LLM_CHAT_API_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
mcp_client = MCPClient.from_config_file("../resources/servers.json")
|
||||||
|
session = await mcp_client.create_session("movies-mcp-server")
|
||||||
|
print("Connexion au serveur d'outils (MCP) établie.")
|
||||||
|
|
||||||
|
print("\n--- 3. Découverte et création dynamique des outils LangChain ---")
|
||||||
|
remote_tools_definitions = await session.list_tools()
|
||||||
|
langchain_tools: List[StructuredTool] = []
|
||||||
|
type_mapping = {'string': str, 'integer': int, 'number': float, 'boolean': bool, 'object': dict}
|
||||||
|
|
||||||
|
async def run_mcp_tool(tool_name: str, **kwargs: Dict[str, Any]) -> str:
|
||||||
|
print(f"--- AGENT -> OUTIL : Appel de '{tool_name}' avec {kwargs} ---")
|
||||||
|
result = await session.call_tool(name=tool_name, arguments=kwargs)
|
||||||
|
# On formate le dictionnaire de retour en une chaîne de caractères
|
||||||
|
# pour que le LLM puisse le lire facilement dans l'étape d'observation.
|
||||||
|
text_result = result.content[0].text if result.content else "Action effectuée."
|
||||||
|
print(f"--- OUTIL -> AGENT : Résultat : {text_result} ---")
|
||||||
|
return text_result
|
||||||
|
|
||||||
|
|
||||||
|
for tool_def in remote_tools_definitions:
|
||||||
|
fields: Dict[str, Any] = {}
|
||||||
|
params_schema = tool_def.inputSchema
|
||||||
|
|
||||||
|
if 'properties' in params_schema:
|
||||||
|
for param_name, param_details in params_schema['properties'].items():
|
||||||
|
if not param_name.startswith('_'):
|
||||||
|
param_type = type_mapping.get(param_details.get('type'), Any)
|
||||||
|
description = param_details.get('description', '')
|
||||||
|
fields[param_name] = (param_type, Field(..., description=description))
|
||||||
|
|
||||||
|
DynamicToolArgs: Type[BaseModel] = create_model(f'{tool_def.name}Args', **fields)
|
||||||
|
tool_func = (lambda name: lambda **kwargs: run_mcp_tool(name, **kwargs))(tool_def.name)
|
||||||
|
langchain_tool = StructuredTool(
|
||||||
|
name=tool_def.name, description=tool_def.description, func=tool_func,
|
||||||
|
coroutine=tool_func, args_schema=DynamicToolArgs
|
||||||
|
)
|
||||||
|
langchain_tools.append(langchain_tool)
|
||||||
|
|
||||||
|
print(f"Outil LangChain créé dynamiquement : {[tool.name for tool in langchain_tools]}")
|
||||||
|
|
||||||
|
print("\n--- 4. Construction de l'agent avec un prompt système adapté ---")
|
||||||
|
system_prompt = """
|
||||||
|
Tu es un assistant expert en cinéma. Tu dois répondre aux questions de l'utilisateur en français.
|
||||||
|
Analyse la question de l'utilisateur. Si tu as besoin d'informations que tu n'as pas, appelle l'outil approprié que tu as à ta disposition.
|
||||||
|
Une fois que tu as obtenu une réponse de l'outil, utilise cette information pour formuler une réponse finale et claire pour l'utilisateur.
|
||||||
|
"""
|
||||||
|
|
||||||
|
prompt = ChatPromptTemplate.from_messages([
|
||||||
|
("system", system_prompt),
|
||||||
|
("human", "{input}"),
|
||||||
|
MessagesPlaceholder("agent_scratchpad"),
|
||||||
|
])
|
||||||
|
|
||||||
|
agent = create_tool_calling_agent(llm, langchain_tools, prompt)
|
||||||
|
print("Agent Executor prêt.")
|
||||||
|
|
||||||
|
return AgentExecutor(agent=agent, tools=langchain_tools, verbose=True)
|
||||||
|
|
||||||
|
async def run_agent():
|
||||||
|
# variable globale utile uniquement si par la suite vous souhaitez initialiser l'agent
|
||||||
|
# depuis FastAPI via @asynccontextmanager / async def lifespan(app: FastAPI):
|
||||||
|
global agent_executor
|
||||||
|
print("Démarrage de l'application : initialisation de l'agent...")
|
||||||
|
agent_executor = await build_agent()
|
||||||
|
print("Agent prêt à transmettre une requête.")
|
||||||
|
if agent_executor:
|
||||||
|
user_request = "Pour le film avec l'ID 1, peux-tu m'afficher le synopsis pertinent ?"
|
||||||
|
response = await agent_executor.ainvoke({"input": user_request})
|
||||||
|
print(response.get("output", "Pas de sortie de l'agent."))
|
||||||
|
print("Arrêt de l'application : fermeture des sessions MCP...")
|
||||||
|
if mcp_client:
|
||||||
|
await mcp_client.close_all_sessions()
|
||||||
|
print("Sessions fermées.")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await run_agent()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Reference in New Issue
Block a user