@mrbaeksang/korea-stock-analyzer-mcp
Version:
Korean stock analyzer with 6 investment legends' strategies for Claude Desktop (MCP)
848 lines (833 loc) • 35 kB
JavaScript
/**
* 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);
});