CourseModel Context Protocol: The Complete Course · Course guide · part 3 of 77
Part 3 · Course guide

Setup: Colab, Your Model Provider, MCP in a Notebook, and Running Locally

24 min read·13 Oct 2026

This takes about fifteen minutes. You do it once.

1. Choose a provider

Most of this course needs no model at all: servers, clients, the wire format, authorisation and most of the security work run without one. From Module 1 on, though, the notes assistant is a host that lets a language model choose tools, and for that you need a model. The course works with three ways of reaching one. You only need one, and you switch by changing one word in the setup cell.

ProviderWhat it isWhere you run itCost
GroqA cloud service that runs open models very fastGoogle Colab, or your own computerFree plan with daily limits
Google GeminiGoogle's own models, through an APIGoogle Colab, or your own computerFree tier with limits
OllamaA program that runs a model on your own machineYour own computer, in a code editorFree; you supply the computer

If you are using Colab, choose Groq or Gemini. They need a free key and nothing else.

If you want Ollama, work on your own computer in an editor such as VS Code. Ollama runs the model on your machine, so no key is needed and nothing you type leaves your computer. Section 7 shows the local setup.

Free plans limit how many requests you can send per minute and per day. If you hit a limit you get an error with the number 429. Wait a minute and try again, or switch provider.

Model names change. The names below were checked against each provider's documentation on 13 October 2026. If a call fails with "model not found", open the provider's model list (links below) and put a current name into the setup cell. Nothing else in the lesson needs to change. One warning from that check: the earlier version of this course used Groq's llama-3.3-70b-versatile. Groq's deprecations page lists it as shut down on 16 August 2026, so no lesson uses it.
Groq free key Colab Gemini free key Colab Ollama your computer no key
Groq and Gemini are reached from Colab with a free key; Ollama runs the model on your own computer and needs no key.

2. Get a key (Groq or Gemini)

Groq

  1. Go to console.groq.com and sign in.
  2. Open API Keys and create a key.
  3. Copy it. You will not be shown it again.

Model list: console.groq.com/docs/models

Google Gemini

  1. Go to aistudio.google.com and sign in.
  2. Choose Get API key and create a key.
  3. Copy it.

Model list: ai.google.dev/gemini-api/docs/models

3. Store the key safely in Colab

Never paste a key into a code cell. Notebooks get shared, and a key in a shared notebook is a key someone else can spend. This course also teaches you to keep secrets away from tools and logs (Modules 4, 7 and 9), so start the habit here.

  1. Open colab.research.google.com and create a new notebook.
  2. In the left sidebar, click the key icon (Secrets).
  3. Add a secret. The name must be exactly GROQ_API_KEY (for Groq) or GOOGLE_API_KEY (for Gemini). The value is your key.
  4. Turn on Notebook access for it.

The secret is remembered across notebooks. For each new notebook you only switch access on.

4. The setup cell

Every lesson that calls a model begins with these two cells. Paste them, set PROVIDER, and run.

The course uses LangChain, a widely used Python library for building applications on language models. One of the things it does is give every provider the same interface, so the rest of your code does not care which one you chose. Later modules also use LangGraph, its companion library for multi-step loops and approvals.

python
%pip install -q -U langchain langgraph langchain-groq langchain-google-genai langchain-ollama
python
import os
from langchain.chat_models import init_chat_model

PROVIDER = "groq"   # "groq" or "gemini" in Colab; "ollama" on your own computer
MODELS = {"groq": "groq:openai/gpt-oss-20b", "gemini": "google_genai:gemini-3.5-flash-lite", "ollama": "ollama:llama3.2:3b"}
KEY = {"groq": "GROQ_API_KEY", "gemini": "GOOGLE_API_KEY", "ollama": None}[PROVIDER]
if KEY and KEY not in os.environ:
    from google.colab import userdata   # keys live in Colab's Secrets panel
    os.environ[KEY] = userdata.get(KEY)
llm = init_chat_model(MODELS[PROVIDER], temperature=0, max_retries=6)
python
print(llm.invoke("Reply with the single word: ready").text)

