run_codegen is the one mutating tool in the surface, so it got the conservative treatment on three axes: default behavior, reported scope, and protocol honesty.
dry_run=true is the signature
Calling run_codegen with no arguments writes nothing. It imports your backend, renders the TypeScript client exactly as pyrpc codegen would, compares against each configured target, and reports one status per file: up to date, would update, or would create. Only an explicit dry_run=false crosses the write boundary. An agent that wants to check type freshness never mutates a repo by accident, and a human reviewing the transcript can see the moment intent changed from reading to writing.
Scope: generated files only
The CLI's full setup also edits tsconfig.json and bundler configs to wire the @pyrpc/types alias. The MCP deliberately does not. Generated output is deterministic, idempotent, and trivially reviewable; config surgery is neither. If targets lack wiring, the tool's report says so and points at pyrpc init and pyrpc codegen. Keeping mutation narrow enough to describe in one sentence is what makes the annotation below honest rather than aspirational.
Idempotence as an annotation
The tool declares read_only_hint false, destructive_hint false, idempotent_hint true. Idempotent means running it twice leaves the same bytes, which the comparison logic guarantees: identical content short-circuits to unchanged without rewriting the file. Well-behaved clients use these hints to decide when to prompt; the spec is careful to call them hints, and our documentation repeats the warning. The actual guarantees live in defaults and scope, where they cannot be ignored.
Diff you can audit
Because dry-run comparison uses the exact renderer the writer uses, would update means byte-level drift exists, nothing fuzzier. Agents get a cheap freshness oracle, and the eventual write lands as a reviewable single-file diff per client root.

pyRPC