# Changelog

## perfetto-mcp-rs 0.x Changes

### [0.14.3](https://github.com/tooluse-labs/perfetto-mcp-rs/releases/tag/v0.14.3) (May 22, 2026)

- **Rust 1.95 / Clippy compatibility fix.** Replaced a `3.14` floating-point test literal in the lenient f64 parameter parser tests with a non-special sample value so `clippy::approx_constant` no longer fails `cargo clippy --all-targets --all-features -- -D warnings`.

### [0.14.2](https://github.com/tooluse-labs/perfetto-mcp-rs/releases/tag/v0.14.2) (May 22, 2026)

- **Download retry with exponential backoff.** Transient failures (HTTP 5xx, 429, connect/timeout errors, mid-stream I/O errors) now retry up to 2 times with 1s/2s delays. URLs are redacted in error messages to prevent credential leaks in logs.
- **Module split.** Extracted `params`, `sql_templates`, `stdlib_catalog` out of `server.rs` (2672 → 1971 lines). Pure mechanical extraction — zero semantic changes.

### [0.14.0](https://github.com/tooluse-labs/perfetto-mcp-rs/releases/tag/v0.14.0) (May 14, 2026)

- **Renamed MCP server registration from `perfetto-mcp-rs` to `perfetto-rs`.** The `-mcp` suffix is redundant inside `/mcp` UI listings (Claude Code already shows entries under a "Manage MCP servers" header — repeating "mcp" in every server name just clutters the column). The new value is what gets passed to `claude mcp add` / `codex mcp add` and what surfaces in `claude mcp list`. **Crate name, binary file name, GitHub repo, release asset names, the local cache directory (`dirs::data_local_dir()/perfetto-mcp-rs`), and the Glama catalog identifier all remain `perfetto-mcp-rs`** — changing those would invalidate user caches (~35MB trace_processor_shell re-download), break `cargo install` / `brew install` lookups, and unlist us from public MCP catalogs. Only the runtime MCP-registration identifier moved.
- **Zero-touch migration from v0.13.x.** Both `install` and `uninstall` subcommands run a best-effort `claude/codex mcp remove perfetto-mcp-rs` before touching the new name, so upgrading users don't end up with a stale `perfetto-mcp-rs` orphan pointing at a re-pointed or missing binary. Errors from the legacy-name remove are intentionally swallowed: the "not found" benign case is dominant on fresh installs, and any real config-corruption error gets re-surfaced by the new-name remove that follows (driving the user-facing outcome). Covered at all four entry points: `register_claude_via_cli` / `register_codex_via_cli` / `deregister_claude_via_cli` / `deregister_codex`. **No manual user action needed** on the `curl|sh`, `cargo install + perfetto-mcp-rs install`, or `brew install + perfetto-mcp-rs install` paths.
- **Manual fallback bodies carry the migration hint too.** When Claude / Codex CLI isn't on PATH, the binary falls back to printing paste-ready config blocks (`Outcome::Manual`). Those hint bodies now name the legacy `perfetto-mcp-rs` registration / `[mcp_servers.perfetto-mcp-rs]` table explicitly so a CLI-less upgrader can hand-clean the orphan during the same edit. Symmetric across all four cells: `claude_manual_install_body` / `claude_manual_uninstall_body` / `codex_manual_install_body` / `codex_manual_uninstall_body`. The two codex bodies were extracted from inline `format!` blocks into dedicated helpers so they're directly unit-testable — matching the existing claude side.
- **Tests**: 4 new manual-body regression locks (one per `*_manual_*_body` helper) hard-assert the legacy-name mention so a future copy-cleanup pass can't silently drop it. 2 new integration tests (`install_cleans_legacy_perfetto_mcp_rs_registration_before_new_name`, `uninstall_cleans_legacy_perfetto_mcp_rs_registration_before_new_name`) pin both the presence AND the ordering of the legacy-remove call relative to the new-name remove (via `.position()` comparison), across both claude and codex. Existing `install_from_clean_state_always_remove_then_add` and `install_with_existing_entry_removes_then_adds` recorded-call sequences expanded from 4 steps to 6 (2 legacy removes + 2 new-name removes + 2 new-name adds).
- **Known edge — cross-scope upgrade.** A v0.13.x user who reinstalls under a different scope than originally used (e.g. v0.13 `--scope user` → v0.14 `--scope local`) will leave the v0.13 user-scope orphan untouched because the legacy cleanup only targets the current install's scope. Cross-scope cleanup would have to blindly sweep all three scopes (user/local/project), which would risk deleting intentional multi-scope setups. Run `claude mcp remove perfetto-mcp-rs --scope user` manually if affected. Same-scope upgrades (the dominant case) need no manual step.
- **`LEGACY_SERVER_NAME` is a one-time transition shim, not permanent compat.** The const + its register/deregister callsites + the manual-body legacy mentions + the pinned regression tests should be dropped once cross-rename upgrades are no longer common in the wild. See its doc comment for the full cleanup checklist — legacy-name string literals also appear in `tests/install_subcommand.rs` recorded-call sequences and in copy text, which don't flow through the const, so a final `grep perfetto-mcp-rs` sweep is needed when retiring the shim.

### [0.13.4](https://github.com/tooluse-labs/perfetto-mcp-rs/releases/tag/v0.13.4) (May 6, 2026)

- **Silent output for clients that aren't installed.** v0.13.3 and earlier emitted a `==> Codex skipped: codex not installed` line for every probed client whose CLI / app / config dir was absent. Users who don't run a given client (e.g. Codex on a Claude-only setup) saw this as noise — telling them about a tool they don't use, every install / uninstall. New `Outcome::Absent` variant takes over the "client not installed at all" cases (`register_claude` / `register_codex` / `register_qoder` and their deregister counterparts) and produces zero output. The wrapper still proceeds normally (exit 0); the user just sees clean output for the clients that ARE installed.
- **`Outcome::Skipped` semantics tightened**: now reserved for "user explicitly opted out" cases (`--skip-claude`, `--skip-codex`, `--skip-qoder`, `--keep-cache`) and benign uninstall conditions (`no user-scoped registration to remove`, `cache root unavailable`). Those still echo so users see their flags acknowledged. `Outcome::Manual` (client present but needs paste action) is unchanged. `Outcome::Failed` (real error) still prints to stderr.
- **Test contract update**: `install_cli_missing_skips_gracefully` now asserts the `==> Claude` / `==> Codex` lines do NOT appear when those CLIs aren't on PATH (the inverse of the old test, which asserted they did appear). New unit test `aggregate_treats_absent_as_silent_success` pins the silent-on-Absent + exit-0 contract so a future regression flipping Absent back to a printing variant is caught at compile time.
- **No code-path changes** beyond the output filtering. Detection logic, install/uninstall flow, env var passthrough, and Manual-outcome semantics carry forward from v0.13.3 unchanged.

### [0.13.3](https://github.com/tooluse-labs/perfetto-mcp-rs/releases/tag/v0.13.3) (May 6, 2026)

- **`install.sh` no longer hits the GitHub anonymous API on the latest path.** v0.13.2 and earlier resolved `VERSION=latest` through `api.github.com/repos/.../releases/latest`, then aborted with `could not resolve release tag` on rate-limit (60 req/hour per IP) or transient network failure — even though the API call wasn't strictly required to download the asset. install.ps1 has used the `releases/latest/download/<asset>` redirect (no API call) since day one, with a comment specifically calling this out. install.sh now mirrors that behavior: download URL goes through the redirect; the API call survives only as a best-effort version-display lookup with `|| true`, so a rate-limit/network blip degrades the install message from `==> Installing perfetto-mcp-rs v0.13.3 (darwin-arm64)` to `==> Installing perfetto-mcp-rs latest (darwin-arm64)` instead of aborting. Pinned-version path unchanged.
- **`claude mcp add` invocation now uses the `--` separator before the binary path.** `claude mcp add --help` documents `--` as the form for stdio-server-with-args registrations ("Add stdio server with subprocess flags: `claude mcp add my-server -- my-command --some-flag arg1`"). Our own paste-ready hint in `claude_manual_install_body` already emits `--`, and the codex `mcp add` invocation already does too — the actual claude invocation was the lone holdout. Pure inconsistency fix; aligns three previously divergent call sites. Practical risk was theoretical (binary paths land at `~/.local/bin/...`, not paths starting with `-`), but adopting the documented form prevents the wpa-mcp-style "argparse ate my path" failure mode if a future user puts the binary somewhere with a leading `-`.
- **Two integration tests** (`tests/install_subcommand.rs:178, 204`) updated to assert on the new `claude|mcp add perfetto-mcp-rs --scope user -- {bin}` recorded-call format. clippy + fmt clean. CI matrix runs unchanged.
- **No code-path changes** beyond the install/uninstall fix-ups above. Everything else from v0.13.2 (multi-client install fallbacks, blocking uninstall semantics, SKIP env-var passthrough, brand assets) carries forward unchanged.

### [0.13.2](https://github.com/tooluse-labs/perfetto-mcp-rs/releases/tag/v0.13.2) (May 6, 2026)

