Homework — Lesson 12
Three tasks · add a second tool, validate at the boundary, capture log samples · ~60–90 min
Lesson 12 walked you through porting the first PKB tool (echo) to
TypeScript: a Zod schema at the boundary, a toolLogger child logger
with a start / ok / err shape, and a
stdio transport ready to be probed by the Inspector. These three homework tasks
push that pattern in three directions: (1) add a second tool
(now) so you exercise registration as well as shape;
(2) verify the input cap actually rejects oversize payloads and
write the rationale for "log the type of failure, never the value";
(3) capture six log lines in both raw-JSON and pino-pretty
forms so you can see what your operators will see when they tail your logs in
production.
Before you start
Lesson 12's lab must be done: the TypeScript sample server at
mcp-servers/sample-server/typescript/ builds with
npm run build, runs under npm run dev, and answers
echo calls from the MCP Inspector. The files
src/schemas.ts, src/tools.ts, src/logger.ts,
and src/server.ts all exist and the echo tool round-trips.
No new dependencies are required for this homework — you already have
zod and pino installed; pino-pretty
ships as a peer dep and is invoked via npx.
Task 1 — Add a second tool: now
Add a now tool to PKB-TS that returns the current UTC timestamp
as an ISO-8601 string. The point is registration mechanics: a second
tool forces you to touch ListTools, the CallTool
dispatch branch, the schemas file, and the tools file all at once —
exactly the surface that gets bigger in Lessons 13–15.
Requirements:
- A Zod input schema with no inputs:
z.object({}).strict()— the.strict()matters; without it callers can pass{ "foo": "bar" }and the schema silently accepts it. - A
toolLogger("now")child logger, with the samestart/ok/errphase shape used byecho. Includedur_mson bothokanderr. - Register in
ListToolswith a clear, one-line description (a sentence ending in a period — the Inspector renders this verbatim). - Add a dispatch branch in
CallToolthat calls your newnowTool()and wraps the return string in the standard{ content: [{ type: "text", text: ... }] }shape.
Hint: open src/tools.ts next to echoTool for the
template; the new function is shorter (no input handling beyond the empty
parse) and otherwise identical in shape.
✓ Self-check: the Inspector lists both
echo and now; calling now returns a
string matching /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
(e.g. 2026-05-25T14:32:01.123Z); the logs show one
phase=start followed by one
phase=ok dur_ms=... line per call, tagged with tool=now.
Task 2 — Reject oversize input, and write down why
The EchoInput schema from the lab caps text at
10 000 characters (z.string().max(10000)).
A cap is only useful if it actually fires — verify it does, and then
write the short rationale for the rule that comes out of this exercise:
validate at the boundary, log the type of failure, never the value.
Part A — trigger the cap. From the Inspector, call
echo with a 12 000-character string. The easiest way is to
paste "x".repeat(12000) evaluated in any JS REPL, or use a quick
one-liner:
node -e 'console.log("x".repeat(12000))' | pbcopy # macOS
node -e 'console.log("x".repeat(12000))' | xclip -sel c # Linux
Confirm two things in the response and the logs:
- The Inspector shows an error result whose message mentions
String must contain at most 10000 character(s)— the standard Zod message. You should not see your 12 000-character payload echoed back. - The matching log line is exactly
phase=err type=ZodError(plustool=echoanddur_ms). The line is short. It does not contain the payload, not even truncated.
Part B — explain the rule. Write a short paragraph (5–8 lines) in your own words answering this question:
Why is "log the type of failure" (ZodError,AuthError,TimeoutError) safer than "log the failing value so I can debug it later"?
Frame it around what happens to the log line after it leaves your process: log rotation, shipping to a centralised store, search indexes, backups, retention policy, who has read access. The boundary is where you have the safest information to log; cross it without redaction and every downstream system inherits whatever the caller pasted in.
✓ Self-check: the oversize call fails cleanly (status
is an error, no truncated output); the phase=err log line never
contains the 12 000-character payload — not even a 200-character
prefix; your paragraph is 5–8 lines and names at least two concrete
places the log line ends up after your server writes it.
Task 3 — Capture log samples in both formats
Run npm run dev once with raw JSON output and once piped through
pino-pretty, and capture six log lines in each
format. The six lines are the standard "what does a successful session look
like" trace you would paste into a runbook or an incident review:
- One
phase=ready transport=stdioat startup. - Two for an
echocall:phase=start+phase=ok. - Two for a
nowcall:phase=start+phase=ok. - One
phase=err type=ZodErrorfrom re-running Task 2's oversize call.
Run twice, redirecting the output each time. The raw run is the one your log shipper will see; the pretty run is what you read at the terminal while debugging.
# raw JSON (this is what production writes to stdout / a file)
npm run dev 2> samples-raw.log
# pretty (this is for humans at the terminal)
npm run dev 2>&1 | npx pino-pretty > samples-pretty.log
Save both files under mcp-servers/sample-server/typescript/
— do not commit them, they go in your own notes /
.gitignore'd scratch space. They exist so you can re-read them
in six months and remember what a healthy startup trace looks like.
✓ Self-check: both files exist; line counts match
(6 each); jq -c '.' < samples-raw.log succeeds line-by-line
(every line is valid JSON); each line of samples-pretty.log
starts with a level token (INFO / WARN) and is
human-readable without any ANSI escape codes leaking in — if you see
\x1b[ sequences, redirect from a non-TTY shell or pass
--colorize false to pino-pretty.