Jaspers Hub

jaspers / yfinance

pluginOfficialReviewed★ 0↓ 11

yfinance: Free Yahoo Finance market data (quotes, history, fundamentals, options, news, movers) via a bundled MCP server, with five views.

7.2 KB
1import { defineConnection, definePlugin, defineSource, defineView } from '@jaspers-ai/sdk'
2import { z } from 'zod'
3import { FinancialsView } from './Financials'
4import {
5 FINANCIALS_INSTRUCTIONS,
6 MARKETS_INSTRUCTIONS,
7 OPTIONS_INSTRUCTIONS,
8 OVERVIEW_INSTRUCTIONS,
9 WATCHLIST_INSTRUCTIONS,
10} from './instructions'
11import { MarketsView } from './Markets'
12import { OptionsView } from './Options'
13import { OverviewView } from './Overview'
14import { FREQUENCIES, STATEMENTS } from './server/financials'
15import { MOVER_LISTS, type MoverList } from './server/quotes'
16import {
17 financialsSummary,
18 marketsSummary,
19 optionsSummary,
20 overviewSummary,
21 watchlistSummary,
22 WATCHLIST_MAX,
23} from './views'
24import { 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
32const SECOND = 1000
33const MINUTE = 60 * SECOND
34const HOUR = 60 * MINUTE
35
36const tool = (name: string, ttlMs: number) => defineSource({ mcp: 'server', tool: name, ttlMs })
37
38const search = tool('search', HOUR)
39const quote = tool('get_quote', 15 * SECOND)
40const history = tool('get_price_history', MINUTE)
41const profile = tool('get_company_profile', 6 * HOUR)
42const statistics = tool('get_key_statistics', 5 * MINUTE)
43const financials = tool('get_financials', 6 * HOUR)
44const earnings = tool('get_earnings', HOUR)
45const analysts = tool('get_analyst_ratings', HOUR)
46const holders = tool('get_holders', 6 * HOUR)
47const options = tool('get_options_chain', MINUTE)
48const news = tool('get_news', 5 * MINUTE)
49const marketSummary = tool('get_market_summary', 30 * SECOND)
50const movers = tool('get_market_movers', MINUTE)
51const fund = tool('get_fund_holdings', 6 * HOUR)
52
53const Symbol = z.string().max(32).default('').describe('A symbol: AAPL, BRK-B, SHOP.TO, ^GSPC, ES=F, EURUSD=X, BTC-USD.')
54const Day = z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
55const Amount = z.number().nullable()
56const Errors = z.string().nullable()
57const LISTS = Object.keys(MOVER_LISTS) as [MoverList, ...MoverList[]]
58
59const Listing = z.object({ symbol: z.string(), name: z.string().nullable(), price: Amount, changePercent: Amount })
60
61const OverviewState = z.object({ symbol: Symbol })
62const 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
84const WatchlistState = z.object({
85 symbols: z.array(z.string().max(32)).max(WATCHLIST_MAX).default([]).describe('The symbols in order, up to 30.'),
86})
87const 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
93const MarketsState = z.object({ list: z.enum(LISTS).default('gainers') })
94const 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
102const 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})
107const 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
118const 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})
123const 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
135export 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})