use crate::{
ElicitCommunicator, ElicitError, ElicitErrorKind, ElicitIntrospect, ElicitResult, Elicitation,
ElicitationPattern, Generator, PatternDetails, Prompt, Select, TypeMetadata,
datetime_common::{DateTimeComponents, DateTimeInputMethod},
mcp,
};
use std::time::{Duration, Instant};
use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
crate::default_style!(OffsetDateTime => OffsetDateTimeStyle);
crate::default_style!(PrimitiveDateTime => PrimitiveDateTimeStyle);
crate::default_style!(Instant => InstantStyle);
crate::default_style!(Time => TimeStyle);
crate::default_style!(OffsetDateTimeGenerationMode => OffsetDateTimeGenerationModeStyle);
crate::default_style!(PrimitiveDateTimeGenerationMode => PrimitiveDateTimeGenerationModeStyle);
#[derive(Debug, Clone, Copy)]
pub enum InstantGenerationMode {
Now,
Offset {
seconds: i64,
nanos: u32,
},
}
crate::default_style!(InstantGenerationMode => InstantGenerationModeStyle);
impl Prompt for InstantGenerationMode {
fn prompt() -> Option<&'static str> {
Some("Choose how to generate the instant:")
}
}
impl crate::Select for InstantGenerationMode {
fn options() -> Vec<Self> {
vec![
InstantGenerationMode::Now,
InstantGenerationMode::Offset {
seconds: 0,
nanos: 0,
},
]
}
fn labels() -> Vec<String> {
vec![
"Now (current time)".to_string(),
"Offset (from reference)".to_string(),
]
}
fn from_label(label: &str) -> Option<Self> {
match label {
"Now (current time)" => Some(InstantGenerationMode::Now),
"Offset (from reference)" => Some(InstantGenerationMode::Offset {
seconds: 0,
nanos: 0,
}),
_ => None,
}
}
}
impl Elicitation for InstantGenerationMode {
type Style = InstantGenerationModeStyle;
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
let params = mcp::select_params(
Self::prompt().unwrap_or("Select an option:"),
&Self::labels(),
);
let result = communicator
.call_tool(rmcp::model::CallToolRequestParams {
meta: None,
name: mcp::tool_names::elicit_select().into(),
arguments: Some(params),
task: None,
})
.await?;
let value = mcp::extract_value(result)?;
let label = mcp::parse_string(value)?;
let selected = Self::from_label(&label).ok_or_else(|| {
ElicitError::new(ElicitErrorKind::ParseError(
"Invalid variant selection".to_string(),
))
})?;
match selected {
InstantGenerationMode::Now => Ok(InstantGenerationMode::Now),
InstantGenerationMode::Offset { .. } => {
let seconds = i64::elicit(communicator).await?;
let nanos = u32::elicit(communicator).await?;
Ok(InstantGenerationMode::Offset { seconds, nanos })
}
}
}
}
impl ElicitIntrospect for InstantGenerationMode {
fn pattern() -> ElicitationPattern {
ElicitationPattern::Select
}
fn metadata() -> TypeMetadata {
TypeMetadata {
type_name: "InstantGenerationMode",
description: Self::prompt(),
details: PatternDetails::Select {
options: Self::labels(),
},
}
}
}
#[derive(Debug, Clone)]
pub struct InstantGenerator {
mode: InstantGenerationMode,
reference: Instant,
}
impl InstantGenerator {
pub fn new(mode: InstantGenerationMode) -> Self {
Self {
mode,
reference: Instant::now(),
}
}
pub fn with_reference(mode: InstantGenerationMode, reference: Instant) -> Self {
Self { mode, reference }
}
}
impl Generator for InstantGenerator {
type Target = Instant;
fn generate(&self) -> Instant {
match &self.mode {
InstantGenerationMode::Now => Instant::now(),
InstantGenerationMode::Offset { seconds, nanos } => {
let duration = Duration::new(*seconds as u64, *nanos);
if *seconds >= 0 {
self.reference + duration
} else {
self.reference - Duration::new((-*seconds) as u64, *nanos)
}
}
}
}
}
#[cfg_attr(not(kani), elicitation_macros::instrumented_impl)]
impl Prompt for Instant {
fn prompt() -> Option<&'static str> {
Some("Specify how to create an instant (now vs offset):")
}
}
#[cfg_attr(not(kani), elicitation_macros::instrumented_impl)]
impl Elicitation for Instant {
type Style = InstantStyle;
#[tracing::instrument(skip(communicator), fields(type_name = "Instant"))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
tracing::debug!("Eliciting time::Instant");
let mode = InstantGenerationMode::elicit(communicator).await?;
let generator = InstantGenerator::new(mode);
Ok(generator.generate())
}
#[cfg(kani)]
fn kani_proof() {
InstantGenerationMode::kani_proof();
assert!(
true,
"time::Instant verified via InstantGenerationMode composition + trusted time crate"
);
}
}
impl ElicitIntrospect for Instant {
fn pattern() -> ElicitationPattern {
ElicitationPattern::Primitive
}
fn metadata() -> TypeMetadata {
TypeMetadata {
type_name: "time::Instant",
description: Self::prompt(),
details: PatternDetails::Primitive,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OffsetDateTimeGenerationMode {
Now,
UnixEpoch,
Offset {
seconds: i64,
nanos: i32,
},
}
impl Select for OffsetDateTimeGenerationMode {
fn options() -> Vec<Self> {
vec![
OffsetDateTimeGenerationMode::Now,
OffsetDateTimeGenerationMode::UnixEpoch,
OffsetDateTimeGenerationMode::Offset {
seconds: 0,
nanos: 0,
},
]
}
fn labels() -> Vec<String> {
vec![
"Now (Current UTC)".to_string(),
"Unix Epoch (1970-01-01)".to_string(),
"Offset (Custom)".to_string(),
]
}
fn from_label(label: &str) -> Option<Self> {
match label {
"Now (Current UTC)" => Some(OffsetDateTimeGenerationMode::Now),
"Unix Epoch (1970-01-01)" => Some(OffsetDateTimeGenerationMode::UnixEpoch),
"Offset (Custom)" => Some(OffsetDateTimeGenerationMode::Offset {
seconds: 0,
nanos: 0,
}),
_ => None,
}
}
}
impl Prompt for OffsetDateTimeGenerationMode {
fn prompt() -> Option<&'static str> {
Some("How should OffsetDateTime values be generated?")
}
}
impl Elicitation for OffsetDateTimeGenerationMode {
type Style = OffsetDateTimeGenerationModeStyle;
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
let params = mcp::select_params(
Self::prompt().unwrap_or("Select an option:"),
&Self::labels(),
);
let result = communicator
.call_tool(rmcp::model::CallToolRequestParams {
meta: None,
name: mcp::tool_names::elicit_select().into(),
arguments: Some(params),
task: None,
})
.await?;
let value = mcp::extract_value(result)?;
let label = mcp::parse_string(value)?;
let selected = Self::from_label(&label).ok_or_else(|| {
ElicitError::new(ElicitErrorKind::ParseError(
"Invalid OffsetDateTime generation mode".to_string(),
))
})?;
match selected {
OffsetDateTimeGenerationMode::Now => Ok(OffsetDateTimeGenerationMode::Now),
OffsetDateTimeGenerationMode::UnixEpoch => Ok(OffsetDateTimeGenerationMode::UnixEpoch),
OffsetDateTimeGenerationMode::Offset { .. } => {
let seconds = i64::elicit(communicator).await?;
let nanos = i32::elicit(communicator).await?;
Ok(OffsetDateTimeGenerationMode::Offset { seconds, nanos })
}
}
}
}
impl ElicitIntrospect for OffsetDateTimeGenerationMode {
fn pattern() -> ElicitationPattern {
ElicitationPattern::Select
}
fn metadata() -> TypeMetadata {
TypeMetadata {
type_name: "OffsetDateTimeGenerationMode",
description: Self::prompt(),
details: PatternDetails::Select {
options: Self::labels(),
},
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct OffsetDateTimeGenerator {
mode: OffsetDateTimeGenerationMode,
reference: OffsetDateTime,
}
impl OffsetDateTimeGenerator {
pub fn new(mode: OffsetDateTimeGenerationMode) -> Self {
Self {
mode,
reference: OffsetDateTime::now_utc(),
}
}
pub fn with_reference(mode: OffsetDateTimeGenerationMode, reference: OffsetDateTime) -> Self {
Self { mode, reference }
}
pub fn mode(&self) -> OffsetDateTimeGenerationMode {
self.mode
}
pub fn reference(&self) -> OffsetDateTime {
self.reference
}
}
impl Generator for OffsetDateTimeGenerator {
type Target = OffsetDateTime;
fn generate(&self) -> Self::Target {
match self.mode {
OffsetDateTimeGenerationMode::Now => OffsetDateTime::now_utc(),
OffsetDateTimeGenerationMode::UnixEpoch => OffsetDateTime::UNIX_EPOCH,
OffsetDateTimeGenerationMode::Offset { seconds, nanos } => {
if seconds >= 0 {
self.reference + Duration::new(seconds as u64, nanos as u32)
} else {
self.reference - Duration::new((-seconds) as u64, nanos.unsigned_abs())
}
}
}
}
}
impl Prompt for OffsetDateTime {
fn prompt() -> Option<&'static str> {
Some("Enter datetime with timezone offset:")
}
}
impl Elicitation for OffsetDateTime {
type Style = OffsetDateTimeStyle;
#[tracing::instrument(skip(communicator))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
tracing::debug!("Eliciting OffsetDateTime");
let method = DateTimeInputMethod::elicit(communicator).await?;
tracing::debug!(?method, "Input method selected");
match method {
DateTimeInputMethod::Iso8601String => {
let prompt =
"Enter ISO 8601 datetime with offset (e.g., \"2024-07-11T15:30:00+05:00\"):";
let params = mcp::text_params(prompt);
let result = communicator
.call_tool(rmcp::model::CallToolRequestParams {
meta: None,
name: mcp::tool_names::elicit_text().into(),
arguments: Some(params),
task: None,
})
.await?;
let value = mcp::extract_value(result)?;
let iso_string = mcp::parse_string(value)?;
OffsetDateTime::parse(&iso_string, &time::format_description::well_known::Rfc3339)
.map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid ISO 8601 datetime: {}",
e
)))
})
}
DateTimeInputMethod::ManualComponents => {
let components = DateTimeComponents::elicit(communicator).await?;
let offset_prompt = "Enter timezone offset in hours (e.g., +5 or -8):";
let offset_params = mcp::number_params(offset_prompt, -12, 14);
let offset_result = communicator
.call_tool(rmcp::model::CallToolRequestParams {
meta: None,
name: mcp::tool_names::elicit_number().into(),
arguments: Some(offset_params),
task: None,
})
.await?;
let offset_value = mcp::extract_value(offset_result)?;
let offset_hours = mcp::parse_integer::<i64>(offset_value)? as i32;
let offset = UtcOffset::from_hms(offset_hours as i8, 0, 0).map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid timezone offset: {}",
e
)))
})?;
let date = time::Date::from_calendar_date(
components.year,
time::Month::try_from(components.month).map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid month: {}",
e
)))
})?,
components.day,
)
.map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!("Invalid date: {}", e)))
})?;
let time =
time::Time::from_hms(components.hour, components.minute, components.second)
.map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid time: {}",
e
)))
})?;
Ok(PrimitiveDateTime::new(date, time).assume_offset(offset))
}
}
}
}
impl ElicitIntrospect for OffsetDateTime {
fn pattern() -> ElicitationPattern {
ElicitationPattern::Primitive
}
fn metadata() -> TypeMetadata {
TypeMetadata {
type_name: "time::OffsetDateTime",
description: Self::prompt(),
details: PatternDetails::Primitive,
}
}
}
impl Prompt for PrimitiveDateTime {
fn prompt() -> Option<&'static str> {
Some("Enter datetime (no timezone):")
}
}
impl Elicitation for PrimitiveDateTime {
type Style = PrimitiveDateTimeStyle;
#[tracing::instrument(skip(communicator))]
async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
tracing::debug!("Eliciting PrimitiveDateTime");
let method = DateTimeInputMethod::elicit(communicator).await?;
tracing::debug!(?method, "Input method selected");
match method {
DateTimeInputMethod::Iso8601String => {
let prompt = "Enter datetime (e.g., \"2024-07-11T15:30:00\"):";
let params = mcp::text_params(prompt);
let result = communicator
.call_tool(rmcp::model::CallToolRequestParams {
meta: None,
name: mcp::tool_names::elicit_text().into(),
arguments: Some(params),
task: None,
})
.await?;
let value = mcp::extract_value(result)?;
let iso_string = mcp::parse_string(value)?;
PrimitiveDateTime::parse(
&iso_string,
&time::format_description::well_known::Rfc3339,
)
.map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid datetime: {}",
e
)))
})
}
DateTimeInputMethod::ManualComponents => {
let components = DateTimeComponents::elicit(communicator).await?;
let date = time::Date::from_calendar_date(
components.year,
time::Month::try_from(components.month).map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid month: {}",
e
)))
})?,
components.day,
)
.map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!("Invalid date: {}", e)))
})?;
let time =
time::Time::from_hms(components.hour, components.minute, components.second)
.map_err(|e| {
ElicitError::new(ElicitErrorKind::ParseError(format!(
"Invalid time: {}",
e
)))
})?;
Ok(PrimitiveDateTime::new(date, time))
}
}
}
}
impl ElicitIntrospect for PrimitiveDateTime {
fn pattern() -> ElicitationPattern {
ElicitationPattern::Primitive
}
fn metadata() -> TypeMetadata {
TypeMetadata {
type_name: "time::PrimitiveDateTime",
description: Self::prompt(),
details: PatternDetails::Primitive,
}
}
}