Build a Local Stock Market Assistant
Build a private AI stock assistant using MCP tools and a local LLM.
Introduction
30 Second Summary
Every time you ask Siri about a stock price, your question travels to Apple's servers, gets processed, and comes back. What if your AI assistant could do the same thing without your data ever leaving your computer?
In this project, you will build a fully private AI stock market assistant that runs entirely on your Mac. You will create custom tools that fetch live stock data, connect them to a local AI model using the Model Context Protocol (MCP), and chat with your assistant right from the terminal.
What You'll Build
A terminal-based AI assistant powered by Ollama and MCP that fetches real-time stock prices on command, compares stocks side by side, and responds in natural language.
By the end of this project, you'll have:
- An MCP server with two custom stock market tools (get_stock_price and compare_stocks) that fetch live data from a free API.
- A Python MCP client that connects a local LLM running on your Mac to your MCP tools.
- A working agent loop where the AI decides which tools to call, fetches real data, and reasons about the results.
- A secret mission where you add a web search tool, proving that extending your AI is just one more function away.
Are there any prerequisites?
You should have basic familiarity with Python syntax and using the terminal. No prior experience with AI models, MCP, or APIs is needed.
Before We Start
Before We Start...
Set Up Your Local AI Environment
Before your AI assistant can fetch stock prices, it needs a brain and a toolkit. The brain is Ollama, which lets you run large language models entirely on your Mac. The toolkit is a set of Python packages that will power your MCP server and connect it to your local LLM.
In this step, you'll install everything your project needs and verify it all works before writing any code.
In This Step, Get Ready To:
- Install Ollama and pull the Qwen 2.5 language model to your Mac.
- Create a Python virtual environment and install your project dependencies.
- Verify Ollama is running and the model responds to a test prompt.
Install Ollama and Pull the Qwen 2.5 Model
Ollama is a tool that lets you download and run large language models locally. Instead of sending your data to a cloud API, everything stays on your machine. You'll install it through Homebrew and then download Qwen 2.5, a 7B parameter model with strong tool-calling support.
Why Qwen 2.5 Instead of Llama or Mistral?
This project relies on the AI deciding when to call your custom tools. Qwen 2.5 has verified tool-calling support from the Ollama team and consistently ranks well for function calling on benchmarks. Other models like Llama 3.1 can also work, but Qwen 2.5 gives the most reliable results for this use case.
- Open your terminal.
- Install Ollama using Homebrew.
brew install ollama
Use Homebrew Only
Ollama also offers a standalone macOS app installer from ollama.com. Do not install both. If you install the Homebrew version and the standalone app, you can run into PATH conflicts. Stick with Homebrew for this project.
Now start Ollama as a background service so it's always ready to respond to requests.
- Start the Ollama service.
brew services start ollama
What does this command do?
This tells Homebrew to run Ollama as a background service. It starts automatically and keeps running so your Python code can send it requests at any time. You can also run ollama serve in a separate terminal window if you prefer to see its logs in real time.
With Ollama running, you can now download the Qwen 2.5 model. This is a 4.7GB download, so it may take a few minutes depending on your internet connection.
- Pull the Qwen 2.5 model.
ollama pull qwen2.5
What does this command do?
This downloads the Qwen 2.5 7B model in Q4_K_M quantized format to your local machine. Quantization compresses the model so it runs efficiently on consumer hardware. Once downloaded, the model stays on your machine and you won't need to download it again.
I see a success message
You should see a progress bar followed by a success message confirming the model has been pulled. You're ready to move on.
I see an error
If you see an error like Error: could not connect to ollama app, the Ollama service isn't running yet.
- Run brew services start ollama again and wait a few seconds.
- Try the pull command again.
If you're still stuck, help me troubleshoot my Ollama installation.
Create Your Project and Install Dependencies
Your project needs three Python packages. The mcp package provides the MCP Python SDK for building your server and client. The ollama package lets your Python code talk to the local Ollama service. The httpx package handles HTTP requests to the stock price API.
- Create a project folder and navigate into it.
mkdir stock-assistant && cd stock-assistant
- Create a Python virtual environment.
python -m venv .venv
- Activate the virtual environment.
macOS
source .venv/bin/activate
Windows
.venv\Scripts\activate
You should see (.venv) appear at the beginning of your terminal prompt. This confirms you're working inside the virtual environment.
- Install the project dependencies.
pip install mcp==1.27.0 ollama httpx
What does this command do?
This installs three packages into your virtual environment:
- mcp==1.27.0 is the MCP Python SDK by Anthropic. It includes FastMCP, the high-level API you'll use to build your MCP server.
- ollama is the Python client for communicating with your local Ollama service.
- httpx is a modern Python HTTP client you'll use to call the stock price API from your MCP tools.
Seeing Installation Errors?
The MCP SDK requires Python 3.10 or higher. Check your version by running python --version. If you're on an older version, use pyenv to install a newer one. Help me upgrade my Python version with pyenv.
Verify Ollama Is Running and the Model Responds
Before you start building, confirm that Ollama can actually run the model and respond to prompts. A quick test now saves debugging time later.
- Send a test prompt to Qwen 2.5.
ollama run qwen2.5 "Say hello in one sentence"
I see a response
The model should respond with a short greeting. The exact wording will vary each time. What matters is that you got a response at all. This confirms Ollama is running and the Qwen 2.5 model is loaded correctly.
I see an error
If you see Error: model 'qwen2.5' not found, the model didn't finish downloading.
- Run ollama pull qwen2.5 again and wait for it to complete.
- Try the test prompt again.
If you're getting a different error, help me troubleshoot my Ollama model.
Now open your project folder in VS Code so you're ready to start writing code.
- Open the project in VS Code from your terminal.
code .
VS Code opens with your stock-assistant folder. You should see the .venv folder in the file explorer on the left.
Your local AI environment is fully set up. Ollama is running, Qwen 2.5 is downloaded, and your Python project has all the packages it needs. Next up, you'll build the MCP server that gives your AI the power to fetch live stock prices.
Build Your MCP Server
Your environment is ready with Ollama running and all dependencies installed. Now it's time to build the thing your AI will actually talk to.
An MCP server is a program that exposes tools for an LLM to call. Your server will have two tools: one to fetch a single stock's price and one to compare two stocks side by side. Each tool is just a Python function decorated with @mcp.tool(). The MCP SDK handles all the communication plumbing so you can focus on what each tool does.
In This Step, Get Ready To:
- Create the project folder structure and MCP server file.
- Write a tool that fetches live stock data from the StockPrices.dev API.
- Write a tool that compares two stocks side by side.
Create the Project Folder and Server File
Your MCP server will live in its own Python file. Keeping the server separate from the client makes the architecture clean. The client will start this server as a subprocess later.
- Open your terminal and navigate to your project folder.
cd ~/stock-assistant
- Create a new file called server.py.
touch server.py
- Open server.py in VS Code.
code server.py
- Add the imports and server initialization at the top of the file.
import httpx
from mcp.server.fastmcp import FastMCP
# Create the MCP server instance
mcp = FastMCP("stock-assistant")
What does this code do?
You import httpx to make HTTP requests to the stock price API. FastMCP is the high-level class from the MCP Python SDK that handles all the protocol details for you. The string "stock-assistant" is just a name to identify your server.
Write the Get Stock Price Tool
An MCP tool is a Python function that your LLM can decide to call. When you decorate a function with @mcp.tool(), the SDK automatically generates a JSON schema from the function's type hints and docstring. The LLM reads this schema to understand what the tool does and what arguments it needs.
Your first tool will call the StockPrices.dev API to fetch live price data for any stock ticker.
- Add the following function below your server initialization in server.py.
@mcp.tool()
async def get_stock_price(ticker: str) -> str:
"""Get the current stock price for a given ticker symbol.
Args:
ticker: The stock ticker symbol (e.g., AAPL, MSFT, TSLA)
"""
url = f"https://stockprices.dev/api/stocks/{ticker.upper()}"
async with httpx.AsyncClient() as client:
response = await client.get(url)
if response.status_code != 200:
return f"Error: Could not fetch data for ticker '{ticker}'. Please check the ticker symbol and try again."
data = response.json()
name = data.get("Name", "Unknown")
price = data.get("Price", "N/A")
change = data.get("ChangeAmount", "N/A")
change_pct = data.get("ChangePercentage", "N/A")
return (
f"Stock: {name} ({ticker.upper()})\n"
f"Price: ${price}\n"
f"Change: {change} ({change_pct})"
)
What does this tool do?
The @mcp.tool() decorator registers this function as a tool that any MCP client can discover and call. FastMCP reads the type hint ticker: str and the docstring to generate a schema the LLM can understand.
The function calls the StockPrices.dev API at /api/stocks/:ticker. The API returns the stock's name, current price, and change amount. The function formats this into a readable string that gets sent back to the LLM.
Why StockPrices.dev?
StockPrices.dev is completely free, requires no API key, and has no rate limits. This means nothing leaves your machine except a simple HTTP request for stock data. No accounts, no tokens, no billing surprises.
StockPrices.dev May Have Intermittent Availability
This free API may occasionally be slow or unavailable. The error handling in your tool covers this. If you get errors when testing later, wait a minute and try again.
Write the Compare Stocks Tool
A single stock lookup is useful, but comparing two stocks is where your assistant starts to feel genuinely helpful. This second tool fetches data for two tickers and formats them for easy comparison.
- Add the compare_stocks function below your get_stock_price function in server.py.
@mcp.tool()
async def compare_stocks(ticker1: str, ticker2: str) -> str:
"""Compare the current stock prices of two companies.
Args:
ticker1: The first stock ticker symbol (e.g., AAPL)
ticker2: The second stock ticker symbol (e.g., MSFT)
"""
async with httpx.AsyncClient() as client:
# Fetch both stocks
response1 = await client.get(f"https://stockprices.dev/api/stocks/{ticker1.upper()}")
response2 = await client.get(f"https://stockprices.dev/api/stocks/{ticker2.upper()}")
if response1.status_code != 200:
return f"Error: Could not fetch data for '{ticker1}'."
if response2.status_code != 200:
return f"Error: Could not fetch data for '{ticker2}'."
data1 = response1.json()
data2 = response2.json()
return (
f"--- {data1.get('Name', ticker1.upper())} ({ticker1.upper()}) ---\n"
f"Price: ${data1.get('Price', 'N/A')}\n"
f"Change: {data1.get('ChangeAmount', 'N/A')} ({data1.get('ChangePercentage', 'N/A')})\n"
f"\n"
f"--- {data2.get('Name', ticker2.upper())} ({ticker2.upper()}) ---\n"
f"Price: ${data2.get('Price', 'N/A')}\n"
f"Change: {data2.get('ChangeAmount', 'N/A')} ({data2.get('ChangePercentage', 'N/A')})"
)
What does this tool do?
This tool takes two ticker symbols and fetches data for both. It formats the results side by side so the LLM can easily compare them and give you a meaningful analysis. The LLM will choose this tool over get_stock_price when you ask a comparison question like "Compare Tesla and Microsoft."
Now add the final piece that makes your server runnable. The mcp.run() call starts the server and listens for incoming connections over stdio transport.
- Add the following at the very bottom of server.py.
if __name__ == "__main__":
mcp.run(transport="stdio")
What is stdio transport?
Stdio transport means the MCP server communicates through standard input and output streams. The MCP client will start your server as a subprocess and send messages to it through stdin. The server sends responses back through stdout.
This is the standard approach for local MCP servers because everything stays on your machine.
Your complete server.py should now have imports at the top, two tool functions in the middle, and the run call at the bottom.
- Save the file.
Want to double-check your code? Expand the tab below to see the full file.
I'll try it first
Nice. Move on to the validation below and you'll test the full server in the next step.
Let me check my work
Here is the complete server.py file.
import httpx
from mcp.server.fastmcp import FastMCP
# Create the MCP server instance
mcp = FastMCP("stock-assistant")
@mcp.tool()
async def get_stock_price(ticker: str) -> str:
"""Get the current stock price for a given ticker symbol.
Args:
ticker: The stock ticker symbol (e.g., AAPL, MSFT, TSLA)
"""
url = f"https://stockprices.dev/api/stocks/{ticker.upper()}"
async with httpx.AsyncClient() as client:
response = await client.get(url)
if response.status_code != 200:
return f"Error: Could not fetch data for ticker '{ticker}'. Please check the ticker symbol and try again."
data = response.json()
# Extract the fields from the API response
name = data.get("Name", "Unknown")
price = data.get("Price", "N/A")
change = data.get("ChangeAmount", "N/A")
change_pct = data.get("ChangePercentage", "N/A")
# Return a formatted string for the LLM to read
return (
f"Stock: {name} ({ticker.upper()})\n"
f"Price: ${price}\n"
f"Change: {change} ({change_pct})"
)
@mcp.tool()
async def compare_stocks(ticker1: str, ticker2: str) -> str:
"""Compare the current stock prices of two companies.
Args:
ticker1: The first stock ticker symbol (e.g., AAPL)
ticker2: The second stock ticker symbol (e.g., MSFT)
"""
async with httpx.AsyncClient() as client:
# Fetch both stocks in sequence
response1 = await client.get(f"https://stockprices.dev/api/stocks/{ticker1.upper()}")
response2 = await client.get(f"https://stockprices.dev/api/stocks/{ticker2.upper()}")
# Check for errors on each ticker
if response1.status_code != 200:
return f"Error: Could not fetch data for '{ticker1}'."
if response2.status_code != 200:
return f"Error: Could not fetch data for '{ticker2}'."
data1 = response1.json()
data2 = response2.json()
# Format both stocks side by side for easy comparison
return (
f"--- {data1.get('Name', ticker1.upper())} ({ticker1.upper()}) ---\n"
f"Price: ${data1.get('Price', 'N/A')}\n"
f"Change: {data1.get('ChangeAmount', 'N/A')} ({data1.get('ChangePercentage', 'N/A')})\n"
f"\n"
f"--- {data2.get('Name', ticker2.upper())} ({ticker2.upper()}) ---\n"
f"Price: ${data2.get('Price', 'N/A')}\n"
f"Change: {data2.get('ChangeAmount', 'N/A')} ({data2.get('ChangePercentage', 'N/A')})"
)
# Start the server with stdio transport
if __name__ == "__main__":
mcp.run(transport="stdio")
Your MCP server is complete. It has two tools that can fetch live stock data, and it's ready to accept connections over stdio. Next up, you'll build the client that connects your Ollama LLM to this server so the AI can actually call these tools.
Build the MCP Client and Agent Loop
Your MCP server exposes stock market tools, but it can't do anything on its own. It needs a client to connect it to an LLM that can actually reason about when and how to use those tools. In this step, you'll build the MCP client that wires Ollama to your server and create the agent loop that makes your AI assistant actually work.
In This Step, Get Ready To:
- Write a Python MCP client that connects Ollama to your MCP server.
- Build an agent loop where the LLM decides which tools to call, gets live data, and formulates an answer.
- Run the full system and have your first conversation with your stock assistant.
Write the MCP Client
The MCP client is the bridge between your LLM and your MCP server. It starts the server as a subprocess, discovers what tools are available, and translates between the LLM's tool call requests and the server's tool execution. Without it, Ollama has no way to know your stock tools exist.
- Create a new file called client.py in your project folder.
- Add the following imports and setup code at the top of the file.
import asyncio
import json
import sys
from ollama import chat
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
What are these imports for?
The ollama package gives you the chat function to talk to your local LLM. The mcp imports handle connecting to your MCP server. ClientSession manages the connection, StdioServerParameters tells the client how to start your server, and stdio_client handles the actual communication over stdin/stdout.
Next, you need to tell the client where your MCP server lives and how to start it.
- Add the following code below your imports in client.py.
# Tell the MCP client to start your server as a subprocess
server_params = StdioServerParameters(
command=sys.executable, # Uses the same Python interpreter running this script
args=["server.py"],
)
What is stdio transport?
MCP servers can communicate over different transports. Stdio transport means the client starts the server as a subprocess and communicates through standard input and output. This is the simplest approach for local servers because everything stays on your machine with no networking involved.
Wire Up the Agent Loop
The agent loop is the core pattern that makes your assistant intelligent. Here's how it works: you send a question to the LLM, the LLM looks at the available tools and decides if it needs to call one, the client executes that tool through the MCP server, and the result goes back to the LLM so it can formulate a final answer. This loop can repeat multiple times if the LLM needs to call several tools.
Why is it called an agent loop?
A regular chatbot just generates text. An agent can take actions in the real world. The "loop" part means the agent keeps cycling through a think-act-observe pattern until it has enough information to answer your question. Your stock assistant might call get_stock_price once for a simple question, or call compare_stocks for a comparison. The LLM decides.
This is the biggest piece of code in the project. It connects to your MCP server, discovers the tools, runs a chat loop, and handles tool calls. Let's build it.
- Add the following run function below your server_params in client.py.
async def run():
# Start the MCP server and connect to it
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
# Initialize the connection and discover available tools
await session.initialize()
tools_result = await session.list_tools()
print(f"Connected to MCP server with tools: {[t.name for t in tools_result.tools]}")
# Convert MCP tool definitions into the format Ollama expects
ollama_tools = []
for tool in tools_result.tools:
ollama_tools.append({
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema,
},
})
# Start the chat loop
print("\nStock Market Assistant (type 'quit' to exit)")
print("=" * 45)
messages = []
while True:
user_input = input("\nYou: ").strip()
if user_input.lower() in ["quit", "exit", "q"]:
print("Goodbye!")
break
if not user_input:
continue
messages.append({"role": "user", "content": user_input})
# Send the message to Ollama with the available tools
response = chat(
model="qwen2.5",
messages=messages,
tools=ollama_tools,
)
# Agent loop: keep going while the LLM wants to call tools
while response.message.tool_calls:
for tool_call in response.message.tool_calls:
tool_name = tool_call.function.name
tool_args = tool_call.function.arguments
print(f"\n[Calling tool: {tool_name} with args: {tool_args}]")
# Execute the tool through the MCP server
result = await session.call_tool(tool_name, tool_args)
print(f"[Tool result received]")
# Add the tool call and result to the conversation
messages.append(response.message)
messages.append({
"role": "tool",
"content": result.content[0].text,
})
# Send the tool results back to the LLM for reasoning
response = chat(
model="qwen2.5",
messages=messages,
tools=ollama_tools,
)
# Print the final answer
print(f"\nAssistant: {response.message.content}")
messages.append(response.message)
# Run the async function
if __name__ == "__main__":
asyncio.run(run())
What does this code do?
This function does four things in sequence:
- Starts your MCP server as a subprocess and connects to it over stdio.
- Discovers the available tools (get_stock_price and compare_stocks) and converts them into the format Ollama expects.
- Runs an interactive chat loop where you type questions.
- Handles the agent loop: when Ollama wants to call a tool, the client executes it through the MCP server and feeds the result back to Ollama for reasoning.
How does Ollama know to call a tool?
When you pass the tools parameter to Ollama's chat() function, the LLM receives the tool names, descriptions, and parameter schemas alongside your message. Qwen 2.5 was specifically trained to understand tool definitions and generate structured tool calls when it determines a tool would help answer the question. If you ask "What's Apple's stock price?", the model recognizes that get_stock_price is the right tool and generates a call with ticker="AAPL".
Run Your Stock Assistant
Everything is in place. Your MCP server has the tools, and your client knows how to connect Ollama to them. Time to see it all work together.
- Make sure Ollama is running in the background. If you stopped it earlier, start it again.
brew services start ollama
- Run the client from your project folder.
python client.py
You should see a message confirming the connection to your MCP server and listing the available tools. Then the chat prompt appears.
What should I see?
Your terminal should display something like:
Connected to MCP server with tools: ['get_stock_price', 'compare_stocks']
Stock Market Assistant (type 'quit' to exit)
=============================================
You:
- Try asking your first question. Type the following and press Enter.
What is Apple's stock price?
Watch your terminal. You should see the assistant decide to call get_stock_price with the ticker AAPL, receive the live data, and then respond with a natural language answer including the current price.
- Try a comparison question next.
Compare Tesla and Microsoft stock
This time the assistant should call compare_stocks with both tickers and give you a side-by-side breakdown.
Getting an error or no tool calls?
If the assistant responds without calling any tools, it may be generating a generic answer instead. Try rephrasing your question to be more direct, like "Look up the current stock price of AAPL." If you see a connection error, make sure Ollama is running with brew services start ollama. If the tool call succeeds but returns an error about the API, the StockPrices.dev API may be temporarily unavailable. Try again in a few minutes.
Still stuck? Help me debug my MCP client.
- Type quit to exit the chat when you're done exploring.
Your AI stock assistant is alive. You've connected a local LLM to custom tools through MCP, and the agent loop handles everything automatically. Next up, you'll put your assistant through its paces with different types of questions and edge cases.
Test Your Assistant
You have a fully wired AI assistant that can fetch live stock data through MCP tools. Now it's time to put it through its paces. In this step, you'll test different types of questions to verify the AI correctly identifies which tool to call, handles multiple tools in sequence, and responds gracefully when things go wrong.
In This Step, Get Ready To:
- Ask single-stock questions and verify the AI calls the correct tool.
- Ask comparison questions and verify multi-tool usage.
- Test edge cases like invalid tickers and observe how the AI handles errors.
Test Single-Stock Questions
The simplest test is asking about one stock. This verifies that Ollama correctly identifies the get_stock_price tool, extracts the ticker symbol from your question, and formats the result into a natural language response.
- Make sure your assistant is running. If you stopped it, start it again from your project directory:
cd ~/stock-assistant
source .venv/bin/activate
python client.py
- Type the following question at the prompt:
What is Apple's current stock price?
Watch your terminal output carefully. You should see the agent loop in action: the AI receives your question, decides to call get_stock_price with the ticker AAPL, gets back live data, and then formulates a response telling you the price.
- Try a few more single-stock questions to confirm consistent behavior:
How is Tesla doing today?
What's the price of Microsoft stock?
Get me the current price of GOOGL.
What Should I See?
For each question, the terminal should show the AI calling get_stock_price with the correct ticker symbol. The response should include the stock's name, current price, and the change amount or percentage. The exact wording varies each time because the LLM generates natural language, but the data should be accurate and consistent with live market values.
Pro tip: watch the tool calls
Pay attention to how the AI extracts ticker symbols from natural language. When you say "Apple" it resolves to AAPL. When you say "Tesla" it resolves to TSLA. This mapping happens inside the LLM itself, not in your code.
Test Comparison Questions
Your compare_stocks tool fetches data for two stocks and returns them side by side. This is a more complex test because the AI needs to identify two ticker symbols from your question and pass both to a single tool call.
- Type a comparison question:
Compare Tesla and Microsoft stock prices.
The AI should call compare_stocks with both TSLA and MSFT as arguments. The response should present both prices and ideally highlight which stock is performing better today.
- Try another comparison to see how the AI handles different phrasing:
Which is doing better today, Apple or Google?
What If the AI Calls get_stock_price Twice Instead?
Sometimes the LLM decides to call get_stock_price twice instead of using compare_stocks once. This is normal behavior. The AI chooses its own strategy based on how it interprets your question. Both approaches give you accurate data. The key thing to observe is that the AI can use your tools in different combinations to answer the same question.
Test Edge Cases and Error Handling
A good test suite includes things that should fail. Edge cases reveal how robust your system is. Your MCP tools include error handling that returns error messages instead of crashing, so the AI should receive those messages and communicate them to you gracefully.
- Try asking about a ticker that doesn't exist:
What's the stock price of XYZFAKE?
The StockPrices.dev API returns an error for this invalid ticker. Your tool's error handling catches this and returns an error message string. The AI then reads that error message and tells you it couldn't find the stock, instead of crashing the entire program.
- Try asking a question that has nothing to do with stocks:
What's the weather like today?
The AI should recognize that none of your MCP tools can answer this question. It responds without calling any tools, using only its built-in knowledge. This demonstrates that tool calling is optional. The LLM only calls tools when it decides they're relevant.
- Try one more edge case with a comparison that includes an invalid ticker:
Compare Apple and FAKESYMBOL.
This tests how the system handles partial failures. The AI should still attempt the comparison, get valid data for AAPL, receive an error for the fake ticker, and explain both results to you.
API Returning Empty Results?
The StockPrices.dev API is free and has no rate limits, but it may occasionally have intermittent availability. If you're getting empty responses for valid tickers like AAPL or MSFT, wait a minute and try again. If the issue persists, test the API directly in your browser by visiting https://stockprices.dev/api/stocks/AAPL to see if it returns data.
If you're still stuck, help me troubleshoot why the StockPrices.dev API isn't returning data for valid tickers.
Why Does This Matter?
These tests prove something important about the MCP architecture. Your AI doesn't blindly call tools on every message. It reasons about which tools are appropriate, extracts the right parameters from natural language, handles errors gracefully, and can combine multiple tool calls to answer complex questions. This is the agent loop pattern in action.
Your assistant is tested and working. You've verified single-stock lookups, multi-stock comparisons, and error handling. Next up, you'll take on a secret mission to extend your assistant with an entirely new capability.
Secret mission
Add Web Search to Your Assistant
Your stock assistant can fetch prices, but what about the latest news? What if your AI could search the web on command? In this secret mission, you'll add a web search tool using DuckDuckGo so your assistant can answer questions no stock API can handle.
Before you go
Clean Up Your Resources
This project runs entirely locally on your Mac, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Ollama (installed via Homebrew, running as a background service).
- Qwen 2.5 model (4.7GB downloaded to your machine).
- Python virtual environment with project dependencies (mcp, ollama, httpx).
- Project folder with MCP server and client files.
- ddgs package (if you completed the Secret Mission).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with your stock assistant.
- Ollama will continue running as a background service.
- Your Qwen 2.5 model stays downloaded and ready to use.
- Your project files and virtual environment remain in place.
- You can restart your assistant anytime by activating the virtual environment and running your client script.
Pause - I'll come back to this later
Stop the Ollama background service to free up memory, but keep everything installed so you can pick up where you left off.
- Stop the Ollama background service by running brew services stop ollama in your terminal.
- Your model, project files, and virtual environment will remain on disk.
- To resume later, run brew services start ollama and activate your virtual environment.
Delete - I don't want to use this again
Remove all project resources and start fresh. This frees up approximately 5GB of disk space.
- Stop the Ollama service by running brew services stop ollama.
- Delete the Qwen 2.5 model by running ollama rm qwen2.5.
- Uninstall Ollama by running brew uninstall ollama.
- Delete your project folder and virtual environment by running rm -rf on your project directory.
- Optionally, clean up Ollama's data directory by running rm -rf ~/.ollama.
Mission accomplished
That's a Wrap!
Nice work! You just built a fully private AI stock market assistant from scratch. It runs entirely on your Mac, fetches live market data, and reasons about which tools to call. No cloud services, no API keys, no data leaving your machine.
Here's what you accomplished:
- Built an MCP server in Python with custom stock market tools that fetch live data from the StockPrices.dev API.
- Connected a local LLM (Qwen 2.5 via Ollama) to your MCP tools through a Python client and agent loop that lets the AI decide which tools to call.
- Learned the core MCP pattern: servers expose tools with @mcp.tool(), clients discover those tools, and LLMs call them on demand.
- Added a web search tool using the ddgs package, proving that extending your AI assistant is just one more tool function away.
Ready to quiz yourself?