---
id: goose-sends-removed-roots-notification
title: goose emits a roots notification that 2026-07-28 removed, and rmcp does not stop it
kind: idiosyncrasy
summary: notify_roots_list_changed fires on every working-directory change with no version check, goose prefers 2026-07-28 where the notification no longer exists, and rmcp 3.1.4 gates nothing on the outbound notification path.
capabilities:
  - roots.listChanged
clients:
  - goose
  - openai-codex
specVersions:
  - 2026-07-28
sources:
  - url: https://github.com/aaif-goose/goose
    title: crates/goose/src/agents/mcp_client.rs
    commit: "c5e5929a"
    path: crates/goose/src/agents/mcp_client.rs:721-726
    date: 2026-08-23
  - url: https://crates.io/crates/rmcp/3.1.4
    title: rmcp 3.1.4 source, service/client.rs and service.rs
    path: src/service/client.rs:1139-1161,1720
    date: 2026-08-24
  - url: https://github.com/openai/codex
    title: codex-rs/rmcp-client/src/logging_client_handler.rs, the full ClientHandler impl
    commit: b705b6b0
    path: codex-rs/rmcp-client/src/logging_client_handler.rs
    date: 2026-08-24
  - url: https://modelcontextprotocol.io/specification/2026-07-28/changelog
    title: 2026-07-28 changelog, major change 5
    date: 2026-08-23
status: current
lastVerified: 2026-08-24
canonical: https://canimcp.io/research/goose-sends-removed-roots-notification
last-updated: 2026-11-11
---

# goose emits a roots notification that 2026-07-28 removed, and rmcp does not stop it

Kind: idiosyncrasy · Status: current · Last verified: 2026-08-24 · Clients: [goose](/clients/goose.md), [openai-codex](/clients/openai-codex.md) · Revisions: `2026-07-28`

`notifications/roots/list_changed` was removed in `2026-07-28` under SEP-2575. goose still sends it, and by default negotiates `2026-07-28`.

The call site has no version check:

```rust
async fn do_update_working_dir(&self, new_dir: PathBuf) -> Result<(), Error> {
    let client = self.client.lock().await;
    let shared = client.service().shared_working_dir();
    *shared.write().await = new_dir;
    client.peer().notify_roots_list_changed().await?;
    Ok(())
}
```

The default lifecycle prefers the revision that removed it:

```rust
ClientLifecycleMode::Auto {
    preferred_versions: vec![ProtocolVersion::V_2026_07_28],
    legacy_version: Some(ProtocolVersion::LATEST),
}
```

## The SDK does not filter it

goose declares `rmcp = "3.0.0"`; the lockfile resolves `3.1.4`, which is what was read.

`Peer::notify_roots_list_changed` is generated by the `method!` macro's `peer_not` arm. That arm constructs the notification and sends it, with no version parameter in scope:

```rust
($(#[$meta:meta])* peer_not $method:ident $Not:ident) => {
    pub async fn $method(&self) -> Result<(), ServiceError> {
        self.send_notification(ClientNotification::$Not($Not {
            method: Default::default(),
            extensions: Default::default(),
        }))
        .await?;
        Ok(())
    }
};
```

`send_notification` pushes onto the peer sink channel and awaits the responder. It reads no protocol version.

Every `V_2026_07_28` comparison in rmcp 3.1.4 sits somewhere else: server-side request handling (`handler/server.rs`), `_meta` key requirements (`model/meta.rs`), and lifecycle and version negotiation (`service/client.rs:278,313`). None is on the outbound client notification path. `uses_legacy_lifecycle` looks like a candidate but only selects initialize-versus-discover, and its callers are the server handler and the streamable HTTP server transport.

`RootsListChangedNotification` also remains a first-class type in the 3.1.4 model, undeprecated and unbounded by version.

So the notification reaches the wire. A server that negotiated `2026-07-28` with goose receives `notifications/roots/list_changed` whenever the session changes working directory, for a method that revision does not define.

## Codex does not hit it

Codex CLI is the other Rust client on rmcp, at 3.1.3 rather than 3.1.4, and it is unaffected — because it does not implement roots at all. Its `ClientHandler` has no `list_roots`, `roots` is absent from its declared capabilities, and `notify_roots_list_changed` is never called. There is no working-directory change that could reach the notification path.

So the gap needs a caller, not just the SDK. A Rust client is exposed only once it opts into roots, which makes goose the case to watch rather than the first of many.

## Scope

This is not goose-specific. rmcp is the official Rust SDK, and any Rust client that calls `notify_roots_list_changed` on a `2026-07-28` connection gets the same result. The SDK leaves version-appropriateness to the caller and no caller has a reason to expect that.

Practical impact is currently small, since roots is itself deprecated as of the same revision and few servers act on the notification. It matters more as a shape: the SDK exposes removed methods without guardrails, so the same pattern will recur for anything else 2026-07-28 dropped.

## Sources

- [crates/goose/src/agents/mcp_client.rs](https://github.com/aaif-goose/goose), commit `c5e5929a`, `crates/goose/src/agents/mcp_client.rs:721-726`, 2026-08-23
- [rmcp 3.1.4 source, service/client.rs and service.rs](https://crates.io/crates/rmcp/3.1.4), `src/service/client.rs:1139-1161,1720`, 2026-08-24
- [codex-rs/rmcp-client/src/logging_client_handler.rs, the full ClientHandler impl](https://github.com/openai/codex), commit `b705b6b0`, `codex-rs/rmcp-client/src/logging_client_handler.rs`, 2026-08-24
- [2026-07-28 changelog, major change 5](https://modelcontextprotocol.io/specification/2026-07-28/changelog), 2026-08-23
