Lesson 12 The TypeScript MCP SDK
Theme:

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:

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:

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:

  1. One phase=ready transport=stdio at startup.
  2. Two for an echo call: phase=start + phase=ok.
  3. Two for a now call: phase=start + phase=ok.
  4. One phase=err type=ZodError from 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.

View solutions →