Back to Blog
GENERATIVE AIMCP

How to Build the Ultimate MCP Stock Analysis Tool for AI Trading Success

Seth Hobson
Seth Hobson
January 9, 2025
15 MIN READ

Update: This tutorial has evolved into MaverickMCP, a production-ready trading platform with 29 professional tools, 4x faster performance, and one-command setup. Read about the complete transformation here.

Hey folks! If you've been following my series on AI-powered trading, you've seen how we built up to a full agent using LangChain and LangGraph in previous articles. Today, I'm taking a fresh angle—creating a dedicated MCP server for stock analysis that any AI assistant can access. Think of it as giving Claude its own Bloomberg Terminal (okay, maybe not quite that fancy, but you get the idea).

What is MCP?

The Model Context Protocol (MCP) is an open protocol that standardizes how AI models interact with external tools, data sources, and systems. For traders and financial developers, MCP provides a structured way to expose market data, analysis tools, and trading capabilities to Large Language Models (LLMs) like Claude.

The protocol's architecture centers around three key primitives: resources for exposing data, tools for executing actions, and prompts for templating interactions. This design allows secure, controlled access to financial data and trading functionality while maintaining a clear separation between AI capabilities and execution authority. While LLMs can analyze data and suggest trades through MCP, all actions require explicit human approval—a critical feature for maintaining oversight in financial operations on your local machine.

MCP's significance in trading applications stems from its ability to standardize how AI assistants interact with financial data and trading infrastructure. Rather than building custom integrations for each AI model, developers can create a single MCP server that works with any compatible client. This approach reduces development overhead and ensures consistent behavior and security across AI implementations.

Getting Started with MCP

Let's begin by initializing our project using the official MCP Python template, which provides a standardized foundation for building MCP servers. The template abstracts some underlying protocol complexity, allowing us to focus on implementing our trading-specific functionality.

We'll also install the ta-lib library for much faster calculations of common technical indicators because it uses optimized, compiled C code instead of pure Python. The C library under the hood significantly reduces overhead in arithmetic loops and array operations, improving speed—especially for large datasets. I'm using Homebrew to install native binaries in macOS, but the ta-lib docs have instructions for other operating systems.

bash
# Install ta-lib
brew install ta-lib

# Create our project using uvx
uvx create-mcp-server # name the project mcp-trader

# Activate the environment and install dependencies
cd mcp-trader
source .venv/bin/activate  # or '.venv\Scripts\activate' on Windows

# Install dependencies
uv sync --dev --all-extras
uv add pandas pandas-ta ta-lib aiohttp python-dotenv numpy==1.26.4

The MCP template streamlines the development process by providing essential server components like initialization, transport handling, and protocol conformance. I'll extend this foundation with our stock analysis capabilities while maintaining clean architectural patterns.

Configuring Our Environment

While several market data providers are available (IEX Cloud, Alpha Vantage, etc.), I chose Tiingo for this project because it offers excellent historical stock data, a developer-friendly API, Websocket data, and generous rate limits, all for a fair price. Plus, their free tier is more than sufficient for our development needs.

bash
touch .env
echo "TIINGO_API_KEY=your_api_key_here" >> .env

Project Structure

The template creates a nice structure for us, but we'll need to modify it for our stock analysis needs. Here's what I'm aiming for:

bash
mcp-trader/
├── pyproject.toml
├── README.md
├── .env
└── src/
    └── mcp-trader/
        ├── __init__.py
        ├── server.py          # Our core MCP server
        ├── indicators.py      # Technical analysis functions
        └── data.py            # Data fetching layer

Building Our Data Layer

Let's start with data.py – our gateway to market data:

python
import os
import aiohttp
import pandas as pd

from datetime import datetime, timedelta
from dotenv import load_dotenv

load_dotenv()