If it prints ready (or something close), you are set up. On 13 October 2026 both groq:openai/gpt-oss-20b and google_genai:gemini-3.5-flash-lite printed ready, and both called a search_notes tool with {'query': 'nap study participants'} when asked how many participants the nap study will have.

Code walkthrough

  • %pip install ... installs the libraries once per notebook. langchain and langgraph are the frameworks; the three langchain-... packages are the connectors for each provider.
  • PROVIDER = "groq" is the one word you change to switch provider.
  • MODELS names the model for each provider as provider:model-name. This is the only place model names appear, so when a name changes you edit one line.
  • KEY is the name of the secret the provider needs. Ollama needs none.
  • The if block runs only in Colab when the key is not already set: it copies your key from the Secrets panel into an environment variable, which is where LangChain looks for it.
  • init_chat_model(...) creates the model object, llm. temperature=0 asks for the most consistent output. max_retries=6 makes the client wait and try again when the provider says "too many requests" (a 429); Groq's free plan allows 8,000 tokens a minute, and a host that sends a long tool list can pass that. It does not retry a reply the provider rejected as malformed: see "When the provider rejects a reply" below.
  • llm.invoke(...) sends one message and returns a reply; .text is the reply as a string.

Why one cell works for three providers

init_chat_model reads the part before the colon (groq, google_genai, ollama) and builds the right connector. All connectors offer the same methods: .invoke() to get a reply, .bind_tools() to offer the model tools, and so on. Your code talks to llm and never mentions the provider again.

This is also how real teams avoid being locked to one provider: the provider is a setting, not something written all through the code. The notes assistant keeps the same rule: notes_assistant/llm.py (Module 1) reads the provider from one environment variable, LLM_PROVIDER, and uses the same MODELS table.

The connectors are not identical underneath. Two differences the course meets: the Gemini connector warns that gemini-3.5-flash-lite "uses fixed sampling defaults" and ignores temperature (seen on 13 October 2026), so do not expect temperature=0 to make Gemini repeatable; and with structured output, Groq applies strict=True while the Gemini connector ignores it without a warning (seen in the LLMs course's runs, October 2026). Code always validates what comes back.

When the provider rejects a reply

Sometimes a provider refuses the model's own reply with an error instead of returning it. In test runs for the LLMs course in this series on 12 October 2026, Groq did this a few times with openai/gpt-oss-20b: an answer that should have been JSON came back empty, or the model called a tool it was never given. The second case matters in this course, where the model chooses tools all the time. max_retries does not help, because the request itself was fine. The course handles it the same way everywhere: try once more, and if that also fails, say so plainly instead of crashing.

python
def ask(model, messages):
    """Returns (reply, None), or (None, reason) after two refusals."""
    for attempt in range(2):                     # one retry: the next reply is usually fine
        try:
            return model.invoke(messages), None
        except Exception as err:
            last = err
    return None, f"provider_error: {type(last).__name__}"

Code walkthrough

  • ask sends the messages and returns the reply with no reason, or no reply with a reason.
  • On an error it tries once more; after a second error it gives up and names the error, for example provider_error: BadRequestError.

Lessons use this shape: a host step that gets no answer returns a clear "no answer" result with the reason provider_error, and the loop stops cleanly, so one bad reply never stops a notebook. Module 6 builds the host's loop around it.

Prices and limits (checked 13 October 2026)

This is the course's one table of model prices and limits. Lessons that count cost say "prices from course-guide/02" and use these numbers; when a provider changes them, this table changes and the lessons follow. Prices are US dollars per 1,000,000 tokens on each provider's paid plan.

Model (as written in MODELS)InputOutputContext windowNotes
groq:openai/gpt-oss-20b (course default)$0.075$0.30131,072 tokens, up to 65,536 outputGroq's rate-limits page lists 30 requests a minute, 1,000 a day, 8,000 tokens a minute and 200,000 a day for this model as its base limits
groq:openai/gpt-oss-120b (stronger)$0.15$0.60131,072 tokens, up to 65,536 outputSame base limits. Groq names it as a replacement for the shut-down llama-3.3-70b-versatile
google_genai:gemini-3.5-flash-lite (course default)$0.30$2.50Stable. Free tier available. Output price includes thinking tokens
google_genai:gemini-3.8-flash (stronger)$0.75$3.75Stable. Listed price until 31 December 2026; $1.50 and $7.50 from 1 January 2027
ollama:llama3.2:3b$0$0No fee per token; you pay for the machine. Tagged "tools" on Ollama's library page; a 2.0 GB download
ollama:qwen3:8b (optional, stronger at tools)$0$0Tagged "tools" on Ollama's library page; a 5.2 GB download

Sources, all fetched 13 October 2026: Groq, "Supported models" (https://console.groq.com/docs/models), "Rate limits" (https://console.groq.com/docs/rate-limits) and "Deprecations" (https://console.groq.com/docs/deprecations); Google, "Gemini models" (https://ai.google.dev/gemini-api/docs/models) and "Gemini Developer API pricing" (https://ai.google.dev/gemini-api/docs/pricing), which recommends "3.5 Flash-Lite or 3.8 Flash" for new projects; Ollama, "llama3.2" (https://ollama.com/library/llama3.2) and "qwen3" (https://ollama.com/library/qwen3). Groq's models page still lists llama-3.3-70b-versatile with "Contact sales" pricing while its deprecations page gives the 16 August 2026 shutdown; the course follows the deprecations page. Free plans change often and your own limits are on each provider's console page; these are the numbers in this course most likely to go out of date.

Closed models with MCP (not run here)

Some hosted model APIs can call an MCP server for you, so your code never runs an MCP client. The course does not run these, because they need paid keys and a server reachable from the public internet, which a Colab notebook is not. They are shown so you recognise them. Both snippets are copied from the providers' documentation on 13 October 2026.

Anthropic's MCP connector, in the Messages API (beta):

python
client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=1000,
    messages=[{"role": "user", "content": "What tools do you have available?"}],
    mcp_servers=[
        {
            "type": "url",
            "url": "https://example-server.modelcontextprotocol.io/sse",
            "name": "example-mcp",
            "authorization_token": "YOUR_TOKEN",
        }
    ],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "example-mcp"}],
    betas=["mcp-client-2025-11-20"],
)

