Skip to content

Commit fa8d028

Browse files
committed
fix: negotiate MCP protocol version and evict idle sessions
- Echo the client's requested protocolVersion when supported, else the latest version this server speaks (both SSE and streamable HTTP). - Track last-seen time per streamable-HTTP session and evict idle ones past SESSION_IDLE_TTL instead of leaking until the cap is reached. - get_stats now counts its own invocation like every other tool, and the unreachable session-id header fallback is dropped. Signed-off-by: lucarlig <luca.carlig@ibm.com>
1 parent 1a9d85e commit fa8d028

11 files changed

Lines changed: 129 additions & 47 deletions

File tree

mcp-servers/rust/fast-time-server/src/app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025
1+
// Copyright 2026
22
// SPDX-License-Identifier: Apache-2.0
33

44
use axum::Router;
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
// Copyright 2025
1+
// Copyright 2026
22
// SPDX-License-Identifier: Apache-2.0
33

4+
use std::time::Duration;
5+
46
pub(crate) const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0:9080";
57
pub(crate) const APP_NAME: &str = "fast-time-server";
68
pub(crate) const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
79
pub(crate) const MCP_PROTOCOL_VERSION: &str = "2025-11-25";
10+
/// Protocol versions echoed back during initialize negotiation; anything else
11+
/// falls back to `MCP_PROTOCOL_VERSION`.
12+
pub(crate) const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &[
13+
"2024-11-05",
14+
"2025-03-26",
15+
"2025-06-18",
16+
MCP_PROTOCOL_VERSION,
17+
];
818
pub(crate) const SESSION_HEADER: &str = "mcp-session-id";
919
pub(crate) const MAX_ACTIVE_SESSIONS: usize = 10_000;
20+
/// Idle lifetime for streamable-HTTP sessions before they are evicted.
21+
pub(crate) const SESSION_IDLE_TTL: Duration = Duration::from_secs(300);
1022
pub(crate) const SSE_CHANNEL_CAPACITY: usize = 64;
1123
pub(crate) const MAX_DELAY_MS: u64 = 60_000;

mcp-servers/rust/fast-time-server/src/delay.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025
1+
// Copyright 2026
22
// SPDX-License-Identifier: Apache-2.0
33

44
use rand_distr::Distribution;

mcp-servers/rust/fast-time-server/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// fast-time-server - Ultra-fast MCP server for performance testing
22
//
3-
// Copyright 2025
3+
// Copyright 2026
44
// SPDX-License-Identifier: Apache-2.0
55

66
mod app;

mcp-servers/rust/fast-time-server/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// fast-time-server - Ultra-fast MCP server for performance testing
22
//
3-
// Copyright 2025
3+
// Copyright 2026
44
// SPDX-License-Identifier: Apache-2.0
55

66
#[tokio::main]

mcp-servers/rust/fast-time-server/src/mcp.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025
1+
// Copyright 2026
22
// SPDX-License-Identifier: Apache-2.0
33

44
use axum::body;
@@ -8,7 +8,7 @@ use chrono::Utc;
88
use serde_json::json;
99
use std::sync::atomic::{AtomicU64, Ordering};
1010

11-
use crate::config::{APP_NAME, APP_VERSION, MCP_PROTOCOL_VERSION};
11+
use crate::config::{APP_NAME, APP_VERSION, MCP_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS};
1212
use crate::delay::{compute_delay, validate_delay};
1313
use crate::time::{parse_time_in_timezone, parse_timezone};
1414

@@ -23,16 +23,30 @@ pub(crate) fn id_json(id: Option<&serde_json::Value>) -> String {
2323
.unwrap_or_else(|| "null".to_string())
2424
}
2525

