feat(elicitation): implement SEP-1330 Elicitation Enum Schema Improvements (#539)

This commit is contained in:
Pavel Bezglasny 2025-12-24 19:54:20 +01:00 • committed by GitHub
commit e9029ccc99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1666 additions and 149 deletions

View file

@ -105,3 +105,7 @@ path = "src/cimd_auth_streamhttp.rs"
[[example]]
name = "servers_calculator_stdio"
path = "src/calculator_stdio.rs"
[[example]]
name = "elicitation_enum_select"
path = "src/elicitation_enum_inference.rs"

View file

@ -45,6 +45,13 @@ A working MCP server demonstrating elicitation for user name collection.
- JSON Schema validation with schemars
- Tools: `greet_user` (collects name), `reset_name` (clears stored name)
### Elicitation with Enum Inference config (`elicitation_enum_inference.rs`)
A demonstration of elicitation using enum inference configuration for MCP server.
- Runs on HTTP with streaming capabilities
- Uses schemars for elicitation schema generation
- Shows how to configure enum inference for elicitation prompts
### Prompt Standard I/O Server (`prompt_stdio.rs`)
A server demonstrating the prompt framework capabilities.

View file

@ -0,0 +1,189 @@
//! Demonstration how to use enum selection in elicitation forms.
//!
//! This example server allows users to select enum values via elicitation forms.
//! To work with enum inference, it is required to use specific `schemars` attributes and apply some workarounds:
//! - Use `#[schemars(inline)]` to ensure the enum is inlined in the schema.
//! - Use `#[schemars(extend("type" = "string"))]` to manually add the required type field, since `schemars` does not provide it for enums.
//! - Optionally, use `#[schemars(title = "...")]` to provide titles for enum variants.
//! For more details, see: https://docs.rs/schemars/latest/schemars/
use std::{
fmt::{Display, Formatter},
sync::Arc,
};
use rmcp::{
ErrorData as McpError, ServerHandler, elicit_safe,
handler::server::router::tool::ToolRouter,
model::*,
service::{RequestContext, RoleServer},
tool, tool_handler, tool_router,
transport::{
StreamableHttpService, streamable_http_server::session::local::LocalSessionManager,
},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
const BIND_ADDRESS: &str = "127.0.0.1:8000";
#[derive(Debug, Serialize, Deserialize, JsonSchema, Default)]
// inline attribute required to work for schema inference in elicitation forms
#[schemars(inline)]
// schemars does not provide required type field for enums, so we extend it here
#[schemars(extend("type" = "string"))]
enum TitledEnum {
#[schemars(title = "Title for the first value")]
#[default]
FirstValue,
#[schemars(title = "Title for the second value")]
SecondValue,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
// inline attribute required to work for schema inference in elicitation forms
#[schemars(inline)]
enum UntitledEnum {
First,
Second,
Third,
}
fn default_untitled_multi_select() -> Vec<UntitledEnum> {
vec![UntitledEnum::Second, UntitledEnum::Third]
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[schemars(description = "User information")]
struct SelectEnumForm {
pub single_select_untitled: UntitledEnum,
#[schemars(
title = "Single Select Titled",
description = "Description for single select enum",
default
)]
pub single_select_titled: TitledEnum,
#[serde(default = "default_untitled_multi_select")]
pub multi_select_untitled: Vec<UntitledEnum>,
#[schemars(
title = "Multi Select Titled",
description = "Multi Select Description"
)]
pub multi_select_titled: Vec<TitledEnum>,
}
impl Display for SelectEnumForm {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let s = format!(
"Current Selections:\n\
Single Select Untitled: {:?}\n\
Single Select Titled: {:?}\n\
Multi Select Untitled: {:?}\n\
Multi Select Titled: {:?}\n",
self.single_select_untitled,
self.single_select_titled,
self.multi_select_untitled,
self.multi_select_titled,
);
write!(f, "{s}")
}
}
elicit_safe!(SelectEnumForm);
#[derive(Clone)]
struct ElicitationEnumFormServer {
selection: Arc<Mutex<SelectEnumForm>>,
tool_router: ToolRouter<ElicitationEnumFormServer>,
}
#[tool_router]
impl ElicitationEnumFormServer {
pub fn new() -> Self {
Self {
selection: Arc::new(Mutex::new(SelectEnumForm {
single_select_untitled: UntitledEnum::First,
single_select_titled: TitledEnum::FirstValue,
multi_select_untitled: vec![UntitledEnum::Second],
multi_select_titled: vec![TitledEnum::SecondValue],
})),
tool_router: Self::tool_router(),
}
}
#[tool(description = "Get current enum selection form")]
async fn get_enum_form(&self) -> Result<CallToolResult, McpError> {
let guard = self.selection.lock().await;
Ok(CallToolResult::success(vec![Content::text(format!(
"{}",
*guard
))]))
}
#[tool(description = "Set enum selection via elicitation form")]
async fn set_enum_form(
&self,
context: RequestContext<RoleServer>,
) -> Result<CallToolResult, McpError> {
match context
.peer
.elicit::<SelectEnumForm>("Please provide your selection".to_string())
.await
{
Ok(Some(form)) => {
let mut guard = self.selection.lock().await;
*guard = form;
Ok(CallToolResult::success(vec![Content::text(format!(
"Updated Selection:\n{}",
*guard
))]))
}
Ok(None) => {
return Ok(CallToolResult::success(vec![Content::text(
"Elicitation cancelled by user.",
)]));
}
Err(err) => {
return Err(McpError::internal_error(
format!("Elicitation failed: {err}"),
None,
));
}
}
}
}
#[tool_handler]
impl ServerHandler for ElicitationEnumFormServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder().enable_tools().build(),
server_info: Implementation::from_build_env(),
instructions: Some(
"Simple server demonstrating elicitation for enum selection".to_string(),
),
..Default::default()
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "debug".to_string().into()))
.with(tracing_subscriber::fmt::layer())
.init();
let service = StreamableHttpService::new(
|| Ok(ElicitationEnumFormServer::new()),
LocalSessionManager::default().into(),
Default::default(),
);
let router = axum::Router::new().nest_service("/mcp", service);
let tcp_listener = tokio::net::TcpListener::bind(BIND_ADDRESS).await?;
let _ = axum::serve(tcp_listener, router)
.with_graceful_shutdown(async { tokio::signal::ctrl_c().await.unwrap() })
.await;
Ok(())
}