Feedback

Chat Icon

Local AI Engineering with Ollama

Run, understand, customize, fine-tune, and build agentic apps on your own hardware

Building Advanced Agents: Integrating MCP Servers
94%

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 /mcp path 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]
  • MultiServerMCPClient is 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 async because MCP transports are async; the rest of the program follows that down to main.

  • 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 hardware

Enroll now to unlock all content and receive all future updates for free.