Chapter 5 — MCP Tools¶
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:
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
from llm_agents_from_scratch.notebook_utils import make_llm
The Hailstone MCP server¶
The examples in this notebook demonstrate how to use the framework's MCP integration using a toy MCP server that exposes the familiar Hailstone tool.
Important: Run this notebook from within the project's root directory.
The code for the MCP server is located at: https://github.com/nerdai/llm-agents-from-scratch/tree/main/extra/mcp-hailstone
Examples¶
Example 1: Creating an MCPToolProvider¶
from llm_agents_from_scratch.tools.mcp import MCPToolProvider
from mcp import StdioServerParameters
from pathlib import Path
server_path = Path.cwd().parents[1] / "extra/mcp-hailstone"
server_params = StdioServerParameters(
command="uv",
args=["run", "--with", "mcp", "mcp", "run", "main.py"],
cwd=server_path,
)
provider = MCPToolProvider(
name="hailstone",
stdio_params=server_params,
)
provider
<llm_agents_from_scratch.tools.mcp.provider.MCPToolProvider at 0x7e03d2e05400>
Example 2: Establishing a session (connection) to Hailstone MCP server¶
import time
provider = MCPToolProvider(
name="hailstone",
stdio_params=server_params,
)
start = time.perf_counter()
await provider.session()
print(f"First call (establish): {time.perf_counter() - start:.6f}s")
start = time.perf_counter()
await provider.session()
print(f"Second call (cached): {time.perf_counter() - start:.6f}s")
First call (establish): 0.477406s Second call (cached): 0.000049s
Example 3: Discovering tools and creating MCPTool representations¶
provider = MCPToolProvider(
name="hailstone",
stdio_params=server_params,
)
tools = await provider.get_tools()
print(f"Number of tools discovered: {len(tools)}")
Number of tools discovered: 1
Example 4: Tearing down the session¶
provider = MCPToolProvider(
name="hailstone",
stdio_params=server_params,
)
await provider.session()
print(f"Session is None: {provider._session is None}")
print(f"Session is ready: {provider._session_ready.is_set()}")
print("-------------")
await provider.close()
print(f"Session is None: {provider._session is None}")
print(f"Session is ready: {provider._session_ready.is_set()}")
Session is None: False Session is ready: True ------------- Session is None: True Session is ready: False
Example 5: Printing the MCP Hailstone tool's attributes¶
provider = MCPToolProvider(
name="hailstone",
stdio_params=server_params,
)
tools = await provider.get_tools()
print(f"Tool name: {tools[0].name}")
print(f"Tool description: {tools[0].description}")
print(f"Tool parameters: {tools[0].parameters_json_schema}")
print(f"Provider reference: {tools[0].provider}")
print(f"Additional annotations: {tools[0].additional_annotations}")
Tool name: mcp__hailstone__hailstone_step_fn
Tool description: Performs a single step of the Hailstone sequence.
Tool parameters: {'properties': {'x': {'title': 'X', 'type': 'integer'}}, 'required': ['x'], 'type': 'object', 'title': 'hailstone_step_fnArguments'}
Provider reference: <llm_agents_from_scratch.tools.mcp.provider.MCPToolProvider object at 0x7e039c777ce0>
Additional annotations: None
Example 6: MCP Hailstone tool execution¶
from llm_agents_from_scratch.data_structures import ToolCall
provider = MCPToolProvider(
name="hailstone",
stdio_params=server_params,
)
tools = await provider.get_tools()
tool_call = ToolCall(
tool_name=tools[0].name,
arguments={"x": 5},
)
result = await tools[0](tool_call)
result
ToolCallResult(tool_call_id='15f01950-e7f4-46a3-bbcf-59d030973263', content=[{'type': 'text', 'text': '16', 'annotations': None, 'meta': None}], error=False)
Example 7: Manual discovery of MCP tools to construct an LLM agent¶
from llm_agents_from_scratch import LLMAgent
provider = MCPToolProvider(
name="hailstone",
stdio_params=server_params,
)
tools = await provider.get_tools()
llm = make_llm()
llm_agent = LLMAgent(
llm=llm,
tools=tools,
)
✓ Using Ollama Cloud (deepseek-v4.1-flash)
llm_agent.tools
[<llm_agents_from_scratch.tools.mcp.tool.MCPTool at 0x7e039c7748a0>]
Example 8: Different ways to construct an LLMAgentBuilder with the same attributes¶
from llm_agents_from_scratch.agent.builder import LLMAgentBuilder
provider = MCPToolProvider(
name="hailstone",
stdio_params=server_params,
)
llm = make_llm()
# directly passing attributes at construction
builder = LLMAgentBuilder(
llm=llm,
mcp_providers=[provider],
)
# fluent style with builder methods
builder = LLMAgentBuilder()\
.with_llm(llm)\
.with_mcp_provider(provider)
# another fluent style
builder = LLMAgentBuilder()\
.with_mcp_providers([provider])\
.with_llm(llm)
✓ Using Ollama Cloud (deepseek-v4.1-flash)
Example 9: Using an LLMAgentBuilder to create the same LLMAgent from example 7¶
from llm_agents_from_scratch.agent.builder import LLMAgentBuilder
provider = MCPToolProvider(
name="hailstone",
stdio_params=server_params,
)
llm = make_llm()
llm_agent = await LLMAgentBuilder()\
.with_llm(llm)\
.with_mcp_provider(provider)\
.build()
✓ Using Ollama Cloud (deepseek-v4.1-flash)
Example 10: Performing a hailstone step with our LLM agent¶
import logging
from llm_agents_from_scratch.logger import enable_console_logging
enable_console_logging(logging.INFO)
from llm_agents_from_scratch.data_structures.agent import Task
instruction = (
"What is hailstone_step_fn(5)? "
"You must call the tool to get the answer."
)
task = Task(instruction=instruction)
result = await llm_agent.run(task)
INFO (llm_agents_fs.LLMAgent) : 🚀 Starting task: What is hailstone_step_fn(5)? You must call the tool to get the answer.
INFO (llm_agents_fs.TaskHandler) : ⚙️ Processing Step: What is hailstone_step_fn(5)? You must call the tool to get the answer.
INFO (llm_agents_fs.TaskHandler) : 🛠️ Executing Tool Call: mcp__hailstone__hailstone_step_fn
INFO (llm_agents_fs.TaskHandler) : ✅ Successful Tool Call: [{'type': 'text', 'text': '16', 'annotations': None, 'meta': None}]
INFO (llm_agents_fs.TaskHandler) : ✅ Step Result: The Hailstone step function for input 5 returns **16**.
This follows the Hailstone (Collatz) rule for odd numbers: since 5 is odd, the...[TRUNCATED]
INFO (llm_agents_fs.TaskHandler) : No new step required.
INFO (llm_agents_fs.LLMAgent) : 🏁 Task completed: The Hailstone step function for input 5 returns **16**.
This follows the Hailstone (Collatz) rule for odd numbers: since 5 is odd, ...[TRUNCATED]
result
TaskResult(task_id='c11435f5-712e-45cc-ab87-30d7ede63630', content='The Hailstone step function for input 5 returns **16**.\n\nThis follows the Hailstone (Collatz) rule for odd numbers: since 5 is odd, the next value is 3 × 5 + 1 = 16.')