Skip to main content
Glama
ricleedo

Google Services MCP Server

by ricleedo

finance-search

Search Google Finance for stocks, ETFs, indices, cryptocurrencies, and futures using symbol:exchange format to get price data, charts, and financial news.

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
NameRequiredDescriptionDefault
qYesStock 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.
windowNoTime range for graph data
asyncNoSubmit search asynchronously
include_marketsNoInclude market overview data
include_discoverNoInclude discover more section
include_newsNoInclude top news
max_futuresNoMaximum number of futures chain items to return
summary_onlyNoReturn only essential information (price, movement, news)
max_newsNoMaximum number of news articles to return (1 = only top news from Google Finance, >1 = additional news from Brave Search)

Implementation Reference

  • Main handler function that queries SerpAPI for Google Finance data, filters response, optionally fetches additional news from Brave, and formats output to 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;
      }
    }
  • Zod schema defining input parameters for the finance-search tool, including query, options for markets, news, etc.
    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)
    Registration of the finance-search tool on the MCP server, providing description, schema, and wrapper handler that calls searchFinance.
    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)
                }`,
              },
            ],
          };
        }
      }
    );
  • Helper function that formats the raw finance API data into a structured Markdown response including price, changes, news, etc.
    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;
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ricleedo/Google-Service-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server