stock-scanner-mcp 1.16.0 → 1.16.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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A modular MCP server for Claude Code and Claude Desktop that provides real-time access to stock and crypto market data. Scan markets, check technicals, monitor insider trades, track earnings, analyze options flow, and optionally save your own watchlists and thesis notes from one server.
8
8
 
9
- **61 tools** across **12 modules** — 9 modules work with zero API keys, including an optional stateful Market Workspace.
9
+ **64 tools** across **13 modules** — 9 modules work with zero API keys, including an optional stateful Market Workspace.
10
10
 
11
11
  ## Quick Start
12
12
 
@@ -35,7 +35,7 @@ Copy this complete config block into your config file:
35
35
  }
36
36
  ```
37
37
 
38
- This gives you **43 tools** immediately with no API keys. API keys are optional and free — they unlock 18 additional tools for real-time quotes, news, earnings, and economic data. See [API Keys](#api-keys-optional) below for where to get them.
38
+ This gives you **46 tools** immediately with no API keys. API keys are optional and free — they unlock 18 additional tools for real-time quotes, news, earnings, and economic data. See [API Keys](#api-keys-optional) below for where to get them.
39
39
 
40
40
  > **Minimal config** — if you don't want workspace or API keys, use this instead:
41
41
  > ```json
@@ -48,7 +48,7 @@ This gives you **43 tools** immediately with no API keys. API keys are optional
48
48
  > }
49
49
  > }
