Skip to main content
Glama
dsl-unibe-ch

mcp-tutorial

by dsl-unibe-ch
README.md
# MCP Tutorial

This repository is a hands-on introduction to the **Model Context Protocol (MCP)**.
It contains a minimal MCP server that exposes two tools, and two interchangeable
command-line clients that let a language model discover and call those tools in a
live conversation.

---

## What is MCP?

MCP is an open standard that defines how a language model client communicates with
external tool servers. The client asks the server what tools are available, the
server responds with a list of JSON-Schema-described functions, and the model can
then decide to call them during a conversation. The transport used here is **stdio**:
the client spawns the server as a subprocess and they talk over stdin/stdout using
JSON-RPC messages.

This design keeps servers and clients fully decoupled. A server written in Python
can be called by a client written in any language, and vice versa.

---

## Repository structure

```
mcp-tutorial/
  mcp_server.py                    # The MCP server - defines and exposes tools
  mcp_client_ollama.py             # Client using a local Ollama model
  mcp_client_openai_compatible.py  # Client using any OpenAI-compatible API
  requirements.txt                 # Python dependencies
  .env.example                     # Template for the required environment variables
  .env                             # Your local secrets (never committed, in .gitignore)
```

---

## Prerequisites

- Python 3.10 or newer
- For the Ollama client: [Ollama](https://ollama.com) installed and running locally
- For the OpenAI-compatible client: credentials for a compatible API (see the
  Configuration section below)

---

## Getting started

### 1. Clone the repository

```bash
git clone <repository-url>
cd mcp-tutorial
```

### 2. Create a virtual environment

A virtual environment isolates the project dependencies from the rest of your
system. This is strongly recommended and avoids conflicts with other Python
projects on your machine.

```bash
python3 -m venv venv
source venv/bin/activate      # On Windows: venv\Scripts\activate
```

You will need to activate the virtual environment every time you open a new
terminal session. When it is active you will see `(venv)` at the start of your
prompt.

### 3. Install dependencies

```bash
pip install -r requirements.txt
```

---

## Configuration

### Ollama client

`mcp_client_ollama.py` does not require a configuration file. The model name is
set as the `OLLAMA_MODEL` constant near the top of the script. Edit that line if
you want to use a different locally available model.

Make sure Ollama is running before you start the client:

```bash
ollama serve              # start the Ollama daemon if it is not already running
ollama pull qwen3.5:9b    # download the model the first time. Or change the model name in the mcp_client_ollama.py script in line 18. the variable name is OLLAMA_MODEL
```

### OpenAI-compatible client

`mcp_client_openai_compatible.py` reads its settings from a `.env` file so that
secrets such as API keys are never hard-coded in source code and never
accidentally committed to a repository.

**Step 1 - Copy the example file:**

```bash
cp .env.example .env
```

**Step 2 - Open `.env` in a text editor and fill in the three variables.**

The file looks like this:

```
OPENAI_API_KEY=your-key-or-token-here
API_BASE_URL=https://your-service.example.com/v1
MODEL_NAME=gpt-4o
```

**Using real OpenAI:**

- Get your API key from https://platform.openai.com/api-keys (keys start with `sk-`)
- Leave `API_BASE_URL` blank or remove the line entirely; the library defaults to
  `https://api.openai.com/v1`
- Set `MODEL_NAME` to any model you have access to, for example `gpt-4o`

**Using GPUStack or another self-hosted OpenAI-compatible service:**

- Set `OPENAI_API_KEY` to the access token generated by that service
- Set `API_BASE_URL` to the service endpoint, for example
  `https://gpustack.example.com/v1`
- Set `MODEL_NAME` to a model name that the service exposes

The `.env` file is listed in `.gitignore` and will never be committed to the
repository. Your credentials stay on your machine only.

---

## Running the clients

Both clients launch `mcp_server.py` automatically as a child process over stdio.
You do not need to start the server separately.

### Ollama client

```bash
python mcp_client_ollama.py
```

### OpenAI-compatible client

```bash
python mcp_client_openai_compatible.py
```

Both clients open an interactive prompt. Type your message and press Enter. The
model will reply, calling tools automatically when the question requires it.
Type `quit` or `exit` to stop.

Example session:

```
You: what time is it in Tokyo?
[Client] Model requested 1 tool call(s).
  [Tool Execution] Calling 'get_current_time' with args: {'timezone': 'Asia/Tokyo'}
  [Tool Result] The current time is 2026-09-07 20:34:53. (Requested timezone: Asia/Tokyo)
[Client] Sending tool results back to the model...

Assistant: In Tokyo (Asia/Tokyo) the current time is 8:34 PM on September 7, 2026.
```

---

## Adding more tools to the server

Open `mcp_server.py`. Every tool is a regular Python function decorated with
`@mcp.tool()`. Adding a new tool is three steps:

**Step 1** - Write the function with standard Python type hints on every parameter.
The type hints are used to generate the JSON Schema that the model sees.

**Step 2** - Write a docstring. The first line becomes the tool description that
the model reads to decide when to call the tool. The `Args` block documents each
parameter.

**Step 3** - Apply the `@mcp.tool()` decorator.

Example - a tool that converts Celsius to Fahrenheit:

```python
@mcp.tool()
def celsius_to_fahrenheit(celsius: float) -> float:
    """
    Convert a temperature from Celsius to Fahrenheit.

    Args:
        celsius (float): The temperature in degrees Celsius.
    """
    return (celsius * 9 / 5) + 32
```

That is all that is needed. The next time either client connects it will
automatically discover and offer the new tool to the model.

A few things to keep in mind when writing tools:

- The function name becomes the tool name the model calls. Use clear, descriptive names.
- All parameters must have type annotations. Supported types include `str`, `int`,
  `float`, `bool`, and optional parameters with default values.
- Tools can do anything: call external APIs, read files, run shell commands, query
  databases. The model only sees the name, description, and parameter schema - not
  the implementation.
- Because the model cannot verify what a tool actually does, only expose tools from
  sources you trust. The commented-out example in `mcp_server.py` illustrates this
  risk: a tool named `calculate_sum` could silently delete a file.

---

## How the tool-calling loop works

Understanding this flow helps when debugging:

1. The client sends the conversation history and the list of available tools to
   the model.
2. The model either responds with plain text (no tool needed) or with a
   `tool_calls` field naming which tool to call and with what arguments.
3. The client executes the tool via the MCP session and captures the result.
4. The client appends the tool result to the conversation history and sends
   everything back to the model.
5. The model uses the result to compose a final human-readable answer.

Steps 3 to 5 repeat for each tool call. Some models request multiple tools in
a single turn; both clients handle this by iterating over the `tool_calls` list.

---

## Security note

MCP tool calls execute real code on your machine. Before connecting a client
to any MCP server:

- Read the tool implementations in the server source code.
- Only use MCP servers from sources you trust.
- Prefer running servers inside virtual environments or containers to limit
  their access to the rest of your system.