market-data-analyzer 2.1.0 → 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.
Files changed (40) hide show
  1. package/dist/index.js +0 -1
  2. package/dist/license.d.ts +6 -13
  3. package/dist/license.d.ts.map +1 -1
  4. package/dist/license.js +19 -36
  5. package/dist/tools/analyze_portfolio.js +0 -1
  6. package/dist/tools/analyze_stock.js +0 -1
  7. package/dist/tools/compare_assets.js +0 -1
  8. package/dist/tools/crypto_analysis.js +0 -1
  9. package/dist/tools/market_overview.js +0 -1
  10. package/dist/tools/screen_stocks.js +0 -1
  11. package/dist/types.js +0 -1
  12. package/dist/utils/api.js +0 -1
  13. package/dist/utils/cache.js +0 -1
  14. package/dist/utils/math.js +0 -1
  15. package/package.json +4 -1
  16. package/dist/index.js.map +0 -1
  17. package/dist/license.js.map +0 -1
  18. package/dist/tools/analyze_portfolio.js.map +0 -1
  19. package/dist/tools/analyze_stock.js.map +0 -1
  20. package/dist/tools/compare_assets.js.map +0 -1
  21. package/dist/tools/crypto_analysis.js.map +0 -1
  22. package/dist/tools/market_overview.js.map +0 -1
  23. package/dist/tools/screen_stocks.js.map +0 -1
  24. package/dist/types.js.map +0 -1
  25. package/dist/utils/api.js.map +0 -1
  26. package/dist/utils/cache.js.map +0 -1
  27. package/dist/utils/math.js.map +0 -1
  28. package/src/index.ts +0 -393
  29. package/src/license.ts +0 -143
  30. package/src/tools/analyze_portfolio.ts +0 -207
  31. package/src/tools/analyze_stock.ts +0 -204
  32. package/src/tools/compare_assets.ts +0 -183
  33. package/src/tools/crypto_analysis.ts +0 -221
  34. package/src/tools/market_overview.ts +0 -236
  35. package/src/tools/screen_stocks.ts +0 -156
  36. package/src/types.ts +0 -175
  37. package/src/utils/api.ts +0 -396
  38. package/src/utils/cache.ts +0 -65
  39. package/src/utils/math.ts +0 -342
  40. package/tsconfig.json +0 -19