Code walkthrough

  • mcp_servers tells Anthropic's servers where your MCP server is and which token to present to it. Anthropic's servers act as the MCP client.
  • tools=[{"type": "mcp_toolset", ...}] offers every tool from that server to the model. betas=[...] switches the beta feature on.
  • The documentation lists two limits: of the MCP feature set "only tool calls are currently supported", and "the server must be publicly exposed through HTTP"; "local STDIO servers cannot be connected directly". Source: Anthropic, "MCP connector" (https://platform.claude.com/docs/en/agents-and-tools/mcp-connector).

OpenAI's Responses API with a remote MCP server:

python
from openai import OpenAI

client = OpenAI()
resp = client.responses.create(
    model="gpt-6-astra",
    tools=[{
        "type": "mcp",
        "server_label": "openai_docs",
        "server_description": "Search and read the public OpenAI documentation.",
        "server_url": "https://developers.openai.com/mcp",
        "require_approval": "never",
    }],
    input="Search the OpenAI docs for Responses API streaming and return the relevant links.",
)
print(resp.output_text)

Code walkthrough

  • The tool of type "mcp" names a remote server by URL; OpenAI's servers connect to it and offer its tools to the model.
  • "require_approval": "never" lets the model call the server's tools without asking. Module 6 explains why the notes assistant does the opposite for any tool that writes.
  • Source: OpenAI, "MCP and Connectors" (https://developers.openai.com/api/docs/guides/tools-connectors-mcp). The page covers remote servers on the public internet; it does not mention stdio servers.

Both features are labelled beta or are changing quickly; check the pages before relying on the exact parameter names.

5. MCP in a notebook

The course is written for Google Colab. MCP was designed for programs that run for a long time in separate processes, which a notebook is not, so this section shows the patterns every lesson relies on. Each pattern was run on 13 October 2026 in a local Jupyter kernel (Python 3.12.15, ipykernel 7.4.0) with mcp 2.3.0; the pilot lesson repeats the run in Colab itself.

Install the SDK

Every lesson that uses MCP starts with this cell, after the setup cell if it has one:

python
%pip install -q mcp==2.3.0
import logging
for name in ("mcp", "httpx2", "uvicorn"):
    logging.getLogger(name).setLevel(logging.WARNING)

Code walkthrough

  • mcp==2.3.0 is the official MCP Python SDK, pinned to the version the course was tested with. It speaks protocol revision 2026-07-28 and also talks to older servers and clients. It brings its own web server (uvicorn) and HTTP client (httpx2).
  • The loop turns down three libraries' log messages to warnings only. Without it, every HTTP request a client makes prints an information line into your notebook, which buries the output you are reading.
  • The loop covers loggers named mcp, httpx2 and uvicorn only. Creating an MCPServer also calls logging.basicConfig at level INFO with its own stderr handler (checked in mcp 2.3.0), so any other logger, including your own server's, then prints its INFO lines. A server's own logger therefore sets propagate = False and gets its own stderr handler (Module 1 Lesson 3). A server's entry point configures logging before it creates the MCPServer, or passes log_level="WARNING" to it; either leaves the root logger at WARNING (the Module 5 lab's main() does the first).
  • A server process that serves HTTP writes uvicorn's access lines (one per request) to stdout, not stderr. When you start one with subprocess, send stdout and stderr to a log file. Over stdio, stdout is the protocol channel, so a stdio server never starts uvicorn and never prints there.

A notebook already runs an event loop

MCP code is asynchronous: it uses async functions and await. Plain Python scripts start the event loop themselves with asyncio.run(main()) or anyio.run(main). A notebook cannot do that, because Jupyter and Colab already run an event loop for you. In the course's run, asyncio.run() raised RuntimeError: asyncio.run() cannot be called from a running event loop, and anyio.run() raised RuntimeError: Already running asyncio in this thread. Instead, write await directly in a cell (top-level await). First, a small server written to a file:

python
%%writefile notes_server.py
from mcp.server import MCPServer

NOTES = {"lab-sync-2026-09-02": "Decision: run the nap study with 24 participants instead of 16."}
mcp = MCPServer("notes")

@mcp.tool()
def search_notes(query: str) -> list[str]:
    """Search Nare's notes and return the ids of notes that mention the query."""
    return [nid for nid, body in NOTES.items() if query.lower() in body.lower()]

if __name__ == "__main__":
    mcp.run()          # stdio by default

Code walkthrough

  • %%writefile notes_server.py must be the first line of its cell. The cell writes notes_server.py next to the notebook (/content in Colab) and prints Writing notes_server.py instead of running the code.
  • MCPServer("notes") creates a server; @mcp.tool() turns the function below it into a tool, using its type hints for the input schema and its docstring for the description. mcp.run() serves over stdio when the file is run as a program.
  • A real file matters for two reasons: a stdio server is a separate program that needs a file to start from, and the next cells can also import notes_server like any module.

Then a client, in memory:

python
import notes_server
from mcp import Client

async with Client(notes_server.mcp) as client:   # in memory: no process, no port
    tools = await client.list_tools()
    print("in memory:", [t.name for t in tools.tools])
    result = await client.call_tool("search_notes", {"query": "nap"})
    print(result.content[0].text)

Code walkthrough

  • async with Client(...) opens a connection and closes it when the block ends, even if an error happens inside. Writing it at the top level of a cell is allowed in Jupyter and Colab, and nowhere else: in a .py file it is a syntax error.
  • import notes_server loads the file you just wrote. Passing the server object itself (notes_server.mcp) gives an in-memory connection. The SDK's documentation describes it as "no subprocess, no port, no bytes on a wire". It is the fastest way to try a server and the one the course's tests use.
  • list_tools() and call_tool() are two protocol requests. The output was in memory: ['search_notes'] and lab-sync-2026-09-02.
  • When a lesson shows a script meant for a terminal, it keeps anyio.run(main) at the bottom and says that in a notebook you await main() instead.

stdio: the server as a child process

A stdio server is a separate program the client starts and talks to through its standard input and output. The client below starts notes_server.py as a child process of the notebook:

python
import sys
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client

params = StdioServerParameters(command=sys.executable, args=["notes_server.py"])
with open("server.log", "w") as log:                       # the server's stderr goes here
    async with Client(stdio_client(params, errlog=log)) as client:
        result = await client.call_tool("search_notes", {"query": "nap"})
        print("stdio:", result.content[0].text)

Code walkthrough

  • StdioServerParameters(command=sys.executable, ...) says how to start the server: with the same Python the notebook runs, so the child process finds the same installed libraries.
  • stdio_client(params, errlog=log) starts the child and sends its error output (stderr) to server.log. The protocol reserves the server's standard output for MCP messages only, and a stdio server writes its logs to stderr. Sending stderr to a file keeps those logs readable and avoids depending on how a notebook handles a child's stderr. The output was stdio: lab-sync-2026-09-02.
  • If a lesson changes notes_server.py after importing it, it calls importlib.reload(notes_server), or the in-memory examples keep using the old version.

Streamable HTTP on localhost

A Streamable HTTP server listens on a port and answers each request as an HTTP POST. In a notebook, the lesson runs the web server in a background thread, so the next cell can be the client:

python
import threading, time, uvicorn

app = notes_server.mcp.streamable_http_app()
server = uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=8010, log_level="warning"))
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
while not server.started:
    time.sleep(0.05)

