market-data-analyzer 2.0.1 → 2.1.1

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/src/index.ts DELETED
@@ -1,344 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Market Data Analyzer -- MCP Server
5
- *
6
- * Finance-focused MCP server with live data from Yahoo Finance and CoinGecko.
7
- * Tools: analyze_stock, screen_stocks, analyze_portfolio, compare_assets,
8
- * market_overview, crypto_analysis.
9
- *
10
- * Transports: stdio (default) or SSE (--sse flag).
11
- */
12
-
13
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
16
- import { z } from "zod";
17
- import http from "node:http";
18
-
19
- import { handleAnalyzeStock } from "./tools/analyze_stock.js";
20
- import { handleScreenStocks } from "./tools/screen_stocks.js";
21
- import { handleAnalyzePortfolio } from "./tools/analyze_portfolio.js";
22
- import { handleCompareAssets } from "./tools/compare_assets.js";
23
- import { handleMarketOverview } from "./tools/market_overview.js";
24
- import { handleCryptoAnalysis } from "./tools/crypto_analysis.js";
25
-
26
- // ---------------------------------------------------------------------------
27
- // Tool registration helper (reused for per-session servers in SSE mode)
28
- // ---------------------------------------------------------------------------
29
-
30
- function registerTools(server: McpServer): void {
31
-
32
- // ---------------------------------------------------------------------------
33
- // Tool: analyze_stock
34
- // ---------------------------------------------------------------------------
35
-
36
- server.tool(
37
- "analyze_stock",
38
- "Deep analysis of a stock symbol: price, moving averages (SMA 20/50/200), RSI, MACD, support/resistance levels. Uses live Yahoo Finance data.",
39
- {
40
- symbol: z
41
- .string()
42
- .describe("Stock ticker symbol (e.g. 'AAPL', 'MSFT', 'TSLA')"),
43
- },
44
- async ({ symbol }) => {
45
- try {
46
- const result = await handleAnalyzeStock(symbol.toUpperCase());
47
- return { content: [{ type: "text" as const, text: result }] };
48
- } catch (err) {
49
- return {
50
- content: [
51
- {
52
- type: "text" as const,
53
- text: `Error analyzing ${symbol}: ${err instanceof Error ? err.message : String(err)}`,
54
- },
55
- ],
56
- isError: true,
57
- };
58
- }
59
- },
60
- );
61
-
62
- // ---------------------------------------------------------------------------
63
- // Tool: screen_stocks
64
- // ---------------------------------------------------------------------------
65
-
66
- server.tool(
67
- "screen_stocks",
68
- "Screen stocks by criteria: market cap range, P/E ratio, sector, volume threshold. Returns top matches with key metrics. Fetches live data from Yahoo Finance for ~70 popular stocks.",
69
- {
70
- min_market_cap: z
71
- .number()
72
- .optional()
73
- .describe("Minimum market cap in USD (e.g. 1000000000 for $1B)"),
74
- max_market_cap: z.number().optional().describe("Maximum market cap in USD"),
75
- min_pe: z.number().optional().describe("Minimum trailing P/E ratio"),
76
- max_pe: z.number().optional().describe("Maximum trailing P/E ratio"),
77
- sector: z
78
- .string()
79
- .optional()
80
- .describe("Sector filter (partial match, e.g. 'tech', 'health', 'energy')"),
81
- min_volume: z.number().optional().describe("Minimum daily trading volume"),
82
- min_price: z.number().optional().describe("Minimum stock price"),
83
- max_price: z.number().optional().describe("Maximum stock price"),
84
- limit: z
85
- .number()
86
- .int()
87
- .min(1)
88
- .max(50)
89
- .optional()
90
- .describe("Max results to return (default: 25)"),
91
- },
92
- async (criteria) => {
93
- try {
94
- const result = await handleScreenStocks(criteria);
95
- return { content: [{ type: "text" as const, text: result }] };
96
- } catch (err) {
97
- return {
98
- content: [
99
- {
100
- type: "text" as const,
101
- text: `Error screening stocks: ${err instanceof Error ? err.message : String(err)}`,
102
- },
103
- ],
104
- isError: true,
105
- };
106
- }
107
- },
108
- );
109
-
110
- // ---------------------------------------------------------------------------
111
- // Tool: analyze_portfolio
112
- // ---------------------------------------------------------------------------
113
-
114
- server.tool(
115
- "analyze_portfolio",
116
- "Analyze a portfolio of holdings. Provide positions with symbol, shares, and average cost. Returns total value, P&L per position, allocation %, diversification score, and risk metrics. Fetches live prices from Yahoo Finance.",
117
- {
118
- holdings: z
119
- .array(
120
- z.object({
121
- symbol: z.string().describe("Ticker symbol (e.g. 'AAPL')"),
122
- shares: z.number().describe("Number of shares held"),
123
- avg_cost: z.number().describe("Average cost per share"),
124
- }),
125
- )
126
- .min(1)
127
- .describe("Array of portfolio holdings"),
128
- },
129
- async ({ holdings }) => {
130
- try {
131
- const result = await handleAnalyzePortfolio(holdings);
132
- return { content: [{ type: "text" as const, text: result }] };
133
- } catch (err) {
134
- return {
135
- content: [
136
- {
137
- type: "text" as const,
138
- text: `Error analyzing portfolio: ${err instanceof Error ? err.message : String(err)}`,
139
- },
140
- ],
141
- isError: true,
142
- };
143
- }
144
- },
145
- );
146
-
147
- // ---------------------------------------------------------------------------
148
- // Tool: compare_assets
149
- // ---------------------------------------------------------------------------
150
-
151
- server.tool(
152
- "compare_assets",
153
- "Compare 2-5 assets side by side: returns, volatility, correlation, Sharpe ratio approximation over a given period. Uses Yahoo Finance historical data.",
154
- {
155
- symbols: z
156
- .array(z.string())
157
- .min(2)
158
- .max(5)
159
- .describe("Array of 2-5 ticker symbols to compare (e.g. ['AAPL', 'MSFT', 'GOOGL'])"),
160
- period: z
161
- .enum(["1mo", "3mo", "6mo", "1y", "2y", "5y"])
162
- .optional()
163
- .describe("Comparison period (default: '6mo')"),
164
- },
165
- async ({ symbols, period }) => {
166
- try {
167
- const result = await handleCompareAssets(
168
- symbols.map((s) => s.toUpperCase()),
169
- period ?? "6mo",
170
- );
171
- return { content: [{ type: "text" as const, text: result }] };
172
- } catch (err) {
173
- return {
174
- content: [
175
- {
176
- type: "text" as const,
177
- text: `Error comparing assets: ${err instanceof Error ? err.message : String(err)}`,
178
- },
179
- ],
180
- isError: true,
181
- };
182
- }
183
- },
184
- );
185
-
186
- // ---------------------------------------------------------------------------
187
- // Tool: market_overview
188
- // ---------------------------------------------------------------------------
189
-
190
- server.tool(
191
- "market_overview",
192
- "Current market snapshot: major indices (S&P 500, NASDAQ, DOW), sector performance via ETFs, market breadth indicators, VIX, crypto, and commodities. Live data from Yahoo Finance.",
193
- {},
194
- async () => {
195
- try {
196
- const result = await handleMarketOverview();
197
- return { content: [{ type: "text" as const, text: result }] };
198
- } catch (err) {
199
- return {
200
- content: [
201
- {
202
- type: "text" as const,
203
- text: `Error fetching market overview: ${err instanceof Error ? err.message : String(err)}`,
204
- },
205
- ],
206
- isError: true,
207
- };
208
- }
209
- },
210
- );
211
-
212
- // ---------------------------------------------------------------------------
213
- // Tool: crypto_analysis
214
- // ---------------------------------------------------------------------------
215
-
216
- server.tool(
217
- "crypto_analysis",
218
- "Crypto-specific analysis: price, 24h volume, market dominance, fear/greed index approximation, supply metrics, ATH/ATL data. Uses CoinGecko free API.",
219
- {
220
- symbol: z
221
- .string()
222
- .describe("Cryptocurrency symbol or name (e.g. 'BTC', 'ETH', 'SOL', 'bitcoin')"),
223
- },
224
- async ({ symbol }) => {
225
- try {
226
- const result = await handleCryptoAnalysis(symbol);
227
- return { content: [{ type: "text" as const, text: result }] };
228
- } catch (err) {
229
- return {
230
- content: [
231
- {
232
- type: "text" as const,
233
- text: `Error analyzing crypto ${symbol}: ${err instanceof Error ? err.message : String(err)}`,
234
- },
235
- ],
236
- isError: true,
237
- };
238
- }
239
- },
240
- );
241
-
242
- }
243
-
244
- // ---------------------------------------------------------------------------
245
- // Server setup (for stdio mode)
246
- // ---------------------------------------------------------------------------
247
-
248
- function createServer(): McpServer {
249
- const s = new McpServer({
250
- name: "market-data-analyzer",
251
- version: "2.0.0",
252
- });
253
- registerTools(s);
254
- return s;
255
- }
256
-
257
- // ---------------------------------------------------------------------------
258
- // Transport: stdio or SSE
259
- // ---------------------------------------------------------------------------
260
-
261
- async function main(): Promise<void> {
262
- const useSSE = process.argv.includes("--sse");
263
-
264
- if (useSSE) {
265
- const port = parseInt(process.env.PORT ?? "3000", 10);
266
- const transports = new Map<string, SSEServerTransport>();
267
-
268
- const httpServer = http.createServer(async (req, res) => {
269
- // CORS headers
270
- res.setHeader("Access-Control-Allow-Origin", "*");
271
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
272
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
273
-
274
- if (req.method === "OPTIONS") {
275
- res.writeHead(204);
276
- res.end();
277
- return;
278
- }
279
-
280
- const url = new URL(req.url ?? "/", `http://localhost:${port}`);
281
-
282
- if (url.pathname === "/sse" && req.method === "GET") {
283
- const transport = new SSEServerTransport("/messages", res);
284
- const sessionId = transport.sessionId;
285
- transports.set(sessionId, transport);
286
-
287
- res.on("close", () => {
288
- transports.delete(sessionId);
289
- });
290
-
291
- // Each SSE session gets its own McpServer instance
292
- const sessionServer = createServer();
293
- await sessionServer.connect(transport);
294
- return;
295
- }
296
-
297
- if (url.pathname === "/messages" && req.method === "POST") {
298
- const sessionId = url.searchParams.get("sessionId");
299
- if (!sessionId || !transports.has(sessionId)) {
300
- res.writeHead(400, { "Content-Type": "application/json" });
301
- res.end(JSON.stringify({ error: "Invalid or missing sessionId" }));
302
- return;
303
- }
304
- const transport = transports.get(sessionId)!;
305
- await transport.handlePostMessage(req, res);
306
- return;
307
- }
308
-
309
- if (url.pathname === "/health") {
310
- res.writeHead(200, { "Content-Type": "application/json" });
311
- res.end(
312
- JSON.stringify({
313
- status: "ok",
314
- server: "market-data-analyzer",
315
- version: "2.0.0",
316
- }),
317
- );
318
- return;
319
- }
320
-
321
- res.writeHead(404, { "Content-Type": "application/json" });
322
- res.end(JSON.stringify({ error: "Not found" }));
323
- });
324
-
325
- httpServer.listen(port, () => {
326
- console.error(
327
- `Market Data Analyzer MCP server (SSE) listening on port ${port}`,
328
- );
329
- console.error(` SSE endpoint: http://localhost:${port}/sse`);
330
- console.error(` Messages endpoint: http://localhost:${port}/messages`);
331
- console.error(` Health check: http://localhost:${port}/health`);
332
- });
333
- } else {
334
- const server = createServer();
335
- const transport = new StdioServerTransport();
336
- await server.connect(transport);
337
- console.error("Market Data Analyzer MCP server running on stdio");
338
- }
339
- }
340
-
341
- main().catch((err) => {
342
- console.error("Fatal error:", err);
343
- process.exit(1);
344
- });
@@ -1,207 +0,0 @@
1
- /**
2
- * analyze_portfolio -- Portfolio analytics with live prices.
3
- *
4
- * Takes an array of { symbol, shares, avg_cost } holdings, fetches
5
- * current prices from Yahoo Finance, and returns P&L, allocation,
6
- * diversification score, and risk metrics.
7
- */
8
-
9
- import { yahooQuote, SYMBOL_SECTORS } from "../utils/api.js";
10
- import { formatCurrency } from "../utils/math.js";
11
-
12
- export interface PortfolioHolding {
13
- symbol: string;
14
- shares: number;
15
- avg_cost: number;
16
- }
17
-
18
- interface PositionResult {
19
- symbol: string;
20
- name: string;
21
- shares: number;
22
- avgCost: number;
23
- currentPrice: number;
24
- marketValue: number;
25
- costBasis: number;
26
- pnl: number;
27
- pnlPercent: number;
28
- allocationPercent: number;
29
- sector: string;
30
- }
31
-
32
- export async function handleAnalyzePortfolio(
33
- holdings: PortfolioHolding[],
34
- ): Promise<string> {
35
- if (holdings.length === 0) {
36
- throw new Error("At least one holding is required.");
37
- }
38
-
39
- // Fetch live quotes for all symbols
40
- const symbols = holdings.map((h) => h.symbol);
41
- const quotes = await yahooQuote(symbols);
42
- const quoteMap = new Map(quotes.map((q) => [q.symbol, q]));
43
-
44
- // Build position analysis
45
- let totalValue = 0;
46
- let totalCost = 0;
47
-
48
- const positions: PositionResult[] = holdings.map((h) => {
49
- const q = quoteMap.get(h.symbol);
50
- const currentPrice = q?.regularMarketPrice ?? 0;
51
- const marketValue = h.shares * currentPrice;
52
- const costBasis = h.shares * h.avg_cost;
53
- totalValue += marketValue;
54
- totalCost += costBasis;
55
-
56
- return {
57
- symbol: h.symbol,
58
- name: q?.shortName ?? q?.longName ?? h.symbol,
59
- shares: h.shares,
60
- avgCost: h.avg_cost,
61
- currentPrice,
62
- marketValue,
63
- costBasis,
64
- pnl: marketValue - costBasis,
65
- pnlPercent: costBasis > 0 ? ((marketValue - costBasis) / costBasis) * 100 : 0,
66
- allocationPercent: 0, // calculated below
67
- sector: q?.sector ?? SYMBOL_SECTORS[h.symbol]?.sector ?? "Unknown",
68
- };
69
- });
70
-
71
- // Calculate allocation percentages
72
- for (const p of positions) {
73
- p.allocationPercent = totalValue > 0 ? (p.marketValue / totalValue) * 100 : 0;
74
- }
75
-
76
- // Sort by market value descending
77
- positions.sort((a, b) => b.marketValue - a.marketValue);
78
-
79
- // Sector breakdown
80
- const sectorAlloc: Record<string, number> = {};
81
- for (const p of positions) {
82
- sectorAlloc[p.sector] = (sectorAlloc[p.sector] ?? 0) + p.allocationPercent;
83
- }
84
-
85
- // Diversification score (HHI-based, 0-100 where 100 = perfectly diversified)
86
- const weights = positions.map((p) => p.allocationPercent / 100);
87
- const hhi = weights.reduce((s, w) => s + w * w, 0);
88
- const n = weights.length;
89
- const minHHI = n > 0 ? 1 / n : 1;
90
- const denom = 1 - minHHI;
91
- const divScore = n <= 1 || denom === 0 || totalValue === 0
92
- ? 0
93
- : Math.max(0, Math.min(100, Math.round((1 - (hhi - minHHI) / denom) * 100)));
94
-
95
- // Risk warnings
96
- const warnings: string[] = [];
97
-
98
- for (const p of positions) {
99
- if (p.allocationPercent > 30) {
100
- warnings.push(`HIGH CONCENTRATION: ${p.symbol} is ${p.allocationPercent.toFixed(1)}% of portfolio.`);
101
- }
102
- }
103
-
104
- for (const [sector, pct] of Object.entries(sectorAlloc)) {
105
- if (pct > 50) {
106
- warnings.push(`SECTOR RISK: ${sector} represents ${pct.toFixed(1)}% of portfolio.`);
107
- }
108
- }
109
-
110
- for (const p of positions) {
111
- if (p.pnlPercent < -20) {
112
- warnings.push(`SIGNIFICANT LOSS: ${p.symbol} is down ${Math.abs(p.pnlPercent).toFixed(1)}%.`);
113
- }
114
- }
115
-
116
- if (n < 5) {
117
- warnings.push(`LOW DIVERSIFICATION: Only ${n} position(s). Consider adding more holdings.`);
118
- }
119
-
120
- if (Object.keys(sectorAlloc).length < 3 && n >= 3) {
121
- warnings.push("LIMITED SECTOR EXPOSURE: Portfolio spans fewer than 3 sectors.");
122
- }
123
-
124
- // Any positions where we couldn't fetch a price?
125
- const missingPrices = positions.filter((p) => p.currentPrice === 0);
126
- for (const p of missingPrices) {
127
- warnings.push(`MISSING DATA: Could not fetch price for ${p.symbol}. Values may be inaccurate.`);
128
- }
129
-
130
- // Format output
131
- const lines: string[] = [];
132
-
133
- lines.push("# Portfolio Analysis");
134
- lines.push("");
135
-
136
- // Summary
137
- lines.push("## Summary");
138
- lines.push("");
139
- lines.push("| Metric | Value |");
140
- lines.push("|--------|-------|");
141
- lines.push(`| Total Value | ${formatCurrency(totalValue)} |`);
142
- lines.push(`| Total Cost Basis | ${formatCurrency(totalCost)} |`);
143
- const totalPnl = totalValue - totalCost;
144
- const totalPnlPct = totalCost > 0 ? ((totalPnl) / totalCost) * 100 : 0;
145
- lines.push(`| Total P&L | ${totalPnl >= 0 ? "+" : ""}${formatCurrency(totalPnl)} (${totalPnlPct >= 0 ? "+" : ""}${totalPnlPct.toFixed(2)}%) |`);
146
- lines.push(`| Positions | ${n} |`);
147
- lines.push(`| Sectors | ${Object.keys(sectorAlloc).length} |`);
148
- lines.push(`| Diversification Score | ${divScore}/100 |`);
149
- lines.push("");
150
-
151
- // Positions
152
- lines.push("## Positions");
153
- lines.push("");
154
- lines.push("| Symbol | Name | Shares | Avg Cost | Price | Value | P&L | P&L% | Alloc% |");
155
- lines.push("|--------|------|--------|----------|-------|-------|-----|------|--------|");
156
-
157
- for (const p of positions) {
158
- const pnlSign = p.pnl >= 0 ? "+" : "";
159
- lines.push(
160
- `| ${p.symbol} | ${p.name.slice(0, 20)} | ${p.shares} | $${p.avgCost.toFixed(2)} | $${p.currentPrice.toFixed(2)} | ${formatCurrency(p.marketValue)} | ${pnlSign}${formatCurrency(p.pnl)} | ${pnlSign}${p.pnlPercent.toFixed(2)}% | ${p.allocationPercent.toFixed(1)}% |`,
161
- );
162
- }
163
- lines.push("");
164
-
165
- // Sector allocation
166
- lines.push("## Sector Allocation");
167
- lines.push("");
168
- lines.push("| Sector | Allocation |");
169
- lines.push("|--------|------------|");
170
- const sortedSectors = Object.entries(sectorAlloc).sort((a, b) => b[1] - a[1]);
171
- for (const [sector, pct] of sortedSectors) {
172
- const bar = "=".repeat(Math.round(pct / 2));
173
- lines.push(`| ${sector} | ${pct.toFixed(1)}% ${bar} |`);
174
- }
175
- lines.push("");
176
-
177
- // Winners and losers
178
- const winners = [...positions].sort((a, b) => b.pnlPercent - a.pnlPercent);
179
- if (winners.length > 0) {
180
- lines.push("## Top Performers");
181
- lines.push("");
182
- const top3 = winners.slice(0, Math.min(3, winners.length));
183
- for (const p of top3) {
184
- lines.push(`- **${p.symbol}**: ${p.pnlPercent >= 0 ? "+" : ""}${p.pnlPercent.toFixed(2)}% (${p.pnl >= 0 ? "+" : ""}${formatCurrency(p.pnl)})`);
185
- }
186
- lines.push("");
187
- const bottom3 = winners.slice(-Math.min(3, winners.length)).reverse();
188
- lines.push("## Underperformers");
189
- lines.push("");
190
- for (const p of bottom3) {
191
- lines.push(`- **${p.symbol}**: ${p.pnlPercent >= 0 ? "+" : ""}${p.pnlPercent.toFixed(2)}% (${p.pnl >= 0 ? "+" : ""}${formatCurrency(p.pnl)})`);
192
- }
193
- lines.push("");
194
- }
195
-
196
- // Risk warnings
197
- if (warnings.length > 0) {
198
- lines.push("## Risk Warnings");
199
- lines.push("");
200
- for (const w of warnings) {
201
- lines.push(`- ${w}`);
202
- }
203
- lines.push("");
204
- }
205
-
206
- return lines.join("\n");
207
- }