Skip to content

Commit b42c72b

Browse files
committed
fix(protocol): fidelity sweep 2 — ttlMs as schema number; absorb SEP-2577 deprecation markers
(a) ttlMs: the schema declares `number` (@Minimum 0); the u64 binding rejected spec-legal fractional values on deserialize. CacheableResult.ttl_ms and its embeddings (ListToolsResult, ListResourcesResult, ListResourceTemplatesResult, ReadResourceResult, ListPromptsResult, DiscoverResult) are now f64 via a shared serde helper: fractional values accepted and round-tripped, negative/non-finite rejected, whole values keep the compact integer wire form (byte-stable for the common ttlMs: 0). Contract test covers all four behaviors. (b) SEP-2577 markers: the 2026-06-07 re-pin deprecated the WHOLE Logging surface — including the per-request _meta logLevel opt-in and the LoggingLevel enum — and the bindings/rustdoc still claimed those were "the non-deprecated replacement". Corrected: #[deprecated] now on LoggingLevel (+ LogLevel alias), META_KEY_LOG_LEVEL, RequestMetaObject.log_level / with_log_level, ServerCapabilities.logging, ModelHint / ModelPreferences / ToolChoice, ContentBlock::ToolUse / ToolResult (variants + constructors), and the sampling/logging trait surface. The behavior remains normative and implemented through the migration window (earliest removal 2027-07-28); framework-internal call sites carry scoped #[allow(deprecated)], so the markers reach downstream consumers without making the framework warn on itself. Zero warnings on the default build, test build, clippy, and the 2025 opt-in lane builds.
1 parent 6e2dc16 commit b42c72b

23 files changed

Lines changed: 250 additions & 47 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3737

3838
### Fixed (2026-06-10)
3939

40+
- **Protocol-fidelity sweep, part 2 — `ttlMs` as a schema `number` + SEP-2577 marker absorption.** (a) `CacheableResult.ttlMs` (and its embeddings in the tools/resources/prompts/discover results) is now `f64` per the schema's `number` type: fractional values are accepted on deserialize and survive round trips, negative/non-finite values reject (`@minimum 0`), and whole values keep the compact integer wire form (byte-stable for the common `ttlMs: 0` case). (b) The re-pinned schema's SEP-2577 deprecations are now fully absorbed as `#[deprecated]` markers: `LoggingLevel` (+ `LogLevel` alias), the per-request `_meta` `logLevel` key and `RequestMetaObject.log_level`/`with_log_level`, `ServerCapabilities.logging`, `ModelHint`/`ModelPreferences`/`ToolChoice`, the `ContentBlock::ToolUse`/`ToolResult` variants and constructors, and the sampling trait surface (`HasCreateMessageRequestParams`/`CreateMessageRequest`/`CreateMessageResult`/`HasLevelParam`). The earlier rustdoc claim that `LoggingLevel`/`logLevel` were "the non-deprecated replacement" was wrong against the re-pin and is corrected — the whole Logging surface (including the per-request opt-in this branch implements) is deprecated-but-normative through the migration window. Framework-internal use sites carry scoped `#[allow(deprecated)]` (the framework intentionally serves the surface through the window); downstream consumers now get compiler nudges.
4041
- **Protocol-fidelity sweep, part 1 (wire/type drift vs the pinned schema).** (a) `ToolChoice` no longer carries a non-spec `name` field on the wire (the `specific()` constructor is gone) and `mode` is optional per schema (`{}` parses; absent means `"auto"`; `effective_mode()` helper). (b) `PromptReference` is `BaseMetadata`-shaped: gains `title`, drops the non-spec `description`. (c) `Annotations.audience` is the closed `Role[]` union instead of `Vec<String>` — wire-invalid values like `"system"` are now rejected at parse time; the builders' `annotation_audience` takes `Role` (converted to strings on the frozen 2025 lane). (d) The duplicate `Role` binding is gone — `sampling::Role` re-exports the single `prompts::Role`. (e) `LoggingCapabilities`/`CompletionsCapabilities` match the schema's opaque `JSONObject`: the invented `enabled`/`levels` keys are removed from the bindings and from both server builders' capability advertisements (presence of the object is the signal). 5 new wire-shape contract tests in `compliance.rs`; existing tests migrated with the contract (e.g. the empty `ToolChoice` parse fails against the pre-fix required-`mode` binding).
4142

