# Model Context Protocol

## My first MCP Server (Stock Market)

This is my first **MCP (Model Context Protocol) server** project. The goal is to build a small and useful service around **stock market data** using **Python** and **yfinance**, following the official MCP guide: [MCP with Python](https://modelcontextprotocol.io/docs/develop/build-server)

### What it can do (tools)

Planned / supported commands:

* **Get stock data** for a ticker: `STOCK_TICKER`
* **Get stock data** for multiple tickers: `[list of tickers]`
* **Search** by **sector** (returns top companies for that sector)
* **Get latest news** for a ticker

### Specific goals

* [x] Implement the features above using **Python** + **yfinance**
* [x] Build the **MCP Server**
* [ ] **Golden:** Run it inside a **Docker container** (so the service starts/stops only when requested)

***

### Getting started

#### Logging (important for debugging)

Note

When working with MCP over `stdio`, prefer `stderr` for debug output, or use `logging`.

```python
import sys
import logging

# ❌ Bad (STDIO)
print("Processing request")

# ✅ Good (STDIO)
print("Processing request", file=sys.stderr)

# ✅ Good (STDIO)
logging.info("Processing request")
```

#### Environment setup (uv)

You need **uv** to manage the virtual environment and dependencies:

```c
uv venv
source .venv/bin/activate

uv add --dev "mcp[cli]" httpx yfinance
touch yFinance.py
```

Then configure VS Code to use the `.venv` interpreter.

***

### Notes about yfinance

I’m using the Yahoo Finance wrapper library **yfinance**. Documentation:\
[yfinance reference](https://ranaroussi.github.io/yfinance/reference/yfinance.search.html)

For now, I’m focusing on:

* ticker data (single and multiple)
* sector search (top companies)
* news section

***

### Current implementation

#### Sector search change

I modified the search tool so it works by **sector key** and returns the **top companies** of that sector.

#### News section

I added a tool that fetches the **latest news** for a given ticker.

***

### Code

The main server script currently looks like this:

```python
import sys
from typing import Any

import httpx
import yfinance as yf
from mcp.server.fastmcp import FastMCP

# Initialize FastMCP server
mcp = FastMCP("yFinance")

# Constants
NWS_API_BASE = "https://api.yfinance.gov"
USER_AGENT = "yFinance-app/1.0"

# Allowed sector keys for yfinance Sector API
SECTOR_KEYS: list[str] = [
    # basic-materials
    "basic-materials",
    "agricultural-inputs",
    "aluminum",
    "building-materials",
    "chemicals",
    "coking-coal",
    "copper",
    "gold",
    "lumber-wood-production",
    "other-industrial-metals-mining",
    "other-precious-metals-mining",
    "paper-paper-products",
    "silver",
    "specialty-chemicals",
    "steel",
    # communication-services
    "communication-services",
    "advertising-agencies",
    "broadcasting",
    "electronic-gaming-multimedia",
    "entertainment",
    "internet-content-information",
    "publishing",
    "telecom-services",
    # energy
    "energy",
    "oil-gas-drilling",
    "oil-gas-e&p",
    "oil-gas-equipment-services",
    "oil-gas-integrated",
    "oil-gas-midstream",
    "oil-gas-refining-marketing",
    "thermal-coal",
    "uranium",
    # healthcare
    "healthcare",
    "biotechnology",
    "diagnostics-research",
    "drug-manufacturers—general",
    "drug-manufacturers—specialty-generic",
    "health-information-services",
    "healthcare-plans",
    "medical-care-facilities",
    "medical-devices",
    "medical-distribution",
    "medical-instruments-supplies",
    "pharmaceutical-retailers",
    # industrials
    "industrials",
    "aerospace-defense",
    "airlines",
    "airports-air-services",
    "building-products-equipment",
    "business-equipment-supplies",
    "conglomerates",
    "consulting-services",
    # technology
    "technology",
    "communication-equipment",
    "computer-hardware",
    "consumer-electronics",
    "electronic-components",
    "electronics-computer-distribution",
    "information-technology-services",
    "scientific-technical-instruments",
    "semiconductor-equipment-materials",
    "semiconductors",
    "software—application",
    "software—infrastructure",
    "solar",
]

"""
Otros posibles sectores que se podrían incorporar son:

# consumer-cyclical
...
"""

# Helper functions for the yfinance API
async def fetch_stock_data(ticker: str) -> Any:
    try:
        print(f"Fetching stock data for ticker: {ticker}", file=sys.stderr)

        dat = yf.Ticker(ticker)
        dat.history(period="1mo")
        dat.option_chain(dat.options[0]).calls

        return {
            "info": dat.info,
            "calendar": dat.calendar,
            "analyst_price_targets": (
                dat.analyst_price_targets.to_dict()
                if dat.analyst_price_targets is not None
                else None
            ),
            "quarterly_income_stmt": (
                dat.quarterly_income_stmt.to_dict()
                if dat.quarterly_income_stmt is not None
                else None
            ),
        }
    except Exception as e:
        print(
            f"Error fetching stock data for ticker {ticker}: {str(e)}",
            file=sys.stderr,
        )
        raise


async def fetch_stocks_data(tickers: list[str]) -> Any:
    json_data: list[Any] = []

    try:
        for ticker in tickers:
            json_data.append(await fetch_stock_data(ticker))
        return json_data
    except Exception as e:
        print(
            f"Error fetching stock data for tickers {tickers}: {str(e)}",
            file=sys.stderr,
        )
        raise


async def search_sector(sector_key: str) -> Any:
    try:
        print(f"Searching for companies in sector: {sector_key}", file=sys.stderr)

        df = yf.Sector(sector_key).top_companies
        if df is None:
            return []

        return df.reset_index(drop=True).to_dict(orient="records")
    except Exception as e:
        print(f"Error searching for sector {sector_key}: {str(e)}", file=sys.stderr)
        raise


async def latest_news(ticker: str) -> Any:
    ticker_search = yf.Search(ticker, news_count=10)
    news = ticker_search.news
    nav = ticker_search.nav

    return {"news": news, "navigation_links": nav}


# Register MCP functions
@mcp.tool()
async def get_stock_data(ticker: str) -> Any:
    """Fetch stock data for a given ticker symbol."""
    return await fetch_stock_data(ticker)


@mcp.tool()
async def get_stocks_data(tickers: list[str]) -> Any:
    """Fetch stock data for multiple ticker symbols."""
    return await fetch_stocks_data(tickers)


@mcp.tool(
    description=(
        """Search for companies in a given sector. The `sector_key` must be one of the allowed values understood by yfinance's Sector API. Valid values are: basic-materials, agricultural-inputs, aluminum, building-materials, chemicals, coking-coal, copper, gold, lumber-wood-production, other-industrial-metals-mining, other-precious-metals-mining, other-precious-metals-mining, paper-paper-products, silver, specialty-chemicals, steel, communication-services, advertising-agencies, broadcasting, electronic-gaming-multimedia, entertainment, internet-content-information, publishing, telecom-services, energy, oil-gas-drilling, oil-gas-e&p, oil-gas-equipment-services, oil-gas-integrated, oil-gas-midstream, oil-gas-refining-marketing, thermal-coal, uranium, healthcare, biotechnology, diagnostics-research, drug-manufact"""
    ),
    meta={"version": "1.0", "author": "Mk"},
)
async def search_sector_tool(sector_key: str) -> Any:
    if sector_key not in SECTOR_KEYS:
        raise ValueError(
            f"Invalid sector_key '{sector_key}'. Must be one of: {', '.join(SECTOR_KEYS)}"
        )

    return await search_sector(sector_key)


@mcp.tool()
async def get_latest_news(ticker: str) -> Any:
    """Fetch the latest news for a given ticker symbol."""
    return await latest_news(ticker)


# Start the MCP server
def main() -> None:
    print("Starting yFinance MCP server...", file=sys.stderr)

    try:
        mcp.run(transport="stdio")
    except Exception as e:
        print(f"Error starting yFinance MCP server: {str(e)}", file=sys.stderr)
        raise


if __name__ == "__main__":
    main()
```

***

### Testing

Testing failed a bit because `mcp.tool()` has some specific parameters that I can use to improve the tool definitions and overall quality.

I updated the script, added the configuration in Claude, and now I’m debugging until everything works properly.

**Resultado:**<br>

<figure><img src="https://3435654066-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6E0ZmuaMCe0dunM4aXl8%2Fuploads%2FIyvLh0UaYWoOXnIsQbiy%2FScreenshot%202026-03-16%20at%2014.09.13.png?alt=media&#x26;token=2bf7b7d0-18b1-4625-a4e2-c3c6fb6ba425" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3435654066-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F6E0ZmuaMCe0dunM4aXl8%2Fuploads%2FrZHkXJ8TFaoEqqF3cmIO%2FScreenshot%202026-03-16%20at%2014.07.55.png?alt=media&#x26;token=d7bd7246-39c9-46a2-b067-31930c348f15" alt=""><figcaption></figcaption></figure>
