Topic 3: Server structure
22 min read·22 Sept 2026
Separating protocol handling from business logic
Look at what server.py does not contain: no file reading, no ranking, no slug rules, no frontmatter parsing. All of that is in NoteStore. Each handler in server.py does four things and nothing else: receive validated arguments, call one store method, translate store errors into MCP errors, and shape the result.
This split pays for itself three ways:
- Tests get cheaper. The 20
NoteStoreunit tests in Part 3 run in about 0.06 s with no event loop and no MCP at all. Rules such as "never overwrite a note" and "reject../in ids" are tested where they live. build_server(store)takes its dependency as an argument. This is plain dependency injection: tests hand it a store over a temporary folder, the Module Lab hands it a copy of the notes, andmain()hands it the real folder. Nothing inside the server readsNOTES_DIRor any other global.- The protocol layer can change without touching the rules. Module 7 adds authentication to
server.pyand Module 11 hardens it;store.pydoes not change in either.
| Situation | Use this | Why |
|---|---|---|
| A rule about notes (valid ids, no overwrites, ranking) | NoteStore in store.py | One place to test it; every caller (server, scripts, future CLI) gets it |
| Turning a failure into something the model can read | the handler in server.py (except InvalidNote: raise ToolError) | Error vocabulary is a protocol concern |
| Where the notes live, which port to use | config.py, read once in main() | Configuration is deployment-specific and must not leak into logic or tests |
| A dependency tests need to replace | a parameter of build_server | Tests pass their own; no monkeypatching of globals |