사용 시기
이 스킬은 NitroStack MCP 서버에서 도구, 리소스 또는 프롬프트를 정의, 편집 또는 검증할 때마다 사용하세요.
@Tool로 도구 정의하기
MCP 도구는 AI 클라이언트가 호출할 수 있는 함수를 노출합니다. 서비스 또는 컨트롤러 메서드를 @Tool로 장식하세요.
주요 도구 옵션:
name: 케밥-케이스 또는 스네이크_케이스의 고유 식별자입니다.description: 클라이언트가 언제, 어떻게 사용해야 하는지 설명하는 상세한 설명입니다.inputSchema: 입력의 엄격한 검증을 위한 Zod 객체 스키마입니다.outputSchema(선택 사항): 출력 구조를 검증하는 Zod 스키마입니다.
import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core';
@Controller('weather')
export class WeatherService {
@Tool({
name: 'get_current_weather',
description: '특정 도시의 현재 일기 예보를 가져옵니다.',
inputSchema: z.object({
city: z.string().describe('도시 이름, 예: 서울'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
})
@InitialTool() // AI 클라이언트 초기화/시작 시 자동 호출
async getWeather(
input: { city: string; unit: 'celsius' | 'fahrenheit' },
ctx: ExecutionContext
) {
ctx.logger.info(`Fetching weather for ${input.city}`);
// 구현
return {
city: input.city,
temp: 22,
condition: 'Sunny',
};
}
}
@Resource로 리소스 정의하기
MCP 리소스는 AI 클라이언트가 읽을 수 있는 정적 또는 동적 데이터 파일/URI를 노출합니다.
주요 리소스 옵션:
uri: URI 패턴 (예:git://{owner}/{repo}/file또는 정적app://config).name: 리소스의 고유 이름입니다.description: 이 리소스가 제공하는 데이터에 대한 설명입니다.mimeType: 응답의 MIME 유형 (예:text/plain,application/json).
import { Resource, ExecutionContext } from '@nitrostack/core';
export class ConfigResources {
@Resource({
uri: 'app://settings',
name: '애플리케이션 설정',
description: '시스템 전체 구성 설정 및 매개변수.',
mimeType: 'application/json',
})
async getSettings(ctx: ExecutionContext) {
return {
environment: 'development',
debugMode: true,
};
}
}
@Prompt로 프롬프트 정의하기
MCP 프롬프트는 LLM을 안내하는 재사용 가능한 템플릿 또는 명령어 세트를 노출합니다.
주요 프롬프트 옵션:
name: 프롬프트의 이름입니다.description: 이 프롬프트가 어떤 작업을 수행하는 데 도움이 되는지 설명합니다.arguments: 클라이언트가 프롬프트 템플릿을 사용자 지정하기 위해 제공할 수 있는 매개변수를 선언합니다.
import { Prompt, ExecutionContext } from '@nitrostack/core';
export class PromptTemplates {
@Prompt({
name: 'code_review',
description: '주어진 코드 조각에 대한 집중적인 코드 리뷰를 제공합니다.',
arguments: [
{ name: 'language', description: '프로그래밍 언어, 예: TypeScript', required: true },
{ name: 'code', description: '리뷰할 코드 조각', required: true },
],
})
async getCodeReviewPrompt(
args: { language: string; code: string },
ctx: ExecutionContext
) {
return {
messages: [
{
role: 'user',
content: `당신은 전문 소프트웨어 엔지니어입니다. 다음 ${args.language} 코드를 리뷰하세요:\n\n${args.code}`,
},
],
};
}
}
도구 정책: 캐싱 (@Cache) 및 속도 제한 (@RateLimit)
메서드 데코레이터를 사용하여 도구 실행 동작(성능 최적화 및 제한 등)을 제어할 수 있습니다.
1. @Cache를 사용한 캐싱
@Cache를 사용하여 지정된 기간(TTL(초)) 동안 도구 실행 출력을 캐시합니다. 이는 빈번한 동일 요청에 대한 데이터베이스 또는 API 오버헤드를 줄입니다.
옵션:
ttl: 캐시 TTL(초) (필수).key(선택 사항): 입력을 기반으로 고유한 캐시 키를 반환하는 사용자 정의 함수(input: any, context?: any) => string. 정의되지 않으면 직렬화된 입력 인수에서 키가 자동 생성됩니다.
예시:
import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core';
export class StationTools {
@Tool({
name: 'get_system_status',
description: '실시간 스테이션 메트릭을 가져옵니다. 응답이 캐시됩니다.',
inputSchema: z.object({}),
})
@Cache({ ttl: 60 }) // 60초 동안 상태 캐시
async getSystemStatus() {
return { temperature: 21.5, oxygen: 0.98 };
}
@Tool({
name: 'get_crew_status',
description: '승무원의 상태를 가져옵니다. 승무원 ID별로 캐시됩니다.',
inputSchema: z.object({ id: z.string() }),
})
@Cache({
ttl: 300,
key: (input) => `crew:status:${input.id}`
})
async getCrewStatus(input: { id: string }) {
// ...
}
}
2. @RateLimit을 사용한 속도 제한
@RateLimit을 사용하여 지정된 시간 창 내에서 도구 호출 수를 제한하여 클라이언트 남용을 방지합니다.
옵션:
requests: 창에서 허용된 요청 수 (필수).window: 제한 시간 창 (필수).'1s','1m','1h'와 같은 형식을 지원합니다.key(선택 사항): 속도 제한을 그룹화하는 사용자 정의 함수(context: ExecutionContext) => string. 사용자 역할 또는 API 키별로 속도를 제한하는 데 유용합니다.
예시:
import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core';
export class DiagnosticTools {
@Tool({
name: 'run_deep_diagnostic',
description: '집중 진단을 실행합니다. 속도가 제한됩니다.',
inputSchema: z.object({}),
})
@RateLimit({ requests: 3, window: '1m' }) // 전역적으로 분당 최대 3회 요청
async runDeepDiagnostic() {
return { diagnosticReport: 'All systems operational.' };
}
@Tool({
name: 'request_supply_drop',
description: '인벤토리 보급품을 요청합니다. 사용자별로 속도가 제한됩니다.',
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) {
// ...
}
}
도구에서 파일 업로드 처리
NitroStack은 파일을 도구의 입력 매개변수 내에 Base64로 인코딩된 문자열로 전달하여 MCP 클라이언트(예: NitroStudio)의 파일 업로드를 지원합니다.
1. 파일 업로드를 위한 입력 스키마 선언
업로드된 파일을 수락하려면 도구의 inputSchema에 세 개의 Zod 필드를 정의하세요:
file_name: 파일 이름 (예:report.csv)file_type: MIME 유형 (예:text/csv)file_content: 파일 데이터를 포함하는 Base64로 인코딩된 문자열입니다.
import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core';
export class FileTools {
@Tool({
name: 'upload_document',
description: '텍스트 문서 또는 이미지를 업로드합니다.',
inputSchema: z.object({
file_name: z.string().describe('업로드된 파일의 이름'),
file_type: z.string().describe('업로드된 파일의 MIME 유형'),
file_content: z.string().describe('Base64로 인코딩된 파일 콘텐츠'),
})
})
async uploadDocument(input: any, ctx: ExecutionContext) {
// 처리 로직
}
}
2. Base64 페이로드 디코딩
파일 업로드는 클라이언트에 따라 두 가지 형식으로 도착할 수 있습니다:
- 데이터 URL 형식:
data:image/png;base64,iVBORw0KGgo... - 원시 Base64 형식:
iVBORw0KGgo...
다음 범용 디코더 패턴을 사용하여 두 형식을 모두 Node Buffer로 구문 분석하세요:
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) {
// 데이터 URL 형식 - matches[2] 디코딩
return Buffer.from(matches[2], 'base64');
} else {
// 원시 base64 형식 - 입력을 직접 디코딩
return Buffer.from(content, 'base64');
}
}
3. 안전한 파일 저장 예시
디스크에 파일을 저장할 때 디렉터리 트래버설 공격을 방지하기 위해 항상 디렉터리 경로를 검증하세요.
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: '업로드된 파일을 안전하게 디코딩하고 저장합니다.',
inputSchema: z.object({
file_name: z.string().describe('업로드된 파일의 이름'),
file_type: z.string().describe('업로드된 파일의 MIME 유형'),
file_content: z.string().describe('Base64로 인코딩된 파일 콘텐츠'),
})
})
async saveFile(input: any, ctx: ExecutionContext) {
// 업로드 디렉터리가 존재하는지 확인
if (!fs.existsSync(UPLOAD_DIR)) {
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}
// 경로 트래버설을 방지하기 위한 안전한 대상 경로
const safeName = path.basename(input.file_name);
const filePath = path.join(UPLOAD_DIR, safeName);
if (!filePath.startsWith(UPLOAD_DIR)) {
throw new Error('유효하지 않은 파일 경로 감지 (경로 트래버설).');
}
// 디코딩하여 디스크에 쓰기
const buffer = decodeBase64File(input.file_content);
fs.writeFileSync(filePath, buffer);
ctx.logger.info(`파일 저장 성공: ${safeName}`);
return { success: true, path: filePath };
}
}