feat: add subscription listen streams (SEP-2575) (#1000)
* feat: add subscription listen streams * refactor: reuse legacy request helper
This commit is contained in:
parent
8ee9ee68f1
commit
ff1a715b6e
2 changed files with 3617 additions and 223 deletions
|
|
@ -1,6 +1,6 @@
|
|||
// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
|
||||
#![expect(deprecated)]
|
||||
use std::{borrow::Cow, sync::Arc, time::Duration};
|
||||
use std::{borrow::Cow, num::NonZeroUsize, sync::Arc, time::Duration};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
|
|
@ -21,8 +21,9 @@ use crate::{
|
|||
ReadResourceRequest, ReadResourceRequestParams, ReadResourceResponse, ReadResourceResult,
|
||||
Reference, RequestId, RequestMetaObject, RootsListChangedNotification, ServerInfo,
|
||||
ServerJsonRpcMessage, ServerNotification, ServerRequest, ServerResult, SetLevelRequest,
|
||||
SetLevelRequestParams, SubscribeRequest, SubscribeRequestParams, UnsubscribeRequest,
|
||||
UnsubscribeRequestParams,
|
||||
SetLevelRequestParams, SubscribeRequest, SubscribeRequestParams, SubscriptionFilter,
|
||||
SubscriptionsListenRequest, SubscriptionsListenRequestParams, SubscriptionsListenResult,
|
||||
UnsubscribeRequest, UnsubscribeRequestParams,
|
||||
},
|
||||
transport::DynamicTransportError,
|
||||
};
|
||||
|
|
@ -184,10 +185,298 @@ impl ServiceRole for RoleClient {
|
|||
type PeerInfo = ServerInfo;
|
||||
type InitializeError = ClientInitializeError;
|
||||
const IS_CLIENT: bool = true;
|
||||
|
||||
fn configure_direct_peer(peer: &Peer<Self>, info: &Self::Info) {
|
||||
let Some(server_info) = peer.peer_info() else {
|
||||
return;
|
||||
};
|
||||
if server_info.protocol_version.as_str() < ProtocolVersion::V_2026_07_28.as_str() {
|
||||
return;
|
||||
}
|
||||
peer.set_client_request_metadata(ClientRequestMetadata {
|
||||
protocol_version: server_info.protocol_version.clone(),
|
||||
client_info: info.client_info.clone(),
|
||||
client_capabilities: info.capabilities.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn peer_cancelled_params(notification: &Self::PeerNot) -> Option<&CancelledNotificationParam> {
|
||||
match notification {
|
||||
ServerNotification::CancelledNotification(notification) => Some(¬ification.params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ServerSink = Peer<RoleClient>;
|
||||
|
||||
/// Default number of notifications buffered for one subscription.
|
||||
pub const DEFAULT_SUBSCRIPTION_CHANNEL_CAPACITY: usize = 64;
|
||||
|
||||
/// How a client-side subscription stream ended.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum SubscriptionEnd {
|
||||
/// The server returned a final `SubscriptionsListenResult`.
|
||||
Graceful(SubscriptionsListenResult),
|
||||
/// The transport closed without a final result. Call `Peer::listen` again
|
||||
/// after reconnecting; subscription streams are not resumable.
|
||||
Abrupt,
|
||||
/// The subscription was explicitly cancelled by either peer.
|
||||
Cancelled,
|
||||
/// The consumer did not drain notifications before the channel filled.
|
||||
Lagged { capacity: usize },
|
||||
}
|
||||
|
||||
/// Handle for one active `subscriptions/listen` request.
|
||||
#[derive(Debug)]
|
||||
pub struct Subscription {
|
||||
id: RequestId,
|
||||
acknowledged: SubscriptionFilter,
|
||||
notifications: tokio::sync::mpsc::Receiver<ServerNotification>,
|
||||
request: Option<RequestHandle<RoleClient>>,
|
||||
end: Option<SubscriptionEnd>,
|
||||
}
|
||||
|
||||
type SubscriptionResponse =
|
||||
Result<Result<ServerResult, ServiceError>, tokio::sync::oneshot::error::RecvError>;
|
||||
|
||||
struct PendingSubscriptionRequest {
|
||||
handle: Option<RequestHandle<RoleClient>>,
|
||||
}
|
||||
|
||||
impl PendingSubscriptionRequest {
|
||||
fn new(handle: RequestHandle<RoleClient>) -> Self {
|
||||
Self {
|
||||
handle: Some(handle),
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv(&mut self) -> Option<SubscriptionResponse> {
|
||||
let handle = self.handle.as_mut()?;
|
||||
Some((&mut handle.rx).await)
|
||||
}
|
||||
|
||||
fn take(&mut self) -> Option<RequestHandle<RoleClient>> {
|
||||
self.handle.take()
|
||||
}
|
||||
|
||||
fn unregister(&self, id: &RequestId) {
|
||||
if let Some(handle) = self.handle.as_ref() {
|
||||
handle.peer.unregister_subscription(id);
|
||||
}
|
||||
}
|
||||
|
||||
fn disarm(&mut self) {
|
||||
self.handle.take();
|
||||
}
|
||||
|
||||
async fn cancel(&mut self, reason: &'static str) {
|
||||
if let Some(handle) = self.handle.take() {
|
||||
let _ = handle.cancel(Some(reason.to_owned())).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PendingSubscriptionRequest {
|
||||
fn drop(&mut self) {
|
||||
let Some(handle) = self.handle.take() else {
|
||||
return;
|
||||
};
|
||||
handle.peer.unregister_subscription(&handle.id);
|
||||
handle.peer.try_cancel_request(
|
||||
handle.id,
|
||||
Some("subscription establishment cancelled".to_owned()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
/// Return the originating listen request ID.
|
||||
pub fn id(&self) -> &RequestId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// Return the notification filter accepted by the server.
|
||||
pub fn acknowledged(&self) -> &SubscriptionFilter {
|
||||
&self.acknowledged
|
||||
}
|
||||
|
||||
/// Return the terminal state after this subscription has ended.
|
||||
pub fn end(&self) -> Option<&SubscriptionEnd> {
|
||||
self.end.as_ref()
|
||||
}
|
||||
|
||||
/// Receive the next notification, or `None` after the subscription ends.
|
||||
///
|
||||
/// A graceful final result and an abrupt transport close are distinguished
|
||||
/// through [`Self::end`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a service or protocol error when the stream carries an invalid
|
||||
/// message, an unexpected final result, or another request failure.
|
||||
pub async fn next(&mut self) -> Result<Option<ServerNotification>, ServiceError> {
|
||||
if self.end.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(request) = self.request.as_mut() else {
|
||||
self.end = Some(SubscriptionEnd::Abrupt);
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
notification = self.notifications.recv() => {
|
||||
let Some(notification) = notification else {
|
||||
let response = (&mut request.rx).await;
|
||||
return self.handle_response(response);
|
||||
};
|
||||
if let ServerNotification::CancelledNotification(cancelled) = ¬ification {
|
||||
if cancelled.params.request_id.as_ref() != Some(&self.id) {
|
||||
self.cancel_as_abrupt("subscription cancellation ID mismatch")
|
||||
.await;
|
||||
return Err(ServiceError::UnexpectedResponse);
|
||||
}
|
||||
self.finish(SubscriptionEnd::Cancelled);
|
||||
return Ok(None);
|
||||
}
|
||||
if notification.get_meta().subscription_id().as_ref() != Some(&self.id) {
|
||||
self.cancel_as_abrupt("subscription notification ID mismatch")
|
||||
.await;
|
||||
return Err(ServiceError::UnexpectedResponse);
|
||||
}
|
||||
if !self.accepts(¬ification) {
|
||||
self.cancel_as_abrupt(
|
||||
"subscription notification was outside the acknowledged filter",
|
||||
)
|
||||
.await;
|
||||
return Err(ServiceError::UnexpectedResponse);
|
||||
}
|
||||
Ok(Some(notification))
|
||||
}
|
||||
response = &mut request.rx => {
|
||||
self.handle_response(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel this subscription.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a transport error when the cancellation signal cannot be sent.
|
||||
pub async fn cancel(&mut self) -> Result<(), ServiceError> {
|
||||
self.cancel_with_reason(None).await
|
||||
}
|
||||
|
||||
/// Cancel this subscription with a diagnostic reason.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a transport error when the cancellation signal cannot be sent.
|
||||
pub async fn cancel_with_reason(&mut self, reason: Option<String>) -> Result<(), ServiceError> {
|
||||
let Some(request) = self.request.take() else {
|
||||
return Ok(());
|
||||
};
|
||||
request.cancel(reason).await?;
|
||||
self.end = Some(SubscriptionEnd::Cancelled);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish(&mut self, end: SubscriptionEnd) {
|
||||
if let Some(request) = self.request.take() {
|
||||
request.peer.unregister_subscription(&self.id);
|
||||
}
|
||||
self.end = Some(end);
|
||||
}
|
||||
|
||||
async fn cancel_as_abrupt(&mut self, reason: &'static str) {
|
||||
if let Some(request) = self.request.take() {
|
||||
let _ = request.cancel(Some(reason.to_owned())).await;
|
||||
}
|
||||
self.end = Some(SubscriptionEnd::Abrupt);
|
||||
}
|
||||
|
||||
fn accepts(&self, notification: &ServerNotification) -> bool {
|
||||
match notification {
|
||||
ServerNotification::ToolListChangedNotification(_) => {
|
||||
self.acknowledged.tools_list_changed == Some(true)
|
||||
}
|
||||
ServerNotification::PromptListChangedNotification(_) => {
|
||||
self.acknowledged.prompts_list_changed == Some(true)
|
||||
}
|
||||
ServerNotification::ResourceListChangedNotification(_) => {
|
||||
self.acknowledged.resources_list_changed == Some(true)
|
||||
}
|
||||
ServerNotification::ResourceUpdatedNotification(update) => self
|
||||
.acknowledged
|
||||
.resource_subscriptions
|
||||
.as_ref()
|
||||
.is_some_and(|uris| uris.contains(&update.params.uri)),
|
||||
ServerNotification::SubscriptionsAcknowledgedNotification(_)
|
||||
| ServerNotification::CancelledNotification(_)
|
||||
| ServerNotification::ProgressNotification(_)
|
||||
| ServerNotification::LoggingMessageNotification(_)
|
||||
| ServerNotification::TaskStatusNotification(_)
|
||||
| ServerNotification::CustomNotification(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&mut self,
|
||||
response: SubscriptionResponse,
|
||||
) -> Result<Option<ServerNotification>, ServiceError> {
|
||||
let response = match response {
|
||||
Ok(response) => response,
|
||||
Err(_) => {
|
||||
self.finish(SubscriptionEnd::Abrupt);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
let response = match response {
|
||||
Ok(response) => response,
|
||||
Err(ServiceError::TransportClosed) => {
|
||||
self.finish(SubscriptionEnd::Abrupt);
|
||||
return Ok(None);
|
||||
}
|
||||
Err(ServiceError::SubscriptionLagged { capacity }) => {
|
||||
self.finish(SubscriptionEnd::Lagged { capacity });
|
||||
return Ok(None);
|
||||
}
|
||||
Err(error) => {
|
||||
self.finish(SubscriptionEnd::Abrupt);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let ServerResult::SubscriptionsListenResult(result) = response else {
|
||||
self.finish(SubscriptionEnd::Abrupt);
|
||||
return Err(ServiceError::UnexpectedResponse);
|
||||
};
|
||||
if !result.result_type.is_complete()
|
||||
|| result.meta.subscription_id().as_ref() != Some(&self.id)
|
||||
{
|
||||
self.finish(SubscriptionEnd::Abrupt);
|
||||
return Err(ServiceError::UnexpectedResponse);
|
||||
}
|
||||
self.finish(SubscriptionEnd::Graceful(result));
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
let Some(request) = self.request.take() else {
|
||||
return;
|
||||
};
|
||||
request.peer.unregister_subscription(&self.id);
|
||||
request.peer.try_cancel_request(
|
||||
self.id.clone(),
|
||||
Some("subscription handle dropped".to_owned()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects how a client establishes its MCP lifecycle.
|
||||
///
|
||||
/// Existing [`ServiceExt::serve`] behavior remains legacy initialization.
|
||||
|
|
@ -621,6 +910,106 @@ macro_rules! method {
|
|||
}
|
||||
|
||||
impl Peer<RoleClient> {
|
||||
/// Open a long-lived notification subscription and wait for its acknowledgment.
|
||||
///
|
||||
/// Notifications routed to the returned [`Subscription`] are not also
|
||||
/// delivered through [`ClientHandler`](crate::ClientHandler) callbacks.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a service, transport, or protocol error when the request cannot
|
||||
/// be established or the acknowledgment is invalid.
|
||||
pub async fn listen(
|
||||
&self,
|
||||
notifications: SubscriptionFilter,
|
||||
) -> Result<Subscription, ServiceError> {
|
||||
self.listen_with_channel_capacity_inner(
|
||||
notifications,
|
||||
DEFAULT_SUBSCRIPTION_CHANNEL_CAPACITY,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Open a subscription with an explicit notification buffer capacity.
|
||||
///
|
||||
/// Notifications routed to the returned [`Subscription`] are not also
|
||||
/// delivered through [`ClientHandler`](crate::ClientHandler) callbacks.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a service, transport, or protocol error when the request cannot
|
||||
/// be established or the acknowledgment is invalid.
|
||||
pub async fn listen_with_capacity(
|
||||
&self,
|
||||
notifications: SubscriptionFilter,
|
||||
channel_capacity: NonZeroUsize,
|
||||
) -> Result<Subscription, ServiceError> {
|
||||
self.listen_with_channel_capacity_inner(notifications, channel_capacity.get())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn listen_with_channel_capacity_inner(
|
||||
&self,
|
||||
notifications: SubscriptionFilter,
|
||||
channel_capacity: usize,
|
||||
) -> Result<Subscription, ServiceError> {
|
||||
let request = ClientRequest::SubscriptionsListenRequest(SubscriptionsListenRequest::new(
|
||||
SubscriptionsListenRequestParams::new(notifications.clone()),
|
||||
));
|
||||
let (handle, mut subscription_notifications) = self
|
||||
.send_subscription_request(request, PeerRequestOptions::no_options(), channel_capacity)
|
||||
.await?;
|
||||
let id = handle.id.clone();
|
||||
let mut pending = PendingSubscriptionRequest::new(handle);
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
notification = subscription_notifications.recv() => {
|
||||
let Some(notification) = notification else {
|
||||
pending.cancel("subscription stream closed before acknowledgment").await;
|
||||
return Err(ServiceError::TransportClosed);
|
||||
};
|
||||
if notification.get_meta().subscription_id().as_ref() != Some(&id) {
|
||||
pending.cancel("subscription acknowledgment ID mismatch").await;
|
||||
return Err(ServiceError::UnexpectedResponse);
|
||||
}
|
||||
let ServerNotification::SubscriptionsAcknowledgedNotification(
|
||||
acknowledgment,
|
||||
) = notification else {
|
||||
pending.cancel("notification received before subscription acknowledgment").await;
|
||||
return Err(ServiceError::UnexpectedResponse);
|
||||
};
|
||||
let accepted = acknowledgment.params.notifications;
|
||||
if !accepted.is_subset_of(¬ifications) {
|
||||
pending.cancel("subscription acknowledged an unrequested filter").await;
|
||||
return Err(ServiceError::UnexpectedResponse);
|
||||
}
|
||||
let Some(handle) = pending.take() else {
|
||||
return Err(ServiceError::TransportClosed);
|
||||
};
|
||||
Ok(Subscription {
|
||||
id,
|
||||
acknowledged: accepted,
|
||||
notifications: subscription_notifications,
|
||||
request: Some(handle),
|
||||
end: None,
|
||||
})
|
||||
}
|
||||
response = pending.recv() => {
|
||||
pending.unregister(&id);
|
||||
pending.disarm();
|
||||
let Some(response) = response else {
|
||||
return Err(ServiceError::TransportClosed);
|
||||
};
|
||||
match response {
|
||||
Ok(Err(error)) => Err(error),
|
||||
Ok(Ok(_)) => Err(ServiceError::UnexpectedResponse),
|
||||
Err(_) => Err(ServiceError::TransportClosed),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Discover the server's supported protocol versions and capabilities.
|
||||
///
|
||||
/// The high-level client currently exposes this peer only after initialization;
|
||||
|
|
@ -716,8 +1105,18 @@ impl Peer<RoleClient> {
|
|||
method!(peer_req list_resources ListResourcesRequest(PaginatedRequestParams)? => ListResourcesResult);
|
||||
method!(peer_req list_resource_templates ListResourceTemplatesRequest(PaginatedRequestParams)? => ListResourceTemplatesResult);
|
||||
method!(peer_req read_resource ReadResourceRequest(ReadResourceRequestParams) => ReadResourceResult);
|
||||
method!(peer_req subscribe SubscribeRequest(SubscribeRequestParams) );
|
||||
method!(peer_req unsubscribe UnsubscribeRequest(UnsubscribeRequestParams));
|
||||
method!(
|
||||
#[deprecated(
|
||||
note = "resources/subscribe is legacy-only; use Peer::listen for protocol version 2026-07-28"
|
||||
)]
|
||||
peer_req subscribe SubscribeRequest(SubscribeRequestParams)
|
||||
);
|
||||
method!(
|
||||
#[deprecated(
|
||||
note = "resources/unsubscribe is legacy-only; cancel the Subscription handle instead"
|
||||
)]
|
||||
peer_req unsubscribe UnsubscribeRequest(UnsubscribeRequestParams)
|
||||
);
|
||||
method!(peer_req call_tool CallToolRequest(CallToolRequestParams) => CallToolResult);
|
||||
method!(peer_req list_tools ListToolsRequest(PaginatedRequestParams)? => ListToolsResult);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected.
|
||||
#![expect(deprecated)]
|
||||
use std::borrow::Cow;
|
||||
#[cfg(feature = "elicitation")]
|
||||
use std::collections::HashSet;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use thiserror::Error;
|
||||
#[cfg(feature = "elicitation")]
|
||||
|
|
@ -20,7 +20,8 @@ use crate::{
|
|||
ProgressNotification, ProgressNotificationParam, PromptListChangedNotification,
|
||||
ProtocolVersion, ResourceListChangedNotification, ResourceUpdatedNotification,
|
||||
ResourceUpdatedNotificationParam, ServerInfo, ServerNotification, ServerRequest,
|
||||
ServerResult, ToolListChangedNotification,
|
||||
ServerResult, SubscriptionFilter, SubscriptionsAcknowledgedNotification,
|
||||
SubscriptionsAcknowledgedNotificationParams, ToolListChangedNotification,
|
||||
},
|
||||
transport::DynamicTransportError,
|
||||
};
|
||||
|
|
@ -41,6 +42,13 @@ impl ServiceRole for RoleServer {
|
|||
|
||||
type InitializeError = ServerInitializeError;
|
||||
const IS_CLIENT: bool = false;
|
||||
|
||||
fn peer_cancelled_params(notification: &Self::PeerNot) -> Option<&CancelledNotificationParam> {
|
||||
match notification {
|
||||
ClientNotification::CancelledNotification(notification) => Some(¬ification.params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// It represents the error that may occur when serving the server.
|
||||
|
|
@ -98,6 +106,284 @@ impl ServerInitializeError {
|
|||
}
|
||||
pub type ClientSink = Peer<RoleServer>;
|
||||
|
||||
/// Failure to send a notification through a [`SubscriptionSink`].
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum SubscriptionSendError {
|
||||
#[error("subscription is no longer active")]
|
||||
SubscriptionClosed,
|
||||
#[error("notification is not allowed on a subscription stream: {0}")]
|
||||
UnsupportedNotification(&'static str),
|
||||
#[error("notification was not accepted for this subscription: {0}")]
|
||||
NotificationNotAccepted(&'static str),
|
||||
#[error(transparent)]
|
||||
Service(#[from] ServiceError),
|
||||
}
|
||||
|
||||
/// A server-side notification sink scoped to one `subscriptions/listen` request.
|
||||
///
|
||||
/// The sink applies the accepted filter and adds the subscription request ID to
|
||||
/// every notification it sends.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SubscriptionSink {
|
||||
peer: Peer<RoleServer>,
|
||||
id: RequestId,
|
||||
accepted: Arc<SubscriptionFilter>,
|
||||
active: CancellationToken,
|
||||
}
|
||||
|
||||
impl SubscriptionSink {
|
||||
fn new(
|
||||
peer: Peer<RoleServer>,
|
||||
id: RequestId,
|
||||
accepted: Arc<SubscriptionFilter>,
|
||||
active: CancellationToken,
|
||||
) -> Self {
|
||||
Self {
|
||||
peer,
|
||||
id,
|
||||
accepted,
|
||||
active,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the JSON-RPC ID of the originating listen request.
|
||||
pub fn id(&self) -> &RequestId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// Return the filter accepted for this subscription.
|
||||
pub fn accepted(&self) -> &SubscriptionFilter {
|
||||
self.accepted.as_ref()
|
||||
}
|
||||
|
||||
/// Send an allowed change notification with subscription metadata attached.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`SubscriptionSendError::SubscriptionClosed`] after the request
|
||||
/// ends, a filter error for disallowed notifications, or a transport error.
|
||||
pub async fn send(
|
||||
&self,
|
||||
mut notification: ServerNotification,
|
||||
) -> Result<(), SubscriptionSendError> {
|
||||
if self.active.is_cancelled() {
|
||||
return Err(SubscriptionSendError::SubscriptionClosed);
|
||||
}
|
||||
match ¬ification {
|
||||
ServerNotification::ToolListChangedNotification(_) => {
|
||||
if self.accepted.tools_list_changed != Some(true) {
|
||||
return Err(SubscriptionSendError::NotificationNotAccepted(
|
||||
"notifications/tools/list_changed",
|
||||
));
|
||||
}
|
||||
}
|
||||
ServerNotification::PromptListChangedNotification(_) => {
|
||||
if self.accepted.prompts_list_changed != Some(true) {
|
||||
return Err(SubscriptionSendError::NotificationNotAccepted(
|
||||
"notifications/prompts/list_changed",
|
||||
));
|
||||
}
|
||||
}
|
||||
ServerNotification::ResourceListChangedNotification(_) => {
|
||||
if self.accepted.resources_list_changed != Some(true) {
|
||||
return Err(SubscriptionSendError::NotificationNotAccepted(
|
||||
"notifications/resources/list_changed",
|
||||
));
|
||||
}
|
||||
}
|
||||
ServerNotification::ResourceUpdatedNotification(update) => {
|
||||
let accepted = self
|
||||
.accepted
|
||||
.resource_subscriptions
|
||||
.as_ref()
|
||||
.is_some_and(|uris| uris.contains(&update.params.uri));
|
||||
if !accepted {
|
||||
return Err(SubscriptionSendError::NotificationNotAccepted(
|
||||
"notifications/resources/updated",
|
||||
));
|
||||
}
|
||||
}
|
||||
ServerNotification::SubscriptionsAcknowledgedNotification(_) => {
|
||||
return Err(SubscriptionSendError::UnsupportedNotification(
|
||||
"notifications/subscriptions/acknowledged",
|
||||
));
|
||||
}
|
||||
ServerNotification::CancelledNotification(_) => {
|
||||
return Err(SubscriptionSendError::UnsupportedNotification(
|
||||
"notifications/cancelled",
|
||||
));
|
||||
}
|
||||
ServerNotification::ProgressNotification(_) => {
|
||||
return Err(SubscriptionSendError::UnsupportedNotification(
|
||||
"notifications/progress",
|
||||
));
|
||||
}
|
||||
ServerNotification::LoggingMessageNotification(_) => {
|
||||
return Err(SubscriptionSendError::UnsupportedNotification(
|
||||
"notifications/message",
|
||||
));
|
||||
}
|
||||
ServerNotification::TaskStatusNotification(_) => {
|
||||
return Err(SubscriptionSendError::UnsupportedNotification(
|
||||
"notifications/tasks/status",
|
||||
));
|
||||
}
|
||||
ServerNotification::CustomNotification(_) => {
|
||||
return Err(SubscriptionSendError::UnsupportedNotification(
|
||||
"custom notification",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
notification
|
||||
.get_meta_mut()
|
||||
.set_subscription_id(self.id.clone());
|
||||
self.peer.send_notification(notification).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send `notifications/tools/list_changed`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Self::send`].
|
||||
pub async fn notify_tool_list_changed(&self) -> Result<(), SubscriptionSendError> {
|
||||
self.send(ServerNotification::ToolListChangedNotification(
|
||||
ToolListChangedNotification {
|
||||
method: Default::default(),
|
||||
extensions: Default::default(),
|
||||
},
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send `notifications/prompts/list_changed`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Self::send`].
|
||||
pub async fn notify_prompt_list_changed(&self) -> Result<(), SubscriptionSendError> {
|
||||
self.send(ServerNotification::PromptListChangedNotification(
|
||||
PromptListChangedNotification {
|
||||
method: Default::default(),
|
||||
extensions: Default::default(),
|
||||
},
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send `notifications/resources/list_changed`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Self::send`].
|
||||
pub async fn notify_resource_list_changed(&self) -> Result<(), SubscriptionSendError> {
|
||||
self.send(ServerNotification::ResourceListChangedNotification(
|
||||
ResourceListChangedNotification {
|
||||
method: Default::default(),
|
||||
extensions: Default::default(),
|
||||
},
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send `notifications/resources/updated` for an accepted URI.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Self::send`].
|
||||
pub async fn notify_resource_updated(
|
||||
&self,
|
||||
uri: impl Into<String>,
|
||||
) -> Result<(), SubscriptionSendError> {
|
||||
self.send(ServerNotification::ResourceUpdatedNotification(
|
||||
ResourceUpdatedNotification::new(ResourceUpdatedNotificationParam::new(uri)),
|
||||
))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Context for one established server-side notification subscription.
|
||||
///
|
||||
/// The acknowledgment has already been sent before this context is handed to
|
||||
/// [`ServerHandler::listen`](crate::ServerHandler::listen).
|
||||
#[derive(Debug)]
|
||||
pub struct SubscriptionContext {
|
||||
request: RequestContext<RoleServer>,
|
||||
requested: SubscriptionFilter,
|
||||
accepted: Arc<SubscriptionFilter>,
|
||||
sink: SubscriptionSink,
|
||||
_active_guard: DropGuard,
|
||||
}
|
||||
|
||||
impl SubscriptionContext {
|
||||
pub(crate) async fn establish(
|
||||
request: RequestContext<RoleServer>,
|
||||
requested: SubscriptionFilter,
|
||||
accepted: SubscriptionFilter,
|
||||
) -> Result<Self, ErrorData> {
|
||||
let active = request.ct.child_token();
|
||||
let accepted = Arc::new(accepted);
|
||||
let sink = SubscriptionSink::new(
|
||||
request.peer.clone(),
|
||||
request.id.clone(),
|
||||
accepted.clone(),
|
||||
active.clone(),
|
||||
);
|
||||
let mut acknowledgment = SubscriptionsAcknowledgedNotification::new(
|
||||
SubscriptionsAcknowledgedNotificationParams::new(accepted.as_ref().clone()),
|
||||
);
|
||||
let mut meta = NotificationMetaObject::new();
|
||||
meta.set_subscription_id(request.id.clone());
|
||||
acknowledgment.extensions.insert(meta);
|
||||
request
|
||||
.peer
|
||||
.send_notification(ServerNotification::SubscriptionsAcknowledgedNotification(
|
||||
acknowledgment,
|
||||
))
|
||||
.await
|
||||
.map_err(|error| {
|
||||
ErrorData::internal_error(
|
||||
format!("failed to acknowledge subscription: {error}"),
|
||||
None,
|
||||
)
|
||||
})?;
|
||||
Ok(Self {
|
||||
request,
|
||||
requested,
|
||||
accepted,
|
||||
sink,
|
||||
_active_guard: active.drop_guard(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the filter requested by the client.
|
||||
pub fn requested(&self) -> &SubscriptionFilter {
|
||||
&self.requested
|
||||
}
|
||||
|
||||
/// Return the subset accepted by the server.
|
||||
pub fn accepted(&self) -> &SubscriptionFilter {
|
||||
self.accepted.as_ref()
|
||||
}
|
||||
|
||||
/// Return a cloneable, filter-enforcing notification sink.
|
||||
pub fn sink(&self) -> &SubscriptionSink {
|
||||
&self.sink
|
||||
}
|
||||
|
||||
/// Wait until the subscription request is cancelled.
|
||||
pub async fn cancelled(&self) {
|
||||
self.request.ct.cancelled().await;
|
||||
}
|
||||
|
||||
/// Access the underlying request context.
|
||||
pub fn request_context(&self) -> &RequestContext<RoleServer> {
|
||||
&self.request
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Service<RoleServer>> ServiceExt<RoleServer> for S {
|
||||
fn serve_with_ct<T, E, A>(
|
||||
self,
|
||||
|
|
|
|||
Loading…
Reference in a new issue