4243
- **2025 opt-in lane build regression (same day, pre-push).** The elicitation enum-union slice used the 2026-only union accessors in `turul-mcp-builders` code that also compiles under `protocol-2025-11-25`, breaking the opt-in lane builds (caught by `scripts/ci-gates.sh all`). The validation is now `#[cfg]`-split per lane.

crates/turul-mcp-aws-lambda/src/builder.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,10 @@ impl LambdaMcpServerBuilder {
647647
#[cfg(feature = "protocol-2025-11-25")]
648648
pub fn with_logging(mut self) -> Self {
649649
use turul_mcp_protocol::initialize::LoggingCapabilities;
650-
self.capabilities.logging = Some(LoggingCapabilities::default());
650+
#[allow(deprecated)] // SEP-2577 migration window
651+
{
652+
self.capabilities.logging = Some(LoggingCapabilities::default());
653+
}
651654
self.handler(LoggingHandler)
652655
}
653656

@@ -1069,7 +1072,11 @@ impl LambdaMcpServerBuilder {
10691072

10701073
// Logging capability: presence of the (opaque) object means the server
10711074
// can send notifications/message (same as McpServer).
1072-
capabilities.logging = Some(turul_mcp_protocol::initialize::LoggingCapabilities::default());
1075+
#[allow(deprecated)] // SEP-2577 migration window
1076+
{
1077+
capabilities.logging =
1078+
Some(turul_mcp_protocol::initialize::LoggingCapabilities::default());
1079+
}
10731080

10741081
// Tasks capabilities — auto-configure when task runtime is set
10751082
#[cfg(feature = "protocol-2025-11-25")]

crates/turul-mcp-builders/src/notification.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::collections::HashMap;
88

99
// Import protocol types
1010
use turul_mcp_json_rpc_server::types::RequestId;
11+
#[allow(deprecated)] // SEP-2577 migration window
1112
use turul_mcp_protocol::logging::LoggingLevel;
1213
// `LoggingMessageNotification` is deprecated-but-present in 2026-07-28 (SEP-2577); logging
1314
// remains a valid feature the framework supports.

crates/turul-mcp-builders/src/traits/logging_traits.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
//!
33
//! **IMPORTANT**: These are framework features, NOT part of the MCP specification.
44
5+
// The Logging surface is SEP-2577-deprecated; these traits carry it
6+
// through the migration window.
7+
#![allow(deprecated)]
8+
59
use serde_json::Value;
610
use turul_mcp_protocol::logging::LoggingLevel;
711

crates/turul-mcp-protocol-2026-07-28/src/caching.rs

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,19 @@ pub struct CacheableResult {
6666
/// - `0` means immediately stale; client MAY re-fetch every use.
6767
/// - Positive: client SHOULD consider the result fresh for this many ms.
6868
///
69-
/// Schema annotation: `@minimum 0`.
70-
pub ttl_ms: u64,
69+
/// Schema type is `number` (`@minimum 0`) — fractional values are
70+
/// spec-legal and accepted on deserialize; whole values serialize as
71+
/// integers so the common case keeps its compact wire form.
72+
#[serde(with = "ttl_ms_serde")]
73+
pub ttl_ms: f64,
7174

7275
/// Sharing scope.
7376
pub cache_scope: CacheScope,
7477
}
7578

7679
impl CacheableResult {
7780
/// Construct with both required fields.
78-
pub fn new(ttl_ms: u64, cache_scope: CacheScope) -> Self {
81+
pub fn new(ttl_ms: f64, cache_scope: CacheScope) -> Self {
7982
Self {
8083
ttl_ms,
8184
cache_scope,
@@ -84,12 +87,38 @@ impl CacheableResult {
8487

8588
/// Convenience: immediately-stale public response (`ttlMs=0`, public scope).
8689
pub fn stale_public() -> Self {
87-
Self::new(0, CacheScope::Public)
90+
Self::new(0.0, CacheScope::Public)
8891
}
8992

9093
/// Convenience: 60-second private cache.
9194
pub fn private_60s() -> Self {
92-
Self::new(60_000, CacheScope::Private)
95+
Self::new(60_000.0, CacheScope::Private)
96+
}
97+
}
98+
99+
/// Serde for `ttlMs`: the schema declares `number` with `@minimum 0`.
100+
/// Deserialize accepts any non-negative finite number; serialize emits an
101+
/// integer for whole values (compact, byte-stable for the common case) and a
102+
/// float otherwise.
103+
pub mod ttl_ms_serde {
104+
use serde::{Deserialize, Deserializer, Serializer};
105+
106+
pub fn serialize<S: Serializer>(value: &f64, serializer: S) -> Result<S::Ok, S::Error> {
107+
if value.fract() == 0.0 && *value >= 0.0 && *value <= (1u64 << 53) as f64 {
108+
serializer.serialize_u64(*value as u64)
109+
} else {
110+
serializer.serialize_f64(*value)
111+
}
112+
}
113+
114+
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
115+
let value = f64::deserialize(deserializer)?;
116+
if !value.is_finite() || value < 0.0 {
117+
return Err(serde::de::Error::custom(
118+
"ttlMs must be a non-negative finite number",
119+
));
120+
}
121+
Ok(value)
93122
}
94123
}
95124

@@ -122,7 +151,7 @@ mod tests {
122151
#[test]
123152
fn cacheable_result_serializes_required_fields() {
124153
// Both fields REQUIRED on the wire.
125-
let c = CacheableResult::new(5000, CacheScope::Public);
154+
let c = CacheableResult::new(5000.0, CacheScope::Public);
126155
let v = serde_json::to_value(&c).unwrap();
127156
assert_eq!(v["ttlMs"], 5000);
128157
assert_eq!(v["cacheScope"], "public");
@@ -152,17 +181,17 @@ mod tests {
152181
#[test]
153182
fn cacheable_result_accepts_zero_ttl() {
154183
// Schema: "If 0, the response SHOULD be considered immediately stale".
155-
let c = CacheableResult::new(0, CacheScope::Public);
184+
let c = CacheableResult::new(0.0, CacheScope::Public);
156185
let v = serde_json::to_value(&c).unwrap();
157186
assert_eq!(v["ttlMs"], 0);
158187
}
159188

160189
#[test]
161190
fn cacheable_result_round_trips() {
162-
let c = CacheableResult::new(86_400_000, CacheScope::Private);
191+
let c = CacheableResult::new(86_400_000.0, CacheScope::Private);
163192
let s = serde_json::to_string(&c).unwrap();
164193
let parsed: CacheableResult = serde_json::from_str(&s).unwrap();
165-
assert_eq!(parsed.ttl_ms, 86_400_000);
194+
assert_eq!(parsed.ttl_ms, 86_400_000.0);
166195
assert_eq!(parsed.cache_scope, CacheScope::Private);
167196
}
168197

@@ -180,7 +209,7 @@ mod tests {
180209

181210
let l = ListLike {
182211
tools: vec!["echo".to_string()],
183-
cache: CacheableResult::new(60_000, CacheScope::Public),
212+
cache: CacheableResult::new(60_000.0, CacheScope::Public),
184213
};
185214
let v = serde_json::to_value(&l).unwrap();
186215
assert_eq!(v["tools"][0], "echo");
@@ -192,18 +221,18 @@ mod tests {
192221

193222
let s = serde_json::to_string(&v).unwrap();
194223
let parsed: ListLike = serde_json::from_str(&s).unwrap();
195-
assert_eq!(parsed.cache.ttl_ms, 60_000);
224+
assert_eq!(parsed.cache.ttl_ms, 60_000.0);
196225
assert_eq!(parsed.cache.cache_scope, CacheScope::Public);
197226
}
198227

199228
#[test]
200229
fn helpers_produce_expected_values() {
201230
let stale = CacheableResult::stale_public();
202-
assert_eq!(stale.ttl_ms, 0);
231+
assert_eq!(stale.ttl_ms, 0.0);
203232
assert_eq!(stale.cache_scope, CacheScope::Public);
204233

205234
let p60 = CacheableResult::private_60s();
206-
assert_eq!(p60.ttl_ms, 60_000);
235+
assert_eq!(p60.ttl_ms, 60_000.0);
207236
assert_eq!(p60.cache_scope, CacheScope::Private);
208237
}
209238
}

crates/turul-mcp-protocol-2026-07-28/src/content.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ pub enum ContentBlock {
152152
meta: Option<HashMap<String, Value>>,
153153
},
154154
/// Tool use content block.
155+
#[deprecated(
156+
since = "0.4.0",
157+
note = "Deprecated per SEP-2577 (DRAFT-2026-v1) with the Sampling surface. \
158+
Earliest removal: first release on/after 2027-07-28."
159+
)]
155160
#[serde(rename = "tool_use")]
156161
ToolUse {
157162
/// Unique identifier for this tool use
@@ -164,6 +169,11 @@ pub enum ContentBlock {
164169
meta: Option<HashMap<String, Value>>,
165170
},
166171
/// Tool result content block.
172+
#[deprecated(
173+
since = "0.4.0",
174+
note = "Deprecated per SEP-2577 (DRAFT-2026-v1) with the Sampling surface. \
175+
Earliest removal: first release on/after 2027-07-28."
176+
)]
167177
#[serde(rename = "tool_result")]
168178
ToolResult {
169179
/// ID of the tool use this result corresponds to
@@ -240,6 +250,11 @@ impl ContentBlock {
240250
}
241251

242252
/// Create tool use content block
253+
#[allow(deprecated)]
254+
#[deprecated(
255+
since = "0.4.0",
256+
note = "Deprecated per SEP-2577 (DRAFT-2026-v1) with the Sampling surface."
257+
)]
243258
pub fn tool_use(
244259
id: impl Into<String>,
245260
name: impl Into<String>,
@@ -254,6 +269,11 @@ impl ContentBlock {
254269
}
255270

256271
/// Create tool result content block
272+
#[allow(deprecated)]
273+
#[deprecated(
274+
since = "0.4.0",
275+
note = "Deprecated per SEP-2577 (DRAFT-2026-v1) with the Sampling surface."
276+
)]
257277
pub fn tool_result(tool_use_id: impl Into<String>, content: Vec<ContentBlock>) -> Self {
258278
Self::ToolResult {
259279
tool_use_id: tool_use_id.into(),
@@ -265,6 +285,11 @@ impl ContentBlock {
265285
}
266286

267287
/// Create tool result error content block
288+
#[allow(deprecated)]
289+
#[deprecated(
290+
since = "0.4.0",
291+
note = "Deprecated per SEP-2577 (DRAFT-2026-v1) with the Sampling surface."
292+
)]
268293
pub fn tool_result_error(tool_use_id: impl Into<String>, content: Vec<ContentBlock>) -> Self {
269294
Self::ToolResult {
270295
tool_use_id: tool_use_id.into(),
@@ -277,6 +302,7 @@ impl ContentBlock {
277302

278303
/// Add annotations to any content block that supports them.
279304
/// ToolUse and ToolResult do not have annotations per spec — this is a no-op for those variants.
305+
#[allow(deprecated)] // matches the SEP-2577-deprecated variants
280306
pub fn with_annotations(mut self, annotations: Annotations) -> Self {
281307
match &mut self {
282308
ContentBlock::Text { annotations: a, .. }
@@ -293,6 +319,7 @@ impl ContentBlock {
293319
}
294320

295321
/// Add meta to any content block
322+
#[allow(deprecated)] // matches the SEP-2577-deprecated variants
296323
pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
297324
match &mut self {
298325
ContentBlock::Text { meta: m, .. }
@@ -455,6 +482,7 @@ impl BlobResourceContents {
455482
}
456483

457484
#[cfg(test)]
485+
#[allow(deprecated)] // exercises SEP-2577-deprecated surfaces
458486
mod tests {
459487
use super::*;
460488
use serde_json::json;

crates/turul-mcp-protocol-2026-07-28/src/discover.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ pub struct DiscoverResult {
7070
pub result_type: ResultType,
7171

7272
/// `CacheableResult.ttlMs` — required by schema (DiscoverResult extends CacheableResult).
73-
pub ttl_ms: u64,
73+
#[serde(with = "crate::caching::ttl_ms_serde")]
74+
pub ttl_ms: f64,
7475
/// `CacheableResult.cacheScope` — required by schema.
7576
pub cache_scope: crate::caching::CacheScope,
7677

@@ -105,7 +106,7 @@ impl DiscoverResult {
105106
) -> Self {
106107
Self {
107108
result_type: ResultType::Complete,
108-
ttl_ms: 0,
109+
ttl_ms: 0.0,
109110
cache_scope: crate::caching::CacheScope::Public,
110111
supported_versions,
111112
capabilities,
@@ -116,7 +117,7 @@ impl DiscoverResult {
116117
}
117118

118119
/// Set the cache-control hint (`ttlMs` + `cacheScope`).
119-
pub fn with_cache(mut self, ttl_ms: u64, cache_scope: crate::caching::CacheScope) -> Self {
120+
pub fn with_cache(mut self, ttl_ms: f64, cache_scope: crate::caching::CacheScope) -> Self {
120121
self.ttl_ms = ttl_ms;
121122
self.cache_scope = cache_scope;
122123
self

crates/turul-mcp-protocol-2026-07-28/src/initialize.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ pub struct CompletionsCapabilities {
251251
pub struct ServerCapabilities {
252252
/// Logging capabilities.
253253
#[serde(skip_serializing_if = "Option::is_none")]
254+
#[deprecated(
255+
since = "0.4.0",
256+
note = "Deprecated per SEP-2577 (DRAFT-2026-v1) — the Logging capability is being \
257+
phased out. Earliest removal: first release on/after 2027-07-28."
258+
)]
254259
pub logging: Option<LoggingCapabilities>,
255260
/// Completion capabilities.
256261
#[serde(skip_serializing_if = "Option::is_none")]

crates/turul-mcp-protocol-2026-07-28/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ pub use input_required::{
160160
};
161161
pub use json_rpc::{JSONRPC_VERSION, PaginatedRequestParams, RequestParams};
162162
pub use meta::{Cursor as MetaCursor, ProgressToken};
163+
#[allow(deprecated)] // META_KEY_LOG_LEVEL re-exported through the SEP-2577 migration window
163164
pub use meta::{
164165
META_KEY_BAGGAGE, META_KEY_CLIENT_CAPABILITIES, META_KEY_CLIENT_INFO, META_KEY_LOG_LEVEL,
165166
META_KEY_PROTOCOL_VERSION, META_KEY_SUBSCRIPTION_ID, META_KEY_TRACEPARENT, META_KEY_TRACESTATE,

crates/turul-mcp-protocol-2026-07-28/src/logging.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@
1818
//! The wire-payload types ([`crate::notifications::LoggingMessageNotification`]
1919
//! and [`crate::notifications::LoggingMessageNotificationParams`]) live in
2020
//! [`crate::notifications`] alongside the other notification payloads. This
21-
//! module carries only the wire-value enum [`LoggingLevel`] (still used by
22-
//! the non-deprecated [`crate::meta::RequestMetaObject::log_level`] field).
21+
//! module carries only the wire-value enum [`LoggingLevel`], used by the
22+
//! per-request [`crate::meta::RequestMetaObject::log_level`] opt-in — the
23+
//! whole Logging surface, including that opt-in and this enum, is deprecated
24+
//! per SEP-2577 and remains functional through the migration window.
2325
2426
use serde::{Deserialize, Serialize};
2527

2628
/// Logging levels (per MCP spec)
2729
/// Maps to syslog message severities as specified in RFC-5424.
28-
///
29-
/// NOT deprecated: this enum is the wire-value type for both the deprecated
30-
/// `notifications/message` surface AND the non-deprecated per-request
31-
/// `_meta.io.modelcontextprotocol/logLevel` opt-in. Deprecating it would
32-
/// cascade a warning into the replacement mechanism.
30+
#[deprecated(
31+
since = "0.4.0",
32+
note = "Deprecated per SEP-2577 (DRAFT-2026-v1) along with the whole Logging surface, \
33+
including the per-request _meta logLevel opt-in this enum values. \
34+
Replacement: stderr (stdio) or OpenTelemetry. \
35+
Earliest removal: first release on/after 2027-07-28."
36+
)]
3337
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3438
#[serde(rename_all = "lowercase")]
3539
pub enum LoggingLevel {
@@ -44,9 +48,15 @@ pub enum LoggingLevel {
4448
}
4549

4650
/// Type alias for compatibility (per MCP spec)
51+
#[allow(deprecated)]
52+
#[deprecated(
53+
since = "0.4.0",
54+
note = "Deprecated per SEP-2577 (DRAFT-2026-v1) — see LoggingLevel."
55+
)]
4756
pub type LogLevel = LoggingLevel;
4857

4958
/// Convenience constructors for LoggingLevel
59+
#[allow(deprecated)]
5060
impl LoggingLevel {
5161
/// Get logging level priority (0 = debug, 7 = emergency)
5262
pub fn priority(&self) -> u8 {
@@ -69,6 +79,7 @@ impl LoggingLevel {
6979
}
7080

7181
#[cfg(test)]
82+
#[allow(deprecated)] // exercises SEP-2577-deprecated surfaces
7283
mod tests {
7384
use super::*;
7485

0 commit comments

Comments
 (0)