26-
pub(crate) fn initialize_body(id: Option<&serde_json::Value>) -> String {
26+
pub(crate) fn initialize_body(id: Option<&serde_json::Value>, protocol_version: &str) -> String {
2727
format!(
2828
r#"{{"jsonrpc":"2.0","id":{},"result":{{"protocolVersion":"{}","capabilities":{{"tools":{{}}}},"serverInfo":{{"name":"{}","version":"{}"}},"instructions":"Ultra-fast MCP test server."}}}}"#,
2929
id_json(id),
30-
MCP_PROTOCOL_VERSION,
30+
protocol_version,
3131
APP_NAME,
3232
APP_VERSION
3333
)
3434
}
3535

36+
/// Echo back the client's requested protocol version when supported, otherwise
37+
/// advertise the latest version this server speaks.
38+
pub(crate) fn negotiate_protocol_version(req: &serde_json::Value) -> &'static str {
39+
let requested = req
40+
.get("params")
41+
.and_then(|params| params.get("protocolVersion"))
42+
.and_then(serde_json::Value::as_str);
43+
SUPPORTED_PROTOCOL_VERSIONS
44+
.iter()
45+
.copied()
46+
.find(|&supported| Some(supported) == requested)
47+
.unwrap_or(MCP_PROTOCOL_VERSION)
48+
}
49+
3650
pub(crate) fn tools_list_response(id: Option<&serde_json::Value>) -> Response {
3751
json_response(format!(
3852
r#"{{"jsonrpc":"2.0","id":{},"result":{{"tools":[{{"name":"echo","description":"Echo back the provided message.","inputSchema":{{"type":"object","properties":{{"message":{{"type":"string"}},"delay":{{"type":"integer","minimum":0,"maximum":60000}},"delay_stddev":{{"type":"number","minimum":0}}}},"required":["message"]}}}},{{"name":"get_system_time","description":"Get current system time in the specified IANA timezone.","inputSchema":{{"type":"object","properties":{{"timezone":{{"type":"string"}}}}}}}},{{"name":"convert_time","description":"Convert a time value from a source IANA timezone to a target IANA timezone.","inputSchema":{{"type":"object","properties":{{"time":{{"type":"string"}},"source_timezone":{{"type":"string"}},"target_timezone":{{"type":"string"}}}},"required":["time","source_timezone","target_timezone"]}}}},{{"name":"schema_error","description":"Always returns isError=true.","inputSchema":{{"type":"object","properties":{{}}}},"outputSchema":{{"type":"object","properties":{{"recognitionId":{{"type":"string"}},"message":{{"type":"string"}}}},"required":["recognitionId"]}}}},{{"name":"schema_success","description":"Returns a JSON payload that conforms to the declared outputSchema.","inputSchema":{{"type":"object","properties":{{}}}},"outputSchema":{{"type":"object","properties":{{"recognitionId":{{"type":"string"}},"message":{{"type":"string"}}}},"required":["recognitionId"]}}}},{{"name":"get_stats","description":"Get server statistics including request count and uptime.","inputSchema":{{"type":"object","properties":{{}}}}}}]}}}}"#,
@@ -113,7 +127,7 @@ pub(crate) async fn tools_call_response(
113127
)
114128
}),
115129
"get_stats" => {
116-
let count = DIRECT_REQUEST_COUNT.load(Ordering::Relaxed);
130+
let count = DIRECT_REQUEST_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
117131
text_result_response(
118132
id,
119133
&format!(
@@ -173,7 +187,7 @@ pub(crate) async fn sse_message_response(req: &serde_json::Value) -> Option<Stri
173187
.and_then(serde_json::Value::as_str)
174188
.unwrap_or_default();
175189
let response = match method {
176-
"initialize" => json_response(initialize_body(id)),
190+
"initialize" => json_response(initialize_body(id, negotiate_protocol_version(req))),
177191
"ping" => empty_result_response(id),
178192
"tools/list" => tools_list_response(id),
179193
"tools/call" => tools_call_response(id, req).await,

mcp-servers/rust/fast-time-server/src/rest.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025
1+
// Copyright 2026
22
// SPDX-License-Identifier: Apache-2.0
33

44
use axum::extract::Query;

mcp-servers/rust/fast-time-server/src/time.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025
1+
// Copyright 2026
22
// SPDX-License-Identifier: Apache-2.0
33

44
#[cfg(test)]

mcp-servers/rust/fast-time-server/src/transports/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025
1+
// Copyright 2026
22
// SPDX-License-Identifier: Apache-2.0
33

44
pub(crate) mod sse;

mcp-servers/rust/fast-time-server/src/transports/sse.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025
1+
// Copyright 2026
22
// SPDX-License-Identifier: Apache-2.0
33

44
use axum::extract::Query;
@@ -171,10 +171,7 @@ mod tests {
171171
.expect("initialize response should be sent as SSE data");
172172
let body: serde_json::Value =
173173
serde_json::from_str(&message).expect("SSE message should be JSON-RPC");
174-
assert_eq!(
175-
body["result"]["protocolVersion"],
176-
crate::config::MCP_PROTOCOL_VERSION
177-
);
174+
assert_eq!(body["result"]["protocolVersion"], "2024-11-05");
178175
assert_eq!(
179176
body["result"]["serverInfo"]["name"],
180177
crate::config::APP_NAME

0 commit comments

Comments
 (0)