UNPKG

sometrend-mcp-server

Version:

TM2 기반의 썸트렌드(sometrend) MCP 서버 - 트렌드 분석 및 데이터 처리

439 lines (387 loc) 17.2 kB
/** * 썸트렌드(sometrend) MCP 서버 메인 클래스 * TM2 기반의 썸트렌드 MCP 서버 */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { SERVER_BASE_INFO, AI_OPTIMIZED_SERVER_DESCRIPTION } from './constants/app-data.constants.js'; import { TOOL_ENDPOINT_MAP } from './constants/api-endpoints.constants.js'; import { CacheService } from './services/CacheService.js'; import { PromptGeneratorService } from './services/PromptGeneratorService.js'; import { PromptHandlerService } from './services/PromptHandlerService.js'; import { CategoryResourceService } from './services/CategoryResourceService.js'; // .js 확장자 추가 import { SnsChannelService } from './services/SnsChannelService.js'; // .js 확장자 추가 import { ToolDefinitions } from './definitions/ToolDefinitions.js'; import { getPromptDefinitions } from './definitions/PromptDefinitions.js'; import type { CacheConfig, DebounceConfig, RequestLogEntry, PerformanceMetrics, MemoryStats } from './types/index.js'; import { TM2ApiClient } from './services/TM2ApiClient.js'; // 디바운스 관련 인터페이스 interface DebounceEntry { timer: NodeJS.Timeout; requestCount: number; lastRequestTime: number; } // 향상된 로거 인터페이스 interface EnhancedLogger { debug(message: string, data?: any): void; info(message: string, data?: any): void; warn(message: string, data?: any): void; error(message: string, error?: any): void; performance(operation: string, duration: number, data?: any): void; } export class SometrendMCPServer { private server: Server; private apiService: TM2ApiClient; private cacheService: CacheService; private promptGeneratorService: PromptGeneratorService; private promptHandlerService: PromptHandlerService; private categoryResourceService: CategoryResourceService; private snsChannelService: SnsChannelService; // 성능 최적화 관련 private debounceMap = new Map<string, DebounceEntry>(); private requestLog: RequestLogEntry[] = []; private readonly maxLogEntries = 1000; // 메모리 효율성을 위한 로그 제한 private readonly debounceConfig: DebounceConfig; private readonly logger: EnhancedLogger; // 메모리 모니터링 private memoryCheckInterval?: NodeJS.Timeout; private readonly memoryThreshold = 500 * 1024 * 1024; // 500MB constructor() { // 환경 변수 설정 const baseUrl = process.env.TM2_BASE_URL || "http://10.1.41.49:9292"; const timeout = parseInt(process.env.API_TIMEOUT || "300000", 10); // 디바운스 설정 this.debounceConfig = { enabled: process.env.DEBOUNCE_ENABLED !== 'false', delayMs: parseInt(process.env.DEBOUNCE_DELAY || "300", 10), maxWaitMs: parseInt(process.env.DEBOUNCE_MAX_WAIT || "1000", 10), maxRequestsPerKey: parseInt(process.env.DEBOUNCE_MAX_REQUESTS || "5", 10) }; // 로거 초기화 this.logger = this.createEnhancedLogger(); // 캐시 설정 const cacheConfig: CacheConfig = { maxSize: parseInt(process.env.CACHE_MAX_SIZE || "1000", 10), ttlMs: parseInt(process.env.CACHE_TTL || "300000", 10), // 5분 checkPeriodMs: parseInt(process.env.CACHE_CHECK_PERIOD || "60000", 10), // 1분 maxMemoryMB: parseInt(process.env.CACHE_MAX_MEMORY || "100", 10) }; // 서비스 초기화 - 생성자 파라미터 수정 this.cacheService = new CacheService(cacheConfig); this.apiService = new TM2ApiClient(baseUrl, timeout); this.promptGeneratorService = new PromptGeneratorService(); this.promptHandlerService = new PromptHandlerService(); this.categoryResourceService = new CategoryResourceService(this.cacheService, this.apiService); this.snsChannelService = new SnsChannelService(this.cacheService, this.apiService); // MCP 서버 초기화 - constants에서 가져온 정보 사용 this.server = new Server({ name: SERVER_BASE_INFO.name, version: SERVER_BASE_INFO.version, description: AI_OPTIMIZED_SERVER_DESCRIPTION }, { capabilities: { tools: {}, prompts: {} } }); this.setupHandlers(); this.setupErrorHandling(); this.setupMemoryMonitoring(); this.logger.info("썸트렌드 MCP 서버 초기화 완료", { serverInfo: SERVER_BASE_INFO, supportedAliases: SERVER_BASE_INFO.displayNames, aliases: SERVER_BASE_INFO.displayNames, cache: cacheConfig, debounce: this.debounceConfig, memoryThreshold: `${this.memoryThreshold / 1024 / 1024}MB` }); } /** * 향상된 로거 생성 */ private createEnhancedLogger(): EnhancedLogger { const isDebugMode = process.env.DEBUG_MODE === 'true'; const logLevel = process.env.LOG_LEVEL || 'info'; return { debug: (message: string, data?: any) => { // MCP 환경에서는 로그 출력을 비활성화 // if (isDebugMode || logLevel === 'debug') { // console.log(`[DEBUG] ${new Date().toISOString()} ${message}`, // data ? JSON.stringify(data, null, 2) : ''); // } }, info: (message: string, data?: any) => { // MCP 환경에서는 로그 출력을 비활성화 // if (['debug', 'info'].includes(logLevel)) { // console.log(`[INFO] ${new Date().toISOString()} ${message}`, // data ? JSON.stringify(data, null, 2) : ''); // } }, warn: (message: string, data?: any) => { // MCP 환경에서는 stderr로만 출력하거나 완전히 비활성화 // console.warn(`[WARN] ${new Date().toISOString()} ${message}`, // data ? JSON.stringify(data, null, 2) : ''); }, error: (message: string, error?: any) => { // 심각한 에러만 stderr로 출력 (선택적) // console.error(`[ERROR] ${new Date().toISOString()} ${message}`, // error ? (error.stack || error) : ''); }, performance: (operation: string, duration: number, data?: any) => { // MCP 환경에서는 성능 로그 비활성화 // if (isDebugMode) { // console.log(`[PERF] ${new Date().toISOString()} ${operation}: ${duration}ms`, // data ? JSON.stringify(data, null, 2) : ''); // } } }; } /** * 메모리 모니터링 설정 */ private setupMemoryMonitoring(): void { this.memoryCheckInterval = setInterval(() => { const memUsage = process.memoryUsage(); if (memUsage.heapUsed > this.memoryThreshold) { // 로그 출력 제거 // this.logger.warn("메모리 사용량이 임계치를 초과했습니다", { // heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`, // threshold: `${this.memoryThreshold / 1024 / 1024}MB` // }); // 메모리 정리 수행 this.performMemoryCleanup(); } // 디버그 로그 제거 // this.logger.debug("메모리 상태", { // heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`, // heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`, // external: `${Math.round(memUsage.external / 1024 / 1024)}MB` // }); }, 30000); } /** * 메모리 정리 수행 */ private performMemoryCleanup(): void { // 캐시 정리 const stats = this.cacheService.getCacheStats(); this.logger.info("메모리 정리 수행 중", stats); // 디바운스 맵 정리 this.debounceMap.clear(); // 요청 로그 정리 if (this.requestLog.length > this.maxLogEntries / 2) { this.requestLog = this.requestLog.slice(-this.maxLogEntries / 2); } // 강제 가비지 컬렉션 (가능한 경우) if (global.gc) { global.gc(); } } /** * 요청 처리기 설정 */ private setupHandlers(): void { // 도구 목록 반환 - 수정된 부분 this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: ToolDefinitions.getAllToolDefinitions() }; }); // 프롬프트 목록 반환 this.server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: getPromptDefinitions() }; }); // 프롬프트 가져오기 this.server.setRequestHandler(GetPromptRequestSchema, async (request) => { const promptName = request.params.name; const args = request.params.arguments as Record<string, any>; try { const promptText = await this.promptHandlerService.getPrompt(promptName, args); return { messages: [ { role: "user", content: { type: "text", text: promptText } } ] }; } catch (error: unknown) { this.logger.error(`프롬프트 처리 오류: ${promptName}`, error); throw new McpError( ErrorCode.InternalError, `프롬프트 처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : String(error)}` ); } }); // 도구 실행 처리 - constants의 TOOL_ENDPOINT_MAP 사용 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const toolName = request.params.name; const args = request.params.arguments as Record<string, any>; try { const endpoint = TOOL_ENDPOINT_MAP[toolName]; if (!endpoint) { throw new McpError( ErrorCode.MethodNotFound, `알 수 없는 도구: ${toolName}` ); } if (endpoint === "LOCAL") { // 로컬 처리 - CategoryResourceService 사용 const localResult = await this.handleLocalTool(toolName, args); // ResourceContent 타입을 MCP 도구 결과 형식으로 변환 if ('content' in localResult && Array.isArray(localResult.content)) { return { content: localResult.content }; } else if ('contents' in localResult && Array.isArray(localResult.contents)) { return { content: localResult.contents }; } else { // 기본 변환 return { content: [{ type: "text", text: JSON.stringify(localResult, null, 2) }] }; } } else { // API 호출 const apiResult = await this.apiService.callAPI(endpoint, args, toolName); // ApiCallResult도 동일하게 처리 if ('content' in apiResult && Array.isArray(apiResult.content)) { return { content: apiResult.content }; } else { return { content: [{ type: "text", text: JSON.stringify(apiResult, null, 2) }] }; } } } catch (error: unknown) { this.logger.error(`도구 실행 오류: ${toolName}`, error); throw error; } }); } /** * 로컬 도구 처리 - CategoryResourceService와 SnsChannelService 활용 */ private async handleLocalTool(toolName: string, args: Record<string, any>) { switch (toolName) { // 기존 CategoryResourceService 도구들 case "availableCategories": return this.categoryResourceService.getAvailableCategories(args.detail || false); case "getCategorySetList": return this.categoryResourceService.getCategorySetList(args.detail || false); // SNS 채널 서비스 도구들 case "getAvailableDataSources": return this.snsChannelService.getAvailableSnsChannels(args.detail || false); case "getAvailableSnsChannels": return this.snsChannelService.getAvailableSnsChannels(args.detail || false); case "searchSnsChannels": return this.snsChannelService.searchSnsChannels(args); case "getChannelsByDataType": if (!args.dataType) { throw new McpError( ErrorCode.InvalidParams, "dataType 매개변수가 필요합니다" ); } return this.snsChannelService.getChannelsByDataType(args.dataType); case "getChannelsByFeature": if (!args.feature) { throw new McpError( ErrorCode.InvalidParams, "feature 매개변수가 필요합니다" ); } return this.snsChannelService.getChannelsByFeature(args.feature); case "getAllSnsDataTypes": return this.snsChannelService.getAllDataTypes(); case "getAllSnsFeatures": return this.snsChannelService.getAllFeatures(); case "getRecommendedSnsChannels": if (!args.analysisType) { throw new McpError( ErrorCode.InvalidParams, "analysisType 매개변수가 필요합니다" ); } // private 메서드 대신 public 메서드 사용 return this.snsChannelService.getRecommendedChannelsForAnalysis(args.analysisType); case "validateSnsChannelId": if (!args.channelId) { throw new McpError( ErrorCode.InvalidParams, "channelId 매개변수가 필요합니다" ); } return this.snsChannelService.validateChannelId(args.channelId); case "getSnsChannelHealthCheck": return this.snsChannelService.getHealthCheck(); default: throw new McpError( ErrorCode.MethodNotFound, `지원되지 않는 로컬 도구: ${toolName}` ); } } /** * 오류 처리 설정 */ private setupErrorHandling(): void { this.server.onerror = (error) => { this.logger.error("썸트렌드 MCP 서버 오류", error); }; process.on('SIGINT', async () => { this.logger.info("서버 종료 중..."); if (this.memoryCheckInterval) { clearInterval(this.memoryCheckInterval); } this.cacheService.destroy(); await this.server.close(); process.exit(0); }); } /** * 서버 실행 */ async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); this.logger.info(`${SERVER_BASE_INFO.name} v${SERVER_BASE_INFO.version}이 시작되었습니다.`); } /** * 서버 인스턴스 반환 (프로덕션 모드에서 사용) */ getServer(): Server { return this.server; } } // 하위 호환성을 위한 별칭 export export const TM2Server = SometrendMCPServer;