sometrend-mcp-server
Version:
TM2 기반의 썸트렌드(sometrend) MCP 서버 - 트렌드 분석 및 데이터 처리
378 lines (326 loc) • 13.3 kB
text/typescript
/**
* 썸트렌드 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}에서 실행 중입니다.`);
});