Subscribe to mailing list

Get notified when we have new updates or new posts!

Subscribe Unicorn Data Science cover image
jen@unicornds.org profile image jen@unicornds.org

MCP One Day Tour: Inject Data into LLM with Model Context Protocol

Ever wonder what Model Context Protocol (MCP) can do for you? This one-day tour shows how you can supercharge LLMs with custom data sources, enabling them to perform specialized tasks like stock analysis.

MCP One Day Tour: Inject Data into LLM with Model Context Protocol
Photo by Yashowardhan Singh / Unsplash

In November 2024, Anthropic announced the Model Context Protocol (MCP). Since then, several open-source MCP projects have emerged that leverage LLMs to manage various workflows with greater convenience and intelligence, including Slack automation, todo list management, web search, database queries, and many more.

LLMs become significantly more capable when provided with relevant context. MCP offers a standardized framework for injecting this additional context, enabling models to perform more specialized and informed tasks without requiring fine-tuning.

There are many applications for MCP. So let's just dive into it with a simple example. In today's one day tour, we will create an MCP server that provides statistical analysis on stock data. Combining with the capability of LLM, we can end up with results like below:

A stock analysis performed by Claude, enabled with MCP.

Prerequisites

We will use the MCP python SDK, so install it with: uv pip install "mcp[cli]"

For guidance on uv, you can check out our uv one day tour.

We will also need an LLM client that interacts with our MCP server. The easiest is to use Claude desktop, which you can download from here. It does mean you should also have a Claude account. If you don't already use Claude, no worries, there are many MCP clients you can choose from.

Getting Data

We will use the Stock Market Dataset. This is a static dataset that you can download. Once you become familiar with MCP and how to feed data, you can use your favorite stock data API for up-to-date data feed.

Download this dataset. What you will see is that there's a symbols_valid_meta.csv file that maps company name to their stock symbol, and in the stocks and etfs subfolder, there are more than 8,000 csv files, each corresponds to an individual stock.

If we were to analyze several stocks, we'd need to load up several csv files and examine them manually. This would be a very tedious process. So we will connect this dataset with LLM using a simple MCP server.

The MCP Server

We will jump right into the code. The full code we are using today is below. Copy it into a server.py file.

# server.py
import json
import pandas as pd
from mcp.server.fastmcp import FastMCP, Context

FILE_PATH = "{YOUR_FILE_PATH_TO_DOWNLOADED_DATA}/data/"
mcp = FastMCP("StockExplorer", dependencies=["pandas"])


def get_likely_symbols(company_name: str) -> dict[str, str]:
    """Get all stock symbols that contain the company name."""
    all_symbols = pd.read_csv(FILE_PATH + "symbols_valid_meta.csv")
    matches = all_symbols[all_symbols["Security Name"].str.lower().str.contains(company_name.lower())]
    return matches.to_dict(orient='records')


@mcp.prompt()
async def find_ticker(company_name: str) -> str:
    likely_symbols = get_likely_symbols(company_name)
    return f"""
I need to find the ticker symbol for {company_name}.

First, please look at the likely list of stocks, provided below.
Then, search through that data to find the best matching company name and return the ticker symbol.
If there are multiple potential matches, please list the most likely one.

Here are the likely stock symbols:
{json.dumps(likely_symbols, indent=2)}
"""