package/src/index.ts DELETED
@@ -1,393 +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
- import { checkLicense } from "./license.js";
26
-
27
- // ---------------------------------------------------------------------------
28
- // Tool registration helper (reused for per-session servers in SSE mode)
29
- // ---------------------------------------------------------------------------
30
-
31
- function registerTools(server: McpServer): void {
32
-
33
- // ---------------------------------------------------------------------------
34
- // Tool: analyze_stock
35
- // ---------------------------------------------------------------------------
36
-
37
- server.tool(
38
- "analyze_stock",
39
- "Deep analysis of a stock symbol: price, moving averages (SMA 20/50/200), RSI, MACD, support/resistance levels. Uses live Yahoo Finance data.",
40
- {
41
- symbol: z
42
- .string()
43
- .describe("Stock ticker symbol (e.g. 'AAPL', 'MSFT', 'TSLA')"),
44
- },
45
- async ({ symbol }) => {
46
- const license = checkLicense("market-data-analyzer");
47
- if (!license.valid) {
48
- return { content: [{ type: "text" as const, text: license.message ?? "License required" }], isError: true };
49
- }
50
- try {
51
- const result = await handleAnalyzeStock(symbol.toUpperCase());
52
- const content = [{ type: "text" as const, text: result }];
53
- if (license.trial && license.message) {
54
- content.push({ type: "text" as const, text: "\n\n" + license.message });
55
- }
56
- return { content };
57
- } catch (err) {
58
- return {
59
- content: [
60
- {
61
- type: "text" as const,
62
- text: `Error analyzing ${symbol}: ${err instanceof Error ? err.message : String(err)}`,
63
- },
64
- ],
65
- isError: true,
66
- };
67
- }
68
- },
69
- );
70
-
71
- // ---------------------------------------------------------------------------
72
- // Tool: screen_stocks
73
- // ---------------------------------------------------------------------------
74
-
75
- server.tool(
76
- "screen_stocks",
77
- "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.",
78
- {
79
- min_market_cap: z
80
- .number()
81
- .optional()
82
- .describe("Minimum market cap in USD (e.g. 1000000000 for $1B)"),
83
- max_market_cap: z.number().optional().describe("Maximum market cap in USD"),
84
- min_pe: z.number().optional().describe("Minimum trailing P/E ratio"),
85
- max_pe: z.number().optional().describe("Maximum trailing P/E ratio"),
86
- sector: z
87
- .string()
88
- .optional()
89
- .describe("Sector filter (partial match, e.g. 'tech', 'health', 'energy')"),
90
- min_volume: z.number().optional().describe("Minimum daily trading volume"),
91
- min_price: z.number().optional().describe("Minimum stock price"),
92
- max_price: z.number().optional().describe("Maximum stock price"),
93
- limit: z
94
- .number()
95
- .int()
96
- .min(1)
97
- .max(50)
98
- .optional()
99
- .describe("Max results to return (default: 25)"),
100
- },
101
- async (criteria) => {
102
- const license = checkLicense("market-data-analyzer");
103
- if (!license.valid) {
104
- return { content: [{ type: "text" as const, text: license.message ?? "License required" }], isError: true };
105
- }
106
- try {
107
- const result = await handleScreenStocks(criteria);
108
- const content = [{ type: "text" as const, text: result }];
109
- if (license.trial && license.message) {
110
- content.push({ type: "text" as const, text: "\n\n" + license.message });
111
- }
112
- return { content };
113
- } catch (err) {
114
- return {
115
- content: [
116
- {
117
- type: "text" as const,
118
- text: `Error screening stocks: ${err instanceof Error ? err.message : String(err)}`,
119
- },
120
- ],
121
- isError: true,
122
- };
123
- }
124
- },
125
- );
126
-
127
- // ---------------------------------------------------------------------------
128
- // Tool: analyze_portfolio
129
- // ---------------------------------------------------------------------------
130
-
131
- server.tool(
132
- "analyze_portfolio",
133
- "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.",
134
- {
135
- holdings: z
136
- .array(
137
- z.object({
138
- symbol: z.string().describe("Ticker symbol (e.g. 'AAPL')"),
139
- shares: z.number().describe("Number of shares held"),
140
- avg_cost: z.number().describe("Average cost per share"),
141
- }),
142
- )
143
- .min(1)
144
- .describe("Array of portfolio holdings"),
145
- },
146
- async ({ holdings }) => {
147
- const license = checkLicense("market-data-analyzer");
148
- if (!license.valid) {
149
- return { content: [{ type: "text" as const, text: license.message ?? "License required" }], isError: true };
150
- }
151
- try {
152
- const result = await handleAnalyzePortfolio(holdings);
153
- const content = [{ type: "text" as const, text: result }];
154
- if (license.trial && license.message) {
155
- content.push({ type: "text" as const, text: "\n\n" + license.message });
156
- }
157
- return { content };
158
- } catch (err) {
159
- return {
160
- content: [
161
- {
162
- type: "text" as const,
163
- text: `Error analyzing portfolio: ${err instanceof Error ? err.message : String(err)}`,
164
- },
165
- ],
166
- isError: true,
167
- };
168
- }
169
- },
170
- );
171
-
172
- // ---------------------------------------------------------------------------
173
- // Tool: compare_assets
174
- // ---------------------------------------------------------------------------
175
-
176
- server.tool(
177
- "compare_assets",
178
- "Compare 2-5 assets side by side: returns, volatility, correlation, Sharpe ratio approximation over a given period. Uses Yahoo Finance historical data.",
179
- {
180
- symbols: z
181
- .array(z.string())
182
- .min(2)
183
- .max(5)
184
- .describe("Array of 2-5 ticker symbols to compare (e.g. ['AAPL', 'MSFT', 'GOOGL'])"),
185
- period: z
186
- .enum(["1mo", "3mo", "6mo", "1y", "2y", "5y"])
187
- .optional()
188
- .describe("Comparison period (default: '6mo')"),
189
- },
190
- async ({ symbols, period }) => {
191
- const license = checkLicense("market-data-analyzer");
192
- if (!license.valid) {
193
- return { content: [{ type: "text" as const, text: license.message ?? "License required" }], isError: true };
194
- }
195
- try {
196
- const result = await handleCompareAssets(
197
- symbols.map((s) => s.toUpperCase()),
198
- period ?? "6mo",
199
- );
200
- const content = [{ type: "text" as const, text: result }];
201
- if (license.trial && license.message) {
202
- content.push({ type: "text" as const, text: "\n\n" + license.message });
203
- }
204
- return { content };
205
- } catch (err) {
206
- return {
207
- content: [
208
- {
209
- type: "text" as const,
210
- text: `Error comparing assets: ${err instanceof Error ? err.message : String(err)}`,
211
- },
212
- ],
213
- isError: true,
214
- };
215
- }
216
- },
217
- );
218
-
219
- // ---------------------------------------------------------------------------
220
- // Tool: market_overview
221
- // ---------------------------------------------------------------------------
222
-
223
- server.tool(
224
- "market_overview",
225
- "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.",
226
- {},
227
- async () => {
228
- const license = checkLicense("market-data-analyzer");
229
- if (!license.valid) {
230
- return { content: [{ type: "text" as const, text: license.message ?? "License required" }], isError: true };
231
- }
232
- try {
233
- const result = await handleMarketOverview();
234
- const content = [{ type: "text" as const, text: result }];
235
- if (license.trial && license.message) {
236
- content.push({ type: "text" as const, text: "\n\n" + license.message });
237
- }
238
- return { content };
239
- } catch (err) {
240
- return {
241
- content: [
242
- {
243
- type: "text" as const,
244
- text: `Error fetching market overview: ${err instanceof Error ? err.message : String(err)}`,
245
- },
246
- ],
247
- isError: true,
248
- };
249
- }
250
- },
251
- );
252
-
253
- // ---------------------------------------------------------------------------
254
- // Tool: crypto_analysis
255
- // ---------------------------------------------------------------------------
256
-
257
- server.tool(
258
- "crypto_analysis",
259
- "Crypto-specific analysis: price, 24h volume, market dominance, fear/greed index approximation, supply metrics, ATH/ATL data. Uses CoinGecko free API.",
260
- {
261
- symbol: z
262
- .string()
263
- .describe("Cryptocurrency symbol or name (e.g. 'BTC', 'ETH', 'SOL', 'bitcoin')"),
264
- },
265
- async ({ symbol }) => {
266
- const license = checkLicense("market-data-analyzer");
267
- if (!license.valid) {
268
- return { content: [{ type: "text" as const, text: license.message ?? "License required" }], isError: true };
269
- }
270
- try {
271
- const result = await handleCryptoAnalysis(symbol);
272
- const content = [{ type: "text" as const, text: result }];
273
- if (license.trial && license.message) {
274
- content.push({ type: "text" as const, text: "\n\n" + license.message });
275
- }
276
- return { content };
277
- } catch (err) {
278
- return {
279
- content: [
280
- {
281
- type: "text" as const,
282
- text: `Error analyzing crypto ${symbol}: ${err instanceof Error ? err.message : String(err)}`,
283
- },
284
- ],
285
- isError: true,
286
- };
287
- }
288
- },
289
- );
290
-
291
- }
292
-
293
- // ---------------------------------------------------------------------------
294
- // Server setup (for stdio mode)
295
- // ---------------------------------------------------------------------------
296
-
297
- function createServer(): McpServer {
298
- const s = new McpServer({
299
- name: "market-data-analyzer",
300
- version: "2.0.0",
301
- });
302
- registerTools(s);
303
- return s;
304
- }
305
-
306
- // ---------------------------------------------------------------------------
307
- // Transport: stdio or SSE
308
- // ---------------------------------------------------------------------------
309
-
310
- async function main(): Promise<void> {
311
- const useSSE = process.argv.includes("--sse");
312
-
313
- if (useSSE) {
314
- const port = parseInt(process.env.PORT ?? "3000", 10);
315
- const transports = new Map<string, SSEServerTransport>();
316
-
317
- const httpServer = http.createServer(async (req, res) => {
318
- // CORS headers
319
- res.setHeader("Access-Control-Allow-Origin", "*");
320
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
321
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
322
-
323
- if (req.method === "OPTIONS") {
324
- res.writeHead(204);
325
- res.end();
326
- return;
327
- }
328
-
329
- const url = new URL(req.url ?? "/", `http://localhost:${port}`);
330
-
331
- if (url.pathname === "/sse" && req.method === "GET") {
332
- const transport = new SSEServerTransport("/messages", res);
333
- const sessionId = transport.sessionId;
334
- transports.set(sessionId, transport);
335
-
336
- res.on("close", () => {
337
- transports.delete(sessionId);
338
- });
339
-
340
- // Each SSE session gets its own McpServer instance
341
- const sessionServer = createServer();
342
- await sessionServer.connect(transport);
343
- return;
344
- }
345
-
346
- if (url.pathname === "/messages" && req.method === "POST") {
347
- const sessionId = url.searchParams.get("sessionId");
348
- if (!sessionId || !transports.has(sessionId)) {
349
- res.writeHead(400, { "Content-Type": "application/json" });
350
- res.end(JSON.stringify({ error: "Invalid or missing sessionId" }));
351
- return;
352
- }
353
- const transport = transports.get(sessionId)!;
354
- await transport.handlePostMessage(req, res);
355
- return;
356
- }
357
-
358
- if (url.pathname === "/health") {
359
- res.writeHead(200, { "Content-Type": "application/json" });
360
- res.end(
361
- JSON.stringify({
362
- status: "ok",
363
- server: "market-data-analyzer",
364
- version: "2.0.0",
365
- }),
366
- );
367
- return;
368
- }
369
-
370
- res.writeHead(404, { "Content-Type": "application/json" });
371
- res.end(JSON.stringify({ error: "Not found" }));
372
- });
373
-
374
- httpServer.listen(port, () => {
375
- console.error(
376
- `Market Data Analyzer MCP server (SSE) listening on port ${port}`,
377
- );
378
- console.error(` SSE endpoint: http://localhost:${port}/sse`);
379
- console.error(` Messages endpoint: http://localhost:${port}/messages`);
380
- console.error(` Health check: http://localhost:${port}/health`);
381
- });
382
- } else {
383
- const server = createServer();
384
- const transport = new StdioServerTransport();
385
- await server.connect(transport);
386
- console.error("Market Data Analyzer MCP server running on stdio");
387
- }
388
- }
389
-
390
- main().catch((err) => {
391
- console.error("Fatal error:", err);
392
- process.exit(1);
393
- });
package/src/license.ts DELETED
@@ -1,143 +0,0 @@
1
- /**
2
- * Shared license validation for AIVP MCP servers.
3
- *
4
- * License keys are HMAC-SHA256 signed strings in the format:
5
- * base64(email:product:expiry):signature
6
- *
7
- * The server validates locally — no network call needed.
8
- *
9
- * Without a key, users get a free trial (limited calls).
10
- * With a valid key, usage is unlimited.
11
- */
12
-
13
- import { createHmac } from "node:crypto";
14
-
15
- // This secret is used to sign/verify license keys.
16
- // In production, this should be injected via env var, but for local
17
- // validation of HMAC-signed keys, it must be embedded.
18
- const LICENSE_SECRET = process.env.AIVP_LICENSE_SECRET ?? "aivp-mcp-2026-s3cr3t-k3y";
19
-
20
- const FREE_TRIAL_LIMIT = 3;
21
-
22
- // In-memory call counter for free trial
23
- const callCounts = new Map<string, number>();
24
-
25
- export interface LicenseResult {
26
- valid: boolean;
27
- message?: string;
28
- trial?: boolean;
29
- remaining?: number;
30
- }
31
-
32
- /**
33
- * Generate a license key for a given email and product.
34
- * Used by the key generation script, not by the server itself.
35
- */
36
- export function generateLicenseKey(
37
- email: string,
38
- product: string,
39
- expiryDays: number = 365,
40
- ): string {
41
- const expiry = new Date(
42
- Date.now() + expiryDays * 24 * 60 * 60 * 1000,
43
- ).toISOString().split("T")[0];
44
- const payload = `${email}:${product}:${expiry}`;
45
- const payloadB64 = Buffer.from(payload).toString("base64url");
46
- const signature = createHmac("sha256", LICENSE_SECRET)
47
- .update(payload)
48
- .digest("base64url");
49
- return `${payloadB64}.${signature}`;
50
- }
51
-
52
- /**
53
- * Validate a license key for a given product.
54
- * Returns { valid: true } if the key is valid for this product.
55
- */
56
- export function validateLicenseKey(
57
- key: string,
58
- product: string,
59
- ): LicenseResult {
60
- try {
61
- const [payloadB64, signature] = key.split(".");
62
- if (!payloadB64 || !signature) {
63
- return { valid: false, message: "Invalid key format" };
64
- }
65
-
66
- const payload = Buffer.from(payloadB64, "base64url").toString("utf-8");
67
- const [email, keyProduct, expiry] = payload.split(":");
68
-
69
- if (!email || !keyProduct || !expiry) {
70
- return { valid: false, message: "Invalid key format" };
71
- }
72
-
73
- // Verify signature
74
- const expectedSignature = createHmac("sha256", LICENSE_SECRET)
75
- .update(payload)
76
- .digest("base64url");
77
-
78
- if (signature !== expectedSignature) {
79
- return { valid: false, message: "Invalid license key" };
80
- }
81
-
82
- // Check product match (allow "all" for bundle keys)
83
- if (keyProduct !== product && keyProduct !== "all") {
84
- return {
85
- valid: false,
86
- message: `This key is for "${keyProduct}", not "${product}"`,
87
- };
88
- }
89
-
90
- // Check expiry
91
- const expiryDate = new Date(expiry);
92
- if (isNaN(expiryDate.getTime()) || expiryDate < new Date()) {
93
- return { valid: false, message: "License key has expired" };
94
- }
95
-
96
- return { valid: true };
97
- } catch {
98
- return { valid: false, message: "Invalid license key" };
99
- }
100
- }
101
-
102
- /**
103
- * Check if the current call is allowed.
104
- * Call this at the start of every tool handler.
105
- *
106
- * - If LICENSE_KEY env var is set and valid: always allowed
107
- * - If no key: allowed for FREE_TRIAL_LIMIT calls, then blocked
108
- */
109
- export function checkLicense(product: string): LicenseResult {
110
- const key = process.env.LICENSE_KEY ?? process.env.AIVP_LICENSE_KEY;
111
-
112
- if (key) {
113
- const result = validateLicenseKey(key, product);
114
- if (result.valid) {
115
- return { valid: true };
116
- }
117
- // Invalid key — still return the error, don't fall through to trial
118
- return result;
119
- }
120
-
121
- // No key — free trial mode
122
- const count = (callCounts.get(product) ?? 0) + 1;
123
- callCounts.set(product, count);
124
-
125
- if (count <= FREE_TRIAL_LIMIT) {
126
- return {
127
- valid: true,
128
- trial: true,
129
- remaining: FREE_TRIAL_LIMIT - count,
130
- message:
131
- count === FREE_TRIAL_LIMIT
132
- ? `⚠️ Free trial ended. Purchase a license at https://aivp-mcp.vercel.app`
133
- : `Free trial: ${FREE_TRIAL_LIMIT - count} calls remaining. Get unlimited access at https://aivp-mcp.vercel.app`,
134
- };
135
- }
136
-
137
- return {
138
- valid: false,
139
- trial: true,
140
- remaining: 0,
141
- message: `🔒 Free trial limit reached (${FREE_TRIAL_LIMIT} calls). Purchase a license at https://aivp-mcp.vercel.app to continue using this tool.`,
142
- };
143
- }