async with Client("http://127.0.0.1:8010/mcp") as client:
    result = await client.call_tool("search_notes", {"query": "nap"})
    print("http:", result.content[0].text)
python
server.should_exit = True     # stop the background server before the next lesson
thread.join(timeout=5)
print("stopped:", not thread.is_alive())

Code walkthrough

  • streamable_http_app() turns the server into a web application with one endpoint, /mcp. By default it accepts only requests addressed to localhost, a protection against DNS rebinding that Module 2 tests.
  • uvicorn.Server(...) is the web server; host="127.0.0.1" listens only on this machine. The specification says a local server "SHOULD bind only to localhost (127.0.0.1) rather than all network interfaces (0.0.0.0)".
  • threading.Thread(target=server.run, daemon=True) runs it in the background. daemon=True means it will not keep the notebook alive when the runtime stops. The while loop waits until it is ready to accept connections.
  • Client("http://127.0.0.1:8010/mcp"): a URL string gives a Streamable HTTP connection. The output was http: lab-sync-2026-09-02.
  • server.should_exit = True asks uvicorn to stop; thread.join waits for it. The output was stopped: True. A server you forget to stop keeps its port until the runtime restarts, which is why each module has its own band of ports (8010 to 8019 for Module 1, 8020 to 8029 for Module 2, and so on; see course-guide/06).
  • To look at the raw protocol, lessons send requests to the running server with !curl or with httpx2, the HTTP client mcp installs. ! runs a shell command from a cell.
  • Modules 9 and 11 also run servers as separate processes (subprocess.Popen([sys.executable, "server.py"])), to stop one with a signal or to run two replicas behind a balancer. Each lesson that does so stops its processes in its last cell.

