chore: declare and check MSRV (#1034)
* chore: declare and check MSRV * fix: clippy fixes for collapsing if-statements * ci: add explicit contents read permission to MSRV job
This commit is contained in:
parent
e660b805ee
commit
76988023b2
22 changed files with 322 additions and 315 deletions
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
|
|
@ -205,6 +205,30 @@ jobs:
|
||||||
- name: Spell Check Repo
|
- name: Spell Check Repo
|
||||||
uses: crate-ci/typos@master
|
uses: crate-ci/typos@master
|
||||||
|
|
||||||
|
msrv:
|
||||||
|
name: Check MSRV
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Read MSRV from Cargo.toml
|
||||||
|
run: |
|
||||||
|
MSRV=$(cargo metadata --no-deps --format-version 1 \
|
||||||
|
| jq -r '.packages[] | select(.name == "rmcp") | .rust_version')
|
||||||
|
echo "MSRV=$MSRV" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Install MSRV Rust
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.MSRV }}
|
||||||
|
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Check default workspace members with MSRV
|
||||||
|
run: cargo +${{ env.MSRV }} check --all-targets --all-features
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Run Tests
|
name: Run Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ rmcp-macros = { version = "3.0.0-beta.1", path = "./crates/rmcp-macros" }
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
rust-version = "1.88"
|
||||||
version = "3.0.0-beta.1"
|
version = "3.0.0-beta.1"
|
||||||
authors = ["4t145 <u4t145@163.com>"]
|
authors = ["4t145 <u4t145@163.com>"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,2 @@
|
||||||
msrv = "1.85"
|
|
||||||
too-many-arguments-threshold = 10
|
too-many-arguments-threshold = 10
|
||||||
check-private-items = false
|
check-private-items = false
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ name = "rmcp-macros"
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
version = { workspace = true }
|
version = { workspace = true }
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
repository = { workspace = true }
|
repository = { workspace = true }
|
||||||
homepage = { workspace = true }
|
homepage = { workspace = true }
|
||||||
readme = { workspace = true }
|
readme = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -55,17 +55,15 @@ pub fn extract_doc_line(
|
||||||
/// Returns the full Parameters<T> type if found
|
/// Returns the full Parameters<T> type if found
|
||||||
pub fn find_parameters_type_in_sig(sig: &Signature) -> Option<Box<Type>> {
|
pub fn find_parameters_type_in_sig(sig: &Signature) -> Option<Box<Type>> {
|
||||||
sig.inputs.iter().find_map(|input| {
|
sig.inputs.iter().find_map(|input| {
|
||||||
if let FnArg::Typed(pat_type) = input {
|
if let FnArg::Typed(pat_type) = input
|
||||||
if let Type::Path(type_path) = &*pat_type.ty {
|
&& let Type::Path(type_path) = &*pat_type.ty
|
||||||
if type_path
|
&& type_path
|
||||||
.path
|
.path
|
||||||
.segments
|
.segments
|
||||||
.last()
|
.last()
|
||||||
.is_some_and(|type_name| type_name.ident == "Parameters")
|
.is_some_and(|type_name| type_name.ident == "Parameters")
|
||||||
{
|
{
|
||||||
return Some(pat_type.ty.clone());
|
return Some(pat_type.ty.clone());
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -132,13 +132,13 @@ pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream>
|
||||||
// 3. make body: { Box::pin(async move { #body }) }
|
// 3. make body: { Box::pin(async move { #body }) }
|
||||||
let new_output = syn::parse2::<ReturnType>({
|
let new_output = syn::parse2::<ReturnType>({
|
||||||
let mut lt = quote! { 'static };
|
let mut lt = quote! { 'static };
|
||||||
if let Some(receiver) = fn_item.sig.receiver() {
|
if let Some(receiver) = fn_item.sig.receiver()
|
||||||
if let Some((_, receiver_lt)) = receiver.reference.as_ref() {
|
&& let Some((_, receiver_lt)) = receiver.reference.as_ref()
|
||||||
if let Some(receiver_lt) = receiver_lt {
|
{
|
||||||
lt = quote! { #receiver_lt };
|
if let Some(receiver_lt) = receiver_lt {
|
||||||
} else {
|
lt = quote! { #receiver_lt };
|
||||||
lt = quote! { '_ };
|
} else {
|
||||||
}
|
lt = quote! { '_ };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match &fn_item.sig.output {
|
match &fn_item.sig.output {
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,13 @@ use crate::common::extract_doc_line;
|
||||||
|
|
||||||
/// Check if a type is Json<T> and extract the inner type T
|
/// Check if a type is Json<T> and extract the inner type T
|
||||||
fn extract_json_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
|
fn extract_json_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
|
||||||
if let syn::Type::Path(type_path) = ty {
|
if let syn::Type::Path(type_path) = ty
|
||||||
if let Some(last_segment) = type_path.path.segments.last() {
|
&& let Some(last_segment) = type_path.path.segments.last()
|
||||||
if last_segment.ident == "Json" {
|
&& last_segment.ident == "Json"
|
||||||
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments {
|
&& let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments
|
||||||
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first() {
|
&& let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
|
||||||
return Some(inner_type);
|
{
|
||||||
}
|
return Some(inner_type);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -286,13 +283,13 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
|
||||||
let omit_send = cfg!(feature = "local") || attribute.local;
|
let omit_send = cfg!(feature = "local") || attribute.local;
|
||||||
let new_output = syn::parse2::<ReturnType>({
|
let new_output = syn::parse2::<ReturnType>({
|
||||||
let mut lt = quote! { 'static };
|
let mut lt = quote! { 'static };
|
||||||
if let Some(receiver) = fn_item.sig.receiver() {
|
if let Some(receiver) = fn_item.sig.receiver()
|
||||||
if let Some((_, receiver_lt)) = receiver.reference.as_ref() {
|
&& let Some((_, receiver_lt)) = receiver.reference.as_ref()
|
||||||
if let Some(receiver_lt) = receiver_lt {
|
{
|
||||||
lt = quote! { #receiver_lt };
|
if let Some(receiver_lt) = receiver_lt {
|
||||||
} else {
|
lt = quote! { #receiver_lt };
|
||||||
lt = quote! { '_ };
|
} else {
|
||||||
}
|
lt = quote! { '_ };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match &fn_item.sig.output {
|
match &fn_item.sig.output {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ name = "rmcp"
|
||||||
license = { workspace = true }
|
license = { workspace = true }
|
||||||
version = { workspace = true }
|
version = { workspace = true }
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
repository = { workspace = true }
|
repository = { workspace = true }
|
||||||
homepage = { workspace = true }
|
homepage = { workspace = true }
|
||||||
readme = { workspace = true }
|
readme = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -550,10 +550,10 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify_if_visible(&self, name: &str) {
|
fn notify_if_visible(&self, name: &str) {
|
||||||
if self.map.contains_key(name) {
|
if self.map.contains_key(name)
|
||||||
if let Some(notifier) = &self.notifier {
|
&& let Some(notifier) = &self.notifier
|
||||||
notifier();
|
{
|
||||||
}
|
notifier();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -983,17 +983,15 @@ impl EnumSchemaBuilder<MultiSelect> {
|
||||||
return Err("One of the provided default values is not in enum values".to_string());
|
return Err("One of the provided default values is not in enum values".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(min) = self.min_items {
|
if let Some(min) = self.min_items
|
||||||
if (default_values.len() as u64) < min {
|
&& (default_values.len() as u64) < min
|
||||||
return Err("Number of provided default values is less than min_items".to_string());
|
{
|
||||||
}
|
return Err("Number of provided default values is less than min_items".to_string());
|
||||||
}
|
}
|
||||||
if let Some(max) = self.max_items {
|
if let Some(max) = self.max_items
|
||||||
if (default_values.len() as u64) > max {
|
&& (default_values.len() as u64) > max
|
||||||
return Err(
|
{
|
||||||
"Number of provided default values is greater than max_items".to_string(),
|
return Err("Number of provided default values is greater than max_items".to_string());
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.default = default_values;
|
self.default = default_values;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
|
|
|
||||||
|
|
@ -554,11 +554,10 @@ impl<R: ServiceRole> RequestHandle<R> {
|
||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
}, if reset_timeout_on_progress && idle_sleep.is_some() && self.progress_reset_rx.is_some() => {
|
}, if reset_timeout_on_progress && idle_sleep.is_some() && self.progress_reset_rx.is_some() => {
|
||||||
if progress.is_some() {
|
if progress.is_some()
|
||||||
if let Some((timeout, sleep)) = idle_sleep.as_mut() {
|
&& let Some((timeout, sleep)) = idle_sleep.as_mut() {
|
||||||
sleep.as_mut().reset(tokio::time::Instant::now() + *timeout);
|
sleep.as_mut().reset(tokio::time::Instant::now() + *timeout);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1351,11 +1350,10 @@ where
|
||||||
tracing::trace!(?evt, "new event");
|
tracing::trace!(?evt, "new event");
|
||||||
match evt {
|
match evt {
|
||||||
Event::SendTaskResult(SendTaskResult::Request { id, result }) => {
|
Event::SendTaskResult(SendTaskResult::Request { id, result }) => {
|
||||||
if let Err(e) = result {
|
if let Err(e) = result
|
||||||
if let Some(responder) = local_responder_pool.remove(&id) {
|
&& let Some(responder) = local_responder_pool.remove(&id) {
|
||||||
let _ = responder.send(Err(ServiceError::TransportSend(e)));
|
let _ = responder.send(Err(ServiceError::TransportSend(e)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Event::SendTaskResult(SendTaskResult::Notification {
|
Event::SendTaskResult(SendTaskResult::Notification {
|
||||||
responder,
|
responder,
|
||||||
|
|
@ -1368,16 +1366,14 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
let _ = responder.send(response);
|
let _ = responder.send(response);
|
||||||
if let Some(param) = cancellation_param {
|
if let Some(param) = cancellation_param
|
||||||
if let Some(request_id) = ¶m.request_id {
|
&& let Some(request_id) = ¶m.request_id
|
||||||
if let Some(responder) = local_responder_pool.remove(request_id) {
|
&& let Some(responder) = local_responder_pool.remove(request_id) {
|
||||||
tracing::info!(id = %request_id, reason = param.reason, "cancelled");
|
tracing::info!(id = %request_id, reason = param.reason, "cancelled");
|
||||||
let _response_result = responder.send(Err(ServiceError::Cancelled {
|
let _response_result = responder.send(Err(ServiceError::Cancelled {
|
||||||
reason: param.reason.clone(),
|
reason: param.reason.clone(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Event::ResponseSendTaskResult(result) => {
|
Event::ResponseSendTaskResult(result) => {
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
|
|
|
||||||
|
|
@ -320,14 +320,12 @@ fn try_parse_with_compatibility<T: serde::de::DeserializeOwned>(
|
||||||
Ok(item) => Ok(Some(item)),
|
Ok(item) => Ok(Some(item)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Check if this is a notification that should be ignored for compatibility
|
// Check if this is a notification that should be ignored for compatibility
|
||||||
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(line_str) {
|
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(line_str)
|
||||||
if let Some(method) =
|
&& let Some(method) =
|
||||||
json_value.get("method").and_then(serde_json::Value::as_str)
|
json_value.get("method").and_then(serde_json::Value::as_str)
|
||||||
{
|
&& should_ignore_notification(&json_value, method)
|
||||||
if should_ignore_notification(&json_value, method) {
|
{
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
|
|
|
||||||
|
|
@ -1248,56 +1248,56 @@ impl AuthorizationManager {
|
||||||
/// the client if credentials are found. Returns `false` when credentials
|
/// the client if credentials are found. Returns `false` when credentials
|
||||||
/// are absent or discarded after an authorization-server change.
|
/// are absent or discarded after an authorization-server change.
|
||||||
pub async fn initialize_from_store(&mut self) -> Result<bool, AuthError> {
|
pub async fn initialize_from_store(&mut self) -> Result<bool, AuthError> {
|
||||||
if let Some(stored) = self.credential_store.load().await? {
|
if let Some(stored) = self.credential_store.load().await?
|
||||||
if stored.token_response.is_some() {
|
&& stored.token_response.is_some()
|
||||||
if self.metadata.is_none() {
|
{
|
||||||
let metadata = self.discover_metadata().await?;
|
if self.metadata.is_none() {
|
||||||
self.metadata = Some(metadata);
|
let metadata = self.discover_metadata().await?;
|
||||||
}
|
self.metadata = Some(metadata);
|
||||||
|
}
|
||||||
if let (Some(stored_issuer), Some(current_issuer)) =
|
|
||||||
(stored.issuer.as_deref(), self.metadata_issuer().as_deref())
|
|
||||||
{
|
|
||||||
// A CIMD client ID is the client's metadata URL, so it is
|
|
||||||
// portable across authorization servers and exempt here.
|
|
||||||
if stored_issuer != current_issuer {
|
|
||||||
if is_https_url(&stored.client_id) {
|
|
||||||
// A CIMD client ID is the client's metadata URL, so it is
|
|
||||||
// portable across authorization servers — but the tokens
|
|
||||||
// were minted by the previous AS and must not be reused.
|
|
||||||
tracing::warn!(
|
|
||||||
stored_issuer,
|
|
||||||
current_issuer,
|
|
||||||
"authorization server issuer changed; discarding tokens but keeping portable CIMD client ID"
|
|
||||||
);
|
|
||||||
self.credential_store
|
|
||||||
.save(
|
|
||||||
StoredCredentials::new(
|
|
||||||
stored.client_id.clone(),
|
|
||||||
None,
|
|
||||||
vec![],
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.with_issuer(self.metadata_issuer()),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
self.configure_client_id(&stored.client_id)?;
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if let (Some(stored_issuer), Some(current_issuer)) =
|
||||||
|
(stored.issuer.as_deref(), self.metadata_issuer().as_deref())
|
||||||
|
{
|
||||||
|
// A CIMD client ID is the client's metadata URL, so it is
|
||||||
|
// portable across authorization servers and exempt here.
|
||||||
|
if stored_issuer != current_issuer {
|
||||||
|
if is_https_url(&stored.client_id) {
|
||||||
|
// A CIMD client ID is the client's metadata URL, so it is
|
||||||
|
// portable across authorization servers — but the tokens
|
||||||
|
// were minted by the previous AS and must not be reused.
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
stored_issuer,
|
stored_issuer,
|
||||||
current_issuer,
|
current_issuer,
|
||||||
"authorization server issuer changed; clearing stored credentials bound to the previous issuer"
|
"authorization server issuer changed; discarding tokens but keeping portable CIMD client ID"
|
||||||
);
|
);
|
||||||
self.credential_store.clear().await?;
|
self.credential_store
|
||||||
|
.save(
|
||||||
|
StoredCredentials::new(
|
||||||
|
stored.client_id.clone(),
|
||||||
|
None,
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.with_issuer(self.metadata_issuer()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
self.configure_client_id(&stored.client_id)?;
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.configure_client_id(&stored.client_id)?;
|
tracing::warn!(
|
||||||
return Ok(true);
|
stored_issuer,
|
||||||
|
current_issuer,
|
||||||
|
"authorization server issuer changed; clearing stored credentials bound to the previous issuer"
|
||||||
|
);
|
||||||
|
self.credential_store.clear().await?;
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.configure_client_id(&stored.client_id)?;
|
||||||
|
return Ok(true);
|
||||||
}
|
}
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
@ -1425,10 +1425,10 @@ impl AuthorizationManager {
|
||||||
|
|
||||||
// RFC 8414 RECOMMENDS response_types_supported in the metadata. This field is optional,
|
// RFC 8414 RECOMMENDS response_types_supported in the metadata. This field is optional,
|
||||||
// but if present and does not include the flow we use ("code"), bail out early with a clear error.
|
// but if present and does not include the flow we use ("code"), bail out early with a clear error.
|
||||||
if let Some(response_types_supported) = metadata.response_types_supported.as_ref() {
|
if let Some(response_types_supported) = metadata.response_types_supported.as_ref()
|
||||||
if !response_types_supported.contains(&response_type.to_string()) {
|
&& !response_types_supported.contains(&response_type.to_string())
|
||||||
return Err(AuthError::InvalidScope(response_type.to_string()));
|
{
|
||||||
}
|
return Err(AuthError::InvalidScope(response_type.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The client always sends an S256 challenge. A server that advertises
|
// The client always sends an S256 challenge. A server that advertises
|
||||||
|
|
@ -1713,12 +1713,11 @@ impl AuthorizationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// nothing requested or challenged yet: seed from AS metadata, then caller defaults
|
// nothing requested or challenged yet: seed from AS metadata, then caller defaults
|
||||||
if let Some(metadata) = &self.metadata {
|
if let Some(metadata) = &self.metadata
|
||||||
if let Some(scopes_supported) = &metadata.scopes_supported {
|
&& let Some(scopes_supported) = &metadata.scopes_supported
|
||||||
if !scopes_supported.is_empty() {
|
&& !scopes_supported.is_empty()
|
||||||
return scopes_supported.clone();
|
{
|
||||||
}
|
return scopes_supported.clone();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default_scopes.iter().map(|s| s.to_string()).collect()
|
default_scopes.iter().map(|s| s.to_string()).collect()
|
||||||
|
|
@ -1730,12 +1729,11 @@ impl AuthorizationManager {
|
||||||
if scopes.is_empty() || scopes.iter().any(|s| s == "offline_access") {
|
if scopes.is_empty() || scopes.iter().any(|s| s == "offline_access") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(metadata) = &self.metadata {
|
if let Some(metadata) = &self.metadata
|
||||||
if let Some(supported) = &metadata.scopes_supported {
|
&& let Some(supported) = &metadata.scopes_supported
|
||||||
if supported.iter().any(|s| s == "offline_access") {
|
&& supported.iter().any(|s| s == "offline_access")
|
||||||
scopes.push("offline_access".to_string());
|
{
|
||||||
}
|
scopes.push("offline_access".to_string());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2255,10 +2253,10 @@ impl AuthorizationManager {
|
||||||
self.validate_resource_metadata_resource(&resource_metadata)?;
|
self.validate_resource_metadata_resource(&resource_metadata)?;
|
||||||
|
|
||||||
// store scopes_supported from protected resource metadata for select_scopes()
|
// store scopes_supported from protected resource metadata for select_scopes()
|
||||||
if let Some(scopes) = resource_metadata.scopes_supported {
|
if let Some(scopes) = resource_metadata.scopes_supported
|
||||||
if !scopes.is_empty() {
|
&& !scopes.is_empty()
|
||||||
*self.resource_scopes.write().await = scopes;
|
{
|
||||||
}
|
*self.resource_scopes.write().await = scopes;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
|
|
@ -2701,20 +2699,18 @@ impl AuthorizationManager {
|
||||||
if let ClientCredentialsConfig::PrivateKeyJwt {
|
if let ClientCredentialsConfig::PrivateKeyJwt {
|
||||||
signing_algorithm, ..
|
signing_algorithm, ..
|
||||||
} = config
|
} = config
|
||||||
{
|
&& let Some(algs) = metadata
|
||||||
if let Some(algs) = metadata
|
|
||||||
.additional_fields
|
.additional_fields
|
||||||
.get("token_endpoint_auth_signing_alg_values_supported")
|
.get("token_endpoint_auth_signing_alg_values_supported")
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
{
|
{
|
||||||
let alg_str = signing_algorithm.as_str();
|
let alg_str = signing_algorithm.as_str();
|
||||||
if !algs.iter().any(|a| a.as_str() == Some(alg_str)) {
|
if !algs.iter().any(|a| a.as_str() == Some(alg_str)) {
|
||||||
let supported: Vec<&str> = algs.iter().filter_map(|a| a.as_str()).collect();
|
let supported: Vec<&str> = algs.iter().filter_map(|a| a.as_str()).collect();
|
||||||
return Err(AuthError::ClientCredentialsError(format!(
|
return Err(AuthError::ClientCredentialsError(format!(
|
||||||
"Authorization server does not support signing algorithm '{}'. Supported: {:?}",
|
"Authorization server does not support signing algorithm '{}'. Supported: {:?}",
|
||||||
alg_str, supported
|
alg_str, supported
|
||||||
)));
|
)));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,10 +178,10 @@ pub struct FixedInterval {
|
||||||
|
|
||||||
impl SseRetryPolicy for FixedInterval {
|
impl SseRetryPolicy for FixedInterval {
|
||||||
fn retry(&self, current_times: usize) -> Option<Duration> {
|
fn retry(&self, current_times: usize) -> Option<Duration> {
|
||||||
if let Some(max_times) = self.max_times {
|
if let Some(max_times) = self.max_times
|
||||||
if current_times >= max_times {
|
&& current_times >= max_times
|
||||||
return None;
|
{
|
||||||
}
|
return None;
|
||||||
}
|
}
|
||||||
Some(self.duration)
|
Some(self.duration)
|
||||||
}
|
}
|
||||||
|
|
@ -222,10 +222,10 @@ impl Default for ExponentialBackoff {
|
||||||
|
|
||||||
impl SseRetryPolicy for ExponentialBackoff {
|
impl SseRetryPolicy for ExponentialBackoff {
|
||||||
fn retry(&self, current_times: usize) -> Option<Duration> {
|
fn retry(&self, current_times: usize) -> Option<Duration> {
|
||||||
if let Some(max_times) = self.max_times {
|
if let Some(max_times) = self.max_times
|
||||||
if current_times >= max_times {
|
&& current_times >= max_times
|
||||||
return None;
|
{
|
||||||
}
|
return None;
|
||||||
}
|
}
|
||||||
Some(self.base_duration * (2u32.pow(current_times as u32)))
|
Some(self.base_duration * (2u32.pow(current_times as u32)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,10 +104,10 @@ fn param_header_annotations(input_schema: &JsonObject) -> Vec<(String, String)>
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
if let Some(Value::Object(props)) = input_schema.get("properties") {
|
if let Some(Value::Object(props)) = input_schema.get("properties") {
|
||||||
for (prop, schema) in props {
|
for (prop, schema) in props {
|
||||||
if let Some(Value::String(header)) = schema.get("x-mcp-header") {
|
if let Some(Value::String(header)) = schema.get("x-mcp-header")
|
||||||
if !header.is_empty() {
|
&& !header.is_empty()
|
||||||
out.push((prop.clone(), header.clone()));
|
{
|
||||||
}
|
out.push((prop.clone(), header.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -236,20 +236,18 @@ pub(crate) fn standard_request_headers(
|
||||||
push(HEADER_MCP_NAME, &encode_header_value(&name));
|
push(HEADER_MCP_NAME, &encode_header_value(&name));
|
||||||
}
|
}
|
||||||
|
|
||||||
if method == "tools/call" {
|
if method == "tools/call"
|
||||||
if let (Some(schema), Some(arguments)) =
|
&& let (Some(schema), Some(arguments)) =
|
||||||
(tool_schema, params.and_then(|p| p.get("arguments")))
|
(tool_schema, params.and_then(|p| p.get("arguments")))
|
||||||
{
|
{
|
||||||
for (prop, header) in param_header_annotations(schema) {
|
for (prop, header) in param_header_annotations(schema) {
|
||||||
let Some(arg) = arguments.get(&prop) else {
|
let Some(arg) = arguments.get(&prop) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some(encoded) = primitive_to_string(arg).map(|s| encode_header_value(&s))
|
let Some(encoded) = primitive_to_string(arg).map(|s| encode_header_value(&s)) else {
|
||||||
else {
|
continue;
|
||||||
continue;
|
};
|
||||||
};
|
push(&format!("{HEADER_MCP_PARAM_PREFIX}{header}"), &encoded);
|
||||||
push(&format!("{HEADER_MCP_PARAM_PREFIX}{header}"), &encoded);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|
@ -296,34 +294,34 @@ pub(crate) fn validate_request_headers(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if method == "tools/call" {
|
if method == "tools/call"
|
||||||
if let Some(schema) = tool_schema {
|
&& let Some(schema) = tool_schema
|
||||||
let arguments = params.and_then(|p| p.get("arguments"));
|
{
|
||||||
for (prop, header) in param_header_annotations(schema) {
|
let arguments = params.and_then(|p| p.get("arguments"));
|
||||||
let full = format!("{HEADER_MCP_PARAM_PREFIX}{header}");
|
for (prop, header) in param_header_annotations(schema) {
|
||||||
let header_value = header_str(headers, &full);
|
let full = format!("{HEADER_MCP_PARAM_PREFIX}{header}");
|
||||||
let arg = arguments.and_then(|a| a.get(&prop));
|
let header_value = header_str(headers, &full);
|
||||||
let body_value = arg.filter(|v| !v.is_null()).and_then(primitive_to_string);
|
let arg = arguments.and_then(|a| a.get(&prop));
|
||||||
|
let body_value = arg.filter(|v| !v.is_null()).and_then(primitive_to_string);
|
||||||
|
|
||||||
match (header_value, body_value) {
|
match (header_value, body_value) {
|
||||||
(None, None) => {}
|
(None, None) => {}
|
||||||
(Some(_), None) => {
|
(Some(_), None) => {
|
||||||
|
return Err(format!(
|
||||||
|
"unexpected {full} header for absent or null `{prop}`"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(None, Some(_)) => {
|
||||||
|
return Err(format!("missing {full} header for `{prop}`"));
|
||||||
|
}
|
||||||
|
(Some(raw), Some(expected)) => {
|
||||||
|
let decoded = decode_header_value(raw)
|
||||||
|
.ok_or_else(|| format!("{full} header is not valid Base64"))?;
|
||||||
|
if decoded != expected {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"unexpected {full} header for absent or null `{prop}`"
|
"{full} header `{decoded}` does not match body value `{expected}`"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
(None, Some(_)) => {
|
|
||||||
return Err(format!("missing {full} header for `{prop}`"));
|
|
||||||
}
|
|
||||||
(Some(raw), Some(expected)) => {
|
|
||||||
let decoded = decode_header_value(raw)
|
|
||||||
.ok_or_else(|| format!("{full} header is not valid Base64"))?;
|
|
||||||
if decoded != expected {
|
|
||||||
return Err(format!(
|
|
||||||
"{full} header `{decoded}` does not match body value `{expected}`"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,36 +178,36 @@ impl StreamableHttpClient for reqwest::Client {
|
||||||
request = request.header(HEADER_SESSION_ID, session_id.as_ref());
|
request = request.header(HEADER_SESSION_ID, session_id.as_ref());
|
||||||
}
|
}
|
||||||
let response = request.json(&message).send().await?;
|
let response = request.json(&message).send().await?;
|
||||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
if response.status() == reqwest::StatusCode::UNAUTHORIZED
|
||||||
if let Some(header) = response.headers().get(WWW_AUTHENTICATE) {
|
&& let Some(header) = response.headers().get(WWW_AUTHENTICATE)
|
||||||
let header = header
|
{
|
||||||
.to_str()
|
let header = header
|
||||||
.map_err(|_| {
|
.to_str()
|
||||||
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
.map_err(|_| {
|
||||||
"invalid www-authenticate header value",
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.to_string();
|
|
||||||
return Err(StreamableHttpError::AuthRequired(AuthRequiredError {
|
|
||||||
www_authenticate_header: header,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if response.status() == reqwest::StatusCode::FORBIDDEN {
|
|
||||||
if let Some(header) = response.headers().get(WWW_AUTHENTICATE) {
|
|
||||||
let header_str = header.to_str().map_err(|_| {
|
|
||||||
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
||||||
"invalid www-authenticate header value",
|
"invalid www-authenticate header value",
|
||||||
))
|
))
|
||||||
})?;
|
})?
|
||||||
let scope = extract_scope_from_header(header_str);
|
.to_string();
|
||||||
return Err(StreamableHttpError::InsufficientScope(
|
return Err(StreamableHttpError::AuthRequired(AuthRequiredError {
|
||||||
InsufficientScopeError {
|
www_authenticate_header: header,
|
||||||
www_authenticate_header: header_str.to_string(),
|
}));
|
||||||
required_scope: scope,
|
}
|
||||||
},
|
if response.status() == reqwest::StatusCode::FORBIDDEN
|
||||||
));
|
&& let Some(header) = response.headers().get(WWW_AUTHENTICATE)
|
||||||
}
|
{
|
||||||
|
let header_str = header.to_str().map_err(|_| {
|
||||||
|
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
||||||
|
"invalid www-authenticate header value",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let scope = extract_scope_from_header(header_str);
|
||||||
|
return Err(StreamableHttpError::InsufficientScope(
|
||||||
|
InsufficientScopeError {
|
||||||
|
www_authenticate_header: header_str.to_string(),
|
||||||
|
required_scope: scope,
|
||||||
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if matches!(
|
if matches!(
|
||||||
|
|
|
||||||
|
|
@ -226,37 +226,37 @@ impl StreamableHttpClient for UnixSocketHttpClient {
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
|
|
||||||
if status == StatusCode::UNAUTHORIZED {
|
if status == StatusCode::UNAUTHORIZED
|
||||||
if let Some(header) = response.headers().get(WWW_AUTHENTICATE) {
|
&& let Some(header) = response.headers().get(WWW_AUTHENTICATE)
|
||||||
let www_authenticate_header = header
|
{
|
||||||
.to_str()
|
let www_authenticate_header = header
|
||||||
.map_err(|_| {
|
.to_str()
|
||||||
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
.map_err(|_| {
|
||||||
"invalid www-authenticate header value",
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.to_string();
|
|
||||||
return Err(StreamableHttpError::AuthRequired(AuthRequiredError {
|
|
||||||
www_authenticate_header,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if status == StatusCode::FORBIDDEN {
|
|
||||||
if let Some(header) = response.headers().get(WWW_AUTHENTICATE) {
|
|
||||||
let header_str = header.to_str().map_err(|_| {
|
|
||||||
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
||||||
"invalid www-authenticate header value",
|
"invalid www-authenticate header value",
|
||||||
))
|
))
|
||||||
})?;
|
})?
|
||||||
let scope = extract_scope_from_header(header_str);
|
.to_string();
|
||||||
return Err(StreamableHttpError::InsufficientScope(
|
return Err(StreamableHttpError::AuthRequired(AuthRequiredError {
|
||||||
InsufficientScopeError {
|
www_authenticate_header,
|
||||||
www_authenticate_header: header_str.to_string(),
|
}));
|
||||||
required_scope: scope,
|
}
|
||||||
},
|
|
||||||
));
|
if status == StatusCode::FORBIDDEN
|
||||||
}
|
&& let Some(header) = response.headers().get(WWW_AUTHENTICATE)
|
||||||
|
{
|
||||||
|
let header_str = header.to_str().map_err(|_| {
|
||||||
|
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
||||||
|
"invalid www-authenticate header value",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let scope = extract_scope_from_header(header_str);
|
||||||
|
return Err(StreamableHttpError::InsufficientScope(
|
||||||
|
InsufficientScopeError {
|
||||||
|
www_authenticate_header: header_str.to_string(),
|
||||||
|
required_scope: scope,
|
||||||
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(status, StatusCode::ACCEPTED | StatusCode::NO_CONTENT) {
|
if matches!(status, StatusCode::ACCEPTED | StatusCode::NO_CONTENT) {
|
||||||
|
|
@ -438,37 +438,37 @@ impl StreamableHttpClient for UnixSocketHttpClient {
|
||||||
return Err(StreamableHttpError::ServerDoesNotSupportSse);
|
return Err(StreamableHttpError::ServerDoesNotSupportSse);
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.status() == StatusCode::UNAUTHORIZED {
|
if response.status() == StatusCode::UNAUTHORIZED
|
||||||
if let Some(header) = response.headers().get(WWW_AUTHENTICATE) {
|
&& let Some(header) = response.headers().get(WWW_AUTHENTICATE)
|
||||||
let www_authenticate_header = header
|
{
|
||||||
.to_str()
|
let www_authenticate_header = header
|
||||||
.map_err(|_| {
|
.to_str()
|
||||||
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
.map_err(|_| {
|
||||||
"invalid www-authenticate header value",
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.to_string();
|
|
||||||
return Err(StreamableHttpError::AuthRequired(AuthRequiredError {
|
|
||||||
www_authenticate_header,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.status() == StatusCode::FORBIDDEN {
|
|
||||||
if let Some(header) = response.headers().get(WWW_AUTHENTICATE) {
|
|
||||||
let header_str = header.to_str().map_err(|_| {
|
|
||||||
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
||||||
"invalid www-authenticate header value",
|
"invalid www-authenticate header value",
|
||||||
))
|
))
|
||||||
})?;
|
})?
|
||||||
let scope = extract_scope_from_header(header_str);
|
.to_string();
|
||||||
return Err(StreamableHttpError::InsufficientScope(
|
return Err(StreamableHttpError::AuthRequired(AuthRequiredError {
|
||||||
InsufficientScopeError {
|
www_authenticate_header,
|
||||||
www_authenticate_header: header_str.to_string(),
|
}));
|
||||||
required_scope: scope,
|
}
|
||||||
},
|
|
||||||
));
|
if response.status() == StatusCode::FORBIDDEN
|
||||||
}
|
&& let Some(header) = response.headers().get(WWW_AUTHENTICATE)
|
||||||
|
{
|
||||||
|
let header_str = header.to_str().map_err(|_| {
|
||||||
|
StreamableHttpError::UnexpectedServerResponse(Cow::from(
|
||||||
|
"invalid www-authenticate header value",
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let scope = extract_scope_from_header(header_str);
|
||||||
|
return Err(StreamableHttpError::InsufficientScope(
|
||||||
|
InsufficientScopeError {
|
||||||
|
www_authenticate_header: header_str.to_string(),
|
||||||
|
required_scope: scope,
|
||||||
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
|
|
|
||||||
|
|
@ -42,20 +42,20 @@ fn build_request_headers(
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
let mut headers = base.clone();
|
let mut headers = base.clone();
|
||||||
if *version >= ProtocolVersion::STANDARD_HEADERS {
|
if *version >= ProtocolVersion::STANDARD_HEADERS
|
||||||
if let Ok(value) = serde_json::to_value(message) {
|
&& let Ok(value) = serde_json::to_value(message)
|
||||||
let schema = value
|
{
|
||||||
.get("method")
|
let schema = value
|
||||||
.and_then(Value::as_str)
|
.get("method")
|
||||||
.filter(|method| *method == "tools/call")
|
.and_then(Value::as_str)
|
||||||
.and_then(|_| value.get("params"))
|
.filter(|method| *method == "tools/call")
|
||||||
.and_then(|params| params.get("name"))
|
.and_then(|_| value.get("params"))
|
||||||
.and_then(Value::as_str)
|
.and_then(|params| params.get("name"))
|
||||||
.and_then(|name| tool_cache.get(name))
|
.and_then(Value::as_str)
|
||||||
.map(Arc::as_ref);
|
.and_then(|name| tool_cache.get(name))
|
||||||
for (name, val) in mcp_headers::standard_request_headers(&value, schema) {
|
.map(Arc::as_ref);
|
||||||
headers.insert(name, val);
|
for (name, val) in mcp_headers::standard_request_headers(&value, schema) {
|
||||||
}
|
headers.insert(name, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
headers
|
headers
|
||||||
|
|
@ -90,9 +90,10 @@ fn cache_tools_from_response(
|
||||||
if protocol_version < &ProtocolVersion::STANDARD_HEADERS {
|
if protocol_version < &ProtocolVersion::STANDARD_HEADERS {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let ServerJsonRpcMessage::Response(response) = message {
|
if let ServerJsonRpcMessage::Response(response) = message
|
||||||
if let ServerResult::ListToolsResult(list) = &mut response.result {
|
&& let ServerResult::ListToolsResult(list) = &mut response.result
|
||||||
list.tools.retain(|tool| {
|
{
|
||||||
|
list.tools.retain(|tool| {
|
||||||
let Err(reason) =
|
let Err(reason) =
|
||||||
mcp_headers::validate_param_header_annotations(&tool.input_schema)
|
mcp_headers::validate_param_header_annotations(&tool.input_schema)
|
||||||
else {
|
else {
|
||||||
|
|
@ -102,7 +103,6 @@ fn cache_tools_from_response(
|
||||||
tracing::warn!(tool = %tool.name, "rejecting invalid x-mcp-header annotations: {reason}");
|
tracing::warn!(tool = %tool.name, "rejecting invalid x-mcp-header annotations: {reason}");
|
||||||
false
|
false
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,13 +112,13 @@ fn negotiate_version_headers(
|
||||||
) -> (ProtocolVersion, HashMap<HeaderName, HeaderValue>) {
|
) -> (ProtocolVersion, HashMap<HeaderName, HeaderValue>) {
|
||||||
let mut version = ProtocolVersion::default();
|
let mut version = ProtocolVersion::default();
|
||||||
let mut headers = base;
|
let mut headers = base;
|
||||||
if let ServerJsonRpcMessage::Response(response) = init_response {
|
if let ServerJsonRpcMessage::Response(response) = init_response
|
||||||
if let ServerResult::InitializeResult(init_result) = &response.result {
|
&& let ServerResult::InitializeResult(init_result) = &response.result
|
||||||
version = init_result.protocol_version.clone();
|
{
|
||||||
// HeaderName::from_static requires lowercase
|
version = init_result.protocol_version.clone();
|
||||||
if let Ok(hv) = HeaderValue::from_str(init_result.protocol_version.as_str()) {
|
// HeaderName::from_static requires lowercase
|
||||||
headers.insert(HeaderName::from_static("mcp-protocol-version"), hv);
|
if let Ok(hv) = HeaderValue::from_str(init_result.protocol_version.as_str()) {
|
||||||
}
|
headers.insert(HeaderName::from_static("mcp-protocol-version"), hv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(version, headers)
|
(version, headers)
|
||||||
|
|
|
||||||
|
|
@ -479,11 +479,11 @@ impl LocalSessionWorker {
|
||||||
&mut self,
|
&mut self,
|
||||||
notification: &JsonRpcNotification<ClientNotification>,
|
notification: &JsonRpcNotification<ClientNotification>,
|
||||||
) {
|
) {
|
||||||
if let ClientNotification::CancelledNotification(n) = ¬ification.notification {
|
if let ClientNotification::CancelledNotification(n) = ¬ification.notification
|
||||||
if let Some(request_id) = n.params.request_id.clone() {
|
&& let Some(request_id) = n.params.request_id.clone()
|
||||||
let resource = ResourceKey::McpRequestId(request_id);
|
{
|
||||||
self.unregister_resource(&resource);
|
let resource = ResourceKey::McpRequestId(request_id);
|
||||||
}
|
self.unregister_resource(&resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn evict_expired_channels(&mut self) {
|
fn evict_expired_channels(&mut self) {
|
||||||
|
|
@ -638,11 +638,9 @@ impl LocalSessionWorker {
|
||||||
OutboundChannel::RequestWise { id, close } => {
|
OutboundChannel::RequestWise { id, close } => {
|
||||||
if let Some(request_wise) = self.tx_router.get_mut(&id) {
|
if let Some(request_wise) = self.tx_router.get_mut(&id) {
|
||||||
request_wise.tx.send(message).await?;
|
request_wise.tx.send(message).await?;
|
||||||
if close {
|
if close && let Some(channel) = self.tx_router.remove(&id) {
|
||||||
if let Some(channel) = self.tx_router.remove(&id) {
|
for resource in channel.resources {
|
||||||
for resource in channel.resources {
|
self.resource_router.remove(&resource);
|
||||||
self.resource_router.remove(&resource);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -238,13 +238,13 @@ impl ClientHandler for MrtrClient {
|
||||||
request: ElicitRequestParams,
|
request: ElicitRequestParams,
|
||||||
_context: RequestContext<RoleClient>,
|
_context: RequestContext<RoleClient>,
|
||||||
) -> Result<ElicitResult, ErrorData> {
|
) -> Result<ElicitResult, ErrorData> {
|
||||||
if let ElicitRequestParams::FormElicitationParams { message, .. } = &request {
|
if let ElicitRequestParams::FormElicitationParams { message, .. } = &request
|
||||||
if message == "FAIL" {
|
&& message == "FAIL"
|
||||||
return Err(ErrorData::internal_error(
|
{
|
||||||
"elicitation handler failed",
|
return Err(ErrorData::internal_error(
|
||||||
None,
|
"elicitation handler failed",
|
||||||
));
|
None,
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
Ok(ElicitResult::new(ElicitationAction::Accept).with_content(json!({ "name": "Ferris" })))
|
Ok(ElicitResult::new(ElicitationAction::Accept).with_content(json!({ "name": "Ferris" })))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue