yfinance: Free Yahoo Finance market data (quotes, history, fundamentals, options, news, movers) via a bundled MCP server, with five views.
| 1 | // What every tool answers: text for the model, and structuredContent with a `rows` array, since rows |
| 2 | // are the one shape a view can read (the app keeps them as a dataset, the rest as its meta). |
| 3 | |
| 4 | export type ToolResult = { |
| 5 | [key: string]: unknown |
| 6 | content: { type: 'text'; text: string }[] |
| 7 | structuredContent: Record<string, unknown> & { rows: unknown[] } |
| 8 | } |
| 9 | |
| 10 | /** About how much text a tool answers with; lines past it are left out, and the text says how many. */ |
| 11 | export const TEXT_MAX = 30_000 |
| 12 | |
| 13 | export function toolResult(text: string, structured: Record<string, unknown> & { rows: unknown[] }): ToolResult { |
| 14 | return { content: [{ type: 'text', text }], structuredContent: structured } |
| 15 | } |
| 16 | |
| 17 | /** The head, then as many lines as fit, then `more(n)` when n lines were left out. */ |
| 18 | export function capped(head: string[], lines: string[], more: (left: number) => string, max = TEXT_MAX): string { |
| 19 | const out = [...head] |
| 20 | let size = out.join('\n').length |
| 21 | let kept = 0 |
| 22 | for (const line of lines) { |
| 23 | if (size + line.length + 1 > max) break |
| 24 | out.push(line) |
| 25 | size += line.length + 1 |
| 26 | kept++ |
| 27 | } |
| 28 | if (kept < lines.length) out.push(more(lines.length - kept)) |
| 29 | return out.join('\n') |
| 30 | } |
| 31 | |
| 32 | /** The parts that have something in them, joined. */ |
| 33 | export function parts(...items: (string | null | undefined | false)[]): string { |
| 34 | return items.filter((item): item is string => typeof item === 'string' && item !== '').join(', ') |
| 35 | } |