Tests and the repository

The project is one repository, notes-assistant/, with the package notes_assistant/ inside it (course-guide/03 lists every file). In Colab, a runtime reset deletes every file you wrote, so keep the repository in your Google Drive:

python
from pathlib import Path
from google.colab import drive

drive.mount("/content/drive")
REPO = Path("/content/drive/MyDrive/notes-assistant")
REPO.mkdir(parents=True, exist_ok=True)
%cd {REPO}

Code walkthrough

  • drive.mount(...) asks for permission once and then shows your Drive as a folder.
  • REPO is where every lab writes the project's files. %cd {REPO} makes it the current folder, so import notes_assistant and !python -m pytest -q work from there.
  • Without Drive the course still runs, in /content/notes-assistant, but you must rebuild the files after each reset. A lesson that needs a package file from an earlier module says which lab writes it.

Tests run with !python -m pytest -q against the files a lab has written. Asynchronous tests use the anyio pytest plugin that comes with anyio (installed by mcp), marked pytest.mark.anyio, so no extra test library is needed.

6. Extra libraries, by module (checked 13 October 2026)

The setup cell and the MCP cell cover most lessons. Some lessons need one more library. Those lessons install it in their own cell, just before first use, with the version pinned so that your run matches the course's. Every pin below was checked on pypi.org on 13 October 2026. "First used" is the first module that installs or imports it.

