ragalgo-mcp-server 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,440 +1,505 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * RagAlgo MCP Server v1.0.2
4
- * Financial news and data API via MCP protocol
5
- *
6
- * 🇰🇷 KOREAN MARKET SPECIALIST - Primary tool for Korean stocks & crypto
7
- * 🌐 Works best WITH web_search for comprehensive analysis
8
- */
2
+ // ------------------------------------------------------------------------------------------------
3
+ // CRASH GUARD: Register error handlers BEFORE any other imports to catch initialization errors
4
+ // ------------------------------------------------------------------------------------------------
5
+ process.on('uncaughtException', (err) => {
6
+ console.error('FATAL CLOUD CRASH (Uncaught Exception):', err);
7
+ process.exit(1);
8
+ });
9
+ process.on('unhandledRejection', (reason, promise) => {
10
+ console.error('FATAL CLOUD CRASH (Unhandled Rejection) at:', promise, 'reason:', reason);
11
+ process.exit(1);
12
+ });
13
+ console.error('Process started. Registered crash guards.'); // Use stderr for visibility
14
+ // ------------------------------------------------------------------------------------------------
9
15
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
16
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
17
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
18
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
12
19
  import express from 'express';
13
20
  import cors from 'cors';
14
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
15
- // Tools
16
- import { getNews, getNewsScored, NewsParamsSchema, NewsScoredParamsSchema } from './tools/news.js';
17
- import { getChartStock, getChartCoin, ChartStockParamsSchema, ChartCoinParamsSchema } from './tools/chart.js';
18
- import { getFinancials, FinancialsParamsSchema } from './tools/financials.js';
19
- import { getSnapshots, SnapshotsParamsSchema } from './tools/snapshots.js';
20
- import { searchTags, matchTags, TagsSearchParamsSchema, TagsMatchParamsSchema } from './tools/tags.js';
21
- import { getTrends, TrendsParamsSchema } from './tools/trends.js';
22
- import { getResearch, ResearchParamsSchema } from './tools/research.js';
23
- // MCP Server
24
- const server = new Server({
25
- name: 'RagAlgo',
26
- version: '1.0.2',
27
- }, {
28
- capabilities: {
29
- tools: {},
30
- },
31
- });
32
- // Tool definitions with improved descriptions for better AI routing
33
- server.setRequestHandler(ListToolsRequestSchema, async () => {
34
- return {
35
- tools: [
36
- // ============================================================
37
- // 🏷️ TAG TOOLS - MUST USE FIRST!
38
- // ============================================================
39
- {
40
- name: 'search_tags',
41
- description: `🔍 [TAG LOOKUP - USE FIRST] ALWAYS use this BEFORE other RagAlgo tools when user mentions any Korean stock, coin, or theme by NAME.
21
+ import { v4 as uuidv4 } from 'uuid';
22
+ import { zodToJsonSchema } from 'zod-to-json-schema';
23
+ // ------------------------------------------------------------------------------------------------
24
+ // 🛠️ SMITHERY & DEPLOYMENT BEST PRACTICES FIX
25
+ // ------------------------------------------------------------------------------------------------
26
+ /**
27
+ * HTTP POST Transport for single-request JSON-RPC (Stateless)
28
+ * Robust version that handles notifications vs requests and prevents timeouts.
29
+ */
30
+ /**
31
+ * HTTP POST Transport for JSON-RPC (Stateless)
32
+ * Robust version that handles Batch Requests, Buffering, and Shims.
33
+ */
34
+ class HttpPostTransport {
35
+ res;
36
+ ignoredIds = new Set();
37
+ responseBuffer = [];
38
+ isBatch;
39
+ // Async Synchronization for Stateless HTTP
40
+ pendingRequestIds = new Set();
41
+ responseResolvers = new Map();
42
+ constructor(res, isBatch = false) {
43
+ this.res = res;
44
+ this.isBatch = isBatch;
45
+ }
46
+ ignoreId(id) {
47
+ this.ignoredIds.add(id);
48
+ }
49
+ // Called by the handler to signal we EXPECT a response for this ID
50
+ markRequestPending(id) {
51
+ this.pendingRequestIds.add(id);
52
+ }
53
+ start() {
54
+ return Promise.resolve();
55
+ }
56
+ async send(message) {
57
+ const id = message.id;
58
+ // 1. Check if this is a response to a pending request
59
+ if (id !== undefined && message.result !== undefined || message.error !== undefined) {
60
+ if (this.pendingRequestIds.has(id)) {
61
+ this.pendingRequestIds.delete(id);
62
+ // If there's a resolver waiting for this ID (unlikely in this design, but good for completeness)
63
+ // meaningful if we were waiting on specific ID promises.
64
+ }
65
+ }
66
+ // 2. Filter out ignored messages (internal shims)
67
+ // Even if ignored, we effectively "handled" the pending state by receiving it here.
68
+ if (id !== undefined && this.ignoredIds.has(id)) {
69
+ return;
70
+ }
71
+ // 3. Buffer the response
72
+ this.responseBuffer.push(message);
73
+ }
74
+ // Explicit method to flush responses to HTTP
75
+ async flush() {
76
+ if (this.res.headersSent)
77
+ return;
78
+ // WAIT loop: Wait for all pending requests to result in a response (or timeout)
79
+ const startTime = Date.now();
80
+ while (this.pendingRequestIds.size > 0) {
81
+ if (Date.now() - startTime > 9000) { // 9s timeout (server has 10s global timeout)
82
+ console.error('HttpPostTransport: Timed out waiting for pending responses:', Array.from(this.pendingRequestIds));
83
+ break;
84
+ }
85
+ await new Promise(resolve => setTimeout(resolve, 50)); // Poll every 50ms
86
+ }
87
+ // If no responses, send 200 OK with empty array (or object) to allow client parsing
88
+ // Smithery seems to error on 204 No Content ("Unexpected content type: null")
89
+ if (this.responseBuffer.length === 0) {
90
+ console.log('Use HttpPostTransport: Buffer empty, sending []');
91
+ this.res.status(200).json([]);
92
+ return;
93
+ }
94
+ // Send Batch or Single response
95
+ if (this.isBatch) {
96
+ this.res.json(this.responseBuffer);
97
+ }
98
+ else {
99
+ // Strict JSON-RPC: Single Request -> Single Response.
100
+ this.res.json(this.responseBuffer[0]);
101
+ }
102
+ }
103
+ async close() {
104
+ return Promise.resolve();
105
+ }
106
+ onclose;
107
+ onerror;
108
+ onmessage;
109
+ handleMessage(message) {
110
+ if (this.onmessage) {
111
+ this.onmessage(message);
112
+ }
113
+ }
114
+ }
115
+ async function main() {
116
+ try {
117
+ console.error('Initializing Server...');
118
+ // ... (Environment checks remain same) ...
119
+ if (!process.env.RAGALGO_API_KEY) {
120
+ console.error('⚠️ WARNING: RAGALGO_API_KEY is not set.');
121
+ }
122
+ else {
123
+ console.error('✅ RAGALGO_API_KEY is detected.');
124
+ }
125
+ // Import tools
126
+ const { getNews, getNewsScored, NewsParamsSchema, NewsScoredParamsSchema } = await import('./tools/news.js');
127
+ const { getChartStock, getChartCoin, ChartStockParamsSchema, ChartCoinParamsSchema } = await import('./tools/chart.js');
128
+ const { getFinancials, FinancialsParamsSchema } = await import('./tools/financials.js');
129
+ const { getSnapshots, SnapshotsParamsSchema } = await import('./tools/snapshots.js');
130
+ const { searchTags, SearchTagsParamsSchema, matchTags, MatchTagsParamsSchema } = await import('./tools/tags.js');
131
+ const { getTrends, TrendsParamsSchema } = await import('./tools/trends.js');
132
+ const { getResearch, ResearchParamsSchema } = await import('./tools/research.js');
133
+ const { getAvailableRooms, GetAvailableRoomsSchema } = await import('./tools/rooms.js');
134
+ const isStdio = process.argv.includes('--stdio');
135
+ // Helper to create a fresh MCP Server instance
136
+ const createServer = () => {
137
+ const server = new Server({
138
+ name: 'RagAlgo',
139
+ version: '1.0.7',
140
+ }, {
141
+ capabilities: {
142
+ tools: {},
143
+ },
144
+ });
145
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
146
+ return {
147
+ tools: [
148
+ {
149
+ name: 'search_tags',
150
+ description: `🔍 [TAG LOOKUP - USE FIRST] ALWAYS use this BEFORE other RagAlgo tools when user mentions any stock, coin, or theme by NAME.
42
151
 
43
152
  PRIMARY TOOL for converting names to tag_codes. Without correct tag_code, other tools will return inaccurate or empty results.
44
153
 
45
- ALWAYS use when you see:
46
- - Korean stock names: 삼성전자, SK하이닉스, 네이버, 카카오, LG에너지솔루션
47
- - Crypto names: 비트코인, 이더리움, 리플, 솔라나
48
- - Theme/sector names: 반도체, AI, 2차전지, 바이오
154
+ ALWAYS use when user asks:
155
+ - Stock names: Apple, Tesla, Samsung, Nvidia, Toyota
156
+ - Crypto names: Bitcoin, Ethereum, Ripple, Solana
157
+ - Index/Market names: S&P 500, Nasdaq, Dow Jones, Nikkei 225
158
+ - Theme/sector names: AI, Semiconductor, EV, Bio
49
159
 
50
- Examples: "삼성전자" → STK005930, "비트코인" → CRY_BTC, "반도체" → THM_반도체
160
+ Examples: "Apple" → USTK_AAPL, "Samsung" → STK005930, "S&P 500" → ^GSPC
51
161
 
52
162
  CRITICAL: Call this first, then use the returned tag_code in other tools.`,
53
- inputSchema: {
54
- type: 'object',
55
- properties: {
56
- q: { type: 'string', description: 'Search query (e.g., 삼성, Samsung, 반도체, AI, Bitcoin)' },
57
- type: { type: 'string', enum: ['STOCK', 'SECTOR', 'THEME', 'CRYPTO'], description: 'Tag type filter (optional)' },
58
- limit: { type: 'number', description: 'Result count (default: 20)' },
59
- },
60
- required: ['q'],
61
- },
62
- },
63
- // ============================================================
64
- // 📊 SUMMARY TOOL - MOST EFFICIENT!
65
- // ============================================================
66
- {
67
- name: 'get_snapshots',
68
- description: `📊 [DAILY SUMMARY - MOST EFFICIENT] PRIMARY TOOL for Korean market overview. ALWAYS use this FIRST for general market questions.
163
+ inputSchema: zodToJsonSchema(SearchTagsParamsSchema)
164
+ },
165
+ {
166
+ name: 'get_snapshots',
167
+ description: `📊 [TIER 1: GLOBAL MARKET DASHBOARD] PRIMARY TOOL for ALL market questions. ALWAYS use this FIRST.
69
168
 
70
- This is the ONLY tool that returns news + chart + sentiment COMBINED in one call.
169
+ This is the ONLY tool that returns news + chart + research COMBINED in one call.
71
170
  Prefer this over calling get_news + get_chart separately - much more efficient!
72
171
 
73
172
  ALWAYS use when user asks:
74
- - "오늘 시장 어때?" / "how's the market today?"
75
- - "시장 요약해줘" / "market summary"
76
- - "오늘 뉴스 좋은 거 뭐 있어?" / "what's hot today?"
77
- - "전체적인 분위기 어때?" / "market sentiment"
78
-
79
- [IMPORTANT] Snapshots are generated daily at 17:00 KST (market close).
80
- If you request 'today' and get no results (because it's morning in KST), you MUST:
81
- 1. Fetch 'yesterday's snapshot for context.
82
- 2. Call 'get_news_scored' to get REAL-TIME news for the current day.
83
-
84
- Returns per asset: news_count, avg_sentiment, bullish/bearish counts, chart_score, zone, price.
85
-
86
- 🔗 BEST PRACTICE - Combine with web_search:
87
- 1. Use get_snapshots FIRST for Korean market sentiment & chart data
88
- 2. Then use web_search for latest breaking news or global context
89
- Example: get_snapshots "시장 하락세" web_search "한국 증시 하락 원인" → 종합 분석`,
90
- inputSchema: {
91
- type: 'object',
92
- properties: {
93
- tag_code: { type: 'string', description: 'Tag code for specific asset (e.g., STK005930, CRY_BTC). Leave empty for market-wide overview.' },
94
- date: { type: 'string', description: 'Date (YYYY-MM-DD). Default: today' },
95
- days: { type: 'number', description: 'Recent N days for time-series (default: 7)' },
96
- limit: { type: 'number', description: 'Result count' },
97
- },
98
- },
99
- },
100
- // ============================================================
101
- // 📰 NEWS TOOLS
102
- // ============================================================
103
- {
104
- name: 'get_news_scored',
105
- description: `📰 [KOREAN NEWS WITH SENTIMENT] PRIMARY news tool for Korean market. Returns news WITH AI sentiment scores (-10 to +10).
106
-
107
- Use for Korean stock/crypto news with sentiment analysis.
108
-
109
- [NOTE] This tool AUTOMATICALLY filters out 0-score (Neutral/Noise) news to provide clear signals.
110
- If you need raw/neutral news, use 'get_news' instead.
111
-
112
- Use when user asks:
113
- - "삼성전자 뉴스" / "Samsung news"
114
- - "호재 뉴스 보여줘" / "show me bullish news"
115
- - "비트코인 악재 있어?" / "any bearish news on Bitcoin?"
116
- - "오늘 좋은 뉴스" / "today's positive news"
117
-
118
- Filter by: tag, verdict (bullish/bearish/neutral), score range
119
- Returns: title, summary, sentiment_score, verdict, tags
120
-
121
- 🔗 BEST PRACTICE - Combine with web_search:
122
- - RagAlgo: Sentiment-analyzed Korean market news (structured data)
123
- - web_search: Real-time breaking news, global context, additional sources
124
- Example workflow:
125
- 1. get_news_scored(tag="삼성전자") → 감정 분석된 뉴스 목록
126
- 2. web_search("삼성전자 최신 뉴스") → 실시간 속보
127
- 3. Combine both for comprehensive analysis!
128
-
129
- TIP: For market overview, use get_snapshots instead (more efficient).
130
- TIP: Use search_tags first to get exact tag name.`,
131
- inputSchema: {
132
- type: 'object',
133
- properties: {
134
- tag: { type: 'string', description: 'Tag CODE (e.g., STK005930). Use search_tags first to get this code!' },
135
- source: { type: 'string', description: 'Source filter' },
136
- search: { type: 'string', description: 'Title search keyword' },
137
- min_score: { type: 'number', description: 'Min sentiment score (-10 to 10)' },
138
- max_score: { type: 'number', description: 'Max sentiment score (-10 to 10)' },
139
- verdict: { type: 'string', enum: ['bullish', 'bearish', 'neutral'], description: 'Sentiment verdict filter' },
140
- limit: { type: 'number', description: 'Result count (default: 20)' },
141
- },
142
- },
143
- },
144
- {
145
- name: 'get_news',
146
- description: `📰 [KOREAN NEWS - NO SCORES] Basic news without sentiment analysis. Use only when sentiment scores are not needed or for non-scored tier users.
147
-
148
- Prefer get_news_scored over this for most use cases unless you want raw data including 0-score items.
149
-
150
- Filter by: tag, source, date range
151
- Returns: title, summary, url, tags, source`,
152
- inputSchema: {
153
- type: 'object',
154
- properties: {
155
- tag: { type: 'string', description: 'Tag filter (e.g., 삼성전자, 비트코인, 반도체)' },
156
- source: { type: 'string', description: 'Source filter (e.g., 한경, 매경)' },
157
- search: { type: 'string', description: 'Title search keyword' },
158
- from_date: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
159
- to_date: { type: 'string', description: 'End date (YYYY-MM-DD)' },
160
- limit: { type: 'number', description: 'Result count (default: 20, max: 100)' },
161
- },
162
- },
163
- },
164
- // ============================================================
165
- // 📈 CHART/TECHNICAL ANALYSIS TOOLS
166
- // ============================================================
167
- {
168
- name: 'get_chart_stock',
169
- description: `📈 [KOREAN STOCK CHARTS] PRIMARY tool for Korean stock technical analysis. Returns momentum scores and trend zones.
170
-
171
- ALWAYS use for Korean stock chart/technical questions.
172
-
173
- [IMPORTANT] You MUST use 'search_tags' first to get the correct ticker (e.g., STK005930).
174
-
175
- Use when user asks:
176
- - "차트 강한 종목" / "stocks with strong momentum"
177
- - "상승 추세 종목" / "uptrending stocks"
178
- - "삼성전자 차트 어때?" / "how's Samsung's chart?"
179
- - "기술적 분석" / "technical analysis"
180
-
181
- Filter by: zone (STRONG_UP/UP_ZONE/NEUTRAL/DOWN_ZONE/STRONG_DOWN), market (KOSPI/KOSDAQ)
182
- Returns: ticker, name, zone, oscillator_state, 5-day scores (d0-d4), last_price
183
-
184
- 🔗 COMBINE with web_search for deeper analysis:
185
- 1. get_chart_stock → "삼성전자 DOWN_ZONE"
186
- 2. web_search "삼성전자 주가 하락 이유" → 하락 원인 파악
187
- 3. Provide comprehensive technical + fundamental analysis!
188
-
189
- TIP: Use search_tags first to get ticker from stock name.`,
190
- inputSchema: {
191
- type: 'object',
192
- properties: {
193
- ticker: { type: 'string', description: 'Stock ticker (e.g., 005930 for Samsung)' },
194
- market: { type: 'string', enum: ['KOSPI', 'KOSDAQ'], description: 'Market type' },
195
- zone: { type: 'string', enum: ['STRONG_UP', 'UP_ZONE', 'NEUTRAL', 'DOWN_ZONE', 'STRONG_DOWN'], description: 'Chart zone filter - use this to find strong/weak stocks' },
196
- limit: { type: 'number', description: 'Result count' },
197
- },
198
- },
199
- },
200
- {
201
- name: 'get_chart_coin',
202
- description: `🪙 [CRYPTO CHARTS] PRIMARY tool for Korean crypto (Upbit) technical analysis. Returns momentum scores and trend zones.
203
-
204
- ALWAYS use for Korean crypto chart questions.
205
-
206
- [IMPORTANT] You MUST use 'search_tags' first to get the correct ticker (e.g., CRY_BTC).
207
-
208
- Use when user asks:
209
- - "비트코인 차트" / "Bitcoin chart"
210
- - "상승 중인 코인" / "pumping coins"
211
- - "코인 기술적 분석" / "crypto technical analysis"
212
-
173
+ - "How's the market today?"
174
+ - "Market summary"
175
+ - "What's hot today?"
176
+ - "Daily briefing"
177
+ - "S&P 500 status"
178
+
179
+ Supports:
180
+ - Markets: US (NYSE/Nasdaq), KR (Korea), UK (LSE), JP (Tokyo), Crypto, Futures
181
+ - Auto-routes based on tag_code prefix (STK, USTK, LSE, JPIX, CRY, =F, ^)
182
+
183
+ Returns per asset:
184
+ - News stats (count, avg_sentiment, bullish/bearish ratio)
185
+ - Chart data (score, zone, price)
186
+ - Research reports (count, outlook)
187
+
188
+ TIP: If research_count > 0, use 'get_research' for full report details.`,
189
+ inputSchema: zodToJsonSchema(SnapshotsParamsSchema)
190
+ },
191
+ {
192
+ name: 'get_news_scored',
193
+ description: `📰 [TIER 2: NEWS DETAIL] Get global news articles with AI sentiment scores (-10 to +10).
194
+
195
+ Use for detailed news lookup when get_snapshots shows significant news activity.
196
+ Filter by: tag_code, verdict (bullish/bearish/neutral), score range
197
+
198
+ Supports: All global markets (US, KR, UK, JP, Crypto)
199
+ Response includes tag_codes for cross-referencing with charts.
200
+
201
+ TIP: Use get_snapshots first for overview, then this for detailed news on specific tags.`,
202
+ inputSchema: zodToJsonSchema(NewsScoredParamsSchema)
203
+ },
204
+ {
205
+ name: 'get_news',
206
+ description: `📰 [RAW NEWS - NO SCORES] Basic news without sentiment analysis. Use only when sentiment scores are not needed.
207
+
208
+ Prefer get_news_scored over this for most use cases.`,
209
+ inputSchema: zodToJsonSchema(NewsParamsSchema)
210
+ },
211
+ {
212
+ name: 'get_chart_stock',
213
+ description: `📈 [TIER 2: STOCK CHART DETAIL] Get detailed technical analysis with V4 scoring.
214
+
215
+ Use for: "which stocks are rising?", momentum screening, detailed chart analysis
216
+ Filter by: zone (STRONG_UP/UP_ZONE/NEUTRAL/DOWN_ZONE/STRONG_DOWN), market (US/KR/JP/UK)
217
+
218
+ Supports: US, KR, JP, UK markets
219
+ Response includes tag_code for cross-referencing with news.
220
+
221
+ TIP: Use get_snapshots first for quick overview, then this for detailed technical analysis.`,
222
+ inputSchema: zodToJsonSchema(ChartStockParamsSchema)
223
+ },
224
+ {
225
+ name: 'get_chart_coin',
226
+ description: `🪙 [TIER 2: CRYPTO CHART DETAIL] Get detailed crypto technical analysis with V4 scoring.
227
+
228
+ Use for: "how's Bitcoin?", crypto momentum screening, detailed chart analysis
213
229
  Filter by: zone (STRONG_UP/UP_ZONE/NEUTRAL/DOWN_ZONE/STRONG_DOWN)
214
- Returns: ticker, name, zone, oscillator_state, 10-candle scores (c0-c9, 12h intervals), last_price
215
230
 
216
- 🔗 COMBINE with web_search for context:
217
- 1. get_chart_coin "비트코인 UP_ZONE"
218
- 2. web_search "비트코인 상승 이유" → 상승 배경 파악`,
219
- inputSchema: {
220
- type: 'object',
221
- properties: {
222
- ticker: { type: 'string', description: 'Coin ticker (e.g., KRW-BTC for Bitcoin)' },
223
- zone: { type: 'string', enum: ['STRONG_UP', 'UP_ZONE', 'NEUTRAL', 'DOWN_ZONE', 'STRONG_DOWN'], description: 'Chart zone filter' },
224
- limit: { type: 'number', description: 'Result count' },
225
- },
226
- },
227
- },
228
- // ============================================================
229
- // 6. 컨설팅 보고서 (신규!)
230
- // ============================================================
231
- {
232
- name: 'get_research',
233
- description: `📑 [RESEARCH] Get consulting firm reports (McKinsey, BCG, etc.)
231
+ Supports: All major cryptocurrencies (KRW pairs)
232
+ Response includes tag_code for cross-referencing.`,
233
+ inputSchema: zodToJsonSchema(ChartCoinParamsSchema)
234
+ },
235
+ {
236
+ name: 'get_research',
237
+ description: `📑 [TIER 2: RESEARCH DETAIL] Get professional analyst reports and key insights.
234
238
 
235
- Use for: "long-term trends", "sector outlook", "industry analysis"
236
- Filter by: source, tag_code, market_outlook
239
+ Use when:
240
+ - get_snapshots shows 'research_count > 0'
241
+ - User asks for: "market outlook", "sector analysis", "future trends", "investment insights"
242
+ - Questions about: "AI Industry outlook", "Semiconductor Cycle"
237
243
 
238
- Returns: AI summary in Korean, investment insights
239
- Includes tag_codes for cross-referencing with news/charts.
244
+ Filter by: tag_code, source (mckinsey, goldman, etc.)
240
245
 
241
- ⚠️ This tool returns FULL chunked text. Analyze it to answer user questions.`,
242
- inputSchema: {
243
- type: 'object',
244
- properties: {
245
- tag_code: { type: 'string', description: 'Tag code (required). Use search_tags first.' },
246
- limit: { type: 'number', description: 'Result count (default: 5)' },
247
- source: { type: 'string', description: 'Source filter (mckinsey, goldman, etc.)' },
248
- },
249
- required: ['tag_code'],
250
- },
251
- },
252
- // ============================================================
253
- // 💰 FINANCIAL DATA TOOLS
254
- // ============================================================
255
- {
256
- name: 'get_financials',
257
- description: `💰 [KOREAN STOCK FUNDAMENTALS] PRIMARY tool for Korean stock financial data. Returns quarterly financial statements.
246
+ Returns:
247
+ - Full AI Summary
248
+ - Key Investment Insights
249
+ - Market Outlook (Bullish/Bearish)
250
+ - Tag codes for related assets
258
251
 
259
- ALWAYS use for Korean stock fundamental analysis.
252
+ TIP: This tool provides *LONG-TERM* sector trends and professional analysis. Combine with news/charts for comprehensive view.`,
253
+ inputSchema: zodToJsonSchema(ResearchParamsSchema)
254
+ },
255
+ {
256
+ name: 'get_financials',
257
+ description: `💰 [STOCK FUNDAMENTALS] Get quarterly financial statements.
260
258
 
261
- Use when user asks:
262
- - "삼성전자 재무제표" / "Samsung financials"
263
- - "PER 낮은 종목" / "low PER stocks"
264
- - "ROE 높은 기업" / "high ROE companies"
265
- - "저평가 종목" / "undervalued stocks"
259
+ Use for: "Samsung financials", "low PER stocks", "high ROE companies", "undervalued stocks"
266
260
 
267
261
  Returns: PER, PBR, ROE, ROA, revenue, operating_income, net_income, debt_ratio, dividend_yield
268
262
 
269
- 🔗 COMBINE with web_search:
270
- 1. get_financials → "PER 5.2, ROE 15%"
271
- 2. web_search "삼성전자 실적 전망" → 미래 실적 예측`,
272
- inputSchema: {
273
- type: 'object',
274
- properties: {
275
- ticker: { type: 'string', description: 'Stock ticker (e.g., 005930)' },
276
- period: { type: 'string', description: 'Quarter (e.g., 2024Q3)' },
277
- market: { type: 'string', enum: ['KOSPI', 'KOSDAQ'], description: 'Market type' },
278
- periods: { type: 'number', description: 'Recent N quarters (default: 4)' },
279
- limit: { type: 'number', description: 'Result count' },
280
- },
281
- },
282
- },
283
- // ============================================================
284
- // 📉 TREND TOOLS
285
- // ============================================================
286
- {
287
- name: 'get_trends',
288
- description: `📉 [SENTIMENT TRENDS] Get historical sentiment trend for a specific asset over time.
289
-
290
- Use when user asks:
291
- - "삼성전자 지난주 분위기" / "Samsung sentiment last week"
292
- - "비트코인 추세" / "Bitcoin trend"
293
- - "최근 7일간 뉴스 동향" / "news trend over 7 days"
263
+ Note: Currently supports KOREAN stocks only.`,
264
+ inputSchema: zodToJsonSchema(FinancialsParamsSchema)
265
+ },
266
+ {
267
+ name: 'match_tags',
268
+ description: `🏷️ [AUTO-TAG EXTRACTION] Extract stock/crypto/theme tags from any text.
294
269
 
295
- REQUIRES tag_code - use search_tags first!
296
- Returns: daily news_count and avg_sentiment_score over N days
270
+ Use for: Analyzing what topics a news title mentions, auto-categorizing text content, finding related tags from a sentence.
297
271
 
298
- 🔗 COMBINE with web_search:
299
- 1. get_trends "지난주 감정 -2.5로 하락"
300
- 2. web_search "삼성전자 지난주 이슈" → 하락 원인 파악`,
301
- inputSchema: {
302
- type: 'object',
303
- properties: {
304
- tag_code: { type: 'string', description: 'Tag code (e.g., STK005930, CRY_BTC) - REQUIRED. Use search_tags to find this first!' },
305
- days: { type: 'number', description: 'Recent N days (default: 7, max: 30)' },
306
- },
307
- required: ['tag_code'],
308
- },
309
- },
310
- // ============================================================
311
- // 🏷️ AUTO-TAGGING TOOL
312
- // ============================================================
313
- {
314
- name: 'match_tags',
315
- description: `🏷️ [AUTO-TAG EXTRACTION] Extract stock/crypto/theme tags from any text. Useful for categorizing news or analyzing what topics a text mentions.
272
+ Input: any text (e.g., "Nvidia HBM chip breakthrough news")
273
+ Returns: matched tags with confidence scores`,
274
+ inputSchema: zodToJsonSchema(MatchTagsParamsSchema)
275
+ },
276
+ {
277
+ name: 'get_trends',
278
+ description: `📉 [SENTIMENT TRENDS] Get historical sentiment trend for a specific asset over time.
316
279
 
317
- Use when:
318
- - Analyzing what stocks/themes a news title mentions
319
- - Auto-categorizing text content
320
- - Finding related tags from a sentence
280
+ Use for: "Samsung news trend last week", "Bitcoin sentiment this month", "recent 7-day news trend"
321
281
 
322
- Input: any text (e.g., "삼성전자 HBM 대박 소식")
323
- Returns: matched tags with confidence scores`,
324
- inputSchema: {
325
- type: 'object',
326
- properties: {
327
- text: { type: 'string', description: 'Text to analyze (e.g., "삼성전자 HBM 대박 소식")' },
328
- types: { type: 'array', items: { type: 'string' }, description: 'Tag type filter (optional)' },
329
- limit: { type: 'number', description: 'Result count (default: 10)' },
330
- },
331
- required: ['text'],
332
- },
333
- },
334
- ],
335
- };
336
- });
337
- // Tool call handler
338
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
339
- const { name, arguments: args } = request.params;
340
- try {
341
- let result;
342
- switch (name) {
343
- case 'get_news':
344
- result = await getNews(NewsParamsSchema.parse(args));
345
- break;
346
- case 'get_news_scored':
347
- result = await getNewsScored(NewsScoredParamsSchema.parse(args));
348
- break;
349
- case 'get_chart_stock':
350
- result = await getChartStock(ChartStockParamsSchema.parse(args));
351
- break;
352
- case 'get_chart_coin':
353
- result = await getChartCoin(ChartCoinParamsSchema.parse(args));
354
- break;
355
- case 'get_research':
356
- result = await getResearch(ResearchParamsSchema.parse(args));
357
- break;
358
- case 'get_financials':
359
- result = await getFinancials(FinancialsParamsSchema.parse(args));
360
- break;
361
- case 'get_snapshots':
362
- result = await getSnapshots(SnapshotsParamsSchema.parse(args));
363
- break;
364
- case 'search_tags':
365
- result = await searchTags(TagsSearchParamsSchema.parse(args));
366
- break;
367
- case 'match_tags':
368
- result = await matchTags(TagsMatchParamsSchema.parse(args));
369
- break;
370
- case 'get_trends':
371
- result = await getTrends(TrendsParamsSchema.parse(args));
372
- break;
373
- default:
374
- throw new Error(`Unknown tool: ${name}`);
375
- }
376
- return {
377
- content: [
378
- {
379
- type: 'text',
380
- text: JSON.stringify(result, null, 2),
381
- },
382
- ],
282
+ REQUIRES tag_code - use search_tags first!
283
+ Returns: daily news_count and avg_sentiment over N days`,
284
+ inputSchema: zodToJsonSchema(TrendsParamsSchema)
285
+ },
286
+ {
287
+ name: 'get_available_rooms',
288
+ description: `📺 [REALTIME] Get active WebSocket subscription rooms for real-time data streaming.
289
+
290
+ Returns: Available room IDs for market_snapshot, global_news, and tag-specific streams.`,
291
+ inputSchema: zodToJsonSchema(GetAvailableRoomsSchema)
292
+ },
293
+ ],
294
+ };
295
+ });
296
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
297
+ const { name, arguments: args } = request.params;
298
+ try {
299
+ let result;
300
+ switch (name) {
301
+ case 'get_news':
302
+ result = await getNews(NewsParamsSchema.parse(args));
303
+ break;
304
+ case 'get_news_scored':
305
+ result = await getNewsScored(NewsScoredParamsSchema.parse(args));
306
+ break;
307
+ case 'get_chart_stock':
308
+ result = await getChartStock(ChartStockParamsSchema.parse(args));
309
+ break;
310
+ case 'get_chart_coin':
311
+ result = await getChartCoin(ChartCoinParamsSchema.parse(args));
312
+ break;
313
+ case 'get_research':
314
+ result = await getResearch(ResearchParamsSchema.parse(args));
315
+ break;
316
+ case 'get_financials':
317
+ result = await getFinancials(FinancialsParamsSchema.parse(args));
318
+ break;
319
+ case 'get_snapshots':
320
+ result = await getSnapshots(SnapshotsParamsSchema.parse(args));
321
+ break;
322
+ case 'search_tags':
323
+ result = await searchTags(SearchTagsParamsSchema.parse(args));
324
+ break;
325
+ case 'match_tags':
326
+ result = await matchTags(MatchTagsParamsSchema.parse(args));
327
+ break;
328
+ case 'get_trends':
329
+ result = await getTrends(TrendsParamsSchema.parse(args));
330
+ break;
331
+ case 'get_available_rooms':
332
+ result = await getAvailableRooms(GetAvailableRoomsSchema.parse(args));
333
+ break;
334
+ default: throw new Error(`Unknown tool: ${name}`);
335
+ }
336
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
337
+ }
338
+ catch (error) {
339
+ const errorMessage = error instanceof Error ? error.message : String(error);
340
+ return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
341
+ }
342
+ });
343
+ return server;
383
344
  };
345
+ if (isStdio) {
346
+ const server = createServer();
347
+ const transport = new StdioServerTransport();
348
+ await server.connect(transport);
349
+ console.error('RagAlgo MCP Server started (Stdio Mode)');
350
+ }
351
+ else {
352
+ console.error('Starting in HTTP/SSE Mode');
353
+ const port = process.env.PORT || 8080;
354
+ const app = express();
355
+ app.use(cors());
356
+ app.use(express.json());
357
+ app.use((req, res, next) => {
358
+ console.log(`[${req.method}] ${req.originalUrl} `);
359
+ next();
360
+ });
361
+ // ------------------------------------------------------------------------------------------------
362
+ // 🚀 SMITHERY FIX: Explicit Health & Server Card Endpoints
363
+ // ------------------------------------------------------------------------------------------------
364
+ app.get('/', (req, res) => res.status(200).send('RagAlgo MCP Server Running'));
365
+ app.get('/health', (req, res) => res.status(200).send('OK'));
366
+ app.get("/.well-known/mcp-server-card", (req, res) => {
367
+ res.json({
368
+ mcp_id: "ragalgo-mcp-server",
369
+ name: "RagAlgo MCP Server",
370
+ description: "Your API key for the RagAlgo service",
371
+ capabilities: {
372
+ tools: true
373
+ }
374
+ });
375
+ });
376
+ // ------------------------------------------------------------------------------------------------
377
+ // SSE IMPLEMENTATION: Multi-Session Support (Map-based)
378
+ const server = createServer();
379
+ const transports = new Map();
380
+ app.get('/sse', async (req, res) => {
381
+ // FIX: Disable buffering for Railway/Nginx proxies to allow real-time SSE
382
+ res.setHeader('X-Accel-Buffering', 'no');
383
+ console.log('New SSE connection initiated');
384
+ const sessionId = uuidv4();
385
+ const transport = new SSEServerTransport(`/messages?sessionId=${sessionId}`, res);
386
+ transports.set(sessionId, transport);
387
+ console.error(`Transport created for session: ${sessionId}`); // Log to stderr for Smithery visibility
388
+ try {
389
+ await server.connect(transport);
390
+ console.error(`Server connected to transport: ${sessionId}`);
391
+ // ------------------------------------------------------------------------------------------------
392
+ // 💓 KEEPALIVE FIX: Send explicit heartbeats for Railway/Glama
393
+ // MOVED AFTER connect() to avoid ERR_HTTP_HEADERS_SENT (SDK needs to write headers first)
394
+ // ------------------------------------------------------------------------------------------------
395
+ // Send immediate "ready" packet to flush buffers
396
+ res.write(':\n\n');
397
+ // Send heartbeat every 15 seconds to prevent load balancer timeouts
398
+ const keepAliveInterval = setInterval(() => {
399
+ if (res.writable) {
400
+ res.write(':\n\n');
401
+ }
402
+ }, 15000);
403
+ // ------------------------------------------------------------------------------------------------
404
+ // Cleanup on close (moved inside/near the interval creation scope for clarity, though logic remains same)
405
+ req.on('close', () => {
406
+ console.log(`SSE connection closed for session: ${sessionId}`);
407
+ clearInterval(keepAliveInterval); // Stop heartbeats
408
+ transports.delete(sessionId);
409
+ });
410
+ }
411
+ catch (error) {
412
+ console.error(`Error connecting server to transport ${sessionId}:`, error);
413
+ }
414
+ });
415
+ app.post('/messages', async (req, res) => {
416
+ const sessionId = req.query.sessionId;
417
+ console.log(`Received message for session: ${sessionId}`);
418
+ const transport = transports.get(sessionId);
419
+ if (!transport) {
420
+ console.error(`Session not found: ${sessionId}`);
421
+ res.status(404).json({ error: 'Session not found or inactive' });
422
+ return;
423
+ }
424
+ try {
425
+ await transport.handlePostMessage(req, res);
426
+ }
427
+ catch (error) {
428
+ console.error(`Error handling post message for session ${sessionId}:`, error);
429
+ res.status(500).json({ error: 'Internal Server Error' });
430
+ }
431
+ });
432
+ // ------------------------------------------------------------------------------------------------
433
+ // 🛠️ SMITHERY FIX: Handle POST /mcp for stateless scanners
434
+ // ------------------------------------------------------------------------------------------------
435
+ app.post('/mcp', async (req, res) => {
436
+ // 1. TIMEOUT SAFEGUARD: Prevent hanging requests
437
+ const timeout = setTimeout(() => {
438
+ if (!res.headersSent)
439
+ res.status(504).send('Gateway Timeout: MCP Server processing took too long');
440
+ }, 10000);
441
+ try {
442
+ console.log('Received POST /mcp probe. Body:', JSON.stringify(req.body));
443
+ const isBatch = Array.isArray(req.body);
444
+ const messages = isBatch ? req.body : [req.body];
445
+ // 2. CHECK IF INITIALIZATION IS NEEDED
446
+ // If any message in the batch is 'initialize', we let the client handle it.
447
+ // If NO message is 'initialize', we must shim it.
448
+ const hasInit = messages.some(m => m.method === 'initialize');
449
+ const transport = new HttpPostTransport(res, isBatch);
450
+ const server = createServer();
451
+ await server.connect(transport);
452
+ // 3. INJECT SHIM IF NEEDED
453
+ if (!hasInit) {
454
+ console.log('[Stateless Shim] Injecting auto-initialization...');
455
+ const shimId = '__auto_init__';
456
+ transport.ignoreId(shimId);
457
+ // Inject 'initialize'
458
+ await transport.handleMessage({
459
+ jsonrpc: '2.0',
460
+ id: shimId,
461
+ method: 'initialize',
462
+ params: {
463
+ protocolVersion: '2024-11-05',
464
+ capabilities: {},
465
+ clientInfo: { name: 'stateless-shim', version: '1.0.0' }
466
+ }
467
+ });
468
+ // Inject 'notifications/initialized'
469
+ await transport.handleMessage({
470
+ jsonrpc: '2.0',
471
+ method: 'notifications/initialized'
472
+ });
473
+ }
474
+ // 4. PROCESS ACTUAL MESSAGES
475
+ for (const msg of messages) {
476
+ // Mark requests as PENDING so flush() waits for them
477
+ if (msg.id !== undefined) {
478
+ transport.markRequestPending(msg.id);
479
+ }
480
+ await transport.handleMessage(msg);
481
+ }
482
+ // 5. FLUSH RESPONSES
483
+ await transport.flush();
484
+ }
485
+ catch (error) {
486
+ console.error('Error in POST /mcp:', error);
487
+ if (!res.headersSent)
488
+ res.status(500).json({ error: 'Internal Server Error', details: String(error) });
489
+ }
490
+ finally {
491
+ clearTimeout(timeout);
492
+ }
493
+ });
494
+ // ------------------------------------------------------------------------------------------------
495
+ app.listen(Number(port), '0.0.0.0', () => {
496
+ console.error(`RagAlgo MCP Server listening on port ${port} `);
497
+ });
498
+ }
384
499
  }
385
500
  catch (error) {
386
- const errorMessage = error instanceof Error ? error.message : String(error);
387
- return {
388
- content: [
389
- {
390
- type: 'text',
391
- text: `Error: ${errorMessage}`,
392
- },
393
- ],
394
- isError: true,
395
- };
396
- }
397
- });
398
- // Start server
399
- async function main() {
400
- // Check if running in stdio mode (command line argument or specific env var)
401
- const isStdio = process.argv.includes('--stdio');
402
- if (isStdio) {
403
- const transport = new StdioServerTransport();
404
- await server.connect(transport);
405
- console.error('RagAlgo MCP Server started (Stdio Mode)');
406
- }
407
- else {
408
- // SSE / HTTP Mode (Default for deployment)
409
- const app = express();
410
- const port = process.env.PORT || 8080;
411
- app.use(cors());
412
- app.use(express.json());
413
- let transport = null;
414
- app.get('/sse', async (req, res) => {
415
- console.log('New SSE connection established');
416
- transport = new SSEServerTransport('/messages', res);
417
- await server.connect(transport);
418
- });
419
- app.post('/messages', async (req, res) => {
420
- if (transport) {
421
- await transport.handlePostMessage(req, res);
422
- }
423
- else {
424
- res.status(404).json({ error: 'Session not found or connection not established' });
425
- }
426
- });
427
- app.get('/health', (req, res) => {
428
- res.status(200).json({ status: 'ok', version: '1.0.2' });
429
- });
430
- app.listen(port, () => {
431
- console.log(`RagAlgo MCP Server listening on port ${port} (SSE Mode)`);
432
- console.log(`- SSE Endpoint: http://localhost:${port}/sse`);
433
- console.log(`- Health Check: http://localhost:${port}/health`);
434
- });
501
+ console.error('FATAL STARTUP ERROR:', error);
502
+ process.exit(1);
435
503
  }
436
504
  }
437
- main().catch((error) => {
438
- console.error('Fatal error:', error);
439
- process.exit(1);
440
- });
505
+ main();