Dans l’épisode précédent, nous avons vu comment créer un serveur MCP en .NET et le connecter à un IBM i via NTi data provider.
Cet article fait partie d’une série consacrée à l’utilisation d’un agent IA avec l'IBM i via le Model Context Protocol:
- Créer un serveur MCP en .NET pour connecter l’IBM i (AS400) à une IA
- Lecture et écriture SQL sur DB2 for i depuis .NET avec un serveur MCP (cet article)
- Appeler un programme RPG IBM i depuis .NET avec un agent IA
Cette fois, on va travailler avec les données: requête SQL avec jointures, raisonnement multi-étapes de l’agent IA, et première écriture en base avec confirmation utilisateur.
Cas d’usage métier: interroger DB2 for i en langage naturel depuis Claude Desktop
Sylvie est responsable commerciale chez Translog. Ce matin, elle ouvre Claude Desktop et tape:
" Donne-moi un état de l'activité du jour ".
Trente secondes plus tard, elle obtient un tableau de bord structuré : nombre d’expéditions en transit et en préparation, incidents ouverts avec alerte visuelle, livraisons du jour, ainsi que les montants et volumes associés. Le tout détaillé par client, destination et statut, et sans ouvrir un seul écran 5250, car soyons honnête, Sylvie n’y connait rien en BM i.
C’est le fil rouge de cet article: un seul projet .NET, quatre tools, et une démo qui va du simple dashboard à l’écriture en base.
Architecture du projet MCP .NET connecté à l’IBM i via NTi
Pour cet article, on repart du projet créé dans l’épisode 1.Trois modifications sont nécessaires avant de commencer :
- Supprimer le dossier Admin et la classe
AdminToolsqu’il contient. - Créer un dossier SQL et y ajouter la classe
TranslogTools.cs. - Mettre à jour le
program.cspour référencer la nouvelle classe à la place de l’ancienne.
Claude voit les tools disponibles simultanément.
SiAdminToolsest présent, il peut décider de l’appeler même pour une question métier.
Le program.cs devient donc:
// ... code
.WithTools<TranslogTools>();Modélisation DB2 for i : schéma métier et tables relationnelles
Translog est une société fictive de transport et logistique. Elle gère des tournées de livraison, affecte des chauffeurs, suit ses expéditions client et traite les incidents en cours de route.
Pour modéliser ce métier, on crée un schéma DB2 for i baptisé TLOG. Il contient cinq tables liées entre elles:
| Table | Rôle |
|---|---|
TLOG.CUSTOMERS |
Clients avec segment commercial (PREMIUM / STANDARD / PROSPECT) |
TLOG.DRIVERS |
Chauffeurs et type de permis |
TLOG.ROUTES |
Tournées de livraison avec chauffeur affecté |
TLOG.SHIPMENTS |
Expéditions rattachées à un client et une tournée |
TLOG.INCIDENTS |
Incidents ouverts sur les expéditions |
Le statut des expéditions SHIPMENTS.STATUS suit un cycle de vie précis:
PREPARING → IN_TRANSIT → DELIVERED avec des bifurcations possibles vers INCIDENT ou CANCELLED.
C’est ce champs que Claude interrogera pour filtrer les données.
INCIDENTS est une table séparée, liée à une expédition. Un incident a un type ( DELAY, DAMAGE, ADDRESS...), et un flag RESOLVED qui passera de 0 à 1 lors de notre démo d’écriture.
Enfin les tournées ROUTES sont liées aux chauffeurs DRIVERS et aux expéditions. Une expédition peut exister sans tournée assignée si elle est sous statut PREPARING. Ce détail aura son importance lors des jointures.
Le script SQL complet de création du schéma et des données fictives est disponible ici.
Conception des tools MCP optimisés DB2 for i
Avant d’écrire le moindre tool, une question se pose: jusqu’où faire travailler .NET, et jusqu’où faire travailler DB2 ?
La réponse est simple pour quiconque connaît l’IBM i : DB2 for i est conçu pour exécuter ce type d’opérations. Jointures, agrégats, fonctions de date, regroupements… ce sont des traitements optimisés au niveau du moteur SQL. Rapatrier des lignes brutes pour les traiter côté .NET reviendrait à contourner des décennies d’optimisations intégrées au moteur relationnel.
Chaque tool développé ci-dessous applique ce principe. On ne remonte que ce dont Claude a besoin, et on laisse DB2 for i travailler pour lui.
1 . Get Dashboard
Premier tool:
Chaque requête retourne un seul chiffre, un COUNT ou un SUM et .NET ne reçoit que le résultat final. COALESCE évite de retourner un NULL si aucune expédition n’existe sur la période.
[McpServerTool]
[Description("Returns a summary of today's activity: shipments in transit, shipments being prepared, open incidents, active routes, and revenue for the current month vs previous month.")]
public async Task<object> GetDashboard()
{
return new
{
ShipmentsInTransit = await _conn.ExecuteScalarAsync("SELECT COUNT(*) FROM TLOG.SHIPMENTS WHERE STATUS = 'IN_TRANSIT'"),
ShipmentsBeingPrepared = await _conn.ExecuteScalarAsync("SELECT COUNT(*) FROM TLOG.SHIPMENTS WHERE STATUS = 'PREPARING'"),
OpenIncidents = await _conn.ExecuteScalarAsync("SELECT COUNT(*) FROM TLOG.INCIDENTS WHERE RESOLVED = '0'"),
ActiveRoutes = await _conn.ExecuteScalarAsync("SELECT COUNT(*) FROM TLOG.ROUTES WHERE STATUS = 'IN_PROGRESS'"),
RevenueCurrentMonth = await _conn.ExecuteScalarAsync(@"
SELECT COALESCE(SUM(AMOUNT), 0)
FROM TLOG.SHIPMENTS
WHERE YEAR(ORDER_DATE) = YEAR(CURRENT DATE)
AND MONTH(ORDER_DATE) = MONTH(CURRENT DATE)"),
RevenuePreviousMonth = await _conn.ExecuteScalarAsync(@"
SELECT COALESCE(SUM(AMOUNT), 0)
FROM TLOG.SHIPMENTS
WHERE YEAR(ORDER_DATE) = YEAR(CURRENT DATE - 1 MONTH)
AND MONTH(ORDER_DATE) = MONTH(CURRENT DATE - 1 MONTH)")
};
}
💬 Prompt Sylvie
“ Donne moi un état de l’activité du jour ”

2 . GetTopCustomers (jointure, GROUP BY et paramètre)
Cette méthode retourne les N meilleurs clients par chiffre d’affaires sur les 30 derniers jours.
Elle effectue une jointure entre TLOG.CUSTOMERS et TLOG.SHIPMENTS afin d’associer chaque expédition à son client, et agrège les données par client (COUNT, SUM), puis trie le résultat par revenu décroissant.
La clause FETCH FIRST {top} ROWS ONLY limite le nombre de lignes retournées.
Côté .NET, on reçoit ainsi une liste déjà agrégée et prête à être affichée.
// TOP CUSTOMERS
[McpServerTool]
[Description("Returns the top N customers by revenue over the last 30 days, with their shipment count, total amount, city and segment.")]
public IEnumerable GetTopCustomers(
[Description("Number of customers to return (e.g. 5)")] int top)
{
// On joint les clients à leurs expéditions des 30 derniers jours
// On laisse DB2 calculer le CA et le nombre d'envois par client.
// FETCH FIRST {top} ROWS ONLY : seuls les {top} meilleurs remontent côté .NET.
return _conn.Query($@"
SELECT
c.CUSTOMER_ID,
c.COMPANY_NAME,
c.CITY,
c.SEGMENT,
COUNT(s.SHIPMENT_ID) AS SHIPMENT_COUNT,
SUM(s.AMOUNT) AS REVENUE_30D
FROM TLOG.CUSTOMERS c
JOIN TLOG.SHIPMENTS s
ON c.CUSTOMER_ID = s.CUSTOMER_ID
AND s.ORDER_DATE >= CURRENT DATE - 30 DAYS
GROUP BY
c.CUSTOMER_ID,
c.COMPANY_NAME,
c.CITY,
c.SEGMENT
ORDER BY REVENUE_30D DESC
FETCH FIRST {top} ROWS ONLY
");
}
💬 Prompt Sylvie
“ Quels sont mes 5 meilleurs clients par chiffre d'affaires ce mois-ci ? ”

3 . GetTopShipmentsByStatus (INNER JOIN vs LEFT JOIN)
Cette méthode retourne les expéditions filtrées par statut, avec les informations client et tournée associées.
Chaque expédition est obligatoirement liée à un client: on utilise donc INNER JOIN avec CUSTOMERS.
En revanche, une expédition en statut PREPARING peut ne pas encore être affectée à une tournée. Donc pour éviter de perdre ces lignes dans le résultat, la jointure avec ROUTES est effectuée avec LEFT JOIN.
// SHIPMENTS BY STATUS
[McpServerTool]
[Description("Returns shipments filtered by status, with customer name, segment, assigned route and region. Allowed values: PREPARING, IN_TRANSIT, DELIVERED, INCIDENT, CANCELLED.")]
public IEnumerable GetShipmentsByStatus(
[Description("Shipment status to filter on")] string status)
{
// On joint chaque expédition à son client (INNER JOIN, obligatoire)
// Et à sa tournée (LEFT JOIN : une expédition en PREPARING n'en a pas encore).
// DB2 résout la jointure et filtre par statut, et .NET reçoit uniquement les lignes utiles.
return _conn.Query(@"
SELECT
s.SHIPMENT_ID,
s.ORDER_DATE,
s.EXPECTED_DATE,
s.DEST_CITY,
s.WEIGHT_KG,
s.AMOUNT,
s.STATUS,
c.COMPANY_NAME,
c.SEGMENT,
r.LABEL AS ROUTE_LABEL,
r.REGION
FROM TLOG.SHIPMENTS s
JOIN TLOG.CUSTOMERS c ON s.CUSTOMER_ID = c.CUSTOMER_ID
LEFT JOIN TLOG.ROUTES r ON s.ROUTE_REF = r.ROUTE_REF
WHERE s.STATUS = @status
ORDER BY s.ORDER_DATE DESC
",
new { status });
}
💬 Prompt Sylvie
“ Peux-tu m'afficher des détails sur les incidents en cours ? ”

Écriture en base DB2 for i depuis un agent IA avec confirmation utilisateur
1 . ResolveIncident()
Cette méthode clôture un incident en base DB2 for i.
Elle exécute un UPDATE sur TLOG.INCIDENTS pour passer RESOLVED à 1 et renseigner RESOLVED_AT avec CURRENT DATE, évalué côté IBM i.
La méthode retourne un message indiquant si la ligne a bien été mise à jour ou non.
À noter que le mécanisme de confirmation utilisateur n’est pas codé en C#, mais mais défini dans la description du tool. C’est cette description qui contraint l’agent à demander une validation explicite avant toute écriture en base.
// UPDATE - RESOLVE INCIDENT
[McpServerTool]
[Description(@"Marks an incident as resolved and sets today as the resolution date.
IMPORTANT: this tool writes to the database.
Before calling it, you MUST:
1. If you only have a shipment ID, call GetShipmentsByStatus first to retrieve the associated incident ID.
2. Warn the user that you are about to modify data in the IBM i database.
3. Show them the shipment ID and the customer name: not the technical incident ID.
4. Ask for explicit confirmation before proceeding.
Only proceed after receiving a clear approval.")]
public string ResolveIncident(
[Description("Incident identifier to close (e.g. INC-0001)")] string incidentId)
{
var rows = _conn.Execute(@"
UPDATE TLOG.INCIDENTS
SET
RESOLVED = '1',
RESOLVED_AT = CURRENT DATE
WHERE INCIDENT_ID = @incidentId
",
new { incidentId });
return rows > 0
? $"Incident {incidentId} marked as resolved"
: $"Incident not found: {incidentId}";
}
Maintenant, revenons à Sylvie. Elle voit dans le dashboard précédent que SHP-100009 (Metal Concept, 3 jours de retard, client PREMIUM) est en incident critique. Le transporteur vient de la rappeler: la livraison est faite ce matin, elle veut donc clôturer l’incident.
💬 Prompt Sylvie
“ L'incident sur SHP-100009 est réglé, le transporteur a livré ce matin. Peux-tu le clôturer ? ”
Claude avance par étape. Il commence par appeler GetShipmentsByStatus pour retrouver l’identifiant technique de l’incident lié à SHP-100009. Puis il s’arrête et présente un récapitulatif à Sylvie:

Sylvie répond “ oui ”. Claude appelle ResolveIncident et la mise à jour SQL est exécutée sur DB2 for i.

Après clôture, Sylvie demande à vérifier si tout est ok:
💬 Prompt Sylvie
“ Affiche-moi les incidents en cours ”
Le dashboard se régénère.
SHP-100009 a disparu de la liste des incidents actifs. Une bannière verte en haut confirme la clôture effectuée ce jour.

L'écriture en base IBM i, déclenchée depuis une question en langage naturel, confirmée par l'utilisateur, et vérifiable immédiatement via un nouveau rendu visuel, ou directement sur ACS:

Sécurité IBM i et contrôle des droits dans un serveur MCP
Deux points importants:
- Les droits du profil IBM i s'appliquent. Si la connexion NTi utilise un profil en lecture seule, toute tentative d’écriture échouera au niveau DB2.
- L’agent ne peut faire que ce que les tools exposent. Il n’a pas accès à DB2, et ne construit pas ses propres requêtes SQL. Le périmètre d’action est celui que vous définissez, tool par tool.
Il est par ailleurs possible de contrôler précisément quels outils Claude est autorisé à utiliser, directement depuis l'onglet Connecteurs de Claude Desktop.
Cela permet de restreindre l'accès à certains tools selon le contexte ou l'utilisateur.

Conseil pratique: commencez par un profil IBM i dédié, lecture seule, sur les tables concernées. Ajoutez les droits d'écriture progressivement, table par table, en testant chaque tool individuellement.
Configurer le rendu HTML (IBM Carbon Design System) dans Claude Desktop
Les captures de cet article montrent un rendu Carbon Design System d’IBM, sans que Sylvie ait précisé quoi que ce soit dans ses prompts.
Ce comportement est configuré une seule fois dans Paramètres → Profil de Claude Desktop

Cette instruction s'applique à toutes les conversations.
Peu importe comment Sylvie formule sa question, en français, en anglais, avec des fautes de frappe… Claude comprend l'intention, appelle les tools appropriés et génère automatiquement le rendu.
Avec cet épisode, Sylvie dispose désormais d’un assistant capable d’interroger l’IBM i, de croiser des données métier et de clôturer un incident en base, le tout depuis Claude Desktop en langage naturel.
Tout repose sur quatre tools, une connexion NTi et des descriptions bien écrites, c’est tout !
Quentin Destrade