UNPKG

sometrend-mcp-server

Version:

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

378 lines (326 loc) 13.3 kB
/** * 썸트렌드 MCP 서버 - 프로덕션 모드 * 실제 TM2 API 연동 및 완전한 인증/로깅 시스템 */ import express from 'express'; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { TM2Server } from './TM2Server.js'; // 프로덕션 개선사항 import import { basicRateLimit, sseRateLimit, messageRateLimit } from './middleware/RateLimit.js'; import { ProductionLogger } from './utils/ProductionLogger.js'; import { healthCheckService } from './monitoring/HealthCheck.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.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'; import { SnsChannelService } from './services/SnsChannelService.js'; import { DatabaseService } from './services/DatabaseService.js'; import { ToolDefinitions } from './definitions/ToolDefinitions.js'; import { getPromptDefinitions } from './definitions/PromptDefinitions.js'; import { TM2ApiClient } from './services/TM2ApiClient.js'; import type { CacheConfig } from './types/index.js'; import npmRegistryProduction from './npm-registry-production.js'; const app = express(); app.use(express.json()); // 전역 서비스 인스턴스 let apiService: TM2ApiClient; let cacheService: CacheService; let promptGeneratorService: PromptGeneratorService; let promptHandlerService: PromptHandlerService; let categoryResourceService: CategoryResourceService; let snsChannelService: SnsChannelService; let databaseService: DatabaseService; let isServicesInitialized = false; // 활성 트랜스포트 관리 const transports: Map<string, SSEServerTransport> = new Map(); /** * 서비스 초기화 함수 */ async function initializeServices(): Promise<void> { if (isServicesInitialized) { return; } // 환경 변수 설정 const baseUrl = process.env.TM2_BASE_URL || "http://10.1.41.49:9292"; const timeout = parseInt(process.env.API_TIMEOUT || "300000", 10); // 캐시 설정 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) }; // 서비스 초기화 cacheService = new CacheService(cacheConfig); apiService = new TM2ApiClient(baseUrl, timeout); promptGeneratorService = new PromptGeneratorService(); promptHandlerService = new PromptHandlerService(); categoryResourceService = new CategoryResourceService(cacheService, apiService); snsChannelService = new SnsChannelService(cacheService, apiService); // 데이터베이스 서비스 초기화 const dbConfig = { host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '3306'), user: process.env.DB_USER || 'sometrend_mcp', password: process.env.DB_PASSWORD || 'sometrend_mcp', database: process.env.DB_NAME || 'sometrend_mcp' }; databaseService = new DatabaseService(dbConfig); console.log('✅ 데이터베이스 서비스 초기화 완료'); // 리소스 프리로딩 초기화 await categoryResourceService.initializeResources(); isServicesInitialized = true; console.log('✅ 프로덕션 서비스 초기화 및 리소스 프리로딩 완료'); } /** * MCP 서버 인스턴스 생성 및 설정 */ function createServer(): Server { // TM2Server 인스턴스 생성 const tm2Server = new TM2Server(); // MCP 서버 생성 - TM2Server의 내부 서버를 직접 사용 const server = tm2Server.getServer(); return server; } // Express 미들웨어 설정 app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Api-Key'); if (req.method === 'OPTIONS') { res.sendStatus(200); return; } next(); }); // Rate limiting app.use(basicRateLimit); // MCP 서버 생성 const server = createServer(); // SSE 엔드포인트 - 클라이언트 연결 app.get('/sse', sseRateLimit, async (req, res) => { console.log('SSE 연결 요청 수신 (GET /sse)'); // API 키 검증 const apiKey = req.query.apiKey as string; if (!apiKey) { res.status(401).json({ error: 'API 키 필요', message: 'API key required in apiKey query parameter' }); return; } // 사용자 인증 let user = null; if (databaseService) { user = await databaseService.getUserByApiKey(apiKey); if (!user) { res.status(401).json({ error: '유효하지 않은 API 키', message: 'Invalid API key' }); return; } console.log(`🔐 인증된 사용자: ${user.name} (${user.email})`); } // 서비스 초기화 if (!isServicesInitialized) { await initializeServices(); } const sessionId = req.query.sessionId as string || `session-${user?.id || 'anonymous'}-${Date.now()}`; const clientIp = req.ip || req.connection.remoteAddress; // SSE 트랜스포트 생성 const transport = new SSEServerTransport("/sse", res); // 연결 해제 핸들러 const cleanup = () => { transports.delete(sessionId); console.log(`SSE 연결 종료: 세션 ID ${sessionId}`); // 세션 종료 로그 if (databaseService && user) { databaseService.endSession(sessionId).catch(console.error); } }; req.on('close', cleanup); req.on('error', cleanup); // 트랜스포트 저장 transports.set(sessionId, transport); // 세션 시작 로그 if (databaseService && user) { await databaseService.startSession({ sessionId, userId: user.id, clientIp: clientIp || '알 수 없음', userAgent: req.get('User-Agent') || 'Unknown', startTime: new Date(), lastActivity: new Date(), requestCount: 0, isActive: true }); } console.log(`SSE 스트림 설정 완료: 세션 ID ${sessionId}`); // 서버와 트랜스포트 연결 await server.connect(transport); }); // 메시지 엔드포인트 - 클라이언트 요청 수신 (프로덕션 모드) app.post('/messages', messageRateLimit, async (req, res) => { const clientIp = req.ip || req.connection.remoteAddress || '알 수 없음'; console.log(`📥 API 요청 수신 - IP: ${clientIp}`); // API 키 인증 const apiKey = req.get('Api-Key') || req.get('Authorization')?.replace('Bearer ', ''); if (!apiKey) { res.status(401).json({ error: 'API 키 필요', message: 'API key required in Api-Key header' }); return; } // 사용자 인증 let user = null; if (databaseService) { user = await databaseService.getUserByApiKey(apiKey); if (!user) { res.status(401).json({ error: '유효하지 않은 API 키', message: 'Invalid API key' }); return; } console.log(`👤 인증된 사용자: ${user.name} (${user.email})`); } let sessionId = req.query.sessionId as string; let transport: any; // 세션 ID가 없으면 기본 세션 생성 if (!sessionId) { sessionId = `session-${user?.id || 'anonymous'}-${Date.now()}`; console.log(`🔧 새 세션 생성: ${sessionId}`); } transport = transports.get(sessionId); if (!transport) { res.status(404).json({ error: '세션을 찾을 수 없습니다', message: `Session not found: ${sessionId}. Please establish SSE connection first.`, sessionId }); return; } const startTime = Date.now(); try { // 실제 MCP 서버로 요청 처리 await transport.handlePostMessage(req, res, req.body); const responseTime = Date.now() - startTime; // 데이터베이스에 API 사용 로그 기록 if (databaseService && user) { await databaseService.recordApiUsage({ userId: user.id, sessionId: sessionId, toolName: req.body?.method || 'unknown', endpoint: '/messages', requestData: req.body, responseStatus: res.statusCode, responseTime: responseTime, clientIp: clientIp, userAgent: req.get('User-Agent') || 'Unknown', createdAt: new Date() }).catch((err: any) => console.error('DB 로그 실패:', err)); } } catch (error) { const responseTime = Date.now() - startTime; console.error('요청 처리 오류:', error); // 오류도 로그에 기록 if (databaseService && user) { await databaseService.recordApiUsage({ userId: user.id, sessionId: sessionId, toolName: req.body?.method || 'unknown', endpoint: '/messages', requestData: req.body, responseStatus: 500, responseTime: responseTime, clientIp: clientIp, userAgent: req.get('User-Agent') || 'Unknown', createdAt: new Date() }).catch((err: any) => console.error('DB 로그 실패:', err)); } if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", id: req.body?.id || 1, error: { code: -32603, message: "Internal error", data: error instanceof Error ? error.message : '알 수 없는 오류' } }); } } }); // NPM Registry (사설 NPM 저장소) app.use('/npm', npmRegistryProduction); // 헬스체크 엔드포인트 app.get('/health', async (req, res) => { const health = await healthCheckService.getHealthStatus(); const statusCode = health.status === 'unhealthy' ? 503 : 200; res.status(statusCode).json({ status: 'ok', server: process.env.SERVER_NAME || "썸트렌드(sometrend) MCP 서버", version: process.env.npm_package_version || "1.0.0", mode: 'production', activeSessions: transports.size, uptime: process.uptime(), timestamp: new Date().toISOString(), protocol: 'MCP v1.18.0', endpoints: { sse: '/sse', messages: '/messages', health: '/health' }, health }); }); // 서버 시작 const port = parseInt(process.env.PORT || "8080", 10); app.listen(port, '0.0.0.0', async () => { // 서비스 초기화 await initializeServices(); console.log('🚀 썸트렌드 MCP 서버 (프로덕션 모드)'); console.log('📡 서버 정보:'); console.log(` - 이름: ${process.env.SERVER_NAME || "썸트렌드(sometrend) MCP 서버"}`); console.log(` - 버전: ${process.env.npm_package_version || "1.0.0"}`); console.log(` - 포트: ${port}`); console.log(' - 모드: 프로덕션 (Production)'); console.log(''); console.log('🔗 엔드포인트:'); console.log(' - GET /sse : SSE 스트림 연결 (API 키 필요)'); console.log(' - POST /messages : 클라이언트 메시지 수신 (API 키 필요)'); console.log(' - GET /health : 서버 상태 확인'); console.log(' - GET /npm/* : NPM Registry (사설 저장소)'); console.log(''); console.log('🔐 인증:'); console.log(' - API 키를 Api-Key 헤더로 전송 필요'); console.log(' - 유효한 사용자만 접근 가능'); console.log(' - 모든 요청이 데이터베이스에 로깅됨'); console.log(''); console.log('📦 NPM Registry 사용법:'); console.log(` npx --registry http://112.175.32.77:${port}/npm sometrend-mcp-server`); console.log(''); console.log('🎯 Claude Desktop 설정:'); console.log(' {'); console.log(' "mcpServers": {'); console.log(' "sometrend": {'); console.log(' "command": "npx",'); console.log(` "args": ["--registry", "http://112.175.32.77:${port}/npm", "sometrend-mcp-server"]`); console.log(' }'); console.log(' }'); console.log(' }'); console.log(''); console.log(`📌 서버가 포트 ${port}에서 실행 중입니다.`); });