yfinance: Free Yahoo Finance market data (quotes, history, fundamentals, options, news, movers) via a bundled MCP server, with five views.
| 1 | import { defineConnection, definePlugin, defineSource, defineView } from '@jaspers-ai/sdk' |
| 2 | import { z } from 'zod' |
| 3 | import { FinancialsView } from './Financials' |
| 4 | import { |
| 5 | FINANCIALS_INSTRUCTIONS, |
| 6 | MARKETS_INSTRUCTIONS, |
| 7 | OPTIONS_INSTRUCTIONS, |
| 8 | OVERVIEW_INSTRUCTIONS, |
| 9 | WATCHLIST_INSTRUCTIONS, |
| 10 | } from './instructions' |
| 11 | import { MarketsView } from './Markets' |
| 12 | import { OptionsView } from './Options' |
| 13 | import { OverviewView } from './Overview' |
| 14 | import { FREQUENCIES, STATEMENTS } from './server/financials' |
| 15 | import { MOVER_LISTS, type MoverList } from './server/quotes' |
| 16 | import { |
| 17 | financialsSummary, |
| 18 | marketsSummary, |
| 19 | optionsSummary, |
| 20 | overviewSummary, |
| 21 | watchlistSummary, |
| 22 | WATCHLIST_MAX, |
| 23 | } from './views' |
| 24 | import { WatchlistView } from './Watchlist' |
| 25 | |
| 26 | // Yahoo Finance over the plugin's own connection, yfinance/server: a small MCP server in server/ that |
| 27 | // asks Yahoo's own endpoints, with no key. One source per tool the connection offers, borrowing the |
| 28 | // server's descriptions, each kept as long as its data lasts; and five views that run them. The |
| 29 | // orchestrator gets yfinance__search, yfinance__quote, and the rest, and places a view for what one |
| 30 | // shows. Research analysts can name the connection too. |
| 31 | |
| 32 | const SECOND = 1000 |
| 33 | const MINUTE = 60 * SECOND |
| 34 | const HOUR = 60 * MINUTE |
| 35 | |
| 36 | const tool = (name: string, ttlMs: number) => defineSource({ mcp: 'server', tool: name, ttlMs }) |
| 37 | |
| 38 | const search = tool('search', HOUR) |
| 39 | const quote = tool('get_quote', 15 * SECOND) |
| 40 | const history = tool('get_price_history', MINUTE) |
| 41 | const profile = tool('get_company_profile', 6 * HOUR) |
| 42 | const statistics = tool('get_key_statistics', 5 * MINUTE) |
| 43 | const financials = tool('get_financials', 6 * HOUR) |
| 44 | const earnings = tool('get_earnings', HOUR) |
| 45 | const analysts = tool('get_analyst_ratings', HOUR) |
| 46 | const holders = tool('get_holders', 6 * HOUR) |
| 47 | const options = tool('get_options_chain', MINUTE) |
| 48 | const news = tool('get_news', 5 * MINUTE) |
| 49 | const marketSummary = tool('get_market_summary', 30 * SECOND) |
| 50 | const movers = tool('get_market_movers', MINUTE) |
| 51 | const fund = tool('get_fund_holdings', 6 * HOUR) |
| 52 | |
| 53 | const Symbol = z.string().max(32).default('').describe('A symbol: AAPL, BRK-B, SHOP.TO, ^GSPC, ES=F, EURUSD=X, BTC-USD.') |
| 54 | const Day = z.string().regex(/^\d{4}-\d{2}-\d{2}$/) |
| 55 | const Amount = z.number().nullable() |
| 56 | const Errors = z.string().nullable() |
| 57 | const LISTS = Object.keys(MOVER_LISTS) as [MoverList, ...MoverList[]] |
| 58 | |
| 59 | const Listing = z.object({ symbol: z.string(), name: z.string().nullable(), price: Amount, changePercent: Amount }) |
| 60 | |
| 61 | const OverviewState = z.object({ symbol: Symbol }) |
| 62 | const OverviewOutput = z.object({ |
| 63 | symbol: z.string(), |
| 64 | name: z.string().nullable().optional(), |
| 65 | type: z.string().nullable().optional(), |
| 66 | currency: z.string().nullable().optional(), |
| 67 | price: Amount.optional(), |
| 68 | changePercent: Amount.optional(), |
| 69 | marketState: z.string().nullable().optional(), |
| 70 | marketCap: Amount.optional(), |
| 71 | pe: Amount.optional(), |
| 72 | forwardPe: Amount.optional(), |
| 73 | dividendYieldPercent: Amount.optional(), |
| 74 | fiftyTwoWeekLow: Amount.optional(), |
| 75 | fiftyTwoWeekHigh: Amount.optional(), |
| 76 | target: z.object({ low: Amount, mean: Amount, high: Amount, analysts: Amount }).nullable().optional(), |
| 77 | recommendation: z.string().nullable().optional(), |
| 78 | nextEarnings: z.string().nullable().optional(), |
| 79 | sector: z.string().nullable().optional(), |
| 80 | industry: z.string().nullable().optional(), |
| 81 | errors: z.array(z.string()), |
| 82 | }) |
| 83 | |
| 84 | const WatchlistState = z.object({ |
| 85 | symbols: z.array(z.string().max(32)).max(WATCHLIST_MAX).default([]).describe('The symbols in order, up to 30.'), |
| 86 | }) |
| 87 | const WatchlistOutput = z.object({ |
| 88 | count: z.number(), |
| 89 | missing: z.array(z.string()), |
| 90 | rows: z.array(z.object({ symbol: z.string(), price: Amount, changePercent: Amount })), |
| 91 | }) |
| 92 | |
| 93 | const MarketsState = z.object({ list: z.enum(LISTS).default('gainers') }) |
| 94 | const MarketsOutput = z.object({ |
| 95 | list: z.enum(LISTS), |
| 96 | title: z.string().nullable(), |
| 97 | total: Amount, |
| 98 | tiles: z.array(Listing), |
| 99 | top: z.array(Listing), |
| 100 | }) |
| 101 | |
| 102 | const FinancialsState = z.object({ |
| 103 | symbol: Symbol, |
| 104 | statement: z.enum(STATEMENTS).default('income'), |
| 105 | frequency: z.enum(FREQUENCIES).default('annual').describe('trailing is twelve months, for income and cashflow only.'), |
| 106 | }) |
| 107 | const FinancialsOutput = z.object({ |
| 108 | symbol: z.string(), |
| 109 | statement: z.enum(STATEMENTS), |
| 110 | frequency: z.enum(FREQUENCIES), |
| 111 | currency: z.string().nullable(), |
| 112 | periods: z.array(z.string()), |
| 113 | lines: z.number(), |
| 114 | highlights: z.record(z.string(), z.array(Amount)), |
| 115 | error: Errors, |
| 116 | }) |
| 117 | |
| 118 | const OptionsState = z.object({ |
| 119 | symbol: Symbol, |
| 120 | expiration: Day.nullable().optional().describe('An expiration the chain lists, YYYY-MM-DD; null or left out for the nearest.'), |
| 121 | strikes: z.number().int().min(1).max(50).optional().describe('Strikes on each side of the price, 10 by default.'), |
| 122 | }) |
| 123 | const OptionsOutput = z.object({ |
| 124 | symbol: z.string(), |
| 125 | expiration: z.string().nullable(), |
| 126 | expirations: z.array(z.string()), |
| 127 | underlying: Amount, |
| 128 | strikes: z.number(), |
| 129 | atm: z.object({ strike: Amount, callMid: Amount, putMid: Amount, callIvPercent: Amount, putIvPercent: Amount }).nullable(), |
| 130 | putCallVolumeRatio: Amount, |
| 131 | putCallOpenInterestRatio: Amount, |
| 132 | error: Errors, |
| 133 | }) |
| 134 | |
| 135 | export default definePlugin({ |
| 136 | id: 'yfinance', |
| 137 | connections: { |
| 138 | // No MCP server, so this runs the small one in server/, on the Node the app |
| 139 | // runs on, which strips the types. No key: Yahoo asks for none. |
| 140 | server: defineConnection({ |
| 141 | command: ['node', '--disable-warning=MODULE_TYPELESS_PACKAGE_JSON', 'server/main.ts'], |
| 142 | tools: [ |
| 143 | 'search', |
| 144 | 'get_quote', |
| 145 | 'get_price_history', |
| 146 | 'get_company_profile', |
| 147 | 'get_key_statistics', |
| 148 | 'get_financials', |
| 149 | 'get_earnings', |
| 150 | 'get_analyst_ratings', |
| 151 | 'get_holders', |
| 152 | 'get_options_chain', |
| 153 | 'get_news', |
| 154 | 'get_market_summary', |
| 155 | 'get_market_movers', |
| 156 | 'get_fund_holdings', |
| 157 | ], |
| 158 | }), |
| 159 | }, |
| 160 | sources: { |
| 161 | search, |
| 162 | quote, |
| 163 | history, |
| 164 | profile, |
| 165 | statistics, |
| 166 | financials, |
| 167 | earnings, |
| 168 | analysts, |
| 169 | holders, |
| 170 | options, |
| 171 | news, |
| 172 | 'market-summary': marketSummary, |
| 173 | movers, |
| 174 | fund, |
| 175 | }, |
| 176 | views: { |
| 177 | overview: defineView(OverviewView, { |
| 178 | title: 'Company', |
| 179 | state: OverviewState, |
| 180 | output: OverviewOutput, |
| 181 | instructions: OVERVIEW_INSTRUCTIONS, |
| 182 | renders: [quote, statistics, analysts, earnings, profile, fund], |
| 183 | summarize: overviewSummary, |
| 184 | }), |
| 185 | watchlist: defineView(WatchlistView, { |
| 186 | title: 'Watchlist', |
| 187 | state: WatchlistState, |
| 188 | output: WatchlistOutput, |
| 189 | instructions: WATCHLIST_INSTRUCTIONS, |
| 190 | renders: [quote], |
| 191 | summarize: watchlistSummary, |
| 192 | }), |
| 193 | markets: defineView(MarketsView, { |
| 194 | title: 'Markets', |
| 195 | state: MarketsState, |
| 196 | output: MarketsOutput, |
| 197 | instructions: MARKETS_INSTRUCTIONS, |
| 198 | renders: [marketSummary, movers], |
| 199 | summarize: marketsSummary, |
| 200 | }), |
| 201 | financials: defineView(FinancialsView, { |
| 202 | title: 'Financials', |
| 203 | state: FinancialsState, |
| 204 | output: FinancialsOutput, |
| 205 | instructions: FINANCIALS_INSTRUCTIONS, |
| 206 | renders: [financials], |
| 207 | summarize: financialsSummary, |
| 208 | }), |
| 209 | options: defineView(OptionsView, { |
| 210 | title: 'Options', |
| 211 | state: OptionsState, |
| 212 | output: OptionsOutput, |
| 213 | instructions: OPTIONS_INSTRUCTIONS, |
| 214 | renders: [options], |
| 215 | summarize: optionsSummary, |
| 216 | }), |
| 217 | }, |
| 218 | }) |