Quand l'utiliser
Utilisez cette compétence chaque fois que vous définissez, modifiez ou validez des outils, ressources ou invites sur un serveur MCP NitroStack.
Définir des outils avec @Tool
Un outil MCP expose une fonction qu'un client IA peut invoquer. Décorez une méthode de service ou de contrôleur avec @Tool.
Options clés des outils :
name: Identifiant unique en kebab-case ou snake_case.description: Description détaillée expliquant quand et comment le client doit l'utiliser.inputSchema: Un schéma d'objet Zod pour une validation stricte des entrées.outputSchema(optionnel) : Schéma Zod validant la structure de sortie.
import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core';
@Controller('weather')
export class WeatherService {
@Tool({
name: 'get_current_weather',
description: 'Obtenez les prévisions météorologiques actuelles pour une ville spécifique.',
inputSchema: z.object({
city: z.string().describe('Le nom de la ville, ex: San Francisco'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
})
@InitialTool() // Invoqué automatiquement lors de l'initialisation/démarrage du client IA
async getWeather(
input: { city: string; unit: 'celsius' | 'fahrenheit' },
ctx: ExecutionContext
) {
ctx.logger.info(`Récupération de la météo pour ${input.city}`);
// implémentation
return {
city: input.city,
temp: 22,
condition: 'Ensoleillé',
};
}
}
Définir des ressources avec @Resource
Une ressource MCP expose des fichiers/URI de données statiques ou dynamiques que le client IA peut lire.
Options clés des ressources :
uri: Modèle d'URI (ex:git://{owner}/{repo}/fileou statiqueapp://config).name: Nom unique de la ressource.description: Explication des données fournies par cette ressource.mimeType: Type MIME de la réponse (ex:text/plain,application/json).
import { Resource, ExecutionContext } from '@nitrostack/core';
export class ConfigResources {
@Resource({
uri: 'app://settings',
name: 'Paramètres de l\'application',
description: 'Paramètres et configurations à l\'échelle du système.',
mimeType: 'application/json',
})
async getSettings(ctx: ExecutionContext) {
return {
environment: 'development',
debugMode: true,
};
}
}
Définir des invites avec @Prompt
Une invite MCP expose des modèles réutilisables ou des ensembles d'instructions qui guident les LLM.
Options clés des invites :
name: Nom de l'invite.description: Décrit la tâche que cette invite aide à accomplir.arguments: Déclare les paramètres que le client peut fournir pour personnaliser le modèle d'invite.
import { Prompt, ExecutionContext } from '@nitrostack/core';
export class PromptTemplates {
@Prompt({
name: 'code_review',
description: 'Fournissez une revue de code approfondie pour un extrait de code donné.',
arguments: [
{ name: 'language', description: 'Le langage de programmation, ex: TypeScript', required: true },
{ name: 'code', description: 'L\'extrait de code à examiner', required: true },
],
})
async getCodeReviewPrompt(
args: { language: string; code: string },
ctx: ExecutionContext
) {
return {
messages: [
{
role: 'user',
content: `Vous êtes un ingénieur logiciel expert. Examinez ce code ${args.language} :\n\n${args.code}`,
},
],
};
}
}
Politiques des outils : Mise en cache (@Cache) et Limitation de débit (@RateLimit)
Vous pouvez contrôler les comportements d'exécution des outils (comme l'optimisation des performances et la limitation) à l'aide de décorateurs de méthode.
1. Mise en cache avec @Cache
Utilisez @Cache pour mettre en cache les sorties d'exécution d'un outil pendant une durée spécifiée (TTL en secondes). Cela réduit la surcharge des requêtes DB ou API pour des requêtes identiques fréquentes.
Options :
ttl: Durée de vie du cache en secondes (obligatoire).key(optionnel) : Fonction personnalisée(input: any, context?: any) => stringqui retourne une clé de cache unique basée sur les entrées. Si non définie, une clé est générée automatiquement à partir des arguments d'entrée sérialisés.
Exemple :
import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core';
export class StationTools {
@Tool({
name: 'get_system_status',
description: 'Récupère les métriques en temps réel de la station. La réponse est mise en cache.',
inputSchema: z.object({}),
})
@Cache({ ttl: 60 }) // Met en cache l'état pendant 60 secondes
async getSystemStatus() {
return { température: 21.5, oxygène: 0.98 };
}
@Tool({
name: 'get_crew_status',
description: 'Récupère l\'état d\'un membre d\'équipage. Mis en cache par ID d\'équipage.',
inputSchema: z.object({ id: z.string() }),
})
@Cache({
ttl: 300,
key: (input) => `crew:status:${input.id}`
})
async getCrewStatus(input: { id: string }) {
// ...
}
}
2. Limitation de débit avec @RateLimit
Utilisez @RateLimit pour restreindre le nombre d'invocations d'un outil dans une fenêtre de temps spécifiée, afin d'éviter les abus du client.
Options :
requests: Nombre de requêtes autorisées dans la fenêtre (obligatoire).window: Durée de la fenêtre de limitation (obligatoire). Supporte des formats comme'1s','1m','1h'.key(optionnel) : Fonction personnalisée(context: ExecutionContext) => stringpour regrouper les limites de débit. Utile pour limiter le débit par rôle utilisateur ou clé API.
Exemple :
import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core';
export class DiagnosticTools {
@Tool({
name: 'run_deep_diagnostic',
description: 'Exécute un diagnostic intensif. Limité en débit.',
inputSchema: z.object({}),
})
@RateLimit({ requests: 3, window: '1m' }) // Maximum 3 requêtes par minute globalement
async runDeepDiagnostic() {
return { diagnosticReport: 'Tous les systèmes sont opérationnels.' };
}
@Tool({
name: 'request_supply_drop',
description: 'Demande des fournitures d\'inventaire. Limité en débit par utilisateur.',
inputSchema: z.object({ item: z.string() }),
})
@RateLimit({
requests: 5,
window: '1h',
key: (ctx: ExecutionContext) => ctx.auth?.subject || 'anonymous'
})
async requestSupply(input: { item: string }, ctx: ExecutionContext) {
// ...
}
}
Gestion des téléchargements de fichiers dans les outils
NitroStack supporte les téléchargements de fichiers depuis les clients MCP (comme NitroStudio) en passant le fichier sous forme de chaîne encodée en base64 dans les paramètres d'entrée d'un outil.
1. Déclaration du schéma d'entrée pour les téléchargements de fichiers
Pour accepter un fichier téléchargé, définissez trois champs Zod dans votre inputSchema de l'outil :
file_name: Le nom du fichier (ex:rapport.csv).file_type: Le type MIME (ex:text/csv).file_content: La chaîne encodée en base64 contenant les données du fichier.
import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core';
export class FileTools {
@Tool({
name: 'upload_document',
description: 'Téléchargez un document texte ou une image.',
inputSchema: z.object({
file_name: z.string().describe('Nom du fichier téléchargé'),
file_type: z.string().describe('Type MIME du fichier téléchargé'),
file_content: z.string().describe('Contenu du fichier encodé en base64'),
})
})
async uploadDocument(input: any, ctx: ExecutionContext) {
// Logique de traitement
}
}
2. Décodage des charges utiles Base64
Les téléchargements de fichiers peuvent arriver dans deux formats selon le client :
- Format Data URL :
data:image/png;base64,iVBORw0KGgo... - Format Base64 brut :
iVBORw0KGgo...
Utilisez le modèle de décodeur universel suivant pour analyser l'un ou l'autre format en un Buffer Node :
import * as fs from 'fs';
import * as path from 'path';
function decodeBase64File(content: string): Buffer {
const matches = content.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
if (matches && matches.length === 3) {
// Format Data URL - décoder matches[2]
return Buffer.from(matches[2], 'base64');
} else {
// Format base64 brut - décoder directement l'entrée
return Buffer.from(content, 'base64');
}
}
3. Exemple de sauvegarde sécurisée de fichier
Validez toujours les chemins de répertoire pour éviter les attaques de traversée de répertoire lors de la sauvegarde des fichiers sur le disque.
import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core';
import * as fs from 'fs';
import * as path from 'path';
const UPLOAD_DIR = path.join(process.cwd(), 'uploads');
export class SecureUploadTools {
@Tool({
name: 'save_uploaded_file',
description: 'Décode et sauvegarde un fichier téléchargé de manière sécurisée.',
inputSchema: z.object({
file_name: z.string().describe('Nom du fichier téléchargé'),
file_type: z.string().describe('Type MIME du fichier téléchargé'),
file_content: z.string().describe('Contenu du fichier encodé en base64'),
})
})
async saveFile(input: any, ctx: ExecutionContext) {
// Assurez-vous que le répertoire uploads existe
if (!fs.existsSync(UPLOAD_DIR)) {
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}
// Chemin de destination sécurisé pour éviter la traversée de répertoire
const safeName = path.basename(input.file_name);
const filePath = path.join(UPLOAD_DIR, safeName);
if (!filePath.startsWith(UPLOAD_DIR)) {
throw new Error('Chemin de fichier invalide détecté (traversée de répertoire).');
}
// Décoder et écrire sur le disque
const buffer = decodeBase64File(input.file_content);
fs.writeFileSync(filePath, buffer);
ctx.logger.info(`Fichier sauvegardé avec succès : ${safeName}`);
return { success: true, path: filePath };
}
}