Transférer les outils MCP via LiteLLM : Guide pas à pas pour une passerelle bancaire de simulation
Enregistrez une banque fictive accessible via HTTP, comparez les boucles d’outils développées manuellement à une seule requête de passerelle, et démontrez que les mécanismes de protection pré-appel n’atteignent jamais MCP.
Photo de Quilia sur Unsplash
On suppose qu’un gateway LiteLLM est déjà en service (interface d’administration + base de données) avec gpt-5.4-mini enregistré auprès d’Azure AI Foundry. La section d’aperçu couvre cette configuration de base.
Que signifie « agentic » dans LiteLLM
Le gateway MCP de LiteLLM permet à un modèle enregistré d’accéder à des outils externes via le proxy. Le gateway découvre les outils, traduit les schémas et gère la boucle d’exécution afin que le code de l’application n’ait pas à gérer les détails du protocole MCP pour chaque fournisseur de modèle. Votre client communique avec une seule interface de type OpenAI ; le gateway se situe entre ce client, le modèle et tous les serveurs MCP que vous enregistrez.
Les identifiants des modèles sont facilement erronés. Selon la manière dont l’entrée Azure a été ajoutée, LiteLLM peut exposer azure_ai/gpt-5.4-mini au lieu de simplement gpt-5.4-mini. Vérifiez d’abord le registre en temps réel :
curl -X GET 'http://localhost:4000/v1/models' -H 'Authorization: Bearer sk-1234'
# Expected Results
{"data":[{"id":"azure_ai/gpt-5.4-mini","object":"model","created":1677610602,"owned_by":"openai"}],"object":"list"}
Utilisez cette chaîne de caractères exacte pour chaque requête. Tout manque de correspondance se manifeste par l’erreur Invalid model name passed in model=... plutôt que par une erreur réseau vague.
Même point de terminaison, LLM simple ou avec des outils
Il n’existe pas d’URL distincte pour le “mode MCP”. /chat/completions et /v1/responses se comportent de la même manière pour les conversations simples et celles enrichies par des outils. La seule différence réside dans le fait que le corps JSON contienne ou non un tableau tools (souvent avec type: "mcp" pour les outils gérés par le gateway). En omettant tools, on obtient une complétion normale, même si des serveurs MCP sont connectés au gateway. En incluant tools, le gateway peut découvrir, appeler ces outils et intégrer leurs résultats dans la réponse.
Une requête simple sans outils a cet aspect :
curl -X POST 'http://localhost:4000/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-1234' \
-d '{
"model": "azure_ai/gpt-5.4-mini",
"messages": [{"role": "user", "content": "What is the capital of France?"}]
}'
# Expected Results
{"id":"chatcmpl-E778uLVTQJQLIJu0Dc2XAI1UYHuwL","created":1785364460,"model":"azure_ai/gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"The capital of France is **Paris**.","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}}],"usage":{"completion_tokens":13,"prompt_tokens":13,"total_tokens":26,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"latency_checkpoint":{"engine_tbt_ms":8,"engine_ttft_ms":86,"engine_ttlt_ms":185,"pre_inference_ms":70,"service_tbt_ms":10,"service_ttft_ms":244,"service_ttlt_ms":354,"total_duration_ms":306,"user_visible_ttft_ms":174}},"service_tier":"default","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"jailbreak":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}]}
Une erreur fréquente lors du copier-coller est la présence d’une guillemet supplémentaire avant le nom du modèle (""azure_ai/gpt-5.4-mini"). Cela provoque l’erreur Invalid JSON payload: unexpected character — un problème de syntaxe du corps, et non une erreur de route.
The MCP Gateway, étape par étape (testé du début à la fin)
Serveur bancaire simulé
Au lieu d’un processus MCP générique de type “hello-world”, cette démarche utilise un petit système bancaire simulé comprenant trois outils — get_balance, list_transactions et transfer_funds — soutenus par deux comptes en mémoire. Les soldes et les virements réels permettent de déterminer clairement si le chemin complet fonctionne. Chaque étape ci-dessous a été exécutée et vérifiée à l’aide de ces données.
Étape 1 — démarrer le serveur (voir également le fichier mock_bank_server.py indépendant) :
mkdir mock-bank-server && cd mock-bank-server
uv init .
uv add "mcp[cli]" openai
touch server.py
server.py fonctionne avec des données simulées, et non avec un registre réel. La structure prévue est la suivante : client → LiteLLM → mécanismes de contrôle → routeur → soit le modèle Azure, soit le processus MCP bancaire simulé ainsi que ses trois outils.
Quelques détails comportementaux sont importants une fois que l’on s’éloigne du diagramme :
- Le parcours pertinent est un boucle, et non une simple flèche. Le modèle demande un outil ; le passage de contrôle redirige vers MCP ; le résultat revient via ce même passage de contrôle ; le modèle continue ou demande à nouveau. Les parcours utilisant un seul outil terminent automatiquement cette boucle ; les parcours parallèles utilisant plusieurs outils ne s’exécutent que partiellement de manière automatique dans la version testée ici.
pre_call examine le chargement brut du chat avant l’acheminement. pre_mcp_call examine les arguments de l’outil plus près du côté MCP. Les diagrammes montrent souvent une seule boîte ; en réalité, les interceptions se produisent à des étapes différentes.server.py, et non trois microservices distincts.Le serveur MCP bancaire simulé
Environ quatre-vingt-dix lignes définissent FastMCP "mock-bank" avec :
get_balance(account_id)— propriétaire et soldelist_transactions(account_id, limit)— dernières transactionstransfer_funds(from_account, to_account, amount)— transfert de fonds avec une validation de base (compte inconnu, solde insuffisant)
from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings
mcp = FastMCP(
"mock-bank",
host="127.0.0.1",
port=3001,
transport_security=TransportSecuritySettings(
allowed_hosts=["localhost:3001", "127.0.0.1:3001", "host.docker.internal:3001"],
),
)
# --- In-memory mock data (resets every restart) ---
ACCOUNTS = {
"ACC1001": {"owner": "Alice Johnson", "balance": 4250.75, "currency": "USD"},
"ACC1002": {"owner": "Bob Smith", "balance": 980.10, "currency": "USD"},
}
TRANSACTIONS = {
"ACC1001": [
{"date": "2026-07-20", "description": "Grocery Store", "amount": -84.32},
{"date": "2026-07-18", "description": "Payroll Deposit", "amount": 2500.00},
{"date": "2026-07-15", "description": "Electric Bill", "amount": -120.44},
],
"ACC1002": [
{"date": "2026-07-21", "description": "Coffee Shop", "amount": -6.75},
{"date": "2026-07-19", "description": "Freelance Payment", "amount": 450.00},
],
}
@mcp.tool()
def get_balance(account_id: str) -> dict:
"""Get the current balance and owner for a bank account."""
account = ACCOUNTS.get(account_id)
if not account:
return {"error": f"Account '{account_id}' not found"}
return {
"account_id": account_id,
"owner": account["owner"],
"balance": account["balance"],
"currency": account["currency"],
}
@mcp.tool()
def list_transactions(account_id: str, limit: int = 5) -> dict:
"""List recent transactions for a bank account, most recent first."""
if account_id not in ACCOUNTS:
return {"error": f"Account '{account_id}' not found"}
txns = TRANSACTIONS.get(account_id, [])[:limit]
return {"account_id": account_id, "transactions": txns}
@mcp.tool()
def transfer_funds(from_account: str, to_account: str, amount: float) -> dict:
"""Transfer funds between two mock bank accounts."""
if from_account not in ACCOUNTS:
return {"error": f"Source account '{from_account}' not found"}
if to_account not in ACCOUNTS:
return {"error": f"Destination account '{to_account}' not found"}
if amount <= 0:
return {"error": "Transfer amount must be positive"}
if ACCOUNTS[from_account]["balance"] < amount:
return {"error": f"Insufficient funds in '{from_account}'"}
ACCOUNTS[from_account]["balance"] -= amount
ACCOUNTS[to_account]["balance"] += amount
return {
"status": "success",
"from_account": from_account,
"to_account": to_account,
"amount": amount,
"new_balance_from": ACCOUNTS[from_account]["balance"],
"new_balance_to": ACCOUNTS[to_account]["balance"],
}
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Des scripts de comparaison permettent de clarifier ce que le gateway remplace :
test-direct-mcp.pycommunique uniquement avec le serveur MCP — il permet de vérifier que les outils fonctionnent, sans jamais impliquer de modèle.test-direct-model.pyappelle directement Azure et MCP, et orchestre manuellement la traduction du schéma, l’exécution des outils ainsi que la deuxième étape ; environ soixante-quinze lignes de code permettent au gateway de regrouper tout cela en un tableautoolslors d’une seule requête HTTP.- Aucun des deux scripts n’accède au port
4000; c’est précisément cette isolation qui justifie la comparaison avant/après.
Démarrez le serveur et laissez-le fonctionner :
uv run python server.py
# Execution results
INFO: Started server process [91854]
INFO: Waiting for application startup.
[07/26/26 21:36:05] INFO StreamableHTTP session manager started streamable_http_manager.py:131
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:3001 (Press CTRL+C to quit)
[07/26/26 21:36:26] INFO Created new transport with session ID: caf5347483ba4256977e29de8892a327
Étape 2 — deux corrections de connectivité avant que LiteLLM (dans Docker) ne puisse communiquer avec le processus hôte
Dans le conteneur Compose/Admin UI, localhost fait référence au conteneur lui-même, et non à votre ordinateur portable. Dirigez l’URL MCP vers le DNS de redirection du hôte Docker :
http://host.docker.internal:3001/mcp
Par ailleurs, la protection contre le réenregistrement DNS du SDK MCP peut répondre par 421 Misdirected Request, à moins que l’adresse Host redirigée ne figure sur la liste des adresses autorisées du serveur :
from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings
mcp = FastMCP(
"mock-bank",
host="127.0.0.1",
port=3001,
transport_security=TransportSecuritySettings(
allowed_hosts=["localhost:3001", "127.0.0.1:3001", "host.docker.internal:3001"],
),
)
Étape 3 — enregistrer le serveur auprès de LiteLLM
Dans l’interface utilisateur : MCP Servers → Add MCP Server
- Nom :
mock_bank - Transport : Streamable HTTP (doit correspondre à
mcp.run(transport="streamable-http")) - URL :
http://host.docker.internal:3001/mcp - Authentification : aucune pour cette démonstration locale
Ou via la configuration :
mcp_servers:
mock_bank:
url: http://host.docker.internal:3001/mcp
transport: streamable_http
auth_type: none
Vérifiez que l’indicateur Connection Status: Connected est affiché et que la section Tool Configuration liste bien les trois outils activés.
Étape 4 — vérifier la chaîne de modèle réellement enregistrée par le gateway
curl -X GET 'http://localhost:4000/v1/models' -H 'Authorization: Bearer sk-1234'
# Expected Result
{"data":[{"id":"azure_ai/gpt-5.4-mini","object":"model","created":1677610602,"owned_by":"openai"}],"object":"list"}
Malgré le préfixe azure_ai/, le trafic a tout de même atteint le pipeline de chat Azure OpenAI lors des tests (des champs de filtrage du contenu apparaissaient dans les réponses). Considérez cet identifiant comme une particularité de nommage à copier telle quelle, et non comme un fournisseur incorrect.
Étape 5 — tour avec une seule outil (chemin propre)
curl --location 'http://localhost:4000/v1/responses' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer sk-1234" \
--data '{
"model": "azure_ai/gpt-5.4-mini",
"input": [
{"role": "user", "content": "What is the balance on account ACC1001, and what were its last 3 transactions?", "type": "message"}
],
"tools": [
{
"type": "mcp",
"server_label": "mock_bank",
"server_url": "litellm_proxy",
"require_approval": "never"
}
],
"tool_choice": "required"
}'
# Expected Result
{"id":"resp_QQRbArl1kNuzmPJahCQIWJvpgxSgsw7SuuwX_pZJfuWmrGiguydiPvF7EhPhfRrojCr6UtQGUTlWEA3FSDBl4c1PSDpccSmbIZWgCMizU1Vp8gDt_jokT8ZOL9tIiYK0pUHjYVGu3PgbJjz5J0jkAJUFKshh_-9xn0zVGf08-33WD19mcLCh3qfxRM4AcMciWE4nNQa_TrS-6U6olfonnXvge9qszI4u6dA02Eo6GlLmEpGE19InxbopOLBny1QNdMDgdC4SzzZh9XWnQvFNu-IjYEe68pfoJ5u0ohZE1scbgF9ABNMAaN94vfGIuApDd9ibCBpgbHbcTsrE86EGP6_XJ_72uwG_aTHTjgZ-laFUxCIjEgEEo8S09Ojq-eupjWI8WTwSOT_ozMKVF0cyOuQKwkpARIrIeGE=","created_at":1785242019,"error":null,"incomplete_details":null,"instructions":null,"metadata":{},"model":"azure_ai/gpt-5.4-mini","object":"response","output":[{"id":"msg_0977207685db3e25006a68a1a3dc108196ad8f53ed0c3c8c73","content":[{"annotations":[],"text":"Account **ACC1001** belongs to **Alice Johnson**.\n\n- **Balance:** **$3,450.75 USD**\n\nLast 3 transactions:\n1. **2026-07-20** — Grocery Store — **-$84.32**\n2. **2026-07-18** — Payroll Deposit — **+$2,500.00**\n3. **2026-07-15** — Electric Bill — **-$120.44**","type":"output_text","logprobs":[]}],"role":"assistant","status":"completed","type":"message","phase":"final_answer"},{"type":"mcp_tools_fetched","id":"mcp_tools_cbeff22f","status":"completed","role":"system","content":[{"type":"output_text","text":"[\n \"name='mock_bank-get_balance' title=None description='Get the current balance and owner for a bank account.' inputSchema={'properties': {'account_id': {'title': 'Account Id', 'type': 'string'}}, 'required': ['account_id'], 'title': 'get_balanceArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\",\n \"name='mock_bank-list_transactions' title=None description='List recent transactions for a bank account, most recent first.' inputSchema={'properties': {'account_id': {'title': 'Account Id', 'type': 'string'}, 'limit': {'default': 5, 'title': 'Limit', 'type': 'integer'}}, 'required': ['account_id'], 'title': 'list_transactionsArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\",\n \"name='mock_bank-transfer_funds' title=None description='Transfer funds between two mock bank accounts.' inputSchema={'properties': {'from_account': {'title': 'From Account', 'type': 'string'}, 'to_account': {'title': 'To Account', 'type': 'string'}, 'amount': {'title': 'Amount', 'type': 'number'}}, 'required': ['from_account', 'to_account', 'amount'], 'title': 'transfer_fundsArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\"\n]","annotations":[]}],"phase":null},{"type":"tool_execution_results","id":"tool_results_ee9f6c7e","status":"completed","role":"system","content":[{"type":"output_text","text":"[\n {\n \"tool_call_id\": \"call_ToKnEWo532C8nDABKElHCSBs\",\n \"result\": \"{\\n \\\"account_id\\\": \\\"ACC1001\\\",\\n \\\"owner\\\": \\\"Alice Johnson\\\",\\n \\\"balance\\\": 3450.75,\\n \\\"currency\\\": \\\"USD\\\"\\n}\",\n \"name\": \"mock_bank-get_balance\"\n },\n {\n \"tool_call_id\": \"call_xZ9GNGQEPXRWI3EEaVuh6J4H\",\n \"result\": \"{\\n \\\"account_id\\\": \\\"ACC1001\\\",\\n \\\"transactions\\\": [\\n {\\n \\\"date\\\": \\\"2026-07-20\\\",\\n \\\"description\\\": \\\"Grocery Store\\\",\\n \\\"amount\\\": -84.32\\n },\\n {\\n \\\"date\\\": \\\"2026-07-18\\\",\\n \\\"description\\\": \\\"Payroll Deposit\\\",\\n \\\"amount\\\": 2500.0\\n },\\n {\\n \\\"date\\\": \\\"2026-07-15\\\",\\n \\\"description\\\": \\\"Electric Bill\\\",\\n \\\"amount\\\": -120.44\\n }\\n ]\\n}\",\n \"name\": \"mock_bank-list_transactions\"\n }\n]","annotations":[]}],"phase":null}],"parallel_tool_calls":true,"temperature":1.0,"tool_choice":"auto","tools":[{"name":"mock_bank-get_balance","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"Get the current balance and owner for a bank account."},{"name":"mock_bank-list_transactions","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"List recent transactions for a bank account, most recent first."},{"name":"mock_bank-transfer_funds","parameters":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"Transfer funds between two mock bank accounts."}],"top_p":0.98,"max_output_tokens":null,"previous_response_id":"resp_0977207685db3e25006a68a1a1f48c8196b7990872db041d14","reasoning":{"context":"current_turn","effort":"none","mode":"standard","summary":null},"status":"completed","text":{"format":{"type":"text"},"verbosity":"medium"},"truncation":"disabled","usage":{"input_tokens":473,"input_tokens_details":{"audio_tokens":null,"cached_tokens":0,"text_tokens":null},"output_tokens":97,"output_tokens_details":{"reasoning_tokens":0,"text_tokens":null},"total_tokens":570,"cost":null},"user":null,"store":true,"background":false,"completed_at":1785242020,"content_filters":[{"blocked":false,"source_type":"completion","content_filter_raw":[],"content_filter_results":{"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"hate":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"}},"content_filter_offsets":{"start_offset":0,"end_offset":255,"check_offset":0}}],"frequency_penalty":0.0,"max_tool_calls":null,"moderation":null,"presence_penalty":0.0,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","safety_identifier":null,"service_tier":"default","top_logprobs":0
Lors de l’exécution réussie, le modèle a appelé les fonctions get_balance et list_transactions, puis a répondu avec des données simulées en temps réel : Alice Johnson sur ACC1001, un solde correspondant au fichier de configuration en mémoire, ainsi que les trois dernières transactions provenant de mock_bank_server.py. Cela confirme le flux : passerelle → découverte → exécution → réponse finale.
Étape 6 — appel modifiant l’état lorsque c’est le seul outil utilisé dans ce tour
curl -X POST 'http://localhost:4000/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-1234' \
-d '{
"model": "azure_ai/gpt-5.4-mini",
"messages": [{"role": "user", "content": "First, transfer $200 from ACC1001 to ACC1002. Do not call any other tools yet."}],
"tools": [{"type": "mcp", "server_label": "mock_bank", "server_url": "litellm_proxy", "require_approval": "never"}],
"tool_choice": "auto"
}'
# Expected result
{"id":"chatcmpl-E6bKV9nPurWICiuBlkgwaJzOS8vIC","created":1785242171,"model":"azure_ai/gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"Done — transferred $200 from ACC1001 to ACC1002 successfully.","role":"assistant","provider_specific_fields":{"refusal":null,"mcp_list_tools":[{"type":"function","function":{"name":"mock_bank-get_balance","description":"Get the current balance and owner for a bank account.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-list_transactions","description":"List recent transactions for a bank account, most recent first.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-transfer_funds","description":"Transfer funds between two mock bank accounts.","parameters":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object","additionalProperties":false},"strict":false}}],"mcp_tool_calls":[{"function":{"arguments":"{\"from_account\":\"ACC1001\",\"to_account\":\"ACC1002\",\"amount\":200}","name":"mock_bank-transfer_funds"},"id":"call_PIN38jt2KT9M4hkoxEKXXXaD","type":"function"}],"mcp_call_results":[{"tool_call_id":"call_PIN38jt2KT9M4hkoxEKXXXaD","result":"{\n \"status\": \"success\",\n \"from_account\": \"ACC1001\",\n \"to_account\": \"ACC1002\",\n \"amount\": 200.0,\n \"new_balance_from\": 3250.75,\n \"new_balance_to\": 1980.1\n}","name":"mock_bank-transfer_funds"}]},"annotations":[]},"provider_specific_fields":{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}}],"usage":{"completion_tokens":19,"prompt_tokens":373,"total_tokens":392,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"latency_checkpoint":{"engine_tbt_ms":5,"engine_ttft_ms":30,"engine_ttlt_ms":116,"pre_inference_ms":120,"service_tbt_ms":5,"service_ttft_ms":306,"service_ttlt_ms":316,"total_duration_ms":223,"user_visible_ttft_ms":186}},"service_tier":"default","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{}}]}
Résultat : finish_reason: "stop", transfer_funds a été exécuté automatiquement, résumé final avec les deux soldes mis à jour. Le transfert d’argent est précisément le type d’action qui nécessite des contrôles avant que quoi que ce soit ne quitte une démonstration sur ordinateur portable.
Leçon — appels de outils en parallèle en une seule étape
Les prompts composés tels que « transférer 200 $ de ACC1001 vers ACC1002, puis confirmer les deux soldes » ont provoqué l’émission simultanée de trois appels d’outil par le modèle. Dans la version LiteLLM testée, l’exécution automatique de MCP n’a réussi à finaliser qu’un seul appel (transfer_funds affiché dans mcp_tool_calls / mcp_call_results) tandis que les appels get_balance restants sont restés non résolus dans le tableau tool_calls de type OpenAI — sans texte final, car le modèle attendait encore. Ce comportement s’est manifesté tant sur /v1/responses que sur /chat/completions, quel que soit le valeur de tool_choice, qu’il s’agisse de "required" ou de "auto". Il ne s’agit donc pas d’une particularité liée à la forme de la requête ; c’est plutôt la manière dont cette version gère plusieurs appels d’outil MCP parallèles en une seule étape.
Conseils pratiques pour cette construction : demandez un outil à la fois (comme indiqué à l’Étape 6). Si vous avez réellement besoin de parallélisme avec plusieurs appels, terminez la boucle vous-même — exécutez les tool_calls restants et envoyez en POST les résultats avec role: "tool", selon le même schéma présenté dans les scripts Direct vs Gateway ci-dessous. Le choix du modèle reste important pour la fiabilité de l’agent ; le choix « parfait » d’hier peut sembler dépassé dès le cycle de mise à jour suivant, il faut donc conserver une structure d’orchestration interchangeable.
Étape 7 — vérifier la découverte sans impliquer le modèle
curl -X POST 'http://localhost:4000/mcp/mock_bank' \
-H 'Authorization: Bearer sk-1234' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Expected result
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"mock_bank-get_balance","description":"Get the current balance and owner for a bank account.","inputSchema":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object"}},{"name":"mock_bank-list_transactions","description":"List recent transactions for a bank account, most recent first.","inputSchema":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object"}},{"name":"mock_bank-transfer_funds","description":"Transfer funds between two mock bank accounts.","inputSchema":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object"}}]}}
Cet appel permet de distinguer le cas où « la découverte ne fonctionne pas » du cas où « le modèle a refusé d’appeler l’outil » ou « seule une partie de la séquence a été exécutée ».
Direct vs Gateway : ce que le proxy vous apporte
Test 1 — MCP brut, sans modèle, sans gateway. Vérifiez le fonctionnement du serveur bancaire seul :
import asyncio
from mcp.client.streamable_http import streamablehttp_client
from mcp.client.session import ClientSession
async def main():
async with streamablehttp_client("http://127.0.0.1:3001/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
# 1. Discover what tools exist
tools = await session.list_tools()
print("Available tools:", [t.name for t in tools.tools])
# 2. Call a tool directly — YOU decide which tool and args.
# There is no model here, no reasoning, no interpretation of
# natural language. You must already know exactly what to call.
result = await session.call_tool(
"get_balance", arguments={"account_id": "ACC1001"}
)
print("\nget_balance result:")
print(result.content[0].text)
result2 = await session.call_tool(
"list_transactions", arguments={"account_id": "ACC1001", "limit": 3}
)
print("\nlist_transactions result:")
print(result2.content[0].text)
if __name__ == "__main__":
asyncio.run(main())
Résultats attendus concernant la liste et le solde :
uv run test-direct-mcp.py
# Expected result
Available tools: ['get_balance', 'list_transactions', 'transfer_funds']
get_balance result:
{
"account_id": "ACC1001",
"owner": "Alice Johnson",
"balance": 3250.75,
"currency": "USD"
}
list_transactions result:
{
"account_id": "ACC1001",
"transactions": [
{
"date": "2026-07-20",
"description": "Grocery Store",
"amount": -84.32
},
{
"date": "2026-07-18",
"description": "Payroll Deposit",
"amount": 2500.0
},
{
"date": "2026-07-15",
"description": "Electric Bill",
"amount": -120.44
}
]
}
Test 2 — modèle direct associé à une orchestration manuelle. Récupérer des outils MCP, traduire les schémas en format d’outil OpenAI, appeler Azure, exécuter les outils, renvoyer les résultats — votre application doit contenir environ soixante-quinze lignes de code, sans mécanismes de contrôle partagés ni suivi des dépenses :
import asyncio
import json
import os
from openai import AzureOpenAI
from mcp.client.streamable_http import streamablehttp_client
from mcp.client.session import ClientSession
MODEL_DEPLOYMENT = "gpt-5.4-mini"
def mcp_tool_to_openai_format(mcp_tool):
"""Manual translation step #2 — the gateway normally does this for you."""
return {
"type": "function",
"function": {
"name": mcp_tool.name,
"description": mcp_tool.description or "",
"parameters": mcp_tool.inputSchema,
},
}
async def main():
client = AzureOpenAI(
api_key=os.environ["AZURE_API_KEY"],
api_version="2024-10-21",
azure_endpoint=os.environ["AZURE_API_BASE"],
)
async with streamablehttp_client("http://127.0.0.1:3001/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
# Step 1: discover tools yourself
mcp_tools = (await session.list_tools()).tools
openai_tools = [mcp_tool_to_openai_format(t) for t in mcp_tools]
messages = [
{
"role": "user",
"content": "What is the balance on account ACC1001, and "
"what were its last 3 transactions?",
}
]
# Step 3: call the model directly
response = client.chat.completions.create(
model=MODEL_DEPLOYMENT,
messages=messages,
tools=openai_tools,
tool_choice="required",
)
msg = response.choices[0].message
messages.append(msg.model_dump(exclude_none=True))
# Step 4: manually execute any tool calls the model requested
for tool_call in msg.tool_calls or []:
args = json.loads(tool_call.function.arguments)
result = await session.call_tool(tool_call.function.name, arguments=args)
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": result.content[0].text,
}
)
# Second turn: give the model the tool results, get final answer
final = client.chat.completions.create(
model=MODEL_DEPLOYMENT,
messages=messages,
)
print(final.choices[0].message.content)
if __name__ == "__main__":
asyncio.run(main())
Exemple de réponse narrative réussie :
uv run test-direct-model.py
# Expected Result
Account **ACC1001** (Alice Johnson) has a balance of **USD 3,250.75**.
Last 3 transactions:
1. **2026-07-20** — Grocery Store — **-84.32**
2. **2026-07-18** — Payroll Deposit — **+2,500.00**
3. **2026-07-15** — Electric Bill — **-120.44**
Test 3 — même intention via LiteLLM. Une seule requête HTTP ; la découverte, la traduction, l’exécution ainsi que la réponse suivante se déroulent directement à l’intérieur du gateway :
curl --location 'http://localhost:4000/v1/responses' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer sk-1234" \
--data '{
"model": "azure_ai/gpt-5.4-mini",
"input": [{"role": "user", "content": "What is the balance on account ACC1001, and what were its last 3 transactions?", "type": "message"}],
"tools": [{"type": "mcp", "server_label": "mock_bank", "server_url": "litellm_proxy", "require_approval": "never"}],
"tool_choice": "required"
}'
# Expected Result
{"id":"resp_DCjguhF87Zw3ekO8Jw3782IkYnZQfG_avgYD_kkI-NmeDU9bNSDRFM3FTGJl31tMzutajvYJ6x_S8siU06zyXozMCPC1pSQhXraDtpREE_RRXq23BZDJR37fxmPqzVsdhlk-o2IEF3C8Z3251nJPIecUJM75GFAYdXygfMfoqmM1Jdaxc3VQfcZKMaW1IOG7528zrRqms58F5xwpR0vvFScwclLhOYrch5WQRY6KBBi-60wMBuylfQNfqT8cWwvbeG7Xbe6OKEiBXFzWOx7QkaGhckDFat-DZ3RFW8Iv8SP0a21rA_9h19QUijB-sPBaD9BNIKk4I2dRqz2B1vMtyut3GpX8mO5D5dinKruIMy2nrMatBun8uJC5Mt76hWgFc6YzH3h_ihsZjCRzWz1j85VowrVdnOYGGhg=","created_at":1785242960,"error":null,"incomplete_details":null,"instructions":null,"metadata":{},"model":"azure_ai/gpt-5.4-mini","object":"response","output":[{"id":"msg_0f2c0a8554062b57006a68a550d828819085dd064764201c32","content":[{"annotations":[],"text":"Account **ACC1001** is owned by **Alice Johnson**.\n\n- **Balance:** **$3,250.75 USD**\n\n**Last 3 transactions:**\n1. **2026-07-20** — Grocery Store — **-$84.32**\n2. **2026-07-18** — Payroll Deposit — **+$2,500.00**\n3. **2026-07-15** — Electric Bill — **-$120.44**","type":"output_text","logprobs":[]}],"role":"assistant","status":"completed","type":"message","phase":"final_answer"},{"type":"mcp_tools_fetched","id":"mcp_tools_9d9b1cdc","status":"completed","role":"system","content":[{"type":"output_text","text":"[\n \"name='mock_bank-get_balance' title=None description='Get the current balance and owner for a bank account.' inputSchema={'properties': {'account_id': {'title': 'Account Id', 'type': 'string'}}, 'required': ['account_id'], 'title': 'get_balanceArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\",\n \"name='mock_bank-list_transactions' title=None description='List recent transactions for a bank account, most recent first.' inputSchema={'properties': {'account_id': {'title': 'Account Id', 'type': 'string'}, 'limit': {'default': 5, 'title': 'Limit', 'type': 'integer'}}, 'required': ['account_id'], 'title': 'list_transactionsArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\",\n \"name='mock_bank-transfer_funds' title=None description='Transfer funds between two mock bank accounts.' inputSchema={'properties': {'from_account': {'title': 'From Account', 'type': 'string'}, 'to_account': {'title': 'To Account', 'type': 'string'}, 'amount': {'title': 'Amount', 'type': 'number'}}, 'required': ['from_account', 'to_account', 'amount'], 'title': 'transfer_fundsArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\"\n]","annotations":[]}],"phase":null},{"type":"tool_execution_results","id":"tool_results_56298345","status":"completed","role":"system","content":[{"type":"output_text","text":"[\n {\n \"tool_call_id\": \"call_t6y64QgQjDT4jGMLOEyFxc5u\",\n \"result\": \"{\\n \\\"account_id\\\": \\\"ACC1001\\\",\\n \\\"owner\\\": \\\"Alice Johnson\\\",\\n \\\"balance\\\": 3250.75,\\n \\\"currency\\\": \\\"USD\\\"\\n}\",\n \"name\": \"mock_bank-get_balance\"\n },\n {\n \"tool_call_id\": \"call_nZZbT734wXIuElJTdSWOORwL\",\n \"result\": \"{\\n \\\"account_id\\\": \\\"ACC1001\\\",\\n \\\"transactions\\\": [\\n {\\n \\\"date\\\": \\\"2026-07-20\\\",\\n \\\"description\\\": \\\"Grocery Store\\\",\\n \\\"amount\\\": -84.32\\n },\\n {\\n \\\"date\\\": \\\"2026-07-18\\\",\\n \\\"description\\\": \\\"Payroll Deposit\\\",\\n \\\"amount\\\": 2500.0\\n },\\n {\\n \\\"date\\\": \\\"2026-07-15\\\",\\n \\\"description\\\": \\\"Electric Bill\\\",\\n \\\"amount\\\": -120.44\\n }\\n ]\\n}\",\n \"name\": \"mock_bank-list_transactions\"\n }\n]","annotations":[]}],"phase":null}],"parallel_tool_calls":true,"temperature":1.0,"tool_choice":"auto","tools":[{"name":"mock_bank-get_balance","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"Get the current balance and owner for a bank account."},{"name":"mock_bank-list_transactions","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"List recent transactions for a bank account, most recent first."},{"name":"mock_bank-transfer_funds","parameters":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"Transfer funds between two mock bank accounts."}],"top_p":0.98,"max_output_tokens":null,"previous_response_id":"resp_0f2c0a8554062b57006a68a54eeaa0819097e6e8dcb9255a38","reasoning":{"context":"current_turn","effort":"none","mode":"standard","summary":null},"status":"completed","text":{"format":{"type":"text"},"verbosity":"medium"},"truncation":"disabled","usage":{"input_tokens":473,"input_tokens_details":{"audio_tokens":null,"cached_tokens":0,"text_tokens":null},"output_tokens":100,"output_tokens_details":{"reasoning_tokens":0,"text_tokens":null},"total_tokens":573,"cost":null},"user":null,"store":true,"background":false,"completed_at":1785242961,"content_filters":[{"blocked":false,"source_type":"completion","content_filter_raw":[],"content_filter_results":{"protected_material_text":{"detected":false,"filtered":false},"protected_material_code":{"detected":false,"filtered":false},"hate":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"}},"content_filter_offsets":{"start_offset":0,"end_offset":260,"check_offset":0}}],"frequency_penalty":0.0,"max_tool_calls":null,"moderation":null,"presence_penalty":0.0,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","safety_identifier":null,"service_tier":"default","top_logprobs":0}
| Problématique | Direct (Test 2) | Gateway (Test 3) |
|---|---|---|
| Code de l’application | ~75 lignes par application | Une seule requête HTTP |
| Traduction des schémas | Manuelle, par serveur MCP | Automatiche |
| Boucle des outils | C’est vous qui la gérez |
modelmcp_serversLes règles de contrôle telles que la censure des e-mails, le masquage des numéros de carte et celui des mots-clés s’exécutent en pre_call — **avant** la découverte ou l’exécution MCP. Une requête bloquée ne doit jamais interagir avec le simulateur de banque. C’est cette propriété qui doit être prouvée, et non seulement le fait que le texte paraisse masqué dans la réponse.
curl -X POST 'http://localhost:4000/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-1234' \
-d '{
"model": "azure_ai/gpt-5.4-mini",
"messages": [{"role": "user", "content": "Check the balance on ACC1001 and email the results to john.doe@example.com"}],
"tools": [{"type": "mcp", "server_label": "mock_bank", "server_url": "litellm_proxy", "require_approval": "never"}],
"tool_choice": "auto",
"guardrails": ["basic-content-filter"]
}'
# Expected result
{"id":"chatcmpl-E6xkAAMouHdsDuJR038UZH4Thej9z","created":1785328330,"model":"azure_ai/gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"I checked the balance for ACC1001:\n\n- Owner: Alice Johnson\n- Balance: USD 3,250.75\n\nI can’t send emails directly from here, but you can forward this result to [EMAIL_REDACTED].","role":"assistant","provider_specific_fields":{"refusal":null,"mcp_list_tools":[{"type":"function","function":{"name":"mock_bank-get_balance","description":"Get the current balance and owner for a bank account.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-list_transactions","description":"List recent transactions for a bank account, most recent first.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-transfer_funds","description":"Transfer funds between two mock bank accounts.","parameters":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object","additionalProperties":false},"strict":false}}],"mcp_tool_calls":[{"function":{"arguments":"{\"account_id\":\"ACC1001\"}","name":"mock_bank-get_balance"},"id":"call_4jiRhEZhMUSY8GLWJHfDWulI","type":"function"}],"mcp_call_results":[{"tool_call_id":"call_4jiRhEZhMUSY8GLWJHfDWulI","result":"{\n \"account_id\": \"ACC1001\",\n \"owner\": \"Alice Johnson\",\n \"balance\": 3250.75,\n \"currency\": \"USD\"\n}","name":"mock_bank-get_balance"}]},"annotations":[]},"provider_specific_fields":{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}}],"usage":{"completion_tokens":53,"prompt_tokens":332,"total_tokens":385,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"latency_checkpoint":{"engine_tbt_ms":4,"engine_ttft_ms":36,"engine_ttlt_ms":260,"pre_inference_ms":125,"service_tbt_ms":4,"service_ttft_ms":375,"service_ttlt_ms":594,"total_duration_ms":478,"user_visible_ttft_ms":250}},"service_tier":"default","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{}}]}
Répétez sans mentionner les règles de contrôle dans le texte utilisateur :
curl -X POST 'http://localhost:4000/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-1234' \
-d '{
"model": "azure_ai/gpt-5.4-mini",
"messages": [{"role": "user", "content": "Check the balance on ACC1001 and email the results to john.doe@example.com"}],
"tools": [{"type": "mcp", "server_label": "mock_bank", "server_url": "litellm_proxy", "require_approval": "never"}],
"tool_choice": "auto"
}'
# Expected result
{"id":"chatcmpl-E6xlbLbaUP1awks92ZGSrsHCB8sl0","created":1785328419,"model":"azure_ai/gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"I checked ACC1001:\n\n- Owner: Alice Johnson\n- Balance: $3,250.75 USD\n\nI can’t send emails directly from here, but you can forward this result to [EMAIL_REDACTED].","role":"assistant","provider_specific_fields":{"refusal":null,"mcp_list_tools":[{"type":"function","function":{"name":"mock_bank-get_balance","description":"Get the current balance and owner for a bank account.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-list_transactions","description":"List recent transactions for a bank account, most recent first.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-transfer_funds","description":"Transfer funds between two mock bank accounts.","parameters":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object","additionalProperties":false},"strict":false}}],"mcp_tool_calls":[{"function":{"arguments":"{\"account_id\":\"ACC1001\"}","name":"mock_bank-get_balance"},"id":"call_59tE386FwTOGOROXJNJPQZlB","type":"function"}],"mcp_call_results":[{"tool_call_id":"call_59tE386FwTOGOROXJNJPQZlB","result":"{\n \"account_id\": \"ACC1001\",\n \"owner\": \"Alice Johnson\",\n \"balance\": 3250.75,\n \"currency\": \"USD\"\n}","name":"mock_bank-get_balance"}]},"annotations":[]},"provider_specific_fields":{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}}],"usage":{"completion_tokens":50,"prompt_tokens":332,"total_tokens":382,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"latency_checkpoint":{"engine_tbt_ms":5,"engine_ttft_ms":39,"engine_ttlt_ms":272,"pre_inference_ms":154,"service_tbt_ms":5,"service_ttft_ms":434,"service_ttlt_ms":646,"total_duration_ms":507,"user_visible_ttft_ms":281}},"service_tier":"default","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{}}]}
Tant les e-mails que les autres éléments doivent être affichés comme CENSURÉS lorsque les règles de contrôle sont activées par défaut. Si elles ne le sont pas, activez-les dans l’interface d’administration.
Explications pratiques
1. Toute requête doit-elle passer par tools ?
Oui, chaque fois que cette étape doit utiliser MCP. Le gateway n’ajoute pas silencieusement de outils aux appels qui les omettent. Si vous omettez tools, vous obtiendrez une simple complétion par LLM, même si mock_bank est connecté.
2. tool_choice: "required" contre "auto"
Utilisez "required" pour un test de base garantissant une seule appel à outil. Préférez "auto" pour des flux conversationnels ou multi-étapes, car "required" ne peut pas, par lui-même, générer une réponse finale en langage naturel.
3. Sélection d’outils plus sûre et plus claire
Les clients doivent en outre connaître les noms exacts des outils et leurs schémas, et rien ne l’empêche à un outil risqué comme transfer_funds de fonctionner aussi facilement que get_balance. LiteLLM propose déjà des solutions :
a) Publier le catalogue — éviter que les intégrateurs ne fassent de reverse engineering des appels aux outils du modèle :
curl -X POST 'http://localhost:4000/mcp/mock_bank' \
-H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
-d '{"method":"tools/list"}'
Faire apparaître ce catalogue (ou /v1/mcp/tools) dans la documentation destinée aux développeurs ou dans une interface d’onboarding afin que les noms, les descriptions et les schémas des arguments soient visibles avant même que quiconque n’écrive du code client.
b) Préférer des permissions MCP par clé ou par équipe plutôt que allow_all_keys. Limiter l’accès à mock_bank via Guardrails/MCP → Gestion des permissions de sorte que chaque clé ne voie que les outils auxquels elle a droit.
c) Séparation par niveau de risque. Marquez transfer_funds comme outil d’écriture à risque et définissez require_approval: "always" afin qu’une personne confirme avant que l’argent ne soit transféré. Laissez les outils de lecture à "never" si cela correspond à votre modèle de menace.
d) Appliquez les contrôles avec le mécanisme de protection côté serveur, et non avec tool_choice du client. Les utilisateurs contrôlent tool_choice ; ce n’est pas une barrière de sécurité. Un mécanisme de protection au niveau du gateway ne peut être contourné par une application cliente :
guardrails:
- guardrail_name: "mcp-tool-permissions"
litellm_params:
guardrail: tool_permission
mode: "pre_mcp_call"
rules:
- rule_id: "block-transfers"
tool_name: "^mock_bank-transfer_fundsquot;
decision: "deny"
default_action: "allow"
e) Restreignez les arguments, et pas seulement les listes d’outils autorisés. allowed_params peut limiter les plages de amount ou déterminer quels valeurs de account_id une clé peut utiliser — ce qui offre un contrôle plus fin qu’un refus total ou partiel.
Traitez le catalogue d’outils comme une API de produit : publiez-le, définissez son champ d’application par client, et appliquez des niveaux de risque différents pour les lectures et écritures grâce à des politiques côté serveur, plutôt que de faire confiance à chaque client.
Que faire ensuite
Avec des mesures de sécurité de base et MCP via LiteLLM en place, les étapes logiques suivantes sont les limites de débit, le nombre maximal de requêtes simultanées, des contrôles d’entrée spécifiques à MCP, ainsi qu’une politique plus stricte de sélection des outils. Jusqu’à ce que tout cela soit mis en place, conservez les démonstrations localement, gardez les outils à risque derrière une approbation préalable, et soyez méticuleux quant à ce que chaque clé API peut appeler.
Lors du débogage de problèmes imprévisibles liés aux interactions avec les agents, enregistrez l’ID de la requête du gateway, la chaîne du modèle enregistré, les noms des serveurs MCP utilisés, ainsi que le fait que tool_choice était réglé sur auto ou required. Ce quatuor permet généralement d’identifier plus rapidement les problèmes tels que « mauvais ID de modèle », « outils omis », « défaillance de la découverte » ou « exécution partielle des appels parallèles », plutôt que de devoir réexécuter manuellement les requêtes. Gardez la banque de données simulée temporaire : effacez ses données et redémarrez-la entre les tests de transfert afin que les soldes restants ne soient pas confondus avec des résultats de démonstration réussis.