Reviewing a change to Jupyter MCP Server
This server is what an agent talks to when it reads and runs somebody's notebooks. Most of what goes wrong here is quiet: a test that passes without exercising its subject, a tool that answers an empty list instead of an error, a page that documents a flag nothing reads. None of it fails loudly, and all of it has happened.
Review for those first. Ordinary bugs are usually caught by the suite.
Start here: does the test exercise its subject?
The most common defect in this repository is not broken code — it is a test that passes for a reason other than the one it claims. Two real examples:
test_an_expired_task_is_gone…calledstore.get()beforestore.list().get()deletes the expired record itself, so by the timelist()ran there was nothing left to sweep and the sweep never executed.list()was iterating a dictionary while popping from it — a guaranteedRuntimeError— and the test named after that path was green.tests/test_mcpb_version_sync.pynever ran at all. It importsscripts.sync_mcpb_version,scripts/had no__init__.py, and the module failed at collection. The suite was green and the version drift it guards was unguarded.
So for every new or changed test, ask:
- Would it fail if the behaviour were removed? If you cannot say yes, delete the assertion or strengthen it. Say so in the review.
- Does an earlier line already do the work? A setup call that cleans up, caches, or short-circuits leaves the code under test unreached.
- Does it assert a cell, or a substring of everything?
assert "no" in outputpasses on almost any English. Pull the row or the field apart. - Does it signal by raising inside a
try? Several functions here catchExceptiondeliberately — a test that raises to signal gets swallowed and asserts nothing. Record into a list and assert the list. - Does it set an environment variable that was already read at import?
config-style modules read once. Patch the attribute, not the environment.
The quiet failure
The pattern to hunt for: a well-formed request, a truthful empty answer, and a legitimate-looking zero. Nobody investigates a zero.
- A tool that answers
[]where it could not reach the thing it was listing. An empty list is what a user with no notebooks looks like. - An
exceptthat returns a default instead of raising. Ask what the caller will render, and whether they could tell that answer from a real one. - A metric or a field that stops being written. A panel showing zero reads the same as a healthy system.
- Retention, sweeps and background jobs. "It ran and found nothing" and "it stopped running" must not look the same in a log.
An error the caller can act on beats an empty answer they will believe.
Result shape and cache hints
Every tool answers through @structured in jupyter_mcp_server/results.py.
Check:
- The tool annotates its return type (
ToolAnswer,TableAnswer,OutputsAnswer). Returning nothing advertises no output schema, and the generated reference silently loses its Output section. shape=matches what the body returns. A mapping is already the structured answer; rendering it underresulthands a client text where it had an object, and nothing looks wrong until a field is read.- A cache hint needs a listing or an ETag.
ttl_mson a read that cannot be revalidated hands an agent a stale notebook and no way to tell. Nothing that changes a notebook may be hinted at all.tests/test_results.pyenforces this — if a change edits that test, ask why. cacheScopestaysprivateunless the answer genuinely is the same for every caller. A shared cache holding one person's notebooks for another is the failure the field exists to prevent.
Contracts that must not drift
These tests exist because nothing else connects the two ends, and a mismatch fails silently:
tests/test_documented_features.py— the docs pages against the code. A page naming an environment variable nothing reads, a method that does not exist, or a store method the protocol does not have sends somebody to a feature that silently does not turn on. When a change adds a config variable, a public helper or a protocol method, the page and this test move with it.tests/test_conformance.pywithtests/conformance-baseline.yaml— the specification's own suite. A new failure blocks the release. A baseline entry is a debt with a name; adding one needs a reason in the review.
Extensions
- Extensions register in name order, and the SDK keeps the original when a tool name is registered twice. An extension that means to replace a tool must remove it first, or the replacement silently does nothing.
- A new extension needs its entry point in
pyproject.tomlunderjupyter_mcp_server.extensions, and its tests underextensions/<name>/tests. - The extension identifier belongs in one constant. When a feature moves into the core protocol, that should be a rename in one place.
Protocol and transport
- The Streamable HTTP transport is stateless by default and issues no
Mcp-Session-Id. That is right for one process many people reach — each request runs in its own context and the identity middleware sees the caller of that request. It is wrong where one process serves one user.JUPYTER_MCP_STATEFULchooses; a change that hard-codes either is a regression for the other. _metakeys are the protocol's (io.modelcontextprotocol/*) or this project's namespace. Never invent an unnamespaced one.- Identity comes from the request, not from the process. A tool that reaches for a credential in the environment works in one deployment and acts as the wrong person in another.
Style this repository keeps
- No backwards compatibility shims. A rename replaces; there is no alias, no deprecation period, no migration path. If a change adds one, ask.
- Comments say why, not what. A comment restating the line below it is noise; one explaining a decision that looks wrong is the point.
- No dead defence. A guard that cannot fire reads as a guarantee and is worse than nothing. If a check is unreachable, remove it and put the reasoning where the decision actually is.
noqaonly for rules this repo enables.ruffflags the rest as unused, and anoqacarrying prose hides its own explanation. Put the reasoning in an ordinary comment.
Before approving
make test-mcp-server # ~15 minutes; do not cancel
make test-extensions # seconds
.github/workflows/lint.sh # ruff, mypy, mdformat, pyproject validation
A change that touches a documented feature also runs
pytest tests/test_documented_features.py, and one that touches the wire
runs pytest tests/test_conformance.py.
Writing the review
Say what breaks and for whom. "This returns an empty list when Spacer is unreachable, so an agent will tell the user they have no notebooks" is worth more than "consider adding error handling".
Rank by what a reader would do about it. A confirmed defect first, a weakened test second, style last — and if the only findings are style, say the change looks correct before listing them.