What Is MCP (and Why Should You Care)?
The Model Context Protocol (MCP) is an open standard that lets AI models call your code. Think of it as a USB-C port for LLMs: one protocol, any tool. Instead of copy-pasting data into ChatGPT, your LLM client (Claude, Cursor, etc.) can call functions you define and get structured results back.
MCP servers expose tools (functions the model can call) and resources (read-only data it can inspect). The protocol handles all the JSON-RPC plumbing. You just write Python functions.
In this tutorial, we’ll build an MCP server that gives any LLM client access to live stock market data using yfinance. The whole thing is under 80 lines of Python.
What We’re Building
A Python MCP server with four tools:
- get_price – Current price, change, and volume for any ticker
- get_financials – Revenue, net income, and EPS from the latest earnings
- compare_stocks – Side-by-side comparison of multiple tickers
- get_price_history – Historical closing prices for charting or analysis
Once connected, you can ask Claude things like “Compare AAPL, MSFT, and GOOG for me” or “What were NVDA’s last quarter earnings?” and it will call your server to get real data.
Prerequisites
You need Python 3.10+ and uv (the fast Python package manager). If you don’t have uv yet:
curl -LsSf https://astral.sh/uv/install.sh | sh
Project Setup
Create a new project and install the dependencies:
uv init stock-mcp-server
cd stock-mcp-server
uv add fastmcp yfinance
FastMCP is the Pythonic way to build MCP servers. It uses decorators to turn plain functions into MCP tools, handles parameter validation, and generates JSON schemas automatically. No boilerplate.
If you want a deeper dive into yfinance itself, check out our complete guide to yfinance.
The Code
Create a file called server.py. Here’s the full server:
from fastmcp import FastMCP
import yfinance as yf
mcp = FastMCP(name="StockData")
@mcp.tool
def get_price(ticker: str) -> dict:
"""Get the current price, daily change, and volume for a stock ticker."""
stock = yf.Ticker(ticker)
info = stock.info
return {
"ticker": ticker.upper(),
"price": info.get("currentPrice") or info.get("regularMarketPrice"),
"currency": info.get("currency", "USD"),
"change_percent": info.get("regularMarketChangePercent"),
"volume": info.get("regularMarketVolume"),
"market_cap": info.get("marketCap"),
"name": info.get("shortName"),
}
@mcp.tool
def get_financials(ticker: str) -> dict:
"""Get the latest quarterly financials: revenue, net income, and EPS."""
stock = yf.Ticker(ticker)
q = stock.quarterly_financials
if q.empty:
return {"error": f"No financial data found for {ticker}"}
latest = q.iloc[:, 0]
return {
"ticker": ticker.upper(),
"period": str(q.columns[0].date()),
"revenue": latest.get("Total Revenue"),
"net_income": latest.get("Net Income"),
"ebitda": latest.get("EBITDA"),
"eps": stock.info.get("trailingEps"),
}
@mcp.tool
def compare_stocks(tickers: list[str]) -> list[dict]:
"""Compare current price, market cap, and P/E ratio for multiple tickers."""
results = []
for t in tickers:
info = yf.Ticker(t).info
results.append({
"ticker": t.upper(),
"name": info.get("shortName"),
"price": info.get("currentPrice") or info.get("regularMarketPrice"),
"market_cap": info.get("marketCap"),
"pe_ratio": info.get("trailingPE"),
"dividend_yield": info.get("dividendYield"),
"52w_high": info.get("fiftyTwoWeekHigh"),
"52w_low": info.get("fiftyTwoWeekLow"),
})
return results
@mcp.tool
def get_price_history(ticker: str, period: str = "1mo") -> dict:
"""Get historical closing prices. Period options: 1d, 5d, 1mo, 3mo, 6mo, 1y, 5y, max."""
stock = yf.Ticker(ticker)
hist = stock.history(period=period)
if hist.empty:
return {"error": f"No history for {ticker}"}
return {
"ticker": ticker.upper(),
"period": period,
"data_points": len(hist),
"prices": [
{"date": str(idx.date()), "close": round(row["Close"], 2)}
for idx, row in hist.iterrows()
],
}
if __name__ == "__main__":
mcp.run()
That’s it. 70 lines, four tools. Let’s break down what’s happening.
How FastMCP Works
FastMCP reads your function signatures and docstrings. The type hints become the JSON schema that tells the LLM what parameters each tool accepts. The docstring becomes the tool description the LLM reads to decide when to use it.
When you call mcp.run(), FastMCP starts a server that speaks the MCP protocol over stdio (standard input/output). The LLM client launches your script as a subprocess and communicates with it through stdin/stdout.
No HTTP server, no ports, no configuration. The client handles everything.
Testing Locally
Before connecting to Claude, test your tools directly. FastMCP ships with a dev mode:
uv run fastmcp dev server.py
This opens the MCP Inspector in your browser, a visual interface where you can call each tool, see the parameters, and inspect the JSON responses. Try calling get_price with ticker AAPL and you should see live market data come back.
You can also test from Python:
from fastmcp import Client
async def test():
async with Client("server.py") as client:
result = await client.call_tool("get_price", {"ticker": "TSLA"})
print(result)
import asyncio
asyncio.run(test())
Connecting to Claude Desktop
To use your server with Claude Desktop, edit the config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%Claudeclaude_desktop_config.json
Add your server:
{
"mcpServers": {
"stock-data": {
"command": "uv",
"args": ["run", "--directory", "/path/to/stock-mcp-server", "python", "server.py"]
}
}
}
Replace /path/to/stock-mcp-server with the actual path to your project folder. Restart Claude Desktop (Cmd+Q, then reopen). You should see a hammer icon in the chat input indicating tools are available.
Now try asking Claude: “Compare Apple, Microsoft, and Nvidia stock prices for me.” It will call your compare_stocks tool and format the results in a nice table.
Adding More Tools
The pattern is always the same: write a function, add @mcp.tool, use type hints and a docstring. Here are some ideas to extend the server:
@mcp.tool
def get_options_chain(ticker: str) -> dict:
"""Get the next expiration's options chain with calls and puts."""
stock = yf.Ticker(ticker)
dates = stock.options
if not dates:
return {"error": "No options data"}
chain = stock.option_chain(dates[0])
return {
"expiration": dates[0],
"top_calls": chain.calls.head(5).to_dict("records"),
"top_puts": chain.puts.head(5).to_dict("records"),
}
@mcp.tool
def get_analyst_recommendations(ticker: str) -> dict:
"""Get recent analyst recommendations and price targets."""
stock = yf.Ticker(ticker)
recs = stock.recommendations
if recs is None or recs.empty:
return {"error": "No recommendations found"}
return {
"ticker": ticker.upper(),
"target_price": stock.info.get("targetMeanPrice"),
"recommendation": stock.info.get("recommendationKey"),
"recent": recs.tail(5).to_dict("records"),
}
How This Compares to Building an AI Agent
If you’ve read our tutorial on building an AI agent in Python, you’ll notice a key difference. In that tutorial, you build the entire loop: the LLM call, the tool dispatch, the response handling. With MCP, the client (Claude, Cursor, or any MCP-compatible app) owns that loop. You only supply the tools.
That’s the whole point of MCP. Write your tools once, and they work with any client that speaks the protocol. No need to rebuild the agent loop every time.
Key Takeaways
- FastMCP turns Python functions into MCP tools with a single decorator
- Type hints and docstrings are all the LLM needs to understand your tools
- The server runs as a subprocess; no HTTP, no ports, no deployment
- You can test everything locally with
fastmcp devbefore connecting to Claude - The pattern is infinitely extensible: any Python API can become an MCP tool
The full source code for this tutorial is about 70 lines. The MCP ecosystem is growing fast. If you have a Python script that fetches data, transforms it, or calls an API, wrapping it as an MCP server takes minutes.
This is exactly what I was looking for. I adapted this to expose our internal Postgres database to Claude — replaced the yfinance calls with SQLAlchemy queries. FastMCP makes the whole thing trivial. One note: if you are exposing sensitive data, definitely add input validation on the tool parameters to prevent SQL injection through the LLM.
Super clean tutorial. Question — how would you handle authentication if you wanted to deploy this as a shared MCP server for a team? I am thinking of wrapping it behind a reverse proxy with API keys, but curious if there is a more native MCP way to handle auth.