class MarketData:
    """Handles all market data fetching operations."""

    def __init__(self):
        self.api_key = os.getenv("TIINGO_API_KEY")
        if not self.api_key:
            raise ValueError("TIINGO_API_KEY not found in environment")

        self.headers = {
            "Content-Type": "application/json",
            "Authorization": f"Token {self.api_key}"
        }

    async def get_historical_data(
        self, symbol: str, lookback_days: int = 365
    ) -> pd.DataFrame:
        """
        Fetch historical daily data for a given symbol.

        Args:
            symbol (str): The stock symbol to fetch data for.
            lookback_days (int): Number of days to look back from today.

        Returns:
            pd.DataFrame: DataFrame containing historical market data.
        """
        end_date = datetime.now()
        start_date = end_date - timedelta(days=lookback_days)

        url = (
            f"https://api.tiingo.com/tiingo/daily/{symbol}/prices?"
            f'startDate={start_date.strftime("%Y-%m-%d")}&'
            f'endDate={end_date.strftime("%Y-%m-%d")}'
        )

        try:
            async with aiohttp.ClientSession(
                timeout=aiohttp.ClientTimeout(total=10)
            ) as session:
                async with session.get(url, headers=self.headers) as response:
                    if response.status == 404:
                        raise ValueError(f"Symbol not found: {symbol}")
                    response.raise_for_status()
                    data = await response.json()

            if not data:
                raise ValueError(f"No data returned for {symbol}")

            df = pd.DataFrame(data)
            df["date"] = pd.to_datetime(df["date"])
            df.set_index("date", inplace=True)

            df[["open", "high", "low", "close"]] = df[
                ["adjOpen", "adjHigh", "adjLow", "adjClose"]
            ].round(2)
            df["volume"] = df["adjVolume"].astype(int)
            df["symbol"] = symbol.upper()

            return df

        except aiohttp.ClientError as e:
            raise ConnectionError(f"Network error: {e}")
        except Exception as e:
            raise Exception(f"Unexpected error fetching data for {symbol}: {e}")

Technical Analysis Layer

Next up is indicators.py, where we'll create our technical analysis toolkit. Based on your strategy and preferences, you can add indicators from pandas-ta that you like. The library is comprehensive and performant, leveraging ta-lib we installed earlier, and it integrates well with the Pandas ecosystem.

python
import pandas as pd
import pandas_ta as ta

from typing import Dict, Any


class TechnicalAnalysis:
    """Technical analysis toolkit with improved performance."""

    @staticmethod
    def add_core_indicators(df: pd.DataFrame) -> pd.DataFrame:
        """Add a core set of technical indicators."""
        try:
            # Adding trend indicators
            df["sma_20"] = ta.sma(df["close"], length=20)
            df["sma_50"] = ta.sma(df["close"], length=50)
            df["sma_200"] = ta.sma(df["close"], length=200)

            # Adding volatility indicators and volume
            daily_range = df["high"].sub(df["low"])
            adr = daily_range.rolling(window=20).mean()
            df["adrp"] = adr.div(df["close"]).mul(100)
            df["avg_20d_vol"] = df["volume"].rolling(window=20).mean()

            # Adding momentum indicators
            df["atr"] = ta.atr(df["high"], df["low"], df["close"], length=14)
            df["rsi"] = ta.rsi(df["close"], length=14)
            macd = ta.macd(df["close"], fast=12, slow=26, signal=9)
            if macd is not None:
                df = pd.concat([df, macd], axis=1)

            return df

        except KeyError as e:
            raise KeyError(f"Missing column in input DataFrame: {str(e)}")
        except Exception as e:
            raise Exception(f"Error calculating indicators: {str(e)}")

    @staticmethod
    def check_trend_status(df: pd.DataFrame) -> Dict[str, Any]:
        """Analyze the current trend status."""
        if df.empty:
            raise ValueError("DataFrame is empty.")

        latest = df.iloc[-1]
        return {
            "above_20sma": latest["close"] > latest["sma_20"],
            "above_50sma": latest["close"] > latest["sma_50"],
            "above_200sma": latest["close"] > latest["sma_200"],
            "20_50_bullish": latest["sma_20"] > latest["sma_50"],
            "50_200_bullish": latest["sma_50"] > latest["sma_200"],
            "rsi": latest["rsi"],
            "macd_bullish": latest.get("MACD_12_26_9", 0) > latest.get("MACDs_12_26_9", 0),
        }

