xstock-mcp 1.1.0 → 1.2.0

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
@@ -26,7 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/server.ts
27
27
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
28
28
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
29
- var import_types6 = require("@modelcontextprotocol/sdk/types.js");
29
+ var import_types7 = require("@modelcontextprotocol/sdk/types.js");
30
30
 
31
31
  // src/tools/quotes.ts
32
32
  var import_zod = require("zod");
@@ -169,6 +169,7 @@ async function fetchHKKline(code, period = "daily") {
169
169
 
170
170
  // src/data/yahoo.ts
171
171
  var import_yahoo_finance2 = __toESM(require("yahoo-finance2"));
172
+ var import_axios2 = __toESM(require("axios"));
172
173
  var yf = new import_yahoo_finance2.default({ suppressNotices: ["yahooSurvey"] });
173
174
  async function fetchUSQuotes(symbols) {
174
175
  process.stderr.write(`[yahoo] fetchUSQuotes symbols=${symbols.join(",")}
@@ -254,6 +255,119 @@ async function fetchEarningsCalendar(symbol) {
254
255
  recentEPS
255
256
  };
256
257
  }
258
+ async function fetchFinancials(symbol, quarterly = false) {
259
+ process.stderr.write(`[yahoo] fetchFinancials symbol=${symbol} quarterly=${quarterly}
260
+ `);
261
+ const modules = quarterly ? ["incomeStatementHistoryQuarterly", "cashflowStatementHistoryQuarterly", "balanceSheetHistoryQuarterly"] : ["incomeStatementHistory", "cashflowStatementHistory", "balanceSheetHistory"];
262
+ const summary = await yf.quoteSummary(symbol, { modules });
263
+ const incomeKey = quarterly ? "incomeStatementHistoryQuarterly" : "incomeStatementHistory";
264
+ const cashKey = quarterly ? "cashflowStatementHistoryQuarterly" : "cashflowStatementHistory";
265
+ const balanceKey = quarterly ? "balanceSheetHistoryQuarterly" : "balanceSheetHistory";
266
+ const incomeArr = summary[incomeKey]?.incomeStatementHistory ?? [];
267
+ const cashArr = summary[cashKey]?.cashflowStatements ?? [];
268
+ const balanceArr = summary[balanceKey]?.balanceSheetStatements ?? [];
269
+ return incomeArr.slice(0, 4).map((inc, i) => {
270
+ const cash = cashArr[i] ?? {};
271
+ const bal = balanceArr[i] ?? {};
272
+ const date = inc.endDate instanceof Date ? inc.endDate.toISOString().slice(0, 10) : String(inc.endDate ?? "");
273
+ const revenue = inc.totalRevenue ?? null;
274
+ const gross = inc.grossProfit ?? null;
275
+ const net = inc.netIncome ?? null;
276
+ const opCash = cash.totalCashFromOperatingActivities ?? null;
277
+ const capex = cash.capitalExpenditures ?? null;
278
+ const debt = bal.totalDebt ?? (bal.longTermDebt ?? null);
279
+ const equity = bal.totalStockholderEquity ?? null;
280
+ return {
281
+ date,
282
+ totalRevenue: revenue,
283
+ grossProfit: gross,
284
+ grossMargin: revenue && gross ? Math.round(gross / revenue * 1e4) / 100 : null,
285
+ netIncome: net,
286
+ netMargin: revenue && net ? Math.round(net / revenue * 1e4) / 100 : null,
287
+ epsDiluted: inc.dilutedEPS ?? null,
288
+ operatingCashFlow: opCash,
289
+ freeCashFlow: opCash !== null && capex !== null ? opCash + capex : null,
290
+ totalDebt: debt,
291
+ stockholdersEquity: equity,
292
+ debtToEquity: debt !== null && equity !== null && equity !== 0 ? Math.round(debt / equity * 100) / 100 : null
293
+ };
294
+ });
295
+ }
296
+ async function fetchAnalystRating(symbol) {
297
+ process.stderr.write(`[yahoo] fetchAnalystRating symbol=${symbol}
298
+ `);
299
+ const summary = await yf.quoteSummary(symbol, {
300
+ modules: ["financialData", "recommendationTrend", "upgradeDowngradeHistory"]
301
+ });
302
+ const fin = summary.financialData;
303
+ const trend = summary.recommendationTrend;
304
+ const history = summary.upgradeDowngradeHistory;
305
+ const trendArr = trend?.trend ?? [];
306
+ const latest = trendArr[0] ?? {};
307
+ const changes = (history?.history ?? []).slice(0, 10).map((h) => ({
308
+ date: h.epochGradeDate instanceof Date ? h.epochGradeDate.toISOString().slice(0, 10) : String(h.epochGradeDate ?? ""),
309
+ firm: String(h.firm ?? ""),
310
+ action: String(h.action ?? ""),
311
+ from: h.fromGrade ? String(h.fromGrade) : null,
312
+ to: h.toGrade ? String(h.toGrade) : null
313
+ }));
314
+ return {
315
+ symbol,
316
+ consensus: fin?.recommendationKey ?? null,
317
+ targetPriceMean: fin?.targetMeanPrice ?? null,
318
+ targetPriceHigh: fin?.targetHighPrice ?? null,
319
+ targetPriceLow: fin?.targetLowPrice ?? null,
320
+ distribution: {
321
+ strongBuy: Number(latest.strongBuy ?? 0),
322
+ buy: Number(latest.buy ?? 0),
323
+ hold: Number(latest.hold ?? 0),
324
+ sell: Number(latest.sell ?? 0),
325
+ strongSell: Number(latest.strongSell ?? 0)
326
+ },
327
+ recentChanges: changes
328
+ };
329
+ }
330
+ function extractXmlTag(xml, tag) {
331
+ const match = xml.match(new RegExp(`<${tag}[^>]*>(?:<!\\[CDATA\\[)?([\\s\\S]*?)(?:\\]\\]>)?</${tag}>`, "i"));
332
+ return match ? match[1].trim() : "";
333
+ }
334
+ async function fetchStockNews(symbol, limit = 20) {
335
+ process.stderr.write(`[yahoo] fetchStockNews symbol=${symbol}
336
+ `);
337
+ const url = `https://feeds.finance.yahoo.com/rss/2.0/headline?s=${symbol}&region=US&lang=en-US`;
338
+ const res = await import_axios2.default.get(url, { timeout: 8e3, responseType: "text" });
339
+ const xml = res.data;
340
+ const items = [];
341
+ const itemMatches = xml.match(/<item>[\s\S]*?<\/item>/g) ?? [];
342
+ for (const item of itemMatches.slice(0, limit)) {
343
+ items.push({
344
+ title: extractXmlTag(item, "title"),
345
+ link: extractXmlTag(item, "link") || extractXmlTag(item, "guid"),
346
+ summary: extractXmlTag(item, "description").replace(/<[^>]+>/g, "").slice(0, 200),
347
+ pubDate: extractXmlTag(item, "pubDate")
348
+ });
349
+ }
350
+ process.stderr.write(`[yahoo] fetchStockNews \u8FD4\u56DE ${items.length} \u6761
351
+ `);
352
+ return items;
353
+ }
354
+ async function fetchInsiderActivity(symbol) {
355
+ process.stderr.write(`[yahoo] fetchInsiderActivity symbol=${symbol}
356
+ `);
357
+ const summary = await yf.quoteSummary(symbol, { modules: ["insiderTransactions"] });
358
+ const raw = summary.insiderTransactions;
359
+ const txArr = raw?.transactions ?? [];
360
+ return txArr.slice(0, 20).map((tx) => ({
361
+ date: tx.startDate instanceof Date ? tx.startDate.toISOString().slice(0, 10) : String(tx.startDate ?? ""),
362
+ name: String(tx.filerName ?? ""),
363
+ relation: String(tx.filerRelation ?? ""),
364
+ transactionType: String(tx.transactionDescription ?? ""),
365
+ shares: tx.shares ?? null,
366
+ value: tx.value ?? null,
367
+ sharesBefore: tx.shareholderBefore ?? null,
368
+ sharesAfter: tx.shareholderAfter ?? null
369
+ }));
370
+ }
257
371
  async function fetchStockProfile(symbol) {
258
372
  process.stderr.write(`[yahoo] fetchStockProfile symbol=${symbol}
259
373
  `);
@@ -286,7 +400,7 @@ async function fetchStockProfile(symbol) {
286
400
  }
287
401
 
288
402
  // src/data/binance.ts
289
- var import_axios2 = __toESM(require("axios"));
403
+ var import_axios3 = __toESM(require("axios"));
290
404
  function normalizeSymbol(symbol) {
291
405
  const s = symbol.toUpperCase();
292
406
  return s.endsWith("USDT") ? s : s + "USDT";
@@ -298,7 +412,7 @@ async function fetchCryptoTickers(symbols) {
298
412
  symbols.map(async (raw) => {
299
413
  const sym = normalizeSymbol(raw);
300
414
  try {
301
- const res = await import_axios2.default.get(
415
+ const res = await import_axios3.default.get(
302
416
  `https://api.binance.com/api/v3/ticker/24hr?symbol=${sym}`,
303
417
  { timeout: 5e3 }
304
418
  );
@@ -331,11 +445,11 @@ async function fetchMarketSentiment(symbol, period = "1h") {
331
445
  const base = "https://fapi.binance.com";
332
446
  const params = `symbol=${sym}&period=${period}&limit=1`;
333
447
  const [oiRes, globalRes, topAccRes, topPosRes, takerRes] = await Promise.all([
334
- import_axios2.default.get(`${base}/fapi/v1/openInterest?symbol=${sym}`, { timeout: 5e3 }),
335
- import_axios2.default.get(`${base}/futures/data/globalLongShortAccountRatio?${params}`, { timeout: 5e3 }),
336
- import_axios2.default.get(`${base}/futures/data/topLongShortAccountRatio?${params}`, { timeout: 5e3 }),
337
- import_axios2.default.get(`${base}/futures/data/topLongShortPositionRatio?${params}`, { timeout: 5e3 }),
338
- import_axios2.default.get(`${base}/futures/data/takerlongshortRatio?${params}`, { timeout: 5e3 })
448
+ import_axios3.default.get(`${base}/fapi/v1/openInterest?symbol=${sym}`, { timeout: 5e3 }),
449
+ import_axios3.default.get(`${base}/futures/data/globalLongShortAccountRatio?${params}`, { timeout: 5e3 }),
450
+ import_axios3.default.get(`${base}/futures/data/topLongShortAccountRatio?${params}`, { timeout: 5e3 }),
451
+ import_axios3.default.get(`${base}/futures/data/topLongShortPositionRatio?${params}`, { timeout: 5e3 }),
452
+ import_axios3.default.get(`${base}/futures/data/takerlongshortRatio?${params}`, { timeout: 5e3 })
339
453
  ]);
340
454
  const oi = oiRes.data;
341
455
  const global = globalRes.data[0] ?? {};
@@ -360,7 +474,7 @@ async function fetchCryptoKlines(symbol, interval = "1d", limit = 100) {
360
474
  const sym = normalizeSymbol(symbol);
361
475
  process.stderr.write(`[binance] fetchCryptoKlines symbol=${sym} interval=${interval} limit=${limit}
362
476
  `);
363
- const res = await import_axios2.default.get(
477
+ const res = await import_axios3.default.get(
364
478
  `https://api.binance.com/api/v3/klines?symbol=${sym}&interval=${interval}&limit=${limit}`,
365
479
  { timeout: 5e3 }
366
480
  );
@@ -909,16 +1023,16 @@ var getStockProfileTool = {
909
1023
 
910
1024
  // src/tools/crypto.ts
911
1025
  var import_zod5 = require("zod");
912
- var import_axios4 = __toESM(require("axios"));
1026
+ var import_axios5 = __toESM(require("axios"));
913
1027
 
914
1028
  // src/data/coingecko.ts
915
- var import_axios3 = __toESM(require("axios"));
1029
+ var import_axios4 = __toESM(require("axios"));
916
1030
  var BASE = "https://api.coingecko.com/api/v3";
917
1031
  var FEAR_GREED_URL = "https://api.alternative.me/fng/?limit=1";
918
1032
  async function fetchGlobalMarket() {
919
1033
  process.stderr.write(`[coingecko] fetchGlobalMarket
920
1034
  `);
921
- const res = await import_axios3.default.get(`${BASE}/global`, { timeout: 8e3 });
1035
+ const res = await import_axios4.default.get(`${BASE}/global`, { timeout: 8e3 });
922
1036
  const d = res.data.data;
923
1037
  const result = {
924
1038
  totalMarketCapUsd: d.total_market_cap?.usd ?? 0,
@@ -935,7 +1049,7 @@ async function fetchGlobalMarket() {
935
1049
  async function fetchTopCoins(limit = 50) {
936
1050
  process.stderr.write(`[coingecko] fetchTopCoins limit=${limit}
937
1051
  `);
938
- const res = await import_axios3.default.get(`${BASE}/coins/markets`, {
1052
+ const res = await import_axios4.default.get(`${BASE}/coins/markets`, {
939
1053
  timeout: 8e3,
940
1054
  params: {
941
1055
  vs_currency: "usd",
@@ -962,7 +1076,7 @@ async function fetchTopCoins(limit = 50) {
962
1076
  async function fetchCategories() {
963
1077
  process.stderr.write(`[coingecko] fetchCategories
964
1078
  `);
965
- const res = await import_axios3.default.get(`${BASE}/coins/categories`, { timeout: 8e3 });
1079
+ const res = await import_axios4.default.get(`${BASE}/coins/categories`, { timeout: 8e3 });
966
1080
  const categories = res.data.map((c) => ({
967
1081
  id: c.id,
968
1082
  name: c.name,
@@ -978,7 +1092,7 @@ async function fetchCategories() {
978
1092
  async function fetchFearGreed() {
979
1093
  process.stderr.write(`[coingecko] fetchFearGreed
980
1094
  `);
981
- const res = await import_axios3.default.get(FEAR_GREED_URL, { timeout: 5e3 });
1095
+ const res = await import_axios4.default.get(FEAR_GREED_URL, { timeout: 5e3 });
982
1096
  const d = res.data.data[0];
983
1097
  const result = {
984
1098
  value: parseInt(d.value),
@@ -1089,7 +1203,7 @@ var getCryptoFundingTool = {
1089
1203
  symbols.map(async (raw) => {
1090
1204
  const sym = raw.toUpperCase().endsWith("USDT") ? raw.toUpperCase() : raw.toUpperCase() + "USDT";
1091
1205
  try {
1092
- const res = await import_axios4.default.get(
1206
+ const res = await import_axios5.default.get(
1093
1207
  `https://fapi.binance.com/fapi/v1/premiumIndex?symbol=${sym}`,
1094
1208
  { timeout: 5e3 }
1095
1209
  );
@@ -1161,6 +1275,143 @@ var getCryptoLiquidationTool = {
1161
1275
  }
1162
1276
  };
1163
1277
 
1278
+ // src/tools/us-fundamentals.ts
1279
+ var import_zod6 = require("zod");
1280
+ var SymbolInput2 = import_zod6.z.object({ symbol: import_zod6.z.string().min(1) });
1281
+ var FinancialsInput = import_zod6.z.object({
1282
+ symbol: import_zod6.z.string().min(1),
1283
+ quarterly: import_zod6.z.boolean().optional()
1284
+ });
1285
+ var getFinancialsTool = {
1286
+ tool: {
1287
+ name: "get_financials",
1288
+ description: "\u83B7\u53D6\u7F8E\u80A1\u8D22\u52A1\u6570\u636E\uFF1A\u8425\u6536\u3001\u6BDB\u5229\u7387\u3001\u51C0\u5229\u6DA6\u3001\u51C0\u5229\u7387\u3001EPS\u3001\u81EA\u7531\u73B0\u91D1\u6D41\u3001\u8D1F\u503A\u7387\u3002\u9ED8\u8BA4\u8FD4\u56DE\u8FD14\u5E74\u5E74\u62A5\uFF0Cquarterly=true \u8FD4\u56DE\u8FD14\u5B63\u5EA6\u5B63\u62A5\u3002",
1289
+ inputSchema: {
1290
+ type: "object",
1291
+ properties: {
1292
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA" },
1293
+ quarterly: { type: "boolean", description: "true=\u5B63\u62A5\uFF0Cfalse=\u5E74\u62A5\uFF08\u9ED8\u8BA4\uFF09" }
1294
+ },
1295
+ required: ["symbol"]
1296
+ }
1297
+ },
1298
+ handler: async (input) => {
1299
+ const parsed = FinancialsInput.safeParse(input);
1300
+ if (!parsed.success) return err(parsed.error.message);
1301
+ try {
1302
+ return ok(await fetchFinancials(parsed.data.symbol, parsed.data.quarterly ?? false));
1303
+ } catch (e) {
1304
+ return err(e.message);
1305
+ }
1306
+ }
1307
+ };
1308
+ var getAnalystRatingTool = {
1309
+ tool: {
1310
+ name: "get_analyst_rating",
1311
+ description: "\u83B7\u53D6\u534E\u5C14\u8857\u5206\u6790\u5E08\u8BC4\u7EA7\uFF1A\u5171\u8BC6\u8BC4\u7EA7\uFF08buy/hold/sell\uFF09\u3001\u5E73\u5747\u76EE\u6807\u4EF7\u3001\u9AD8\u4F4E\u76EE\u6807\u4EF7\u3001\u5F3A\u4E70/\u4E70/\u6301\u6709/\u5356/\u5F3A\u5356\u4EBA\u6570\u5206\u5E03\uFF0C\u4EE5\u53CA\u6700\u8FD110\u6761\u8BC4\u7EA7\u53D8\u52A8\u8BB0\u5F55\u3002",
1312
+ inputSchema: {
1313
+ type: "object",
1314
+ properties: {
1315
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA" }
1316
+ },
1317
+ required: ["symbol"]
1318
+ }
1319
+ },
1320
+ handler: async (input) => {
1321
+ const parsed = SymbolInput2.safeParse(input);
1322
+ if (!parsed.success) return err(parsed.error.message);
1323
+ try {
1324
+ return ok(await fetchAnalystRating(parsed.data.symbol));
1325
+ } catch (e) {
1326
+ return err(e.message);
1327
+ }
1328
+ }
1329
+ };
1330
+ var NewsInput = import_zod6.z.object({
1331
+ symbol: import_zod6.z.string().min(1),
1332
+ limit: import_zod6.z.number().int().min(1).max(30).optional()
1333
+ });
1334
+ var getStockNewsTool = {
1335
+ tool: {
1336
+ name: "get_stock_news",
1337
+ description: "\u83B7\u53D6\u7F8E\u80A1\u4E2A\u80A1\u6700\u65B0\u65B0\u95FB\uFF08\u8FD17\u5929\uFF09\uFF0C\u542B\u6807\u9898\u3001\u6458\u8981\u3001\u94FE\u63A5\u3001\u53D1\u5E03\u65F6\u95F4\u3002\u6700\u591A30\u6761\uFF0C\u9ED8\u8BA415\u6761\u3002",
1338
+ inputSchema: {
1339
+ type: "object",
1340
+ properties: {
1341
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA" },
1342
+ limit: { type: "number", description: "\u8FD4\u56DE\u6761\u6570\uFF0C\u9ED8\u8BA415\uFF0C\u6700\u592730" }
1343
+ },
1344
+ required: ["symbol"]
1345
+ }
1346
+ },
1347
+ handler: async (input) => {
1348
+ const parsed = NewsInput.safeParse(input);
1349
+ if (!parsed.success) return err(parsed.error.message);
1350
+ try {
1351
+ return ok(await fetchStockNews(parsed.data.symbol, parsed.data.limit ?? 15));
1352
+ } catch (e) {
1353
+ return err(e.message);
1354
+ }
1355
+ }
1356
+ };
1357
+ var getInsiderActivityTool = {
1358
+ tool: {
1359
+ name: "get_insider_activity",
1360
+ description: "\u83B7\u53D6\u7F8E\u80A1\u5185\u90E8\u4EBA\uFF08\u9AD8\u7BA1/\u8463\u4E8B\uFF09\u8FD1\u671F\u4E70\u5356\u8BB0\u5F55\uFF0C\u57FA\u4E8E SEC Form 4 \u516C\u5F00\u6570\u636E\u3002\u542B\u4EA4\u6613\u4EBA\u59D3\u540D\u3001\u804C\u4F4D\u3001\u4EA4\u6613\u7C7B\u578B\u3001\u80A1\u6570\u3001\u4EA4\u6613\u91D1\u989D\u3002",
1361
+ inputSchema: {
1362
+ type: "object",
1363
+ properties: {
1364
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA" }
1365
+ },
1366
+ required: ["symbol"]
1367
+ }
1368
+ },
1369
+ handler: async (input) => {
1370
+ const parsed = SymbolInput2.safeParse(input);
1371
+ if (!parsed.success) return err(parsed.error.message);
1372
+ try {
1373
+ return ok(await fetchInsiderActivity(parsed.data.symbol));
1374
+ } catch (e) {
1375
+ return err(e.message);
1376
+ }
1377
+ }
1378
+ };
1379
+ var getStockFullOverviewTool = {
1380
+ tool: {
1381
+ name: "get_stock_full_overview",
1382
+ description: "\u4E00\u6B21\u8C03\u7528\u83B7\u53D6\u7F8E\u80A1\u5B8C\u6574\u5206\u6790\u6570\u636E\uFF1A\u5B9E\u65F6\u884C\u60C5 + \u516C\u53F8\u57FA\u672C\u9762 + \u5206\u6790\u5E08\u8BC4\u7EA7 + \u8FD1\u671F\u65B0\u95FB\uFF08\u5E76\u884C\u83B7\u53D6\uFF09\u3002\u9002\u5408\u56DE\u7B54\u7EFC\u5408\u5206\u6790\u7C7B\u95EE\u9898\uFF08\u5982\uFF1A\u5E2E\u6211\u5206\u6790\u4E00\u4E0B NVDA\uFF09\uFF0C\u907F\u514D\u591A\u6B21\u8C03\u7528\u4E0D\u540C\u5DE5\u5177\u3002",
1383
+ inputSchema: {
1384
+ type: "object",
1385
+ properties: {
1386
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA\u3001TSLA" }
1387
+ },
1388
+ required: ["symbol"]
1389
+ }
1390
+ },
1391
+ handler: async (input) => {
1392
+ const parsed = SymbolInput2.safeParse(input);
1393
+ if (!parsed.success) return err(parsed.error.message);
1394
+ const { symbol } = parsed.data;
1395
+ try {
1396
+ const [quotes, profile, analyst, news] = await Promise.allSettled([
1397
+ fetchUSQuotes([symbol]),
1398
+ fetchStockProfile(symbol),
1399
+ fetchAnalystRating(symbol),
1400
+ fetchStockNews(symbol, 10)
1401
+ ]);
1402
+ return ok({
1403
+ symbol,
1404
+ quote: quotes.status === "fulfilled" ? quotes.value[0] ?? null : null,
1405
+ profile: profile.status === "fulfilled" ? profile.value : null,
1406
+ analyst: analyst.status === "fulfilled" ? analyst.value : null,
1407
+ news: news.status === "fulfilled" ? news.value : []
1408
+ });
1409
+ } catch (e) {
1410
+ return err(e.message);
1411
+ }
1412
+ }
1413
+ };
1414
+
1164
1415
  // src/tools/index.ts
1165
1416
  var tools = [
1166
1417
  getQuoteTool,
@@ -1175,7 +1426,12 @@ var tools = [
1175
1426
  getCryptoTopTool,
1176
1427
  getCryptoCatsTool,
1177
1428
  getCryptoFundingTool,
1178
- getCryptoLiquidationTool
1429
+ getCryptoLiquidationTool,
1430
+ getFinancialsTool,
1431
+ getAnalystRatingTool,
1432
+ getStockNewsTool,
1433
+ getInsiderActivityTool,
1434
+ getStockFullOverviewTool
1179
1435
  ];
1180
1436
  var toolMap = new Map(
1181
1437
  tools.map((t) => [t.tool.name, t])
@@ -1186,10 +1442,10 @@ var server = new import_server.Server(
1186
1442
  { name: "stock-mcp", version: "1.0.0" },
1187
1443
  { capabilities: { tools: {} } }
1188
1444
  );
1189
- server.setRequestHandler(import_types6.ListToolsRequestSchema, async () => ({
1445
+ server.setRequestHandler(import_types7.ListToolsRequestSchema, async () => ({
1190
1446
  tools: tools.map((t) => t.tool)
1191
1447
  }));
1192
- server.setRequestHandler(import_types6.CallToolRequestSchema, async (request) => {
1448
+ server.setRequestHandler(import_types7.CallToolRequestSchema, async (request) => {
1193
1449
  const { name, arguments: args } = request.params;
1194
1450
  const def = toolMap.get(name);
1195
1451
  if (!def) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xstock-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Stock & Crypto analysis MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/data/yahoo.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { default as YahooFinance } from "yahoo-finance2";
2
+ import axios from "axios";
2
3
 
3
4
  // v3 需要实例化
4
5
  const yf = new YahooFinance({ suppressNotices: ["yahooSurvey"] });
@@ -158,6 +159,187 @@ export async function fetchEarningsCalendar(symbol: string): Promise<EarningsCal
158
159
  };
159
160
  }
160
161
 
162
+ // ─── Financials ───────────────────────────────────────────────────────────────
163
+
164
+ export interface FinancialPeriod {
165
+ date: string;
166
+ totalRevenue: number | null;
167
+ grossProfit: number | null;
168
+ grossMargin: number | null;
169
+ netIncome: number | null;
170
+ netMargin: number | null;
171
+ epsDiluted: number | null;
172
+ operatingCashFlow: number | null;
173
+ freeCashFlow: number | null;
174
+ totalDebt: number | null;
175
+ stockholdersEquity: number | null;
176
+ debtToEquity: number | null;
177
+ }
178
+
179
+ export async function fetchFinancials(symbol: string, quarterly = false): Promise<FinancialPeriod[]> {
180
+ process.stderr.write(`[yahoo] fetchFinancials symbol=${symbol} quarterly=${quarterly}\n`);
181
+ const modules = quarterly
182
+ ? ["incomeStatementHistoryQuarterly", "cashflowStatementHistoryQuarterly", "balanceSheetHistoryQuarterly"]
183
+ : ["incomeStatementHistory", "cashflowStatementHistory", "balanceSheetHistory"];
184
+
185
+ const summary = await yf.quoteSummary(symbol, { modules: modules as never[] });
186
+
187
+ const incomeKey = quarterly ? "incomeStatementHistoryQuarterly" : "incomeStatementHistory";
188
+ const cashKey = quarterly ? "cashflowStatementHistoryQuarterly" : "cashflowStatementHistory";
189
+ const balanceKey = quarterly ? "balanceSheetHistoryQuarterly" : "balanceSheetHistory";
190
+
191
+ const incomeArr = ((summary[incomeKey as keyof typeof summary] as Record<string, unknown>)
192
+ ?.incomeStatementHistory as Record<string, unknown>[]) ?? [];
193
+ const cashArr = ((summary[cashKey as keyof typeof summary] as Record<string, unknown>)
194
+ ?.cashflowStatements as Record<string, unknown>[]) ?? [];
195
+ const balanceArr = ((summary[balanceKey as keyof typeof summary] as Record<string, unknown>)
196
+ ?.balanceSheetStatements as Record<string, unknown>[]) ?? [];
197
+
198
+ return incomeArr.slice(0, 4).map((inc, i) => {
199
+ const cash = cashArr[i] ?? {};
200
+ const bal = balanceArr[i] ?? {};
201
+ const date = inc.endDate instanceof Date ? inc.endDate.toISOString().slice(0, 10) : String(inc.endDate ?? "");
202
+ const revenue = inc.totalRevenue as number | null ?? null;
203
+ const gross = inc.grossProfit as number | null ?? null;
204
+ const net = inc.netIncome as number | null ?? null;
205
+ const opCash = cash.totalCashFromOperatingActivities as number | null ?? null;
206
+ const capex = cash.capitalExpenditures as number | null ?? null;
207
+ const debt = bal.totalDebt as number | null ?? (bal.longTermDebt as number | null ?? null);
208
+ const equity = bal.totalStockholderEquity as number | null ?? null;
209
+
210
+ return {
211
+ date,
212
+ totalRevenue: revenue,
213
+ grossProfit: gross,
214
+ grossMargin: revenue && gross ? Math.round((gross / revenue) * 10000) / 100 : null,
215
+ netIncome: net,
216
+ netMargin: revenue && net ? Math.round((net / revenue) * 10000) / 100 : null,
217
+ epsDiluted: inc.dilutedEPS as number | null ?? null,
218
+ operatingCashFlow: opCash,
219
+ freeCashFlow: opCash !== null && capex !== null ? opCash + capex : null,
220
+ totalDebt: debt,
221
+ stockholdersEquity: equity,
222
+ debtToEquity: debt !== null && equity !== null && equity !== 0
223
+ ? Math.round((debt / equity) * 100) / 100 : null,
224
+ };
225
+ });
226
+ }
227
+
228
+ // ─── Analyst Rating ───────────────────────────────────────────────────────────
229
+
230
+ export interface AnalystRating {
231
+ symbol: string;
232
+ consensus: string | null;
233
+ targetPriceMean: number | null;
234
+ targetPriceHigh: number | null;
235
+ targetPriceLow: number | null;
236
+ distribution: { strongBuy: number; buy: number; hold: number; sell: number; strongSell: number };
237
+ recentChanges: { date: string; firm: string; action: string; from: string | null; to: string | null }[];
238
+ }
239
+
240
+ export async function fetchAnalystRating(symbol: string): Promise<AnalystRating> {
241
+ process.stderr.write(`[yahoo] fetchAnalystRating symbol=${symbol}\n`);
242
+ const summary = await yf.quoteSummary(symbol, {
243
+ modules: ["financialData", "recommendationTrend", "upgradeDowngradeHistory"],
244
+ });
245
+
246
+ const fin = summary.financialData as Record<string, unknown> | undefined;
247
+ const trend = summary.recommendationTrend as Record<string, unknown> | undefined;
248
+ const history = summary.upgradeDowngradeHistory as Record<string, unknown> | undefined;
249
+
250
+ const trendArr = (trend?.trend as Record<string, unknown>[]) ?? [];
251
+ const latest = trendArr[0] ?? {};
252
+
253
+ const changes = ((history?.history as Record<string, unknown>[]) ?? []).slice(0, 10).map((h) => ({
254
+ date: h.epochGradeDate instanceof Date ? h.epochGradeDate.toISOString().slice(0, 10) : String(h.epochGradeDate ?? ""),
255
+ firm: String(h.firm ?? ""),
256
+ action: String(h.action ?? ""),
257
+ from: h.fromGrade ? String(h.fromGrade) : null,
258
+ to: h.toGrade ? String(h.toGrade) : null,
259
+ }));
260
+
261
+ return {
262
+ symbol,
263
+ consensus: fin?.recommendationKey as string | null ?? null,
264
+ targetPriceMean: fin?.targetMeanPrice as number | null ?? null,
265
+ targetPriceHigh: fin?.targetHighPrice as number | null ?? null,
266
+ targetPriceLow: fin?.targetLowPrice as number | null ?? null,
267
+ distribution: {
268
+ strongBuy: Number(latest.strongBuy ?? 0),
269
+ buy: Number(latest.buy ?? 0),
270
+ hold: Number(latest.hold ?? 0),
271
+ sell: Number(latest.sell ?? 0),
272
+ strongSell: Number(latest.strongSell ?? 0),
273
+ },
274
+ recentChanges: changes,
275
+ };
276
+ }
277
+
278
+ // ─── News ─────────────────────────────────────────────────────────────────────
279
+
280
+ export interface StockNewsItem {
281
+ title: string;
282
+ link: string;
283
+ summary: string;
284
+ pubDate: string;
285
+ }
286
+
287
+ function extractXmlTag(xml: string, tag: string): string {
288
+ const match = xml.match(new RegExp(`<${tag}[^>]*>(?:<!\\[CDATA\\[)?([\\s\\S]*?)(?:\\]\\]>)?</${tag}>`, "i"));
289
+ return match ? match[1].trim() : "";
290
+ }
291
+
292
+ export async function fetchStockNews(symbol: string, limit = 20): Promise<StockNewsItem[]> {
293
+ process.stderr.write(`[yahoo] fetchStockNews symbol=${symbol}\n`);
294
+ const url = `https://feeds.finance.yahoo.com/rss/2.0/headline?s=${symbol}&region=US&lang=en-US`;
295
+ const res = await axios.get<string>(url, { timeout: 8000, responseType: "text" });
296
+ const xml = res.data;
297
+
298
+ const items: StockNewsItem[] = [];
299
+ const itemMatches = xml.match(/<item>[\s\S]*?<\/item>/g) ?? [];
300
+ for (const item of itemMatches.slice(0, limit)) {
301
+ items.push({
302
+ title: extractXmlTag(item, "title"),
303
+ link: extractXmlTag(item, "link") || extractXmlTag(item, "guid"),
304
+ summary: extractXmlTag(item, "description").replace(/<[^>]+>/g, "").slice(0, 200),
305
+ pubDate: extractXmlTag(item, "pubDate"),
306
+ });
307
+ }
308
+ process.stderr.write(`[yahoo] fetchStockNews 返回 ${items.length} 条\n`);
309
+ return items;
310
+ }
311
+
312
+ // ─── Insider Activity ─────────────────────────────────────────────────────────
313
+
314
+ export interface InsiderTransaction {
315
+ date: string;
316
+ name: string;
317
+ relation: string;
318
+ transactionType: string;
319
+ shares: number | null;
320
+ value: number | null;
321
+ sharesBefore: number | null;
322
+ sharesAfter: number | null;
323
+ }
324
+
325
+ export async function fetchInsiderActivity(symbol: string): Promise<InsiderTransaction[]> {
326
+ process.stderr.write(`[yahoo] fetchInsiderActivity symbol=${symbol}\n`);
327
+ const summary = await yf.quoteSummary(symbol, { modules: ["insiderTransactions"] });
328
+ const raw = summary.insiderTransactions as Record<string, unknown> | undefined;
329
+ const txArr = (raw?.transactions as Record<string, unknown>[]) ?? [];
330
+
331
+ return txArr.slice(0, 20).map((tx) => ({
332
+ date: tx.startDate instanceof Date ? tx.startDate.toISOString().slice(0, 10) : String(tx.startDate ?? ""),
333
+ name: String(tx.filerName ?? ""),
334
+ relation: String(tx.filerRelation ?? ""),
335
+ transactionType: String(tx.transactionDescription ?? ""),
336
+ shares: tx.shares as number | null ?? null,
337
+ value: tx.value as number | null ?? null,
338
+ sharesBefore: tx.shareholderBefore as number | null ?? null,
339
+ sharesAfter: tx.shareholderAfter as number | null ?? null,
340
+ }));
341
+ }
342
+
161
343
  export async function fetchStockProfile(symbol: string): Promise<StockProfile> {
162
344
  process.stderr.write(`[yahoo] fetchStockProfile symbol=${symbol}\n`);
163
345
  const summary = await yf.quoteSummary(symbol, {
@@ -4,6 +4,7 @@ import { getKlineTool, getKlineWithIndicatorsTool } from "./kline";
4
4
  import { searchStockTool } from "./search";
5
5
  import { getUsIndicesTool, getStockProfileTool, getEarningsCalendarTool, getUsSectorHeatmapTool } from "./us-market";
6
6
  import { getCryptoOverviewTool, getCryptoTopTool, getCryptoCatsTool, getCryptoFundingTool, getCryptoLiquidationTool } from "./crypto";
7
+ import { getFinancialsTool, getAnalystRatingTool, getStockNewsTool, getInsiderActivityTool, getStockFullOverviewTool } from "./us-fundamentals";
7
8
 
8
9
  export * from "./types";
9
10
 
@@ -21,6 +22,11 @@ export const tools: ToolDef[] = [
21
22
  getCryptoCatsTool,
22
23
  getCryptoFundingTool,
23
24
  getCryptoLiquidationTool,
25
+ getFinancialsTool,
26
+ getAnalystRatingTool,
27
+ getStockNewsTool,
28
+ getInsiderActivityTool,
29
+ getStockFullOverviewTool,
24
30
  ];
25
31
 
26
32
  export const toolMap = new Map<string, ToolDef>(
@@ -0,0 +1,174 @@
1
+ import { z } from "zod";
2
+ import {
3
+ fetchFinancials,
4
+ fetchAnalystRating,
5
+ fetchStockNews,
6
+ fetchInsiderActivity,
7
+ fetchUSQuotes,
8
+ fetchStockProfile,
9
+ } from "../data/yahoo";
10
+ import type { ToolDef } from "./types";
11
+ import { ok, err } from "./types";
12
+
13
+ const SymbolInput = z.object({ symbol: z.string().min(1) });
14
+
15
+ // ─── get_financials ───────────────────────────────────────────────────────────
16
+
17
+ const FinancialsInput = z.object({
18
+ symbol: z.string().min(1),
19
+ quarterly: z.boolean().optional(),
20
+ });
21
+
22
+ export const getFinancialsTool: ToolDef = {
23
+ tool: {
24
+ name: "get_financials",
25
+ description:
26
+ "获取美股财务数据:营收、毛利率、净利润、净利率、EPS、自由现金流、负债率。" +
27
+ "默认返回近4年年报,quarterly=true 返回近4季度季报。",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA" },
32
+ quarterly: { type: "boolean", description: "true=季报,false=年报(默认)" },
33
+ },
34
+ required: ["symbol"],
35
+ },
36
+ },
37
+ handler: async (input) => {
38
+ const parsed = FinancialsInput.safeParse(input);
39
+ if (!parsed.success) return err(parsed.error.message);
40
+ try {
41
+ return ok(await fetchFinancials(parsed.data.symbol, parsed.data.quarterly ?? false));
42
+ } catch (e) {
43
+ return err((e as Error).message);
44
+ }
45
+ },
46
+ };
47
+
48
+ // ─── get_analyst_rating ───────────────────────────────────────────────────────
49
+
50
+ export const getAnalystRatingTool: ToolDef = {
51
+ tool: {
52
+ name: "get_analyst_rating",
53
+ description:
54
+ "获取华尔街分析师评级:共识评级(buy/hold/sell)、平均目标价、高低目标价、" +
55
+ "强买/买/持有/卖/强卖人数分布,以及最近10条评级变动记录。",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA" },
60
+ },
61
+ required: ["symbol"],
62
+ },
63
+ },
64
+ handler: async (input) => {
65
+ const parsed = SymbolInput.safeParse(input);
66
+ if (!parsed.success) return err(parsed.error.message);
67
+ try {
68
+ return ok(await fetchAnalystRating(parsed.data.symbol));
69
+ } catch (e) {
70
+ return err((e as Error).message);
71
+ }
72
+ },
73
+ };
74
+
75
+ // ─── get_stock_news ───────────────────────────────────────────────────────────
76
+
77
+ const NewsInput = z.object({
78
+ symbol: z.string().min(1),
79
+ limit: z.number().int().min(1).max(30).optional(),
80
+ });
81
+
82
+ export const getStockNewsTool: ToolDef = {
83
+ tool: {
84
+ name: "get_stock_news",
85
+ description:
86
+ "获取美股个股最新新闻(近7天),含标题、摘要、链接、发布时间。最多30条,默认15条。",
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: {
90
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA" },
91
+ limit: { type: "number", description: "返回条数,默认15,最大30" },
92
+ },
93
+ required: ["symbol"],
94
+ },
95
+ },
96
+ handler: async (input) => {
97
+ const parsed = NewsInput.safeParse(input);
98
+ if (!parsed.success) return err(parsed.error.message);
99
+ try {
100
+ return ok(await fetchStockNews(parsed.data.symbol, parsed.data.limit ?? 15));
101
+ } catch (e) {
102
+ return err((e as Error).message);
103
+ }
104
+ },
105
+ };
106
+
107
+ // ─── get_insider_activity ─────────────────────────────────────────────────────
108
+
109
+ export const getInsiderActivityTool: ToolDef = {
110
+ tool: {
111
+ name: "get_insider_activity",
112
+ description:
113
+ "获取美股内部人(高管/董事)近期买卖记录,基于 SEC Form 4 公开数据。" +
114
+ "含交易人姓名、职位、交易类型、股数、交易金额。",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA" },
119
+ },
120
+ required: ["symbol"],
121
+ },
122
+ },
123
+ handler: async (input) => {
124
+ const parsed = SymbolInput.safeParse(input);
125
+ if (!parsed.success) return err(parsed.error.message);
126
+ try {
127
+ return ok(await fetchInsiderActivity(parsed.data.symbol));
128
+ } catch (e) {
129
+ return err((e as Error).message);
130
+ }
131
+ },
132
+ };
133
+
134
+ // ─── get_stock_full_overview ──────────────────────────────────────────────────
135
+
136
+ export const getStockFullOverviewTool: ToolDef = {
137
+ tool: {
138
+ name: "get_stock_full_overview",
139
+ description:
140
+ "一次调用获取美股完整分析数据:实时行情 + 公司基本面 + 分析师评级 + 近期新闻(并行获取)。" +
141
+ "适合回答综合分析类问题(如:帮我分析一下 NVDA),避免多次调用不同工具。",
142
+ inputSchema: {
143
+ type: "object",
144
+ properties: {
145
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA、TSLA" },
146
+ },
147
+ required: ["symbol"],
148
+ },
149
+ },
150
+ handler: async (input) => {
151
+ const parsed = SymbolInput.safeParse(input);
152
+ if (!parsed.success) return err(parsed.error.message);
153
+ const { symbol } = parsed.data;
154
+
155
+ try {
156
+ const [quotes, profile, analyst, news] = await Promise.allSettled([
157
+ fetchUSQuotes([symbol]),
158
+ fetchStockProfile(symbol),
159
+ fetchAnalystRating(symbol),
160
+ fetchStockNews(symbol, 10),
161
+ ]);
162
+
163
+ return ok({
164
+ symbol,
165
+ quote: quotes.status === "fulfilled" ? quotes.value[0] ?? null : null,
166
+ profile: profile.status === "fulfilled" ? profile.value : null,
167
+ analyst: analyst.status === "fulfilled" ? analyst.value : null,
168
+ news: news.status === "fulfilled" ? news.value : [],
169
+ });
170
+ } catch (e) {
171
+ return err((e as Error).message);
172
+ }
173
+ },
174
+ };