- **Multi-client install/uninstall fallbacks for MCP clients without first-class CLI control.** `perfetto-mcp-rs install` previously only wired up Claude Code (`claude mcp add`) and Codex (`codex mcp add`); if either CLI wasn't on PATH it silently skipped, leaving users with a freshly-downloaded binary that no client knew about. v0.13.2 adds a paste-ready-snippet fallback path for three additional surfaces, gated on positive install signals so users without a given client see no noise:
  - **Qoder** — detects via `which qoder`, `/Applications/Qoder.app` (macOS), and `%LOCALAPPDATA%\Programs\Qoder` (Windows); emits a `{"mcpServers": {"perfetto-mcp-rs": ...}}` JSON snippet for the user to paste into Qoder Settings → MCP → + Add. Qoder's MCP config is UI-only with no documented disk path, so we never claim to manage it.
  - **Codex desktop app / `~/.codex/` config dir** — when the CLI is hidden (the `curl|sh` subshell case where login-shell-only PATH additions don't propagate, or when only the Mac app is installed), emits a TOML snippet for `~/.codex/config.toml`. All Codex surfaces (CLI, VS Code ext, desktop app) read this same file.
  - **Claude Desktop / Claude Code path-hidden** — detects Claude Desktop via `/Applications/Claude.app` and the per-OS data dir (`dirs::config_dir().join("Claude")`); detects Claude Code via `~/.claude.json` / `~/.claude/`. Emits a Manual outcome with one or both sections depending on which products are present: a JSON snippet for `claude_desktop_config.json` and/or a paste-ready `claude mcp add ... --scope <user-requested-scope>` line. Desktop fallback is gated to `--scope user` because Local/Project scopes are Claude Code-only concepts without a Desktop analog.
- **Uninstall correctness: blocking Manual outcomes prevent dangling MCP entries.** When the binary's `uninstall` subcommand can't programmatically remove an entry but the client *does* have a documented config path (Codex, Claude Desktop), the new `Outcome::Manual { blocking: true }` variant exits non-zero so `uninstall.sh:70` and `uninstall.ps1` keep the binary in place. Without this, the wrapper would happily delete the binary while the client config still pointed at the deleted path — silently breaking MCP integration on the next client launch. Qoder uninstall stays `blocking: false` (informational-only) because its config path is undocumented; blocking would just trap the user with no concrete file to edit.
- **`SKIP_CLAUDE` / `SKIP_CODEX` env-var passthrough on the uninstall wrappers.** After manually cleaning up a config entry (per the printed Manual hint), users finish uninstall with `SKIP_CLAUDE=1 curl ... | sh` (or `$env:SKIP_CLAUDE='1'; irm ... | iex`). Pattern parallels the existing `SCOPE` env. No `SKIP_QODER` passthrough — Qoder uninstall doesn't block, so it isn't needed.
- **Quick install README reorg.** The `curl ... | sh` / `irm ... | iex` one-line script is now the recommended path on every platform, since it's the only path that auto-registers with Claude Code and Codex. Homebrew and `cargo install` move under "alternatives" with their manual `perfetto-mcp-rs install --binary-path` follow-up step explicitly shown. zh-CN README also gets a language polish pass — phrasings like "顺手自动注册" and "沿着错误消息的提示串起" read like machine translation; rewritten to flow as native Chinese without changing technical content.
- **`list_tables` description: SQLite engine fact anchor.** Real-world v0.13.1 session showed an LLM trying `SELECT name FROM perfetto_tables WHERE ...`, assuming `trace_processor` exposes a Postgres/BigQuery-style catalog. It doesn't — the engine is SQLite, schema introspection runs through this MCP tool (or `sqlite_master`). The "Use when:" section now anchors the underlying-engine fact so the LLM has ground truth before reaching for catalog SQL.
- **Brand refresh.** Lighter wordmark, refined trace/MCP mark for better small-size legibility, refreshed social preview (PNG + SVG resynced at 1280×640).
- **Tests**: 2 new aggregate-policy tests pin `Outcome::Manual { blocking }` semantics; 4 new claude-manual-body tests cover Desktop-only, CLI-only, both-detected, and scope-propagation paths; 2 new Qoder-outcome tests hard-assert non-blocking by construction (avoiding the vacuous-pass trap of going through `deregister_qoder` on machines without Qoder); JSON/TOML snippet builders pinned for Windows backslash escaping. clippy + fmt clean. CI matrix runs on 3 OSes; local Windows AppLocker still blocks new test-binary execs but unit tests verified in CI.
- **No breaking changes** to MCP tool schemas, install/uninstall CLI surface (existing `--scope` / `--skip-claude` / `--skip-codex` continue to work; new `--skip-qoder` is additive), or wrapper interface (existing `SCOPE` / `INSTALL_DIR` env vars unchanged; new `SKIP_*` env vars are additive). Existing v0.13.1 install + load + query flows continue without any caller-side changes.

### [0.13.1](https://github.com/tooluse-labs/perfetto-mcp-rs/releases/tag/v0.13.1) (May 1, 2026)

- **Tool descriptions optimized for Glama's [Tool Definition Quality Score (TDQS)](https://glama.ai/blog/2026-04-03-tool-definition-quality-score-tdqs).** Glama's per-tool breakdown after v0.13.0 surfaced two systematic dimension gaps. Both fixed in this release: (1) **Behavior** — 5 tools (`execute_sql`, `list_processes`, `chrome_page_load_summary`, `chrome_scroll_jank_summary`, `chrome_startup_summary`) now declare "Read-only" explicitly so the LLM knows the side-effect surface up-front; (2) **Parameters** — 5 zero-param tools (the four `chrome_*_summary` plus `chrome_web_content_interactions` and `list_processes`) now state "Parameters: none — operates on the loaded trace." rather than omitting the section. Estimated TDQS impact: server average 4.78/5.0 → ~4.95/5.0, with most tools moving to 5.0/5.0. Will materialize on Glama at next Make Release.
- **`list_tables` rewrite (4.5 → ~5.0).** The lone TDQS outlier — losing 1 point each on Behavior, Conciseness, Completeness, and Parameters dims simultaneously. Tightened from ~120 to ~95 words while adding the "Read-only" declaration, an "Errors when: no trace is loaded — call `load_trace` first" section, more compact GLOB-pattern semantics in Parameters, and the "Don't use for" rephrased to lead with the alternative (`execute_sql` + `INCLUDE PERFETTO MODULE`). The v0.11.2-anchored anti-injection guard preserved verbatim ("execute_sql" + "separate MCP tool" still in the description, covered by `schema_discovery_tools_warn_against_execute_sql_misuse`).
- **`list_table_structure` Parameters dim 4→5.** `table_name` parameter now spells out type (`string`), value source (the `list_tables` output), case-sensitivity, and the v0.11.3+ `name` alias. Was the only single-param tool losing the Parameters dim; now matches the depth of the `chrome_main_thread_hotspots` Parameters block.
- **`load_trace` Completeness dim 4→5.** Added an "Errors when:" section covering the three real failure modes: file doesn't exist, file isn't a valid Perfetto trace (corrupt or version mismatch), and the first-run `trace_processor_shell` download failure from the Perfetto LUCI bucket.
- **Trace-file format framing broadened.** Earlier README, `glama.yaml`, and `load_trace` tool description said `.perfetto-trace / .pftrace` only — accidentally implying narrower support than reality. Perfetto's `trace_processor` content-sniffs and accepts `.bin` (raw protobuf — `adb shell perfetto -o /data/misc/perfetto-traces/foo.bin` emits this), `.json`, `.json.gz`, or any extension. Updated README + zh-CN main pitch and Tools table; `glama.yaml` server description and `load_trace` tool description; `src/server.rs` `LoadTraceParams` field doc, `STDLIB_INSTRUCTIONS` const, and `load_trace` `#[tool(...)]` description (visible to LLMs at `tools/list`). Fixture paths and concrete example file paths in tests left untouched — those are real specific files, not framing-of-support claims.
- **`release.yml` Homebrew tap auth: PAT → GitHub App.** Replaces the v0.12.x `HOMEBREW_TAP_TOKEN` fine-grained PAT (capped at 366 days, forcing yearly rotation) with `actions/create-github-app-token@v1` minting a short-lived installation token per workflow run. The `tooluse-labs-tap-bot` GitHub App (App ID `3560724`) holds Contents:Write scope on `tooluse-labs/homebrew-tap` only; private key + App ID stored as `HOMEBREW_TAP_APP_KEY` / `HOMEBREW_TAP_APP_ID` repo secrets. Same "install once, never touch" pattern as the publish-crate job's crates.io Trusted Publishing. **v0.13.1 is the first release that actually exercises the new App auth in CI** (v0.13.0's tap formula was hand-pushed externally).
- **Tests**: 169 lib tests pass unchanged (pure description-text changes, no logic touched). clippy + fmt clean. CI matrix runs on 3 OSes; local Windows AppLocker still blocks new test-binary execs but the change is description-only so the risk is minimal.
- **No code-path or schema changes.** `tools/list` still advertises the same 12 tools with the same input schemas; only the human-readable `description` strings change. Existing v0.13.0 install + load + query flows continue to work without any caller-side changes.

### [0.13.0](https://github.com/tooluse-labs/perfetto-mcp-rs/releases/tag/v0.13.0) (April 30, 2026)

