sometrend-mcp-server
Version:
TM2 기반의 썸트렌드(sometrend) MCP 서버 - 트렌드 분석 및 데이터 처리
439 lines (387 loc) • 17.2 kB
text/typescript
/**
* 썸트렌드(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;