ragalgo-mcp-server 1.0.5 → 1.0.7
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/README.md +192 -119
- package/dist/check_sdk.d.ts +1 -0
- package/dist/check_sdk.js +11 -0
- package/dist/check_sse.d.ts +1 -0
- package/dist/check_sse.js +10 -0
- package/dist/index.d.ts +0 -7
- package/dist/index.js +490 -208
- package/dist/tools/chart.d.ts +37 -20
- package/dist/tools/chart.js +6 -7
- package/dist/tools/financials.d.ts +16 -5
- package/dist/tools/financials.js +1 -1
- package/dist/tools/news.d.ts +56 -11
- package/dist/tools/news.js +9 -22
- package/dist/tools/research.d.ts +27 -14
- package/dist/tools/research.js +9 -9
- package/dist/tools/rooms.d.ts +33 -0
- package/dist/tools/rooms.js +43 -0
- package/dist/tools/snapshots.d.ts +16 -1
- package/dist/tools/snapshots.js +16 -3
- package/dist/tools/tags.d.ts +56 -16
- package/dist/tools/tags.js +13 -2
- package/dist/tools/trends.d.ts +8 -1
- package/dist/utils/api.js +100 -26
- package/package.json +45 -39
- package/server.json +32 -0
package/dist/index.js
CHANGED
|
@@ -1,223 +1,505 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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';
|
|
21
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
14
22
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
name: 'get_chart_stock',
|
|
76
|
-
description: `📈 [KOREAN STOCK CHARTS] PRIMARY tool for Korean stock technical analysis. Returns momentum scores and trend zones.`,
|
|
77
|
-
inputSchema: zodToJsonSchema(ChartStockParamsSchema),
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: 'get_chart_coin',
|
|
81
|
-
description: `🪙 [CRYPTO CHARTS] PRIMARY tool for Korean crypto (Upbit) technical analysis.`,
|
|
82
|
-
inputSchema: zodToJsonSchema(ChartCoinParamsSchema),
|
|
83
|
-
},
|
|
84
|
-
// ============================================================
|
|
85
|
-
// 6. 컨설팅 보고서 (신규!)
|
|
86
|
-
// ============================================================
|
|
87
|
-
{
|
|
88
|
-
name: 'get_research',
|
|
89
|
-
description: `📑 [RESEARCH] Get consulting firm reports (McKinsey, BCG, etc.)`,
|
|
90
|
-
inputSchema: zodToJsonSchema(ResearchParamsSchema),
|
|
91
|
-
},
|
|
92
|
-
// ============================================================
|
|
93
|
-
// 💰 FINANCIAL DATA TOOLS
|
|
94
|
-
// ============================================================
|
|
95
|
-
{
|
|
96
|
-
name: 'get_financials',
|
|
97
|
-
description: `💰 [KOREAN STOCK FUNDAMENTALS] PRIMARY tool for Korean stock financial data.`,
|
|
98
|
-
inputSchema: zodToJsonSchema(FinancialsParamsSchema),
|
|
99
|
-
},
|
|
100
|
-
// ============================================================
|
|
101
|
-
// 📉 TREND TOOLS
|
|
102
|
-
// ============================================================
|
|
103
|
-
{
|
|
104
|
-
name: 'get_trends',
|
|
105
|
-
description: `📉 [SENTIMENT TRENDS] Get historical sentiment trend for a specific asset over time.`,
|
|
106
|
-
inputSchema: zodToJsonSchema(TrendsParamsSchema),
|
|
107
|
-
},
|
|
108
|
-
// ============================================================
|
|
109
|
-
// 🏷️ AUTO-TAGGING TOOL
|
|
110
|
-
// ============================================================
|
|
111
|
-
{
|
|
112
|
-
name: 'match_tags',
|
|
113
|
-
description: `🏷️ [AUTO-TAG EXTRACTION] Extract stock/crypto/theme tags from any text.`,
|
|
114
|
-
inputSchema: zodToJsonSchema(TagsMatchParamsSchema),
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
};
|
|
118
|
-
});
|
|
119
|
-
// Tool call handler
|
|
120
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
121
|
-
const { name, arguments: args } = request.params;
|
|
122
|
-
try {
|
|
123
|
-
let result;
|
|
124
|
-
switch (name) {
|
|
125
|
-
case 'get_news':
|
|
126
|
-
result = await getNews(NewsParamsSchema.parse(args));
|
|
127
|
-
break;
|
|
128
|
-
case 'get_news_scored':
|
|
129
|
-
result = await getNewsScored(NewsScoredParamsSchema.parse(args));
|
|
130
|
-
break;
|
|
131
|
-
case 'get_chart_stock':
|
|
132
|
-
result = await getChartStock(ChartStockParamsSchema.parse(args));
|
|
133
|
-
break;
|
|
134
|
-
case 'get_chart_coin':
|
|
135
|
-
result = await getChartCoin(ChartCoinParamsSchema.parse(args));
|
|
136
|
-
break;
|
|
137
|
-
case 'get_research':
|
|
138
|
-
result = await getResearch(ResearchParamsSchema.parse(args));
|
|
139
|
-
break;
|
|
140
|
-
case 'get_financials':
|
|
141
|
-
result = await getFinancials(FinancialsParamsSchema.parse(args));
|
|
142
|
-
break;
|
|
143
|
-
case 'get_snapshots':
|
|
144
|
-
result = await getSnapshots(SnapshotsParamsSchema.parse(args));
|
|
145
|
-
break;
|
|
146
|
-
case 'search_tags':
|
|
147
|
-
result = await searchTags(TagsSearchParamsSchema.parse(args));
|
|
148
|
-
break;
|
|
149
|
-
case 'match_tags':
|
|
150
|
-
result = await matchTags(TagsMatchParamsSchema.parse(args));
|
|
151
|
-
break;
|
|
152
|
-
case 'get_trends':
|
|
153
|
-
result = await getTrends(TrendsParamsSchema.parse(args));
|
|
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));
|
|
154
83
|
break;
|
|
155
|
-
|
|
156
|
-
|
|
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]);
|
|
157
101
|
}
|
|
158
|
-
return {
|
|
159
|
-
content: [
|
|
160
|
-
{
|
|
161
|
-
type: 'text',
|
|
162
|
-
text: JSON.stringify(result, null, 2),
|
|
163
|
-
},
|
|
164
|
-
],
|
|
165
|
-
};
|
|
166
102
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
content: [
|
|
171
|
-
{
|
|
172
|
-
type: 'text',
|
|
173
|
-
text: `Error: ${errorMessage}`,
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
isError: true,
|
|
177
|
-
};
|
|
103
|
+
async close() {
|
|
104
|
+
return Promise.resolve();
|
|
178
105
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const transport = new StdioServerTransport();
|
|
187
|
-
await server.connect(transport);
|
|
188
|
-
console.error('RagAlgo MCP Server started (Stdio Mode)');
|
|
106
|
+
onclose;
|
|
107
|
+
onerror;
|
|
108
|
+
onmessage;
|
|
109
|
+
handleMessage(message) {
|
|
110
|
+
if (this.onmessage) {
|
|
111
|
+
this.onmessage(message);
|
|
112
|
+
}
|
|
189
113
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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.
|
|
151
|
+
|
|
152
|
+
PRIMARY TOOL for converting names to tag_codes. Without correct tag_code, other tools will return inaccurate or empty results.
|
|
153
|
+
|
|
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
|
|
159
|
+
|
|
160
|
+
Examples: "Apple" → USTK_AAPL, "Samsung" → STK005930, "S&P 500" → ^GSPC
|
|
161
|
+
|
|
162
|
+
CRITICAL: Call this first, then use the returned tag_code in other tools.`,
|
|
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.
|
|
168
|
+
|
|
169
|
+
This is the ONLY tool that returns news + chart + research COMBINED in one call.
|
|
170
|
+
Prefer this over calling get_news + get_chart separately - much more efficient!
|
|
171
|
+
|
|
172
|
+
ALWAYS use when user asks:
|
|
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
|
|
229
|
+
Filter by: zone (STRONG_UP/UP_ZONE/NEUTRAL/DOWN_ZONE/STRONG_DOWN)
|
|
230
|
+
|
|
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.
|
|
238
|
+
|
|
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"
|
|
243
|
+
|
|
244
|
+
Filter by: tag_code, source (mckinsey, goldman, etc.)
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
- Full AI Summary
|
|
248
|
+
- Key Investment Insights
|
|
249
|
+
- Market Outlook (Bullish/Bearish)
|
|
250
|
+
- Tag codes for related assets
|
|
251
|
+
|
|
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.
|
|
258
|
+
|
|
259
|
+
Use for: "Samsung financials", "low PER stocks", "high ROE companies", "undervalued stocks"
|
|
260
|
+
|
|
261
|
+
Returns: PER, PBR, ROE, ROA, revenue, operating_income, net_income, debt_ratio, dividend_yield
|
|
262
|
+
|
|
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.
|
|
269
|
+
|
|
270
|
+
Use for: Analyzing what topics a news title mentions, auto-categorizing text content, finding related tags from a sentence.
|
|
271
|
+
|
|
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.
|
|
279
|
+
|
|
280
|
+
Use for: "Samsung news trend last week", "Bitcoin sentiment this month", "recent 7-day news trend"
|
|
281
|
+
|
|
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;
|
|
344
|
+
};
|
|
345
|
+
if (isStdio) {
|
|
346
|
+
const server = createServer();
|
|
347
|
+
const transport = new StdioServerTransport();
|
|
200
348
|
await server.connect(transport);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
console.error('FATAL STARTUP ERROR:', error);
|
|
502
|
+
process.exit(1);
|
|
218
503
|
}
|
|
219
504
|
}
|
|
220
|
-
main()
|
|
221
|
-
console.error('Fatal error:', error);
|
|
222
|
-
process.exit(1);
|
|
223
|
-
});
|
|
505
|
+
main();
|