GoodNews MCP Example¶
This notebook demonstrates how to connect an LLMAgent to a locally running
stdio MCP server using the stdio transport introduced in Chapter 5. The MCP
server used here is mcp-goodnews,
developed by the author, which fetches and surfaces positive news articles via
the NewsAPI.
Requirements¶
- A NewsAPI API key stored in the environment variable
NEWS_API_KEY - A Cohere API key stored in the environment variable
COHERE_API_KEY - The
mcp-goodnewsserver cloned and available locally
Setup Instructions¶
To ensure you have the required dependencies to run this notebook, you'll need to have our llm-agents-from-scratch framework installed on the running Jupyter kernel. To do this, you can launch this notebook with the following command while within the project's root directory:
# we need the notebook-utils and openai extras for this notebook
uv sync --extra notebook-utils --extra openai
# to launch the notebook
uv run --with jupyter jupyter lab
Alternatively, if you just want to use the published version of llm-agents-from-scratch without local development, you can install it from PyPI by uncommenting the cell below.
Setting up the LLM¶
I recommend running these notebooks on Ollama Cloud. It serves much larger models than most laptops can run. The default cloud model is picked by benchmarking candidates on these notebooks, and an automated run checks every notebook against it each week. The notebook status page shows the current model and results.
- Create an API key at https://ollama.com/settings/keys.
- Export it before launching Jupyter:
export OLLAMA_API_KEY=<your key>. - That's all. When a notebook builds its LLM, it detects the key and uses Ollama Cloud.
Prefer to run locally? Install Ollama from https://ollama.com/download
and pull the model with ollama pull qwen3:14b. If no key is set, the
notebook starts the Ollama service for you, so there is no need to run
ollama serve. Local models are smaller, so your outputs will differ from
the recorded ones.
Prefer OpenAI or Anthropic?
- Install the extra:
uv sync --extra openaioruv sync --extra anthropic. - Export
OPENAI_API_KEYorANTHROPIC_API_KEYbefore launching Jupyter. If you forget, you will be prompted for it, which keeps the key out of the saved notebook. - Set
LLM_PROVIDER=openai(oranthropic), or passprovider="openai"tomake_llm(). A key on its own never switches providers, so one exported for unrelated work cannot reroute you.
The switch applies wherever a notebook builds its LLM with make_llm().
A notebook that constructs OllamaLLM directly stays on Ollama. Output on
gpt-5 or Claude will differ from the recorded outputs, and prompt-sensitive
examples (the ch09 evaluator pattern, ch08 supervised trajectories) may
behave noticeably differently.
# Uncomment the line below to install `llm-agents-from-scratch` from PyPI
# !pip install 'llm-agents-from-scratch[notebook-utils,openai]'
import os
from llm_agents_from_scratch.notebook_utils import make_llm
import logging
from llm_agents_from_scratch.logger import enable_console_logging
enable_console_logging(logging.INFO)
Creating an MCPToolProvider for GoodNews MCP Server¶
Unlike the HTTP-based MCP servers, mcp-goodnews runs locally as a subprocess
and is connected to via the stdio transport. We connect to it using the
stdio_params parameter on MCPToolProvider.
Before running the cells below, you'll need to clone the mcp-goodnews repo
to your local machine:
mkdir -p ~/mcp-servers
cd ~/mcp-servers
git clone https://github.com/VectorInstitute/mcp-goodnews
import getpass
if not os.environ.get("COHERE_API_KEY"):
os.environ["COHERE_API_KEY"] = getpass.getpass("Cohere API Key: ")
if not os.environ.get("NEWS_API_KEY"):
os.environ["NEWS_API_KEY"] = getpass.getpass("News API Key: ")
[scripted human] > yes
[scripted human] > yes
from pathlib import Path
from mcp import StdioServerParameters
from llm_agents_from_scratch.tools.mcp import MCPToolProvider
server_path = Path.home() / "mcp-servers/mcp-goodnews/src/mcp_goodnews"
server_params = StdioServerParameters(
command="uv",
args=["run", "--with", "mcp", "mcp", "run", "server.py"],
cwd=server_path,
env={
"NEWS_API_KEY": os.environ["NEWS_API_KEY"],
"COHERE_API_KEY": os.environ["COHERE_API_KEY"],
},
)
goodnews_mcp_provider = MCPToolProvider(
name="goodnews_mcp",
stdio_params=server_params,
)
Create our LLM agent¶
We'll now use the LLMAgentBuilder class to create an LLM agent equipped with
the tools from the GoodNews MCP server. Recall from Chapter 5 that the builder
takes care of tool discovery for each of its attached MCP tool providers.
from llm_agents_from_scratch import LLMAgentBuilder
llm = make_llm()
agent = (
await LLMAgentBuilder()
.with_llm(llm)
.with_mcp_provider(goodnews_mcp_provider)
.build()
)
✓ Using Ollama Cloud (kimi-k2.7-code:cloud)
# Print all tools
for tool in agent.tools:
desc = (tool.description or "")[:100]
print(f"{tool.name}:\n\t{desc}...")
mcp__goodnews_mcp__fetch_list_of_goodnews: Fetch a list of headlines and return only top-ranked news based on positivity....
agent.tools_registry[
"mcp__goodnews_mcp__fetch_list_of_goodnews"
].parameters_json_schema
{'properties': {'category': {'default': 'all',
'enum': ['all', 'science', 'health', 'technology'],
'title': 'Category',
'type': 'string'}},
'title': 'fetch_list_of_goodnewsArguments',
'type': 'object'}
A simple task for our agent¶
Now that we have our agent, let's give it a simple task to perform. The GoodNews
MCP server exposes a single tool, fetch_list_of_goodnews, which accepts an
optional category parameter. The supported categories are all, science,
health, and technology. We'll ask our agent to fetch some good news in the
health category.
# task
from llm_agents_from_scratch.data_structures import Task
instruction = (
"Show me some good news articles in the science category. "
"Your final answer must contain ONLY the article titles and URLs from the "
"tool results. Do not summarize or describe what you did."
)
task = Task(
instruction=instruction,
)
handler = agent.run(task, max_steps=10)
INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: Show me some good news articles in the science category. Your final answer must contain ONLY the article titles and URLs from the too...[TRUNCATED] INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: Show me some good news articles in the science category. Your final answer must contain ONLY the article titles and URLs from the ...[TRUNCATED]
result = handler.exception() or handler.result()
print(result)
The tool returned an error and no articles are available, so I have no titles or URLs to list.