Cuándo usar
Usa esta habilidad siempre que estés definiendo, editando o validando herramientas, recursos o prompts en un servidor MCP de NitroStack.
Definiendo herramientas con @Tool
Una herramienta MCP expone una función que un cliente de IA puede invocar. Decora un método de servicio o controlador con @Tool.
Opciones clave de herramienta:
name: Identificador único en kebab-case o snake_case.description: Descripción detallada que explica cuándo y cómo el cliente debe usarla.inputSchema: Un esquema de objeto Zod para una validación estricta de entradas.outputSchema(opcional): Esquema Zod que valida la estructura de salida.
import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core';
@Controller('weather')
export class WeatherService {
@Tool({
name: 'get_current_weather',
description: 'Obtén el pronóstico del tiempo actual para una ciudad específica.',
inputSchema: z.object({
city: z.string().describe('El nombre de la ciudad, p. ej., San Francisco'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
})
@InitialTool() // Se invoca automáticamente cuando el cliente de IA se inicializa/inicia
async getWeather(
input: { city: string; unit: 'celsius' | 'fahrenheit' },
ctx: ExecutionContext
) {
ctx.logger.info(`Obteniendo clima para ${input.city}`);
// implementación
return {
city: input.city,
temp: 22,
condition: 'Soleado',
};
}
}
Definiendo recursos con @Resource
Un recurso MCP expone archivos/URIs de datos estáticos o dinámicos que el cliente de IA puede leer.
Opciones clave de recurso:
uri: Patrón URI (p. ej.,git://{owner}/{repo}/fileo estáticoapp://config).name: Nombre único del recurso.description: Explicación de qué datos proporciona este recurso.mimeType: Tipo MIME de la respuesta (p. ej.,text/plain,application/json).
import { Resource, ExecutionContext } from '@nitrostack/core';
export class ConfigResources {
@Resource({
uri: 'app://settings',
name: 'Configuración de la aplicación',
description: 'Parámetros y configuraciones de todo el sistema.',
mimeType: 'application/json',
})
async getSettings(ctx: ExecutionContext) {
return {
environment: 'development',
debugMode: true,
};
}
}
Definiendo prompts con @Prompt
Un prompt MCP expone plantillas reutilizables o conjuntos de instrucciones que guían a los LLM.
Opciones clave de prompt:
name: Nombre del prompt.description: Describe qué tarea ayuda a realizar este prompt.arguments: Declara parámetros que el cliente puede proporcionar para personalizar la plantilla del prompt.
import { Prompt, ExecutionContext } from '@nitrostack/core';
export class PromptTemplates {
@Prompt({
name: 'code_review',
description: 'Proporciona una revisión de código exhaustiva para un fragmento de código dado.',
arguments: [
{ name: 'language', description: 'El lenguaje de programación, p. ej., TypeScript', required: true },
{ name: 'code', description: 'El fragmento de código a revisar', required: true },
],
})
async getCodeReviewPrompt(
args: { language: string; code: string },
ctx: ExecutionContext
) {
return {
messages: [
{
role: 'user',
content: `Eres un ingeniero de software experto. Revisa este código ${args.language}:\n\n${args.code}`,
},
],
};
}
}
Políticas de herramientas: Almacenamiento en caché (@Cache) y limitación de tasa (@RateLimit)
Puedes controlar los comportamientos de ejecución de herramientas (como la optimización de rendimiento y la limitación) usando decoradores de método.
1. Almacenamiento en caché con @Cache
Usa @Cache para almacenar en caché las salidas de ejecución de herramientas durante una duración específica (TTL en segundos). Esto reduce la sobrecarga de base de datos o API para solicitudes idénticas frecuentes.
Opciones:
ttl: Tiempo de vida de la caché en segundos (obligatorio).key(opcional): Función personalizada(input: any, context?: any) => stringque devuelve una clave de caché única basada en las entradas. Si no se define, se genera automáticamente una clave a partir de los argumentos de entrada serializados.
Ejemplo:
import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core';
export class StationTools {
@Tool({
name: 'get_system_status',
description: 'Obtiene métricas de la estación en tiempo real. La respuesta se almacena en caché.',
inputSchema: z.object({}),
})
@Cache({ ttl: 60 }) // Almacena en caché el estado durante 60 segundos
async getSystemStatus() {
return { temperature: 21.5, oxygen: 0.98 };
}
@Tool({
name: 'get_crew_status',
description: 'Obtiene el estado de un miembro de la tripulación. Almacenado en caché por ID de tripulación.',
inputSchema: z.object({ id: z.string() }),
})
@Cache({
ttl: 300,
key: (input) => `crew:status:${input.id}`
})
async getCrewStatus(input: { id: string }) {
// ...
}
}
2. Limitación de tasa con @RateLimit
Usa @RateLimit para restringir el número de invocaciones de herramientas dentro de una ventana de tiempo específica para evitar abusos del cliente.
Opciones:
requests: Número de solicitudes permitidas en la ventana (obligatorio).window: Duración de la ventana de limitación (obligatorio). Soporta formatos como'1s','1m','1h'.key(opcional): Función personalizada(context: ExecutionContext) => stringpara agrupar límites de tasa. Útil para limitar la tasa por rol de usuario o clave de API.
Ejemplo:
import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core';
export class DiagnosticTools {
@Tool({
name: 'run_deep_diagnostic',
description: 'Ejecuta diagnósticos intensivos. Con limitación de tasa.',
inputSchema: z.object({}),
})
@RateLimit({ requests: 3, window: '1m' }) // Máximo 3 solicitudes por minuto a nivel global
async runDeepDiagnostic() {
return { diagnosticReport: 'Todos los sistemas operativos.' };
}
@Tool({
name: 'request_supply_drop',
description: 'Solicita suministros de inventario. Con limitación de tasa por usuario.',
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) {
// ...
}
}
Manejo de cargas de archivos en herramientas
NitroStack admite la carga de archivos desde clientes MCP (como NitroStudio) mediante el paso del archivo como una cadena codificada en base64 dentro de los parámetros de entrada de una herramienta.
1. Declaración del esquema de entrada para cargas de archivos
Para aceptar un archivo cargado, define tres campos Zod en el inputSchema de tu herramienta:
file_name: El nombre del archivo (p. ej.report.csv).file_type: El tipo MIME (p. ej.text/csv).file_content: La cadena codificada en base64 que contiene los datos del archivo.
import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core';
export class FileTools {
@Tool({
name: 'upload_document',
description: 'Carga un documento de texto o una imagen.',
inputSchema: z.object({
file_name: z.string().describe('Nombre del archivo cargado'),
file_type: z.string().describe('Tipo MIME del archivo cargado'),
file_content: z.string().describe('Contenido del archivo codificado en base64'),
})
})
async uploadDocument(input: any, ctx: ExecutionContext) {
// Lógica de procesamiento
}
}
2. Decodificación de cargas en base64
Las cargas de archivos pueden llegar en dos formatos dependiendo del cliente:
- Formato de URL de datos:
data:image/png;base64,iVBORw0KGgo... - Formato base64 sin procesar:
iVBORw0KGgo...
Usa el siguiente patrón de decodificador universal para analizar cualquiera de los formatos en un Buffer de 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) {
// Formato de URL de datos - decodificar matches[2]
return Buffer.from(matches[2], 'base64');
} else {
// Formato base64 sin procesar - decodificar entrada directamente
return Buffer.from(content, 'base64');
}
}
3. Ejemplo de guardado seguro de archivos
Valida siempre las rutas de directorio para prevenir ataques de path traversal al guardar archivos en el disco.
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: 'Decodifica y guarda un archivo cargado de forma segura.',
inputSchema: z.object({
file_name: z.string().describe('Nombre del archivo cargado'),
file_type: z.string().describe('Tipo MIME del archivo cargado'),
file_content: z.string().describe('Contenido del archivo codificado en base64'),
})
})
async saveFile(input: any, ctx: ExecutionContext) {
// Asegura que el directorio de cargas exista
if (!fs.existsSync(UPLOAD_DIR)) {
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}
// Ruta de destino segura para prevenir path traversal
const safeName = path.basename(input.file_name);
const filePath = path.join(UPLOAD_DIR, safeName);
if (!filePath.startsWith(UPLOAD_DIR)) {
throw new Error('Se detectó una ruta de archivo no válida (path traversal).');
}
// Decodificar y escribir en disco
const buffer = decodeBase64File(input.file_content);
fs.writeFileSync(filePath, buffer);
ctx.logger.info(`Archivo guardado correctamente: ${safeName}`);
return { success: true, path: filePath };
}
}