Skip to content

v0.4.0

Latest

Choose a tag to compare

@RomanEmreis RomanEmreis released this 23 Jun 11:25
fe4d634

Release notes

neva 0.4.0 — MCP 2026-07-28 Release Candidate

This release adds opt-in support for the MCP 2026-07-28 Release Candidate spec, behind the compile-time proto-2026-07-28-rc feature flag. The legacy spec remains the default and is unchanged for users who don't opt in.

RC status. Wire format and APIs gated by proto-2026-07-28-rc are not covered by semver and may change before the final spec ships. Once it graduates, the flag will invert: the RC path becomes the default and the current default moves under a legacy-spec flag. Plan for that swap as a deliberate breaking change — the spec itself is breaking.

What the RC changes

The 2026-07-28 spec re-architects MCP around stateless, horizontally scalable HTTP transports, removes a few features that pushed too much policy into the protocol (sampling, roots, server logging), and introduces an extensions mechanism for everything that doesn't belong in core. neva 0.4.0 covers all of this except authorization tightening and MCP Apps,
which are scheduled for 0.5.

Stateless HTTP and discovery

The initialize/initialized handshake is replaced by a single server/discover request, and the HTTP transport carries no
Mcp-Session-Id. Every POST carries a required MCP-Protocol-Version header — the client adds it automatically; the server rejects missing or unsupported values. Any request can land on any server instance.

Server-initiated notifications are inert on this transport — clients poll instead. The new ttlMs / cacheScope cache hints on list results give
the server a way to suggest how long each list is valid.

Elicitation via MRTR

Multi Round-Trip Requests replace the legacy push-channel elicit. A handler calls ctx.elicit(key, params).await?; on a miss the framework
returns an InputRequiredResult with an AEAD-sealed requestState, and the client re-issues with inputResponses until the handler completes. The state blob is the only thing that persists across rounds, so a retry can land on any instance.

Because the handler re-runs each round, neva exposes three effect helpers on Context for safe side effects:

ctx.once("charge_card", async { billing.charge(&order).await }).await?;
let quote: Quote = ctx.memo("quote", async { pricing.fetch(&order).await }).await?;
ctx.on_commit(async move { mailer.send_receipt(&order).await });
  • once runs an effect at most once (marker lives in requestState).
  • memo caches a computed value (also in requestState, encrypted).
  • on_commit defers an effect to the final result.

All three are at-most-once within a single requestState chain — pair non-idempotent effects with a downstream idempotency key.

For multi-instance deployments you need both a shared signing secret (App::with_request_state_secret) and a shared idempotency store (App::with_request_state_storeRequestStateStore trait, default in-memory). The secret keeps requestState portable across instances; the store closes the lost-response retry window (handler runs, response is lost in transit, client retries the same state — without the store the handler runs twice and on_commit double-fires). neva warns at startup if you forget the secret.

Task-augmented elicit

A task-augmented tool runs on a different execution substrate from MRTR — it genuinely suspends, instead of re-running. The two never mix:

let answer = if ctx.is_task() {
    ctx.task().elicit(params).await?   // suspend the background task
} else {
    ctx.elicit("name", params).await?  // MRTR re-run
};

once/memo/on_commit are MRTR helpers; in a Required task they
error (clear misuse), in an Optional task they degrade to inline.

JSON Schema 2020-12 tools

Tool.input_schema/output_schema now use a per-flag ToolInputSchema alias — ToolSchema under legacy, the new
schema_2020::InputSchema (serde_json::Value-backed) under RC. The #[tool] macro emits full 2020-12 documents: primitive args become inline primitives, structured Json<T> args derive a rich inlined schema when T: JsonSchema, with a graceful {"type":"object"} fallback. Explicit input_schema = "…" literals are validated at compile time on every feature configuration. schemars is re-exported by neva, so user crates don't need a direct dependency.

Extensions, with Tasks as the first consumer

Anything not in core now lives behind the new Extension trait:

let app = App::new()
    .with_extension(TasksExtension::new(ServerTasksCapability::default()));

Extensions advertise a capability under capabilities.extensions[<reverse-DNS-id>] in server/discover and register their own handlers. Tasks is the built-in example (io.modelcontextprotocol/tasks). The legacy with_tasks API is unchanged — it's now a thin wrapper that registers the extension under the hood.

What changed for legacy users

Nothing on the wire. Two minor additions:

  • ErrorCode::RESOURCE_NOT_FOUND is a new constant that emits the right wire code per active spec. Prefer it over ErrorCode::ResourceNotFound (now #[deprecated]); the variant stays for back-compat.
  • Dc<T> extractors now work as handler arguments for tools and prompts,
    not just resources.

Migration & deployment notes for the RC

If you're trying the RC flag:

  • Set App::with_request_state_secret(<shared secret>) for any HTTP deployment with more than one instance — the default ephemeral
    per-process key works for single-instance development only. Cross-instance retries will fail to decrypt without a shared secret.
  • Set App::with_request_state_store(<shared store>) for the same reason. Implement RequestStateStore over Redis (or similar) for
    production.
  • Rewrite ctx.elicit(params).await calls as ctx.elicit(key, params).await? with a stable string key. Wrap any side effects between/around elicit points in ctx.once / ctx.memo / ctx.on_commit — handlers re-run on each round, so anything not idempotent will fire repeatedly.
  • roots/list, sampling/createMessage, and logging/setLevel are removed under RC. The corresponding APIs are #[cfg]-gated out — the
    spec's intended replacements are host-side: roots → out-of-band, sampling → host-provided tool, logging → host's own telemetry.
  • Server-initiated notifications are inert on the stateless transport. If your server relies on listChanged / subscribe push, keep the
    legacy spec until the spec settles on a stateless replacement (ttlMs / cacheScope cover most listChanged cases via polling).

Deferred to 0.5

  • Authorization tightening (RFC 9207 iss validation, DCR application_type, refresh-token / scope-accumulation rules) — its
    own epic.
  • MCP Apps (server-rendered UI in sandboxed iframe) — still experimental in the RC; will land once it stabilizes.

What's Changed

Full Changelog: 0.3.4...0.4.0