Skip to content

Commit fd54781

Browse files
committed
feat: add elicitation example with user name collection
- Add elicitation server example demonstrating real MCP usage - Implement greet_user tool with context.peer.elicit::<T>() API - Show type-safe elicitation with elicit_safe! macro - Include reset_name tool and MCP Inspector instructions - Update examples documentation and dependencies
1 parent 3c98177 commit fd54781

File tree

3 files changed

+159
-2
lines changed

3 files changed

+159
-2
lines changed

examples/servers/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ rmcp = { workspace = true, features = [
1313
"transport-io",
1414
"transport-streamable-http-server",
1515
"auth",
16+
"elicitation",
17+
"schemars",
1618
] }
1719
tokio = { version = "1", features = [
1820
"macros",
@@ -33,7 +35,7 @@ tracing-subscriber = { version = "0.3", features = [
3335
futures = "0.3"
3436
rand = { version = "0.9", features = ["std"] }
3537
axum = { version = "0.8", features = ["macros"] }
36-
schemars = { version = "1.0", optional = true }
38+
schemars = "1.0"
3739
reqwest = { version = "0.12", features = ["json"] }
3840
chrono = "0.4"
3941
uuid = { version = "1.6", features = ["v4", "serde"] }
@@ -82,3 +84,7 @@ path = "src/counter_hyper_streamable_http.rs"
8284
[[example]]
8385
name = "servers_sampling_stdio"
8486
path = "src/sampling_stdio.rs"
87+
88+
[[example]]
89+
name = "servers_elicitation_stdio"
90+
path = "src/elicitation_stdio.rs"

examples/servers/README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ A server using streamable HTTP transport for MCP communication, with axum.
4747
### Counter Streamable HTTP Server with Hyper (`counter_hyper_streamable_http.rs`)
4848

4949
A server using streamable HTTP transport for MCP communication, with hyper.
50+
5051
- Runs on HTTP with streaming capabilities
5152
- Provides counter tools via HTTP streaming
5253
- Demonstrates streamable HTTP transport configuration
@@ -72,6 +73,16 @@ A simplified OAuth example showing basic token-based authentication.
7273
- Simplified authentication flow
7374
- Good starting point for adding authentication to MCP servers
7475

76+
### Elicitation Demo (`elicitation_stdio.rs`)
77+
78+
A working MCP server demonstrating elicitation for user name collection.
79+
80+
- Real MCP server using rmcp library
81+
- `context.peer.elicit::<T>()` API usage
82+
- Type-safe elicitation with `elicit_safe!` macro
83+
- JSON Schema validation with schemars
84+
- Tools: `greet_user` (collects name), `reset_name` (clears stored name)
85+
7586
## How to Run
7687

7788
Each example can be run using Cargo:
@@ -97,6 +108,9 @@ cargo run --example servers_complex_auth_sse
97108

98109
# Run the simple OAuth SSE server
99110
cargo run --example servers_simple_auth_sse
111+
112+
# Run the elicitation standard I/O server
113+
cargo run --example servers_elicitation_stdio
100114
```
101115

102116
## Testing with MCP Inspector
@@ -120,6 +134,7 @@ These examples use the following main dependencies:
120134
- `uuid`: UUID generation (used in OAuth examples)
121135
- `chrono`: Date and time handling (used in OAuth examples)
122136
- `rand`: Random number generation (used in OAuth examples)
137+
- `schemars`: JSON Schema generation (used in elicitation examples)
123138

124139
## Common Module
125140

@@ -129,4 +144,4 @@ The `common/` directory contains shared code used across examples:
129144
- `calculator.rs`: Calculator tool examples
130145
- `generic_service.rs`: Generic service implementations
131146

132-
This modular approach allows for code reuse and demonstrates how to structure larger MCP server applications.
147+
This modular approach allows for code reuse and demonstrates how to structure larger MCP server applications.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//! Simple MCP Server with Elicitation
2+
//!
3+
//! Demonstrates user name collection via elicitation
4+
5+
use anyhow::Result;
6+
use rmcp::schemars::JsonSchema;
7+
use rmcp::{
8+
ErrorData as McpError, ServerHandler, ServiceExt, elicit_safe,
9+
handler::server::{router::tool::ToolRouter, tool::Parameters},
10+
model::*,
11+
service::{RequestContext, RoleServer},
12+
tool, tool_handler, tool_router,
13+
transport::stdio,
14+
};
15+
use serde::{Deserialize, Serialize};
16+
use std::sync::Arc;
17+
use tokio::sync::Mutex;
18+
use tracing_subscriber::{self, EnvFilter};
19+
20+
/// User information request
21+
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
22+
#[schemars(description = "User information")]
23+
pub struct UserInfo {
24+
#[schemars(description = "User's name")]
25+
pub name: String,
26+
}
27+
28+
// Mark as safe for elicitation
29+
elicit_safe!(UserInfo);
30+
31+
/// Simple tool request
32+
#[derive(Debug, Deserialize, JsonSchema)]
33+
pub struct GreetRequest {
34+
pub greeting: String,
35+
}
36+
37+
/// Simple server with elicitation
38+
#[derive(Clone)]
39+
pub struct ElicitationServer {
40+
user_name: Arc<Mutex<Option<String>>>,
41+
tool_router: ToolRouter<ElicitationServer>,
42+
}
43+
44+
#[tool_router]
45+
impl ElicitationServer {
46+
pub fn new() -> Self {
47+
Self {
48+
user_name: Arc::new(Mutex::new(None)),
49+
tool_router: Self::tool_router(),
50+
}
51+
}
52+
53+
#[tool(description = "Greet user with name collection")]
54+
async fn greet_user(
55+
&self,
56+
context: RequestContext<RoleServer>,
57+
Parameters(request): Parameters<GreetRequest>,
58+
) -> Result<CallToolResult, McpError> {
59+
// Check if we have user name
60+
let current_name = self.user_name.lock().await.clone();
61+
62+
let user_name = if let Some(name) = current_name {
63+
name
64+
} else {
65+
// Request user name via elicitation
66+
match context
67+
.peer
68+
.elicit::<UserInfo>("Please provide your name".to_string())
69+
.await
70+
{
71+
Ok(Some(user_info)) => {
72+
let name = user_info.name.clone();
73+
*self.user_name.lock().await = Some(name.clone());
74+
name
75+
}
76+
Ok(None) => "Guest".to_string(), // Never happen if client checks schema
77+
Err(_) => "Unknown".to_string(),
78+
}
79+
};
80+
81+
Ok(CallToolResult::success(vec![Content::text(format!(
82+
"{} {}!",
83+
request.greeting, user_name
84+
))]))
85+
}
86+
87+
#[tool(description = "Reset stored user name")]
88+
async fn reset_name(&self) -> Result<CallToolResult, McpError> {
89+
*self.user_name.lock().await = None;
90+
Ok(CallToolResult::success(vec![Content::text(
91+
"User name reset. Next greeting will ask for name again.".to_string(),
92+
)]))
93+
}
94+
}
95+
96+
#[tool_handler]
97+
impl ServerHandler for ElicitationServer {
98+
fn get_info(&self) -> ServerInfo {
99+
ServerInfo {
100+
capabilities: ServerCapabilities::builder().enable_tools().build(),
101+
server_info: Implementation::from_build_env(),
102+
instructions: Some(
103+
"Simple server demonstrating elicitation for user name collection".to_string(),
104+
),
105+
..Default::default()
106+
}
107+
}
108+
}
109+
110+
#[tokio::main]
111+
async fn main() -> Result<()> {
112+
tracing_subscriber::fmt()
113+
.with_env_filter(EnvFilter::from_default_env())
114+
.init();
115+
116+
println!("Simple MCP Elicitation Demo");
117+
118+
// Get current executable path for Inspector
119+
let current_exe = std::env::current_exe()
120+
.map(|path| path.display().to_string())
121+
.unwrap();
122+
123+
println!("To test with MCP Inspector:");
124+
println!("1. Run: npx @modelcontextprotocol/inspector");
125+
println!("2. Enter server command: {}", current_exe);
126+
127+
let service = ElicitationServer::new()
128+
.serve(stdio())
129+
.await
130+
.inspect_err(|e| {
131+
tracing::error!("serving error: {:?}", e);
132+
})?;
133+
134+
service.waiting().await?;
135+
Ok(())
136+
}

0 commit comments

Comments
 (0)