finance-search
Search for financial instruments including stocks, ETFs, cryptocurrencies, and futures using Google Finance data with symbol:exchange format queries.
Instructions
Search for stocks, indices, mutual funds, currencies, and futures using Google Finance. Use format 'SYMBOL:EXCHANGE'. Stocks: 'AAPL:NASDAQ', 'TSLA:NASDAQ'. ETFs/Index funds: 'SPY:NYSEARCA', 'VTI:NYSEARCA', 'QQQ:NASDAQ', 'VOO:NYSEARCA'. Crypto: 'BTC-USD', 'ETH-USD'. Note: Query 'SPY' (no exchange) returns market overview with S&P 500 related ETFs.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| q | Yes | Stock symbol with exchange in format 'SYMBOL:EXCHANGE'. Examples: 'AAPL:NASDAQ', 'TSLA:NASDAQ', 'MSFT:NASDAQ', 'GOOGL:NASDAQ', 'NVDA:NASDAQ' for stocks. For ETFs/Index funds: 'SPY:NYSEARCA', 'VTI:NYSEARCA', 'QQQ:NASDAQ', 'VOO:NYSEARCA'. For crypto: 'BTC-USD', 'ETH-USD'. Note: Just 'SPY' returns market overview with related ETFs in futures_chain. | |
| window | No | Time range for graph data | |
| async | No | Submit search asynchronously | |
| include_markets | No | Include market overview data | |
| include_discover | No | Include discover more section | |
| include_news | No | Include top news | |
| max_futures | No | Maximum number of futures chain items to return | |
| summary_only | No | Return only essential information (price, movement, news) | |
| max_news | No | Maximum number of news articles to return (1 = only top news from Google Finance, >1 = additional news from Brave Search) |
Implementation Reference
- src/finance.ts:425-481 (handler)Main handler function that performs the finance search using SerpAPI Google Finance, optionally Brave Search for news, filters response, and returns formatted Markdown.export async function searchFinance( params: z.infer<typeof financeSearchSchema> ): Promise<string> { const apiKey = process.env.SERP_API_KEY; if (!apiKey) { throw new Error("SERP_API_KEY environment variable is required"); } const { include_markets, include_discover, include_news, max_futures, summary_only, max_news, ...apiParams } = params; const searchParams = new URLSearchParams({ engine: "google_finance", api_key: apiKey, q: apiParams.q, hl: "en", ...(apiParams.window && { window: apiParams.window }), ...(apiParams.async !== undefined && { async: apiParams.async.toString() }), }); try { const response = await axios.get<SerpApiFinanceResponse>( `${SERPAPI_BASE_URL}?${searchParams.toString()}` ); // Check for API errors first if (response.data.error) { throw new Error(`SerpAPI Error: ${response.data.error}`); } const filteredData = filterFinanceResponse(response.data, params); // Fetch additional news if requested let braveNews: BraveNewsResult[] = []; if (max_news && max_news > 1 && include_news !== false) { braveNews = await fetchAdditionalNews(params.q, max_news); } return formatFinanceToMarkdown(filteredData, params, braveNews); } catch (error) { if (axios.isAxiosError(error)) { throw new Error( `Finance API request failed: ${ error.response?.data?.error || error.message }` ); } throw error; } }
- src/finance.ts:285-328 (schema)Zod schema for input validation of the finance-search tool parameters.const financeSearchSchema = z.object({ q: z .string() .describe( "Stock symbol with exchange in format 'SYMBOL:EXCHANGE'. Examples: 'AAPL:NASDAQ', 'TSLA:NASDAQ', 'MSFT:NASDAQ', 'GOOGL:NASDAQ', 'NVDA:NASDAQ' for stocks. For ETFs/Index funds: 'SPY:NYSEARCA', 'VTI:NYSEARCA', 'QQQ:NASDAQ', 'VOO:NYSEARCA'. For crypto: 'BTC-USD', 'ETH-USD'. Note: Just 'SPY' returns market overview with related ETFs in futures_chain." ), window: z .enum(["1D", "5D", "1M", "6M", "YTD", "1Y", "5Y", "MAX"]) .optional() .describe("Time range for graph data"), async: z.boolean().optional().describe("Submit search asynchronously"), include_markets: z .boolean() .optional() .default(false) .describe("Include market overview data"), include_discover: z .boolean() .optional() .default(false) .describe("Include discover more section"), include_news: z .boolean() .optional() .default(true) .describe("Include top news"), max_futures: z .number() .optional() .default(3) .describe("Maximum number of futures chain items to return"), summary_only: z .boolean() .optional() .default(true) .describe("Return only essential information (price, movement, news)"), max_news: z .number() .optional() .default(1) .describe( "Maximum number of news articles to return (1 = only top news from Google Finance, >1 = additional news from Brave Search)" ), });
- src/index.ts:120-148 (registration)MCP server tool registration for 'finance-search', providing description, schema, and handler wrapper.server.tool( "finance-search", "Search for stocks, indices, mutual funds, currencies, and futures using Google Finance. Use format 'SYMBOL:EXCHANGE'. Stocks: 'AAPL:NASDAQ', 'TSLA:NASDAQ'. ETFs/Index funds: 'SPY:NYSEARCA', 'VTI:NYSEARCA', 'QQQ:NASDAQ', 'VOO:NYSEARCA'. Crypto: 'BTC-USD', 'ETH-USD'. Note: Query 'SPY' (no exchange) returns market overview with S&P 500 related ETFs.", financeSearchSchema.shape, async (params) => { try { const result = await searchFinance(params); return { content: [ { type: "text", text: result, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error searching finance data: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } } );
- src/finance.ts:121-283 (helper)Helper function to format the financial data and news into a structured Markdown response.function formatFinanceToMarkdown( data: any, params: z.infer<typeof financeSearchSchema>, braveNews: BraveNewsResult[] = [] ): string { if (!data) return "No financial data available."; let markdown = `# ${params.q}\n\n`; // Main stock/security info if (data.summary) { const summary = data.summary; const price = summary.price !== undefined ? summary.price : "N/A"; markdown += `Current Price: ${summary.currency || "$"}${price} \n`; if (summary.price_movement) { const movement = summary.price_movement; const arrow = movement.movement === "up" ? "📈" : movement.movement === "down" ? "📉" : "➡️"; markdown += `Change: ${arrow} ${movement.percentage}% (${ movement.value >= 0 ? "+" : "" }${movement.value}) \n`; } if (summary.name && summary.name !== summary.symbol) { markdown += `Name: ${summary.name} \n`; } markdown += `\n`; } // Price insights if (data.price_insights) { const insights = data.price_insights; markdown += `## Price Analysis\n\n`; if (insights.previous_close) markdown += `Previous Close: $${insights.previous_close} \n`; if (insights.day_range) markdown += `Day Range: $${insights.day_range} \n`; if (insights.year_range) markdown += `52-Week Range: $${insights.year_range} \n`; if (insights.market_cap) markdown += `Market Cap: ${insights.market_cap} \n`; if (insights.pe_ratio) markdown += `P/E Ratio: ${insights.pe_ratio} \n`; markdown += `\n`; } // Futures chain (for summary mode) if ( data.futures_chain && params.summary_only && data.futures_chain.length > 0 ) { const futures = data.futures_chain.slice(0, params.max_futures || 3); if (futures.length > 1) { markdown += `## Futures Contracts\n\n`; futures.forEach((future: any) => { markdown += `${future.date || future.stock}: $${ future.price || future.extracted_price }`; if (future.change) markdown += ` (${future.change})`; markdown += ` \n`; }); markdown += `\n`; } } // Top news - safely handle different data structures if (data.top_news && params.include_news !== false) { try { let newsItems: any[] = []; if (Array.isArray(data.top_news)) { newsItems = data.top_news; } else if ( data.top_news.results && Array.isArray(data.top_news.results) ) { newsItems = data.top_news.results; } else if (typeof data.top_news === "object") { // If it's a single news object, put it in an array if ( data.top_news.title || data.top_news.headline || data.top_news.snippet ) { newsItems = [data.top_news]; } else { // Try to convert object values to array newsItems = Object.values(data.top_news).filter( (item: any) => item && typeof item === "object" && (item.title || item.headline || item.snippet) ); } } if (newsItems.length > 0) { markdown += `## Latest News\n\n`; newsItems.slice(0, 5).forEach((news: any, index: number) => { if (news && (news.title || news.headline || news.snippet)) { markdown += `### ${index + 1}. ${ news.title || news.headline || "News Update" }\n`; if (news.source) markdown += `Source: ${news.source} \n`; if (news.date || news.published_date) markdown += `Date: ${news.date || news.published_date} \n`; if (news.snippet || news.description) markdown += `${news.snippet || news.description} \n`; if (news.link || news.url) markdown += `[Read More](${news.link || news.url}) \n`; markdown += `\n`; } }); } } catch (error) { // Silently skip news section if there's an error } } // Additional news from Brave Search if (braveNews.length > 0 && params.include_news !== false) { if (!data.top_news || Object.keys(data.top_news || {}).length === 0) { markdown += `## Latest News\n\n`; } braveNews.forEach((news, index) => { const newsIndex = data.top_news ? index + 2 : index + 1; // Start after Google Finance news markdown += `### ${newsIndex}. ${news.title}\n`; if (news.source?.name) markdown += `Source: ${news.source.name} \n`; if (news.published_datetime) markdown += `Date: ${news.published_datetime} \n`; if (news.description) markdown += `${news.description} \n`; if (news.url) markdown += `[Read More](${news.url}) \n`; markdown += `\n`; }); } // Market overview (if included) if (data.markets && params.include_markets) { markdown += `## Market Overview\n\n`; if (data.markets.top_news) { markdown += `Market News Available: ${data.markets.top_news.length} articles \n`; } markdown += `\n`; } // Discover more (if included) if (data.discover_more && params.include_discover) { markdown += `## Related\n\n`; if (data.discover_more.similar_stocks) { markdown += `Similar Stocks: ${data.discover_more.similar_stocks .map((s: any) => s.stock || s) .join(", ")} \n`; } markdown += `\n`; } return markdown; }