- **Repository moved from `0xZOne/perfetto-mcp-rs` to `tooluse-labs/perfetto-mcp-rs`.** Same with the Homebrew tap (`0xZOne/homebrew-tap` → `tooluse-labs/homebrew-tap`). The `tooluse-labs` org now hosts perfetto-mcp-rs alongside forthcoming sibling tools (e.g. wpa-mcp-net for Windows ETL traces) — keeping the AI-agent tooling collection coherent under one namespace. GitHub auto-redirects the old URLs (301), so existing clones, browser bookmarks, and `cargo install perfetto-mcp-rs` keep working without action. The `0xZOne` GitHub identity remains unchanged as the maintainer in `glama.json` and as the human author behind the org.
- **Homebrew tap command changes**: `brew tap 0xZOne/tap` → `brew tap tooluse-labs/tap`. Homebrew uses curl under the hood and follows redirects, so the old command still resolves the formula via the GitHub redirect — but pinning to the new path is cleaner and avoids future drift if GitHub ever ages out the redirect.
- **Internal URL constants updated** in `Cargo.toml` (`repository`, `homepage`), `src/check_update.rs` (`RELEASES_LATEST_URL`, `UPGRADE_BASH`, `UPGRADE_POWERSHELL`), `install.sh` / `install.ps1` / `uninstall.sh` / `uninstall.ps1` (`REPO` default), `glama.yaml` (`homepage`, install command examples), `README.md` / `README.zh-CN.md` (every link, badge, and curl-piped command), and `.github/workflows/release.yml` (Homebrew tap repo target, `gh release download` `--repo` flag). `glama.json`'s `maintainers` array stays at `["0xZOne"]` because that field expects GitHub user accounts, not org names.
- **`check-update` upgrade hint** now points at `tooluse-labs/perfetto-mcp-rs/main/install.{sh,ps1}` for both PowerShell and Git Bash branches. Pre-v0.13.0 binaries continue to receive the `0xZOne/...` URL via the GitHub release JSON they fetch from the API endpoint we updated; the old endpoint also redirects, so all upgrade paths remain functional.
- **Tests**: 169 lib tests pass unchanged (URL-string changes don't affect logic). clippy + fmt clean.
- **No code-path changes** beyond the URL constants. This is a metadata/distribution-coordinate migration, not a feature release.
- **Migration follow-ups not on this commit's dependency path** (handled outside the source release): crates.io Trusted Publishing config (https://crates.io/crates/perfetto-mcp-rs/settings/trusted-publishing) needs `repository owner = tooluse-labs`; Glama needs a fresh Build & Release v0.13.0 to refresh its accumulated state under the new repo coordinate.

### [0.12.3](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.12.3) (April 30, 2026)

- **Tool descriptions restructured to a consistent 6-section skeleton.** Every `#[tool(description = ...)]` annotation in `src/server.rs` now follows the same pattern: one-line Purpose → "Use when:" → "Don't use for:" → "Parameters:" → "Empty result:" / "Errors when:" → "Reference docs:". Targets the 89% gap Glama's [Tool Definition Quality Score (TDQS)](https://glama.ai/blog/2026-04-03-tool-definition-quality-score-tdqs) rubric flags as "missing usage guidelines" — only one of the six TDQS dimensions, but the dominant defect across MCP servers in the wild. Net token cost across all 12 tool descriptions: ~+47% (1500 → ~2200), partly offset by collapsing `execute_sql`'s 5-link doc list into 2 anchored references — `execute_sql` itself loses ~20 tokens despite gaining the new sections.
- **No code-path changes.** Pure tool-description text rewrite. The only behavior-affecting bit is what an LLM reads when calling `tools/list`. Real-user (Claude Code, Codex) tool selection on already-decent v0.12.2 descriptions was working — the v0.11.3 cross-client validation session is unchanged ground truth — so this release is calibrated for Glama's automated TDQS scoring on the hosted Glama instance, not for fixing observed real-world friction.
- **Anti-injection guards preserved**: the v0.11.2-anchored "separate MCP tool, not a SQL function" disclaimer on `list_tables` and `list_table_structure` survives the rewrite (covered by `schema_discovery_tools_warn_against_execute_sql_misuse`). `list_table_structure` keeps its historically-grounded `SELECT * FROM list_table_structure` example — the exact mistake from the v0.11.2 session log; `list_tables` uses the broader "Don't reference this tool name inside SQL" guard rather than a fabricated example.
- **Tests**: 169 lib tests still pass unchanged; clippy + fmt clean. The v0.11.2 anti-injection assertion (`schema_discovery_tools_warn_against_execute_sql_misuse`) caught an over-zealous rewrite during development — fixed before commit.

### [0.12.2](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.12.2) (April 30, 2026)

- **`check-update` upgrade hint is platform-aware.** Pre-v0.12.2, the "Behind" branch always emitted the bash form `curl -fsSL … | sh` regardless of platform. PowerShell aliases `curl` to `Invoke-WebRequest`, which rejects `-fsSL` — so a Windows user who copy-pasted the suggested command verbatim got a parameter binding error instead of an upgrade. v0.12.2 detects the platform at compile time via `cfg!(windows)`: the Windows binary now lists BOTH the PowerShell form (`irm … | iex`) and the Git Bash form (because the same `.exe` serves both shells), while Unix binaries keep the single bash form unchanged. Behavior caught by the v0.12.1 smoke test on PowerShell.
- **2 new unit tests** (`upgrade_hint_unix_form_is_bash_only`, `upgrade_hint_windows_form_lists_both_shells`) pin the platform-specific renderer via a parameterized helper `upgrade_hint_for_platform(is_windows: bool)` so both branches are tested regardless of CI host. 169 lib + 9 integration tests pass; clippy + fmt clean.

### [0.12.1](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.12.1) (April 30, 2026)

- **crates.io Trusted Publishing**: replaces the static `CARGO_REGISTRY_TOKEN` GitHub secret (90-day expiring) with crates.io OIDC Trusted Publishing — the `publish-crate` workflow job now mints a short-lived registry credential per run via `rust-lang/crates-io-auth-action@v1`. Concrete consequences: (1) no more rotation chore — token never expires because it doesn't exist between runs; (2) blast radius of CI compromise is one job's worth of publish-update on this crate, not 90 days; (3) the `CARGO_REGISTRY_TOKEN` repo secret and the per-crate scoped crates.io token both become unused and can be revoked. Required one-time setup on crates.io (https://crates.io/crates/perfetto-mcp-rs/settings → Trusted Publishers): repository owner `0xZOne`, repo `perfetto-mcp-rs`, workflow filename `release.yml`, no environment.
- **`publish-crate` job idempotency guard**: the job now hits `https://crates.io/api/v1/crates/perfetto-mcp-rs/<version>` anonymously before attempting `cargo publish`. If the version already exists the job emits a `::notice::` and exits success rather than failing with `error: crate perfetto-mcp-rs@<version> already exists`. Prompted by the v0.12.0 release where the first `cargo publish` was manual (to claim the crate name + verify metadata) and the subsequent CI publish-crate job failed with that exact "already exists" — cosmetically red but operationally fine. Future re-tags or any other manual-then-CI bootstrap path now reduces to a clean skip.
- **No code changes** (Rust source unchanged at v0.12.0). Net diff: `release.yml` `publish-crate` job (+27 / -3 lines), `Cargo.toml` version bump, this CHANGELOG entry. 167 lib tests + 9 integration tests still pass; clippy + fmt clean.

### [0.12.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.12.0) (April 30, 2026)

- **`check-update` subcommand**: `perfetto-mcp-rs check-update` queries `https://api.github.com/repos/0xZOne/perfetto-mcp-rs/releases/latest`, compares against the local `CARGO_PKG_VERSION` via `semver` crate, and reports the verdict on stdout. Exit codes pin the contract for shell-prompt and CI integrations: `0` (up to date or ahead of releases — local dev build), `2` (newer release exists), `1` (network or parse error). New module `src/check_update.rs` splits HTTP fetch from pure JSON parse / version compare / render so the decision logic is unit-testable without network. 14 new tests; 167 lib tests pass.
- **Homebrew tap**: macOS and Linux users can now `brew tap 0xZOne/tap && brew install perfetto-mcp-rs`. The formula at `0xZOne/homebrew-tap` Formula/perfetto-mcp-rs.rb is auto-bumped on each release by a new `update-homebrew-tap` workflow job that downloads the four brew-relevant binaries from the GitHub release, computes their SHA256, rewrites `version` and the four `sha256` lines in the formula, runs `brew style` for lint, and pushes the change to the tap's `main`. Caveats block instructs users to run `perfetto-mcp-rs install --binary-path "$(brew --prefix)/bin/perfetto-mcp-rs"` to register with Claude Code / Codex — same self-registration entry point as `install.sh`, just decoupled from the binary distribution step (rationale: brew is for binaries, not for silent Claude Code config mutations).
- **crates.io publishing**: `cargo install --locked perfetto-mcp-rs` is now a supported install path. Cargo.toml gains the full metadata set (`authors = ["The perfetto-mcp-rs Authors"]`, `repository`, `homepage`, `readme`, `keywords`, `categories`); README links absolutified for crates.io's renderer. New `publish-crate` workflow job runs `cargo publish` on each release tag using a per-crate scoped `CARGO_REGISTRY_TOKEN` secret. v0.12.0's first publish is manual (claims the crate name); v0.12.1+ are auto from CI.
- **release.yml topology**: `publish-notes`, `update-homebrew-tap`, and `publish-crate` all `needs: build` and run in parallel. Independent failure isolation — crates.io 5xx or brew formula lint error doesn't block the GitHub release notes from being published, and vice versa.
- **README updates**: Quick install section reordered to put brew + cargo first (most macOS/Linux developers' default channel), then the existing `curl … | sh` / `irm … | iex` (still primary on Windows). New "Check for updates" subsection above "Uninstall". Same edits in README.zh-CN.md.
- **Release artifact rename**: macOS binaries renamed from `perfetto-mcp-rs-mac-{arm64,amd64}` to `perfetto-mcp-rs-darwin-{arm64,amd64}` to match the canonical platform identifier — Rust target triple is `aarch64-apple-darwin`, `uname -s` returns `darwin`. The pre-existing `darwin) os_tag="mac"` mapping in `install.sh` is removed. The only user-visible breakage: `install.sh --version=v0.11.x` (or any pre-v0.12.0 tag) will hit a 404 because that older release only published `mac-*` assets. **No fallback** — pinning old releases is a cosmetic-only path and the failure is loud. `install.sh` (default `latest`), `brew install`, `cargo install`, and the `tools/list` JSON-RPC surface are unaffected.
- **Version bump**: from v0.11.x ("LLM friction fixes") to v0.12.x ("distribution surface") — semver-minor signals the chapter break in CHANGELOG. The artifact rename is the one breaking change; everything else is additive.
- **Tests**: 14 new (compare {equal, greater, less}; render {up-to-date, ahead, behind, error, exit-code-contract-smoke}; parse_release {valid fixture, missing tag_name, garbage}; parse_release_tag {v-prefix, no-prefix, garbage}). 167 lib tests + 9 integration tests pass; clippy + fmt clean.

### [0.11.3](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.11.3) (April 30, 2026)

- **`path` parameter removed from every tool except `load_trace`.** A v0.11.2 Claude Code session showed a stark failure mode: the LLM forgot the `path` field on its first `execute_sql`, then tried `chrome_main_thread_hotspots(filter: "{...}")` (hallucinated wrapper), then read `src/server.rs` twice to figure out the schema, then gave up entirely. Root diagnosis: `path` is a *routing key* in the server's mind (LRU caches multiple `trace_processor_shell` processes by path) but is *redundant context* in the LLM's mind (the trace is already loaded, why repeat the path?). v0.11.3 reflects the LLM's mental model — `load_trace` sets a server-side `current_trace`; every other tool reads it. The `tools/list` schema for those tools simply has no `path` field, so the LLM can't get it wrong. Switching between cached traces is now done via re-calling `load_trace`, which is near-zero-cost on a manager hit.
- **String-or-number coercion on numeric tool params.** The same v0.11.2 session passed `chrome_main_thread_hotspots(pid: "12800", min_dur_ms: "50", limit: "30")` — every numeric value as a JSON string. serde correctly rejected each call (type mismatch), so the LLM bounced 4 times before bailing. v0.11.3 adds `lenient_i64` / `lenient_f64` / `lenient_u32` deserializers via `#[serde(deserialize_with = ...)]` on five numeric fields (`pid`, `upid`, `min_dur_ms`, `limit` on `ChromeMainThreadHotspotsParams`; `upid` on `ListThreadsInProcessParams`). They accept JSON numbers and JSON strings holding the same value; reject everything else with an actionable message. JsonSchema is unaffected — `tools/list` still advertises `integer`/`number`, so well-behaved LLMs see strict types and the lenient path is only a server-side safety net (Postel: be conservative in what you advertise, liberal in what you accept).
- **`name` accepted as alias for `table_name` on `list_table_structure`.** Same kind of v0.10.1-style designer-side gap as the v0.11.2 `upid` symmetry fix: the LLM occasionally writes `{name: "chrome_page_loads"}` instead of `{table_name: "chrome_page_loads"}` because "name" is the more generic mental label. `#[serde(alias = "name")]` on the field accepts both with no behavior change otherwise.
- **`load_trace` is now the only entry point with a `path` parameter.** Its description gained one sentence stating "Sets the current trace — every other tool's `path` parameter then defaults to this value." The legacy `#[serde(alias = "trace_path")]` on `LoadTraceParams::path` remains for v0.10.x backward compat. Other tools' `path` fields (whether canonical or via `trace_path` alias) now correctly fail with `deny_unknown_fields` so v0.10.x callers get a clear migration error rather than silent path divergence.
- **Tests**: 9 new (`lenient_i64_accepts_number_string_and_null`, `lenient_i64_rejects_garbage_string`, `lenient_f64_accepts_number_and_string`, `lenient_u32_accepts_and_rejects_correctly`, `chrome_main_thread_hotspots_params_accepts_stringified_numerics`, `schema_for_chrome_hotspots_advertises_strict_numeric_types`, `list_table_structure_accepts_name_alias`, `current_trace_path_errors_clearly_when_no_trace_loaded`, `only_load_trace_advertises_path_field`); 4 existing tests adapted (`all_chrome_handlers_reject_non_chrome_via_preflight` now `load_trace`s first; `params_accept_trace_path_alias_for_backwards_compat` rewritten as `load_trace_accepts_trace_path_alias_...` since only `LoadTraceParams` retains `path`; new `execute_sql_rejects_v0_10_x_path_field` pins the rejection of the legacy field; `chrome_main_thread_hotspots_params_rejects_unknown_fields` updated to drop the now-unknown `path`). 153 lib tests + 9 integration tests pass; clippy + fmt clean.
- **Cross-client validation**: v0.11.3 was independently exercised by a Codex (gpt-5.5) session against the same multi-renderer Chrome trace used to motivate v0.11.x design. Zero observed instances of any v0.11.x-targeted friction — no `path` re-passing, no field hallucination, no source-code escape, no string-vs-number coercion needed. The two LLMs converged on the same root causes via different diagnostic paths — Claude Code preferred the dedicated `chrome_*` tools, Codex preferred raw `slice` aggregation — confirming the tool-surface design serves both styles, and that the path-removal + `deny_unknown_fields` + lenient deserializer trio reads as universally-clear schema rather than Claude-Code-specific affordances.

### [0.11.2](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.11.2) (April 30, 2026)

- **`chrome_main_thread_hotspots` accepts `upid` for symmetry with `list_threads_in_process`.** A v0.11.1 Claude Code session log showed the LLM passing `chrome_main_thread_hotspots(upid: "3", ...)` and getting two `deny_unknown_fields` rejections before recovering with `pid: 12800`. Root cause is a real designer-side gap, not just LLM hallucination: v0.10.1 trained the LLM that `upid` is the precision tool for "disambiguate same-named processes" via `list_threads_in_process(upid: ...)`, and that mental model correctly transfers to "scope hotspots to one specific Renderer" — but the hotspots tool only accepted the OS-level `pid` (which can be recycled within a long trace, the exact case `upid` exists to handle). Now both tools take `upid`, with the same precedence rationale.
- **Filter semantics**: `upid` ANDs with `process_name` and `pid` (matching the existing two-knob behavior). Redundant pairings like `upid=3 AND pid=12800` are documented as harmless — when both refer to one process the AND still hits, when they refer to different processes the SQL returns 0 rows (a self-evident "your filter combination doesn't match anything" outcome, not a silent default).
- **SQL builder signature**: `chrome_main_thread_hotspots_sql(process_name, pid, min_dur_ms, limit)` becomes `chrome_main_thread_hotspots_sql(process_name, pid, upid, min_dur_ms, limit)` — `upid` slots between the two pid-shaped filters. Integration test and 11 unit-test call sites adapted; no other callers in tree.
- **Tests**: 2 new (`chrome_main_thread_hotspots_sql_with_upid_emits_filter`, `chrome_main_thread_hotspots_sql_with_upid_and_pid_emits_both`); existing no-filter test extended to assert no `p.upid =` clause; pid-only test extended to assert no upid leak. 142 lib tests + 9 integration tests pass; clippy + fmt clean.

### [0.11.1](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.11.1) (April 30, 2026)

- **`chrome_main_thread_hotspots` accepts optional `min_dur_ms` and `limit` parameters.** The previously-hardcoded 16 ms threshold and 100-row LIMIT are now exposed as adjustable knobs. Defaults preserve pre-v0.11.1 behavior (unset = 16 ms / 100 rows), so callers that pass nothing get byte-equivalent SQL. `min_dur_ms = 0` returns all main-thread tasks (matches `dur > 0`); `min_dur_ms = 33` (30 Hz) or `100` (definite stutter) focuses on bigger offenders. `limit` is capped at `MAX_ROWS = 5000` to match `execute_sql`'s row cap (no unbounded JSON dumps to the LLM); explicit `limit = 0` returns `Err` rather than silently producing an empty result. Negative, NaN, or absurdly-large `min_dur_ms` (where `ms * 1e6` would overflow `i64` ns and silently saturate to `i64::MAX`, returning 0 rows on a meaningless query) likewise returns `Err` with a clear message.
- **Why**: a v0.11.0 Claude Code session log showed the LLM passing `chrome_main_thread_hotspots(min_dur_ms: "16", limit: "25")` — both fields hallucinated, both silently dropped by serde's default unknown-field handling, the call succeeded with the hardcoded 16 ms / 100 LIMIT defaults. The LLM never learned its filter didn't apply. With real params, the LLM's mental model now matches the wire contract: pass `{min_dur_ms: 16, limit: 25}` and actually get 25 rows above the explicit threshold instead of 100 above an implicit one.
- **SQL builder signature change**: `chrome_main_thread_hotspots_sql(process_name: Option<&str>, pid: Option<i64>)` becomes `chrome_main_thread_hotspots_sql(process_name, pid, min_dur_ms: Option<f64>, limit: Option<u32>)`. Integration tests and the one e2e call site adapted; no other callers in tree.
- **`#[serde(deny_unknown_fields)]` on every params type (8 data tools + the empty `ListStdlibModulesParams`).** The same incident motivating the new params surfaced a deeper failure mode: serde's default unknown-field policy is *silently ignore*, so any future hallucinated field (`threshold_ms`, `max_results`, `lazy`, …) gets dropped on the floor and the call succeeds with whatever defaults the struct picks. With `deny_unknown_fields`, the same call now fails fast with a serde error naming the offending field; an agentic client can read the error and self-correct on the retry. Schemars 1.2.1 also propagates this to the advertised `inputSchema` as `additionalProperties: false`, so LLMs reading `tools/list` see a closed contract — the schema itself becomes a (weak) guard against hallucination, not just the deserializer. Field-level `#[serde(alias = "trace_path")]` is still honored — aliases count as known fields, so v0.10.x callers passing `trace_path` keep working.
- **Tests**: 12 new (`chrome_main_thread_hotspots_sql_with_min_dur_ms_emits_threshold`, `..._with_min_dur_ms_zero_emits_zero_threshold`, `..._rejects_negative_min_dur_ms`, `..._rejects_nan_min_dur_ms`, `..._rejects_min_dur_ms_overflowing_i64_ns`, `..._accepts_min_dur_ms_just_under_i64_ns_overflow`, `..._with_limit_overrides_default`, `..._clamps_limit_to_max_rows`, `..._rejects_zero_limit`, `chrome_main_thread_hotspots_params_rejects_unknown_fields`, `load_trace_params_rejects_unknown_fields`, `tool_input_schemas_advertise_closed_object`). 140 lib tests + 9 integration tests pass; clippy + fmt clean.

### [0.11.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.11.0) (April 29, 2026)

- **Renames the trace-file input parameter from `trace_path` to `path` across all 8 tool params types**, with `#[serde(alias = "trace_path")]` keeping the old name working as a deserialization alias for v0.10.x callers. The change is motivated by repeated LLM hallucinations on the longer name — every fresh Claude Code session observed against this server tries `load_trace(path: ...)` first, fails, and retries with `trace_path`. The old name was redundant anyway (every tool's context already implies "trace"). Touches `LoadTraceParams`, `ExecuteSqlParams`, `ListTablesParams`, `TableStructureParams`, `ListProcessesParams`, `ListThreadsInProcessParams`, `ChromeTraceParams`, `ChromeMainThreadHotspotsParams`, plus the two prose mentions of "trace_path" in `load_trace` and `execute_sql` descriptions.
- **No breaking change for live callers**: the `serde(alias)` makes `{"trace_path": "..."}` still deserialize correctly, even though the canonical schema name is now `path`. So v0.10.x scripts and any wire automation pinned to `trace_path` keep working unchanged. Only the JSON Schema advertised on `tools/list` changes — agents that read the schema fresh each session (which is the common case) see `path` and will write it correctly on first try.
- **Tests**: 2 new `server.rs` unit tests pin both halves of the contract: `params_accept_trace_path_alias_for_backwards_compat` deserializes both `path` and `trace_path` JSON forms successfully; `tool_input_schemas_use_path_not_trace_path` walks every registered tool's `input_schema` and asserts `trace_path` does not appear (the schema-only-emits-canonical guarantee — if schemars ever changed to emit serde aliases this would catch it). 128 lib tests pass.
- **Single-session benchmark, v0.8.8 vs v0.11.0** (Claude Code v2.1.123, Opus 4.7 1M xhigh, same prompt: *"分析性能问题"* on `round13_2_trace.bin`, an 11.66 s Chrome trace from a low-end Windows host loading `pre-www.qianwen.com/chat`). The two sessions reached **substantively identical core conclusions** (786 ms + 616 ms parser-blocking scripts driving DCL/Load slowness) — v0.11.0 additionally caught a forced layout (93 ms) and ruled out a 5.1 s `wpk_broker_impl` task as a false bottleneck (background worker, CPU 0.2 ms vs wall 5100 ms — disambiguated by checking `thread_dur`). Numbers:

  | Metric | v0.8.8 | v0.11.0 | Δ |
  |---|---|---|---|
  | Total tokens | 100174 | 63880 | **−36.2%** |
  | Tool calls | 32 | 17 | −47% |
  | Thinking time | 7m 25s | 4m 27s | −40% |

  The token reduction (−36%) is smaller than the tool-call reduction (−47%) because session-level fixed costs (tools/list payload, the markdown analysis report) are roughly equal across versions; the savings concentrate in the per-tool-interaction round-trip. n=1, so noise is real, but the gap is large enough to attribute to the v0.10.x → v0.11.0 path: compact wire format (v0.9.0+), description trim (v0.10.0), `path` rename eliminating retries (v0.11.0), and `MissingColumn` hint training the LLM to inspect schema before guessing column names (v0.10.1).

### [0.10.2](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.10.2) (April 29, 2026)

- **`install.sh` accepts a `--version` flag.** v0.10.1 documented the correct env-var pinning form (`curl … | VERSION=v0.7.0 sh`), but old/cached invocations of the form `VERSION=v0.7.0 curl … | sh` still silently fall through to "latest" — POSIX shell scopes `VAR=value cmd` to the immediately-following command only, so `VERSION` reaches `curl` (which doesn't read it) and never the piped `sh`. The flag form `curl … | sh -s -- --version v0.7.0` follows the rustup / pnpm convention and survives the shell-pipe pitfall. Both `--version <tag>`, `--version=<tag>`, and `-V <tag>` are accepted; flag wins over env var when both are set. Added a `-h`/`--help` summary too. README en/zh now lead with the flag form, document the env-var form with an explicit warning about the `VAR=value` ordering, and call out that `$env:VERSION = '...'; irm ... | iex` is correct on PowerShell because `iex` runs in the current session.
- `install.ps1` is unchanged — PowerShell has no equivalent of POSIX `VAR=value cmd` syntax (it errors instead of silently scoping), so the broken pattern that motivated the flag doesn't exist on the Windows side.

### [0.10.1](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.10.1) (April 29, 2026)

- **`chrome_main_thread_hotspots` accepts optional `process_name` and `pid` filters.** Multi-renderer Chrome traces previously mixed all main-thread hotspots from Browser, GPU, and every Renderer into one `LIMIT 100` bucket — analyzing a specific page's renderer required falling back to `execute_sql` with custom `WHERE` clauses. Both filters are optional and AND together when set: `process_name='Renderer'` to scope to all renderers, `pid=<pid>` to disambiguate among same-named processes (look up via `list_processes`). The previously-exported `CHROME_MAIN_THREAD_HOTSPOTS_SQL` constant becomes the `chrome_main_thread_hotspots_sql(process_name, pid)` builder function — integration tests adapted accordingly. Base SQL gains `JOIN process p ON ct.upid = p.upid` so `p.pid` is referenceable; the join is harmless when no pid filter is set.
- **`list_threads_in_process` accepts `upid` as an alternative to `process_name`.** Previously the tool only matched on process name, so an agent that already had a `upid` from `list_processes` had to convert it to a name first. Now either input works: `upid` takes precedence when both are set (it disambiguates same-named processes — e.g. multiple Renderer instances). Calling with neither set returns a clear `Err("Either upid or process_name must be provided.")` eagerly, before the trace_processor RPC.
- **`MissingColumn` error classification + view-agnostic recovery hint.** SQL errors that previously fell through to a bare `"SQL error: no such column: …"` now surface a hint pointing at `list_table_structure('<table>')`. The hint phrasing is **deliberately view-agnostic** — naming specific stdlib views (e.g. `chrome_page_loads`) would bias recovery for queries against base tables like `slice` / `args` / `thread_state`, so the hint names both the stdlib path (`INCLUDE PERFETTO MODULE …`) and base tables (`slice`, `thread`, `process`, …) without favoring either. New `execute_sql_hint_fires_on_missing_column` test pins both the positive content and the absence of stdlib-view examples.
- **Documentation fix: `VERSION` pin example.** `VERSION=v0.7.0 curl … | sh` doesn't pin — POSIX shell semantics scope `VAR=value` to the immediately-following command only, so `VERSION` reaches `curl` (which doesn't read it) but not the piped `sh`. Corrected to `curl … | VERSION=v0.7.0 sh` in `README.md` and `README.zh-CN.md`. PowerShell example was already correct (`$env:VERSION` propagates through the inline `iex`). install.sh's `==> Detecting latest release` banner is now gated on `VERSION = "latest"`; pinned installs print `==> Installing pinned release v0.x.y from github.com/…` instead.
- **Tests**: 6 new (`classify_recognizes_missing_column`, `execute_sql_hint_fires_on_missing_column`, `chrome_main_thread_hotspots_sql_no_filter_runs_all_main_threads`, `chrome_main_thread_hotspots_sql_with_pid_emits_filter`, `chrome_main_thread_hotspots_sql_with_process_name_emits_filter`, `chrome_main_thread_hotspots_sql_with_both_filters_ands_them`, `list_threads_in_process_requires_one_of_upid_or_process_name`). 126 lib tests pass; clippy + fmt clean.

### [0.10.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.10.0) (April 29, 2026)

- **Reverts v0.9.0's `rmcp::Json<T>` return-type migration on the 10 data-returning MCP tools.** Tabular tools (`execute_sql`, `list_processes`, `list_threads_in_process`, the 5 `chrome_*`) and the schema-discovery tools (`list_tables`, `list_table_structure`) all return `Result<String, String>` again, with the body produced via `serde_json::to_string(&value)` against the same typed `DecodedTable` / `TableList` / `TableInfo` / `ColumnInfo` structs introduced in v0.9.0. Wire shape unchanged at the byte level — still compact `{columns:[...],rows:[[v,...],...]}` for tabular tools, `{names:[...]}` for `list_tables`, `{table,columns:[{name,type,nullable}]}` for `list_table_structure`.
- **Why**: `rmcp::Json<T>` filled `CallToolResult.structured_content` with the parsed object alongside `content[0].text`, and Claude Code's UI renders `structured_content` as multi-line pretty-printed JSON — every cell on its own indented line. A 30-row × 8-column result blew up to ~340 rendered lines, and a single agent analysis chain (16 tool calls) buried the conversation under thousands of pretty-printed lines. Codex renders the same wire response compactly because it falls back to `content[0].text` rather than expanding `structured_content`. The `Json<T>` path was added in v0.9.0 to register `outputSchema` for free; the UX cost on the dominant client (Claude Code) outweighs the formal-contract benefit.
- **`outputSchema` removed from `tools/list`** as a side effect — there is no longer a typed `Json<T>` return for rmcp's macro to auto-derive a schema from. Agents that consumed `outputSchema` programmatically (Claude API tool-use callers) infer the response shape from the first tool result, which is self-describing JSON. New `no_tool_carries_output_schema` regression test pins the absence so a future re-introduction of `Json<T>` becomes a deliberate, visible change.
- **All other v0.9.0 work preserved**: decoder-direct columnar output, the typed `DecodedTable`, the `pragma_row_to_column_info` helper that surfaces SQLite contract violations as `Err`, column-ordering-from-`SELECT`-clause behavior, every other regression test. The change is mechanical at the rmcp adapter layer; everything below `tp_client::query` is untouched.
- 119 tests pass (120 from v0.9.0 + 1 new `no_tool_carries_output_schema` − 2 obsolete `output_schema_*` tests). `cargo clippy --lib --all-targets -D warnings` clean; `cargo fmt --all --check` clean.

### [0.9.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.9.0) (April 28, 2026)

- **Decoder rewrite eliminates the row-of-objects intermediate.** `decode_query_result` now returns a typed `DecodedTable { columns: Vec<String>, rows: Vec<Vec<Value>> }` directly from the protobuf — `columns` is sourced verbatim from `proto.column_names` and each row is a positional value vector. The previous pipeline allocated one `BTreeMap<String, Value>` per row, cloned column names into the map keys (one clone per cell), then `rows_to_columnar` re-extracted the column list from the first row's keys and removed each cell into a `Vec<Vec<Value>>`. On a max-size 5000-row × 8-column result that's ~5k BTreeMap allocations + ~40k name clones + ~40k log-K removals — now zero of each.
- **One observable wire change**: column ordering on the JSON response flips from lexicographic (BTreeMap iteration order) to **SELECT-clause order** (the order of names in `proto.column_names`). This is a strict improvement (`SELECT b, a, c` now actually serializes columns in `["b", "a", "c"]` order) and the columnar shape itself was only introduced in this same release cycle, so no agent has accumulated a dependency on the lexicographic order. New `decode_preserves_proto_column_order` unit test (`src/query.rs`) and `e2e_smoke` regression assertion pin the property.
- **`outputSchema` + `structuredContent` for 10 tools (MCP 2025-06-18)**: every data-returning tool now returns `rmcp::Json<T>`, which auto-registers `outputSchema` on the `tools/list` entry and emits `structuredContent` on `CallToolResult`. Tabular tools (`execute_sql`, `list_processes`, `list_threads_in_process`, all five `chrome_*`) return `Json<DecodedTable>`; `list_tables` returns `Json<TableList>`; `list_table_structure` returns `Json<TableInfo>` (with a typed `ColumnInfo` per column). `load_trace` (text confirmation) and `list_stdlib_modules` (a JSON-encoded `String`, candidate for a future `Json<...>` migration) keep their existing return types.
- **Purely additive on the wire.** rmcp 0.16's `CallToolResult::structured` populates both `structured_content` *and* `content[0].text` (the latter via `Value::to_string()`, byte-equivalent to the previous manual `serde_json::to_string`), so older MCP clients that don't yet read `structuredContent` keep working with no adaptation.
- **Removed**: the `format` parameter on `execute_sql` and the `SqlResultFormat` enum (introduced earlier in the same cycle, unused by any released agent). `rows_to_columnar` helper deleted. Tool descriptions trimmed of redundant shape spell-outs (`outputSchema` is the formal contract now) — saves ~230 tokens per `tools/list` response (measured byte delta).
- **`list_tables` / `list_table_structure` surface SQLite contract violations as errors.** Pre-v0.9.0 the schema-discovery handlers swallowed upstream contract drift: `list_tables`'s `filter_map` silently dropped any non-string row from `sqlite_master`, and `list_table_structure`'s projection used `unwrap_or("?")` for `name` / `type`, producing `ColumnInfo { name: "?", data_type: "?", nullable: true }` placeholders that looked like real columns an agent could try to query. Now that `outputSchema` formalizes the wire shape, both paths surface the violation as the tool's `Err` return. The PRAGMA `notnull` lookup keeps `unwrap_or(0)` (= `nullable: true`) because exotic vtables can legitimately produce NULL there. Extracts a `pragma_row_to_column_info` helper used by both production and the regression tests, eliminating the prior test/production drift risk.
- **Tests**: 7 new `query.rs` unit tests (cell lookup, serialize shape, column-order preservation, empty-table envelope), 9 new `server.rs` unit tests (description regressions, `outputSchema` presence, schema-shape consistency across tabular tools, `TableList` / `TableInfo` / `ColumnInfo` shape pins, PRAGMA contract-violation surfacing for missing `name` / `type`), 1 new `e2e_smoke` assertion pinning the SELECT-order property end-to-end.

### [0.8.8](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.8.8) (April 28, 2026)

- **Hotfix on top of v0.8.7's CJK-path support.** v0.8.7 correctly delivered the Chinese path through `trace_processor_shell.exe`'s argv (verified end-to-end: trace loads, HTTP server binds, /status responds), but `load_trace` still timed out at 20s on real CJK-locale Windows hosts. Root cause was one layer above the resolver: `trace_processor_shell` echoes the argv path bytes **raw** into `/status`'s `loaded_trace_name` protobuf field — on a cp936 host that means cp936-encoded bytes (`低端机` → `\xb5\xcd\xb6\xcb\xbb\xfa`) inside what was declared `optional string`. prost rejects those bytes as invalid UTF-8 and the entire `StatusResult::decode()` returns `Err`, so `wait_ready`'s `check_status().await.is_ok()` was permanently false even though stderr fired both Ready markers. The 20s deadline elapsed and `load_trace` reported "did not become ready" with no upstream signal as to why.
- Two-line wire fix: `loaded_trace_name` retyped from `optional string` to `optional bytes` in `proto/trace_processor.proto`. Wire format is identical (both length-delimited), so the change is forward- and backward-compatible with every `trace_processor_shell` build. Consumers (`tp_manager::loaded_name_matches`, `server::format_loaded_trace_display`) lossy-decode the bytes via `String::from_utf8_lossy` for display and matching — replacement chars on CJK directory components are fine because the basename match always wins for our use case.
- `loaded_name_matches` gains a path-suffix fallback: if the full-path comparison fails, accept any loaded name that ends with `/<expected_basename>`. This rescues CJK paths whose directory portion came back mojibake'd from the lossy decode but whose ASCII basename (`round13_2_trace.bin`) survived intact. Existing exact-match and bare-basename matchers are unchanged; suffix-collision rejection (`foo.trace.1` ≠ `foo.trace`) still passes because `/foo.trace.1` does not end with `/foo.trace`.
- 2 new tests: `format_loaded_trace_display_matches_when_cjk_dir_arrives_as_cp936` (`src/server.rs`) constructs the exact byte stream a cp936 host's `/status` returns (literal `\xB5\xCD\xB6\xCB\xBB\xFA` directory bytes + ASCII basename + ` (28 MB)` suffix) and asserts the basename match returns the original Path verbatim. `status_result_decodes_when_loaded_trace_name_has_invalid_utf8` (`src/tp_manager.rs`) hand-builds a real `/status` protobuf wire payload with cp936 bytes inside field 1 and asserts `StatusResult::decode()` succeeds — locks the schema so a future revert to `optional string` would fail CI. Fix verified end-to-end on a real cp936 Win11 host.

### [0.8.7](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.8.7) (April 28, 2026)

- **Fixes a v0.8.6 regression for CJK-locale users whose trace path's 8.3 alias is itself non-ASCII.** v0.8.6 added an 8.3-short-name path-rewrite gated on `is_ascii()` for both the canonical path *and* the resulting short name. NTFS 8.3 short-name generation runs under the system OEM codepage at directory creation, so on a cp936 (Chinese) Windows host a `低端机traces` directory keeps `低端机~1` as its short alias — still non-ASCII. The double `is_ascii()` check rejected that and surfaced the v0.8.6 fallback error, telling those users to copy to an ASCII path even though their system would have read the original path fine. Empirically confirmed on a cp936 host: `cmd /c trace_processor_shell.exe "C:\Users\…\低端机traces\…\round13_2_trace.bin"` loads the trace and enters the REPL — CreateProcessW UTF-16 → CP_ACP `char*` is lossless when every char in the path is in the active codepage.
- v0.8.7 replaces both `is_ascii()` checks with a `WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS, …, &used_default)` round-trip probe (Vista+; well within MSRV). True iff every UTF-16 code unit has an exact CP_ACP mapping with no silent best-fit substitution. Resolution order: strip the `\\?\` verbatim prefix that `Path::canonicalize` adds (trace_processor_shell doesn't accept it) → if canonical round-trips, use it directly (cp936 + Chinese path; cp932 + Japanese path; ASCII anywhere) → else try `GetShortPathNameW` (cp1252 + Chinese path created on a cp1252 host) → else surface the actionable error. Cache-key invariant from v0.8.6 preserved: `get_client` keys instances on the canonical path, not the rewritten one, so two requests for the same trace via different aliases share one `trace_processor_shell` process. Net effect: cp936/cp932 users no longer hit the fallback at all; cp1252 users keep the short-name rescue when 8.3 is enabled.
- The two `extern "system"` blocks (`WideCharToMultiByte`, `GetShortPathNameW`) and their constants (`CP_ACP = 0`, `WC_NO_BEST_FIT_CHARS = 0x400`) are consolidated into one module-level `mod win32 { … }` instead of being declared per-function. Still no new crate dependencies — the v0.8.6 "no `windows-sys` for one extern" stance carries forward.
- 5 unit tests in `src/tp_manager.rs` (`#[cfg(windows)]`): `windows_cp_acp_lossless` accepts ASCII + empty, rejects supplementary-plane chars (U+1F600 — unrepresentable on every Windows ANSI codepage); `strip_verbatim_prefix` handles `\\?\C:\…`, `\\?\UNC\server\share\…`, and pass-through; the resolver returns a CP_ACP-clean path with verbatim prefix stripped (or surfaces an actionable error when neither canonical nor short name round-trips); the resolver rejects unrepresentable non-existent paths. The v0.8.6 ASCII-only-Unix test stays unchanged.

### [0.8.6](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.8.6) (April 28, 2026)

- **Windows non-ASCII paths now load transparently**. `ensure_trace_path_supported` is replaced with `resolve_trace_path_for_shell` which, on Windows + non-ASCII canonical path, calls `GetShortPathNameW` (kernel32.dll, `extern "system"` FFI — no new crate dependency) and substitutes the NTFS-native 8.3 short alias (`低端机traces` → `E4BDB~1`) for the argv passed to `trace_processor_shell.exe`. The cache key remains the canonical path, so two requests for the same trace under different aliases share one instance. Users no longer need to manually copy traces to `C:\traces\`. Internal `refactor: simplify v0.8.5 output drains` (`744837a`) is folded into this release.
- Fallback chain when 8.3 generation is disabled on the volume (`fsutil 8dot3name query`) or any path component lacks a short alias: returns the v0.8.5 LLM-friendly error, now with three workarounds — copy to ASCII path, re-enable 8.3 names, or pass an existing short name from `dir /x`.
- 4 new tests in `src/tp_manager.rs`: ASCII pass-through, Unix non-ASCII pass-through (argv is byte-exact on Unix, no rewrite needed), `#[cfg(windows)]` integration test that creates a real `低端机traces/` directory + file in a `tempdir` and asserts the rewritten path is ASCII (skips gracefully if 8.3 generation is disabled on the temp volume), and `#[cfg(windows)]` rejection test for non-existent non-ASCII paths. The Windows tests run automatically in the windows-amd64 release.yml job.
- Upstream: this is fundamentally a `trace_processor_shell.exe` bug — its `int main(int, char*[])` entry point loses non-ANSI-codepage characters. Chromium's `base::CommandLine::Init` solves it via `GetCommandLineW` + `CommandLineToArgvW`. An issue will be filed at https://github.com/google/perfetto/issues; until upstream fixes the entry point, this client-side rewrite is the lowest-friction path.

### [0.8.5](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.8.5) (April 28, 2026)

- **Diagnostics fix**: `load_trace` now surfaces actionable error signals when `trace_processor_shell` fails to start, instead of bottoming out at the generic 20s readiness timeout. Three independent leaks plugged in `src/tp_manager.rs`:
  - **stdout was discarded** (`Stdio::null()` at `spawn`). Some Perfetto failure paths print to stdout; `Stdio::piped()` plus a new `spawn_stdout_drain` (folds into the same shared tail with a `[stdout] ` prefix) preserves them. The `format_stderr_tail` label is updated to `output tail (stderr + stdout, last N lines)` so the LLM sees both streams are folded in.
  - **stderr drain died on the first non-UTF-8 byte**. Tokio's `BufReader::lines()` returns `InvalidData` on any byte sequence that isn't valid UTF-8 and our drain `break`'d on Err — so on a Chinese-locale Windows shell, where trace_processor_shell prints error text via the active ANSI codepage (GBK / Shift-JIS / etc.), the very first error line killed the drain and every subsequent stderr byte was lost. Replaced with a manual `read_until(b'\n')` plus `String::from_utf8_lossy`: invalid sequences become `U+FFFD` (`�`) but the drain stays alive, ASCII portions (errno codes, port numbers, file sizes, paths under cp1252-overlap) survive intact, and startup needle matching keeps working because the needles are pure ASCII.
  - **Non-ASCII trace paths failed silently as a 20s timeout on Windows**. `trace_processor_shell.exe` consumes argv via `int main(int, char*[])` — bytes are decoded against the active ANSI codepage, so any character outside that codepage (Chinese paths under non-CJK locales, Japanese names under English locales, etc.) becomes `?`, fopen fails on a path that does not exist, and the shell exits with no actionable banner. New `ensure_trace_path_supported` runs after `canonicalize` and bails before spawn with a directly-actionable error: it reproduces the offending path and points at the two known workarounds — copy/move to an ASCII-only path, or pass the 8.3 short name from `cmd /c dir /x "<parent>"`. Unix is a no-op (argv is byte-exact).
- 5 new unit tests in `src/tp_manager.rs`: lossy decode preserves ASCII prefix and replaces invalid bytes with `U+FFFD`, LF / CRLF / bare-CR handling, `format_stderr_tail` label coverage, ASCII path always accepted, Unix non-ASCII no-op, and `#[cfg(windows)]` non-ASCII rejection (runs only in the Windows CI matrix).
- **Why this still warrants the v0.8.4 20s timeout**: the two changes target disjoint failure modes. v0.8.4 raised the deadline so a healthy-but-slow startup (low-end Windows + DirectWrite cold-cache font enumeration during stdlib init) doesn't time out; v0.8.5 makes shell-crash failures fail fast with rich diagnostics. The poll loop's `try_wait()` already detects an exited child within one `READY_POLL_INTERVAL`, so crash-mode failures resolve in ~100–300ms — they never approach the 20s ceiling.

### [0.8.4](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.8.4) (April 27, 2026)

- **UX fix**: `--startup-timeout-ms` default raised from `5000` to `20000` (`src/tp_manager.rs:35`, `src/main.rs:90`). Surfaced by a real Quark-on-low-end-Windows session loading a ~11s Chrome trace where `trace_processor_shell` stdlib initialization plus `DWriteFontProxy` cold-cache font enumeration consistently pushed readiness past 5s. The 5s deadline was a hard fail — `wait_ready` (`src/tp_manager.rs:115-223`) bails the entire `load_trace` call rather than surfacing a soft signal — so MCP clients saw the wrapper repeatedly restart `trace_processor_shell` and eventually try to bypass it by invoking the shell directly. Why a single bumped default instead of a tiered "soft 5s → hard 30s" retry: `wait_ready` is already a poll loop that returns the instant the stderr ready marker (or the `STATUS_FALLBACK_DELAY` /status fallback after 1s) fires. Healthy traces still resolve in ~1–2s and never approach the deadline; only the slow path benefits from the longer ceiling. A tiered retry inside the same poll loop is observationally equivalent to extending the deadline. Override entrypoints (`PERFETTO_STARTUP_TIMEOUT_MS` env, `--startup-timeout-ms` flag) unchanged from v0.8.0; CI/scripted contexts that want fast-fail can still pin a smaller value. README tables (en + zh) updated to match.

### [0.8.3](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.8.3) (April 20, 2026)

- **Agent-guidance fix**: the `chrome_*` preflight error on non-Chrome traces now points the agent to the recovery tool instead of dead-ending. When a trace lacks `chrome.process_type` track-descriptor args, the error message appended `Call list_stdlib_modules to discover modules that fit this trace, then query via execute_sql.` Prior wording only said "use execute_sql for a different trace type", and observation samples (v0.7 #2, v0.8 #1) consistently showed agents falling back to `list_processes` + hand-rolled 4-table JOINs (`slice JOIN thread_track JOIN thread JOIN process`) instead of the stdlib path. v0.8 sample #2 on the same Quark/sina trace — with this fix deployed — recorded 3/3 `execute_sql` calls using `INCLUDE PERFETTO MODULE` (`chrome.page_loads`, `chrome.tasks`), 0 raw `slice` scans. All five `chrome_*` handlers share the same `ensure_chrome_trace` preflight (`src/server.rs`), so the redirect applies uniformly. The integration test `all_chrome_handlers_reject_non_chrome_via_preflight` now asserts the `list_stdlib_modules` substring in every handler's error (previously asserted only in the first), so future refactors can't silently delete the redirect for any single tool.

### [0.8.2](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.8.2) (April 19, 2026)

- **Windows fix**: `uninstall` no longer fails hard when the cache directory can't be removed due to locked files (a running MCP client holding `trace_processor_shell.exe` open). `clean_cache` now classifies `ErrorKind::PermissionDenied` as Skipped-with-guidance ("close Claude Code / Codex and retry, or delete manually") instead of Failed. Claude/Codex deregistration — the meaningful work — already succeeded at that point, so the wrapper correctly proceeds to remove the binary. Cache leftovers are harmless (they just re-populate on the next install).
- **Windows fix**: `uninstall.ps1` no longer discards the binary's stdout. The v0.8.1 PR accidentally added `| Out-Null` around the `_invokeNative { & $dest uninstall ... }` call as part of an EAP-dance refactor, which ate the step-by-step progress output (`==> Claude: deregistered...`, `==> Codex: deregistered...`, etc.). Only stderr was visible, making users think only errors happened. Dropping `| Out-Null` restores the flow; `_invokeNative` still manages `$ErrorActionPreference` and `$LASTEXITCODE` correctly. Surfaced by a real Windows PowerShell uninstall run.

### [0.8.1](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.8.1) (April 19, 2026)

- **Windows fix**: `install` subcommand now correctly invokes `codex` (and any other CLI) when the installed form is a `.cmd`/`.bat` shim instead of a native `.exe`. npm-installed CLIs like OpenAI's Codex land as `codex.cmd` on Windows; v0.8.0's `Command::new("codex")` failed with `program not found` because `CreateProcessW` can't exec batch files directly. v0.8.1 resolves the path through `which::which` (applies PATHEXT consistently) and on Windows wraps `.cmd`/`.bat` shims through `cmd /c`. Surfaced by a real Windows PowerShell install run where Claude registered but Codex failed.

### [0.8.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.8.0) (April 19, 2026)

- **New `install` / `uninstall` subcommands**. The binary now self-registers with Claude Code (`claude mcp add`) and Codex (`codex mcp add`) and self-cleans its `dirs::data_local_dir()` cache, replacing the register/deregister blocks that lived in `install.sh` / `install.ps1` / `uninstall.sh` / `uninstall.ps1` (four copies of the same CLI-schema + cache-path knowledge that had already drifted into bugs — e.g. the Git Bash `LOCALAPPDATA` cygpath issue fixed in v0.7.0's uninstall.sh). Default subcommand-less invocation still runs the MCP server, so existing client configs don't need to change. New flags: `install --scope user|local|project`, `install --binary-path <P>`, `uninstall --scope X [--keep-cache]`, and `--skip-claude` / `--skip-codex` on both.
- **Shell scripts shrink to distribution glue** — `install.sh` and `uninstall.sh` (plus PowerShell counterparts) now do download / platform detect / file-lock aside / PATH hint / binary delete, and delegate all Claude/Codex registration + cache cleanup to `perfetto-mcp-rs install|uninstall`. Uninstall scripts **preserve the binary on subcommand failure** so users can fix the underlying issue (locked cache, wrong project dir, broken config) and re-run — only a successful uninstall (or confirmed v0.7 "no subcommand" case, or already-missing binary) deletes the binary.
- **`SCOPE` env var** on all four scripts (default `user`) plumbs through to `--scope`. For local/project installs, run the script from the target project directory: `SCOPE=local bash install.sh` / `SCOPE=local bash uninstall.sh`. Claude stores local/project scope in a CWD-keyed way inside `~/.claude.json` / project's `.mcp.json`, so the CWD must match at both install and uninstall time.
- **No more `claude mcp list` probe on the install/uninstall paths**. We used to list before add/remove to distinguish first-install from upgrade, but `claude mcp list` is **not a passive probe**: it skips the workspace-trust dialog and spawns every visible stdio server to run a health check. Running that from `--scope local|project` in a hostile or broken project directory is unsafe (arbitrary command execution from that repo's `.mcp.json`) and unreliable (hangs / fails on any broken sibling server). v0.8.0 always calls `mcp remove` first and classifies its stderr: `No <scope>-scoped MCP server found with name: ...` (exit 1) is benign; anything else — including output that mixes corruption-recovery text with the benign line — is a hard failure. See `src/install.rs::claude_remove_error_is_not_found` + its unit tests for the exact pattern.
- **`PERFETTO_STARTUP_TIMEOUT_MS` / `PERFETTO_QUERY_TIMEOUT_MS` are read lazily inside `run_server`**, not by clap at top-level parse. A stale or invalid value in the user's shell no longer blocks `perfetto-mcp-rs install` / `uninstall` with a parse error — those maintenance commands don't care about server timeouts. Precedence preserved: explicit `--flag` > env var > built-in default; invalid env in server mode still fails loudly.
- `install.rs::run_install` absolutizes `--binary-path` via `std::path::absolute` before passing it to the MCP clients. MCP clients spawn the registered command from their own working directory, so a relative `./perfetto-mcp-rs` would silently break. We deliberately don't `canonicalize` (that would resolve symlinks and diverge from the `$INSTALL_DIR` path users see). Windows POSIX paths (`/c/Users/...` from Git Bash) are converted by the shell wrapper via `cygpath -m` before reaching the Rust binary.
- **Migration 0.7.x → 0.8.0**: re-run `install.sh` (or `install.ps1`) — the new binary brings the subcommands and subsequent `uninstall.sh` picks them up. Uninstalling a 0.7.x install with the new `uninstall.sh` only deletes the binary and prints a manual-cleanup hint, because the old binary has no `uninstall` subcommand. Manual cleanup command bundle for reference:
  ```sh
  claude mcp remove perfetto-mcp-rs --scope user
  codex  mcp remove perfetto-mcp-rs
  # Cache (pick the one for your OS):
  rm -rf "${XDG_DATA_HOME:-$HOME/.local/share}/perfetto-mcp-rs"   # Linux
  rm -rf "$HOME/Library/Application Support/perfetto-mcp-rs"      # macOS
  # Windows Git Bash/MSYS ($LOCALAPPDATA is a native 'C:\...' path that
  # POSIX rm can't walk; convert with cygpath, or use PowerShell):
  rm -rf "$(cygpath -u "$LOCALAPPDATA")/perfetto-mcp-rs"          # Git Bash
  # PowerShell: Remove-Item -Recurse -Force "$env:LOCALAPPDATA\perfetto-mcp-rs"
  ```
- Ship includes 23 hermetic integration contracts (`tests/install_subcommand.rs`) exercising the subcommands via stateful shell fixtures (`tests/fixtures/{claude,codex}`), plus 12 unit tests in `src/install.rs`. Integration tests run under a restricted `PATH=<fixtures>` so `which::which` can never find real CLIs. Design in `docs/plans/v0.8-binary-self-install.zh-CN.md`.
- `cargo install perfetto-mcp-rs` remains a **developer-fallback channel** only — it requires a system `protoc` because `build.rs` invokes `prost-build`. End users should use `install.sh` / `install.ps1`.

### [0.7.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.7.0) (April 18, 2026)

- **Breaking**: tool surface change from v0.6. Four Chrome domain tools return as row-preserving thin wrappers over PerfettoSQL stdlib views, plus one new tool. v0.5→v0.7 upgraders see a fully new Chrome tool set; v0.6→v0.7 see these as additions.
- **Breaking (behavioral)**: `chrome_scroll_jank_summary` keeps its v0.5 name but **changes output structure** from pre-aggregated frequency (`COUNT(*) GROUP BY cause_of_jank`) to row-level worst-frame output (sorted by `delay_since_last_frame DESC`, limit 100). The name is preserved for consistency with the other `chrome_*_summary` tools — all five Chrome tools return row-level top-N overviews of their domain entity. New columns: `sub_cause_of_jank`, `event_latency_id`, `scroll_id`, `vsync_interval` — per-frame data lets agents group, filter, and correlate rather than stopping at a pre-baked table. v0.5.0 users must adapt to the new output shape even though the tool name is unchanged.
- `chrome_main_thread_hotspots` uses `thread.is_main_thread = 1` (Linux convention: `tid == pid`, populated in `trace_processor` when thread metadata is present) to identify main threads — more robust than filtering by thread name strings that could break across Chrome versions. Adds `task_type`, `cpu_pct` (thread_dur/dur), `thread_dur_ms`. **Caveat**: `is_main_thread` is `CppOptional` and may be NULL for traces lacking complete thread creation metadata; empty result means retry with `execute_sql WHERE thread_name IN ('CrBrowserMain', 'CrRendererMain')`.
- `chrome_page_load_summary` and `chrome_startup_summary` add `id` (and `browser_upid` for startup) for correlation queries. New `chrome_web_content_interactions` surfaces clicks, taps, and keyboard interactions ranked by duration for INP analysis.
- Design rationale: v0.3/v0.4/v0.6 observation samples across Claude and Codex show `tools/list` channel drives stable behavior — tools get called reliably. The description-URL / `instructions` channel shows high variance: most sessions had 0/N INCLUDE, one had ≥4 INCLUDE but with two wrong module names (`chrome.chrome_scrolls` and `chrome.chrome_loads` don't exist). Both failure modes — not using stdlib, and using it with wrong names — are solved by domain tools (skip the name) + `list_stdlib_modules` (provide the correct name). One session demonstrated agents depth-drilling to call-stack depth 7 (DWriteFontProxy sync IPC, forced synchronous layout) — only possible with row-level output, validating the row-preserving design. v0.6 removed Chrome domain tools based on the "answer-shaped" critique of v0.5's pre-aggregated SQL — but that critique applied to implementation, not direction. v0.7 restores the tools with row-preserving SQL following the Perfetto stdlib's own "high-level domain tables" design intent.
- Add `list_stdlib_modules` tool: returns a curated JSON array of PerfettoSQL stdlib modules (module name, domain, views, description, illustrative usage). Takes no parameters — call before loading a trace to discover which stdlib module covers analyses **not** handled by the dedicated `chrome_*` tools (Android tools, `slices.with_context`, etc.). Positioned as auxiliary discovery, not the primary path.

### [0.6.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.6.0) (April 17, 2026)

- **Breaking**: remove the four pre-built Chrome domain tools shipped in v0.5.0 (`chrome_scroll_jank_summary`, `chrome_page_load_summary`, `chrome_main_thread_hotspots`, `chrome_startup_summary`) along with their SQL constants, the shared `ChromeTraceParams` struct, and the `format_chrome_tool_error` helper. The tools were answer-shaped wrappers whose fixed SQL and narrow column sets risked steering even top-tier LLMs away from the richer analyses they could do with their own SQL against the stdlib. `chrome_main_thread_hotspots` additionally dropped `ipc_hash` / `posted_from` attribution columns and did not actually filter to the main thread. Clients invoking these tool names will fail; re-express via `execute_sql` with an appropriate stdlib module (e.g. `chrome.scroll_jank.scroll_jank_v3`, `chrome.page_loads`, `chrome.tasks`, `chrome.startups`).
- Augment the `execute_sql` tool description with stdlib guidance following the Google `com.google.PerfettoMcp` plugin's single-tool pattern — subtopic URL list (stdlib-docs with anchor-based per-package fetch guidance, perfetto-sql-syntax, frametimeline, cpu-scheduling, memory-counters, battery-counters, android-log) and Google's "worth fetching fully" phrasing. The description carries the nudge on the `tools/list` channel v0.4 confirmed reaches agents. The v0.4-era "`INCLUDE PERFETTO MODULE` must be in a separate execute_sql call" warning is removed — empirical testing against `trace_processor_shell` v54.0 (recorded in `docs/google-perfetto-mcp-analysis.md`) shows the combined `INCLUDE X; SELECT FROM X_view` form works, and the prior caveat was actively steering agents toward a two-call pattern they do not need. The description also carries a single stdlib-first signal only — no raw-table fallback note (prelude is part of the stdlib and `list_tables` already tells the agent which tables are directly available).
- Server `instructions` retain the v0.4 curated stdlib module directory. v0.3/0.4 samples showed the `instructions` channel is not behavior-active for the current Claude Code agent, but the handshake token cost is already paid and the directory is a free hedge for future agent frameworks or MCP clients that do route `instructions` into their system prompt. Multi-channel redundancy is by design.
- New `tests/e2e_stdlib_include.rs` with two cases that replace the behavior coverage removed alongside the Chrome domain tools. The first exercises a single-call `INCLUDE PERFETTO MODULE slices.with_context; SELECT ... FROM thread_slice` against `basic.perfetto-trace` — the generic regression net for the description's "stdlib-first, single-call" posture against upstream `trace_processor_shell` changes. The second exercises exactly the `chrome.scroll_jank.scroll_jank_v3` → `chrome_janky_frames` SQL printed in `README.md` as the canonical replacement for the removed `chrome_scroll_jank_summary` tool, run against `tests/fixtures/scroll_jank.pftrace` (kept for this reason; the original Chrome fixture removal would have left the README example unverified). `tests/fixtures/page_loads.pftrace` is still removed — no README example depends on it.

### [0.5.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.5.0) (April 17, 2026)

- Three new Chrome domain tools — `chrome_page_load_summary` (stdlib `chrome.page_loads`; URL + FCP / LCP / DCL / load ms per navigation), `chrome_main_thread_hotspots` (stdlib `chrome.tasks`; top 50 longest renderer/browser main-thread tasks), and `chrome_startup_summary` (stdlib `chrome.startups`; browser startup events with time-to-first-visible-content). v0.4's instructions experiment established that the `tools/list` channel reaches agents (they invoke advertised tools) but the `instructions` channel does not drive behavior — even when an agent can recite the stdlib module directory verbatim, it still reaches for raw `slice` + `LIKE '%x%'` scans on the next query. These wrappers route the most common Chrome investigations through the proven-to-fire tool channel instead of the verified-but-ignored instructions channel. Each SQL constant is exported so integration tests catch stdlib schema drift, following the `chrome_scroll_jank_summary` template.
- `ChromeScrollJankParams` renamed to `ChromeTraceParams` and reused across all four Chrome domain tools (pre-1.0 internal API, no MCP wire-level change). `format_chrome_scroll_jank_error` generalized to `format_chrome_tool_error(tool_label, err)` and reused by the three new tools so the missing-table / missing-module hint stays consistent.

### [0.4.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.4.0) (April 17, 2026)

- Server `instructions` now ship a curated PerfettoSQL stdlib directory — 10 modules covering Chrome (`chrome.page_loads`, `chrome.scroll_jank.scroll_jank_v3`, `chrome.tasks`, `chrome.startups`, `chrome.web_content_interactions`), Android (`android.startup.startups`, `android.anrs`, `android.binder`), and generic (`slices.with_context`, `linux.cpu.frequency`), each mapped to its public table/view name. Two post-v0.3.0 transcripts confirmed the soft doc-URL nudge from `bea850c` was insufficient: even on Chrome cold-start + FCP analyses where `chrome.page_loads` / `chrome.startups` directly apply, agents still fell back to raw `slice` + `LIKE '%x%'` scans and manual `thread_track → thread → process` JOIN chains. Pushing the directory into handshake instructions costs one-shot token spend per session but removes the WebFetch step agents never took. Module and view names are pinned against the vendored Perfetto stdlib source so drift surfaces as a test failure, not silent wrong-advice.
- `execute_sql` tool description slimmed: the eight-line data-source doc-URL list collapses to `stdlib-docs` + `perfetto-sql-syntax` only (the rest are reachable from `stdlib-docs`), and the stdlib nudge now points at "server instructions" for the curated list instead of repeating it. Keeps the single-signal shape — one direction per surface.

### [0.3.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.3.0) (April 16, 2026)

- [a0efea7](https://github.com/0xZOne/perfetto-mcp-rs/commit/a0efea7) **Breaking**: rename tool `table_structure` → `list_table_structure` so utility tools consistently use the `list_{noun}` shape (alongside `list_tables`, `list_processes`, `list_threads_in_process`), matching the `verb_noun` convention borrowed from `com.google.PerfettoMcp`. Clients pinned to the old name must update.
- [bea850c](https://github.com/0xZOne/perfetto-mcp-rs/commit/bea850c) `execute_sql` tool description aligned with `com.google.PerfettoMcp` conventions: stdlib docs index, PerfettoSQL syntax, jank / CPU / memory / battery / log doc links, "INCLUDE PERFETTO MODULE must be in a separate call", "reuse stdlib views where possible". The row-cap guidance collapses to a single signal so nudges do not compete. Agents get richer context for picking the right stdlib module before falling back to `LIKE '%...%'` scans.
- [a0efea7](https://github.com/0xZOne/perfetto-mcp-rs/commit/a0efea7) `load_trace` now annotates its confirmation string with the embedded recording name when `/status` reports a name that differs from the loaded path, so a trace whose recording metadata uses a different file name is no longer mistaken for "wrong file loaded". `loaded_name_matches` in `tp_manager` is the single shared predicate for both the spawn-time identity check and this display branch.

### [0.2.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.2.0) (April 15, 2026)

- [f1fc78c](https://github.com/0xZOne/perfetto-mcp-rs/commit/f1fc78c) Close out Milestone 2 failure-path coverage: real-probe port-conflict tests, a Unix non-executable-binary spawn-error test driven through `new_with_binary`, and a download HTTP 500 test that exercises the `.error_for_status()` branch the drop-connection test cannot reach. Adds an end-to-end `chrome_scroll_jank_summary` test against the bundled `scroll_jank.pftrace` fixture, driven through a newly exported `server::CHROME_SCROLL_JANK_SUMMARY_SQL` constant so the tool and integration test share one source of truth.
- [75484ed](https://github.com/0xZOne/perfetto-mcp-rs/commit/75484ed) Milestone 4 download hardening: rewrite `trace_processor_shell` acquisition around atomic `NamedTempFile` persist plus a rolling SHA-256 sidecar. Cached binaries are trusted indefinitely once the sidecar verifies, and pre-sidecar caches self-heal in place so air-gapped upgrades do not require network. A single download attempt is capped at a 10-minute wall-clock ceiling so a drip-feeding mirror cannot hang startup.
- [75484ed](https://github.com/0xZOne/perfetto-mcp-rs/commit/75484ed) Add `--artifacts-base-url` (env `PERFETTO_ARTIFACTS_BASE_URL`) for pointing `trace_processor_shell` downloads at a mirror or proxy. Userinfo and query strings are stripped from logs and from `reqwest::Error` via `without_url`, so mirror tokens cannot leak through the error chain.
- [75484ed](https://github.com/0xZOne/perfetto-mcp-rs/commit/75484ed) Split CI into a `lint` job and a `test` matrix across Ubuntu, macOS, and Windows with `fail-fast: false`. The `trace_processor_shell` cache is deliberately not restored, so every PR × OS exercises the full cold download path: atomic persist, sidecar write, and the Windows antivirus rename-retry loop.
- [75484ed](https://github.com/0xZOne/perfetto-mcp-rs/commit/75484ed) **Breaking**: `TraceProcessorManager::new_with_config(max, tp_config)` and `new_with_starting_port_and_config(max, port, tp_config)` are replaced by `new_with_configs` / `new_with_starting_port_and_configs` variants that take a `DownloadConfig` to thread the mirror override end-to-end. Pre-1.0 API, intentional breaking change for the 0.2 bump.
- [e9e718d](https://github.com/0xZOne/perfetto-mcp-rs/commit/e9e718d) New visual identity under `assets/brand/`: primary mark, small-size avatar, horizontal wordmark, and a 1280x640 GitHub social preview card, all sharing one palette and gradient set and linked from both READMEs.
- [25b72b0](https://github.com/0xZOne/perfetto-mcp-rs/commit/25b72b0) Classify query errors at the decode boundary. `QueryErrorKind` is now `MissingTable` / `MissingModule` / `Other` (non-exhaustive), and the server-layer hint formatters exhaust the enum via `match` instead of fragile `msg.contains` string matching, so hint behavior no longer silently drifts when upstream error text changes.
- [8d01dba](https://github.com/0xZOne/perfetto-mcp-rs/commit/8d01dba) Milestone 2 regression coverage: tests for LRU eviction, subprocess recovery after abnormal exit, concurrent same-trace and cross-trace access, and the server hint paths for missing-table / missing-module failures.
- [8eaba62](https://github.com/0xZOne/perfetto-mcp-rs/commit/8eaba62) End-to-end smoke test that drives `spawn → HTTP RPC → decode` against a real trace fixture on every CI run, replacing the prior "single-chain-works" assumption with an automated check.
- [7aeaae6](https://github.com/0xZOne/perfetto-mcp-rs/commit/7aeaae6) Milestone 1 runtime hardening: instance lifecycle, spawn / wait-ready coordination, port allocation, and trace-identity checks on `/status` are reworked for correctness under concurrent MCP traffic.
- [a7c648e](https://github.com/0xZOne/perfetto-mcp-rs/commit/a7c648e) New domain analysis tools built on top of the generic `execute_sql` executor: process and thread summaries, Chrome scroll-jank summary, and a stdlib URL hint for module discovery.
- [68a3954](https://github.com/0xZOne/perfetto-mcp-rs/commit/68a3954) Borrow curated LLM nudges from the `com.google.PerfettoMcp` plugin for missing-table / missing-module errors, giving agents actionable hints instead of raw `trace_processor` error strings.
- [8bf9197](https://github.com/0xZOne/perfetto-mcp-rs/commit/8bf9197) Filter internal (`__`-prefixed) tables from `list_tables` output, and nudge aggregation when `execute_sql` would otherwise overflow the row cap.
- [7c609e9](https://github.com/0xZOne/perfetto-mcp-rs/commit/7c609e9) Native PowerShell installer `install.ps1` for Windows, complementing the existing POSIX `install.sh`. Supports ConstrainedLanguage mode, survives first-install `claude mcp remove` non-zero exit, and renames locked `.exe` aside to allow in-place upgrades.

### [0.1.1](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.1.1) (April 12, 2026)

- [7552f18](https://github.com/0xZOne/perfetto-mcp-rs/commit/7552f18) Defer the `trace_processor_shell` download until the first MCP tool call instead of eagerly downloading at startup, so `claude mcp add` completes even on restricted networks. On Windows, append `.exe` to the cached binary name so the cache path matches the installed file.
- [7f54864](https://github.com/0xZOne/perfetto-mcp-rs/commit/7f54864) `install.sh` automatically adds the install directory to the Windows user `PATH`, with forward-slash paths so Git Bash / MSYS2 / Cygwin can source it without further quoting.
- [a3b9c4e](https://github.com/0xZOne/perfetto-mcp-rs/commit/a3b9c4e) `install.sh` supports Windows via Git Bash, MSYS2, and Cygwin: binary-name detection, platform-arch mapping, and path normalization all branch correctly under the three POSIX-on-Windows shells.

### [0.1.0](https://github.com/0xZOne/perfetto-mcp-rs/releases/tag/v0.1.0) (April 12, 2026)

- [e61bd86](https://github.com/0xZOne/perfetto-mcp-rs/commit/e61bd86) Initial public release of `perfetto-mcp-rs`: an MCP server for Perfetto trace analysis, backed by an auto-downloaded `trace_processor_shell`. Exposes four core tools — `load_trace`, `execute_sql`, `list_tables`, `table_structure` — over the MCP stdio transport.
- [8fbceb7](https://github.com/0xZOne/perfetto-mcp-rs/commit/8fbceb7) Process lifecycle management: spawn `trace_processor_shell` per trace, wait for readiness on stderr, reclaim ports and processes through an LRU instance pool, and re-spawn automatically after abnormal child exit.
- [7a69ce0](https://github.com/0xZOne/perfetto-mcp-rs/commit/7a69ce0) HTTP RPC client plus protobuf query-result decoder that talks to the `trace_processor_shell` loopback server and converts its binary response into `serde_json::Value` rows for MCP callers.
- [a2e9441](https://github.com/0xZOne/perfetto-mcp-rs/commit/a2e9441) Fix the MCP handshake against `claude mcp add` and add the initial CI / release scaffold so cross-platform builds and release assets can be produced from `main`.