LibraryPinned installFirst usedAlso used inWhat it is for
mcpmcp==2.3.0Module 1Every moduleThe official MCP Python SDK (released 2 October 2026). Brings uvicorn 0.54.0, httpx2 2.13.1, starlette, pydantic, jsonschema, pyjwt[crypto] and opentelemetry-api, so those are not installed separately
LangChain and LangGraphunpinned with -U in the setup cell (1.4.5 and 1.2.15 on 13 October 2026)Module 1Modules 3, 4, 6, 10, 11Calling models through one interface; the host's loop and approvals
pytestpytest==9.1.1Module 1 (lab)Every labRunning the repository's tests. The Module 1 lab installs it beside mcp and lists it in requirements.txt
LangChain's MCP adapter"langchain[mcp]==1.4.5"Module 6Module 10MCPAdapter, which turns an MCP server's tools into LangChain tools. LangChain's documentation says it is built on FastMCP; the extra installs fastmcp 4.x, which accepts mcp 2.x. Do not install the older langchain-mcp-adapters: its repository says it is archived, and its latest release (0.3.2) requires mcp<2.0.0, which would remove the course's SDK
pyjwt with cryptographycomes with mcp (PyJWT 2.15.1); locally "pyjwt[crypto]==2.15.1"Module 7Modules 9, 11Development tokens, RS256 signatures and a local JWKS document
opentelemetry-sdkopentelemetry-sdk==1.45.1Module 9Module 11Tracing spans across host, server and a downstream call, printed to the console. Matches the opentelemetry-api 1.45.1 that mcp installs
opentelemetry-exporter-otlp-proto-httpopentelemetry-exporter-otlp-proto-http==1.45.1Module 9 (local only)Sending spans to a self-hosted Langfuse or Phoenix
fakeredisfakeredis==2.39.0Module 9A Redis stand-in for rate limits and the job queue in a notebook
rqrq==2.12.0Module 9A Redis-backed job queue for the bulk import
redisredis==8.1.0Module 9 (local, with Docker Compose)The real Redis client
sentence-transformerssentence-transformers==6.1.0Module 10 (Optional section)Local embeddings for the retrieval server, no key
faiss-cpufaiss-cpu==1.15.1Module 10 (Optional section)A vector index beside the hand-built TF-IDF index
uvicorn, redis and the LangChain packagesuvicorn==0.54.0 redis==8.1.0 rq==2.12.0 langchain==1.4.5 langgraph==1.2.15 langchain-groq==1.1.3 langchain-google-genai==4.4.1 langchain-ollama==1.1.0Module 9 (requirements-service.txt)Module 11The container's fixed versions. Notebooks install the LangChain packages unpinned with -U (the setup cell)

The source course pinned openai==3.16.2 as an HTTP client for the three providers. This course reaches all three through LangChain instead, so openai is not installed. Python itself: the mcp 2.3.0 package declares Python 3.10 or newer, while the SDK's installation page says 3.11 or newer; use 3.11 or newer on your own computer. Run !python --version in Colab to see its version.

A lesson writes the install as, for example:

python
%pip install -q fakeredis==2.39.0 rq==2.12.0

If pip reports a version conflict in Colab, keep the pinned library and let pip choose its dependencies. On your own computer, add the libraries you need to the pip install line in section 7.

7. Running on your own computer (required for Ollama and the "not run here" steps)

This is the course's one page for working locally. You can run every lesson locally with any provider. For Ollama it is the way to do it. A few things in the course cannot run in Colab at all; each is listed below with exact steps, and each lesson that needs one says "not run here" and names the documentation it was checked against.

Install

  1. Python 3.11 or newer from python.org. Check with python --version (on some systems python3 --version).
  2. A code editor. VS Code with the Python and Jupyter extensions is a good choice. Cursor, PyCharm and others work the same way.
  3. Ollama, if you are using it, from ollama.com/download. After installing, download the course's default model once, in a terminal:
