Tutoriel · MCP · TypeScript

Ressources et prompts MCP en TypeScript

Au-delà des outils : exposer des documents par URI, des gabarits paramétrés et des prompts réutilisables, et savoir qui, de l’utilisateur ou du modèle, les déclenche.

Trois primitives, trois déclencheurs

Le précédent article portait sur les outils. Le protocole définit deux autres primitives, qui se distinguent surtout par qui décide de les utiliser :

  • un outil est déclenché par le modèle, au fil de son raisonnement ;
  • une ressource est choisie par l’application ou l’utilisateur, qui la joint au contexte, comme on attache un fichier ;
  • un prompt est lancé par l’utilisateur, souvent depuis un menu ou une commande /.

Pour le support fictif d’Acme Assurances, la base de connaissances devient des ressources, et la rédaction d’une réponse type devient un prompt.

Une ressource statique

Une ressource est identifiée par une URI, dont le schéma est libre (kb://, file://, https://). Elle renvoie un ou plusieurs contenus, texte ou binaire encodé en base64, avec leur type MIME.

import { readFile } from "node:fs/promises";

server.registerResource(
  "charte-reponse",
  "kb://acme/charte-reponse",
  {
    title: "Charte de réponse du support",
    description: "Ton, formules et engagements à respecter dans toute réponse à un client Acme.",
    mimeType: "text/markdown",
  },
  async (uri) => ({
    contents: [{ uri: uri.href, mimeType: "text/markdown", text: await readFile("kb/charte.md", "utf8") }],
  })
);

Le client liste les ressources disponibles et lit celles qu’on lui demande. Rien n’est envoyé au modèle tant que l’application ou l’utilisateur ne l’a pas choisi : c’est ce qui distingue une ressource d’un outil de lecture.

Un gabarit d’URI

Pour exposer une collection, on ne déclare pas cent ressources : un ResourceTemplate décrit une famille d’URI avec une variable, au format des gabarits d’URI (RFC 6570).

import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";

server.registerResource(
  "fiche-kb",
  new ResourceTemplate("kb://acme/fiches/{slug}", {
    // list : permet au client de proposer les fiches disponibles.
    list: async () => ({
      resources: (await kb.lister()).map((f) => ({
        uri: `kb://acme/fiches/${f.slug}`,
        name: f.titre,
        mimeType: "text/markdown",
      })),
    }),
    // complete : autocomplétion de la variable pendant la saisie.
    complete: { slug: async (debut) => (await kb.slugs()).filter((s) => s.startsWith(debut)) },
  }),
  { title: "Fiche de la base de connaissances", description: "Procédure du support, par identifiant." },
  async (uri, { slug }) => {
    const fiche = await kb.lire(String(slug));
    if (!fiche) throw new Error(`Fiche inconnue : ${slug}`);
    return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: fiche.markdown }] };
  }
);

Le callback list permet au client de proposer les fiches existantes, et complete d’autocompléter la variable pendant la saisie. Le handler reçoit l’URI et les variables extraites.

Ici l’absence de fiche lève une exception : une lecture de ressource n’est pas un appel d’outil, il n’existe pas de résultat isError à interpréter par le modèle, et le client affiche l’erreur à l’utilisateur.

Un prompt paramétré

Un prompt est un gabarit de conversation : il prend des arguments et renvoie une liste de messages prêts à être envoyés au modèle. Il peut y intégrer une ressource, ici la charte, pour que le modèle l’ait sous les yeux.

server.registerPrompt(
  "repondre_ticket",
  {
    title: "Rédiger la réponse à un ticket",
    description: "Prépare une réponse au client à partir du ticket et de la charte du support.",
    argsSchema: {
      ticketId: z.string().describe("Identifiant du ticket, ex. : T-1003"),
      ton: z.enum(["formel", "chaleureux"]).optional(),
    },
  },
  async ({ ticketId, ton }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "resource",
          resource: { uri: "kb://acme/charte-reponse", mimeType: "text/markdown", text: await kb.charte() },
        },
      },
      {
        role: "user",
        content: {
          type: "text",
          text:
            `Lis le ticket ${ticketId} avec l'outil rechercher_tickets, puis rédige une réponse ` +
            `au ton ${ton ?? "formel"} qui respecte la charte ci-dessus. Ne promets aucun délai ` +
            `absent de la charte. Termine par la liste des points à vérifier avant envoi.`,
        },
      },
    ],
  })
);

Dans un client compatible, l’utilisateur choisit « Rédiger la réponse à un ticket », saisit T-1003, et l’agent démarre avec une consigne complète et constante d’une personne à l’autre. Le prompt peut s’appuyer sur les outils du même serveur : il les nomme, le modèle les appelle.

Les arguments d’un prompt arrivent sous forme de chaînes : on s’en tient à des chaînes et des énumérations, pas à des nombres ou des objets.

Outil, ressource ou prompt ?

  • Le modèle doit pouvoir aller chercher l’information seul, au moment où il en a besoin : un outil, même en lecture.
  • L’information est un document que l’utilisateur veut joindre explicitement, ou que l’application met en cache et indexe : une ressource.
  • Il s’agit d’un flux de travail récurrent qu’un humain lance : un prompt.

Le support des ressources et des prompts varie selon les clients : certains n’exposent que les outils. Une information indispensable mérite donc aussi un outil de lecture, la ressource apportant le confort en plus.

Bonnes pratiques

  • URI stables : un client peut les mémoriser ; changer le schéma casse ses références.
  • Notifications : quand une ressource change, le serveur peut notifier les clients abonnés, qui la relisent au lieu de garder une version périmée.
  • Contenu maîtrisé : une fiche rédigée par un tiers peut contenir des consignes cachées. Le risque est le même que pour tout contenu lu par un agent, voir la prompt injection.
  • Prompts courts et explicites : ce sont des consignes, avec les mêmes règles qu’un skill : un objectif, des limites et le résultat attendu.

Le serveur tourne pour l’instant sur le poste de chacun. Le prochain article le transforme en service partagé.

🍪 Gestion des cookies
Ce site utilise des cookies pour mesurer l'audience (Google Analytics) et lire des vidéos (YouTube). Vous pouvez personnaliser votre choix.
Mentions légales
Réglages des cookies
Essentiels Nécessaires au fonctionnement du site — ne peuvent pas être désactivés.
Google Analytics Analyse d'audience (pages vues, sources de trafic). Aucune donnée personnelle identifiante.
YouTube Permet de lire les vidéos YouTube intégrées sur cette page.