UNPKG

@mrbaeksang/korea-stock-analyzer-mcp

Version:

Korean stock analyzer with 6 investment legends' strategies for Claude Desktop (MCP)

848 lines (833 loc) 35 kB
/** * MCP 서버 메인 파일 */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { PythonExecutor } from './services/python-executor.js'; import { MarketDataService } from './services/market-data.js'; import { FinancialDataService } from './services/financial-data.js'; import { SupplyDemandService } from './services/supply-demand.js'; import { GuruAnalyzers } from './analyzers/index.js'; /** * 한국 주식 전문 분석 MCP 서버 * @version 3.0 */ class KoreanStockAnalysisMCP { server; guruAnalyzers; constructor() { this.server = new Server({ name: 'korean-stock-analysis', version: '1.1.1', }, { capabilities: { tools: {}, }, }); this.guruAnalyzers = new GuruAnalyzers(); this.setupHandlers(); } setupHandlers() { // 도구 목록 제공 this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.getTools(), })); // 도구 실행 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'analyze_equity': return await this.analyzeEquity(args); case 'get_financial_data': return await this.getFinancialData(args); case 'get_technical_indicators': return await this.getTechnicalIndicators(args); case 'calculate_dcf': return await this.calculateDCF(args); case 'search_news': return await this.searchNews(args); case 'get_supply_demand': return await this.getSupplyDemand(args); case 'compare_peers': return await this.comparePeers(args); default: throw new Error(`Unknown tool: ${name}`); } }); } getTools() { return [ { name: 'analyze_equity', description: '한국 주식 종목 종합 분석 (빠른 분석 / 요약 / 전체 보고서)', inputSchema: { type: 'object', properties: { ticker: { type: 'string', description: '종목 코드 (예: 005930)', }, company_name: { type: 'string', description: '회사명 (예: 삼성전자)', }, report_type: { type: 'string', enum: ['quick', 'summary', 'full'], description: '보고서 유형: quick(빠른 분석 1페이지), summary(요약 3-5페이지), full(전체 10-15페이지)', default: 'quick', }, }, required: ['ticker', 'company_name'], }, }, { name: 'get_financial_data', description: '재무제표 데이터 조회 (PER, PBR, EPS, BPS, 배당수익률)', inputSchema: { type: 'object', properties: { ticker: { type: 'string', description: '종목 코드', }, years: { type: 'number', description: '조회 기간 (년)', default: 3, }, }, required: ['ticker'], }, }, { name: 'get_technical_indicators', description: '기술적 지표 분석 (이동평균, RSI, MACD, 볼린저밴드)', inputSchema: { type: 'object', properties: { ticker: { type: 'string', description: '종목 코드', }, }, required: ['ticker'], }, }, { name: 'calculate_dcf', description: 'DCF(현금흐름할인) 모델로 적정가치 계산', inputSchema: { type: 'object', properties: { ticker: { type: 'string', description: '종목 코드', }, growth_rate: { type: 'number', description: '예상 성장률 (%)', default: 10, }, discount_rate: { type: 'number', description: '할인율 (%)', default: 10, }, }, required: ['ticker'], }, }, { name: 'search_news', description: '종목 관련 최신 뉴스 검색', inputSchema: { type: 'object', properties: { ticker: { type: 'string', description: '종목 코드', }, company_name: { type: 'string', description: '회사명', }, limit: { type: 'number', description: '뉴스 개수', default: 5, }, }, required: ['company_name'], }, }, { name: 'get_supply_demand', description: '수급 데이터 조회 (외국인, 기관, 개인)', inputSchema: { type: 'object', properties: { ticker: { type: 'string', description: '종목 코드', }, days: { type: 'number', description: '조회 기간 (일)', default: 20, }, }, required: ['ticker'], }, }, { name: 'compare_peers', description: '동종업계 비교 분석 (자동으로 유사 종목 탐지)', inputSchema: { type: 'object', properties: { ticker: { type: 'string', description: '종목 코드', }, peer_tickers: { type: 'array', items: { type: 'string', }, description: '비교할 종목 코드들 (선택사항, 미입력시 자동 탐지)', }, }, required: ['ticker'], }, }, ]; } async analyzeEquity(args) { const { ticker, company_name, report_type = 'quick' } = args; try { // 기본 데이터 수집 const [marketData, financialData] = await Promise.all([ MarketDataService.fetchBasic(ticker), FinancialDataService.fetch(ticker), ]); // 에러 체크 if (marketData?.error) { throw new Error(`시장 데이터 조회 실패: ${marketData.error}`); } if (!marketData?.currentPrice || marketData.currentPrice === 0) { throw new Error(`종목 코드 ${ticker}의 현재가를 가져올 수 없습니다. 종목 코드를 확인해주세요.`); } // 재무 데이터 파싱 const fund = Array.isArray(financialData) && financialData.length > 0 ? financialData[0] : null; const currentPrice = marketData.currentPrice; // report_type에 따라 다른 보고서 생성 if (report_type === 'quick') { // 빠른 분석 (1페이지) const actualPER = fund?.per || 15; const eps = fund?.eps || (currentPrice / 15); const fairValue = eps * actualPER; const upside = currentPrice > 0 ? ((fairValue - currentPrice) / currentPrice) * 100 : 0; const report = [ `# 📊 ${company_name || ticker} 실시간 분석`, '', '## 주요 지표', `- 현재가: ₩${currentPrice.toLocaleString()}`, `- 거래량: ${marketData.volume?.toLocaleString() || 'N/A'}`, `- 시가총액: ${marketData.marketCap ? `₩${(marketData.marketCap / 100000000).toFixed(1)}억` : 'N/A'}`, `- PER: ${fund?.per ? fund.per.toFixed(2) : 'N/A'}`, `- PBR: ${fund?.pbr ? fund.pbr.toFixed(2) : 'N/A'}`, `- EPS: ${fund?.eps ? `₩${fund.eps.toLocaleString()}` : 'N/A'}`, `- BPS: ${fund?.bps ? `₩${fund.bps.toLocaleString()}` : 'N/A'}`, `- 배당수익률: ${fund?.div ? `${fund.div.toFixed(2)}%` : 'N/A'}`, '', '## 간단 밸류에이션', `- 적정가치: ₩${Math.round(fairValue).toLocaleString()}`, `- 상승여력: ${upside.toFixed(1)}%`, `- 투자의견: ${upside > 20 ? '**매수**' : upside > 0 ? '**보유**' : '**매도**'}`, '', `*분석 시점: ${new Date().toLocaleDateString('ko-KR')}*`, ].join('\n'); return { content: [ { type: 'text', text: report, }, ], }; } else if (report_type === 'summary') { // 요약 보고서 (3-5페이지) const [technicalData, supplyDemandData] = await Promise.all([ MarketDataService.fetchTechnicalIndicators(ticker), SupplyDemandService.fetch(ticker), ]); const report = await this.generateSummaryReport({ ticker, company_name, marketData, financialData: fund, technicalData, supplyDemandData }); return { content: [ { type: 'text', text: report, }, ], }; } else { // 전체 보고서는 나중에 구현 (현재는 요약 보고서와 동일) const [technicalData, supplyDemandData] = await Promise.all([ MarketDataService.fetchTechnicalIndicators(ticker), SupplyDemandService.fetch(ticker), ]); const report = await this.generateSummaryReport({ ticker, company_name, marketData, financialData: fund, technicalData, supplyDemandData }); return { content: [ { type: 'text', text: report + '\n\n*Note: 전체 보고서(full) 기능은 개발 중입니다.*', }, ], }; } } catch (error) { // Error logging removed (causes encoding issues) const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [ { type: 'text', text: JSON.stringify({ error: `Analysis failed: ${errorMessage}` }), }, ], }; } } // 재무 데이터 조회 async getFinancialData(args) { const { ticker, years = 3 } = args; try { const data = await FinancialDataService.fetch(ticker); const result = Array.isArray(data) ? data.slice(0, years) : [data]; return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { return this.errorResponse(error); } } // 기술적 지표 조회 async getTechnicalIndicators(args) { const { ticker } = args; try { const data = await MarketDataService.fetchTechnicalIndicators(ticker); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } catch (error) { return this.errorResponse(error); } } // DCF 계산 async calculateDCF(args) { const { ticker, growth_rate = 10, discount_rate = 10 } = args; try { const financialData = await FinancialDataService.fetch(ticker); const marketData = await MarketDataService.fetchBasic(ticker); const current = Array.isArray(financialData) ? financialData[0] : financialData; // FCF 추정 개선: EPS * (1 - 재투자율) // 보수적으로 EPS의 70%를 FCF로 가정 (30% 재투자) const fcfPerShare = current.eps * 0.7; // 5년 예측 기간 DCF 계산 let pvOfFCF = 0; for (let year = 1; year <= 5; year++) { const futureValue = fcfPerShare * Math.pow(1 + growth_rate / 100, year); const presentValue = futureValue / Math.pow(1 + discount_rate / 100, year); pvOfFCF += presentValue; } // 터미널 가치 (Gordon Growth Model) const terminalGrowth = 3; // 영구성장률 3% (GDP 성장률 수준) // 터미널 가치 계산 수정: 6년차 FCF / (할인율 - 성장률) const year6FCF = fcfPerShare * Math.pow(1 + growth_rate / 100, 5) * (1 + terminalGrowth / 100); const terminalValue = year6FCF / (discount_rate / 100 - terminalGrowth / 100); const terminalPV = terminalValue / Math.pow(1 + discount_rate / 100, 5); // 주당 내재가치 = 5년 FCF 현재가치 + 터미널 가치 현재가치 const intrinsicValuePerShare = pvOfFCF + terminalPV; // 민감도 분석을 위한 추가 계산 const terminalValueRatio = (terminalPV / intrinsicValuePerShare * 100).toFixed(1); const result = { currentPrice: marketData.currentPrice, intrinsicValue: Math.round(intrinsicValuePerShare), upside: ((intrinsicValuePerShare - marketData.currentPrice) / marketData.currentPrice * 100).toFixed(1), details: { fcfPerShare: Math.round(fcfPerShare), pvOfFCF: Math.round(pvOfFCF), terminalValue: Math.round(terminalPV), terminalValueRatio: `${terminalValueRatio}%`, }, assumptions: { growthRate: `${growth_rate}%`, discountRate: `${discount_rate}%`, terminalGrowth: `${terminalGrowth}%`, fcfMargin: '70% of EPS', }, recommendation: intrinsicValuePerShare > marketData.currentPrice * 1.2 ? '매수' : intrinsicValuePerShare > marketData.currentPrice * 0.9 ? '보유' : '매도', }; return { content: [ { type: 'text', text: `DCF 분석 결과:\n${JSON.stringify(result, null, 2)}`, }, ], }; } catch (error) { return this.errorResponse(error); } } // 뉴스 검색 async searchNews(args) { const { company_name, limit = 5 } = args; try { // 네이버 뉴스 검색 API 시뮬레이션 const newsData = await this.fetchNewsFromNaver(company_name, limit); return { content: [ { type: 'text', text: newsData, }, ], }; } catch (error) { return this.errorResponse(error); } } // 수급 데이터 조회 async getSupplyDemand(args) { const { ticker, days = 20 } = args; try { const data = await SupplyDemandService.fetch(ticker); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } catch (error) { return this.errorResponse(error); } } // 동종업계 비교 async comparePeers(args) { const { ticker, peer_tickers = [] } = args; try { // 자동 동종업계 탐지 let finalPeerTickers = peer_tickers; // 'auto' 문자열이거나 빈 배열인 경우 자동 탐지 if (peer_tickers === 'auto' || !peer_tickers || peer_tickers.length === 0) { // Python을 사용해 pykrx로 동종업계 자동 탐지 const pythonCode = ` import json from pykrx import stock from datetime import datetime, timedelta ticker = '${ticker}' try: # 최근 거래일 찾기 for i in range(7): check_date = (datetime.now() - timedelta(days=i)).strftime('%Y%m%d') market_cap = stock.get_market_cap_by_ticker(check_date) if ticker in market_cap.index: break if ticker not in market_cap.index: raise ValueError(f"Ticker {ticker} not found") target_cap = market_cap.loc[ticker, '시가총액'] # 시가총액 유사 종목 찾기 (대형주는 넓은 범위, 중소형주는 좁은 범위) if target_cap > 10000000000000: # 10조원 이상 min_ratio = 0.1 # 10% max_ratio = 10.0 # 1000% elif target_cap > 1000000000000: # 1조원 이상 min_ratio = 0.3 # 30% max_ratio = 3.0 # 300% else: min_ratio = 0.5 # 50% max_ratio = 2.0 # 200% similar_caps = market_cap[ (market_cap['시가총액'] >= target_cap * min_ratio) & (market_cap['시가총액'] <= target_cap * max_ratio) & (market_cap.index != ticker) ].sort_values('시가총액', ascending=False) # 상위 5개 종목 선택 peer_tickers = similar_caps.index[:5].tolist() result = { 'ticker': ticker, 'peer_tickers': peer_tickers, 'method': 'market_cap_similarity' } except Exception as e: # 에러시 하드코딩된 기본값 사용 default_map = { '005930': ['000660', '005935'], # 삼성전자 '000660': ['005930', '005935'], # SK하이닉스 '005380': ['000270', '012330'], # 현대차 '035720': ['035420'], # 카카오 '035420': ['035720'], # 네이버 } result = { 'ticker': ticker, 'peer_tickers': default_map.get(ticker, []), 'method': 'fallback_default', 'error': str(e) } print(json.dumps(result)) `; try { const result = await PythonExecutor.execute(pythonCode); finalPeerTickers = result.peer_tickers || []; if (finalPeerTickers.length === 0) { return { content: [ { type: 'text', text: JSON.stringify({ mainTicker: ticker, message: "Could not find peers automatically", suggestion: "Please specify peer_tickers manually", example: "peer_tickers: ['000660', '066570']" }, null, 2) } ] }; } } catch (pythonError) { // Python 실패시 빈 배열 반환 (console.error 사용 금지) return { content: [ { type: 'text', text: JSON.stringify({ mainTicker: ticker, error: "Auto-detection failed", suggestion: "Please specify peer_tickers manually", example: "peer_tickers: ['000660', '066570']" }, null, 2) } ] }; } } // peer가 있을 때만 실제 비교 const [financial, market] = await Promise.all([ FinancialDataService.fetch(ticker), MarketDataService.fetchBasic(ticker) ]); const latestFinancial = Array.isArray(financial) ? financial[0] : financial; const mainCompany = { ticker, currentPrice: market?.currentPrice || 0, marketCap: market?.marketCap || 0, per: latestFinancial?.per || 0, pbr: latestFinancial?.pbr || 0, eps: latestFinancial?.eps || 0, div: latestFinancial?.div || 0, }; // 비교 종목들 데이터 수집 (메인 종목 제외) const peerCompanies = finalPeerTickers.filter((t) => t !== ticker); const comparisonData = [mainCompany]; // 병렬 처리로 속도 개선 const dataPromises = peerCompanies.map(async (t) => { try { // 각 종목별로 타임아웃 설정 (10초) const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`${t} 데이터 수집 타임아웃`)), 10000)); const dataPromise = Promise.all([ FinancialDataService.fetch(t), MarketDataService.fetchBasic(t) ]); const [financial, market] = await Promise.race([dataPromise, timeoutPromise]); const latestFinancial = Array.isArray(financial) ? financial[0] : financial; return { ticker: t, currentPrice: market?.currentPrice || 0, marketCap: market?.marketCap || 0, per: latestFinancial?.per || 0, pbr: latestFinancial?.pbr || 0, eps: latestFinancial?.eps || 0, div: latestFinancial?.div || 0, }; } catch (e) { // console.error 사용 금지 - JSON 파싱 오류 방지 return { ticker: t, currentPrice: 0, marketCap: 0, per: 0, pbr: 0, eps: 0, div: 0, }; } }); // 모든 데이터 병렬 수집 const results = await Promise.all(dataPromises); comparisonData.push(...results.filter(r => r.per > 0 || r.pbr > 0)); // 평균 계산 const avgPer = comparisonData.reduce((sum, d) => sum + d.per, 0) / comparisonData.length; const avgPbr = comparisonData.reduce((sum, d) => sum + d.pbr, 0) / comparisonData.length; const avgDiv = comparisonData.reduce((sum, d) => sum + d.div, 0) / comparisonData.length; // 주종목 분석 const mainData = comparisonData[0]; const analysis = { mainTicker: ticker, peerTickers: peer_tickers, comparison: comparisonData, averages: { per: Math.round(avgPer * 100) / 100, pbr: Math.round(avgPbr * 100) / 100, div: Math.round(avgDiv * 100) / 100, }, valuation: { perVsPeers: mainData.per < avgPer ? '저평가' : '고평가', perGap: Math.round((mainData.per - avgPer) / avgPer * 100), pbrVsPeers: mainData.pbr < avgPbr ? '저평가' : '고평가', pbrGap: Math.round((mainData.pbr - avgPbr) / avgPbr * 100), }, recommendation: this.getPeerComparisonRecommendation(mainData, avgPer, avgPbr), }; return { content: [ { type: 'text', text: JSON.stringify(analysis, null, 2), }, ], }; } catch (error) { return this.errorResponse(error); } } getPeerComparisonRecommendation(company, avgPer, avgPbr) { const perDiscount = (avgPer - company.per) / avgPer; const pbrDiscount = (avgPbr - company.pbr) / avgPbr; if (perDiscount > 0.2 && pbrDiscount > 0.2) { return '동종업계 대비 매력적 (매수 고려)'; } else if (perDiscount > 0 && pbrDiscount > 0) { return '동종업계 대비 양호 (보유 권장)'; } else if (perDiscount < -0.2 && pbrDiscount < -0.2) { return '동종업계 대비 고평가 (매도 고려)'; } else { return '동종업계 평균 수준'; } } // 네이버 금융 뉴스 크롤링 async fetchNewsFromNaver(companyName, limit) { // 실제 뉴스 API나 크롤링 대신 예시 데이터 반환 // 실제 구현 시 뉴스 API 사용 권장 const pythonCode = ` import json from datetime import datetime, timedelta import random # 실제 뉴스처럼 보이는 예시 데이터 생성 company = "${companyName}" limit = ${limit} # 뉴스 제목 템플릿 templates = [ f"{company}, AI 반도체 신제품 출시 예정", f"{company} 3분기 실적 시장 예상치 상회", f"외국인 {company} 5거래일 연속 순매수", f"{company}, 베트남 공장 증설 투자 확대", f"{company} 주가 52주 신고가 경신", f"증권가 '{company} 목표주가 상향 조정'", f"{company}, 글로벌 시장 점유율 확대", f"{company} 배당금 인상 검토 중" ] news_items = [] for i in range(min(limit, len(templates))): date = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d') news_items.append({ 'title': templates[i], 'link': f'https://finance.naver.com/news/news_read.naver?article_id={random.randint(100000, 999999)}', 'date': date, 'source': '한국경제' if i % 2 == 0 else '매일경제', 'summary': f'{company}의 최신 소식입니다. 시장 전망은 긍정적입니다.' }) print(json.dumps(news_items, ensure_ascii=False)) `; try { const result = await PythonExecutor.execute(pythonCode); return JSON.stringify(result); } catch (error) { // Python 실패 시 기본 응답 return JSON.stringify([{ title: `${companyName} 관련 뉴스`, link: '#', date: new Date().toLocaleDateString('ko-KR'), source: '뉴스 서비스', summary: '뉴스 데이터를 가져올 수 없습니다.' }]); } } // 요약 보고서 생성 async generateSummaryReport(data) { const { ticker, company_name, marketData, financialData, technicalData, supplyDemandData } = data; const sections = [ `# 📊 ${company_name || ticker} 투자 분석 보고서 (요약)`, '', '---', '', '## 1. 기업 개요', `- 종목코드: ${ticker}`, `- 시가총액: ${marketData.marketCap ? `₩${(marketData.marketCap / 100000000).toFixed(1)}억` : 'N/A'}`, `- 현재가: ₩${marketData.currentPrice?.toLocaleString()}`, `- 거래량: ${marketData.volume?.toLocaleString()} 주`, '', '## 2. 투자 지표', '### 밸류에이션', `- PER: ${financialData?.per?.toFixed(2) || 'N/A'} (업종 평균 대비 ${financialData?.per < 15 ? '저평가' : '고평가'})`, `- PBR: ${financialData?.pbr?.toFixed(2) || 'N/A'} (${financialData?.pbr < 1 ? '청산가치 이하' : '청산가치 이상'})`, `- EPS: ₩${financialData?.eps?.toLocaleString() || 'N/A'}`, `- BPS: ₩${financialData?.bps?.toLocaleString() || 'N/A'}`, `- 배당수익률: ${financialData?.div?.toFixed(2) || 0}%`, '', '### 기술적 지표', `- RSI(14): ${technicalData?.rsi14?.toFixed(1) || 'N/A'} ${technicalData?.rsi14 < 30 ? '(과매도)' : technicalData?.rsi14 > 70 ? '(과매수)' : '(중립)'}`, `- MACD: ${technicalData?.macd?.toFixed(1) || 'N/A'}`, `- 5일 이동평균: ₩${technicalData?.ma5?.toLocaleString() || 'N/A'}`, `- 20일 이동평균: ₩${technicalData?.ma20?.toLocaleString() || 'N/A'}`, `- 변동성(연간): ${technicalData?.volatilityAnnual?.toFixed(1) || 'N/A'}%`, '', '## 3. 수급 분석', supplyDemandData?.recent ? [ `- 외국인: ${supplyDemandData.recent.foreignNet > 0 ? '매수' : '매도'} ${Math.abs(supplyDemandData.recent.foreignNet).toLocaleString()} 주`, `- 기관: ${supplyDemandData.recent.institutionNet > 0 ? '매수' : '매도'} ${Math.abs(supplyDemandData.recent.institutionNet).toLocaleString()} 주`, `- 개인: ${supplyDemandData.recent.individualNet > 0 ? '매수' : '매도'} ${Math.abs(supplyDemandData.recent.individualNet).toLocaleString()} 주`, ].join('\n') : '- 수급 데이터 없음', '', '## 4. 투자 판단', this.generateInvestmentDecision(marketData.currentPrice, financialData, technicalData), '', '---', `*분석 일시: ${new Date().toLocaleString('ko-KR')}*`, ]; return sections.join('\n'); } // 투자 판단 생성 generateInvestmentDecision(currentPrice, financialData, technicalData) { let score = 0; const factors = []; // PER 평가 if (financialData?.per && financialData.per < 10) { score += 2; factors.push('✅ PER 10 미만 (저평가)'); } else if (financialData?.per && financialData.per < 15) { score += 1; factors.push('✅ PER 15 미만 (적정)'); } // PBR 평가 if (financialData?.pbr && financialData.pbr < 1) { score += 2; factors.push('✅ PBR 1 미만 (청산가치 이하)'); } // RSI 평가 if (technicalData?.rsi14 && technicalData.rsi14 < 30) { score += 1; factors.push('✅ RSI 과매도 구간'); } else if (technicalData?.rsi14 && technicalData.rsi14 > 70) { score -= 1; factors.push('⚠️ RSI 과매수 구간'); } // 최종 판단 let recommendation = ''; if (score >= 3) { recommendation = '### 📈 적극 매수'; } else if (score >= 1) { recommendation = '### 📊 매수 고려'; } else if (score >= 0) { recommendation = '### ⏸️ 관망'; } else { recommendation = '### 📉 매도 고려'; } return [ recommendation, '', '**판단 근거:**', ...factors, '', `**종합 점수: ${score}/5**`, ].join('\n'); } // 에러 응답 헬퍼 errorResponse(error) { return { content: [ { type: 'text', text: JSON.stringify({ error: error.message || 'Unknown error' }), }, ], }; } async start() { const transport = new StdioServerTransport(); // 즉시 연결하여 초기화 타임아웃 방지 await this.server.connect(transport); // 프로세스가 예기치 않게 종료되지 않도록 처리 process.on('SIGINT', () => { process.exit(0); }); process.on('SIGTERM', () => { process.exit(0); }); } } // 서버 시작 const mcpServer = new KoreanStockAnalysisMCP(); mcpServer.start().catch((error) => { // console.error 사용 금지 - MCP 통신 방해 process.stderr.write(`MCP Server Error: ${error.message}\n`); process.exit(1); });