@mcp.resource("stats://{symbol}")
def resource_stock_stats(symbol: str) -> str:
    """Get stock stats for a given symbol."""
    all_symbols = pd.read_csv(FILE_PATH + "symbols_valid_meta.csv")

    matching_symbols = all_symbols[all_symbols["Symbol"] == symbol]
    if matching_symbols.empty:
        raise ValueError(f"Symbol '{symbol}' not found in our database")

    symbol_data = matching_symbols.iloc[0]
    is_etf = symbol_data["ETF"] == "Y"
    if is_etf:
        folder = "etfs/"
    else:
        folder = "stocks/"

    full_path = FILE_PATH + folder + symbol + ".csv"
    df = pd.read_csv(full_path)
    oldest_date = df['Date'].min()
    newest_date = df['Date'].max()

    # Take the most recent 50 rows for analysis
    recent_df = df.tail(50).copy()

    # Calculate daily returns
    recent_df['Daily Return'] = recent_df['Adj Close'].pct_change()

    # Calculate key metrics on recent data
    recent_return = ((recent_df['Adj Close'].iloc[-1] / recent_df['Adj Close'].iloc[0]) - 1) * 100
    recent_volatility = recent_df['Daily Return'].std() * (252 ** 0.5) * 100  # Annualized

    # Calculate moving averages
    recent_df['MA20'] = recent_df['Adj Close'].rolling(window=20).mean()

    # Calculate RSI (14-day)
    delta = recent_df['Adj Close'].diff()
    gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
    # Avoid division by zero
    loss_nonzero = loss.replace(0, 0.001)
    rs = gain / loss_nonzero
    recent_df['RSI'] = 100 - (100 / (1 + rs))

    # Get high and low
    recent_high = float(recent_df['High'].max())
    recent_high_date = recent_df.loc[recent_df['High'].idxmax(), 'Date']
    recent_low = float(recent_df['Low'].min())
    recent_low_date = recent_df.loc[recent_df['Low'].idxmin(), 'Date']

    # All-time high and low
    all_time_high = float(df['High'].max())
    all_time_high_date = df.loc[df['High'].idxmax(), 'Date']
    all_time_low = float(df['Low'].min())
    all_time_low_date = df.loc[df['Low'].idxmin(), 'Date']

    # Recent average volume
    recent_avg_volume = int(recent_df['Volume'].mean())

    # Current price
    current_price = float(recent_df['Close'].iloc[-1])

    stats = {
        "data_summary": {
            "full_date_range": f"{oldest_date} to {newest_date}",
            "analysis_period": "Last 50 trading days"
        },
        "recent_performance": {
            "return_pct": round(recent_return, 2),
            "annualized_volatility_pct": round(recent_volatility, 2),
            "current_rsi": round(float(recent_df['RSI'].iloc[-1]), 2),
            "highest_price": {"price": round(recent_high, 2), "date": recent_high_date},
            "lowest_price": {"price": round(recent_low, 2), "date": recent_low_date}
        },
        "technical_indicators": {
            "current_price": round(current_price, 2),
            "20_day_ma": round(float(recent_df['MA20'].iloc[-1]), 2) if not pd.isna(recent_df['MA20'].iloc[-1]) else None,
            "price_vs_ma20": f"{round((current_price / float(recent_df['MA20'].iloc[-1]) - 1) * 100, 2)}%" if not pd.isna(recent_df['MA20'].iloc[-1]) else None
        },
        "historical_context": {
            "all_time_high": {"price": round(all_time_high, 2), "date": all_time_high_date},
            "all_time_low": {"price": round(all_time_low, 2), "date": all_time_low_date},
            "current_vs_ath": f"{round((current_price / all_time_high - 1) * 100, 2)}%"
        },
        "volume": {
            "recent_avg_volume": recent_avg_volume,
            "latest_volume": int(recent_df['Volume'].iloc[-1]),
            "volume_trend": "Above average" if recent_df['Volume'].iloc[-1] > recent_avg_volume else "Below average"
        }
    }
    return json.dumps(stats, indent=2)



@mcp.tool()
async def get_stock_stats(symbol: str, ctx: Context) -> str:
    """Get stats on a stock."""
    res = await ctx.read_resource(f"stats://{symbol}")
    return res


Once you have set up this server.py, you can run it with mcp install server.py

Then, you should see the StockExplorer server being recognized as an MCP server within Claude Desktop by going Claude → Settings → Developer

View installed MCP server on Claude Desktop.

Prompt

Now let's leverage the @mcp.prompt() we have defined:

# server.py

# ...

def get_likely_symbols(company_name: str) -> dict[str, str]:
    """Get all stock symbols that contain the company name."""
    all_symbols = pd.read_csv(FILE_PATH + "symbols_valid_meta.csv")
    matches = all_symbols[all_symbols["Security Name"].str.lower().str.contains(company_name.lower())]
    return matches.to_dict(orient='records')

    
@mcp.prompt()
async def find_ticker(company_name: str) -> str:
    likely_symbols = get_likely_symbols(company_name)
    return f"""
I need to find the ticker symbol for {company_name}.

First, please look at the likely list of stocks, provided below.
Then, search through that data to find the best matching company name and return the ticker symbol.
If there are multiple potential matches, please list the most likely one.

Here are the likely stock symbols:
{json.dumps(likely_symbols, indent=2)}
"""