Building the MCP Server

Now for the main event—our server.py. This is where we expose our stock analysis capabilities to Claude:

python
import json
import logging

from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
import mcp.server.stdio

from .data import MarketData
from .indicators import TechnicalAnalysis

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mcp-trader")

# Initialize components
market_data = MarketData()
ta = TechnicalAnalysis()
server = Server("mcp-trader")


@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
    """List available tools."""
    return [
        types.Tool(
            name="analyze-stock",
            description="Perform technical analysis on a stock symbol",
            inputSchema={
                "type": "object",
                "properties": {
                    "symbol": {
                        "type": "string",
                        "description": "Stock ticker symbol (e.g., AAPL)",
                    },
                },
                "required": ["symbol"],
            },
        ),
    ]


@server.call_tool()
async def handle_call_tool(
    name: str, arguments: dict
) -> list[types.TextContent]:
    """Handle tool calls."""
    if name != "analyze-stock":
        raise ValueError(f"Unknown tool: {name}")

    symbol = arguments.get("symbol", "").upper()
    if not symbol:
        raise ValueError("Symbol is required")

    try:
        # Fetch data and calculate indicators
        df = await market_data.get_historical_data(symbol)
        df = ta.add_core_indicators(df)

        # Get latest values
        latest = df.iloc[-1]

        # Prepare analysis result
        analysis = {
            "symbol": symbol,
            "price": {
                "current": latest["close"],
                "open": latest["open"],
                "high": latest["high"],
                "low": latest["low"],
            },
            "indicators": {
                "sma_20": round(latest["sma_20"], 2),
                "sma_50": round(latest["sma_50"], 2),
                "sma_200": round(latest["sma_200"], 2),
                "rsi": round(latest["rsi"], 2),
                "atr": round(latest["atr"], 2),
            },
            "trend": ta.check_trend_status(df),
            "volume": {
                "current": int(latest["volume"]),
                "avg_20d": int(latest["avg_20d_vol"]),
            },
        }

        return [
            types.TextContent(
                type="text",
                text=json.dumps(analysis, indent=2),
            )
        ]

    except Exception as e:
        logger.error(f"Error analyzing {symbol}: {e}")
        raise


async def main():
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="mcp-trader",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )

Configuring Claude Desktop

To use our MCP server with Claude Desktop, we need to add it to the configuration file. On macOS, this file is located at ~/Library/Application Support/Claude/claude_desktop_config.json:

json
{
  "mcpServers": {
    "mcp-trader": {
      "command": "uv",
      "args": [
        "--directory",
        "/path/to/mcp-trader",
        "run",
        "mcp-trader"
      ]
    }
  }
}

After saving this configuration, restart Claude Desktop. You should see the MCP tools available in the interface.

Testing Our Server

With everything set up, we can now ask Claude to analyze stocks. Try prompts like:

  • "Analyze AAPL for me"
  • "What's the technical outlook for NVDA?"
  • "Is MSFT in an uptrend?"

Claude will use our MCP server to fetch real market data, calculate technical indicators, and provide analysis based on current market conditions.

What's Next?

In Part 2, we'll expand our MCP server with more advanced features:

  • Relative Strength Calculations – Compare stocks against benchmarks
  • Volume Profile Analysis – Identify key support and resistance levels
  • Pattern Recognition – Detect chart patterns automatically
  • Risk Management Tools – Position sizing and stop loss suggestions

Wrapping Up

We've built a functional MCP server that gives Claude access to real-time stock analysis capabilities. This is just the beginning—the MCP protocol opens up incredible possibilities for integrating AI with financial data and trading systems.

The code for this project is available on GitHub. Feel free to fork it, extend it, and make it your own!

Remember: This is for educational purposes. Always do your own research and consider consulting with a financial advisor before making trading decisions.