Lesson 20 of 30, Module 3: Make It Trustworthy. It refuses to leave the folder, asks before it overwrites, and remembers your answers.
What you can do after this lesson
Put state in a file the server owns, next to the server file and not in the chat, and a rule you give once survives every new chat. Anchor the path to the server file, because a host can start the server from a folder you cannot write to.
The problem we inherited
A server that can write can write over the wrong document, and there is no undo you can reach from a chat.
The 5 steps this lesson runs
- Write the plain Python function
- Put the @mcp line above it
- Write the one-line docstring
- Restart Claude Desktop
- Ask Claude to use it
Code in this lesson
Every block the lesson shows on screen, in the order it appears.
1. Write the plain Python function
2. Put the @mcp line above it
3. Write the one-line docstring
4. Restart Claude Desktop
5. Ask Claude to use it
HERE = os.path.dirname(os.path.abspath(__file__))
MEMORY = os.environ.get("SCRIBE_MEMORY",
os.path.join(HERE, "memory.json"))
def _notes() -> list:
if not os.path.exists(MEMORY):
return []
with open(MEMORY) as f:
return json.load(f)
def _save(notes: list) -> None:
with open(MEMORY, "w") as f:
json.dump(notes, f, indent=2)
def _trusted(name: str) -> bool:
return any(n["note"] == f"trust {name}" for n in _notes())
def remember(note: str) -> str:
notes = _notes()
notes.append({"on": str(date.today()), "note": note})
_save(notes)
return f"Remembered. Scribe now holds {len(notes)} note(s)."
def recall() -> str:
notes = _notes()
if not notes:
return "Scribe remembers nothing yet."
return "\n".join(f"{n['on']}: {n['note']}" for n in notes)
@mcp.tool()
def remember(note: str) -> str:
"""Keep one note for every future chat. A rule, or a choice."""
notes = _notes()
notes.append({"on": str(date.today()), "note": note})
_save(notes)
return f"Remembered. Scribe now holds {len(notes)} note(s)."
@mcp.tool()
def recall() -> str:
"""Every note Scribe remembers, oldest first."""
notes = _notes()
if not notes:
return "Scribe remembers nothing yet."
return "\n".join(f"{n['on']}: {n['note']}" for n in notes)
def ask_first(name: str, find: str, replace: str,
said_yes: bool, ctx: Context):
"""Ask before a write, unless you told Scribe to trust it."""
if not find:
raise ToolError("Tell me what to find. An empty find"
" would change every spot in the draft.")
if _trusted(name) or said_yes:
return Confirm(yes=True)
question = f"Change every '{find}' to '{replace}' in {name}?"
if _can_ask(ctx):
return Elicit(question, Confirm)
raise ToolError(
f"{question} This app cannot show my question, so I"
" stopped. Ask the person. If they say yes, call me"
" again with said_yes set to true.")
Use scribe to remember that my openings stay under 12 words.
What does scribe remember?
Use scribe to remember exactly these words: trust draft.md
Downloads
- Module 3 checkpoint (scribe-checkpoint-m3.zip): working code as the module leaves it, so a broken session costs you nothing
- The course GitHub repo: every checkpoint, the README and the full lesson list
Prefer to read?
The written version of this part of the course is on Substack: https://genaiunplugged.substack.com/p/give-your-ai-agents-memory-mcp-shared.
Lesson transcript
Scribe remembers, and the map
Today Scribe remembers what I told it last week. And the guard stops asking about the draft I trust.
Hello, and welcome to lesson 20 of the MCP Masterclass. Last lesson we found out why every chat starts at zero, and where the rules should live instead. Today we build that. And we start with our map, as always.
12 lit boxes. By the end of this lesson the memory lights up, right beside the guard.
The same five steps, run 4
Well, you know the list. Here it is, and it has not changed by a word.
1. Write the plain Python function
2. Put the @mcp line above it
3. Write the one-line docstring
4. Restart Claude Desktop
5. Ask Claude to use it
Write the plain Python function. Put the @mcp line above it. Write the one line docstring. Restart Claude Desktop. Ask Claude to use it.
Run number 4. I am not going to explain the steps this time. Watch the clock instead.
The memory is one file
Before the steps, 1 line near the top of the file, under the other settings.
HERE = os.path.dirname(os.path.abspath(__file__))
MEMORY = os.environ.get("SCRIBE_MEMORY",
os.path.join(HERE, "memory.json"))
The memory is a file called memory.json, and it sits next to your server. That is the whole design. No database, nothing to install, and you can open it in any text editor and read it.
The first line matters more than it looks. Here is the folder your server file lives in, and the memory goes beside it. I did not write that at first. I wrote plain memory.json, and it worked in my terminal and failed inside Claude Desktop, because Claude Desktop starts your server from a folder you cannot write to. Next to your server means next to the file, not wherever the app happened to start it.
Then 3 small helpers.
def _notes() -> list:
if not os.path.exists(MEMORY):
return []
with open(MEMORY) as f:
return json.load(f)
def _save(notes: list) -> None:
with open(MEMORY, "w") as f:
json.dump(notes, f, indent=2)
def _trusted(name: str) -> bool:
return any(n["note"] == f"trust {name}" for n in _notes())
Notes reads the file, and gives back an empty list if there is no file yet. Save writes the list back. And trusted asks 1 question of the notes. Is there a note that says, trust this draft?
Two tools in 40 seconds
Okay. The 5 steps. Step 1, the plain Python function. Two of them, because a memory you cannot read back is a diary with a lock and no key.
def remember(note: str) -> str:
notes = _notes()
notes.append({"on": str(date.today()), "note": note})
_save(notes)
return f"Remembered. Scribe now holds {len(notes)} note(s)."
def recall() -> str:
notes = _notes()
if not notes:
return "Scribe remembers nothing yet."
return "\n".join(f"{n['on']}: {n['note']}" for n in notes)
Remember takes 1 sentence, stamps today's date on it, and saves it. Recall gives every note back, oldest first.
Step 2, the @mcp line. Step 3, the docstring.
@mcp.tool()
def remember(note: str) -> str:
"""Keep one note for every future chat. A rule, or a choice."""
notes = _notes()
notes.append({"on": str(date.today()), "note": note})
_save(notes)
return f"Remembered. Scribe now holds {len(notes)} note(s)."
@mcp.tool()
def recall() -> str:
"""Every note Scribe remembers, oldest first."""
notes = _notes()
if not notes:
return "Scribe remembers nothing yet."
return "\n".join(f"{n['on']}: {n['note']}" for n in notes)
And now the guard learns to use the memory. One change inside ask first, on 1 line.
def ask_first(name: str, find: str, replace: str,
said_yes: bool, ctx: Context):
"""Ask before a write, unless you told Scribe to trust it."""
if not find:
raise ToolError("Tell me what to find. An empty find"
" would change every spot in the draft.")
if _trusted(name) or said_yes:
return Confirm(yes=True)
question = f"Change every '{find}' to '{replace}' in {name}?"
if _can_ask(ctx):
return Elicit(question, Confirm)
raise ToolError(
f"{question} This app cannot show my question, so I"
" stopped. Ask the person. If they say yes, call me"
" again with said_yes set to true.")
If the draft is trusted, ask first hands back a yes on your behalf, and no question appears, in either kind of app. Otherwise it asks, exactly as before.
So the docstring you wrote in lesson 17 just came true.
Step 4. Restart Claude Desktop. Step 5. Ask Claude to use it.
Use scribe to remember that my openings stay under 12 words.
On screen: Claude Desktop calls remember. "Remembered. Scribe now holds 1 note(s)."
Remembered. And that took about 40 seconds. No commentary needed.
Proof it survives a new chat
Now the part that matters. Close that chat. Open a brand new one.
What does scribe remember?
On screen: a new chat. Claude calls recall. "2026-09-30: my openings stay under 12 words."
Same rule, new chat. It lives in the file now, not in the conversation.
And you can prove it without Claude at all. The second script in your checkpoint is called prove_memory.py. It tells the server 1 note, stops the server, starts it again, and asks.
On screen: prove_memory.py. "Remembered. Scribe now holds 1 note(s)." Then the restart. Then the note read back, and the file itself. 3 PASS. "RESULT: Scribe remembers."
The note survives the restart, and the last thing the script does is print the file, so you can see there is no magic in it. A date, and your words.
Trust one draft
One more thing, and it is the reason the guard got 2 new lines.
Use scribe to remember exactly these words: trust draft.md
On screen: Claude calls remember with "trust draft.md". Then an edit to draft.md runs, and no question appears.
Tell Scribe to trust a draft. Say exactly these words, because the note has to match the draft's name and nothing else. From now on, edits to that 1 draft run without the question, in every app, in every chat. Every other draft still asks.
And if you change your mind, open memory.json and delete the line. It is your file.
What it cost and the map
So what did all of that cost you? Two tools and 3 helpers, about 25 lines, and the 5 steps for the fourth time. The list has still not changed by a word.
So where does our map stand at the end of this lesson?
13 boxes lit. The guard and the memory, side by side, and the guard reads the memory.
Next lesson is different. I set you an assignment, and you build 1 tool on your own.
Bye now, and I will see you in the next lesson.