# ...

We are effectively defining a prompt template that can be used, say, with Claude Desktop. For example, I can look up the ticker symbol for Apple by doing the following:

Using a prompt template defined in an MCP server.

As you see, we can load up the local data, and search for companies with the word "apple". There are a few choices, and the LLM determines for us which one is the most likely candidate. At the end of this prompt, we get the correct stock symbol "AAPL". In addition, since we have also defined a tool that can analyze stock statistics, Claude is also asking us if it should "check the current stats for AAPL using the stock stats tool". So let's take a look deeper into how the MCP tool is defined.

Claude is aware that we have a stock stats tool installed with MCP.

Resource and Tool

Returning to the server.py code, there is a get_stock_stats() MCP tool defined:

# server.py

# ...

@mcp.tool()
async def get_stock_stats(symbol: str, ctx: Context) -> str:
    """Get stats on a stock."""
    res = await ctx.read_resource(f"stats://{symbol}")
    return res

# ...

The function is very simple – it's just calling ctx.read_resource(f"stats://{symbol}"). So what does it do?

MCP Resource is exactly how data is exposed to the LLM client. We define resource "protocol" as a python operation, assigning it with a resource identifier "stats". Within this operation, we can load up the data, and perform any statistical analysis we'd like. We can also retrieve live data via API calls, instead of static, downloaded dataset.

# server.py

# ...


@mcp.resource("stats://{symbol}")
def resource_stock_stats(symbol: str) -> str:
    """Get stock stats for a given symbol."""
    all_symbols = pd.read_csv(FILE_PATH + "symbols_valid_meta.csv")

    matching_symbols = all_symbols[all_symbols["Symbol"] == symbol]
    if matching_symbols.empty:
        raise ValueError(f"Symbol '{symbol}' not found in our database")

    symbol_data = matching_symbols.iloc[0]
    is_etf = symbol_data["ETF"] == "Y"
    if is_etf:
        folder = "etfs/"
    else:
        folder = "stocks/"

    full_path = FILE_PATH + folder + symbol + ".csv"
    df = pd.read_csv(full_path)
    oldest_date = df['Date'].min()
    newest_date = df['Date'].max()

    # Take the most recent 50 rows for analysis
    recent_df = df.tail(50).copy()

    # Calculate daily returns
    recent_df['Daily Return'] = recent_df['Adj Close'].pct_change()

    # ... Add any stats operations you'd like ...

    stats = {
        "data_summary": {
            "full_date_range": f"{oldest_date} to {newest_date}",
            "analysis_period": "Last 50 trading days"
        },
        # ... Add any stats results you'd like ...
    }
    return json.dumps(stats, indent=2)

# ...
💡
The return of a an @mcp.resource function should be string, as far as I can tell. Didn't work when I returned a full Pandas dataframe.

While MCP Resource obtains data, it is the MCP Tool that interfaces with the LLM client, and acts just like a typical tools call (or function calling). When the get_stock_stats() tool is available, we can easily look up the stats of the AAPL stock.

Using MCP Tool to analyze a stock.

Data-Enhanced LLM

Now that Claude is injected with real data, we can perform ad hoc analysis very easily. For example, we can ask for some visual illustration of the statistics.

Ask LLM to make quick plots of stock statistics.

We can also retrieve stock statistics for multiple companies in one go. Much easier than looking up and analyzing individual csv files.

With data-enhanced LLM, we can generate insights from a large amount of data super fast!

In a typical data analysis workflow that involves multiple datasets, we have to load each of them into Pandas, and run our analysis one at a time. We can define functions to handle large-scale analysis, but these functions need to be clearly defined for a specific purpose.

With MCP, we can make the data source available to LLM. And then we can have a natural language interface to easily interrogate our datasets. In addition, we can have LLM take some quick passes at simple data visualization – so we can check if there are meaningful patterns to investigate further. This makes exploratory data analysis a lot more efficient and, honestly, quite fun!

I found MCP to be a really cool way to turbo charge LLM, so it can help me get more work done. As we move towards a reality where LLMs exist in all human-computer interfaces, a consistent framework for introducing external data into LLMs will be as prevalent as APIs are in today's software ecosystem. So give it a try and see what you can build with MCP!