Building Advanced Agents: Integrating MCP Servers
Pass 9: Get Tools from an External MCP Server Instead of Writing Them In-Process
We will implement the same weather agent as in Pass 8, but this time with the MCP client and LangChain MCP adapters.
Instead of local functions, we will use open-meteo-mcp.
Step 1: Start the MCP Server
Before running the code, make sure to run the MCP server locally (on the machine where you are running the code).
# Download and install nvm:
curl -o- \
https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | \
bash
# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"
# Download and install Node.js:
nvm install 24
# Install Open-Meteo MCP Server
npm install -g open-meteo-mcp-server@1.6.1
# Start the server
TRANSPORT=http PORT=3000 npx open-meteo-mcp-server
TRANSPORT=http makes it speak streamable-HTTP (which our client uses). PORT=3000 matches the default URL we'll configure below.
Step 2: Add the MCP Settings to config.py
Three new entries:
# config.py
MCP_SERVER_URL: str = os.environ.get(
"OPEN_METEO_MCP_URL", "http://localhost:3000/mcp"
)
ALLOWED_TOOLS: set[str] = {
name.strip()
for name in os.environ.get(
"ALLOWED_TOOLS",
"geocoding,weather_forecast,air_quality",
).split(",")
if name.strip()
}
TOOL_GUIDANCE: str = os.environ.get(
"TOOL_GUIDANCE",
"You have access to weather tools that require latitude and longitude. "
"When the user names a place, first call the `geocoding` tool with that "
"name, take the latitude/longitude from the first result, and then call "
"the relevant weather tool (e.g. `weather_forecast`, `air_quality`) "
"with ONLY the `latitude` and `longitude` arguments..."
"Report the answer in plain English.",
)
What each one does:
MCP_SERVER_URL: where to reach the MCP server. The/mcppath is the streamable-HTTP endpoint for the server (this is the default).ALLOWED_TOOLS: the server offers around 14 tools. Models may get overwhelmed by long, similar-looking tool catalogs. We whitelist just the few we need.
(i) This is a good optimization practice. When you pass tools to a model, their names, descriptions, and schemas (e.g., input and output types) get serialized into the context window as tokens on every request. Fourteen tools means 14 descriptions the model reads and reasons over before each turn. That costs context budget and, worse, raises the odds the model picks a wrong or near-duplicate tool. Whitelisting trims both the token cost and the decision space, so the model chooses faster and more accurately. Add tools back as the task actually needs them, not preemptively. Typically, small and medium models might get overwhelmed by the full catalog, but larger ones will be fine.
TOOL_GUIDANCE: a system prompt that nudges the model into the right two-step pattern: geocode the place first, then call the weather tool with just the coordinates.
Step 3: Connect to the Server and Fetch Tools
load_mcp_tools() is the new piece. It opens a connection to the MCP server, asks for its tool list, and filters it down to our whitelist:
async def load_mcp_tools() -> list:
"""Connect to the MCP server and grab its tools.
The server advertises a list of tools it provides. The MCP
adapter wraps each one so LangChain treats them just like the
local @tool functions from pass 8, the rest of the code doesn't
know the difference.
We then filter to ALLOWED_TOOLS so small models don't get
overwhelmed by every tool the server offers.
"""
client = MultiServerMCPClient(
{
"open-meteo": {
"url": MCP_SERVER_URL,
"transport": "streamable_http",
}
}
)
tools = await client.get_tools()
return [t for t in tools if t.name in ALLOWED_TOOLS]
MultiServerMCPClientis built to talk to several MCP servers at once. Here we configure only one ("open-meteo"), but the shape is the same.client.get_tools()is the network call: it hands back LangChain-compatible tool objects, one per tool the server advertises. The adapter takes care of translating MCP's wire format into the same interface LangChain uses for local tools.The function is
asyncbecause MCP transports are async; the rest of the program follows that down tomain.The tools from the previous pass were deleted since we no longer need them as the MCP server provides them.
Step 4: Make main Async
Because we await load_mcp_tools() which is async, main itself becomes async. This is a Python detail: await only works inside an async def function, so the moment one call in main needs awaiting, main has to be async too, and something has to drive it. That driver is asyncio.run(main()) at the entry point.
async def main() -> None:
global USER_ID
USER_ID = (
input("Enter your user id: ").strip() or "default"
)
llm = ChatOllama(
model=OLLAMA_MODEL,
base_url=OLLAMA_HOST,
num_predict=2048,
)
summarizer = ChatOllama(
model=OLLAMA_MODEL,
base_url=OLLAMA_HOST,
num_predict=2048,
)Local AI Engineering with Ollama
Run, understand, customize, fine-tune, and build agentic apps on your own hardwareEnroll now to unlock all content and receive all future updates for free.
