#!/usr/bin/env npx tsx /** * MCP Financial Data Tool — Monetized with SettleGrid * * A complete MCP server that provides stock quotes, historical prices, * and financial statements. Fork, add your API key, and deploy. * * Setup: * 1. npm install @settlegrid/mcp * 2. Set FINANCIAL_API_KEY and SETTLEGRID_API_KEY in your env * 3. Register your tool at settlegrid.ai/dashboard/tools * 4. Run: npx tsx mcp-financial-data.ts * * Pricing: 2 cents per quote, 3 cents per historical, 5 cents per financials * - Alpha Vantage premium costs ~$0.002/call (500 calls/min at $49.99/mo) * - 2 cents per quote = ~10x margin * - Historical data is heavier, 3 cents = ~7x margin * - Financial statements require parsing, 5 cents = ~12x margin * * Revenue: You keep 95-100% (100% on Free tier, 95% on paid tiers) */ import { settlegrid } from '@settlegrid/mcp' // ── SettleGrid Setup ──────────────────────────────────────────────────────── const sg = settlegrid.init({ toolSlug: 'my-financial-data', // Replace with your tool slug pricing: { defaultCostCents: 2, methods: { get_quote: { costCents: 2, displayName: 'Get Quote' }, get_historical: { costCents: 3, displayName: 'Historical Prices' }, get_financials: { costCents: 5, displayName: 'Financial Statements' }, }, }, }) // ── Alpha Vantage API Helper ──────────────────────────────────────────────── async function avFetch(params: Record): Promise> { const url = new URL('https://www.alphavantage.co/query') url.searchParams.set('apikey', process.env.FINANCIAL_API_KEY!) for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v) const response = await fetch(url.toString()) if (!response.ok) throw new Error(`Financial API returned ${response.status}: ${response.statusText}`) const data = await response.json() if (data['Error Message']) throw new Error(`API error: ${data['Error Message']}`) return data } const TICKER_RE = /^[A-Z]{1,5}$/ function validateSymbol(symbol: string): string { const upper = (symbol ?? '').toUpperCase().trim() if (!TICKER_RE.test(upper)) throw new Error('Symbol must be 1-5 uppercase letters (e.g. "AAPL", "MSFT")') return upper } // ── Financial Data Methods ────────────────────────────────────────────────── interface QuoteArgs { symbol: string } async function getQuote(args: QuoteArgs): Promise<{ quote: { symbol: string; price: number; change: number; changePercent: string; volume: number; latestDay: string } }> { const symbol = validateSymbol(args.symbol) const data = await avFetch({ function: 'GLOBAL_QUOTE', symbol }) const q = data['Global Quote'] as Record if (!q || !q['05. price']) throw new Error(`No quote data found for ${symbol}`) return { quote: { symbol, price: parseFloat(q['05. price']), change: parseFloat(q['09. change']), changePercent: q['10. change percent'] ?? '0%', volume: parseInt(q['06. volume'] ?? '0', 10), latestDay: q['07. latest trading day'] ?? '', }, } } interface HistoricalArgs { symbol: string; period?: 'daily' | 'weekly' | 'monthly'; limit?: number } async function getHistorical(args: HistoricalArgs): Promise<{ candles: Array<{ date: string; open: number; high: number; low: number; close: number; volume: number }> }> { const symbol = validateSymbol(args.symbol) const limit = Math.min(Math.max(args.limit ?? 30, 1), 365) const fnMap = { daily: 'TIME_SERIES_DAILY', weekly: 'TIME_SERIES_WEEKLY', monthly: 'TIME_SERIES_MONTHLY' } as const const data = await avFetch({ function: fnMap[args.period ?? 'daily'], symbol, outputsize: 'compact' }) const seriesKey = Object.keys(data).find((k) => k.includes('Time Series')) if (!seriesKey) throw new Error(`No historical data found for ${symbol}`) const series = data[seriesKey] as Record> const candles = Object.entries(series).slice(0, limit).map(([date, v]) => ({ date, open: parseFloat(v['1. open']), high: parseFloat(v['2. high']), low: parseFloat(v['3. low']), close: parseFloat(v['4. close']), volume: parseInt(v['5. volume'] ?? '0', 10), })) return { candles } } interface FinancialsArgs { symbol: string; statement?: 'income' | 'balance' | 'cashflow' } async function getFinancials(args: FinancialsArgs): Promise<{ reports: Array> }> { const symbol = validateSymbol(args.symbol) const fnMap = { income: 'INCOME_STATEMENT', balance: 'BALANCE_SHEET', cashflow: 'CASH_FLOW' } as const const data = await avFetch({ function: fnMap[args.statement ?? 'income'], symbol }) const reports = (data.annualReports as Array>) ?? [] if (reports.length === 0) throw new Error(`No financial statements found for ${symbol}`) return { reports: reports.slice(0, 4) } } // ── Wrap with SettleGrid Billing ───────────────────────────────────────────── export const billedQuote = sg.wrap(getQuote, { method: 'get_quote' }) export const billedHistorical = sg.wrap(getHistorical, { method: 'get_historical' }) export const billedFinancials = sg.wrap(getFinancials, { method: 'get_financials' }) // ── REST Alternative ──────────────────────────────────────────────────────── // import { settlegridMiddleware } from '@settlegrid/mcp/rest' // // const withBilling = settlegridMiddleware({ // toolSlug: 'my-financial-data', // pricing: { // defaultCostCents: 2, // methods: { // get_quote: { costCents: 2 }, // get_historical: { costCents: 3 }, // get_financials: { costCents: 5 }, // }, // }, // }) // // export async function POST(request: Request) { // return withBilling(request, async () => { // const { symbol } = await request.json() // const result = await getQuote({ symbol }) // return Response.json(result) // }, 'get_quote') // }