183 lines
7.3 KiB
JavaScript
183 lines
7.3 KiB
JavaScript
/**
|
|
* Outils MCP de lecture de données — partagés entre le serveur local (stdio,
|
|
* index.js) et le serveur distant (HTTP, http-server.js).
|
|
*
|
|
* Ce module ne contient AUCUNE logique de transport ni d'authentification :
|
|
* il reçoit un `apiGet(path, params)` déjà prêt à l'emploi (déjà lié à une
|
|
* clé API et une base URL précises) et enregistre les 6 outils de lecture
|
|
* sur le `McpServer` fourni. Ainsi, le comportement des outils ne peut pas
|
|
* diverger entre les deux serveurs — un seul endroit à maintenir.
|
|
*
|
|
* `crowdlending_fetch_url` n'est PAS ici : il reste réservé au serveur local
|
|
* (voir index.js) — l'exposer à des utilisateurs distants tiers via le
|
|
* serveur HTTP augmenterait le risque SSRF sans bénéfice pour ce cas d'usage.
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
|
|
const READ_ONLY_ANNOTATIONS = {
|
|
readOnlyHint: true,
|
|
destructiveHint: false,
|
|
idempotentHint: true,
|
|
openWorldHint: true,
|
|
};
|
|
|
|
/**
|
|
* Enregistre les 6 outils de lecture de données sur `server`.
|
|
*
|
|
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
* @param {object} ctx
|
|
* @param {(path: string, params?: object) => Promise<any>} ctx.apiGet
|
|
* Fonction d'appel à l'API v1, déjà authentifiée pour ce serveur/session.
|
|
* @param {(title: string) => string} [ctx.withLabel]
|
|
* Ajoute un libellé d'environnement au titre d'un outil (ex. "[PROD]").
|
|
* Par défaut : identité (pas de libellé).
|
|
* @param {(description: string) => string} [ctx.withSource]
|
|
* Ajoute la source de données en fin de description.
|
|
* Par défaut : identité (pas de source affichée).
|
|
*/
|
|
export function registerDataTools(server, { apiGet, withLabel = (t) => t, withSource = (d) => d }) {
|
|
server.registerTool(
|
|
'crowdlending_get_investisseur',
|
|
{
|
|
title: withLabel('Profil investisseur'),
|
|
description: withSource(
|
|
"Retourne le profil de l'investisseur associé à la clé API utilisée " +
|
|
"(nom, type famille/entreprise, régime fiscal). Utile pour savoir sur " +
|
|
"quel portefeuille portent les autres outils."),
|
|
inputSchema: {},
|
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
},
|
|
async () => {
|
|
try { return toolResult(await apiGet('/investisseur')); }
|
|
catch (e) { return toolError(e); }
|
|
},
|
|
);
|
|
|
|
server.registerTool(
|
|
'crowdlending_get_dashboard',
|
|
{
|
|
title: withLabel('Synthèse du portefeuille'),
|
|
description: withSource(
|
|
"Retourne les KPIs du portefeuille : nombre d'investissements, total " +
|
|
"investi, capital investi actuel (= capital restant dû sur les prêts en " +
|
|
"cours et en défaut, net des remboursements déjà perçus — équivalent au " +
|
|
"KPI \"Capital investi\" de l'app), capital en risque (sous-ensemble en " +
|
|
"retard/procédure), montant remboursé, intérêts bruts/nets perçus, " +
|
|
"capital reçu, total dépôts/retraits. Les montants d'investissements sont " +
|
|
"des soldes actuels (photo à aujourd'hui), pas des cumuls par période. " +
|
|
"Point d'entrée idéal pour une vue d'ensemble avant d'aller chercher le détail."),
|
|
inputSchema: {
|
|
annee: z.number().int().optional()
|
|
.describe("Filtre les intérêts/capital reçu sur une année (ex. 2026). Omettre pour le cumul total. N'affecte pas le capital investi/en risque, qui sont toujours des soldes actuels."),
|
|
},
|
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
},
|
|
async ({ annee }) => {
|
|
try { return toolResult(await apiGet('/dashboard', { annee })); }
|
|
catch (e) { return toolError(e); }
|
|
},
|
|
);
|
|
|
|
server.registerTool(
|
|
'crowdlending_list_investissements',
|
|
{
|
|
title: withLabel('Liste des investissements'),
|
|
description: withSource(
|
|
"Liste les investissements (prêts participatifs) du portefeuille : " +
|
|
"projet, émetteur, plateforme, montant investi, taux, durée, statut. " +
|
|
"Filtrable par statut. Ne renvoie pas les remboursements détaillés — " +
|
|
"utiliser crowdlending_get_investissement pour le détail d'un projet."),
|
|
inputSchema: {
|
|
statut: z.enum(['en_cours', 'rembourse', 'en_retard', 'procedure', 'cloture'])
|
|
.optional()
|
|
.describe("Filtrer par statut. Omettre pour lister tous les investissements."),
|
|
},
|
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
},
|
|
async ({ statut }) => {
|
|
try { return toolResult(await apiGet('/investissements', { statut })); }
|
|
catch (e) { return toolError(e); }
|
|
},
|
|
);
|
|
|
|
server.registerTool(
|
|
'crowdlending_get_investissement',
|
|
{
|
|
title: withLabel("Détail d'un investissement"),
|
|
description: withSource(
|
|
"Retourne le détail complet d'un investissement (identifié par son id, " +
|
|
"obtenu via crowdlending_list_investissements), y compris la liste de " +
|
|
"ses remboursements réels perçus (capital, intérêts bruts/nets, cashback)."),
|
|
inputSchema: {
|
|
id: z.number().int().positive().describe("Identifiant de l'investissement"),
|
|
},
|
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
},
|
|
async ({ id }) => {
|
|
try { return toolResult(await apiGet(`/investissements/${id}`)); }
|
|
catch (e) { return toolError(e); }
|
|
},
|
|
);
|
|
|
|
server.registerTool(
|
|
'crowdlending_list_remboursements',
|
|
{
|
|
title: withLabel('Liste des remboursements'),
|
|
description: withSource(
|
|
"Liste les remboursements réels perçus (toutes plateformes confondues), " +
|
|
"avec le nom du projet et de la plateforme. Filtrable par période."),
|
|
inputSchema: {
|
|
date_debut: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
|
|
.describe('Date de début au format YYYY-MM-DD (incluse)'),
|
|
date_fin: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
|
|
.describe('Date de fin au format YYYY-MM-DD (incluse)'),
|
|
},
|
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
},
|
|
async ({ date_debut, date_fin }) => {
|
|
try { return toolResult(await apiGet('/remboursements', { date_debut, date_fin })); }
|
|
catch (e) { return toolError(e); }
|
|
},
|
|
);
|
|
|
|
server.registerTool(
|
|
'crowdlending_list_depots_retraits',
|
|
{
|
|
title: withLabel('Liste des dépôts / retraits'),
|
|
description: withSource(
|
|
"Liste les mouvements de cash (dépôts et retraits) sur les plateformes " +
|
|
"de crowdlending, du plus récent au plus ancien."),
|
|
inputSchema: {},
|
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
},
|
|
async () => {
|
|
try { return toolResult(await apiGet('/depots-retraits')); }
|
|
catch (e) { return toolError(e); }
|
|
},
|
|
);
|
|
}
|
|
|
|
/** Formate le résultat d'un outil : texte JSON lisible + structuredContent.
|
|
* Le protocole MCP exige que `structuredContent` soit un objet JSON (pas un
|
|
* tableau brut) — les endpoints qui renvoient une liste sont enveloppés
|
|
* dans { items: [...] }. */
|
|
function toolResult(data) {
|
|
const structuredContent = Array.isArray(data) ? { items: data } : data;
|
|
return {
|
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
structuredContent,
|
|
};
|
|
}
|
|
|
|
/** Formate une erreur d'outil de façon à ce que l'agent comprenne quoi faire.
|
|
* Exporté pour être réutilisé tel quel par le serveur HTTP distant (mêmes
|
|
* messages d'erreur que le serveur local, pour ne pas dérouter l'agent
|
|
* selon le transport utilisé). */
|
|
export function toolError(e) {
|
|
return {
|
|
content: [{ type: 'text', text: `Erreur : ${e.message}` }],
|
|
isError: true,
|
|
};
|
|
}
|