50
50
  > ```
51
- > This gives you **36 stateless tools** with no local data storage.
51
+ > This gives you **39 stateless tools** with no local data storage.
52
52
 
53
53
  Restart Claude Desktop after saving. Claude Code picks up the config automatically.
54
54
 
@@ -97,7 +97,7 @@ Once answered, it saves your profile and creates a `core` watchlist:
97
97
 
98
98
  You can also skip the skill and ask Claude directly: *"Set up my workspace — I'm a swing trader, create a core watchlist with MARA, HOOD, BTC, daily reviews."*
99
99
 
100
- **That's it.** You now have 61 tools, 19 skills, and a personalized workspace. Try `/workspace-morning-brief` for your first tailored market scan.
100
+ **That's it.** You now have 64 tools, 19 skills, and a personalized workspace. Try `/workspace-morning-brief` for your first tailored market scan.
101
101
 
102
102
  ## What You Can Do
103
103
 
@@ -220,6 +220,7 @@ For the full list of workspace tools, see the [tool reference](#workspace--perso
220
220
  | options-cboe | 1 | None | CBOE put/call ratio sentiment indicator |
221
221
  | sentiment | 2 | None | CNN Fear & Greed Index, Crypto Fear & Greed Index |
222
222
  | frankfurter | 5 | None | Forex exchange rates — 31 currencies from ECB (daily reference rates) |
223
+ | reddit | 3 | None | Reddit trending tickers, mention tracking, and sentiment from r/wallstreetbets, r/stocks, r/investing, r/options |
223
224
  | workspace | 7 | None | Optional stateful profile, watchlists, and thesis tracking for personalized workflows (`--enable-workspace`) |
224
225
  | finnhub | 9 | `FINNHUB_API_KEY` | Quotes, news, earnings, analyst ratings, short interest |
225
226
  | alpha-vantage | 5 | `ALPHA_VANTAGE_API_KEY` | Quotes, daily prices, fundamentals, earnings, dividends |
@@ -227,9 +228,9 @@ For the full list of workspace tools, see the [tool reference](#workspace--perso
227
228
 
228
229
  Modules auto-enable when their API key is set. No-key modules are always enabled, except `workspace`, which requires `--enable-workspace`.
229
230
 
230
- For a complete list of every tool with descriptions, see the [Full Tool Reference](#full-tool-reference-61-tools) below.
231
+ For a complete list of every tool with descriptions, see the [Full Tool Reference](#full-tool-reference-64-tools) below.
231
232
 
232
- ## Full Tool Reference (61 tools)
233
+ ## Full Tool Reference (64 tools)
233
234
 
234
235
  ### TradingView — Stock Scanning (no API key)
235
236
 
@@ -307,6 +308,14 @@ For a complete list of every tool with descriptions, see the [Full Tool Referenc
307
308
  | `frankfurter_convert` | Convert an amount between two currencies |
308
309
  | `frankfurter_currencies` | List all supported currency codes |
309
310
 
311
+ ### Reddit — Trending Tickers & Sentiment (no API key)
312
+
313
+ | Tool | Description |
314
+ |------|-------------|
315
+ | `reddit_trending` | Trending stock tickers from Reddit by mention frequency across r/wallstreetbets, r/stocks, r/investing, r/options |
316
+ | `reddit_mentions` | Mention count and top posts for a specific ticker across Reddit investing subreddits |
317
+ | `reddit_sentiment` | Keyword-based sentiment analysis (bullish/bearish/neutral) for a ticker from Reddit discussions |
318
+
310
319
  ### Workspace — Personalized Context (optional, no API key)
311
320
 
312
321
  Enabled only when you start the server with `--enable-workspace`.
@@ -443,7 +452,8 @@ src/
443
452
  │ ├── alpha-vantage/ # 5 tools — quotes, fundamentals, dividends
444
453
  │ ├── fred/ # 4 tools — economic calendar, indicators, historical data
445
454
  │ ├── sentiment/ # 2 tools — Fear & Greed indexes (market + crypto)
446
- └── frankfurter/ # 5 tools — forex exchange rates (ECB, 31 currencies)
455
+ ├── frankfurter/ # 5 tools — forex exchange rates (ECB, 31 currencies)
456
+ │ └── reddit/ # 3 tools — trending tickers, mentions, sentiment from Reddit
447
457
  ├── sidecar/
448
458
  │ ├── index.ts # HTTP sidecar entry point (port 3200)
449
459
  │ └── server.ts # HTTP request handler (55 endpoints)
@@ -1602,9 +1602,11 @@ async function getCurrencies() {
1602
1602
  export {
1603
1603
  errorResult,
1604
1604
  successResult,
1605
+ httpGet,
1605
1606
  scanStocks,
1606
1607
  resolveTicker,
1607
1608
  scanCrypto,
1609
+ TtlCache,
1608
1610
  searchFilings,
1609
1611
  getCompanyFilings,
1610
1612
  getCompanyFacts,
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  runInstallSkills
4
4
  } from "./chunk-YEZEE6FS.js";
5
5
  import {
6
+ TtlCache,
6
7
  convertCurrency,
7
8
  errorResult,
8
9
  fetchOptionChain,
@@ -38,13 +39,14 @@ import {
38
39
  getShortInterest,
39
40
  getTimeSeries,
40
41
  getTrending,
42
+ httpGet,
41
43
  resolveTicker,
42
44
  scanCrypto,
43
45
  scanStocks,
44
46
  searchFilings,
45
47
  searchSeries,
46
48
  successResult
47
- } from "./chunk-HAYAISNP.js";
49
+ } from "./chunk-T5IF6777.js";
48
50
 
49
51
  // src/index.ts
50
52
  import { readFileSync } from "fs";
@@ -52,7 +54,7 @@ import { fileURLToPath } from "url";
52
54
  import { dirname as dirname2, join as join2 } from "path";
53
55
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
54
56
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
55
- import { z as z14 } from "zod";
57
+ import { z as z15 } from "zod";
56
58
 
57
59
  // src/config.ts
58
60
  import * as path from "path";
@@ -1454,8 +1456,601 @@ function createFrankfurterModule() {
1454
1456
  };
1455
1457
  }
1456
1458
 
1459
+ // src/modules/reddit/index.ts
1460
+ import { z as z12 } from "zod";
1461
+
1462
+ // src/modules/reddit/client.ts
1463
+ var REDDIT_BASE = "https://www.reddit.com";
1464
+ var REDDIT_HEADERS = {
1465
+ "User-Agent": "stock-scanner-mcp/1.0 (by /u/stock-scanner-bot)"
1466
+ };
1467
+ var DEFAULT_SUBREDDITS = [
1468
+ "wallstreetbets",
1469
+ "stocks",
1470
+ "investing",
1471
+ "options"
1472
+ ];
1473
+ var trendingCache = new TtlCache(5 * 60 * 1e3);
1474
+ var mentionCache = new TtlCache(2 * 60 * 1e3);
1475
+ var STOP_WORDS = /* @__PURE__ */ new Set([
1476
+ // 1-2 letter
1477
+ "I",
1478
+ "A",
1479
+ "AM",
1480
+ "AN",
1481
+ "AS",
1482
+ "AT",
1483
+ "BE",
1484
+ "BY",
1485
+ "DO",
1486
+ "GO",
1487
+ "HE",
1488
+ "IF",
1489
+ "IN",
1490
+ "IS",
1491
+ "IT",
1492
+ "ME",
1493
+ "MY",
1494
+ "NO",
1495
+ "OF",
1496
+ "OK",
1497
+ "ON",
1498
+ "OR",
1499
+ "SO",
1500
+ "TO",
1501
+ "UP",
1502
+ "US",
1503
+ "WE",
1504
+ // 3 letter common English
1505
+ "ALL",
1506
+ "AND",
1507
+ "ANY",
1508
+ "ARE",
1509
+ "BAD",
1510
+ "BIG",
1511
+ "BUT",
1512
+ "CAN",
1513
+ "DAY",
1514
+ "DID",
1515
+ "END",
1516
+ "FAR",
1517
+ "FEW",
1518
+ "FOR",
1519
+ "GET",
1520
+ "GOD",
1521
+ "GOT",
1522
+ "GUY",
1523
+ "HAS",
1524
+ "HAD",
1525
+ "HER",
1526
+ "HIM",
1527
+ "HIS",
1528
+ "HOW",
1529
+ "ITS",
1530
+ "JOB",
1531
+ "LET",
1532
+ "LOT",
1533
+ "MAN",
1534
+ "MAY",
1535
+ "MEN",
1536
+ "MOM",
1537
+ "NEW",
1538
+ "NOT",
1539
+ "NOW",
1540
+ "OLD",
1541
+ "ONE",
1542
+ "OUR",
1543
+ "OUT",
1544
+ "OWN",
1545
+ "PUT",
1546
+ "RAN",
1547
+ "RUN",
1548
+ "SAT",
1549
+ "SAY",
1550
+ "SET",
1551
+ "SHE",
1552
+ "SIT",
1553
+ "THE",
1554
+ "TOO",
1555
+ "TOP",
1556
+ "TRY",
1557
+ "TWO",
1558
+ "USE",
1559
+ "WAR",
1560
+ "WAS",
1561
+ "WAY",
1562
+ "WHO",
1563
+ "WHY",
1564
+ "WIN",
1565
+ "WON",
1566
+ "YES",
1567
+ "YET",
1568
+ "YOU",
1569
+ // 4 letter common English
1570
+ "ALSO",
1571
+ "BACK",
1572
+ "BEEN",
1573
+ "BEST",
1574
+ "BODY",
1575
+ "BOTH",
1576
+ "CALL",
1577
+ "CAME",
1578
+ "COME",
1579
+ "DARK",
1580
+ "DAYS",
1581
+ "DEAD",
1582
+ "DOES",
1583
+ "DONE",
1584
+ "DOWN",
1585
+ "EACH",
1586
+ "EVEN",
1587
+ "EVER",
1588
+ "EYES",
1589
+ "FACE",
1590
+ "FACT",
1591
+ "FEEL",
1592
+ "FIND",
1593
+ "FOUR",
1594
+ "FROM",
1595
+ "FULL",
1596
+ "GAVE",
1597
+ "GIVE",
1598
+ "GOOD",
1599
+ "HALF",
1600
+ "HAND",
1601
+ "HARD",
1602
+ "HAVE",
1603
+ "HEAD",
1604
+ "HEAR",
1605
+ "HELP",
1606
+ "HERE",
1607
+ "HIGH",
1608
+ "HOME",
1609
+ "HOPE",
1610
+ "IDEA",
1611
+ "INTO",
1612
+ "JUST",
1613
+ "KEEP",
1614
+ "KIND",
1615
+ "KNEW",
1616
+ "KNOW",
1617
+ "LAND",
1618
+ "LAST",
1619
+ "LEFT",
1620
+ "LESS",
1621
+ "LIFE",
1622
+ "LIKE",
1623
+ "LINE",
1624
+ "LIST",
1625
+ "LIVE",
1626
+ "LONG",
1627
+ "LOOK",
1628
+ "LOST",
1629
+ "LOVE",
1630
+ "MADE",
1631
+ "MAKE",
1632
+ "MANY",
1633
+ "MIND",
1634
+ "MORE",
1635
+ "MOST",
1636
+ "MUCH",
1637
+ "MUST",
1638
+ "NAME",
1639
+ "NEED",
1640
+ "NEWS",
1641
+ "NEXT",
1642
+ "NICE",
1643
+ "NONE",
1644
+ "ONCE",
1645
+ "ONLY",
1646
+ "OPEN",
1647
+ "OVER",
1648
+ "PAID",
1649
+ "PART",
1650
+ "PAST",
1651
+ "PLAN",
1652
+ "PLAY",
1653
+ "POST",
1654
+ "PULL",
1655
+ "PUSH",
1656
+ "RATE",
1657
+ "READ",
1658
+ "REAL",
1659
+ "REST",
1660
+ "RISK",
1661
+ "ROOM",
1662
+ "SAFE",
1663
+ "SAID",
1664
+ "SAME",
1665
+ "SEEN",
1666
+ "SHOW",
1667
+ "SIDE",
1668
+ "SOME",
1669
+ "SOON",
1670
+ "STOP",
1671
+ "SUCH",
1672
+ "SURE",
1673
+ "TAKE",
1674
+ "TALK",
1675
+ "TELL",
1676
+ "THAN",
1677
+ "THAT",
1678
+ "THEM",
1679
+ "THEN",
1680
+ "THEY",
1681
+ "THIS",
1682
+ "TIME",
1683
+ "TOOK",
1684
+ "TRUE",
1685
+ "TURN",
1686
+ "UPON",
1687
+ "VERY",
1688
+ "WANT",
1689
+ "WEEK",
1690
+ "WELL",
1691
+ "WENT",
1692
+ "WERE",
1693
+ "WHAT",
1694
+ "WHEN",
1695
+ "WILL",
1696
+ "WITH",
1697
+ "WORD",
1698
+ "WORK",
1699
+ "YEAR",
1700
+ "YOUR",
1701
+ // 5 letter common English
1702
+ "ABOUT",
1703
+ "AFTER",
1704
+ "AGAIN",
1705
+ "BEING",
1706
+ "BELOW",
1707
+ "BLACK",
1708
+ "BRING",
1709
+ "COULD",
1710
+ "EARLY",
1711
+ "EVERY",
1712
+ "FIRST",
1713
+ "GREAT",
1714
+ "GREEN",
1715
+ "HEARD",
1716
+ "HOUSE",
1717
+ "LARGE",
1718
+ "LATER",
1719
+ "LEARN",
1720
+ "LEVEL",
1721
+ "LIGHT",
1722
+ "MIGHT",
1723
+ "MONEY",
1724
+ "MONTH",
1725
+ "NEVER",
1726
+ "NIGHT",
1727
+ "OTHER",
1728
+ "PLACE",
1729
+ "POINT",
1730
+ "POWER",
1731
+ "PRICE",
1732
+ "RIGHT",
1733
+ "SHALL",
1734
+ "SINCE",
1735
+ "SMALL",
1736
+ "SORRY",
1737
+ "STAND",
1738
+ "START",
1739
+ "STATE",
1740
+ "STILL",
1741
+ "STUDY",
1742
+ "THEIR",
1743
+ "THERE",
1744
+ "THESE",
1745
+ "THING",
1746
+ "THINK",
1747
+ "THREE",
1748
+ "TODAY",
1749
+ "UNDER",
1750
+ "UNTIL",
1751
+ "WATER",
1752
+ "WHERE",
1753
+ "WHICH",
1754
+ "WHITE",
1755
+ "WHILE",
1756
+ "WHOLE",
1757
+ "WORLD",
1758
+ "WOULD",
1759
+ "WRITE",
1760
+ "YOUNG",
1761
+ // Finance acronyms that aren't tickers
1762
+ "ETF",
1763
+ "IPO",
1764
+ "SEC",
1765
+ "GDP",
1766
+ "FDA",
1767
+ "FED",
1768
+ "CEO",
1769
+ "CFO",
1770
+ "COO",
1771
+ "CTO",
1772
+ "ATH",
1773
+ "ATL",
1774
+ "EOD",
1775
+ "WSB",
1776
+ "OTC",
1777
+ "ITM",
1778
+ "OTM",
1779
+ "IMO",
1780
+ "LOL",
1781
+ "OMG",
1782
+ "USA",
1783
+ "NYSE",
1784
+ "YOLO",
1785
+ "FOMO",
1786
+ "HODL",
1787
+ "MOASS",
1788
+ "TLDR",
1789
+ "IMHO",
1790
+ "LMAO",
1791
+ "EDIT",
1792
+ "TLDR",
1793
+ "INFO",
1794
+ "HUGE",
1795
+ "GAIN",
1796
+ "HOLD",
1797
+ "SELL",
1798
+ "MOON",
1799
+ "CASH",
1800
+ "DEBT",
1801
+ "FUND",
1802
+ "BEAR",
1803
+ "BULL",
1804
+ "BOND",
1805
+ "LOAN",
1806
+ "LOSS",
1807
+ "PUMP",
1808
+ "DUMP",
1809
+ "LONG",
1810
+ "MAMA",
1811
+ "PAPA",
1812
+ "FREE",
1813
+ "ZERO",
1814
+ "PLUS"
1815
+ ]);
1816
+ var CASHTAG_RE = /\$([A-Z]{2,5})\b/g;
1817
+ var BARE_TICKER_RE = /\b([A-Z]{2,5})\b/g;
1818
+ function extractTickers(text) {
1819
+ const tickers = /* @__PURE__ */ new Set();
1820
+ let match;
1821
+ CASHTAG_RE.lastIndex = 0;
1822
+ while ((match = CASHTAG_RE.exec(text)) !== null) {
1823
+ const ticker = match[1];
1824
+ if (!STOP_WORDS.has(ticker)) {
1825
+ tickers.add(ticker);
1826
+ }
1827
+ }
1828
+ BARE_TICKER_RE.lastIndex = 0;
1829
+ while ((match = BARE_TICKER_RE.exec(text)) !== null) {
1830
+ const word = match[1];
1831
+ if (!STOP_WORDS.has(word)) {
1832
+ tickers.add(word);
1833
+ }
1834
+ }
1835
+ return [...tickers];
1836
+ }
1837
+ var BULLISH_TERMS = [
1838
+ "bull",
1839
+ "calls",
1840
+ "long",
1841
+ "moon",
1842
+ "rocket",
1843
+ "tendies",
1844
+ "diamond hands",
1845
+ "buy",
1846
+ "buying",
1847
+ "bought",
1848
+ "dip",
1849
+ "undervalued",
1850
+ "breakout",
1851
+ "squeeze",
1852
+ "gamma",
1853
+ "rip",
1854
+ "printing",
1855
+ "lambo",
1856
+ "gains",
1857
+ "green",
1858
+ "upside"
1859
+ ];
1860
+ var BEARISH_TERMS = [
1861
+ "bear",
1862
+ "puts",
1863
+ "short",
1864
+ "dump",
1865
+ "crash",
1866
+ "sell",
1867
+ "selling",
1868
+ "sold",
1869
+ "overvalued",
1870
+ "bubble",
1871
+ "bag",
1872
+ "bagholder",
1873
+ "red",
1874
+ "loss",
1875
+ "losses",
1876
+ "downside",
1877
+ "drill",
1878
+ "tank",
1879
+ "fade",
1880
+ "rug",
1881
+ "rekt",
1882
+ "worthless"
1883
+ ];
1884
+ function scoreSentiment(text) {
1885
+ const lower = text.toLowerCase();
1886
+ let score = 0;
1887
+ for (const term of BULLISH_TERMS) {
1888
+ if (lower.includes(term)) score += 1;
1889
+ }
1890
+ for (const term of BEARISH_TERMS) {
1891
+ if (lower.includes(term)) score -= 1;
1892
+ }
1893
+ return score;
1894
+ }
1895
+ async function fetchSubredditPosts(subreddit, query, timeFilter, limit = 25) {
1896
+ const url = `${REDDIT_BASE}/r/${encodeURIComponent(subreddit)}/search.json?q=${encodeURIComponent(query)}&sort=new&restrict_sr=on&t=${encodeURIComponent(timeFilter)}&limit=${encodeURIComponent(String(limit))}`;
1897
+ const listing = await httpGet(url, {
1898
+ headers: REDDIT_HEADERS
1899
+ });
1900
+ return listing.data.children.map((child) => ({
1901
+ title: child.data.title,
1902
+ selftext: child.data.selftext,
1903
+ score: child.data.score,
1904
+ numComments: child.data.num_comments,
1905
+ createdUtc: child.data.created_utc,
1906
+ permalink: child.data.permalink,
1907
+ subreddit: child.data.subreddit
1908
+ }));
1909
+ }
1910
+ async function getTrendingTickers(subreddits = DEFAULT_SUBREDDITS, limit = 20) {
1911
+ const cacheKey = `trending:${[...subreddits].sort().join(",")}:${limit}`;
1912
+ const cached = trendingCache.get(cacheKey);
1913
+ if (cached) return cached;
1914
+ const tickerMap = /* @__PURE__ */ new Map();
1915
+ for (const sub of subreddits) {
1916
+ let posts;
1917
+ try {
1918
+ posts = await fetchSubredditPosts(sub, "stocks OR market OR trading", "week");
1919
+ } catch {
1920
+ continue;
1921
+ }
1922
+ for (const post of posts) {
1923
+ const text = `${post.title} ${post.selftext}`;
1924
+ const tickers = extractTickers(text);
1925
+ for (const ticker of tickers) {
1926
+ const existing = tickerMap.get(ticker) ?? { mentions: 0, subreddits: {} };
1927
+ existing.mentions += 1;
1928
+ existing.subreddits[sub] = (existing.subreddits[sub] ?? 0) + 1;
1929
+ tickerMap.set(ticker, existing);
1930
+ }
1931
+ }
1932
+ }
1933
+ const result = [...tickerMap.entries()].map(([symbol, data]) => ({ symbol, mentions: data.mentions, subreddits: data.subreddits })).sort((a, b) => b.mentions - a.mentions).slice(0, limit);
1934
+ trendingCache.set(cacheKey, result);
1935
+ return result;
1936
+ }
1937
+ async function getTickerMentions(symbol, period = "week") {
1938
+ const cacheKey = `mentions:${symbol}:${period}`;
1939
+ const cached = mentionCache.get(cacheKey);
1940
+ if (cached) return cached;
1941
+ const subredditCounts = {};
1942
+ const allPosts = [];
1943
+ for (const sub of DEFAULT_SUBREDDITS) {
1944
+ let posts;
1945
+ try {
1946
+ posts = await fetchSubredditPosts(sub, symbol, period);
1947
+ } catch {
1948
+ continue;
1949
+ }
1950
+ subredditCounts[sub] = posts.length;
1951
+ allPosts.push(...posts);
1952
+ }
1953
+ const topPosts = allPosts.sort((a, b) => b.score - a.score).slice(0, 10);
1954
+ const result = {
1955
+ symbol,
1956
+ totalMentions: allPosts.length,
1957
+ subreddits: subredditCounts,
1958
+ topPosts
1959
+ };
1960
+ mentionCache.set(cacheKey, result);
1961
+ return result;
1962
+ }
1963
+ async function getTickerSentiment(symbol, limit = 50) {
1964
+ const cacheKey = `sentiment:${symbol}:${limit}`;
1965
+ const cached = mentionCache.get(cacheKey);
1966
+ if (cached) return cached;
1967
+ const allPosts = [];
1968
+ for (const sub of DEFAULT_SUBREDDITS) {
1969
+ let posts;
1970
+ try {
1971
+ posts = await fetchSubredditPosts(sub, symbol, "week", limit);
1972
+ } catch {
1973
+ continue;
1974
+ }
1975
+ allPosts.push(...posts);
1976
+ }
1977
+ let bullishCount = 0;
1978
+ let bearishCount = 0;
1979
+ let neutralCount = 0;
1980
+ let totalScore = 0;
1981
+ const scored = allPosts.map((post) => {
1982
+ const text = `${post.title} ${post.selftext}`;
1983
+ const sentimentScore = scoreSentiment(text);
1984
+ totalScore += sentimentScore;
1985
+ if (sentimentScore > 0) bullishCount += 1;
1986
+ else if (sentimentScore < 0) bearishCount += 1;
1987
+ else neutralCount += 1;
1988
+ return { ...post, sentimentScore };
1989
+ });
1990
+ const samplePosts = scored.sort((a, b) => Math.abs(b.sentimentScore) - Math.abs(a.sentimentScore)).slice(0, 10);
1991
+ const result = {
1992
+ symbol,
1993
+ bullishCount,
1994
+ bearishCount,
1995
+ neutralCount,
1996
+ averageScore: allPosts.length > 0 ? totalScore / allPosts.length : 0,
1997
+ samplePosts
1998
+ };
1999
+ mentionCache.set(cacheKey, result);
2000
+ return result;
2001
+ }
2002
+
2003
+ // src/modules/reddit/index.ts
2004
+ var trendingTool2 = {
2005
+ name: "reddit_trending",
2006
+ description: "Get trending stock tickers from Reddit based on mention frequency. Scans r/wallstreetbets, r/stocks, r/investing, and r/options for posts mentioning tickers. Returns tickers sorted by mention count with per-subreddit breakdown. Limitation: uses keyword extraction (cashtags + uppercase words), not NLP \u2014 some false positives possible. Best for gauging retail buzz, not precise sentiment.",
2007
+ inputSchema: z12.object({
2008
+ subreddits: z12.array(z12.string()).max(10).optional().describe("Subreddits to scan (default: wallstreetbets, stocks, investing, options)"),
2009
+ limit: z12.number().min(1).max(50).default(20).describe("Maximum number of trending tickers to return (default: 20)")
2010
+ }),
2011
+ readOnly: true,
2012
+ handler: withMetadata(async (args) => {
2013
+ const result = await getTrendingTickers(args.subreddits ?? [...DEFAULT_SUBREDDITS], args.limit ?? 20);
2014
+ return successResult(JSON.stringify(result, null, 2));
2015
+ }, { source: "reddit", dataDelay: "real-time" })
2016
+ };
2017
+ var mentionsTool = {
2018
+ name: "reddit_mentions",
2019
+ description: "Get mention count and top posts for a specific stock ticker across Reddit. Searches r/wallstreetbets, r/stocks, r/investing, and r/options. Returns total mentions, per-subreddit breakdown, and top 10 posts by score. Use this to check how much retail attention a ticker is getting.",
2020
+ inputSchema: z12.object({
2021
+ symbol: z12.string().max(10).describe("Stock ticker symbol (e.g. AAPL, TSLA, GME)"),
2022
+ period: z12.enum(["hour", "day", "week"]).default("day").describe("Time period to search (default: day)")
2023
+ }),
2024
+ readOnly: true,
2025
+ handler: withMetadata(async (args) => {
2026
+ const result = await getTickerMentions(args.symbol.toUpperCase(), args.period ?? "day");
2027
+ return successResult(JSON.stringify(result, null, 2));
2028
+ }, { source: "reddit", dataDelay: "real-time" })
2029
+ };
2030
+ var sentimentTool = {
2031
+ name: "reddit_sentiment",
2032
+ description: "Get sentiment analysis for a stock ticker from Reddit discussions. Searches r/wallstreetbets, r/stocks, r/investing, and r/options, then scores each post using keyword matching (bullish terms like 'moon', 'calls', 'breakout' vs bearish terms like 'crash', 'puts', 'dump'). Returns bullish/bearish/neutral counts, average sentiment score, and sample posts. Limitation: keyword-based scoring, not NLP \u2014 sarcasm and context may be missed.",
2033
+ inputSchema: z12.object({
2034
+ symbol: z12.string().max(10).describe("Stock ticker symbol (e.g. AAPL, TSLA, GME)"),
2035
+ limit: z12.number().min(1).max(100).default(50).describe("Maximum posts to analyze per subreddit (default: 50)")
2036
+ }),
2037
+ readOnly: true,
2038
+ handler: withMetadata(async (args) => {
2039
+ const result = await getTickerSentiment(args.symbol.toUpperCase(), args.limit ?? 50);
2040
+ return successResult(JSON.stringify(result, null, 2));
2041
+ }, { source: "reddit", dataDelay: "real-time" })
2042
+ };
2043
+ function createRedditModule() {
2044
+ return {
2045
+ name: "reddit",
2046
+ description: "Reddit sentiment \u2014 trending tickers, mention tracking, and sentiment analysis from popular investing subreddits",
2047
+ requiredEnvVars: [],
2048
+ tools: [trendingTool2, mentionsTool, sentimentTool]
2049
+ };
2050
+ }
2051
+
1457
2052
  // src/modules/workspace/index.ts
1458
- import { z as z13 } from "zod";
2053
+ import { z as z14 } from "zod";
1459
2054
 
1460
2055
  // src/modules/workspace/storage.ts
1461
2056
  import * as fs from "fs/promises";
@@ -1463,57 +2058,57 @@ import * as path2 from "path";
1463
2058
  import { lock } from "proper-lockfile";
1464
2059
 
1465
2060
  // src/modules/workspace/types.ts
1466
- import { z as z12 } from "zod";
2061
+ import { z as z13 } from "zod";
1467
2062
  var RESERVED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype", "toString", "valueOf", "hasOwnProperty"]);
1468
- var ProfileSchema = z12.object({
1469
- defaultExchange: z12.string().max(20).default("NASDAQ"),
1470
- tradingStyle: z12.string().max(100).optional(),
1471
- assetFocus: z12.array(z12.string().max(50)).max(20).default([]),
1472
- preferredTimeframe: z12.string().max(50).optional(),
1473
- workflowCadence: z12.enum(["daily", "weekly"]).default("daily"),
1474
- updatedAt: z12.string().default(() => (/* @__PURE__ */ new Date(0)).toISOString())
2063
+ var ProfileSchema = z13.object({
2064
+ defaultExchange: z13.string().max(20).default("NASDAQ"),
2065
+ tradingStyle: z13.string().max(100).optional(),
2066
+ assetFocus: z13.array(z13.string().max(50)).max(20).default([]),
2067
+ preferredTimeframe: z13.string().max(50).optional(),
2068
+ workflowCadence: z13.enum(["daily", "weekly"]).default("daily"),
2069
+ updatedAt: z13.string().default(() => (/* @__PURE__ */ new Date(0)).toISOString())
1475
2070
  });
1476
- var InstrumentSchema = z12.object({
1477
- full: z12.string().max(80),
1478
- ticker: z12.string().max(50),
1479
- exchange: z12.string().optional(),
1480
- isCrypto: z12.boolean(),
1481
- input: z12.string().max(50),
1482
- note: z12.string().max(500).optional(),
1483
- addedAt: z12.string()
2071
+ var InstrumentSchema = z13.object({
2072
+ full: z13.string().max(80),
2073
+ ticker: z13.string().max(50),
2074
+ exchange: z13.string().optional(),
2075
+ isCrypto: z13.boolean(),
2076
+ input: z13.string().max(50),
2077
+ note: z13.string().max(500).optional(),
2078
+ addedAt: z13.string()
1484
2079
  });
1485
- var WatchlistSchema = z12.object({
1486
- id: z12.string().max(100),
1487
- name: z12.string().max(100),
1488
- instruments: z12.array(InstrumentSchema).max(200).default([]),
1489
- createdAt: z12.string(),
1490
- updatedAt: z12.string()
2080
+ var WatchlistSchema = z13.object({
2081
+ id: z13.string().max(100),
2082
+ name: z13.string().max(100),
2083
+ instruments: z13.array(InstrumentSchema).max(200).default([]),
2084
+ createdAt: z13.string(),
2085
+ updatedAt: z13.string()
1491
2086
  });
1492
- var ThesisSchema = z12.object({
1493
- full: z12.string().max(80),
1494
- ticker: z12.string().max(50),
1495
- exchange: z12.string().optional(),
1496
- isCrypto: z12.boolean(),
1497
- input: z12.string().max(50),
1498
- summary: z12.string().max(5e3),
1499
- bullCase: z12.string().max(2e3).optional(),
1500
- bearCase: z12.string().max(2e3).optional(),
1501
- catalyst: z12.string().max(2e3).optional(),
1502
- invalidation: z12.string().max(2e3).optional(),
1503
- timeframe: z12.string().max(100).optional(),
1504
- nextReviewDate: z12.string().optional(),
1505
- confidence: z12.number().min(0).max(5).optional(),
1506
- updatedAt: z12.string(),
1507
- archivedAt: z12.string().optional()
2087
+ var ThesisSchema = z13.object({
2088
+ full: z13.string().max(80),
2089
+ ticker: z13.string().max(50),
2090
+ exchange: z13.string().optional(),
2091
+ isCrypto: z13.boolean(),
2092
+ input: z13.string().max(50),
2093
+ summary: z13.string().max(5e3),
2094
+ bullCase: z13.string().max(2e3).optional(),
2095
+ bearCase: z13.string().max(2e3).optional(),
2096
+ catalyst: z13.string().max(2e3).optional(),
2097
+ invalidation: z13.string().max(2e3).optional(),
2098
+ timeframe: z13.string().max(100).optional(),
2099
+ nextReviewDate: z13.string().optional(),
2100
+ confidence: z13.number().min(0).max(5).optional(),
2101
+ updatedAt: z13.string(),
2102
+ archivedAt: z13.string().optional()
1508
2103
  });
1509
- var WorkspaceSchema = z12.object({
1510
- schemaVersion: z12.number().default(1),
2104
+ var WorkspaceSchema = z13.object({
2105
+ schemaVersion: z13.number().default(1),
1511
2106
  profile: ProfileSchema.default(() => ProfileSchema.parse({})),
1512
- watchlists: z12.record(z12.string(), WatchlistSchema).refine(
2107
+ watchlists: z13.record(z13.string(), WatchlistSchema).refine(
1513
2108
  (rec) => Object.keys(rec).every((k) => !RESERVED_KEYS.has(k)),
1514
2109
  { message: "Workspace contains a reserved key in watchlists" }
1515
2110
  ).default({}),
1516
- theses: z12.record(z12.string(), ThesisSchema).refine(
2111
+ theses: z13.record(z13.string(), ThesisSchema).refine(
1517
2112
  (rec) => Object.keys(rec).every((k) => !RESERVED_KEYS.has(k)),
1518
2113
  { message: "Workspace contains a reserved key in theses" }
1519
2114
  ).default({})
@@ -1645,7 +2240,7 @@ function createWorkspaceModule(dataDir, defaultExchange = "NASDAQ") {
1645
2240
  {
1646
2241
  name: "workspace_get_profile",
1647
2242
  description: "Get the current user's trading profile and workspace settings.",
1648
- inputSchema: z13.object({}),
2243
+ inputSchema: z14.object({}),
1649
2244
  readOnly: true,
1650
2245
  openWorld: false,
1651
2246
  handler: withMetadata(async () => {
@@ -1656,10 +2251,10 @@ function createWorkspaceModule(dataDir, defaultExchange = "NASDAQ") {
1656
2251
  {
1657
2252
  name: "workspace_update_profile",
1658
2253
  description: "Update the user's trading profile (style, asset focus, review cadence).",
1659
- inputSchema: z13.object({
1660
- tradingStyle: z13.string().max(100).optional().describe("E.g., 'options', 'swing', 'day'"),
1661
- assetFocus: z13.array(z13.string().max(50)).max(20).optional().describe("Asset classes like 'equities', 'crypto'"),
1662
- workflowCadence: z13.enum(["daily", "weekly"]).optional()
2254
+ inputSchema: z14.object({
2255
+ tradingStyle: z14.string().max(100).optional().describe("E.g., 'options', 'swing', 'day'"),
2256
+ assetFocus: z14.array(z14.string().max(50)).max(20).optional().describe("Asset classes like 'equities', 'crypto'"),
2257
+ workflowCadence: z14.enum(["daily", "weekly"]).optional()
1663
2258
  }),
1664
2259
  readOnly: false,
1665
2260
  openWorld: false,
@@ -1687,7 +2282,7 @@ function createWorkspaceModule(dataDir, defaultExchange = "NASDAQ") {
1687
2282
  {
1688
2283
  name: "workspace_list_watchlists",
1689
2284
  description: "List all watchlists and their instruments.",
1690
- inputSchema: z13.object({}),
2285
+ inputSchema: z14.object({}),
1691
2286
  readOnly: true,
1692
2287
  openWorld: false,
1693
2288
  handler: withMetadata(async () => {
@@ -1698,8 +2293,8 @@ function createWorkspaceModule(dataDir, defaultExchange = "NASDAQ") {
1698
2293
  {
1699
2294
  name: "workspace_create_watchlist",
1700
2295
  description: "Create a new empty watchlist (max 50 per workspace).",
1701
- inputSchema: z13.object({
1702
- name: z13.string().max(100).describe("The ID/name of the watchlist (e.g., 'core', 'swing')")
2296
+ inputSchema: z14.object({
2297
+ name: z14.string().max(100).describe("The ID/name of the watchlist (e.g., 'core', 'swing')")
1703
2298
  }),
1704
2299
  readOnly: false,
1705
2300
  openWorld: false,
@@ -1738,9 +2333,9 @@ function createWorkspaceModule(dataDir, defaultExchange = "NASDAQ") {
1738
2333
  {
1739
2334
  name: "workspace_update_watchlist",
1740
2335
  description: "Replace the instruments in a specific watchlist.",
1741
- inputSchema: z13.object({
1742
- name: z13.string().max(100).describe("The ID of the watchlist to update"),
1743
- symbols: z13.array(z13.string().max(50)).max(200).describe("Raw symbols to track (e.g., ['AAPL', 'BTC'])")
2336
+ inputSchema: z14.object({
2337
+ name: z14.string().max(100).describe("The ID of the watchlist to update"),
2338
+ symbols: z14.array(z14.string().max(50)).max(200).describe("Raw symbols to track (e.g., ['AAPL', 'BTC'])")
1744
2339
  }),
1745
2340
  readOnly: false,
1746
2341
  openWorld: false,
@@ -1787,8 +2382,8 @@ function createWorkspaceModule(dataDir, defaultExchange = "NASDAQ") {
1787
2382
  {
1788
2383
  name: "workspace_get_thesis",
1789
2384
  description: "Get the saved global investment thesis for a specific symbol. Returns a stable JSON shape with a 'found' flag.",
1790
- inputSchema: z13.object({
1791
- symbol: z13.string().max(50).describe("The raw symbol (e.g., 'AAPL')")
2385
+ inputSchema: z14.object({
2386
+ symbol: z14.string().max(50).describe("The raw symbol (e.g., 'AAPL')")
1792
2387
  }),
1793
2388
  readOnly: true,
1794
2389
  openWorld: false,
@@ -1806,13 +2401,13 @@ function createWorkspaceModule(dataDir, defaultExchange = "NASDAQ") {
1806
2401
  {
1807
2402
  name: "workspace_save_thesis",
1808
2403
  description: "Save or update the global investment thesis for a specific symbol (max 200 per workspace).",
1809
- inputSchema: z13.object({
1810
- symbol: z13.string().max(50),
1811
- summary: z13.string().max(5e3),
1812
- bullCase: z13.string().max(2e3).optional(),
1813
- bearCase: z13.string().max(2e3).optional(),
1814
- catalyst: z13.string().max(2e3).optional(),
1815
- timeframe: z13.string().max(100).optional()
2404
+ inputSchema: z14.object({
2405
+ symbol: z14.string().max(50),
2406
+ summary: z14.string().max(5e3),
2407
+ bullCase: z14.string().max(2e3).optional(),
2408
+ bearCase: z14.string().max(2e3).optional(),
2409
+ catalyst: z14.string().max(2e3).optional(),
2410
+ timeframe: z14.string().max(100).optional()
1816
2411
  }),
1817
2412
  readOnly: false,
1818
2413
  openWorld: false,
@@ -1877,6 +2472,7 @@ var MODULE_CATALOG = [
1877
2472
  { name: "fred", envVar: "FRED_API_KEY", toolCount: 4, factory: (config) => config.env.FRED_API_KEY ? createFredModule(config.env.FRED_API_KEY) : null },
1878
2473
  { name: "sentiment", envVar: null, toolCount: 2, factory: () => createSentimentModule() },
1879
2474
  { name: "frankfurter", envVar: null, toolCount: 5, factory: () => createFrankfurterModule() },
2475
+ { name: "reddit", envVar: null, toolCount: 3, factory: () => createRedditModule() },
1880
2476
  { name: "workspace", envVar: null, toolCount: 7, factory: (config) => config.enableWorkspace ? createWorkspaceModule(config.dataDir || path3.join(os2.homedir(), ".stock-scanner-mcp"), config.defaultExchange) : null }
1881
2477
  ];
1882
2478
  var TOTAL_TOOLS = MODULE_CATALOG.reduce((n, m) => n + m.toolCount, 0);
@@ -1896,7 +2492,7 @@ OPTIONS
1896
2492
  --enable-workspace Enable stateful workspace (watchlists, theses, profile)
1897
2493
  --data-dir <path> Directory for workspace storage (default: ~/.stock-scanner-mcp)
1898
2494
 
1899
- MODULES (61 tools total)
2495
+ MODULES (64 tools total)
1900
2496
  tradingview 10 tools Stock scanning, quotes, technicals (no key)
1901
2497
  tradingview-crypto 4 tools Crypto pair scanning and technicals (no key)
1902
2498
  sec-edgar 6 tools SEC filings, insider trades, holdings (no key)
@@ -1905,6 +2501,7 @@ MODULES (61 tools total)
1905
2501
  options-cboe 1 tool CBOE put/call ratio sentiment (no key)
1906
2502
  sentiment 2 tools Market & crypto Fear & Greed sentiment (no key)
1907
2503
  frankfurter 5 tools Forex exchange rates and conversion (no key)
2504
+ reddit 3 tools Reddit trending tickers and sentiment (no key)
1908
2505
  workspace 7 tools Stateful watchlists, theses, and profile (no key)
1909
2506
  finnhub 9 tools Quotes, profiles, peers, news, earnings (FINNHUB_API_KEY)
1910
2507
  alpha-vantage 5 tools Stock quotes, fundamentals, dividends (ALPHA_VANTAGE_API_KEY)
@@ -2004,7 +2601,7 @@ Tools registered: ${registeredTools}/${TOTAL_TOOLS}`);
2004
2601
  server.registerPrompt("analyze_stock", {
2005
2602
  description: "Analyze a stock ticker \u2014 includes crypto correlation check for crypto-related companies",
2006
2603
  argsSchema: {
2007
- ticker: z14.string().describe("Stock ticker symbol (e.g. AAPL, CIFR, MARA)")
2604
+ ticker: z15.string().describe("Stock ticker symbol (e.g. AAPL, CIFR, MARA)")
2008
2605
  }
2009
2606
  }, async ({ ticker }) => {
2010
2607
  return {
@@ -2038,8 +2635,8 @@ Tools registered: ${registeredTools}/${TOTAL_TOOLS}`);
2038
2635
  server.registerPrompt("intraday_candidates", {
2039
2636
  description: "Find intraday trading candidates with custom price range and filters",
2040
2637
  argsSchema: {
2041
- min_price: z14.string().optional().describe("Minimum stock price (default: 10)"),
2042
- max_price: z14.string().optional().describe("Maximum stock price (default: 50)")
2638
+ min_price: z15.string().optional().describe("Minimum stock price (default: 10)"),
2639
+ max_price: z15.string().optional().describe("Maximum stock price (default: 50)")
2043
2640
  }
2044
2641
  }, async ({ min_price, max_price }) => {
2045
2642
  const min = min_price || "10";
@@ -39,7 +39,7 @@ import {
39
39
  scanStocks,
40
40
  searchFilings,
41
41
  searchSeries
42
- } from "../chunk-HAYAISNP.js";
42
+ } from "../chunk-T5IF6777.js";
43
43
 
44
44
  // src/sidecar/server.ts
45
45
  import * as http from "http";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stock-scanner-mcp",
3
3
  "mcpName": "io.github.yyordanov-tradu/stock-scanner-mcp",
4
- "version": "1.16.0",
4
+ "version": "1.16.1",
5
5
  "description": "MCP server providing Claude Code with real-time stock and crypto market data, SEC filings, insider trades, and technical analysis",
6
6
  "type": "module",
7
7
  "bin": {
@@ -88,7 +88,7 @@
88
88
  "@types/proper-lockfile": "^4.1.4",
89
89
  "tsup": "^8.0.0",
90
90
  "tsx": "^4.21.0",
91
- "typescript": "^5.7.0",
91
+ "typescript": "^6.0.2",
92
92
  "vitest": "^4.1.1"
93
93
  }
94
94
  }