use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use hashbrown::HashMap as HashbrownMap;
use serde_json::Value;
use crate::auth::Principal;
use crate::error::{McpError, McpResult};
use crate::session::McpSession;
#[cfg(feature = "std")]
use crate::session::Cancellable;
#[cfg(feature = "std")]
use std::time::Instant;
use turbomcp_types::{
CreateMessageRequest, CreateMessageResult, ElicitResult, IncludeContext, ProtocolVersion,
SamplingContent, SamplingContentBlock,
};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum TransportType {
#[default]
Stdio,
Http,
WebSocket,
Tcp,
Unix,
Wasm,
Channel,
Unknown,
}
impl TransportType {
#[inline]
pub fn is_network(&self) -> bool {
matches!(self, Self::Http | Self::WebSocket | Self::Tcp)
}
#[inline]
pub fn is_local(&self) -> bool {
matches!(self, Self::Stdio | Self::Unix | Self::Channel)
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Stdio => "stdio",
Self::Http => "http",
Self::WebSocket => "websocket",
Self::Tcp => "tcp",
Self::Unix => "unix",
Self::Wasm => "wasm",
Self::Channel => "channel",
Self::Unknown => "unknown",
}
}
}
impl core::fmt::Display for TransportType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[cfg(feature = "std")]
static MIN_LOG_LEVEL: std::sync::LazyLock<
std::sync::RwLock<std::collections::HashMap<String, usize>>,
> = std::sync::LazyLock::new(|| std::sync::RwLock::new(std::collections::HashMap::new()));
#[cfg(feature = "std")]
pub fn clear_min_log_level(session_id: &str) {
if let Ok(mut map) = MIN_LOG_LEVEL.write() {
map.remove(session_id);
}
}
#[cfg(feature = "std")]
pub const MAX_LOG_NOTIFICATIONS_PER_SECOND: u32 = 100;
#[cfg(feature = "std")]
#[derive(Debug)]
struct LogBudget {
window_start: std::time::Instant,
emitted: u32,
suppressed: u64,
}
#[cfg(feature = "std")]
static PROGRESS_SENT: std::sync::LazyLock<
std::sync::Mutex<std::collections::HashMap<String, f64>>,
> = std::sync::LazyLock::new(|| std::sync::Mutex::new(std::collections::HashMap::new()));
#[cfg(feature = "std")]
static LOG_BUDGET: std::sync::LazyLock<
std::sync::RwLock<std::collections::HashMap<String, LogBudget>>,
> = std::sync::LazyLock::new(|| std::sync::RwLock::new(std::collections::HashMap::new()));
#[cfg(feature = "std")]
pub fn clear_session_log_state(session_id: &str) {
clear_min_log_level(session_id);
if let Ok(mut map) = LOG_BUDGET.write() {
map.remove(session_id);
}
}
const PROGRESS_TOKEN_KEY: &str = "io.turbomcp/progressToken";
#[derive(Debug, Clone, Default)]
pub struct RequestContext {
pub request_id: String,
pub transport: TransportType,
pub user_id: Option<String>,
pub session_id: Option<String>,
pub client_id: Option<String>,
pub metadata: HashbrownMap<String, Value>,
pub principal: Option<Principal>,
pub session: Option<Arc<dyn McpSession>>,
pub headers: Option<HashbrownMap<String, String>>,
#[cfg(feature = "std")]
pub start_time: Option<Instant>,
#[cfg(feature = "std")]
pub cancellation_token: Option<Arc<dyn Cancellable>>,
}
impl RequestContext {
pub fn new() -> Self {
#[cfg(feature = "std")]
{
Self {
request_id: uuid::Uuid::new_v4().to_string(),
..Default::default()
}
}
#[cfg(not(feature = "std"))]
{
Self::default()
}
}
pub fn with_id_and_transport(request_id: impl Into<String>, transport: TransportType) -> Self {
Self {
request_id: request_id.into(),
transport,
..Default::default()
}
}
pub fn with_id(request_id: impl Into<String>) -> Self {
Self {
request_id: request_id.into(),
..Default::default()
}
}
#[inline]
pub fn stdio() -> Self {
Self::new().with_transport(TransportType::Stdio)
}
#[inline]
pub fn http() -> Self {
Self::new().with_transport(TransportType::Http)
}
#[inline]
pub fn websocket() -> Self {
Self::new().with_transport(TransportType::WebSocket)
}
#[inline]
pub fn tcp() -> Self {
Self::new().with_transport(TransportType::Tcp)
}
#[inline]
pub fn unix() -> Self {
Self::new().with_transport(TransportType::Unix)
}
#[inline]
pub fn wasm() -> Self {
Self::new().with_transport(TransportType::Wasm)
}
#[inline]
pub fn channel() -> Self {
Self::new().with_transport(TransportType::Channel)
}
}
impl RequestContext {
#[must_use]
pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
self.request_id = id.into();
self
}
#[must_use]
pub fn with_transport(mut self, transport: TransportType) -> Self {
self.transport = transport;
self
}
#[must_use]
pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
#[must_use]
pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
#[must_use]
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
#[must_use]
pub fn with_principal(mut self, principal: Principal) -> Self {
self.principal = Some(principal);
self
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
#[must_use]
pub fn with_progress_token(mut self, token: Value) -> Self {
self.metadata.insert(PROGRESS_TOKEN_KEY.to_string(), token);
self
}
#[must_use]
pub fn with_session(mut self, session: Arc<dyn McpSession>) -> Self {
self.session = Some(session);
self
}
#[must_use]
pub fn with_headers(mut self, headers: HashbrownMap<String, String>) -> Self {
self.headers = Some(headers);
self
}
#[cfg(feature = "std")]
#[must_use]
pub fn with_start_time(mut self, start: Instant) -> Self {
self.start_time = Some(start);
self
}
#[cfg(feature = "std")]
#[must_use]
pub fn with_cancellation_token(mut self, token: Arc<dyn Cancellable>) -> Self {
self.cancellation_token = Some(token);
self
}
}
impl RequestContext {
pub fn insert_metadata(&mut self, key: impl Into<String>, value: impl Into<Value>) {
self.metadata.insert(key.into(), value.into());
}
pub fn set_principal(&mut self, principal: Principal) {
self.principal = Some(principal);
}
pub fn clear_principal(&mut self) {
self.principal = None;
}
pub fn set_session(&mut self, session: Arc<dyn McpSession>) {
self.session = Some(session);
}
}
impl RequestContext {
#[inline]
pub fn request_id(&self) -> &str {
&self.request_id
}
#[inline]
pub fn has_request_id(&self) -> bool {
!self.request_id.is_empty()
}
#[inline]
pub fn transport(&self) -> TransportType {
self.transport
}
#[inline]
pub fn user_id(&self) -> Option<&str> {
self.user_id.as_deref()
}
#[inline]
pub fn session_id(&self) -> Option<&str> {
self.session_id.as_deref()
}
#[inline]
pub fn client_id(&self) -> Option<&str> {
self.client_id.as_deref()
}
#[inline]
pub fn get_metadata(&self, key: &str) -> Option<&Value> {
self.metadata.get(key)
}
pub fn get_metadata_str(&self, key: &str) -> Option<&str> {
self.metadata.get(key).and_then(|v| v.as_str())
}
#[inline]
pub fn has_metadata(&self, key: &str) -> bool {
self.metadata.contains_key(key)
}
#[inline]
pub fn principal(&self) -> Option<&Principal> {
self.principal.as_ref()
}
pub fn is_authenticated(&self) -> bool {
self.principal.is_some() || self.user_id.is_some()
}
pub fn subject(&self) -> Option<&str> {
self.principal
.as_ref()
.map(|p| p.subject.as_str())
.or(self.user_id.as_deref())
}
#[inline]
pub fn session(&self) -> Option<&Arc<dyn McpSession>> {
self.session.as_ref()
}
#[inline]
pub fn has_session(&self) -> bool {
self.session.is_some()
}
#[inline]
pub fn progress_token(&self) -> Option<&Value> {
self.metadata.get(PROGRESS_TOKEN_KEY)
}
#[inline]
pub fn wants_progress(&self) -> bool {
self.progress_token().is_some()
}
#[cfg(feature = "std")]
pub fn set_min_log_level(&self, level: &str) {
let (Some(session_id), Some(rank)) =
(self.session_id.as_ref(), crate::log_level_rank(level))
else {
return;
};
if let Ok(mut map) = MIN_LOG_LEVEL.write() {
map.insert(session_id.clone(), rank);
}
}
#[cfg(feature = "std")]
#[must_use]
pub fn wants_log(&self, level: &str) -> bool {
let Some(rank) = crate::log_level_rank(level) else {
return true;
};
let Some(session_id) = self.session_id.as_ref() else {
return true;
};
match MIN_LOG_LEVEL.read() {
Ok(map) => map.get(session_id).is_none_or(|minimum| rank >= *minimum),
Err(_) => true,
}
}
#[cfg(not(feature = "std"))]
#[must_use]
pub fn wants_log(&self, _level: &str) -> bool {
true
}
#[cfg(feature = "std")]
pub fn admit_log(&self) -> Option<u64> {
let Some(session_id) = self.session_id.as_ref() else {
return Some(0);
};
let Ok(mut budgets) = LOG_BUDGET.write() else {
return Some(0);
};
let now = std::time::Instant::now();
let budget = budgets.entry(session_id.clone()).or_insert(LogBudget {
window_start: now,
emitted: 0,
suppressed: 0,
});
if now.duration_since(budget.window_start) >= core::time::Duration::from_secs(1) {
budget.window_start = now;
budget.emitted = 0;
}
if budget.emitted >= MAX_LOG_NOTIFICATIONS_PER_SECOND {
budget.suppressed = budget.suppressed.saturating_add(1);
return None;
}
budget.emitted += 1;
Some(core::mem::take(&mut budget.suppressed))
}
#[cfg(not(feature = "std"))]
pub fn admit_log(&self) -> Option<u64> {
Some(0)
}
#[inline]
pub fn headers(&self) -> Option<&HashbrownMap<String, String>> {
self.headers.as_ref()
}
pub fn header(&self, name: &str) -> Option<&str> {
let headers = self.headers.as_ref()?;
headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
#[cfg(feature = "std")]
pub fn elapsed(&self) -> Option<core::time::Duration> {
self.start_time.map(|t| t.elapsed())
}
#[cfg(feature = "std")]
pub fn is_cancelled(&self) -> bool {
self.cancellation_token
.as_ref()
.is_some_and(|c| c.is_cancelled())
}
pub fn roles(&self) -> Vec<String> {
if let Some(p) = &self.principal
&& !p.roles.is_empty()
{
return p.roles.to_vec();
}
self.metadata
.get("auth")
.and_then(|auth| auth.get("roles"))
.and_then(|r| r.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(ToString::to_string))
.collect()
})
.unwrap_or_default()
}
pub fn has_any_role<S: AsRef<str>>(&self, required: &[S]) -> bool {
if required.is_empty() {
return true;
}
let roles = self.roles();
required
.iter()
.any(|need| roles.iter().any(|have| have == need.as_ref()))
}
}
impl RequestContext {
pub async fn sample(&self, request: CreateMessageRequest) -> McpResult<CreateMessageResult> {
if request.task.is_some() {
return Err(McpError::invalid_request(
"task-augmented sampling/createMessage returns CreateTaskResult, which sample() \
cannot return; task-augmented sampling is not supported",
));
}
request.validate().map_err(McpError::invalid_params)?;
let session = self.require_session("sampling/createMessage")?;
self.require_sampling_capability(session, &request).await?;
self.require_representable_sampling(session, &request)
.await?;
let params = serde_json::to_value(request).map_err(|e| {
McpError::invalid_params(alloc::format!("Failed to serialize sampling request: {e}"))
})?;
let result = session.call("sampling/createMessage", params).await?;
serde_json::from_value(result)
.map_err(|e| McpError::internal(alloc::format!("Failed to parse sampling result: {e}")))
}
pub async fn elicit_form(
&self,
message: impl Into<String>,
schema: Value,
) -> McpResult<ElicitResult> {
let session = self.require_session("elicitation/create")?;
self.require_elicitation_capability(session, "form").await?;
let requested: turbomcp_types::ElicitationSchema = serde_json::from_value(schema.clone())
.ok()
.filter(|parsed: &turbomcp_types::ElicitationSchema| parsed.schema_type == "object")
.ok_or_else(|| {
McpError::invalid_params(
"requestedSchema must be an object schema whose properties are all primitives (string, number, integer, boolean, or an enum)",
)
})?;
if session.protocol_version().await? == Some(ProtocolVersion::V2025_06_18) {
requested
.check_representable_on_2025_06_18()
.map_err(McpError::invalid_params)?;
}
let params = serde_json::json!({
"message": message.into(),
"requestedSchema": schema,
});
let result = session.call("elicitation/create", params).await?;
let result: ElicitResult = serde_json::from_value(result).map_err(|e| {
McpError::internal(alloc::format!("Failed to parse elicitation result: {e}"))
})?;
if result.action == turbomcp_types::ElicitAction::Accept {
let empty = Value::Object(serde_json::Map::new());
let content = result.content.as_ref().unwrap_or(&empty);
requested.validate_content(content).map_err(|reason| {
McpError::invalid_params(alloc::format!(
"the client's answer does not match the requested schema: {reason}"
))
})?;
}
Ok(result)
}
pub async fn elicit_url(
&self,
message: impl Into<String>,
url: impl Into<String>,
elicitation_id: impl Into<String>,
) -> McpResult<ElicitResult> {
let session = self.require_session("elicitation/create")?;
self.require_elicitation_capability(session, "url").await?;
let params = serde_json::json!({
"mode": "url",
"message": message.into(),
"url": url.into(),
"elicitationId": elicitation_id.into(),
});
let result = session.call("elicitation/create", params).await?;
serde_json::from_value(result).map_err(|e| {
McpError::internal(alloc::format!("Failed to parse elicitation result: {e}"))
})
}
pub async fn list_roots(&self) -> McpResult<Vec<turbomcp_types::Root>> {
let session = self.require_session("roots/list")?;
if let Some(caps) = session.client_capabilities().await?
&& caps.roots.is_none()
{
return Err(McpError::capability_not_supported(
"client roots capability required for roots/list",
));
}
let result = session.call("roots/list", serde_json::json!({})).await?;
let parsed: turbomcp_types::ListRootsResult = serde_json::from_value(result)
.map_err(|e| McpError::internal(alloc::format!("Failed to parse roots result: {e}")))?;
Ok(parsed.roots)
}
pub async fn notify_resource_updated(&self, uri: impl Into<String>) -> McpResult<()> {
let uri = self.scoped_resource_uri(uri.into());
self.notify_client(
"notifications/resources/updated",
serde_json::json!({ "uri": uri }),
)
.await
}
#[must_use]
pub fn with_resource_uri_scope(mut self, prefix: &str) -> Self {
let scope = match self
.metadata
.get(RESOURCE_URI_SCOPE)
.and_then(Value::as_str)
{
Some(outer) => alloc::format!("{outer}://{prefix}"),
None => prefix.into(),
};
self.metadata
.insert(RESOURCE_URI_SCOPE.into(), Value::String(scope));
self
}
#[must_use]
pub fn scoped_resource_uri(&self, uri: String) -> String {
match self
.metadata
.get(RESOURCE_URI_SCOPE)
.and_then(Value::as_str)
{
Some(scope) => alloc::format!("{scope}://{uri}"),
None => uri,
}
}
pub async fn notify_tools_list_changed(&self) -> McpResult<()> {
self.notify_client("notifications/tools/list_changed", serde_json::json!({}))
.await
}
pub async fn notify_resources_list_changed(&self) -> McpResult<()> {
self.notify_client(
"notifications/resources/list_changed",
serde_json::json!({}),
)
.await
}
pub async fn notify_prompts_list_changed(&self) -> McpResult<()> {
self.notify_client("notifications/prompts/list_changed", serde_json::json!({}))
.await
}
pub async fn notify_elicitation_complete(
&self,
elicitation_id: impl Into<String>,
) -> McpResult<()> {
self.notify_client(
"notifications/elicitation/complete",
serde_json::json!({ "elicitationId": elicitation_id.into() }),
)
.await
}
pub async fn notify_client(&self, method: impl AsRef<str>, params: Value) -> McpResult<()> {
let session = self.require_session(method.as_ref())?;
session.notify(method.as_ref(), params).await
}
pub async fn report_progress(
&self,
progress: f64,
total: Option<f64>,
message: Option<&str>,
) -> McpResult<()> {
let Some(token) = self.progress_token() else {
return Ok(());
};
if self.session.is_none() {
return Ok(());
}
#[cfg(feature = "std")]
if let Some(key) = self.progress_key()
&& let Ok(mut sent) = PROGRESS_SENT.lock()
{
if sent.get(&key).is_some_and(|last| progress <= *last) {
return Ok(());
}
sent.insert(key, progress);
}
let mut params = serde_json::json!({
"progressToken": token,
"progress": progress,
});
if let Some(total) = total {
params["total"] = serde_json::json!(total);
}
if let Some(message) = message {
params["message"] = Value::String(message.to_string());
}
let _ = self.notify_client("notifications/progress", params).await;
Ok(())
}
#[cfg(feature = "std")]
fn progress_key(&self) -> Option<String> {
let token = self.progress_token()?;
Some(alloc::format!(
"{}\u{1f}{}\u{1f}{}",
self.session_id.as_deref().unwrap_or(""),
self.request_id,
token
))
}
#[doc(hidden)]
pub fn end_progress(&self) {
#[cfg(feature = "std")]
if let Some(key) = self.progress_key()
&& let Ok(mut sent) = PROGRESS_SENT.lock()
{
sent.remove(&key);
}
}
fn require_session(&self, op: &str) -> McpResult<&Arc<dyn McpSession>> {
self.session.as_ref().ok_or_else(|| {
McpError::capability_not_supported(alloc::format!(
"Bidirectional session required for {op} but transport does not support it"
))
})
}
async fn require_sampling_capability(
&self,
session: &Arc<dyn McpSession>,
request: &CreateMessageRequest,
) -> McpResult<()> {
let Some(caps) = session.client_capabilities().await? else {
return Ok(());
};
let Some(sampling) = caps.sampling.as_ref() else {
return Err(McpError::capability_not_supported(
"client sampling capability required for sampling/createMessage",
));
};
if (request.tools.is_some() || request.tool_choice.is_some()) && sampling.tools.is_none() {
return Err(McpError::capability_not_supported(
"client sampling.tools capability required for tool-enabled sampling/createMessage",
));
}
let has_sampling_context = match session.protocol_version().await? {
Some(version) => version != ProtocolVersion::V2025_06_18,
None => sampling.tools.is_some(),
};
if matches!(
request.include_context,
Some(IncludeContext::ThisServer | IncludeContext::AllServers)
) && sampling.context.is_none()
&& has_sampling_context
{
return Err(McpError::capability_not_supported(
"client sampling.context capability required for includeContext \
\"thisServer\"/\"allServers\" (soft-deprecated; omit it or use \"none\")",
));
}
Ok(())
}
async fn require_representable_sampling(
&self,
session: &Arc<dyn McpSession>,
request: &CreateMessageRequest,
) -> McpResult<()> {
if session.protocol_version().await? != Some(ProtocolVersion::V2025_06_18) {
return Ok(());
}
for message in &request.messages {
let unrepresentable = match &message.content {
SamplingContentBlock::Multiple(_) => true,
SamplingContentBlock::Single(content) => matches!(
content,
SamplingContent::ToolUse(_) | SamplingContent::ToolResult(_)
),
};
if unrepresentable {
return Err(McpError::invalid_params(
"multi-block and tool_use/tool_result sampling content require protocol \
2025-11-25; this session negotiated 2025-06-18",
));
}
}
Ok(())
}
pub async fn client_supports_sampling_context(&self) -> McpResult<bool> {
let session = self.require_session("sampling/createMessage")?;
Ok(session
.client_capabilities()
.await?
.and_then(|caps| caps.sampling)
.is_some_and(|sampling| sampling.context.is_some()))
}
async fn require_elicitation_capability(
&self,
session: &Arc<dyn McpSession>,
mode: &str,
) -> McpResult<()> {
let Some(caps) = session.client_capabilities().await? else {
return Ok(());
};
let Some(elicitation) = caps.elicitation.as_ref() else {
return Err(McpError::capability_not_supported(
"client elicitation capability required for elicitation/create",
));
};
let supported = match mode {
"form" => elicitation.supports_form(),
"url" => elicitation.supports_url(),
_ => false,
};
if supported {
Ok(())
} else {
Err(McpError::capability_not_supported(alloc::format!(
"client elicitation.{mode} capability required for elicitation/create"
)))
}
}
}
const RESOURCE_URI_SCOPE: &str = "io.turbomcp/resourceUriScope";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transport_type_display() {
assert_eq!(TransportType::Stdio.to_string(), "stdio");
assert_eq!(TransportType::Http.to_string(), "http");
assert_eq!(TransportType::WebSocket.to_string(), "websocket");
assert_eq!(TransportType::Tcp.to_string(), "tcp");
assert_eq!(TransportType::Unix.to_string(), "unix");
assert_eq!(TransportType::Wasm.to_string(), "wasm");
assert_eq!(TransportType::Channel.to_string(), "channel");
assert_eq!(TransportType::Unknown.to_string(), "unknown");
}
#[test]
fn test_transport_type_classification() {
assert!(TransportType::Http.is_network());
assert!(TransportType::WebSocket.is_network());
assert!(TransportType::Tcp.is_network());
assert!(!TransportType::Stdio.is_network());
assert!(TransportType::Stdio.is_local());
assert!(TransportType::Unix.is_local());
assert!(TransportType::Channel.is_local());
assert!(!TransportType::Http.is_local());
}
#[test]
fn test_request_context_new() {
let ctx = RequestContext::with_id_and_transport("test-123", TransportType::Http);
assert_eq!(ctx.request_id(), "test-123");
assert_eq!(ctx.transport(), TransportType::Http);
assert!(ctx.metadata.is_empty());
assert!(!ctx.has_session());
}
#[test]
fn test_request_context_factory_methods() {
assert_eq!(RequestContext::stdio().transport(), TransportType::Stdio);
assert_eq!(RequestContext::http().transport(), TransportType::Http);
assert_eq!(
RequestContext::websocket().transport(),
TransportType::WebSocket
);
assert_eq!(RequestContext::tcp().transport(), TransportType::Tcp);
assert_eq!(RequestContext::unix().transport(), TransportType::Unix);
assert_eq!(RequestContext::wasm().transport(), TransportType::Wasm);
assert_eq!(
RequestContext::channel().transport(),
TransportType::Channel
);
}
#[test]
fn test_request_context_metadata() {
let ctx = RequestContext::with_id_and_transport("1", TransportType::Http)
.with_metadata("key1", "value1")
.with_metadata("count", 42);
assert_eq!(ctx.get_metadata_str("key1"), Some("value1"));
assert_eq!(ctx.get_metadata("count"), Some(&serde_json::json!(42)));
assert_eq!(ctx.get_metadata("key3"), None);
assert!(ctx.has_metadata("key1"));
assert!(!ctx.has_metadata("key3"));
}
#[test]
fn test_request_context_ids() {
let ctx = RequestContext::with_id_and_transport("r", TransportType::Http)
.with_user_id("u")
.with_session_id("s")
.with_client_id("c");
assert_eq!(ctx.user_id(), Some("u"));
assert_eq!(ctx.session_id(), Some("s"));
assert_eq!(ctx.client_id(), Some("c"));
assert!(ctx.is_authenticated());
}
#[test]
fn test_request_context_principal() {
let ctx = RequestContext::with_id_and_transport("1", TransportType::Http);
assert!(!ctx.is_authenticated());
assert!(ctx.principal().is_none());
assert!(ctx.subject().is_none());
let principal = Principal::new("user-123")
.with_email("user@example.com")
.with_role("admin");
let ctx = ctx.with_principal(principal);
assert!(ctx.is_authenticated());
assert_eq!(ctx.subject(), Some("user-123"));
assert!(ctx.principal().unwrap().has_role("admin"));
assert_eq!(ctx.roles(), alloc::vec![String::from("admin")]);
assert!(ctx.has_any_role(&["admin"]));
assert!(!ctx.has_any_role(&["root"]));
}
#[test]
fn test_request_context_default() {
let ctx = RequestContext::default();
assert!(ctx.request_id.is_empty());
assert_eq!(ctx.transport, TransportType::Stdio);
assert!(ctx.metadata.is_empty());
assert!(!ctx.has_session());
}
#[test]
fn test_request_context_headers() {
let mut headers: HashbrownMap<String, String> = HashbrownMap::new();
headers.insert("User-Agent".into(), "Test/1.0".into());
let ctx =
RequestContext::with_id_and_transport("1", TransportType::Http).with_headers(headers);
assert_eq!(ctx.header("user-agent"), Some("Test/1.0"));
assert_eq!(ctx.header("USER-AGENT"), Some("Test/1.0"));
assert_eq!(ctx.header("missing"), None);
}
#[cfg(feature = "std")]
#[tokio::test]
async fn test_sampling_without_session_fails() {
use turbomcp_types::CreateMessageRequest;
let ctx = RequestContext::stdio();
let err = ctx
.sample(CreateMessageRequest::default())
.await
.unwrap_err();
assert_eq!(err.kind, crate::error::ErrorKind::CapabilityNotSupported);
}
}