1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
//! Client wrapper for style-aware elicitation.
use rmcp::service::{Peer, RoleClient};
use std::sync::Arc;
use crate::{
ElicitCommunicator, ElicitResult, Elicitation, ElicitationContext, ElicitationStyle,
StyleContext,
};
/// Client wrapper that carries style context.
///
/// Wraps an RMCP peer and maintains style selections for different types.
/// Each type can have its own style, allowing nested types to use different
/// styles independently.
///
/// Users can provide custom style types for any type by implementing
/// [`ElicitationStyle`] and calling [`with_style`](Self::with_style).
///
/// # Example
///
/// ```rust,ignore
/// use elicitation::{ElicitClient, ElicitationStyle, Elicitation};
///
/// // Define custom style for i32
/// #[derive(Clone, Default)]
/// enum MyI32Style {
/// #[default]
/// Terse,
/// Verbose
/// }
///
/// impl ElicitationStyle for MyI32Style {}
///
/// // Use it
/// let client = ElicitClient::new(peer);
/// let styled = client.with_style::<i32, _>(MyI32Style::Verbose);
/// let age = i32::elicit(&styled).await?;
/// ```
#[derive(Clone)]
pub struct ElicitClient {
peer: Arc<Peer<RoleClient>>,
style_context: StyleContext,
elicitation_context: ElicitationContext,
}
impl ElicitClient {
/// Create a new client wrapper from an RMCP peer.
#[tracing::instrument(skip(peer))]
pub fn new(peer: Arc<Peer<RoleClient>>) -> Self {
tracing::debug!("Creating new ElicitClient");
Self {
peer,
style_context: StyleContext::default(),
elicitation_context: ElicitationContext::default(),
}
}
/// Get the underlying RMCP peer for making tool calls.
#[tracing::instrument(skip(self), level = "trace")]
pub fn peer(&self) -> &Peer<RoleClient> {
&self.peer
}
/// Create a new client with a custom style for a specific type.
///
/// Accepts any style type that implements [`ElicitationStyle`], allowing
/// users to define custom styles for built-in types.
///
/// Returns a new `ElicitClient` with the style added to the context.
/// The original client is unchanged.
///
/// # Example
///
/// ```rust,ignore
/// // Use default style
/// let client = client.with_style::<Config, _>(ConfigStyle::default());
///
/// // Use custom style for i32
/// let client = client.with_style::<i32, _>(MyI32Style::Verbose);
/// ```
#[tracing::instrument(skip(self, style))]
pub fn with_style<T: Elicitation + 'static, S: ElicitationStyle>(&self, style: S) -> Self {
let type_name = std::any::type_name::<T>();
tracing::debug!(type_name, "Setting custom style");
let mut ctx = self.style_context.clone();
// Note: We ignore the error here because this is a convenience builder method
// If the lock is poisoned, the clone will work with the data anyway
let _ = ctx.set_style::<T, S>(style);
Self {
peer: Arc::clone(&self.peer),
style_context: ctx,
elicitation_context: self.elicitation_context.clone(),
}
}
/// Get the current style for a type, or use default if not set.
///
/// This method checks if a custom style was set via `with_style()`.
/// If a style was set, it returns that style. Otherwise, it returns
/// the default style for the type.
///
/// # Example
///
/// ```rust,ignore
/// // Get style - uses custom if set, default otherwise
/// let style = client.style_or_default::<Config>();
/// ```
#[tracing::instrument(skip(self))]
pub fn style_or_default<T: Elicitation + 'static>(&self) -> T::Style
where
T::Style: ElicitationStyle,
{
let type_name = std::any::type_name::<T>();
match self.style_context.get_style::<T, T::Style>() {
Ok(Some(style)) => {
tracing::debug!(type_name, has_custom = true, "Getting style or default");
style
}
Ok(None) => {
tracing::debug!(type_name, has_custom = false, "Getting style or default");
T::Style::default()
}
Err(e) => {
tracing::warn!(type_name, error = %e, "Lock poisoned, using default style");
T::Style::default()
}
}
}
/// Get the current style for a type, eliciting if not set.
///
/// This method checks if a custom style was set via `with_style()`.
/// If a style was set, it returns that style. Otherwise, it elicits
/// the style from the user.
///
/// This enables "auto-selection": styles are only elicited when needed.
///
/// # Example
///
/// ```rust,ignore
/// // Get style - uses custom if set, otherwise asks user
/// let style = client.style_or_elicit::<Config>().await?;
/// ```
#[tracing::instrument(skip(self))]
pub async fn style_or_elicit<T: Elicitation + 'static>(&self) -> ElicitResult<T::Style>
where
T::Style: ElicitationStyle,
{
if let Some(style) = self.style_context.get_style::<T, T::Style>()? {
tracing::debug!(
type_name = std::any::type_name::<T>(),
"Using pre-set style"
);
Ok(style)
} else {
tracing::debug!(type_name = std::any::type_name::<T>(), "Eliciting style");
T::Style::elicit(self).await
}
}
/// Get the current style for a type, or use the default.
///
/// If a custom style has been set via `with_style()`, returns that.
/// Otherwise, returns `T::Style::default()` as fallback.
#[tracing::instrument(skip(self))]
pub async fn current_style<T: Elicitation + 'static>(&self) -> ElicitResult<T::Style>
where
T::Style: Clone + Send + Sync + 'static,
{
// Try to get custom style first
if let Some(style) = self.style_context.get_style::<T, T::Style>()? {
tracing::debug!(type_name = std::any::type_name::<T>(), "Using custom style");
return Ok(style);
}
// Fall back to default
tracing::debug!(
type_name = std::any::type_name::<T>(),
"Using default style"
);
Ok(T::Style::default())
}
}
// Implement ElicitCommunicator for client-side communication
impl ElicitCommunicator for ElicitClient {
async fn send_prompt(&self, prompt: &str) -> ElicitResult<String> {
// TODO: Implement client-side prompt sending
// This likely involves calling an MCP tool and getting the response
let _ = prompt;
let _ = &self.peer;
Err(crate::ElicitError::new(crate::ElicitErrorKind::ParseError(
"Client-side send_prompt not yet implemented".to_string(),
)))
}
async fn call_tool(
&self,
params: rmcp::model::CallToolRequestParams,
) -> Result<rmcp::model::CallToolResult, rmcp::service::ServiceError> {
self.peer.call_tool(params).await
}
fn style_context(&self) -> &StyleContext {
&self.style_context
}
fn with_style<T: 'static, S: ElicitationStyle>(&self, style: S) -> Self {
let mut ctx = self.style_context.clone();
let _ = ctx.set_style::<T, S>(style);
Self {
peer: Arc::clone(&self.peer),
style_context: ctx,
elicitation_context: self.elicitation_context.clone(),
}
}
fn elicitation_context(&self) -> &ElicitationContext {
&self.elicitation_context
}
}