Chapter 23
Welcome to Week 6 Day 3: Context Engineering with MCP
Welcome to Week 6 Day 3: Context Engineering with MCP
People used to talk about Prompt Engineering, but now it's given way to a new Skill "Context Engineering".
Philipp Schmid of Google DeepMind wrote the seminal post about Context Engineering:
https://www.philschmid.de/context-engineering
For this week, we will put Context Engineering into practice, heavily using MCP servers.
- Long-term memory: a knowledge graph the agent writes to and reads back
- Web search: fresh information from the live web
- Agentic RAG: a vector store the agent fills from its own research, then searches
- Integrations: connecting to a live external service, with a local fallback
from dotenv import load_dotenv
from agents import Agent, Runner, trace
from agents.mcp import MCPServerStdio, create_static_tool_filter
import os
from pathlib import Path
from datetime import datetime
from IPython.display import Markdown, display
load_dotenv(override=True)A quick note for Windows
Launching a local MCP server from a notebook hits one rough edge on Windows. The server writes its startup output to stderr, but inside a Windows Jupyter kernel that stream has no real file handle behind it, so the launch fails with an io.UnsupportedOperation: fileno error, while Mac and Linux are unaffected.
The fix is to send the server's stderr to the null device, so it always has somewhere real to write. We do that once in the next cell, which lets every cell below use MCPServerStdio exactly as the OpenAI Agents SDK documents it. On Mac and Linux it costs nothing beyond keeping the server's startup banner out of the notebook.
# On Windows, a stdio MCP server started from a Jupyter kernel writes to a stderr stream with no
# real file descriptor and crashes with io.UnsupportedOperation: fileno. We send the server's
# stderr to the null device so it always has somewhere real to write, which lets every cell below
# use MCPServerStdio exactly as the OpenAI Agents SDK documents it. Mac and Linux are unaffected.
import functools
import subprocess
import agents.mcp.server
agents.mcp.server.stdio_client = functools.partial(agents.mcp.server.stdio_client, errlog=subprocess.DEVNULL)Part 1: Long-term memory
Our first context source is memory. This is the official knowledge graph server: it stores entities, observations about them, and the relationships between them, and keeps them on disk between runs. We point it at memory/memory.json, so you can open that file and read the graph it builds.
That gives an agent something it normally lacks: a memory that outlives the conversation. The agent writes facts as it learns them and reads them back later.
https://github.com/modelcontextprotocol/servers/tree/main/src/memory
memory_path = os.path.abspath("memory/memory.json")
memory_params = {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-memory"], "env": {"MEMORY_FILE_PATH": memory_path}}
async with MCPServerStdio(params=memory_params, client_session_timeout_seconds=60) as server:
memory_tools = await server.list_tools()
memory_toolsinstructions = "You use your entity tools as a persistent memory to store and recall information about your conversations."
request = "My name's Ed. I'm an LLM engineer. I'm teaching a course about AI Agents, including the incredible MCP protocol. MCP is a protocol for connecting agents with tools, resources and prompt templates, and makes it easy to integrate AI agents with capabilities."
model = "gpt-5.4-mini"async with MCPServerStdio(params=memory_params, client_session_timeout_seconds=30) as mcp_server:
agent = Agent(name="agent", instructions=instructions, model=model, mcp_servers=[mcp_server])
with trace("conversation"):
result = await Runner.run(agent, request)
display(Markdown(result.final_output))async with MCPServerStdio(params=memory_params, client_session_timeout_seconds=30) as mcp_server:
agent = Agent(name="agent", instructions=instructions, model=model, mcp_servers=[mcp_server])
with trace("conversation"):
result = await Runner.run(agent, "My name's Ed. What do you know about me?")
display(Markdown(result.final_output))Check out the trace
Part 2: Web search
A model only knows what it was trained on. Web search is the context source that keeps it current.
We use Tavily, a search API built for agents: it returns clean, ranked results an LLM can use directly. Tavily maintains its own MCP server, so connecting is a one-liner.
This needs a free API key:
- Sign up at https://www.tavily.com
- The free tier gives you 1,000 searches a month, with no credit card
- Copy your API key (it starts with
tvly-) and add it to your.envfile:
TAVILY_API_KEY=tvly-xxxx
tavily_params = {"command": "npx", "args": ["-y", "tavily-mcp@latest"], "env": {"TAVILY_API_KEY": os.getenv("TAVILY_API_KEY")}}
async with MCPServerStdio(params=tavily_params, client_session_timeout_seconds=60) as server:
tavily_tools = await server.list_tools()
tavily_toolsTavily's server offers several tools (search, extract, crawl, map, research). For this lab we only want plain web search, so we restrict the server to tavily_search. The OpenAI Agents SDK lets you hand an agent just the tools you choose with a static tool filter. Curating an agent's tools like this is itself context engineering.
instructions = "You search the web for information and briefly summarize the takeaways."
request = f"Please research the latest news on Amazon stock price and briefly summarize its outlook. For context, the current date is {datetime.now().strftime('%Y-%m-%d')}"
model = "gpt-5.4-mini"
search_only = create_static_tool_filter(allowed_tool_names=["tavily_search"])async with MCPServerStdio(params=tavily_params, client_session_timeout_seconds=60, tool_filter=search_only) as mcp_server:
agent = Agent(name="agent", instructions=instructions, model=model, mcp_servers=[mcp_server])
with trace("conversation"):
result = await Runner.run(agent, request)
display(Markdown(result.final_output))Part 3: Agentic RAG
RAG, retrieval augmented generation, means giving a model relevant documents to ground its answer. The usual setup loads documents into a vector store up front. Agentic RAG turns that around: the agent builds the knowledge base itself, deciding what is worth keeping and storing it as it works.
We use the official Qdrant MCP server. Qdrant is a vector database, and the server exposes two tools: one stores a piece of text, the other finds the most relevant stored text for a query. It runs fully locally here. QDRANT_LOCAL_PATH keeps everything on disk with no separate database to run, and it embeds text with a local model, so there is no extra API key. The first store or search downloads that small embedding model, so it pauses once on first use.
https://github.com/qdrant/mcp-server-qdrant
We hand one agent both Tavily and Qdrant: it researches a topic on the web, stores what it learns, then answers from its own knowledge base.
vectordb_path = Path("memory/qdrant")
vectorstore_params = {
"command": "uvx",
"args": ["mcp-server-qdrant"],
"env": {
"QDRANT_LOCAL_PATH": str(vectordb_path),
"COLLECTION_NAME": "knowledge",
},
}
async with MCPServerStdio(params=vectorstore_params, client_session_timeout_seconds=120) as server:
vectorstore_tools = await server.list_tools()
vectorstore_toolsINSTRUCTIONS = """You research topics on the web and build up a knowledge base for later.
When you learn something worth keeping, store it in your knowledge base.
When you are asked what you know, search your knowledge base and answer from it."""
model = "gpt-5.4-mini"
async with MCPServerStdio(params=tavily_params, client_session_timeout_seconds=60, tool_filter=search_only) as search_server:
async with MCPServerStdio(params=vectorstore_params, client_session_timeout_seconds=120) as vector_server:
agent = Agent(name="researcher", instructions=INSTRUCTIONS, model=model, mcp_servers=[search_server, vector_server])
with trace("research and store"):
result = await Runner.run(agent, "Research the latest news on Nvidia and store the key facts in your knowledge base.", max_turns=20)
display(Markdown(result.final_output))The agent below has only the knowledge base, no web search. Whatever it tells us about Nvidia, it is recalling from what the first agent stored. That is the retrieval half of RAG.
async with MCPServerStdio(params=vectorstore_params, client_session_timeout_seconds=120) as vector_server:
agent = Agent(name="researcher", instructions=INSTRUCTIONS, model=model, mcp_servers=[vector_server])
with trace("retrieve"):
result = await Runner.run(agent, "Based on your knowledge base, what's the latest on Nvidia?")
display(Markdown(result.final_output))Check out the trace
Part 4: Integrations
The last context source is a live external service. Here that is market data from Massive (formerly Polygon.io), a popular financial data provider that publishes its own MCP server.
Setting this up is optional. Massive offers a free API key with no credit card, giving you their real end of day market data:
- Sign up at https://www.massive.com
- Create an API key
- Add it to your
.envfile:
MASSIVE_API_KEY=xxxx
If you would rather not sign up, the next cell falls back to a local market server, the same kind of MCP server we built on Day 2, which serves simulated prices so the rest of the lab still works.
massive_api_key = os.getenv("MASSIVE_API_KEY")
if massive_api_key:
market_params = {
"command": "uvx",
"args": ["--from", "git+https://github.com/massive-com/mcp_massive@v0.10.0", "mcp_massive"],
"env": {"MASSIVE_API_KEY": massive_api_key},
}
else:
market_params = {"command": "uv", "args": ["run", "-m", "backend.market_server"]}
async with MCPServerStdio(params=market_params, client_session_timeout_seconds=120) as server:
market_tools = await server.list_tools()
market_toolsinstructions = "You answer questions about the stock market."
request = "What was the most recent price that Apple (AAPL) traded at?"
model = "gpt-5.4-mini"
async with MCPServerStdio(params=market_params, client_session_timeout_seconds=120) as mcp_server:
agent = Agent(name="agent", instructions=instructions, model=model, mcp_servers=[mcp_server])
with trace("conversation"):
result = await Runner.run(agent, request)
display(Markdown(result.final_output))That's it for today
Four MCP servers, four kinds of context: long-term memory, web search, a knowledge base the agent builds itself, and a live integration. Choosing which sources an agent needs, and wiring them in, is much of what context engineering is in practice.
