use mcpkit_core::capability::{ClientCapabilities, ServerCapabilities};
use mcpkit_core::error::McpError;
use mcpkit_core::protocol::{Notification, ProgressToken, RequestId, Response};
use mcpkit_core::protocol_version::ProtocolVersion;
use mcpkit_core::types::elicitation::{ElicitRequest, ElicitResult, UrlElicitRequest};
use mcpkit_core::types::logging::{LoggingLevel, LoggingMessageNotificationParams};
use mcpkit_core::types::notifications::ProgressNotificationParams;
use mcpkit_core::types::roots::{ListRootsResult, Root};
use mcpkit_core::types::sampling::{CreateMessageRequest, CreateMessageResult};
use mcpkit_core::types::task::TaskId;
use std::borrow::Cow;
use std::future::Future;
use std::pin::Pin;
pub trait Peer: Send + Sync {
fn notify(
&self,
notification: Notification,
) -> Pin<Box<dyn Future<Output = Result<(), McpError>> + Send + '_>>;
fn request(
&self,
method: Cow<'static, str>,
params: Option<serde_json::Value>,
) -> Pin<Box<dyn Future<Output = Result<Response, McpError>> + Send + '_>> {
let _ = (method, params);
Box::pin(async {
Err(McpError::internal(
"this peer does not support server-initiated requests",
))
})
}
}
pub use mcpkit_core::tasks::{CancellationToken, CancelledFuture};
pub struct Context<'a> {
pub request_id: &'a RequestId,
pub progress_token: Option<&'a ProgressToken>,
pub client_caps: &'a ClientCapabilities,
pub server_caps: &'a ServerCapabilities,
pub protocol_version: ProtocolVersion,
peer: &'a dyn Peer,
cancel: CancellationToken,
related_task: Option<TaskId>,
}
static NOTIFICATION_REQUEST_ID: std::sync::LazyLock<RequestId> =
std::sync::LazyLock::new(|| RequestId::String("__notification__".to_string()));
impl<'a> Context<'a> {
#[must_use]
pub fn new(
request_id: &'a RequestId,
progress_token: Option<&'a ProgressToken>,
client_caps: &'a ClientCapabilities,
server_caps: &'a ServerCapabilities,
protocol_version: ProtocolVersion,
peer: &'a dyn Peer,
) -> Self {
Self {
request_id,
progress_token,
client_caps,
server_caps,
protocol_version,
peer,
cancel: CancellationToken::new(),
related_task: None,
}
}
#[must_use]
pub fn with_cancellation(
request_id: &'a RequestId,
progress_token: Option<&'a ProgressToken>,
client_caps: &'a ClientCapabilities,
server_caps: &'a ServerCapabilities,
protocol_version: ProtocolVersion,
peer: &'a dyn Peer,
cancel: CancellationToken,
) -> Self {
Self {
request_id,
progress_token,
client_caps,
server_caps,
protocol_version,
peer,
cancel,
related_task: None,
}
}
#[must_use]
pub fn for_notification(
client_caps: &'a ClientCapabilities,
server_caps: &'a ServerCapabilities,
protocol_version: ProtocolVersion,
peer: &'a dyn Peer,
) -> Self {
Self {
request_id: &NOTIFICATION_REQUEST_ID,
progress_token: None,
client_caps,
server_caps,
protocol_version,
peer,
cancel: CancellationToken::new(),
related_task: None,
}
}
#[must_use]
pub fn with_related_task(mut self, task_id: TaskId) -> Self {
self.related_task = Some(task_id);
self
}
#[must_use]
pub const fn related_task(&self) -> Option<&TaskId> {
self.related_task.as_ref()
}
fn tag_related_task(&self, params: serde_json::Value) -> serde_json::Value {
match self.related_task.as_ref() {
Some(id) => mcpkit_core::tasks::inject_related_task(params, id),
None => params,
}
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.cancel.is_cancelled()
}
pub fn cancelled(&self) -> impl Future<Output = ()> + '_ {
self.cancel.cancelled()
}
#[must_use]
pub const fn cancellation_token(&self) -> &CancellationToken {
&self.cancel
}
pub async fn notify(
&self,
method: &str,
params: Option<serde_json::Value>,
) -> Result<(), McpError> {
let notification = if let Some(p) = params {
Notification::with_params(method.to_string(), p)
} else {
Notification::new(method.to_string())
};
self.peer.notify(notification).await
}
pub async fn progress(
&self,
current: f64,
total: Option<f64>,
message: Option<&str>,
) -> Result<(), McpError> {
let Some(token) = self.progress_token else {
return Ok(());
};
let params = ProgressNotificationParams {
total,
message: message.map(String::from),
..ProgressNotificationParams::new(token.clone(), current)
};
self.notify(
mcpkit_core::methods::notifications::PROGRESS,
Some(serde_json::to_value(params)?),
)
.await
}
pub async fn log(
&self,
level: LoggingLevel,
logger: Option<&str>,
data: serde_json::Value,
) -> Result<(), McpError> {
let params = LoggingMessageNotificationParams {
logger: logger.map(String::from),
..LoggingMessageNotificationParams::new(level, data)
};
self.notify(
mcpkit_core::methods::notifications::MESSAGE,
Some(serde_json::to_value(params)?),
)
.await
}
pub async fn request(
&self,
method: impl Into<Cow<'static, str>>,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value, McpError> {
use futures::future::{Either, select};
let request = self.peer.request(method.into(), params);
let cancelled = self.cancel.cancelled();
let response = match select(request, cancelled).await {
Either::Left((result, _)) => result?,
Either::Right(((), _)) => return Err(McpError::internal("request cancelled")),
};
if let Some(error) = response.error {
return Err(McpError::internal(error.message));
}
response
.result
.ok_or_else(|| McpError::internal("response contained neither result nor error"))
}
pub async fn elicit(&self, request: ElicitRequest) -> Result<ElicitResult, McpError> {
if !self.protocol_version.supports_elicitation() {
return Err(McpError::internal(
"the negotiated protocol version does not support elicitation",
));
}
if !self.client_caps.has_elicitation() {
return Err(McpError::internal(
"the client did not declare the elicitation capability",
));
}
let params = self.tag_related_task(serde_json::to_value(&request).map_err(McpError::from)?);
let result = self.request("elicitation/create", Some(params)).await?;
serde_json::from_value(result).map_err(McpError::from)
}
pub async fn list_roots(&self) -> Result<Vec<Root>, McpError> {
if !self.client_caps.has_roots() {
return Err(McpError::internal(
"the client did not declare the roots capability",
));
}
let result = self.request("roots/list", None).await?;
let result: ListRootsResult = serde_json::from_value(result).map_err(McpError::from)?;
Ok(result.roots)
}
pub async fn elicit_url(&self, request: UrlElicitRequest) -> Result<ElicitResult, McpError> {
if !self.protocol_version.supports_elicitation() {
return Err(McpError::internal(
"the negotiated protocol version does not support elicitation",
));
}
if !self.client_caps.has_url_elicitation() {
return Err(McpError::internal(
"the client did not declare URL-mode elicitation support",
));
}
let params = self.tag_related_task(serde_json::to_value(&request).map_err(McpError::from)?);
let result = self.request("elicitation/create", Some(params)).await?;
serde_json::from_value(result).map_err(McpError::from)
}
pub async fn create_message(
&self,
request: CreateMessageRequest,
) -> Result<CreateMessageResult, McpError> {
if !self.client_caps.has_sampling() {
return Err(McpError::internal(
"the client did not declare the sampling capability",
));
}
let params = self.tag_related_task(serde_json::to_value(&request).map_err(McpError::from)?);
let result = self.request("sampling/createMessage", Some(params)).await?;
serde_json::from_value(result).map_err(McpError::from)
}
}
impl std::fmt::Debug for Context<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Context")
.field("request_id", &self.request_id)
.field("progress_token", &self.progress_token)
.field("client_caps", &self.client_caps)
.field("server_caps", &self.server_caps)
.field("protocol_version", &self.protocol_version)
.field("is_cancelled", &self.is_cancelled())
.finish()
}
}
#[derive(Debug, Clone, Copy)]
pub struct NoOpPeer;
impl Peer for NoOpPeer {
fn notify(
&self,
_notification: Notification,
) -> Pin<Box<dyn Future<Output = Result<(), McpError>> + Send + '_>> {
Box::pin(async { Ok(()) })
}
}
pub struct ContextData {
pub request_id: RequestId,
pub progress_token: Option<ProgressToken>,
pub client_caps: ClientCapabilities,
pub server_caps: ServerCapabilities,
pub protocol_version: ProtocolVersion,
}
impl ContextData {
#[must_use]
pub const fn new(
request_id: RequestId,
client_caps: ClientCapabilities,
server_caps: ServerCapabilities,
protocol_version: ProtocolVersion,
) -> Self {
Self {
request_id,
progress_token: None,
client_caps,
server_caps,
protocol_version,
}
}
#[must_use]
pub fn with_progress_token(mut self, token: ProgressToken) -> Self {
self.progress_token = Some(token);
self
}
#[must_use]
pub fn to_context<'a>(&'a self, peer: &'a dyn Peer) -> Context<'a> {
Context::new(
&self.request_id,
self.progress_token.as_ref(),
&self.client_caps,
&self.server_caps,
self.protocol_version,
peer,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_creation() {
let request_id = RequestId::Number(1);
let client_caps = ClientCapabilities::default();
let server_caps = ServerCapabilities::default();
let peer = NoOpPeer;
let ctx = Context::new(
&request_id,
None,
&client_caps,
&server_caps,
ProtocolVersion::LATEST,
&peer,
);
assert!(!ctx.is_cancelled());
assert!(ctx.progress_token.is_none());
assert_eq!(ctx.protocol_version, ProtocolVersion::LATEST);
}
#[test]
fn test_context_with_progress_token() {
let request_id = RequestId::Number(1);
let progress_token = ProgressToken::String("token".to_string());
let client_caps = ClientCapabilities::default();
let server_caps = ServerCapabilities::default();
let peer = NoOpPeer;
let ctx = Context::new(
&request_id,
Some(&progress_token),
&client_caps,
&server_caps,
ProtocolVersion::V2025_03_26,
&peer,
);
assert!(ctx.progress_token.is_some());
assert_eq!(ctx.protocol_version, ProtocolVersion::V2025_03_26);
}
#[test]
fn test_context_data() {
let data = ContextData::new(
RequestId::Number(42),
ClientCapabilities::default(),
ServerCapabilities::default(),
ProtocolVersion::V2025_06_18,
)
.with_progress_token(ProgressToken::String("test".to_string()));
let peer = NoOpPeer;
let ctx = data.to_context(&peer);
assert!(ctx.progress_token.is_some());
assert_eq!(ctx.protocol_version, ProtocolVersion::V2025_06_18);
assert!(ctx.protocol_version.supports_elicitation());
assert!(!ctx.protocol_version.supports_tasks()); }
#[tokio::test]
async fn list_roots_requests_and_parses_when_advertised() {
use mcpkit_core::protocol::Response;
struct RootsPeer;
impl Peer for RootsPeer {
fn notify(
&self,
_n: Notification,
) -> Pin<Box<dyn Future<Output = Result<(), McpError>> + Send + '_>> {
Box::pin(async { Ok(()) })
}
fn request(
&self,
method: Cow<'static, str>,
_params: Option<serde_json::Value>,
) -> Pin<Box<dyn Future<Output = Result<Response, McpError>> + Send + '_>> {
assert_eq!(method, "roots/list");
let result = serde_json::to_value(ListRootsResult {
roots: vec![Root::new("file:///a").name("a")],
meta: None,
})
.unwrap();
Box::pin(async move { Ok(Response::success(RequestId::Number(1), result)) })
}
}
let request_id = RequestId::Number(1);
let server_caps = ServerCapabilities::default();
let peer = RootsPeer;
let client_caps = ClientCapabilities::default().with_roots();
let ctx = Context::new(
&request_id,
None,
&client_caps,
&server_caps,
ProtocolVersion::LATEST,
&peer,
);
let roots = ctx.list_roots().await.expect("roots listed");
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].name.as_deref(), Some("a"));
let no_roots = ClientCapabilities::default();
let ctx = Context::new(
&request_id,
None,
&no_roots,
&server_caps,
ProtocolVersion::LATEST,
&peer,
);
assert!(ctx.list_roots().await.is_err());
}
}