The Project: Nare's Notes Assistant
Every lesson in this course is set in one small lab, on one project. The lab is made up. All people, notes, bookings, tokens, secrets, dates and events in the project are fictional and simulated. The real cases that open each lesson are separate, cited, and clearly marked as real.
The lab
Nare runs a small sleep and memory research lab with two colleagues, Priya and Tomas. The lab is planning a nap study: a group of participants learn word pairs, some nap in the sleep lab while wearing EEG caps, and the lab measures what they recall afterwards. Nare keeps her research notes as Markdown files in one folder: meeting notes, a reading list, half-formed ideas about naps and recall.
Money in this course is in US dollars, the currency the model providers price in. The project spends almost none: the only costs are model fees, which course-guide/02 prices.
The problem
Nare uses four AI applications in a normal week: a desktop chat assistant, an AI code editor for her analysis scripts, a command-line coding agent, and a small assistant the lab is building for itself. She wants every one of them to answer "what did we decide about the nap study?" from her notes. Then Tomas wants the same assistants to see the lab's equipment calendar, and Priya wants her reference manager. Without a shared protocol, every pairing of application and tool system is another piece of custom glue: 4 applications times 6 tool systems is 24 integrations to write and keep working.
Nare wants a notes assistant that:
- Answers questions from her notes, in any AI application that speaks MCP, and cites the note it used.
- Says plainly when the notes do not answer the question, instead of guessing.
- Saves a new note when asked, only after she approves it, and never overwrites an existing note.
- Works alongside the lab calendar and the reference manager, without mixing up their tools.
- Can be reached over HTTP from Priya's and Tomas's computers, with Priya and Tomas able to read but not write.
- Does not obey instructions hidden in a note or in another server's tool descriptions, and never leaks what it reads to a log.
You are the engineer building it.
What this course uses the project for
This course is about the Model Context Protocol: why it exists, what goes over the wire, how to build servers and hosts, how to authorise and secure them, and how to run them in production. The notes assistant is the instrument. It is deliberately small (eight notes, two tools) so that the protocol, not the domain, is what you learn, and so you can check every answer by reading the notes yourself.
It is not the BrightCart store of the AI Engineering course, the Harbourline Freight engagement of the Forward Deployed Engineer course, the returns desk of the Agentic AI Engineer course or the Quillpath support desk of the LLMs course. The Agentic AI Engineer course's Module 4 teaches MCP as one tool in an agent stack; this course goes deeper on every point and links to it where the two overlap.
The people
| Name | Role | In the project |
|---|---|---|
| Nare | Runs the lab | Owns the notes folder and the notes server. The main user of the assistant. The only person who may create notes (scope notes:read and notes:write) |
| Priya | Approves budgets; keeps the lab's references | Approved the larger nap study. Asks for her reference manager (Zotero) in Module 4 and imports six papers in Module 9. Reads notes but does not write them (scope notes:read) |
| Tomas | Books the lab equipment | Keeps the equipment calendar, the second MCP server from Module 6. Asks for a "reindex" button in Module 5. Reads notes but does not write them (scope notes:read) |
| You | The engineer | Build the server, the host and everything around them |
One minor name recurs: the Motor lab, the other group that shares the EEG caps, appears in the calendar's bookings. Other minor characters may appear in a single lesson. The main cast above does not change.
The lab's systems
Nare's lab has six tool systems. The course builds servers for the first three; the other three appear only in Module 1's arithmetic and as made-up servers in Module 6.
| System | Owner | In the course |
|---|---|---|
Notes folder (notes/, eight Markdown files) | Nare | The notes server, built from Module 1, finished in Module 5, protected in Module 7 |
| Lab equipment calendar (sleep lab, EEG caps, meeting room) | Tomas | The calendar server, examples/calendar_server.py, from Module 6 |
| Reference manager (Zotero) | Priya | A stand-in with no network, Module 4; never the real Zotero API |
| EEG data store, email, participant spreadsheet | The lab | Named in Module 1; email appears as a made-up server in Module 6 |
Fixed names
Every module calls the server's parts by the same names, so that code from one module works in the next.
The notes server (final form from Module 5, with authorisation from Module 7):
| Part | Kind | What it does |
|---|---|---|
search_notes(query, limit, tag) | Tool, read-only | Ranks notes for a query; returns ids, titles, scores, a one-line snippet and each note's URI |
create_note(title, body, tags) | Tool, not read-only, not destructive | Writes a new note. Fails if a note with that id exists, so it never overwrites. Needs notes:write |
notes://index | Resource | A list of every note with its title and tags |
notes://{note_id} | Resource template | One note in full |
summarise_topic(topic) | Prompt | A ready-made request to summarise what the notes say about a topic |
create_note is marked read_only_hint=False and destructive_hint=False. It is not destructive, because it cannot overwrite anything; the host still asks Nare before calling it, because its rule is "anything that is not read-only needs approval". Lessons keep that wording.
The calendar server (Module 6; Module 10 adds one tool): search_notes (read-only; searches the notes attached to bookings, and collides on purpose with the notes server's tool of the same name), list_bookings(week_of, resource) (read-only), book_slot(resource, day, booked_by, note) (not read-only; fails if the resource is booked that day), cancel_booking(booking_id) (destructive), calendar://bookings (resource), and from Module 10 check_availability(resource, week_of) (read-only).
The host (Module 6): namespaces tools as <server>__<tool>, for example notes__search_notes and calendar__search_notes, and adds one tool of its own, read_resource, so the model can open a note by its URI. With HostConfig(tool_search=True) it shows a second tool of its own, find_tools, instead of the full list (Module 6 Lesson 3).
Library names (from Module 1): NoteStore, Note, SearchHit, NoteError, NoteNotFound, NoteExists, InvalidNote, slugify, validate_note_id in store.py; chat(), ToolCall, ChatReply, LLM_PROVIDER, LLM_MODEL in llm.py. Lessons may add names; they do not rename these.
Fixed facts
| Fact | Value |
|---|---|
| Note ids | Match ^[a-z0-9][a-z0-9-]{0,63}$: 1 to 64 lowercase letters, digits and hyphens, starting with a letter or digit. Anything else, including ../secrets, is refused before a file is touched |
| Note limits | Title 1 to 120 characters; body 1 to 20,000 characters |
| Search scoring | Each query word scores 1 per appearance in the body, 3 in the title, 2 in the tags; ties broken by note id |
| The anchor note | lab-sync-2026-09-02: 24 participants instead of 16 (budget approved by Priya), the sleep lab booked for the weeks of 5 and 12 October, the consent form due 15 September, EEG caps shared with another group. The participant count is on the second body line |
| Scopes | notes:read for searching and reading; notes:write for create_note. Nare has both; Priya and Tomas have notes:read |
| Approval | The host asks before any tool not explicitly marked read-only (needs_approval). Unattended runs and tests use deny_all, which refuses every write. Annotations are the server's claim, not proof: from Module 8, a tool whose definition changed since it was pinned is blocked until reviewed |
| Abstaining | When the notes do not answer, the host replies with the fixed text I could not find this in your notes. (ABSTAIN_TEXT in host.py) |
| The secret | HERON-4471, the made-up freezer alarm code in the capstone's planted note. It must never appear in a log, a reply or a created note |
| Ports | Module N uses ports 8000 + 10N to 8009 + 10N (8010 to 8019 for Module 1); the stand-in identity provider uses 9000. Servers bind to 127.0.0.1, never 0.0.0.0 |
| Dates in code | Code that needs "today" uses DATA_AS_OF from course-guide/06, or the date of the event it replays; never date.today(). NoteStore.create takes the date to stamp as an argument for this reason. The packaged server's build_server(store, *, today=date.today) keeps the clock as its production default; lessons and tests always pass a fixed date |
| Protocol and SDK | MCP specification revision 2026-07-28; Python SDK mcp 2.3.0 (checked 13 October 2026) |
| Currency | US dollars |
The shared data (the notes, the bookings, the reference stand-in, the attack notes, the evaluation sets) is in 06-shared-data.md as code that writes every file. Nothing is downloaded.
Models
The hosted models are the course defaults in 02-setup-colab-and-llm-providers.md: Groq openai/gpt-oss-20b, Gemini gemini-3.5-flash-lite, or Ollama llama3.2:3b on your own computer, switched by one setting. Many lessons also use a scripted stand-in: a few lines of Python that pretend to be a model and always choose the same tool. It is free and repeatable, so it tests plumbing (the loop, the approval gate, the pinning check) and drives the attack demonstrations. Every lesson that uses one says so, and its output is never presented as what a model did. Whenever a lesson claims something about model behaviour, the output comes from a real model, run with real keys.
Dates
- Course date: Tuesday 13 October 2026. The specification, SDK and Inspector versions, model names and prices were checked on this day. Lessons say "(checked October 2026)".
- The project: Monday 7 September 2026 to Friday 9 October 2026, five weeks. Lessons are told in the present tense, as if you are there.
- The data date: Monday 7 September 2026, the first day. The eight notes are a snapshot of the folder that morning (
DATA_AS_OF).
Key events
Lessons refer to these events. They are fixed, so that every module tells the same story. Week N starts on the Monday shown. All of them are simulated.
| When | Week | Event |
|---|---|---|
| Wednesday 2 September 2026 | Before the project. The lab sync: 24 participants instead of 16, budget approved by Priya; Tomas to book the sleep lab for the weeks of 5 and 12 October; Nare to draft the consent form by 15 September | |
| Monday 7 September | 1 | The notes folder is snapshotted (DATA_AS_OF). Nare asks for one assistant that answers from her notes in every AI application she uses. You count the integrations (24 without a protocol, 10 with one) and build "search my notes" twice: with native function calling, then as an MCP server |
| Thursday 10 September | 1 | The prototype server runs over stdio and over HTTP. A debugging print() left in the stdio server breaks the connection; the server's logs move to stderr for good |
| Monday 14 September | 2 | Tools are designed properly. create_note joins search_notes, and tool choice is measured on 20 labelled questions |
| Tuesday 15 September | 2 | Nare asks the assistant to save "Nap study consent form", the draft due that day. The same day a note with a hidden instruction, urgent-todo, is slipped into a shared copy of the folder; nobody notices yet |
| Thursday 17 September | 2 | Resources, prompts and asking the user. Priya asks to pull references from her reference manager; her key is collected on a web page, never in the chat. create_note asks Nare to confirm before writing |
| Monday 21 September | 3 | The server becomes a tested package. Tomas asks for a reindex button, which becomes a long-running handler with progress and cancellation |
| Thursday 24 September | 3 | The host is built. Tomas's equipment calendar becomes the second server, and its search_notes collides with the notes server's; the host namespaces every tool |
| Monday 28 September | 4 | Priya and Tomas want to reach the server from their own computers. The HTTP server gets authorisation: tokens with an audience and scopes, read for them, write for Nare |
| Wednesday 30 September | 4 | Security review. The urgent-todo note from 15 September is found, along with a poisoned tool description and a rug pull in a test server. Each attack is run, then stopped, layer by layer |
| Monday 5 October | 5 | The first nap study cohort is in the sleep lab. The server goes to production: two replicas, rate limits per person, redacted logs, tracing. Priya imports six papers in one job that survives an interruption |
| Wednesday 7 October | 5 | Design patterns and evaluation. A well-meant change to the search_notes description is measured on the 16 cases: tool selection rises, task success does not move, and the regression gate blocks the change under its per-case rule, because a case that passed before now fails |
| Friday 9 October | 5 | Capstone review with Nare, Priya and Tomas: six questions (five answerable, one not), the acceptance checklist, the planted visitor-policy note, and WHAT_BROKE.md |
The course does not fix the assistant's accuracy, speed or cost in advance. Those numbers come from running your own code, and they are what your capstone write-up reports.
What you build, module by module
| Module | When | What the notes assistant gains |
|---|---|---|
| 1. Foundations | Week 1 | The notes, NoteStore, chat() for three providers, a first MCP server and client in memory and over stdio |
| 2. Protocol Architecture and Transports | Week 1 | The wire format by hand, errors, versions, stdio and Streamable HTTP, Origin and Host checks |
| 3. Tools | Week 2 | search_notes and create_note with schemas, annotations, results and errors, measured tool choice |
| 4. Resources, Prompts, and User Interaction | Week 2 | notes://index, notes://{note_id}, summarise_topic, confirmation by form, a key collected by URL |
| 5. Building Servers | Week 3 | The server as a configured, tested package; a long-running reindex with progress and cancellation |
| 6. Building Clients and Hosts | Week 3 | A host with an agent loop, two servers, namespacing, approvals and timeouts |
| 7. Authorization | Week 4 | Token verification with audience and scopes on the HTTP server |
| 8. Security | Week 4 | Six attacks run and stopped: poisoning, rug pulls, shadowing, injection, exfiltration, supply chain; pinned tool definitions |
| 9. Production and Ecosystem | Week 5 | Stateless replicas, rate limits, redacted logs, tracing, a resumable bulk import, a registry entry, a container |
| 10. Design Patterns and Agentic Systems | Week 5 | A task-shaped server, composition, a retrieval server, an evaluation with a regression gate |
| 11. Capstone | Week 5 | The finished, tested repository with a README, the acceptance checklist and WHAT_BROKE.md |
The repository, file by file
You build one repository, notes-assistant/, with one package, notes_assistant/. Lesson cells try ideas in the notebook; only the module labs write files into the repository (in your Google Drive, see course-guide/02). The table lists what each lab adds or changes. Scripts in examples/ are named mNN_*.py by module.
| Module | Files the lab adds or changes | Tests (passing after the lab) |
|---|---|---|
| 1 | requirements.txt (mcp, pytest and the LangChain packages, pinned); notes/ (the eight notes, written by write_notes from course-guide/06); notes_assistant/__init__.py, notes_assistant/store.py (NoteStore and its errors; no MCP in it), notes_assistant/llm.py (chat() over LangChain's init_chat_model, provider from LLM_PROVIDER); examples/m01_first_server.py (one search_notes tool, stdio or HTTP); examples/m01_lab.py | tests/test_m01_store.py, tests/test_m01_server.py, tests/test_m01_llm.py (8; first use of pytest) |
| 2 | examples/m02_server.py (the prototype: search_notes, reindex_notes, and early versions of notes://index, notes://{note_id} and summarise_topic, which Module 4 teaches in full), examples/m02_wire.py (a raw stdio driver with no SDK); examples/m02_lab.py. No package file changes | tests/test_m02_wire.py (3; 11 in all) |
| 3 | examples/m03_server.py (build_m03_server(store, today): search_notes and create_note in their final design, version 0.3.0; search_notes returns {"query": ..., "hits": [...]}); examples/m03_lab.py | tests/test_m03_tools.py (7, the tool contract; 18 in all) |
| 4 | examples/m04_server.py (build_m04_server: Module 3's tools plus the resources notes://index and notes://{note_id}, the summarise_topic prompt with completion, and a create_note that asks Nare to confirm), examples/m04_interaction_server.py (import_zotero with a key collected by URL, the /connect page, port 8044); examples/m04_lab.py | tests/test_m04_interaction.py (9; 27 in all) |
| 5 | notes_assistant/config.py (Settings, load_settings() from NOTES_DIR, NOTES_TRANSPORT, NOTES_HOST, NOTES_PORT, NOTES_LOG_LEVEL; ConfigError, check_settings()), notes_assistant/server.py (build_server(store, *, today=date.today), main(); see the changes from Module 3 below); examples/m05_reindex_server.py (adds reindex_notes; reads NOTES_INDEX and NOTES_REINDEX_DELAY); examples/m05_inspector.json (Inspector configuration, local only); examples/m05_lab.py | tests/conftest.py (shared fixtures), tests/test_store.py, tests/test_server.py, tests/test_reindex.py, tests/test_stdio_smoke.py (20; 47 in all) |
| 6 | notes_assistant/host.py (Host, HostConfig, HostAnswer, needs_approval(), to_openai_tool(), read_resource_tool(), find_tools_tool(), result_text(), deny_all, console_approve, ABSTAIN_TEXT); examples/calendar_server.py (the second server, build_calendar(), version 0.6.0, port 8061; CALENDAR_DELAY_SECONDS slows it for timeout tests); examples/m06_lab.py | tests/test_host.py (8; 55 in all) |
| 7 | notes_assistant/auth.py (JWTTokenVerifier, mint_dev_token, auth_from_env(), require, protected_app, READ_SCOPE, WRITE_SCOPE); notes_assistant/server.py changed by a code cell, not %%ensure (see below); examples/m07_idp.py (the stand-in identity provider on port 9000); examples/m07_lab.py | tests/test_auth.py (11; 66 in all) |
| 8 | notes_assistant/pinning.py (fingerprint, pin_tools, save_pins, diff_pins, check_pins, PinChange, check_description, and PinnedHost, a subclass of Host), notes_assistant/guard.py (GuardedHost, DefendedHost, scoped_env, redact); examples/m08_gateway.py (build_gateway); examples/m08_lab.py. host.py is not changed: every defence subclasses Module 6's Host. Pins are stored in .pins/, which is not committed | tests/test_m08_security.py (10; 76 in all) |
| 9 | Being written. Planned: examples/m09_app.py (create_app(): health check, replica id, rate limit, deadline, redacted logs, graceful shutdown), examples/m09_ratelimit.py, examples/m09_redact.py, examples/m09_bulk_import.py, examples/m09_balancer.py, examples/data/m09_import_batch.json (written from IMPORT_BATCH); registry/server.json, registry/bad-server.json; requirements-service.txt, Dockerfile, compose.yaml, .github/workflows/ci.yml (local only, not run in Colab); examples/m09_lab.py | Planned: tests/test_redaction.py, tests/test_ratelimit.py, tests/test_bulk_import.py |
| 10 | Being written. Planned: examples/calendar_server.py changed (adds check_availability); examples/m10_aggregator.py, examples/m10_retrieval_server.py, examples/m10_eval.py; tests/m10_eval_baseline.json; examples/m10_lab.py | Planned: tests/test_m10_tool_descriptions.py |
| 11 | Being written. Planned: README.md, docs/WHAT_BROKE.md, .gitignore (.env, .pins/, mnotes/, demo-notes/, scratch-notes/), pytest.ini; examples/m11_cli.py; examples/m11_lab.py | Planned: tests/test_capstone.py |
Labs write repository files with %%ensure, the course's cell magic from the Module 1 lab: it writes a missing file and keeps one the repository already has. Each lab first rebuilds every earlier lab's files the same way, so a learner who skipped a lab catches up. The rows above are what each lab adds; the test counts are the lab's own tests and, in brackets after the semicolon, the whole suite after that lab.
The packaged server from Module 5 changes three things against Module 3's examples/m03_server.py, and every later module uses the package:
search_notesreturns a list of hits, which the SDK wraps as{"result": [...]}. Code that readstructured_content["hits"]readsstructured_content["result"]. Each hit keepsnote_id,title,score,snippetanduri.todayis a function, passed by keyword and called on every save:build_server(store, *, today=date.today). Module 3 took a date. Lessons and tests always pass a fixed date (today=lambda: DATA_AS_OF).- The server is
MCPServer("notes", title="Nare's notes", version="0.5.0"). Module 4's confirmingcreate_note, completion andimport_zoterostay inexamples/m04_*.py; the package's resources and prompt are plain.
The Module 7 upgrade. server.py is not rewritten. A code cell in the Module 7 lab (marked # Module 7 lab: upgrade) makes guarded edits and the cell after it applies them: build_server gains token_verifier=None, auth=None (left out, it is exactly the Module 5 server), each tool checks its scope when a verifier is present, and main() turns auth on for HTTP only, from NOTES_AUTH_ISSUER, NOTES_AUTH_JWKS_URL and NOTES_RESOURCE_URL. Every later lab pastes those two cells after the Module 5 server.py cells.
Settings. The package reads NOTES_DIR, NOTES_TRANSPORT, NOTES_HOST, NOTES_PORT (default 8050) and NOTES_LOG_LEVEL (Module 5), and NOTES_AUTH_ISSUER, NOTES_AUTH_JWKS_URL and NOTES_RESOURCE_URL (Module 7). The reindex server, examples/m05_reindex_server.py, also reads NOTES_INDEX (where the index file goes, default notes-index.json) and NOTES_REINDEX_DELAY (seconds per note, default 0.3, so progress and cancellation can be seen). The calendar reads CALENDAR_DELAY_SECONDS.
.gitignore. No lab before Module 11 writes one. Until then the labs commit by naming the project's files in git add, so logs, caches, .pins/ and the scratch folders stay out; Module 11 adds the file.
Two rules hold throughout. store.py never imports mcp: the notes logic stays testable without a protocol. And server.py stays thin while host.py owns policy (approval, namespacing, pinning, timeouts), so a change to one never forces a change to the other.
The finished notes assistant
- One MCP server over Nare's notes, served over stdio and Streamable HTTP, with two tools, two resources and a prompt
- A host that connects a model to two servers, namespaces their tools and asks before any write
- Answers that cite the note they came from, and a plain "the notes do not say" when they do not
- Token verification with audience and scope checks on the HTTP server
- Pinned tool definitions, an approval gate and tests for six attack classes
- Two stateless replicas, rate limits per person, redacted logs, tracing and a resumable bulk import
- An evaluation of tool choice and answers with a regression gate
- A repository with tests that pass, a README a stranger can follow, and a record of what broke
The one idea
A protocol is a promise between programs written by people who never meet. Most of this course is about keeping that promise honest: saying exactly what a tool does, refusing what it should not do, checking who is asking, and treating everything that arrives over the wire, including a tool's own description, as something to verify rather than obey.
Changes from the earlier version (for authors)
The earlier version of this course (/Courses/MCP/) used the same project. What was kept and what changed:
- Kept. The notes assistant, Nare, Priya and Tomas, the eight notes word for word, the anchor note, the store's rules, the tool, resource and prompt names, the calendar's bookings, the attack notes, the evaluation sets, the
notes_assistant/package name and the port bands. - A fixed timeline. The earlier version had dated notes but no project calendar. The project now runs from 7 September to 9 October 2026, with
DATA_AS_OFon 7 September and the events above. - Models. The earlier default on Groq,
llama-3.3-70b-versatile, was shut down on 16 August 2026. The course now uses the defaults shared by every course in this series (course-guide/02), andllm.pyis built on LangChain instead of theopenaiclient library. - Real outputs only. The earlier version had no model key and marked model output "illustrative". Every model output in this course comes from a real run; scripted stand-ins remain for plumbing and attacks, and are labelled.
- One calendar. Modules 6 and 10 of the earlier version had two calendar servers with different bookings. There is now one,
examples/calendar_server.py, with the bookings in course-guide/06; Module 10 addscheck_availabilityto it. - Dates in code.
NoteStore.createstamped notes withdate.today(), so outputs changed with the day you ran them. It now takes the date as an argument. - Colab first. The earlier version ran from a terminal, with
PYTHONPATH=., servers started in a second shell andcurl. Lessons now write their files from cells, use top-levelawait, and run servers in background threads or subprocesses (course-guide/02). - Versions.
mcp2.2.0 became 2.3.0; the Inspector 2.7.0 became 2.10.1 (Node.js 22.19.0 or newer).