bash
ollama pull llama3.2:3b

That is a download of about 2 GB, and it runs acceptably on a recent laptop with 8 GB of memory or more. A small local model chooses tools less reliably than the cloud models. That is useful here: several "break it" steps in Modules 3, 6 and 8 are easier to see on a small model. If you want a stronger local tool caller, ollama pull qwen3:8b (about 5.2 GB) and change the ollama entry in MODELS.

Create the project folder

In a terminal:

bash
mkdir notes-assistant
cd notes-assistant
python -m venv .venv

Switch the virtual environment on:

bash
# macOS and Linux
source .venv/bin/activate

# Windows (PowerShell)
.venv\Scripts\Activate.ps1

Then install the libraries:

bash
pip install -U langchain langgraph langchain-groq langchain-google-genai langchain-ollama python-dotenv ipykernel
pip install mcp==2.3.0 pytest==9.1.1

Keys on your own computer

Ollama needs no key. For Groq or Gemini, create a file named .env in the project folder:

text
GROQ_API_KEY=paste-your-key-here
GOOGLE_API_KEY=paste-your-key-here

and add these two lines at the very top of the setup cell:

python
from dotenv import load_dotenv
load_dotenv()

Never share the .env file or commit it to Git. The capstone's .gitignore lists it.

Run the lessons

Open the folder in your editor and create a notebook file, for example module-01-lesson-3.ipynb. Select the .venv Python when asked. A notebook in VS Code works like Colab, including top-level await: paste each code block into a cell and run it. Set PROVIDER = "ollama" in the setup cell. Skip the %pip install cells and the Google Drive cell, because you installed the libraries already and your files stay on disk.

Scripts outside a notebook run from the repository root, for example python -m notes_assistant.server (stdio) or NOTES_TRANSPORT=streamable-http NOTES_PORT=8050 python -m notes_assistant.server (HTTP, from Module 5). In a .py file, start the event loop yourself with anyio.run(main).

Trying the server in a desktop host (Module 1 on; not run here)

A host such as Claude Desktop starts a stdio server from a configuration file. The MCP documentation gives the file's location as ~/Library/Application Support/Claude/claude_desktop_config.json on macOS and %APPDATA%\Claude\claude_desktop_config.json on Windows (open it from Settings, Developer, Edit Config). For the notes server, the entry looks like this; use absolute paths, as the documentation requires:

json
{
  "mcpServers": {
    "notes": {
      "command": "/absolute/path/to/notes-assistant/.venv/bin/python",
      "args": ["-m", "notes_assistant.server"],
      "env": {"NOTES_DIR": "/absolute/path/to/notes-assistant/notes", "PYTHONPATH": "/absolute/path/to/notes-assistant"}
    }
  }
}

Restart the host completely after saving. If the server does not appear, the logs are in ~/Library/Logs/Claude on macOS and %APPDATA%\Claude\logs on Windows; mcp-server-notes.log holds the server's stderr. Source: "Connect to local MCP servers" (https://modelcontextprotocol.io/docs/develop/connect-local-servers), fetched 13 October 2026. Other hosts (VS Code, Cursor, Claude Code) have their own configuration formats, documented by each; Module 1 Lesson 3 names them. Host configuration is the part of MCP most likely to change.

The MCP Inspector (Module 5; not run here)

The Inspector is the reference tool for poking at a server by hand. It is a Node.js program: version 2.10.1 on 13 October 2026, which requires Node.js 22.19.0 or newer. Its documentation gives the general form npx @modelcontextprotocol/inspector <command that starts the server>. For the one-file server from section 5, with Node installed:

bash
npx @modelcontextprotocol/inspector .venv/bin/python notes_server.py
npx @modelcontextprotocol/inspector --cli .venv/bin/python notes_server.py --method tools/list

