feat: add subscription listen streams (SEP-2575) (#1000)

* feat: add subscription listen streams

* refactor: reuse legacy request helper
This commit is contained in:
Dale Seo 2026-07-21 09:53:04 -04:00 • committed by GitHub
commit ff1a715b6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 3617 additions and 223 deletions

View file

@ -10,12 +10,6 @@
# `conformance list --spec-version 2026-07-28` and update #977.
server:
# SEP-2575 stateless lifecycle gaps: discover capability declaration,
# missing-capability rejection, HTTP 404 for removed methods, and
# diagnostic tools (test_missing_capability, test_streaming_elicitation,
# test_logging_tool) not yet implemented.
# tracked in #1004
- server-stateless
# SEP-2106: composition/conditional/$anchor keywords are stripped from
# published tool input schemas.
# tracked in #1003

View file

@ -1,10 +1,16 @@
#![allow(deprecated)]
use std::{collections::HashSet, sync::Arc};
use std::{
collections::{HashMap, HashSet},
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
use rmcp::{
ErrorData, RoleServer, ServerHandler,
model::*,
service::RequestContext,
service::{RequestContext, SubscriptionContext, SubscriptionSink},
transport::{
StreamableHttpServerConfig, StreamableHttpService,
streamable_http_server::session::local::LocalSessionManager,
@ -48,7 +54,9 @@ const REQUEST_STATE_KEY: &[u8] = b"rust-sdk-conformance-request-state-key!!";
#[derive(Clone)]
struct ConformanceServer {
subscriptions: Arc<Mutex<HashSet<String>>>,
legacy_resource_subscriptions: Arc<Mutex<HashSet<String>>>,
subscriptions: Arc<Mutex<HashMap<u64, SubscriptionSink>>>,
next_subscription: Arc<AtomicU64>,
log_level: Arc<Mutex<LoggingLevel>>,
request_state_codec: RequestStateCodec,
}
@ -56,7 +64,9 @@ struct ConformanceServer {
impl ConformanceServer {
fn new() -> Self {
Self {
subscriptions: Arc::new(Mutex::new(HashSet::new())),
legacy_resource_subscriptions: Arc::new(Mutex::new(HashSet::new())),
subscriptions: Arc::new(Mutex::new(HashMap::new())),
next_subscription: Arc::new(AtomicU64::new(0)),
log_level: Arc::new(Mutex::new(LoggingLevel::Debug)),
request_state_codec: RequestStateCodec::new(REQUEST_STATE_KEY),
}
@ -379,22 +389,51 @@ impl ServerHandler for ConformanceServer {
(name == "test_custom_header").then(custom_header_tool)
}
fn get_info(&self) -> ServerInfo {
ServerInfo::new(
ServerCapabilities::builder()
.enable_prompts()
.enable_prompts_list_changed()
.enable_resources()
.enable_resources_subscribe()
.enable_resources_list_changed()
.enable_tools()
.enable_tool_list_changed()
.enable_logging()
.build(),
)
.with_server_info(Implementation::new("rust-conformance-server", "0.1.0"))
.with_instructions("Rust MCP conformance test server")
}
async fn initialize(
&self,
request: InitializeRequestParams,
_cx: RequestContext<RoleServer>,
) -> Result<InitializeResult, ErrorData> {
Ok(InitializeResult::new(
ServerCapabilities::builder()
.enable_prompts()
.enable_resources()
.enable_tools()
.enable_logging()
.build(),
)
.with_protocol_version(request.protocol_version)
.with_server_info(Implementation::new("rust-conformance-server", "0.1.0"))
.with_instructions("Rust MCP conformance test server"))
let info = self.get_info();
Ok(InitializeResult::new(info.capabilities)
.with_protocol_version(request.protocol_version)
.with_server_info(info.server_info)
.with_instructions(info.instructions.unwrap_or_default()))
}
fn accepted_subscription_filter(
&self,
requested: &SubscriptionFilter,
) -> Option<SubscriptionFilter> {
Some(requested.supported_by(&self.get_info().capabilities))
}
async fn listen(&self, context: SubscriptionContext) -> Result<(), ErrorData> {
let key = self.next_subscription.fetch_add(1, Ordering::Relaxed);
self.subscriptions
.lock()
.await
.insert(key, context.sink().clone());
context.cancelled().await;
self.subscriptions.lock().await.remove(&key);
Ok(())
}
async fn ping(&self, _cx: RequestContext<RoleServer>) -> Result<(), ErrorData> {
@ -540,6 +579,46 @@ impl ServerHandler for ConformanceServer {
})),
),
custom_header_tool(),
Tool::new(
"test_trigger_tool_change",
"Triggers a tools/list_changed notification on matching subscriptions",
json_object(json!({
"type": "object",
"properties": {}
})),
),
Tool::new(
"test_trigger_prompt_change",
"Triggers a prompts/list_changed notification on matching subscriptions",
json_object(json!({
"type": "object",
"properties": {}
})),
),
Tool::new(
"test_missing_capability",
"Requires the sampling client capability",
json_object(json!({
"type": "object",
"properties": {}
})),
),
Tool::new(
"test_streaming_elicitation",
"Returns an input_required result containing an elicitation request",
json_object(json!({
"type": "object",
"properties": {}
})),
),
Tool::new(
"test_logging_tool",
"Emits notifications/message only when logLevel is requested",
json_object(json!({
"type": "object",
"properties": {}
})),
),
];
// SEP-2322 MRTR test tools; all take no arguments.
let mrtr_tools = [
@ -922,6 +1001,81 @@ impl ServerHandler for ConformanceServer {
)]))
}
"test_trigger_tool_change" => {
let subscriptions = self
.subscriptions
.lock()
.await
.values()
.cloned()
.collect::<Vec<_>>();
for subscription in subscriptions {
let _ = subscription.notify_tool_list_changed().await;
}
Ok(CallToolResult::success(vec![ContentBlock::text(
"Tool list change triggered",
)]))
}
"test_trigger_prompt_change" => {
let subscriptions = self
.subscriptions
.lock()
.await
.values()
.cloned()
.collect::<Vec<_>>();
for subscription in subscriptions {
let _ = subscription.notify_prompt_list_changed().await;
}
Ok(CallToolResult::success(vec![ContentBlock::text(
"Prompt list change triggered",
)]))
}
"test_missing_capability" => {
let capabilities = cx.meta.client_capabilities().unwrap_or_default();
if capabilities.sampling.is_none() {
return Err(ErrorData::missing_required_client_capability(
ClientCapabilities::builder().enable_sampling().build(),
));
}
Ok(CallToolResult::success(vec![ContentBlock::text(
"Required capability declared",
)]))
}
"test_streaming_elicitation" => {
let mut requests = InputRequests::new();
requests.insert(
"streaming_elicitation".into(),
mrtr_elicitation_request(
"Provide a value",
json!({ "value": { "type": "string" } }),
json!(["value"]),
),
);
return Ok(InputRequiredResult::from_input_requests(requests).into());
}
"test_logging_tool" => {
if let Some(level) = cx.meta.log_level() {
let _ = cx
.peer
.notify_logging_message(
LoggingMessageNotificationParam::new(
level,
json!("logLevel was requested"),
)
.with_logger("conformance-server"),
)
.await;
}
Ok(CallToolResult::success(vec![ContentBlock::text(
"Logging tool completed",
)]))
}
_ => Err(ErrorData::invalid_params(
format!("Unknown tool: {}", request.name),
None,
@ -1029,7 +1183,7 @@ impl ServerHandler for ConformanceServer {
request: SubscribeRequestParams,
_cx: RequestContext<RoleServer>,
) -> Result<(), ErrorData> {
let mut subs = self.subscriptions.lock().await;
let mut subs = self.legacy_resource_subscriptions.lock().await;
subs.insert(request.uri.to_string());
Ok(())
}
@ -1039,7 +1193,7 @@ impl ServerHandler for ConformanceServer {
request: UnsubscribeRequestParams,
_cx: RequestContext<RoleServer>,
) -> Result<(), ErrorData> {
let mut subs = self.subscriptions.lock().await;
let mut subs = self.legacy_resource_subscriptions.lock().await;
subs.remove(request.uri.as_str());
Ok(())
}