Quando Usar
Use esta habilidade sempre que estiver definindo, editando ou validando ferramentas, recursos ou prompts em um servidor MCP NitroStack.
Definindo Ferramentas com @Tool
Uma ferramenta MCP expõe uma função que um cliente de IA pode invocar. Decore um método de serviço ou controlador com @Tool.
Principais Opções da Ferramenta:
name: Identificador único em kebab-case ou snake_case.description: Descrição detalhada explicando quando e como o cliente deve usá-la.inputSchema: Um esquema de objeto Zod para validação rigorosa das entradas.outputSchema(opcional): Esquema Zod validando a estrutura de saída.
import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core';
@Controller('weather')
export class WeatherService {
@Tool({
name: 'get_current_weather',
description: 'Obtenha a previsão do tempo atual para uma cidade específica.',
inputSchema: z.object({
city: z.string().describe('O nome da cidade, ex: São Paulo'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
})
@InitialTool() // Invocado automaticamente quando o cliente de IA inicializa/inicia
async getWeather(
input: { city: string; unit: 'celsius' | 'fahrenheit' },
ctx: ExecutionContext
) {
ctx.logger.info(`Buscando clima para ${input.city}`);
// implementação
return {
city: input.city,
temp: 22,
condition: 'Ensolarado',
};
}
}
Definindo Recursos com @Resource
Um recurso MCP expõe arquivos/URIs de dados estáticos ou dinâmicos que o cliente de IA pode ler.
Principais Opções do Recurso:
uri: Padrão de URI (ex:git://{owner}/{repo}/fileou estáticoapp://config).name: Nome único do recurso.description: Explicação sobre quais dados este recurso fornece.mimeType: Tipo MIME da resposta (ex:text/plain,application/json).
import { Resource, ExecutionContext } from '@nitrostack/core';
export class ConfigResources {
@Resource({
uri: 'app://settings',
name: 'Configurações da Aplicação',
description: 'Configurações e parâmetros de todo o sistema.',
mimeType: 'application/json',
})
async getSettings(ctx: ExecutionContext) {
return {
environment: 'development',
debugMode: true,
};
}
}
Definindo Prompts com @Prompt
Um prompt MCP expõe modelos reutilizáveis ou conjuntos de instruções que guiam LLMs.
Principais Opções do Prompt:
name: Nome do prompt.description: Descreve qual tarefa este prompt ajuda a realizar.arguments: Declara parâmetros que o cliente pode fornecer para personalizar o modelo do prompt.
import { Prompt, ExecutionContext } from '@nitrostack/core';
export class PromptTemplates {
@Prompt({
name: 'code_review',
description: 'Forneça uma revisão de código intensiva para um trecho de código fornecido.',
arguments: [
{ name: 'language', description: 'A linguagem de programação, ex: TypeScript', required: true },
{ name: 'code', description: 'O trecho de código a ser revisado', required: true },
],
})
async getCodeReviewPrompt(
args: { language: string; code: string },
ctx: ExecutionContext
) {
return {
messages: [
{
role: 'user',
content: `Você é um engenheiro de software especialista. Revise este código ${args.language}:
${args.code}`,
},
],
};
}
}
Políticas de Ferramentas: Cache (@Cache) e Limitação de Taxa (@RateLimit)
Você pode controlar comportamentos de execução de ferramentas (como otimização de desempenho e limitação) usando decoradores de método.
1. Cache com @Cache
Use @Cache para armazenar em cache as saídas de execução de ferramentas por uma duração especificada (TTL em segundos). Isso reduz a sobrecarga de banco de dados ou API para solicitações idênticas frequentes.
Opções:
ttl: Tempo de vida do cache em segundos (obrigatório).key(opcional): Função personalizada(input: any, context?: any) => stringque retorna uma chave de cache única com base nas entradas. Se não definida, uma chave é gerada automaticamente a partir dos argumentos de entrada serializados.
Exemplo:
import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core';
export class StationTools {
@Tool({
name: 'get_system_status',
description: 'Busca métricas da estação em tempo real. A resposta é armazenada em cache.',
inputSchema: z.object({}),
})
@Cache({ ttl: 60 }) // Armazena em cache o status por 60 segundos
async getSystemStatus() {
return { temperature: 21.5, oxygen: 0.98 };
}
@Tool({
name: 'get_crew_status',
description: 'Busca o status de um membro da tripulação. Armazenado em cache pelo ID da tripulação.',
inputSchema: z.object({ id: z.string() }),
})
@Cache({
ttl: 300,
key: (input) => `crew:status:${input.id}`
})
async getCrewStatus(input: { id: string }) {
// ...
}
}
2. Limitação de Taxa com @RateLimit
Use @RateLimit para restringir o número de invocações de ferramentas dentro de uma janela de tempo especificada para evitar abusos do cliente.
Opções:
requests: Número de solicitações permitidas na janela (obrigatório).window: Duração da janela de limitação (obrigatório). Suporta formatos como'1s','1m','1h'.key(opcional): Função personalizada(context: ExecutionContext) => stringpara agrupar limites de taxa. Útil para limitar a taxa por função de usuário ou chave de API.
Exemplo:
import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core';
export class DiagnosticTools {
@Tool({
name: 'run_deep_diagnostic',
description: 'Executa diagnósticos intensivos. Com limite de taxa.',
inputSchema: z.object({}),
})
@RateLimit({ requests: 3, window: '1m' }) // Máximo de 3 requisições por minuto globalmente
async runDeepDiagnostic() {
return { diagnosticReport: 'Todos os sistemas operacionais.' };
}
@Tool({
name: 'request_supply_drop',
description: 'Solicita suprimentos de inventário. Com limite de taxa por usuário.',
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) {
// ...
}
}
Manipulando Uploads de Arquivos em Ferramentas
O NitroStack suporta uploads de arquivos de clientes MCP (como o NitroStudio) passando o arquivo como uma string codificada em base64 dentro dos parâmetros de entrada de uma ferramenta.
1. Declarando Esquema de Entrada para Uploads de Arquivos
Para aceitar um arquivo enviado, defina três campos Zod no inputSchema da sua ferramenta:
file_name: O nome do arquivo (ex:relatorio.csv).file_type: O tipo MIME (ex:text/csv).file_content: A string codificada em base64 contendo os dados do arquivo.
import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core';
export class FileTools {
@Tool({
name: 'upload_document',
description: 'Envie um documento de texto ou imagem.',
inputSchema: z.object({
file_name: z.string().describe('Nome do arquivo enviado'),
file_type: z.string().describe('Tipo MIME do arquivo enviado'),
file_content: z.string().describe('Conteúdo do arquivo codificado em base64'),
})
})
async uploadDocument(input: any, ctx: ExecutionContext) {
// Lógica de processamento
}
}
2. Decodificando Payloads Base64
Uploads de arquivos podem chegar em dois formatos, dependendo do cliente:
- Formato Data URL:
data:image/png;base64,iVBORw0KGgo... - Formato Raw Base64:
iVBORw0KGgo...
Use o seguinte padrão de decodificador universal para analisar qualquer formato em um Buffer do 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 Data URL - decodifica matches[2]
return Buffer.from(matches[2], 'base64');
} else {
// Formato Raw base64 - decodifica a entrada diretamente
return Buffer.from(content, 'base64');
}
}
3. Exemplo Seguro de Salvamento de Arquivo
Sempre valide os caminhos de diretório para prevenir ataques de path traversal ao salvar arquivos em 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 e salva um arquivo enviado de forma segura.',
inputSchema: z.object({
file_name: z.string().describe('Nome do arquivo enviado'),
file_type: z.string().describe('Tipo MIME do arquivo enviado'),
file_content: z.string().describe('Conteúdo do arquivo codificado em base64'),
})
})
async saveFile(input: any, ctx: ExecutionContext) {
// Garante que o diretório de uploads existe
if (!fs.existsSync(UPLOAD_DIR)) {
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}
// Caminho de destino seguro 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('Caminho de arquivo inválido detectado (path traversal).');
}
// Decodifica e escreve em disco
const buffer = decodeBase64File(input.file_content);
fs.writeFileSync(filePath, buffer);
ctx.logger.info(`Arquivo salvo com sucesso: ${safeName}`);
return { success: true, path: filePath };
}
}