The plan

A notes server. Three tools and one resource, in one file, in about sixty lines.

pip install mcp

The server

Save as notes_server.py:

import json
from pathlib import Path

from mcp.server import MCPServer

mcp = MCPServer("notes")
NOTES = Path("notes.json")


def load():
    return json.loads(NOTES.read_text()) if NOTES.exists() else []


def save(notes):
    NOTES.write_text(json.dumps(notes, indent=2))


@mcp.tool()
def add_note(text: str) -> str:
    """Add a note to the notebook.

    Use this when the user wants something written down for later.

    Args:
        text: The note to save.
    """
    notes = load()
    notes.append(text)
    save(notes)
    return f"Added note {len(notes)}: {text}"


@mcp.tool()
def list_notes() -> str:
    """List every note in the notebook, numbered.

    Use this when the user asks what has been written down.
    """
    notes = load()
    if not notes:
        return "The notebook is empty."
    return "\n".join(f"{i}. {n}" for i, n in enumerate(notes, 1))


@mcp.tool()
def delete_note(number: int) -> str:
    """Delete one note by its number.

    Use this only when the user explicitly asks to remove a note.

    Args:
        number: The note's number, as shown by list_notes.
    """
    notes = load()
    if not 1 <= number <= len(notes):
        return f"There is no note {number}. There are {len(notes)} notes."
    removed = notes.pop(number - 1)
    save(notes)
    return f"Deleted: {removed}"


@mcp.resource("notes://all")
def all_notes() -> str:
    """The full contents of the notebook."""
    return "\n".join(load()) or "(empty)"


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

Read what you just wrote

Notice how little of this is MCP.

Three functions with docstrings and type hints. The decorator turns the type hints into
inputSchema and the docstring into description — the same two jobs you did by hand in
Module 2, done by a library.

And notice that Lessons 2.2 and 2.3 still apply in full. "Use this when the user wants
something written down for later"
is a trigger sentence. "Use this only when the user
explicitly asks to remove a note"
is a boundary on a destructive action. The library writes
the schema. It does not write your descriptions, and descriptions are still where failures
live.

Talk to it by hand

Before connecting an agent, drive it yourself. This is the payoff for Lesson 5.2:

python3 notes_server.py

It waits on stdin. Paste this and press Enter:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"me","version":"1"}}}

You get a result back. Then:

{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}

There is your menu, with the schemas generated from your type hints. Then call one:

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add_note","arguments":{"text":"Call the plumber"}}}

Check notes.json. Your note is there.

You just used an MCP server with no model, no agent, and no client library. That is worth
sitting with — it is the clearest possible demonstration that MCP is a plain protocol and not
an AI thing.

Try this before the next lesson

Add a search_notes tool. Write the docstring before the code, and give it a trigger
sentence.

Then check it appears in tools/list by hand. Getting used to inspecting the menu directly
will save you an hour the first time a real server misbehaves.