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.
- package/dist/index.js +0 -1
- package/dist/license.d.ts +6 -13
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +19 -36
- package/dist/tools/analyze_portfolio.js +0 -1
- package/dist/tools/analyze_stock.js +0 -1
- package/dist/tools/compare_assets.js +0 -1
- package/dist/tools/crypto_analysis.js +0 -1
- package/dist/tools/market_overview.js +0 -1
- package/dist/tools/screen_stocks.js +0 -1
- package/dist/types.js +0 -1
- package/dist/utils/api.js +0 -1
- package/dist/utils/cache.js +0 -1
- package/dist/utils/math.js +0 -1
- package/package.json +4 -1
- package/dist/index.js.map +0 -1
- package/dist/license.js.map +0 -1
- package/dist/tools/analyze_portfolio.js.map +0 -1
- package/dist/tools/analyze_stock.js.map +0 -1
- package/dist/tools/compare_assets.js.map +0 -1
- package/dist/tools/crypto_analysis.js.map +0 -1
- package/dist/tools/market_overview.js.map +0 -1
- package/dist/tools/screen_stocks.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils/api.js.map +0 -1
- package/dist/utils/cache.js.map +0 -1
- package/dist/utils/math.js.map +0 -1
- package/src/index.ts +0 -393
- package/src/license.ts +0 -143
- package/src/tools/analyze_portfolio.ts +0 -207
- package/src/tools/analyze_stock.ts +0 -204
- package/src/tools/compare_assets.ts +0 -183
- package/src/tools/crypto_analysis.ts +0 -221
- package/src/tools/market_overview.ts +0 -236
- package/src/tools/screen_stocks.ts +0 -156
- package/src/types.ts +0 -175
- package/src/utils/api.ts +0 -396
- package/src/utils/cache.ts +0 -65
- package/src/utils/math.ts +0 -342
- 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
|
-
}
|