ragalgo-mcp-server 1.0.5 → 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,223 +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';
21
+ import { v4 as uuidv4 } from 'uuid';
14
22
  import { zodToJsonSchema } from 'zod-to-json-schema';
15
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
16
- // Tools
17
- import { getNews, getNewsScored, NewsParamsSchema, NewsScoredParamsSchema } from './tools/news.js';
18
- import { getChartStock, getChartCoin, ChartStockParamsSchema, ChartCoinParamsSchema } from './tools/chart.js';
19
- import { getFinancials, FinancialsParamsSchema } from './tools/financials.js';
20
- import { getSnapshots, SnapshotsParamsSchema } from './tools/snapshots.js';
21
- import { searchTags, matchTags, TagsSearchParamsSchema, TagsMatchParamsSchema } from './tools/tags.js';
22
- import { getTrends, TrendsParamsSchema } from './tools/trends.js';
23
- import { getResearch, ResearchParamsSchema } from './tools/research.js';
24
- // MCP Server
25
- const server = new Server({
26
- name: 'RagAlgo',
27
- version: '1.0.2',
28
- }, {
29
- capabilities: {
30
- tools: {},
31
- },
32
- });
33
- // Tool definitions with improved descriptions for better AI routing
34
- server.setRequestHandler(ListToolsRequestSchema, async () => {
35
- return {
36
- tools: [
37
- // ============================================================
38
- // 🏷️ TAG TOOLS - MUST USE FIRST!
39
- // ============================================================
40
- {
41
- name: 'search_tags',
42
- description: `🔍 [TAG LOOKUP - USE FIRST] ALWAYS use this BEFORE other RagAlgo tools when user mentions any Korean stock, coin, or theme by NAME.
43
-
44
- PRIMARY TOOL for converting names to tag_codes. Without correct tag_code, other tools will return inaccurate or empty results.`,
45
- inputSchema: zodToJsonSchema(TagsSearchParamsSchema),
46
- },
47
- // ============================================================
48
- // 📊 SUMMARY TOOL - MOST EFFICIENT!
49
- // ============================================================
50
- {
51
- name: 'get_snapshots',
52
- description: `📊 [DAILY SUMMARY - MOST EFFICIENT] PRIMARY TOOL for Korean market overview. ALWAYS use this FIRST for general market questions.
53
-
54
- This is the ONLY tool that returns news + chart + sentiment COMBINED in one call.
55
- Prefer this over calling get_news + get_chart separately - much more efficient!`,
56
- inputSchema: zodToJsonSchema(SnapshotsParamsSchema),
57
- },
58
- // ============================================================
59
- // 📰 NEWS TOOLS
60
- // ============================================================
61
- {
62
- name: 'get_news_scored',
63
- description: `📰 [KOREAN NEWS WITH SENTIMENT] PRIMARY news tool for Korean market. Returns news WITH AI sentiment scores (-10 to +10).`,
64
- inputSchema: zodToJsonSchema(NewsScoredParamsSchema),
65
- },
66
- {
67
- name: 'get_news',
68
- description: `📰 [KOREAN NEWS - NO SCORES] Basic news without sentiment analysis.`,
69
- inputSchema: zodToJsonSchema(NewsParamsSchema),
70
- },
71
- // ============================================================
72
- // 📈 CHART/TECHNICAL ANALYSIS TOOLS
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
- default:
156
- throw new Error(`Unknown tool: ${name}`);
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
- catch (error) {
168
- const errorMessage = error instanceof Error ? error.message : String(error);
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
- // Start server
181
- // Start server
182
- async function main() {
183
- // Check if running in stdio mode (command line argument or specific env var)
184
- const isStdio = process.argv.includes('--stdio');
185
- if (isStdio) {
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
- else {
191
- // SSE / HTTP Mode (Default for deployment)
192
- const app = express();
193
- const port = process.env.PORT || 8080;
194
- app.use(cors());
195
- app.use(express.json());
196
- let transport = null;
197
- app.get('/sse', async (req, res) => {
198
- console.log('New SSE connection established');
199
- transport = new SSEServerTransport('/messages', res);
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
- app.post('/messages', async (req, res) => {
203
- if (transport) {
204
- await transport.handlePostMessage(req, res);
205
- }
206
- else {
207
- res.status(404).json({ error: 'Session not found or connection not established' });
208
- }
209
- });
210
- app.get('/health', (req, res) => {
211
- res.status(200).json({ status: 'ok', version: '1.0.2' });
212
- });
213
- app.listen(port, () => {
214
- console.log(`RagAlgo MCP Server listening on port ${port} (SSE Mode)`);
215
- console.log(`- SSE Endpoint: http://localhost:${port}/sse`);
216
- console.log(`- Health Check: http://localhost:${port}/health`);
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().catch((error) => {
221
- console.error('Fatal error:', error);
222
- process.exit(1);
223
- });
505
+ main();