| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -38,6 +38,10 @@ pub struct ToolCallContext<'s, S> { | |||
| 38 | 38 | pub service: &'s S, | |
| 39 | 39 | pub name: Cow<'static, str>, | |
| 40 | 40 | pub arguments: Option<JsonObject>, | |
| 41 | + /// Client responses to input requests from the previous MRTR round. | ||
| 42 | + pub input_responses: Option<crate::model::InputResponses>, | ||
| 43 | + /// Opaque state returned by the server during the previous MRTR round. | ||
| 44 | + pub request_state: Option<String>, | ||
| 41 | 45 | } | |
| 42 | 46 | ||
| 43 | 47 | impl<'s, S> ToolCallContext<'s, S> { | |
@@ -47,6 +51,8 @@ impl<'s, S> ToolCallContext<'s, S> { | |||
| 47 | 51 | meta: _, | |
| 48 | 52 | name, | |
| 49 | 53 | arguments, | |
| 54 | + input_responses, | ||
| 55 | + request_state, | ||
| 50 | 56 | .. | |
| 51 | 57 | }: CallToolRequestParams, | |
| 52 | 58 | request_context: RequestContext<RoleServer>, | |
@@ -56,6 +62,8 @@ impl<'s, S> ToolCallContext<'s, S> { | |||
| 56 | 62 | service, | |
| 57 | 63 | name, | |
| 58 | 64 | arguments, | |
| 65 | + input_responses, | ||
| 66 | + request_state, | ||
| 59 | 67 | } | |
| 60 | 68 | } | |
| 61 | 69 | pub fn name(&self) -> &str { | |
@@ -98,6 +106,12 @@ impl IntoCallToolResult for InputRequiredResult { | |||
| 98 | 106 | } | |
| 99 | 107 | } | |
| 100 | 108 | ||
| 109 | + impl IntoCallToolResult for CallToolResponse { | ||
| 110 | + fn into_call_tool_result(self) -> Result<CallToolResponse, crate::ErrorData> { | ||
| 111 | + Ok(self) | ||
| 112 | + } | ||
| 113 | + } | ||
| 114 | + | ||
| 101 | 115 | impl IntoCallToolResult for crate::ErrorData { | |
| 102 | 116 | fn into_call_tool_result(self) -> Result<CallToolResponse, crate::ErrorData> { | |
| 103 | 117 | Err(self) | |
@@ -193,6 +207,26 @@ impl<S> FromContextPart<ToolCallContext<'_, S>> for ToolName { | |||
| 193 | 207 | } | |
| 194 | 208 | } | |
| 195 | 209 | ||
| 210 | + /// Extracts the opaque state returned by the server during the previous MRTR round. | ||
| 211 | + #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] | ||
| 212 | + pub struct RequestState(pub Option<String>); | ||
| 213 | + | ||
| 214 | + impl<S> FromContextPart<ToolCallContext<'_, S>> for RequestState { | ||
| 215 | + fn from_context_part(context: &mut ToolCallContext<S>) -> Result<Self, crate::ErrorData> { | ||
| 216 | + Ok(Self(context.request_state.take())) | ||
| 217 | + } | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + /// Extracts client responses to input requests from the previous MRTR round. | ||
| 221 | + #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] | ||
| 222 | + pub struct InputResponses(pub Option<crate::model::InputResponses>); | ||
| 223 | + | ||
| 224 | + impl<S> FromContextPart<ToolCallContext<'_, S>> for InputResponses { | ||
| 225 | + fn from_context_part(context: &mut ToolCallContext<S>) -> Result<Self, crate::ErrorData> { | ||
| 226 | + Ok(Self(context.input_responses.take())) | ||
| 227 | + } | ||
| 228 | + } | ||
| 229 | + | ||
| 196 | 230 | // Special implementation for Parameters that handles tool arguments | |
| 197 | 231 | impl<S, P> FromContextPart<ToolCallContext<'_, S>> for Parameters<P> | |
| 198 | 232 | where | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,9 +13,16 @@ use std::sync::{ | |||
| 13 | 13 | ||
| 14 | 14 | use rmcp::{ | |
| 15 | 15 | ClientHandler, ServerHandler, | |
| 16 | + handler::server::{ | ||
| 17 | + tool::{InputResponses as ToolInputResponses, RequestState}, | ||
| 18 | + wrapper::Parameters, | ||
| 19 | + }, | ||
| 16 | 20 | model::*, | |
| 17 | - service::{RequestContext, RoleClient, RoleServer, ServiceError, serve_directly}, | ||
| 21 | + service::{RequestContext, RoleClient, RoleServer, Service, ServiceError, serve_directly}, | ||
| 22 | + tool, tool_handler, tool_router, | ||
| 18 | 23 | }; | |
| 24 | + use schemars::JsonSchema; | ||
| 25 | + use serde::Deserialize; | ||
| 19 | 26 | use serde_json::json; | |
| 20 | 27 | ||
| 21 | 28 | /// A `requestState` value with characters that must survive a byte-exact echo: | |
@@ -66,6 +73,54 @@ fn single_elicitation(state: &str) -> InputRequiredResult { | |||
| 66 | 73 | InputRequiredResult::new(Some(requests), Some(state.into())) | |
| 67 | 74 | } | |
| 68 | 75 | ||
| 76 | + #[derive(Clone)] | ||
| 77 | + struct MacroMrtrServer; | ||
| 78 | + | ||
| 79 | + #[derive(Deserialize, JsonSchema)] | ||
| 80 | + struct MacroMrtrArguments { | ||
| 81 | + greeting: String, | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + #[tool_router] | ||
| 85 | + impl MacroMrtrServer { | ||
| 86 | + #[tool(description = "Greet a user after collecting their name")] | ||
| 87 | + async fn greet( | ||
| 88 | + &self, | ||
| 89 | + Parameters(arguments): Parameters<MacroMrtrArguments>, | ||
| 90 | + RequestState(request_state): RequestState, | ||
| 91 | + ToolInputResponses(input_responses): ToolInputResponses, | ||
| 92 | + ) -> Result<CallToolResponse, ErrorData> { | ||
| 93 | + match request_state.as_deref() { | ||
| 94 | + None => Ok(single_elicitation("macro-state").into()), | ||
| 95 | + Some("macro-state") => { | ||
| 96 | + let name = input_responses | ||
| 97 | + .as_ref() | ||
| 98 | + .and_then(|responses| responses.get("answer")) | ||
| 99 | + .and_then(|response| response["content"]["name"].as_str()) | ||
| 100 | + .ok_or_else(|| ErrorData::invalid_params("missing name response", None))?; | ||
| 101 | + Ok(CallToolResult::success(vec![ContentBlock::text(format!( | ||
| 102 | + "{}, {name}", | ||
| 103 | + arguments.greeting | ||
| 104 | + ))]) | ||
| 105 | + .into()) | ||
| 106 | + } | ||
| 107 | + Some(other) => Err(ErrorData::invalid_params( | ||
| 108 | + format!("unexpected request state {other:?}"), | ||
| 109 | + None, | ||
| 110 | + )), | ||
| 111 | + } | ||
| 112 | + } | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + #[tool_handler] | ||
| 116 | + impl ServerHandler for MacroMrtrServer { | ||
| 117 | + fn get_info(&self) -> ServerInfo { | ||
| 118 | + let mut info = ServerInfo::new(ServerCapabilities::builder().enable_tools().build()); | ||
| 119 | + info.protocol_version = ProtocolVersion::V_2026_07_28; | ||
| 120 | + info | ||
| 121 | + } | ||
| 122 | + } | ||
| 123 | + | ||
| 69 | 124 | impl MrtrServer { | |
| 70 | 125 | fn call_tool_impl( | |
| 71 | 126 | &self, | |
@@ -289,12 +344,13 @@ fn server_info(protocol_version: ProtocolVersion) -> ServerInfo { | |||
| 289 | 344 | ||
| 290 | 345 | /// Runs `body` inside a `LocalSet` so `spawn_local` (used when the `local` | |
| 291 | 346 | /// feature is active) is available, wiring up a connected client/server pair. | |
| 292 | - async fn with_pair<F, Fut>( | ||
| 293 | - server: MrtrServer, | ||
| 347 | + async fn with_pair<S, F, Fut>( | ||
| 348 | + server: S, | ||
| 294 | 349 | client_protocol: ProtocolVersion, | |
| 295 | 350 | body: F, | |
| 296 | 351 | ) -> anyhow::Result<()> | |
| 297 | 352 | where | |
| 353 | + S: Service<RoleServer>, | ||
| 298 | 354 | F: FnOnce(rmcp::service::RunningService<RoleClient, MrtrClient>) -> Fut, | |
| 299 | 355 | Fut: std::future::Future<Output = anyhow::Result<()>>, | |
| 300 | 356 | { | |
@@ -346,6 +402,23 @@ async fn client_auto_fulfills_input_required_tool_call() -> anyhow::Result<()> { | |||
| 346 | 402 | .await | |
| 347 | 403 | } | |
| 348 | 404 | ||
| 405 | + #[tokio::test(flavor = "current_thread")] | ||
| 406 | + async fn tool_macro_receives_mrtr_retry_fields() -> anyhow::Result<()> { | ||
| 407 | + with_pair( | ||
| 408 | + MacroMrtrServer, | ||
| 409 | + ProtocolVersion::V_2026_07_28, | ||
| 410 | + |client| async move { | ||
| 411 | + let arguments = serde_json::from_value(json!({ "greeting": "hello" })).unwrap(); | ||
| 412 | + let result = client | ||
| 413 | + .call_tool(CallToolRequestParams::new("greet").with_arguments(arguments)) | ||
| 414 | + .await?; | ||
| 415 | + assert_eq!(result.content[0].as_text().unwrap().text, "hello, Ferris"); | ||
| 416 | + Ok(()) | ||
| 417 | + }, | ||
| 418 | + ) | ||
| 419 | + .await | ||
| 420 | + } | ||
| 421 | + | ||
| 349 | 422 | #[tokio::test(flavor = "current_thread")] | |
| 350 | 423 | async fn manual_once_returns_input_required_without_retry() -> anyhow::Result<()> { | |
| 351 | 424 | let server = MrtrServer::default(); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments