Home / Articles / Model Context Protocol for Beginners with FastMCP and Ollama

This article is published in English.

Model Context Protocol for Beginners with FastMCP and Ollama

Learn MCP roles—host, client, server, transport—then wire a weather tool server to a local qwen3:8b model through FastMCP and STDIO.

1841 words

Model Context Protocol, usually shortened to MCP, is a shared language for connecting large language models to tools and data sources they cannot touch by themselves. Calling it a protocol emphasizes that it standardizes how the conversation should look; concrete libraries then implement that standard so teams are not inventing sockets and message schemas alone. FastMCP is one such implementation used in the walkthrough below.

Companion repository: https://github.com/harshagangari747/MCPTutorial/tree/main

Prerequisites

The demo depends on three packages: fastmcp, ollama, and langchain-community. Inference runs against the local qwen3:8b model. Launch it with:

ollama run qwen3:8b

Prepare a project folder that already contains empty stubs named weather_server_mcp.py and app.py so the server and host have clear homes.

Understanding MCP

Left alone, an LLM is a token transducer. Tokens enter; tokens leave. It does not dial weather APIs, open databases, or read the system clock unless something outside the model performs those actions. Cloud providers sometimes bolt proprietary tool runners onto their APIs, which is convenient in production and awkward when the goal is to see the protocol itself. Running a local model through Ollama keeps the experiment self-contained.

Try a question such as “what is the weather as of today in Italy?” A typical local reply begins by admitting there is no live weather feed. Even so, the sentence encodes three cues the system must resolve: weather as the topic, “today” as a date, and Italy as a place. The model needs a path to compute or fetch weather, a way to ground “today,” and a way to bind that weather to Italy.

Calendar grounding is the obvious gap. The weights do not reliably know the current date. MCP becomes useful when the model can propose tools and arguments, and a surrounding runtime actually executes those tools, returning fresh observations the model can weave into an answer.

Components in MCP

A practical MCP deployment usually names several cooperating pieces:

  1. A working API — any service that already answers the domain question, such as a weather endpoint found online.
  2. MCP server — a process that hides how the API or database is reached and publishes callable tools.
  3. MCP host — the product surface, for example an intelligent trip planner that combines LLM reasoning with live data.
  4. MCP client — a bridge living inside the host. It tells the model which tools exist, turns model intents into MCP requests, and turns MCP replies into model-friendly context.
  5. Transport layer — JSON-RPC 2.0 delivered either over HTTP/SSE when components are remote, or over STDIO when the model and tools share one machine.
  6. LLM — here qwen3:8b served by Ollama.
  7. Harness — optional orchestration that stitches the stack with less hand-written glue; the original write-up cites Goose AI as one example.

With those roles named, the Italy weather question becomes a choreography instead of a single model call.

Analogy

A driving metaphor keeps the roles sticky. The intention to drive is the host application. The brain is the LLM: it reads the highway context and decides to accelerate, brake, or change gears, yet it cannot depress pedals. Limbs correspond to the MCP server; the muscles and bones inside a limb are individual tools—one limb steers or shifts, another brakes or accelerates. The nerve interface between brain and muscle is the MCP client. The nerves that carry electrical impulses are the transport. The car is the external API. The assembled body is the harness that makes the parts cooperate.

Compressed mapping:

  • LLM → brain
  • MCP server → limb
  • Tool → muscle action
  • MCP host → driving intent
  • MCP client → nerve interface
  • Transport → nerves
  • Working API → car
  • Harness → body assembly

That picture is enough to keep server, client, and transport from collapsing into one vague “plugin.”

Working of MCP

Implementation then follows the roles: stand up a server, a host, an LLM, a transport, optionally a harness, and a real API or service. The server abstracts the API and exposes tools. Each tool is a single action the model may request; the model never executes the HTTP call itself. The client both advertises the catalog and translates in both directions so the model and server stay loosely coupled.

Given a server that offers get_todays_date() and get_weather_data(city, date), a query like “What is the weather today in Paris?” can unfold as:

  1. The model notices it needs today’s date.
  2. It tells the MCP client to use get_todays_date.
  3. The client forwards the request to the server.
  4. The server fulfills it.
  5. The client reshapes the server reply for the model.
  6. Armed with a date, the model still needs Paris weather.
  7. It asks the client to call get_weather_data with city and date.
  8. The client converts that intent into a server request.
  9. The server calls the weather API and returns the payload.
  10. The client converts the payload again for the model.
  11. The model presents the final answer to the user.

Historical questions that fall inside the training window might be answered from memory alone, but the point of MCP is current context: dates and weather that change after training.

The project

The sample keeps the weather story concrete. An MCP server owns the logic that talks to an external weather API. A host application creates the MCP client, registers the server, and queries Ollama. Isolating LLM access in its own helper keeps transport wiring readable.

MCP Server

# MCP Server
# weather_server_mcp.py
from fastmcp import FastMCP
import requests

# This is a server instance that we register in our host
server = FastMCP("weather-mcp-server")


# Third party api data
WEATHER_API_KEY = "api_key_here"
WEATHER_BASE_URL = "https://api.weatherapi.com/v1/"

# Tool 1
@server.tool()
def get_weather_data(city: str) -> float:
    """Get current temperature in Celsius"""
    response = requests.get(
        WEATHER_BASE_URL + "current.json",
        params={"key": WEATHER_API_KEY, "q": city},
    )
    response.raise_for_status()
    return response.json()["current"]["temp_c"]

# Tool 2
@server.tool()
def get_historical_weather_data(city: str, date: str) -> float:
    """Get max temperature for a historical date"""
    response = requests.get(
        WEATHER_BASE_URL + "history.json",
        params={"key": WEATHER_API_KEY, "q": city, "dt": date},
    )
    response.raise_for_status()
    return response.json()["forecast"]["forecastday"][0]["day"]["maxtemp_c"]


if __name__ == "__main__":
    server.run()

Functions that hit the API are annotated with @server.tool(), which publishes them as tools. Docstrings at the top of each function are not decoration; they teach the model when to pick that tool. The example ships two tools: one that fetches current weather for a city, and one that fetches historical weather for a city on a past day.

MCP Host, Client, LLM, Transport method

import asyncio
import sys
import json
from pathlib import Path
from langchain_community.llms import Ollama
from fastmcp import Client
from fastmcp.client.transports import StdioTransport


async def main():
    # We mention the mcp server path.
    server_path = Path(__file__).parent / "weather_server_mcp.py"

    # The transport method here is STDIO
    transport = StdioTransport(
        command=sys.executable,
        args=[str(server_path)],
    )

    # Register the MCP Client
    mcp_client = Client(transport)

    # LLM via Ollama
    llm = Ollama(model="qwen3:8b", temperature=0.5)

    async with mcp_client:
        print("✓ Connected to MCP server!")

        # We can now access that tools are present in the weather server mcp now.
        mcp_tools = await mcp_client.list_tools()
        tools_info = "\n".join([f"- {t.name}: {t.description or t.name}" for t in mcp_tools])

        print(f"✓ Available tools:\n{tools_info}\n")

        # Interactive loop
        while True:
            question = input("🌤️  Ask: ").strip()
            if question.lower() == 'exit':
                break

            try:
                # Step 1: Ask LLM to decide which tool to use
                decision_prompt = f"""Given the question: "{question}"

Available tools:
{tools_info}

Respond with ONLY a JSON object (no other text):
{{"tool": "tool_name", "params": {{"city": "city_name"}}}}

For get_historical_weather_data, use: {{"tool": "get_historical_weather_data", "params": {{"city": "city_name", "date": "YYYY-MM-DD"}}}}"""

                print(f"\n📍 Processing: {question}")
                llm_response = llm.invoke(decision_prompt)

                # Step 2: Parse JSON from LLM response
                json_start = llm_response.find('{')
                json_end = llm_response.rfind('}') + 1

                if json_start == -1 or json_end == 0:
                    print("❌ LLM didn't return valid tool call")
                    continue

                json_str = llm_response[json_start:json_end]
                tool_call = json.loads(json_str)

                print("Tool call: ", tool_call)

                # Handle array responses
                if isinstance(tool_call, list):
                    tool_call = tool_call[0]

                tool_name = tool_call.get("tool")
                params = tool_call.get("params", {})

                print(f"🔧 Calling: {tool_name} with {params}")

                # Step 3: Call MCP tool. This is where we actually call the tool.
                result = await mcp_client.call_tool(tool_name, params)
                answer = result.content[0].text

                print(f"✓ Answer: {answer}°C\n")

            except json.JSONDecodeError as e:
                print(f"❌ JSON parsing error: {e}")
            except Exception as e:
                print(f"❌ Error: {e}\n")


if __name__ == "__main__":
    asyncio.run(main())

Whats happening?

Resolve the server module path next to the host:

server_path = Path(__file__).parent / "weather_server_mcp.py"

Construct a STDIO transport that launches that module with the current Python interpreter:

  # The transport method here is STDIO
    transport = StdioTransport(
        command=sys.executable,
        args=[str(server_path)],
    )

Instantiate the MCP client from the transport:

mcp_client = Client(transport)

The host now has a registered server path, a chosen transport, and a client. Attach the model through Ollama:

llm = Ollama(model="qwen3:8b", temperature=0.5)

Ask the client for the tool catalog published by weather_server_mcp.py:

mcp_tools = await mcp_client.list_tools()

Pass that catalog into the prompt and instruct the model to reply with only a tool name and parameters. After parsing, execute the chosen tool:

result = await mcp_client.call_tool(tool_name, params)

The spine of the tutorial is therefore: create the server, register it, register the client, attach an LLM, and select a transport. Agent harnesses can hide more of this wiring; a plain loop keeps every protocol hop visible while learning.

Taken together, MCP is less a single library call and more a division of labor. The model proposes; the client translates; the server acts; the transport carries JSON-RPC messages; the host owns the user-facing loop. Once those boundaries are clear, swapping weather for calendars, CRMs, or internal search is mostly a matter of writing new tools and documenting them well enough for the model to choose correctly. When the loop is running, watch what the model emits before each tool call. A healthy trace shows the model naming a tool that actually exists, supplying the argument keys the docstring described, and waiting for the client to return data before drafting the user-facing sentence. If the model invents a tool name, tighten the prompt or improve the tool descriptions. If the server throws, surface the error through the client so the model can retry or apologize instead of hallucinating weather values. That feedback discipline matters as much as the initial wiring.