Homework — Lesson 13
Three tasks · healthcheck, Streamable HTTP probe, image-size experiment · ~60–90 min
Lesson 13 turned PKB-TS into a Docker-deployable HTTP+SSE server sitting behind
Caddy. The lab walked you through the PKB_TRANSPORT branch in
server.ts, the multi-stage Dockerfile, the
.dockerignore that keeps the build context small, and the
six-line Caddyfile that terminates TLS in front of the Node
process. These three homework tasks push the same surface in three
operational directions: (1) make the compose stack
self-aware with a Docker healthcheck so Caddy waits for PKB to be ready;
(2) read the SDK source for the newer Streamable HTTP
transport and write a short comparison — no code change, just a
reading-the-source exercise; (3) run three Dockerfile
variants and record image sizes so you have real numbers when you defend
the multi-stage / Alpine choice in a code review.
Before you start
Lesson 13's lab must be done end-to-end: docker compose up
from mcp-servers/sample-server/typescript/ brings up
pkb + caddy, the Inspector connects over the
Caddy hostname (HTTPS on the staging issuer), and
curl -sk https://<your-hostname>/healthz returns
{"ok":true}. You need a working Docker daemon (Docker
Desktop on macOS, Docker Engine on Linux) and roughly 2 GB of free
disk for Task 3's three image variants. No new npm dependencies are
required.
Task 1 — Add a Docker healthcheck
Extend docker-compose.yml so the pkb service has
a healthcheck that hits /healthz every 10 s with a
2 s timeout and 3 retries, and make caddy wait for
pkb to be healthy before starting. Without this,
Caddy can come up first, fail its upstream dial, and either crash-loop or
cache a stale 502 — you have seen both in the wild.
Requirements:
- The healthcheck command must not assume
curlis installed — the runtime image isnode:20-alpine, which ships neithercurlnorwget. Use Node 18+'s built-infetchvianode -e "..."instead. interval: 10s,timeout: 2s,retries: 3. Astart_periodof5savoids spurious "unhealthy" during cold start.- Replace the
depends_on: [pkb]short form with the long-formdepends_on.pkb.condition: service_healthyundercaddy. The short form only waits for started, not healthy.
Bring the stack up clean with docker compose down && docker compose up
and watch the order: pkb should reach
(healthy) first, then Caddy starts.
✓ Self-check: docker compose up
logs show pkb-pkb-1 reach (healthy)
before Caddy's first log line; docker inspect
pkb-pkb-1 --format='{{.State.Health.Status}}' prints
healthy; if you kill the Node process inside the container
(docker exec pkb-pkb-1 kill 1 — PID 1 is Node),
the status flips to unhealthy within ~30 s and Compose
restarts it (because of restart: unless-stopped).
Task 2 — Streamable HTTP probe (reading exercise)
The MCP spec has been moving away from the two-endpoint SSE design toward a single-endpoint Streamable HTTP transport. PKB-TS uses SSE today because it is the transport L13 introduces; the newer one is what Cloudflare Workers, Vercel Functions, and Deno Deploy actually prefer. Do not migrate the code. This is a reading-the-source exercise — the deliverable is a short summary.
Open the SDK file directly from your node_modules:
code node_modules/@modelcontextprotocol/sdk/server/streamableHttp.js
# or, if you only have less:
less node_modules/@modelcontextprotocol/sdk/server/streamableHttp.js
Read the StreamableHTTPServerTransport class top to bottom
(it is short; the .d.ts next to it is the cleanest
reference for the public surface). Then write a
12–20-line summary in your own words that
answers all three of:
- Wire shape. How does Streamable HTTP differ from
SSE on the wire? SSE uses a long-lived
GET /ssefor server-to-client plus a separatePOST /messagesfor client-to-server. Streamable HTTP collapses those into one endpoint — describe how. - Why edge runtimes prefer it. Cloudflare Workers,
Vercel Functions, and Deno Deploy all have short
per-request CPU/connection budgets and limited concurrent
open streams per worker. Name one concrete reason a long-lived SSE
GETis hostile to that model, and one reason chunked HTTP is friendlier. - Migration step. Name one concrete change
you would have to make in PKB-TS's
server.tsto swapSSEServerTransportforStreamableHTTPServerTransport. (One bullet is enough — you are not implementing this.)
The summary must cite at least one specific SDK identifier (a class name, function name, or HTTP route) by name — this is how the self-check distinguishes "actually read it" from "summarised a blog post".
✓ Self-check: your summary is 12–20 lines;
it names at least one identifier from the SDK source verbatim (e.g.,
StreamableHTTPServerTransport,
handleRequest, or the exact HTTP method + path used);
it does not describe the transport as "WebSockets" — it
is plain HTTP chunked transfer, and saying otherwise means you read a
summary, not the source.
Task 3 — Image-size reduction experiment
The Lesson 13 Dockerfile is multi-stage with a
node:20-alpine runtime. That is the recommended
configuration, but "recommended" is more convincing when you have
measured the alternatives. Build three variants, record the resulting
image sizes, and write the comparison as a markdown table.
Variants:
- Baseline. As-is — multi-stage with
node:20-alpineruntime. Tag itpkb-ts:alpine-multi. - Single-stage on full
node:20. One stage, noFROM ... AS build/FROM ... AS runtimesplit — build and run live in the same image. Tag itpkb-ts:node-single. Expect roughly 3× the baseline. - Multi-stage with
node:20-bookworm-slimruntime. Same multi-stage shape as the baseline, but swap the runtime base from Alpine to Debian Slim. Tag itpkb-ts:slim-multi. Expect roughly +30% over the Alpine baseline.
To compare cold-build times fairly, prune Docker's build cache between variants:
docker builder prune -af
docker build -t pkb-ts:alpine-multi -f Dockerfile .
docker builder prune -af
docker build -t pkb-ts:node-single -f Dockerfile.single .
docker builder prune -af
docker build -t pkb-ts:slim-multi -f Dockerfile.slim .
docker images | grep pkb-ts
Record the output as a 3-row markdown table with columns
Variant, Final image size, and Build time
(cold). Use the docker images "SIZE" column for the
first number and a wall-clock time docker build ... for
the second.
✓ Self-check: all three images build
successfully and run (docker run --rm -e PKB_TRANSPORT=stdio
pkb-ts:<tag> answers an Inspector connect); the alpine
multi-stage variant is the smallest; the
single-stage variant on full node:20 is the
largest; if the slim variant comes out smaller than
Alpine, double-check you actually swapped the runtime base
and not the build base.