use std::{collections::BTreeMap, sync::Arc};
use derive_more::{Display, From};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::client::{ELICITATION_COMPLETE_NOTIFICATION, ELICITATION_CREATE_METHOD_NAME};
use crate::tool_call::ToolCallId;
use crate::{IntoOption, Meta, RequestId, SessionId};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
#[serde(transparent)]
#[from(Arc<str>, String, &'static str)]
#[non_exhaustive]
pub struct ElicitationId(pub Arc<str>);
impl ElicitationId {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum StringFormat {
Email,
Uri,
Date,
DateTime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ElicitationSchemaType {
#[default]
Object,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub struct EnumOption {
#[serde(rename = "const")]
pub value: String,
pub title: String,
}
impl EnumOption {
#[must_use]
pub fn new(value: impl Into<String>, title: impl Into<String>) -> Self {
Self {
value: value.into(),
title: title.into(),
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct StringPropertySchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<StringFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<String>>,
#[serde(rename = "oneOf", skip_serializing_if = "Option::is_none")]
pub one_of: Option<Vec<EnumOption>>,
}
impl StringPropertySchema {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn email() -> Self {
Self {
format: Some(StringFormat::Email),
..Default::default()
}
}
#[must_use]
pub fn uri() -> Self {
Self {
format: Some(StringFormat::Uri),
..Default::default()
}
}
#[must_use]
pub fn date() -> Self {
Self {
format: Some(StringFormat::Date),
..Default::default()
}
}
#[must_use]
pub fn date_time() -> Self {
Self {
format: Some(StringFormat::DateTime),
..Default::default()
}
}
#[must_use]
pub fn title(mut self, title: impl IntoOption<String>) -> Self {
self.title = title.into_option();
self
}
#[must_use]
pub fn description(mut self, description: impl IntoOption<String>) -> Self {
self.description = description.into_option();
self
}
#[must_use]
pub fn min_length(mut self, min_length: impl IntoOption<u32>) -> Self {
self.min_length = min_length.into_option();
self
}
#[must_use]
pub fn max_length(mut self, max_length: impl IntoOption<u32>) -> Self {
self.max_length = max_length.into_option();
self
}
#[must_use]
pub fn pattern(mut self, pattern: impl IntoOption<String>) -> Self {
self.pattern = pattern.into_option();
self
}
#[must_use]
pub fn format(mut self, format: impl IntoOption<StringFormat>) -> Self {
self.format = format.into_option();
self
}
#[must_use]
pub fn default_value(mut self, default: impl IntoOption<String>) -> Self {
self.default = default.into_option();
self
}
#[must_use]
pub fn enum_values(mut self, enum_values: impl IntoOption<Vec<String>>) -> Self {
self.enum_values = enum_values.into_option();
self
}
#[must_use]
pub fn one_of(mut self, one_of: impl IntoOption<Vec<EnumOption>>) -> Self {
self.one_of = one_of.into_option();
self
}
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct NumberPropertySchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<f64>,
}
impl NumberPropertySchema {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn title(mut self, title: impl IntoOption<String>) -> Self {
self.title = title.into_option();
self
}
#[must_use]
pub fn description(mut self, description: impl IntoOption<String>) -> Self {
self.description = description.into_option();
self
}
#[must_use]
pub fn minimum(mut self, minimum: impl IntoOption<f64>) -> Self {
self.minimum = minimum.into_option();
self
}
#[must_use]
pub fn maximum(mut self, maximum: impl IntoOption<f64>) -> Self {
self.maximum = maximum.into_option();
self
}
#[must_use]
pub fn default_value(mut self, default: impl IntoOption<f64>) -> Self {
self.default = default.into_option();
self
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct IntegerPropertySchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<i64>,
}
impl IntegerPropertySchema {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn title(mut self, title: impl IntoOption<String>) -> Self {
self.title = title.into_option();
self
}
#[must_use]
pub fn description(mut self, description: impl IntoOption<String>) -> Self {
self.description = description.into_option();
self
}
#[must_use]
pub fn minimum(mut self, minimum: impl IntoOption<i64>) -> Self {
self.minimum = minimum.into_option();
self
}
#[must_use]
pub fn maximum(mut self, maximum: impl IntoOption<i64>) -> Self {
self.maximum = maximum.into_option();
self
}
#[must_use]
pub fn default_value(mut self, default: impl IntoOption<i64>) -> Self {
self.default = default.into_option();
self
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct BooleanPropertySchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
}
impl BooleanPropertySchema {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn title(mut self, title: impl IntoOption<String>) -> Self {
self.title = title.into_option();
self
}
#[must_use]
pub fn description(mut self, description: impl IntoOption<String>) -> Self {
self.description = description.into_option();
self
}
#[must_use]
pub fn default_value(mut self, default: impl IntoOption<bool>) -> Self {
self.default = default.into_option();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ElicitationStringType {
#[default]
String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub struct UntitledMultiSelectItems {
#[serde(rename = "type")]
pub type_: ElicitationStringType,
#[serde(rename = "enum")]
pub values: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub struct TitledMultiSelectItems {
#[serde(rename = "anyOf")]
pub options: Vec<EnumOption>,
}
impl TitledMultiSelectItems {
#[must_use]
pub fn new(options: Vec<EnumOption>) -> Self {
Self { options }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
#[non_exhaustive]
pub enum MultiSelectItems {
Untitled(UntitledMultiSelectItems),
Titled(TitledMultiSelectItems),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct MultiSelectPropertySchema {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<u64>,
pub items: MultiSelectItems,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Vec<String>>,
}
impl MultiSelectPropertySchema {
#[must_use]
pub fn new(values: Vec<String>) -> Self {
Self {
title: None,
description: None,
min_items: None,
max_items: None,
items: MultiSelectItems::Untitled(UntitledMultiSelectItems {
type_: ElicitationStringType::String,
values,
}),
default: None,
}
}
#[must_use]
pub fn titled(options: Vec<EnumOption>) -> Self {
Self {
title: None,
description: None,
min_items: None,
max_items: None,
items: MultiSelectItems::Titled(TitledMultiSelectItems { options }),
default: None,
}
}
#[must_use]
pub fn title(mut self, title: impl IntoOption<String>) -> Self {
self.title = title.into_option();
self
}
#[must_use]
pub fn description(mut self, description: impl IntoOption<String>) -> Self {
self.description = description.into_option();
self
}
#[must_use]
pub fn min_items(mut self, min_items: impl IntoOption<u64>) -> Self {
self.min_items = min_items.into_option();
self
}
#[must_use]
pub fn max_items(mut self, max_items: impl IntoOption<u64>) -> Self {
self.max_items = max_items.into_option();
self
}
#[must_use]
pub fn default_value(mut self, default: impl IntoOption<Vec<String>>) -> Self {
self.default = default.into_option();
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
#[schemars(extend("discriminator" = {"propertyName": "type"}))]
#[non_exhaustive]
pub enum ElicitationPropertySchema {
String(StringPropertySchema),
Number(NumberPropertySchema),
Integer(IntegerPropertySchema),
Boolean(BooleanPropertySchema),
Array(MultiSelectPropertySchema),
}
impl From<StringPropertySchema> for ElicitationPropertySchema {
fn from(schema: StringPropertySchema) -> Self {
Self::String(schema)
}
}
impl From<NumberPropertySchema> for ElicitationPropertySchema {
fn from(schema: NumberPropertySchema) -> Self {
Self::Number(schema)
}
}
impl From<IntegerPropertySchema> for ElicitationPropertySchema {
fn from(schema: IntegerPropertySchema) -> Self {
Self::Integer(schema)
}
}
impl From<BooleanPropertySchema> for ElicitationPropertySchema {
fn from(schema: BooleanPropertySchema) -> Self {
Self::Boolean(schema)
}
}
impl From<MultiSelectPropertySchema> for ElicitationPropertySchema {
fn from(schema: MultiSelectPropertySchema) -> Self {
Self::Array(schema)
}
}
fn default_object_type() -> ElicitationSchemaType {
ElicitationSchemaType::Object
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ElicitationSchema {
#[serde(rename = "type", default = "default_object_type")]
pub type_: ElicitationSchemaType,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default)]
pub properties: BTreeMap<String, ElicitationPropertySchema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl Default for ElicitationSchema {
fn default() -> Self {
Self {
type_: default_object_type(),
title: None,
properties: BTreeMap::new(),
required: None,
description: None,
}
}
}
impl ElicitationSchema {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn title(mut self, title: impl IntoOption<String>) -> Self {
self.title = title.into_option();
self
}
#[must_use]
pub fn description(mut self, description: impl IntoOption<String>) -> Self {
self.description = description.into_option();
self
}
#[must_use]
pub fn property<S>(mut self, name: impl Into<String>, schema: S, required: bool) -> Self
where
S: Into<ElicitationPropertySchema>,
{
let name = name.into();
self.properties.insert(name.clone(), schema.into());
if required {
let required_fields = self.required.get_or_insert_with(Vec::new);
if !required_fields.contains(&name) {
required_fields.push(name);
}
} else if let Some(required_fields) = &mut self.required {
required_fields.retain(|field| field != &name);
if required_fields.is_empty() {
self.required = None;
}
}
self
}
#[must_use]
pub fn string(self, name: impl Into<String>, required: bool) -> Self {
self.property(name, StringPropertySchema::new(), required)
}
#[must_use]
pub fn email(self, name: impl Into<String>, required: bool) -> Self {
self.property(name, StringPropertySchema::email(), required)
}
#[must_use]
pub fn uri(self, name: impl Into<String>, required: bool) -> Self {
self.property(name, StringPropertySchema::uri(), required)
}
#[must_use]
pub fn date(self, name: impl Into<String>, required: bool) -> Self {
self.property(name, StringPropertySchema::date(), required)
}
#[must_use]
pub fn date_time(self, name: impl Into<String>, required: bool) -> Self {
self.property(name, StringPropertySchema::date_time(), required)
}
#[must_use]
pub fn number(self, name: impl Into<String>, min: f64, max: f64, required: bool) -> Self {
self.property(
name,
NumberPropertySchema::new().minimum(min).maximum(max),
required,
)
}
#[must_use]
pub fn integer(self, name: impl Into<String>, min: i64, max: i64, required: bool) -> Self {
self.property(
name,
IntegerPropertySchema::new().minimum(min).maximum(max),
required,
)
}
#[must_use]
pub fn boolean(self, name: impl Into<String>, required: bool) -> Self {
self.property(name, BooleanPropertySchema::new(), required)
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ElicitationCapabilities {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub form: Option<ElicitationFormCapabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<ElicitationUrlCapabilities>,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl ElicitationCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn form(mut self, form: impl IntoOption<ElicitationFormCapabilities>) -> Self {
self.form = form.into_option();
self
}
#[must_use]
pub fn url(mut self, url: impl IntoOption<ElicitationUrlCapabilities>) -> Self {
self.url = url.into_option();
self
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ElicitationFormCapabilities {
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl ElicitationFormCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ElicitationUrlCapabilities {
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl ElicitationUrlCapabilities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ElicitationScope {
Session(ElicitationSessionScope),
Request(ElicitationRequestScope),
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ElicitationSessionScope {
pub session_id: SessionId,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<ToolCallId>,
}
impl ElicitationSessionScope {
#[must_use]
pub fn new(session_id: impl Into<SessionId>) -> Self {
Self {
session_id: session_id.into(),
tool_call_id: None,
}
}
#[must_use]
pub fn tool_call_id(mut self, tool_call_id: impl IntoOption<ToolCallId>) -> Self {
self.tool_call_id = tool_call_id.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ElicitationRequestScope {
pub request_id: RequestId,
}
impl ElicitationRequestScope {
#[must_use]
pub fn new(request_id: impl Into<RequestId>) -> Self {
Self {
request_id: request_id.into(),
}
}
}
impl From<ElicitationSessionScope> for ElicitationScope {
fn from(scope: ElicitationSessionScope) -> Self {
Self::Session(scope)
}
}
impl From<ElicitationRequestScope> for ElicitationScope {
fn from(scope: ElicitationRequestScope) -> Self {
Self::Request(scope)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CreateElicitationRequest {
#[serde(flatten)]
pub mode: ElicitationMode,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl CreateElicitationRequest {
#[must_use]
pub fn new(mode: impl Into<ElicitationMode>, message: impl Into<String>) -> Self {
Self {
mode: mode.into(),
message: message.into(),
meta: None,
}
}
#[must_use]
pub fn scope(&self) -> &ElicitationScope {
self.mode.scope()
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "mode", rename_all = "snake_case")]
#[schemars(extend("discriminator" = {"propertyName": "mode"}))]
#[non_exhaustive]
pub enum ElicitationMode {
Form(ElicitationFormMode),
Url(ElicitationUrlMode),
}
impl From<ElicitationFormMode> for ElicitationMode {
fn from(mode: ElicitationFormMode) -> Self {
Self::Form(mode)
}
}
impl From<ElicitationUrlMode> for ElicitationMode {
fn from(mode: ElicitationUrlMode) -> Self {
Self::Url(mode)
}
}
impl ElicitationMode {
#[must_use]
pub fn scope(&self) -> &ElicitationScope {
match self {
Self::Form(f) => &f.scope,
Self::Url(u) => &u.scope,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ElicitationFormMode {
#[serde(flatten)]
pub scope: ElicitationScope,
pub requested_schema: ElicitationSchema,
}
impl ElicitationFormMode {
#[must_use]
pub fn new(scope: impl Into<ElicitationScope>, requested_schema: ElicitationSchema) -> Self {
Self {
scope: scope.into(),
requested_schema,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ElicitationUrlMode {
#[serde(flatten)]
pub scope: ElicitationScope,
pub elicitation_id: ElicitationId,
#[schemars(extend("format" = "uri"))]
pub url: String,
}
impl ElicitationUrlMode {
#[must_use]
pub fn new(
scope: impl Into<ElicitationScope>,
elicitation_id: impl Into<ElicitationId>,
url: impl Into<String>,
) -> Self {
Self {
scope: scope.into(),
elicitation_id: elicitation_id.into(),
url: url.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CreateElicitationResponse {
#[serde(flatten)]
pub action: ElicitationAction,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl CreateElicitationResponse {
#[must_use]
pub fn new(action: ElicitationAction) -> Self {
Self { action, meta: None }
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "action", rename_all = "snake_case")]
#[schemars(extend("discriminator" = {"propertyName": "action"}))]
#[non_exhaustive]
pub enum ElicitationAction {
Accept(ElicitationAcceptAction),
Decline,
Cancel,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ElicitationAcceptAction {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<BTreeMap<String, ElicitationContentValue>>,
}
impl ElicitationAcceptAction {
#[must_use]
pub fn new() -> Self {
Self { content: None }
}
#[must_use]
pub fn content(
mut self,
content: impl IntoOption<BTreeMap<String, ElicitationContentValue>>,
) -> Self {
self.content = content.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(untagged)]
#[non_exhaustive]
pub enum ElicitationContentValue {
String(String),
Integer(i64),
Number(f64),
Boolean(bool),
StringArray(Vec<String>),
}
impl From<String> for ElicitationContentValue {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<&str> for ElicitationContentValue {
fn from(value: &str) -> Self {
Self::String(value.to_string())
}
}
impl From<i64> for ElicitationContentValue {
fn from(value: i64) -> Self {
Self::Integer(value)
}
}
impl From<i32> for ElicitationContentValue {
fn from(value: i32) -> Self {
Self::Integer(i64::from(value))
}
}
impl From<f64> for ElicitationContentValue {
fn from(value: f64) -> Self {
Self::Number(value)
}
}
impl From<bool> for ElicitationContentValue {
fn from(value: bool) -> Self {
Self::Boolean(value)
}
}
impl From<Vec<String>> for ElicitationContentValue {
fn from(value: Vec<String>) -> Self {
Self::StringArray(value)
}
}
impl From<Vec<&str>> for ElicitationContentValue {
fn from(value: Vec<&str>) -> Self {
Self::StringArray(value.into_iter().map(str::to_string).collect())
}
}
impl Default for ElicitationAcceptAction {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_COMPLETE_NOTIFICATION))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct CompleteElicitationNotification {
pub elicitation_id: ElicitationId,
#[serde(skip_serializing_if = "Option::is_none", rename = "_meta")]
pub meta: Option<Meta>,
}
impl CompleteElicitationNotification {
#[must_use]
pub fn new(elicitation_id: impl Into<ElicitationId>) -> Self {
Self {
elicitation_id: elicitation_id.into(),
meta: None,
}
}
#[must_use]
pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
self.meta = meta.into_option();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UrlElicitationRequiredData {
pub elicitations: Vec<UrlElicitationRequiredItem>,
}
impl UrlElicitationRequiredData {
#[must_use]
pub fn new(elicitations: Vec<UrlElicitationRequiredItem>) -> Self {
Self { elicitations }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UrlElicitationRequiredItem {
pub mode: ElicitationUrlOnlyMode,
pub elicitation_id: ElicitationId,
#[schemars(extend("format" = "uri"))]
pub url: String,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ElicitationUrlOnlyMode {
#[default]
Url,
}
impl UrlElicitationRequiredItem {
#[must_use]
pub fn new(
elicitation_id: impl Into<ElicitationId>,
url: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
mode: ElicitationUrlOnlyMode::Url,
elicitation_id: elicitation_id.into(),
url: url.into(),
message: message.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn form_mode_request_serialization() {
let schema = ElicitationSchema::new().string("name", true);
let req = CreateElicitationRequest::new(
ElicitationFormMode::new(ElicitationSessionScope::new("sess_1"), schema),
"Please enter your name",
);
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["sessionId"], "sess_1");
assert!(json.get("toolCallId").is_none());
assert_eq!(json["mode"], "form");
assert_eq!(json["message"], "Please enter your name");
assert!(json["requestedSchema"].is_object());
assert_eq!(json["requestedSchema"]["type"], "object");
assert_eq!(
json["requestedSchema"]["properties"]["name"]["type"],
"string"
);
let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
assert_eq!(
*roundtripped.scope(),
ElicitationSessionScope::new("sess_1").into()
);
assert_eq!(roundtripped.message, "Please enter your name");
assert!(matches!(roundtripped.mode, ElicitationMode::Form(_)));
}
#[test]
fn url_mode_request_serialization() {
let req = CreateElicitationRequest::new(
ElicitationUrlMode::new(
ElicitationSessionScope::new("sess_2").tool_call_id("tc_1"),
"elic_1",
"https://example.com/auth",
),
"Please authenticate",
);
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["sessionId"], "sess_2");
assert_eq!(json["toolCallId"], "tc_1");
assert_eq!(json["mode"], "url");
assert_eq!(json["elicitationId"], "elic_1");
assert_eq!(json["url"], "https://example.com/auth");
assert_eq!(json["message"], "Please authenticate");
let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
assert_eq!(
*roundtripped.scope(),
ElicitationSessionScope::new("sess_2")
.tool_call_id("tc_1")
.into()
);
assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
}
#[test]
fn response_accept_serialization() {
let resp = CreateElicitationResponse::new(ElicitationAction::Accept(
ElicitationAcceptAction::new().content(BTreeMap::from([(
"name".to_string(),
ElicitationContentValue::from("Alice"),
)])),
));
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["action"], "accept");
assert_eq!(json["content"]["name"], "Alice");
let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
assert!(matches!(
roundtripped.action,
ElicitationAction::Accept(ElicitationAcceptAction {
content: Some(_),
..
})
));
}
#[test]
fn response_decline_serialization() {
let resp = CreateElicitationResponse::new(ElicitationAction::Decline);
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["action"], "decline");
let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
assert!(matches!(roundtripped.action, ElicitationAction::Decline));
}
#[test]
fn response_cancel_serialization() {
let resp = CreateElicitationResponse::new(ElicitationAction::Cancel);
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["action"], "cancel");
let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
}
#[test]
fn url_mode_request_scope_serialization() {
let req = CreateElicitationRequest::new(
ElicitationUrlMode::new(
ElicitationRequestScope::new(RequestId::Number(42)),
"elic_2",
"https://example.com/setup",
),
"Please complete setup",
);
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["requestId"], 42);
assert!(json.get("sessionId").is_none());
assert_eq!(json["mode"], "url");
assert_eq!(json["elicitationId"], "elic_2");
assert_eq!(json["url"], "https://example.com/setup");
assert_eq!(json["message"], "Please complete setup");
let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
assert_eq!(
*roundtripped.scope(),
ElicitationRequestScope::new(RequestId::Number(42)).into()
);
assert!(matches!(roundtripped.mode, ElicitationMode::Url(_)));
}
#[test]
fn request_scope_request_serialization() {
let req = CreateElicitationRequest::new(
ElicitationFormMode::new(
ElicitationRequestScope::new(RequestId::Number(99)),
ElicitationSchema::new().string("workspace", true),
),
"Enter workspace name",
);
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["requestId"], 99);
assert!(json.get("sessionId").is_none());
let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap();
assert_eq!(
*roundtripped.scope(),
ElicitationRequestScope::new(RequestId::Number(99)).into()
);
}
#[test]
fn client_response_serialization_accept() {
use crate::ClientResponse;
let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
ElicitationAction::Accept(ElicitationAcceptAction::new().content(BTreeMap::from([(
"name".to_string(),
ElicitationContentValue::from("Alice"),
)]))),
));
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["action"], "accept");
assert_eq!(json["content"]["name"], "Alice");
let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
assert!(matches!(roundtripped.action, ElicitationAction::Accept(_)));
}
#[test]
fn client_response_serialization_decline() {
use crate::ClientResponse;
let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
ElicitationAction::Decline,
));
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["action"], "decline");
let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
assert!(matches!(roundtripped.action, ElicitationAction::Decline));
}
#[test]
fn client_response_serialization_cancel() {
use crate::ClientResponse;
let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new(
ElicitationAction::Cancel,
));
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["action"], "cancel");
let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap();
assert!(matches!(roundtripped.action, ElicitationAction::Cancel));
}
#[test]
fn request_tolerates_extra_fields() {
let json = json!({
"sessionId": "sess_1",
"mode": "form",
"message": "Enter your name",
"requestedSchema": {
"type": "object",
"properties": {
"name": { "type": "string", "title": "Name" }
},
"required": ["name"]
},
"unknownStringField": "hello",
"unknownNumberField": 42
});
let req: CreateElicitationRequest = serde_json::from_value(json).unwrap();
assert_eq!(*req.scope(), ElicitationSessionScope::new("sess_1").into());
assert_eq!(req.message, "Enter your name");
assert!(matches!(req.mode, ElicitationMode::Form(_)));
}
#[test]
fn completion_notification_serialization() {
let notif = CompleteElicitationNotification::new("elic_1");
let json = serde_json::to_value(¬if).unwrap();
assert_eq!(json["elicitationId"], "elic_1");
let roundtripped: CompleteElicitationNotification = serde_json::from_value(json).unwrap();
assert_eq!(roundtripped.elicitation_id, ElicitationId::new("elic_1"));
}
#[test]
fn capabilities_form_only() {
let caps = ElicitationCapabilities::new().form(ElicitationFormCapabilities::new());
let json = serde_json::to_value(&caps).unwrap();
assert!(json["form"].is_object());
assert!(json.get("url").is_none());
let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
assert!(roundtripped.form.is_some());
assert!(roundtripped.url.is_none());
}
#[test]
fn capabilities_url_only() {
let caps = ElicitationCapabilities::new().url(ElicitationUrlCapabilities::new());
let json = serde_json::to_value(&caps).unwrap();
assert!(json.get("form").is_none());
assert!(json["url"].is_object());
let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
assert!(roundtripped.form.is_none());
assert!(roundtripped.url.is_some());
}
#[test]
fn capabilities_both() {
let caps = ElicitationCapabilities::new()
.form(ElicitationFormCapabilities::new())
.url(ElicitationUrlCapabilities::new());
let json = serde_json::to_value(&caps).unwrap();
assert!(json["form"].is_object());
assert!(json["url"].is_object());
let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap();
assert!(roundtripped.form.is_some());
assert!(roundtripped.url.is_some());
}
#[test]
fn url_elicitation_required_data_serialization() {
let data = UrlElicitationRequiredData::new(vec![UrlElicitationRequiredItem::new(
"elic_1",
"https://example.com/auth",
"Please authenticate",
)]);
let json = serde_json::to_value(&data).unwrap();
assert_eq!(json["elicitations"][0]["mode"], "url");
assert_eq!(json["elicitations"][0]["elicitationId"], "elic_1");
assert_eq!(json["elicitations"][0]["url"], "https://example.com/auth");
let roundtripped: UrlElicitationRequiredData = serde_json::from_value(json).unwrap();
assert_eq!(roundtripped.elicitations.len(), 1);
assert_eq!(
roundtripped.elicitations[0].mode,
ElicitationUrlOnlyMode::Url
);
}
#[test]
fn schema_default_sets_object_type() {
let schema = ElicitationSchema::default();
assert_eq!(schema.type_, ElicitationSchemaType::Object);
assert!(schema.properties.is_empty());
let json = serde_json::to_value(&schema).unwrap();
assert_eq!(json["type"], "object");
}
#[test]
fn schema_builder_serialization() {
let schema = ElicitationSchema::new()
.string("name", true)
.email("email", true)
.integer("age", 0, 150, true)
.boolean("newsletter", false)
.description("User registration");
let json = serde_json::to_value(&schema).unwrap();
assert_eq!(json["type"], "object");
assert_eq!(json["description"], "User registration");
assert_eq!(json["properties"]["name"]["type"], "string");
assert_eq!(json["properties"]["email"]["type"], "string");
assert_eq!(json["properties"]["email"]["format"], "email");
assert_eq!(json["properties"]["age"]["type"], "integer");
assert_eq!(json["properties"]["age"]["minimum"], 0);
assert_eq!(json["properties"]["age"]["maximum"], 150);
assert_eq!(json["properties"]["newsletter"]["type"], "boolean");
let required = json["required"].as_array().unwrap();
assert!(required.contains(&json!("name")));
assert!(required.contains(&json!("email")));
assert!(required.contains(&json!("age")));
assert!(!required.contains(&json!("newsletter")));
let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
assert_eq!(roundtripped.properties.len(), 4);
assert!(roundtripped.required.unwrap().contains(&"name".to_string()));
}
#[test]
fn schema_string_enum_serialization() {
let schema = ElicitationSchema::new().property(
"color",
StringPropertySchema::new().enum_values(vec![
"red".into(),
"green".into(),
"blue".into(),
]),
true,
);
let json = serde_json::to_value(&schema).unwrap();
assert_eq!(json["properties"]["color"]["type"], "string");
let enum_vals = json["properties"]["color"]["enum"].as_array().unwrap();
assert_eq!(enum_vals.len(), 3);
let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("color").unwrap()
{
assert_eq!(s.enum_values.as_ref().unwrap().len(), 3);
} else {
panic!("expected String variant");
}
}
#[test]
fn schema_multi_select_serialization() {
let schema = ElicitationSchema::new().property(
"colors",
MultiSelectPropertySchema::new(vec!["red".into(), "green".into(), "blue".into()])
.min_items(1)
.max_items(3),
false,
);
let json = serde_json::to_value(&schema).unwrap();
assert_eq!(json["properties"]["colors"]["type"], "array");
assert_eq!(json["properties"]["colors"]["items"]["type"], "string");
assert_eq!(json["properties"]["colors"]["minItems"], 1);
assert_eq!(json["properties"]["colors"]["maxItems"], 3);
let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
assert!(matches!(
roundtripped.properties.get("colors").unwrap(),
ElicitationPropertySchema::Array(_)
));
}
#[test]
fn schema_titled_enum_serialization() {
let schema = ElicitationSchema::new().property(
"country",
StringPropertySchema::new().one_of(vec![
EnumOption::new("us", "United States"),
EnumOption::new("uk", "United Kingdom"),
]),
true,
);
let json = serde_json::to_value(&schema).unwrap();
assert_eq!(json["properties"]["country"]["type"], "string");
let one_of = json["properties"]["country"]["oneOf"].as_array().unwrap();
assert_eq!(one_of.len(), 2);
assert_eq!(one_of[0]["const"], "us");
assert_eq!(one_of[0]["title"], "United States");
let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
if let ElicitationPropertySchema::String(s) =
roundtripped.properties.get("country").unwrap()
{
assert_eq!(s.one_of.as_ref().unwrap().len(), 2);
} else {
panic!("expected String variant");
}
}
#[test]
fn schema_number_property_serialization() {
let schema = ElicitationSchema::new().number("rating", 0.0, 5.0, true);
let json = serde_json::to_value(&schema).unwrap();
assert_eq!(json["properties"]["rating"]["type"], "number");
assert_eq!(json["properties"]["rating"]["minimum"], 0.0);
assert_eq!(json["properties"]["rating"]["maximum"], 5.0);
let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
if let ElicitationPropertySchema::Number(n) = roundtripped.properties.get("rating").unwrap()
{
assert_eq!(n.minimum, Some(0.0));
assert_eq!(n.maximum, Some(5.0));
} else {
panic!("expected Number variant");
}
}
#[test]
fn schema_string_format_serialization() {
let schema = ElicitationSchema::new()
.uri("website", true)
.date("birthday", true)
.date_time("updated_at", false);
let json = serde_json::to_value(&schema).unwrap();
assert_eq!(json["properties"]["website"]["type"], "string");
assert_eq!(json["properties"]["website"]["format"], "uri");
assert_eq!(json["properties"]["birthday"]["type"], "string");
assert_eq!(json["properties"]["birthday"]["format"], "date");
assert_eq!(json["properties"]["updated_at"]["type"], "string");
assert_eq!(json["properties"]["updated_at"]["format"], "date-time");
let required = json["required"].as_array().unwrap();
assert!(required.contains(&json!("website")));
assert!(required.contains(&json!("birthday")));
assert!(!required.contains(&json!("updated_at")));
}
#[test]
fn schema_string_pattern_serialization() {
let schema = ElicitationSchema::new().property(
"name",
StringPropertySchema::new()
.min_length(1)
.max_length(64)
.pattern("^[a-zA-Z_][a-zA-Z0-9_]*$"),
true,
);
let json = serde_json::to_value(&schema).unwrap();
assert_eq!(json["properties"]["name"]["type"], "string");
assert_eq!(
json["properties"]["name"]["pattern"],
"^[a-zA-Z_][a-zA-Z0-9_]*$"
);
let roundtripped: ElicitationSchema = serde_json::from_value(json).unwrap();
if let ElicitationPropertySchema::String(s) = roundtripped.properties.get("name").unwrap() {
assert_eq!(s.pattern.as_deref(), Some("^[a-zA-Z_][a-zA-Z0-9_]*$"));
} else {
panic!("expected String variant");
}
}
#[test]
fn schema_property_updates_required_state() {
let schema = ElicitationSchema::new()
.string("name", true)
.email("name", false);
let json = serde_json::to_value(&schema).unwrap();
assert!(json.get("required").is_none());
assert_eq!(json["properties"]["name"]["format"], "email");
}
#[test]
fn schema_rejects_invalid_object_type() {
let err = serde_json::from_value::<ElicitationSchema>(json!({
"type": "array",
"properties": {
"name": {
"type": "string"
}
}
}))
.unwrap_err();
assert!(err.to_string().contains("unknown variant"));
}
#[test]
fn titled_multi_select_items_reject_one_of() {
let err = serde_json::from_value::<TitledMultiSelectItems>(json!({
"oneOf": [
{
"const": "red",
"title": "Red"
}
]
}))
.unwrap_err();
assert!(err.to_string().contains("missing field `anyOf`"));
}
#[test]
fn response_accept_rejects_non_object_content() {
let err = serde_json::from_value::<CreateElicitationResponse>(json!({
"action": "accept",
"content": "Alice"
}))
.unwrap_err();
assert!(err.to_string().contains("invalid type"));
}
#[test]
fn response_accept_rejects_nested_object_content() {
let err = serde_json::from_value::<CreateElicitationResponse>(json!({
"action": "accept",
"content": {
"profile": {
"name": "Alice"
}
}
}))
.unwrap_err();
assert!(err.to_string().contains("data did not match any variant"));
}
#[test]
fn response_accept_allows_primitive_and_string_array_content() {
let response = CreateElicitationResponse::new(ElicitationAction::Accept(
ElicitationAcceptAction::new().content(BTreeMap::from([
("name".to_string(), ElicitationContentValue::from("Alice")),
("age".to_string(), ElicitationContentValue::from(30_i32)),
("score".to_string(), ElicitationContentValue::from(9.5_f64)),
(
"subscribed".to_string(),
ElicitationContentValue::from(true),
),
(
"tags".to_string(),
ElicitationContentValue::from(vec!["rust", "acp"]),
),
])),
));
let json = serde_json::to_value(&response).unwrap();
assert_eq!(json["action"], "accept");
assert_eq!(json["content"]["name"], "Alice");
assert_eq!(json["content"]["age"], 30);
assert_eq!(json["content"]["score"], 9.5);
assert_eq!(json["content"]["subscribed"], true);
assert_eq!(json["content"]["tags"][0], "rust");
assert_eq!(json["content"]["tags"][1], "acp");
}
}