The first opens the web interface in your browser; the second (--cli) lists the tools and exits, which suits scripts. Module 5 Lesson 4 gives the command for the packaged notes server. The documentation warns that on a machine with no keychain the Inspector saves secrets to ~/.mcp-inspector/secrets.json unencrypted unless you supply a key. Sources: "MCP Inspector" (https://modelcontextprotocol.io/docs/tools/inspector) and the npm registry entry for @modelcontextprotocol/inspector, both checked 13 October 2026.

A real identity provider (Module 7; not run here)

In Colab, Module 7 uses a stand-in identity provider: a key pair and a JWKS document served on 127.0.0.1:9000. To try the same server against a real one, Keycloak's documentation gives this command for a local development instance (Docker required):

bash
docker run -p 127.0.0.1:8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.8.0 start-dev

The admin console is then at http://localhost:8080/admin. Development mode only; never expose it. Source: Keycloak, "Getting started with Docker" (https://www.keycloak.org/getting-started/getting-started-docker), fetched 13 October 2026. Module 7 Lesson 3 gives the realm and client settings.

Docker, Docker Compose and GitHub Actions (Modules 8, 9 and 11; not run here)

Containers do not run inside Colab. Install Docker from docs.docker.com/get-started/get-docker. Module 8 runs a stdio server inside a container for real isolation (the notebook version, a subprocess with limits, is stated plainly as not a sandbox). Module 9 builds the server image with docker build, runs two replicas, Redis and a reverse proxy with docker compose up, and adds a GitHub Actions workflow that runs the tests on every push. Each lesson gives the exact files and commands and says what was checked against the official documentation.

On a Mac: FAISS and PyTorch together

Only if you take Module 10's Optional embeddings section locally: loading FAISS and PyTorch in the same Python process can crash with an OpenMP error, because each brings its own copy of the OpenMP library. If you see it, set two environment variables in the terminal before you start Python, Jupyter or your editor:

bash
export KMP_DUPLICATE_LIB_OK=TRUE
export OMP_NUM_THREADS=1

The first tells OpenMP to accept the second copy; it is a workaround, not a fix. The second keeps OpenMP to one thread, which is fine for the eight notes in this course. You do not need it in Colab.

8. When something goes wrong

What you seeWhat it meansWhat to do
SecretNotFoundErrorThe secret is missing or misnamedCheck the name is exactly GROQ_API_KEY or GOOGLE_API_KEY
NotebookAccessErrorThe secret exists but this notebook cannot read itTurn on Notebook access for the secret
ModuleNotFoundError: google.colabRunning locally with no key setCreate the .env file and add the two load_dotenv lines
AuthenticationError (401) from the model providerThe key is wrong or was deletedCreate a new key and update the secret
NotFoundError (404) mentioning the modelThe model name is out of datePut a current model name in MODELS
RateLimitError (429) from the model providerYou hit the free plan's limitWait a minute, or switch PROVIDER
RuntimeError: asyncio.run() cannot be called from a running event loopA script's asyncio.run(...) or anyio.run(...) was pasted into a notebookWrite await main() in the cell instead
SyntaxError: 'async with' outside async functionTop-level await in a .py filePut the code in async def main() and end the file with anyio.run(main)
A stdio client hangs or fails on its first requestThe server wrote something that is not an MCP message to stdout, often a print()Log to stderr (print(..., file=sys.stderr) or logging); read server.log
ModuleNotFoundError: No module named 'notes_assistant'The notebook is not in the repository folderRun the Drive cell and %cd {REPO}, or check the lab that writes the file
OSError: [Errno 98] Address already in useA server from an earlier cell still holds the portStop it (server.should_exit = True), use the next port in the module's band, or restart the runtime
421 Misdirected Request from your own serverThe request's Host header is not on the server's allowlistConnect to 127.0.0.1 or localhost, or pass transport_security= with your host (Module 2)
403 Forbidden from your own serverThe Origin header is not allowedExpected when testing DNS rebinding protection; otherwise send no Origin or an allowed one (Module 2)
401 Unauthorized from your own server (Module 7 on)No token, or a token for a different server or audienceMint a token with the right audience and scope (Module 7)
A connection error with OllamaOllama is not runningOpen the Ollama app, or run ollama serve in a terminal
Ollama says the model is not foundThe model has not been downloadedRun ollama pull llama3.2:3b