feat: replace the greet example with a testable notes domain - #31
Merged
Merged
Conversation
The SDK templates shipped a greet tool that returned a string. Nothing about
it was worth testing, which is a poor thing for an MCP scaffolder to teach -
a tool description is a contract with a non-deterministic caller, and an
unverified contract is the default failure.
Replaces it with a small notes server designed against the tool-design
principles, chosen so the contract has something to assert:
list_notes(tag?, limit=20) -> page + matched/returned/truncated + hint
get_note(id) -> the note, or an actionable isError
create_note(title, body, tags?) -> the note including its new id
notes://{id} -> ResourceTemplate with list implemented
summarize-notes -> prompt over a tag
Four properties the old example could not demonstrate:
- Truncation is signalled, not silently applied. The store returns the page
and the unfiltered total, so the tool can say "showing 5 of 31" and suggest
how to narrow. A store that capped internally could not report this.
- An unknown tag is distinguished from a tag with no notes. Both produce an
empty list; conflating them makes an agent report "you have no notes" when
the truth is "that tag does not exist".
- Failures come back as isError with a next move, not as thrown exceptions.
- create_note returns the id, so the agent can chain to get_note with no
intervening lookup.
Tool names carry their noun but no server namespace. Most MCP clients prepend
the server name, so a "notes_" prefix would surface as "my-server_notes_list";
a bare verb would collide with every other server's "list" on the clients that
do not prefix. "list_notes" reads correctly either way.
Splits the primitives into tools.ts, prompts.ts and resources.ts, each
exporting a register function that server.ts calls. server.ts is now just the
composition root. One file per primitive keeps each small enough to read at a
glance and gives the next change an obvious home; the single-file layout only
held while the example was a one-line greet.
Domain logic goes in a new notes-store.ts, free of MCP imports. Two reasons
beyond testability: server.ts is re-exported by stateful/ and stdio/, so
anything added there grows in three places; and createMcpHandler runs the
factory once per request, so state held on the server instance is discarded
between calls and a created note would never survive to a later list. Keeping
the notes at module scope is what makes the example work over HTTP at all.
ResourceTemplate is constructed with an explicit list: the v2 type declares
that key as required rather than optional, so omitting it fails tsc with
TS2741 in the generated project.
FastMCP keeps its existing example for now.
IBJunior
force-pushed
the
feat/notes-example
branch
from
September 19, 2026 12:41
2b2fc04 to
4c24a38
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the
greetexample in the SDK templates with a small notes domain built to be worth testing, and splits the generatedserver.tsinto one file per MCP primitive.Third of the sequence. This is the groundwork for the next PR, which adds the generated test suite — wiring vitest against
greetand then deletinggreetwould have doubled the review.Why
The old example returned a string from a tool called
greet. There was nothing in it to assert, which is a poor thing for an MCP scaffolder to teach: a tool description is a contract with a non-deterministic caller, so an unverified contract is the default failure mode.The example
Four properties
greetcould not demonstrate, each one an assertion the next PR can make:isErrorwith a next move, not thrown exceptions.create_notereturns the id, so the agent chains straight toget_notewith no intervening lookup.Naming: no server namespace
Tool names carry their noun but no prefix. Most MCP clients prepend the server name, so
notes_listwould surface asmy-server_notes_list; a bare verb likelistwould collide with every other server'sliston the clients that don't prefix.list_notesreads correctly either way. Confirmed against the SDK that the protocol carries the bare name — prefixing is the client's job.Structure
server.tsis now just the composition root:One file per primitive keeps each small enough to read at a glance and gives the next change an obvious home. The single-file layout only held while the example was a one-line
greet.notes-store.tsstays free of MCP imports, for two reasons beyond testability:server.tsis re-exported bystateful/andstdio/, so anything added there grows in three places.createMcpHandlerruns the factory once per request. State held on the server instance is discarded between calls, so a created note would never survive to a later list. Keeping notes at module scope is what makes the example work over HTTP at all — verified below.ResourceTemplateis constructed with an explicitlist. The v2 type declares that key as required rather than optional, so omitting it failstscwith TS2741 in the generated project. Implementing it also makesresources/listenumerate the notes.Verification
All three SDK variants generated, installed and compiled on TS 7 (
tscexit 0), each emittingserver.ts,tools.ts,prompts.ts,resources.tsandnotes-store.ts.Behaviour driven through a real client over
InMemoryTransport:The per-request state invariant, over real HTTP: a note created in one request is visible in a second, separate request (
"matched": 1, with the note returned). That is the check that would have caught the factory bug.stdio: stdout carries 2 lines, both valid JSON-RPC, 0 non-JSON;
MCP Server running on stdioon stderr.npm test— 410 tests across 16 files (up from 380)npm run lintclean; new files passprettier --checkTest changes
Three files asserted on
greet/greeting-resourcethrough the re-export shim and were updated. Rather than restate string presence, the stateless assertions now pin design guarantees — truncation is signalled, unknown tags are distinguishable, errors areisError, no server namespace is hardcoded into tool names, andResourceTemplategets itslist. Those assertions moved to newgetToolsTemplate/getPromptsTemplate/getResourcesTemplateblocks, withserver.tsnow asserted to compose rather than register. Thestateful/andstdio/tests only check the composition arrives via re-export.FastMCP keeps its existing example for now; it has no test setup yet either. Worth a tracked follow-up so the gap does not go quiet.