chore: consolidate repeated rmcp tests (#931)
This commit is contained in:
parent
4b9bea7e7d
commit
e1af378949
14 changed files with 306 additions and 332 deletions
|
|
@ -200,6 +200,7 @@ tracing-subscriber = { version = "0.3", features = [
|
||||||
"fmt",
|
"fmt",
|
||||||
] }
|
] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
rstest = "0.26.1"
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "test_tool_macros"
|
name = "test_tool_macros"
|
||||||
required-features = ["server", "client"]
|
required-features = ["server", "client"]
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,8 @@ pub trait AsRequestContext {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
|
#[derive(serde::Serialize, serde::Deserialize, JsonSchema)]
|
||||||
|
|
@ -245,46 +247,44 @@ mod tests {
|
||||||
value: i32,
|
value: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[rstest]
|
||||||
fn test_schema_for_type_handles_primitive() {
|
#[case::primitive(schema_for_type::<i32>, "integer")]
|
||||||
let schema = schema_for_type::<i32>();
|
#[case::array(schema_for_type::<Vec<i32>>, "array")]
|
||||||
|
#[case::struct_object(schema_for_type::<TestObject>, "object")]
|
||||||
|
fn schema_for_type_sets_expected_root_type(
|
||||||
|
#[case] schema_fn: fn() -> Arc<JsonObject>,
|
||||||
|
#[case] expected_type: &str,
|
||||||
|
) {
|
||||||
|
let schema = schema_fn();
|
||||||
|
|
||||||
assert_eq!(schema.get("type"), Some(&serde_json::json!("integer")));
|
assert_eq!(schema.get("type"), Some(&serde_json::json!(expected_type)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_schema_for_type_handles_array() {
|
fn schema_for_type_sets_array_item_type() {
|
||||||
let schema = schema_for_type::<Vec<i32>>();
|
let schema = schema_for_type::<Vec<i32>>();
|
||||||
|
let items = schema.get("items").and_then(|v| v.as_object()).unwrap();
|
||||||
|
|
||||||
assert_eq!(schema.get("type"), Some(&serde_json::json!("array")));
|
assert_eq!(items.get("type"), Some(&serde_json::json!("integer")));
|
||||||
let items = schema.get("items").and_then(|v| v.as_object());
|
|
||||||
assert_eq!(
|
|
||||||
items.unwrap().get("type"),
|
|
||||||
Some(&serde_json::json!("integer"))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_schema_for_type_handles_struct() {
|
fn schema_for_type_sets_struct_properties() {
|
||||||
let schema = schema_for_type::<TestObject>();
|
let schema = schema_for_type::<TestObject>();
|
||||||
|
let properties = schema
|
||||||
|
.get("properties")
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(schema.get("type"), Some(&serde_json::json!("object")));
|
assert!(properties.contains_key("value"));
|
||||||
let properties = schema.get("properties").and_then(|v| v.as_object());
|
|
||||||
assert!(properties.unwrap().contains_key("value"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[rstest]
|
||||||
fn test_schema_for_type_caches_primitive_types() {
|
#[case::primitive(schema_for_type::<i32>)]
|
||||||
let schema1 = schema_for_type::<i32>();
|
#[case::struct_object(schema_for_type::<TestObject>)]
|
||||||
let schema2 = schema_for_type::<i32>();
|
fn test_schema_for_type_caches_schemas(#[case] schema_fn: fn() -> Arc<JsonObject>) {
|
||||||
|
let schema1 = schema_fn();
|
||||||
assert!(Arc::ptr_eq(&schema1, &schema2));
|
let schema2 = schema_fn();
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_schema_for_type_caches_struct_types() {
|
|
||||||
let schema1 = schema_for_type::<TestObject>();
|
|
||||||
let schema2 = schema_for_type::<TestObject>();
|
|
||||||
|
|
||||||
assert!(Arc::ptr_eq(&schema1, &schema2));
|
assert!(Arc::ptr_eq(&schema1, &schema2));
|
||||||
}
|
}
|
||||||
|
|
@ -305,51 +305,36 @@ mod tests {
|
||||||
assert!(Arc::ptr_eq(&schema, &cloned));
|
assert!(Arc::ptr_eq(&schema, &cloned));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[rstest]
|
||||||
fn test_schema_for_output_rejects_primitive() {
|
#[case::output(schema_for_output::<i32>)]
|
||||||
let result = schema_for_output::<i32>();
|
#[case::input(schema_for_input::<i32>)]
|
||||||
assert!(result.is_err(),);
|
fn test_schema_for_object_wrappers_reject_primitives(
|
||||||
}
|
#[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
|
||||||
|
) {
|
||||||
#[test]
|
let result = schema_fn();
|
||||||
fn test_schema_for_output_accepts_object() {
|
|
||||||
let result = schema_for_output::<TestObject>();
|
|
||||||
assert!(result.is_ok(),);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_schema_for_output_strips_top_level_title() {
|
|
||||||
let schema = schema_for_output::<TestObject>().unwrap();
|
|
||||||
assert!(!schema.contains_key("title"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_schema_for_output_strips_top_level_description() {
|
|
||||||
let schema = schema_for_output::<TestObject>().unwrap();
|
|
||||||
assert!(!schema.contains_key("description"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_schema_for_input_rejects_primitive() {
|
|
||||||
let result = schema_for_input::<i32>();
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[rstest]
|
||||||
fn test_schema_for_input_accepts_object() {
|
#[case::output(schema_for_output::<TestObject>)]
|
||||||
let result = schema_for_input::<TestObject>();
|
#[case::input(schema_for_input::<TestObject>)]
|
||||||
|
fn test_schema_for_object_wrappers_accept_objects(
|
||||||
|
#[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
|
||||||
|
) {
|
||||||
|
let result = schema_fn();
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[rstest]
|
||||||
fn test_schema_for_input_strips_top_level_title() {
|
#[case::output_title(schema_for_output::<TestObject>, "title")]
|
||||||
let schema = schema_for_input::<TestObject>().unwrap();
|
#[case::output_description(schema_for_output::<TestObject>, "description")]
|
||||||
assert!(!schema.contains_key("title"));
|
#[case::input_title(schema_for_input::<TestObject>, "title")]
|
||||||
}
|
#[case::input_description(schema_for_input::<TestObject>, "description")]
|
||||||
|
fn test_schema_for_object_wrappers_strip_top_level_metadata(
|
||||||
#[test]
|
#[case] schema_fn: fn() -> Result<Arc<JsonObject>, String>,
|
||||||
fn test_schema_for_input_strips_top_level_description() {
|
#[case] field: &str,
|
||||||
let schema = schema_for_input::<TestObject>().unwrap();
|
) {
|
||||||
assert!(!schema.contains_key("description"));
|
let schema = schema_fn().unwrap();
|
||||||
|
assert!(!schema.contains_key(field));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1701,49 +1701,70 @@ impl ElicitationSchemaBuilder {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use rstest::rstest;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
fn string_schema_json() -> serde_json::Value {
|
||||||
fn test_string_schema_serialization() {
|
serde_json::to_value(StringSchema::email().description("Email address")).unwrap()
|
||||||
let schema = StringSchema::email().description("Email address");
|
|
||||||
let json = serde_json::to_value(&schema).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(json["type"], "string");
|
|
||||||
assert_eq!(json["format"], "email");
|
|
||||||
assert_eq!(json["description"], "Email address");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn number_schema_json() -> serde_json::Value {
|
||||||
fn test_number_schema_serialization() {
|
serde_json::to_value(
|
||||||
let schema = NumberSchema::new()
|
NumberSchema::new()
|
||||||
.range(0.0, 100.0)
|
.range(0.0, 100.0)
|
||||||
.description("Percentage");
|
.description("Percentage"),
|
||||||
let json = serde_json::to_value(&schema).unwrap();
|
)
|
||||||
|
.unwrap()
|
||||||
assert_eq!(json["type"], "number");
|
|
||||||
assert_eq!(json["minimum"], 0.0);
|
|
||||||
assert_eq!(json["maximum"], 100.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn integer_schema_json() -> serde_json::Value {
|
||||||
fn test_integer_schema_serialization() {
|
serde_json::to_value(IntegerSchema::new().range(0, 150)).unwrap()
|
||||||
let schema = IntegerSchema::new().range(0, 150);
|
|
||||||
let json = serde_json::to_value(&schema).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(json["type"], "integer");
|
|
||||||
assert_eq!(json["minimum"], 0);
|
|
||||||
assert_eq!(json["maximum"], 150);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn boolean_schema_json() -> serde_json::Value {
|
||||||
fn test_boolean_schema_serialization() {
|
serde_json::to_value(BooleanSchema::new().with_default(true)).unwrap()
|
||||||
let schema = BooleanSchema::new().with_default(true);
|
}
|
||||||
let json = serde_json::to_value(&schema).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(json["type"], "boolean");
|
#[rstest]
|
||||||
assert_eq!(json["default"], true);
|
#[case::string_schema(
|
||||||
|
string_schema_json,
|
||||||
|
json!({
|
||||||
|
"type": "string",
|
||||||
|
"format": "email",
|
||||||
|
"description": "Email address",
|
||||||
|
})
|
||||||
|
)]
|
||||||
|
#[case::number_schema(
|
||||||
|
number_schema_json,
|
||||||
|
json!({
|
||||||
|
"type": "number",
|
||||||
|
"description": "Percentage",
|
||||||
|
"minimum": 0.0,
|
||||||
|
"maximum": 100.0,
|
||||||
|
})
|
||||||
|
)]
|
||||||
|
#[case::integer_schema(
|
||||||
|
integer_schema_json,
|
||||||
|
json!({
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 150,
|
||||||
|
})
|
||||||
|
)]
|
||||||
|
#[case::boolean_schema(
|
||||||
|
boolean_schema_json,
|
||||||
|
json!({
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
})
|
||||||
|
)]
|
||||||
|
fn primitive_schema_serializes_to_expected_json(
|
||||||
|
#[case] schema_json: fn() -> serde_json::Value,
|
||||||
|
#[case] expected: serde_json::Value,
|
||||||
|
) {
|
||||||
|
assert_eq!(schema_json(), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -2975,6 +2975,7 @@ mod tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
use oauth2::{AuthType, CsrfToken, HttpResponse, PkceCodeVerifier};
|
use oauth2::{AuthType, CsrfToken, HttpResponse, PkceCodeVerifier};
|
||||||
|
use rstest::rstest;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
|
@ -3348,44 +3349,35 @@ mod tests {
|
||||||
|
|
||||||
// -- header value parsing --
|
// -- header value parsing --
|
||||||
|
|
||||||
#[test]
|
#[rstest]
|
||||||
fn parse_auth_param_value_handles_quoted_string() {
|
#[case::quoted_string(r#""example", realm="foo""#, "example", r#""example""#)]
|
||||||
let fragment = r#""example", realm="foo""#;
|
#[case::escaped_quotes_and_whitespace(
|
||||||
let parsed = AuthorizationManager::parse_next_header_value(fragment).unwrap();
|
r#" "a\"b\\c" ,next=value"#,
|
||||||
assert_eq!(parsed.0, "example");
|
r#"a"b\c"#,
|
||||||
assert_eq!(parsed.1, 9);
|
r#" "a\"b\\c""#
|
||||||
}
|
)]
|
||||||
|
#[case::token_values(" token,next", "token", " token")]
|
||||||
|
#[case::semicolon_separated_tokens(
|
||||||
|
r#" https://example.com/meta; error="invalid_token""#,
|
||||||
|
"https://example.com/meta",
|
||||||
|
" https://example.com/meta"
|
||||||
|
)]
|
||||||
|
#[case::semicolon_after_quoted_value(
|
||||||
|
r#" "https://example.com/meta"; error="invalid_token""#,
|
||||||
|
"https://example.com/meta",
|
||||||
|
r#" "https://example.com/meta""#
|
||||||
|
)]
|
||||||
|
fn parse_auth_param_value_handles_supported_values(
|
||||||
|
#[case] fragment: &str,
|
||||||
|
#[case] expected_value: &str,
|
||||||
|
#[case] expected_consumed_prefix: &str,
|
||||||
|
) {
|
||||||
|
let (value, consumed) = AuthorizationManager::parse_next_header_value(fragment).unwrap();
|
||||||
|
|
||||||
#[test]
|
assert_eq!(
|
||||||
fn parse_auth_param_value_handles_escaped_quotes_and_whitespace() {
|
(value.as_str(), &fragment[..consumed]),
|
||||||
let fragment = r#" "a\"b\\c" ,next=value"#;
|
(expected_value, expected_consumed_prefix)
|
||||||
let parsed = AuthorizationManager::parse_next_header_value(fragment).unwrap();
|
);
|
||||||
assert_eq!(parsed.0, r#"a"b\c"#);
|
|
||||||
assert_eq!(parsed.1, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_auth_param_value_handles_token_values() {
|
|
||||||
let fragment = " token,next";
|
|
||||||
let parsed = AuthorizationManager::parse_next_header_value(fragment).unwrap();
|
|
||||||
assert_eq!(parsed.0, "token");
|
|
||||||
assert_eq!(parsed.1, 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_auth_param_value_handles_semicolon_separated_tokens() {
|
|
||||||
let fragment = r#" https://example.com/meta; error="invalid_token""#;
|
|
||||||
let parsed = AuthorizationManager::parse_next_header_value(fragment).unwrap();
|
|
||||||
assert_eq!(parsed.0, "https://example.com/meta");
|
|
||||||
assert_eq!(&fragment[..parsed.1], " https://example.com/meta");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_auth_param_value_handles_semicolon_after_quoted_value() {
|
|
||||||
let fragment = r#" "https://example.com/meta"; error="invalid_token""#;
|
|
||||||
let parsed = AuthorizationManager::parse_next_header_value(fragment).unwrap();
|
|
||||||
assert_eq!(parsed.0, "https://example.com/meta");
|
|
||||||
assert_eq!(&fragment[..parsed.1], r#" "https://example.com/meta""#);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -4010,104 +4002,113 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[rstest]
|
||||||
fn validate_authorization_response_issuer_accepts_match_and_missing_issuer() {
|
#[case::matching_issuer(
|
||||||
|
Some("https://auth.example.com"),
|
||||||
|
false,
|
||||||
|
Some("https://auth.example.com")
|
||||||
|
)]
|
||||||
|
#[case::missing_issuer_when_not_required(Some("https://auth.example.com"), false, None)]
|
||||||
|
fn validate_authorization_response_issuer_accepts_valid_cases(
|
||||||
|
#[case] expected_issuer: Option<&str>,
|
||||||
|
#[case] require_issuer: bool,
|
||||||
|
#[case] received_issuer: Option<&str>,
|
||||||
|
) {
|
||||||
let pkce = PkceCodeVerifier::new("verifier".to_string());
|
let pkce = PkceCodeVerifier::new("verifier".to_string());
|
||||||
let csrf = CsrfToken::new("csrf".to_string());
|
let csrf = CsrfToken::new("csrf".to_string());
|
||||||
let state = StoredAuthorizationState::new_with_expected_issuer(
|
let state = StoredAuthorizationState::new_with_expected_issuer(
|
||||||
&pkce,
|
&pkce,
|
||||||
&csrf,
|
&csrf,
|
||||||
Some("https://auth.example.com".to_string()),
|
expected_issuer.map(str::to_owned),
|
||||||
false,
|
require_issuer,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
AuthorizationManager::validate_authorization_response_issuer(
|
AuthorizationManager::validate_authorization_response_issuer(&state, received_issuer)
|
||||||
&state,
|
|
||||||
Some("https://auth.example.com")
|
|
||||||
)
|
|
||||||
.is_ok()
|
.is_ok()
|
||||||
);
|
);
|
||||||
assert!(AuthorizationManager::validate_authorization_response_issuer(&state, None).is_ok());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[derive(Clone, Copy, Debug)]
|
||||||
fn validate_authorization_response_issuer_requires_issuer_when_advertised() {
|
enum ExpectedIssuerError {
|
||||||
let pkce = PkceCodeVerifier::new("verifier".to_string());
|
Missing {
|
||||||
let csrf = CsrfToken::new("csrf".to_string());
|
expected_issuer: &'static str,
|
||||||
let state = StoredAuthorizationState::new_with_expected_issuer(
|
},
|
||||||
&pkce,
|
NotRecorded,
|
||||||
&csrf,
|
Mismatch {
|
||||||
Some("https://auth.example.com".to_string()),
|
expected_issuer: &'static str,
|
||||||
true,
|
received_issuer: &'static str,
|
||||||
);
|
},
|
||||||
|
}
|
||||||
|
|
||||||
let error =
|
fn assert_expected_issuer_error(error: AuthError, expected: ExpectedIssuerError) {
|
||||||
AuthorizationManager::validate_authorization_response_issuer(&state, None).unwrap_err();
|
match expected {
|
||||||
|
ExpectedIssuerError::Missing { expected_issuer } => assert!(matches!(
|
||||||
assert!(matches!(
|
|
||||||
error,
|
error,
|
||||||
AuthError::AuthorizationServerMissingIssuer { expected_issuer }
|
AuthError::AuthorizationServerMissingIssuer { expected_issuer: actual }
|
||||||
if expected_issuer == "https://auth.example.com"
|
if actual == expected_issuer
|
||||||
));
|
)),
|
||||||
}
|
ExpectedIssuerError::NotRecorded => assert!(
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_authorization_response_issuer_rejects_present_issuer_without_expected_issuer() {
|
|
||||||
let pkce = PkceCodeVerifier::new("verifier".to_string());
|
|
||||||
let csrf = CsrfToken::new("csrf".to_string());
|
|
||||||
let state = StoredAuthorizationState::new_with_expected_issuer(&pkce, &csrf, None, false);
|
|
||||||
|
|
||||||
let error = AuthorizationManager::validate_authorization_response_issuer(
|
|
||||||
&state,
|
|
||||||
Some("https://auth.example.com"),
|
|
||||||
)
|
|
||||||
.unwrap_err();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
matches!(error, AuthError::AuthorizationFailed(message) if message.contains("expected issuer was not recorded"))
|
matches!(error, AuthError::AuthorizationFailed(message) if message.contains("expected issuer was not recorded"))
|
||||||
);
|
),
|
||||||
}
|
ExpectedIssuerError::Mismatch {
|
||||||
|
expected_issuer,
|
||||||
#[test]
|
received_issuer,
|
||||||
fn validate_authorization_response_issuer_rejects_required_issuer_without_expected_issuer() {
|
} => assert!(matches!(
|
||||||
let pkce = PkceCodeVerifier::new("verifier".to_string());
|
|
||||||
let csrf = CsrfToken::new("csrf".to_string());
|
|
||||||
let state = StoredAuthorizationState::new_with_expected_issuer(&pkce, &csrf, None, true);
|
|
||||||
|
|
||||||
let error =
|
|
||||||
AuthorizationManager::validate_authorization_response_issuer(&state, None).unwrap_err();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
matches!(error, AuthError::AuthorizationFailed(message) if message.contains("expected issuer was not recorded"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validate_authorization_response_issuer_rejects_mismatch() {
|
|
||||||
let pkce = PkceCodeVerifier::new("verifier".to_string());
|
|
||||||
let csrf = CsrfToken::new("csrf".to_string());
|
|
||||||
let state = StoredAuthorizationState::new_with_expected_issuer(
|
|
||||||
&pkce,
|
|
||||||
&csrf,
|
|
||||||
Some("https://auth.example.com".to_string()),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
let error = AuthorizationManager::validate_authorization_response_issuer(
|
|
||||||
&state,
|
|
||||||
Some("https://evil.example.com"),
|
|
||||||
)
|
|
||||||
.unwrap_err();
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
error,
|
error,
|
||||||
AuthError::AuthorizationServerMismatch {
|
AuthError::AuthorizationServerMismatch {
|
||||||
expected_issuer,
|
expected_issuer: actual_expected,
|
||||||
received_issuer
|
received_issuer: actual_received
|
||||||
} if expected_issuer == "https://auth.example.com"
|
} if actual_expected == expected_issuer && actual_received == received_issuer
|
||||||
&& received_issuer == "https://evil.example.com"
|
)),
|
||||||
));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::requires_advertised_issuer(
|
||||||
|
Some("https://auth.example.com"),
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
ExpectedIssuerError::Missing {
|
||||||
|
expected_issuer: "https://auth.example.com",
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
#[case::present_issuer_without_expected(
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
Some("https://auth.example.com"),
|
||||||
|
ExpectedIssuerError::NotRecorded
|
||||||
|
)]
|
||||||
|
#[case::required_issuer_without_expected(None, true, None, ExpectedIssuerError::NotRecorded)]
|
||||||
|
#[case::mismatched_issuer(
|
||||||
|
Some("https://auth.example.com"),
|
||||||
|
false,
|
||||||
|
Some("https://evil.example.com"),
|
||||||
|
ExpectedIssuerError::Mismatch {
|
||||||
|
expected_issuer: "https://auth.example.com",
|
||||||
|
received_issuer: "https://evil.example.com",
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
fn validate_authorization_response_issuer_rejects_invalid_cases(
|
||||||
|
#[case] expected_issuer: Option<&str>,
|
||||||
|
#[case] require_issuer: bool,
|
||||||
|
#[case] received_issuer: Option<&str>,
|
||||||
|
#[case] expected_error: ExpectedIssuerError,
|
||||||
|
) {
|
||||||
|
let pkce = PkceCodeVerifier::new("verifier".to_string());
|
||||||
|
let csrf = CsrfToken::new("csrf".to_string());
|
||||||
|
let state = StoredAuthorizationState::new_with_expected_issuer(
|
||||||
|
&pkce,
|
||||||
|
&csrf,
|
||||||
|
expected_issuer.map(str::to_owned),
|
||||||
|
require_issuer,
|
||||||
|
);
|
||||||
|
|
||||||
|
let error =
|
||||||
|
AuthorizationManager::validate_authorization_response_issuer(&state, received_issuer)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert_expected_issuer_error(error, expected_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -4642,62 +4643,40 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
fn client_secret_credentials_config() -> super::ClientCredentialsConfig {
|
||||||
async fn validate_client_credentials_metadata_accepts_supported_method() {
|
super::ClientCredentialsConfig::ClientSecret {
|
||||||
|
client_id: "id".to_string(),
|
||||||
|
client_secret: "secret".to_string(),
|
||||||
|
scopes: vec![],
|
||||||
|
resource: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metadata_with_auth_methods(methods: serde_json::Value) -> AuthorizationMetadata {
|
||||||
let mut additional_fields = HashMap::new();
|
let mut additional_fields = HashMap::new();
|
||||||
additional_fields.insert(
|
additional_fields.insert("token_endpoint_auth_methods_supported".to_string(), methods);
|
||||||
"token_endpoint_auth_methods_supported".to_string(),
|
AuthorizationMetadata {
|
||||||
serde_json::json!(["client_secret_post", "client_secret_basic"]),
|
|
||||||
);
|
|
||||||
let meta = AuthorizationMetadata {
|
|
||||||
authorization_endpoint: "http://localhost/authorize".to_string(),
|
authorization_endpoint: "http://localhost/authorize".to_string(),
|
||||||
token_endpoint: "http://localhost/token".to_string(),
|
token_endpoint: "http://localhost/token".to_string(),
|
||||||
additional_fields,
|
additional_fields,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
}
|
||||||
let mgr = manager_with_metadata(Some(meta)).await;
|
|
||||||
let config = super::ClientCredentialsConfig::ClientSecret {
|
|
||||||
client_id: "id".to_string(),
|
|
||||||
client_secret: "secret".to_string(),
|
|
||||||
scopes: vec![],
|
|
||||||
resource: None,
|
|
||||||
};
|
|
||||||
mgr.validate_client_credentials_metadata(&config).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::supported_methods(Some(serde_json::json!([
|
||||||
|
"client_secret_post",
|
||||||
|
"client_secret_basic"
|
||||||
|
])))]
|
||||||
|
#[case::field_absent(None)]
|
||||||
|
#[case::client_secret_basic_only(Some(serde_json::json!(["client_secret_basic"])))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn validate_client_credentials_metadata_permits_when_field_absent() {
|
async fn validate_client_credentials_metadata_accepts_supported_configurations(
|
||||||
let mgr = manager_with_metadata(None).await;
|
#[case] auth_methods: Option<serde_json::Value>,
|
||||||
let config = super::ClientCredentialsConfig::ClientSecret {
|
) {
|
||||||
client_id: "id".to_string(),
|
let mgr = manager_with_metadata(auth_methods.map(metadata_with_auth_methods)).await;
|
||||||
client_secret: "secret".to_string(),
|
let config = client_secret_credentials_config();
|
||||||
scopes: vec![],
|
|
||||||
resource: None,
|
|
||||||
};
|
|
||||||
mgr.validate_client_credentials_metadata(&config).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn validate_client_credentials_metadata_accepts_client_secret_basic_only() {
|
|
||||||
let mut additional_fields = HashMap::new();
|
|
||||||
additional_fields.insert(
|
|
||||||
"token_endpoint_auth_methods_supported".to_string(),
|
|
||||||
serde_json::json!(["client_secret_basic"]),
|
|
||||||
);
|
|
||||||
let meta = AuthorizationMetadata {
|
|
||||||
authorization_endpoint: "http://localhost/authorize".to_string(),
|
|
||||||
token_endpoint: "http://localhost/token".to_string(),
|
|
||||||
additional_fields,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let mgr = manager_with_metadata(Some(meta)).await;
|
|
||||||
let config = super::ClientCredentialsConfig::ClientSecret {
|
|
||||||
client_id: "id".to_string(),
|
|
||||||
client_secret: "secret".to_string(),
|
|
||||||
scopes: vec![],
|
|
||||||
resource: None,
|
|
||||||
};
|
|
||||||
// A server advertising only client_secret_basic must be accepted.
|
|
||||||
mgr.validate_client_credentials_metadata(&config).unwrap();
|
mgr.validate_client_credentials_metadata(&config).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,8 @@ impl StreamableHttpClientTransport<reqwest::Client> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use rstest::rstest;
|
||||||
|
|
||||||
use super::parse_json_rpc_error;
|
use super::parse_json_rpc_error;
|
||||||
use crate::{
|
use crate::{
|
||||||
model::JsonRpcMessage,
|
model::JsonRpcMessage,
|
||||||
|
|
@ -346,25 +348,15 @@ mod tests {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[rstest]
|
||||||
fn parse_json_rpc_error_rejects_non_error_request() {
|
#[case::non_error_request(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#)]
|
||||||
// A valid JSON-RPC request (method + id) must not be accepted as an error.
|
#[case::notification(
|
||||||
let body = r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#;
|
r#"{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":1}}"#
|
||||||
|
)]
|
||||||
|
#[case::plain_text("not json at all")]
|
||||||
|
#[case::empty("")]
|
||||||
|
#[case::truncated_json(r#"{"broken":"#)]
|
||||||
|
fn parse_json_rpc_error_rejects_non_error_bodies(#[case] body: &str) {
|
||||||
assert!(parse_json_rpc_error(body).is_none());
|
assert!(parse_json_rpc_error(body).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_json_rpc_error_rejects_notification() {
|
|
||||||
// A notification (method, no id) must not be accepted as an error.
|
|
||||||
let body =
|
|
||||||
r#"{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":1}}"#;
|
|
||||||
assert!(parse_json_rpc_error(body).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_json_rpc_error_rejects_malformed_json() {
|
|
||||||
assert!(parse_json_rpc_error("not json at all").is_none());
|
|
||||||
assert!(parse_json_rpc_error("").is_none());
|
|
||||||
assert!(parse_json_rpc_error(r#"{"broken":"#).is_none());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ use tokio::io::{AsyncRead, ReadBuf};
|
||||||
// A slow tool server that sleeps before returning a response.
|
// A slow tool server that sleeps before returning a response.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct SlowToolServer {
|
struct SlowToolServer {
|
||||||
|
#[expect(dead_code, reason = "tool_handler macro accesses this router field")]
|
||||||
tool_router: ToolRouter<Self>,
|
tool_router: ToolRouter<Self>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use serde_json::json;
|
||||||
use tokio::sync::{Mutex, Notify};
|
use tokio::sync::{Mutex, Notify};
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
pub struct Server {}
|
struct Server {}
|
||||||
|
|
||||||
impl ServerHandler for Server {
|
impl ServerHandler for Server {
|
||||||
fn get_info(&self) -> ServerInfo {
|
fn get_info(&self) -> ServerInfo {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ impl ClientHandler for MyClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MyServer {
|
pub struct MyServer {
|
||||||
|
#[expect(dead_code, reason = "tool_handler macro accesses this router field")]
|
||||||
tool_router: ToolRouter<Self>,
|
tool_router: ToolRouter<Self>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@ use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct CodeReviewRequest {
|
struct CodeReviewRequest {
|
||||||
pub file_path: String,
|
file_path: String,
|
||||||
pub language: String,
|
language: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[prompt_handler(router = self.prompt_router)]
|
#[prompt_handler(router = self.prompt_router)]
|
||||||
|
|
@ -194,17 +194,17 @@ impl CodeReviewRequest {}
|
||||||
|
|
||||||
// Struct defined for testing optional field schema generation
|
// Struct defined for testing optional field schema generation
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct OptionalFieldTestSchema {
|
struct OptionalFieldTestSchema {
|
||||||
#[schemars(description = "An optional description field")]
|
#[schemars(description = "An optional description field")]
|
||||||
pub description: Option<String>,
|
description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Struct defined for testing optional i64 field schema generation and null handling
|
// Struct defined for testing optional i64 field schema generation and null handling
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct OptionalI64TestSchema {
|
struct OptionalI64TestSchema {
|
||||||
#[schemars(description = "An optional i64 field")]
|
#[schemars(description = "An optional i64 field")]
|
||||||
pub count: Option<i64>,
|
count: Option<i64>,
|
||||||
pub mandatory_field: String, // Added to ensure non-empty object schema
|
mandatory_field: String, // Added to ensure non-empty object schema
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dummy struct to host the test prompt method
|
// Dummy struct to host the test prompt method
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,21 @@ use rmcp::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct TestHandler<T: 'static = ()> {
|
struct TestHandler<T: 'static = ()> {
|
||||||
pub _marker: std::marker::PhantomData<fn(*const T)>,
|
_marker: std::marker::PhantomData<fn(*const T)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static> ServerHandler for TestHandler<T> {}
|
impl<T: 'static> ServerHandler for TestHandler<T> {}
|
||||||
|
|
||||||
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Request {
|
struct Request {
|
||||||
pub fields: HashMap<String, String>,
|
fields: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Sum {
|
struct Sum {
|
||||||
pub a: i32,
|
a: i32,
|
||||||
pub b: i32,
|
b: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rmcp::prompt_router(router = "test_router")]
|
#[rmcp::prompt_router(router = "test_router")]
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ use rmcp::{
|
||||||
/// Server with tools having different task support modes.
|
/// Server with tools having different task support modes.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TaskSupportTestServer {
|
pub struct TaskSupportTestServer {
|
||||||
|
#[expect(dead_code, reason = "tool_handler macro accesses this router field")]
|
||||||
tool_router: ToolRouter<Self>,
|
tool_router: ToolRouter<Self>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,34 +45,27 @@ impl ServerHandler for TestToolServer {
|
||||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn call_tool(
|
async fn call_tool(
|
||||||
&self,
|
&self,
|
||||||
request: rmcp::model::CallToolRequestParams,
|
request: rmcp::model::CallToolRequestParams,
|
||||||
context: rmcp::service::RequestContext<RoleServer>,
|
context: rmcp::service::RequestContext<RoleServer>,
|
||||||
) -> impl std::future::Future<Output = Result<CallToolResult, rmcp::ErrorData>> + MaybeSendFuture + '_
|
) -> Result<CallToolResult, rmcp::ErrorData> {
|
||||||
{
|
|
||||||
async move {
|
|
||||||
let router = self.router.read().await;
|
let router = self.router.read().await;
|
||||||
let tcc = ToolCallContext::new(self, request, context);
|
let tcc = ToolCallContext::new(self, request, context);
|
||||||
router.call(tcc).await
|
router.call(tcc).await
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn list_tools(
|
async fn list_tools(
|
||||||
&self,
|
&self,
|
||||||
_request: Option<rmcp::model::PaginatedRequestParams>,
|
_request: Option<rmcp::model::PaginatedRequestParams>,
|
||||||
_context: rmcp::service::RequestContext<RoleServer>,
|
_context: rmcp::service::RequestContext<RoleServer>,
|
||||||
) -> impl std::future::Future<Output = Result<rmcp::model::ListToolsResult, rmcp::ErrorData>>
|
) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> {
|
||||||
+ MaybeSendFuture
|
|
||||||
+ '_ {
|
|
||||||
async move {
|
|
||||||
let router = self.router.read().await;
|
let router = self.router.read().await;
|
||||||
Ok(rmcp::model::ListToolsResult {
|
Ok(rmcp::model::ListToolsResult {
|
||||||
tools: router.list_all(),
|
tools: router.list_all(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn on_initialized(
|
fn on_initialized(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Parameters for weather tool.
|
/// Parameters for weather tool.
|
||||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct GetWeatherRequest {
|
struct GetWeatherRequest {
|
||||||
/// City of interest.
|
/// City of interest.
|
||||||
pub city: String,
|
city: String,
|
||||||
/// Date of interest.
|
/// Date of interest.
|
||||||
pub date: String,
|
date: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool_handler(router = self.tool_router)]
|
#[tool_handler(router = self.tool_router)]
|
||||||
|
|
@ -162,21 +162,21 @@ impl GetWeatherRequest {}
|
||||||
|
|
||||||
/// Struct defined for testing optional field schema generation.
|
/// Struct defined for testing optional field schema generation.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct OptionalFieldTestSchema {
|
struct OptionalFieldTestSchema {
|
||||||
/// Field description.
|
/// Field description.
|
||||||
#[schemars(description = "An optional description field")]
|
#[schemars(description = "An optional description field")]
|
||||||
pub description: Option<String>,
|
description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Struct defined for testing optional i64 field schema generation and null handling.
|
/// Struct defined for testing optional i64 field schema generation and null handling.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct OptionalI64TestSchema {
|
struct OptionalI64TestSchema {
|
||||||
/// Optional count field.
|
/// Optional count field.
|
||||||
#[schemars(description = "An optional i64 field")]
|
#[schemars(description = "An optional i64 field")]
|
||||||
pub count: Option<i64>,
|
count: Option<i64>,
|
||||||
|
|
||||||
/// Added to ensure non-empty object schema.
|
/// Added to ensure non-empty object schema.
|
||||||
pub mandatory_field: String,
|
mandatory_field: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dummy struct to host the test tool method.
|
/// Dummy struct to host the test tool method.
|
||||||
|
|
@ -370,7 +370,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> {
|
||||||
|
|
||||||
/// Minimal server: no tool_router field, no new(), no get_info().
|
/// Minimal server: no tool_router field, no new(), no get_info().
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MinimalServer;
|
struct MinimalServer;
|
||||||
|
|
||||||
#[tool_router]
|
#[tool_router]
|
||||||
impl MinimalServer {
|
impl MinimalServer {
|
||||||
|
|
@ -452,7 +452,7 @@ async fn test_minimal_server_tool_call() -> anyhow::Result<()> {
|
||||||
/// Same minimal pattern as [`MinimalServer`], but `#[tool_handler]` is omitted using
|
/// Same minimal pattern as [`MinimalServer`], but `#[tool_handler]` is omitted using
|
||||||
/// `#[tool_router(server_handler)]` (emits `#[tool_handler]` for a second macro pass).
|
/// `#[tool_router(server_handler)]` (emits `#[tool_handler]` for a second macro pass).
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ElidedToolHandlerServer;
|
struct ElidedToolHandlerServer;
|
||||||
|
|
||||||
#[tool_router(server_handler)]
|
#[tool_router(server_handler)]
|
||||||
impl ElidedToolHandlerServer {
|
impl ElidedToolHandlerServer {
|
||||||
|
|
@ -509,7 +509,7 @@ async fn test_tool_router_server_handler_flag_end_to_end_tool_call() -> anyhow::
|
||||||
|
|
||||||
/// Server with custom name/version/instructions via tool_handler attributes.
|
/// Server with custom name/version/instructions via tool_handler attributes.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CustomInfoServer;
|
struct CustomInfoServer;
|
||||||
|
|
||||||
#[tool_router]
|
#[tool_router]
|
||||||
impl CustomInfoServer {
|
impl CustomInfoServer {
|
||||||
|
|
@ -539,7 +539,7 @@ fn test_custom_info_server() {
|
||||||
|
|
||||||
/// Server that provides its own get_info() — macro should not override it.
|
/// Server that provides its own get_info() — macro should not override it.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ManualInfoServer;
|
struct ManualInfoServer;
|
||||||
|
|
||||||
#[tool_router]
|
#[tool_router]
|
||||||
impl ManualInfoServer {
|
impl ManualInfoServer {
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,20 @@ use rmcp::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct TestHandler<T: 'static = ()> {
|
struct TestHandler<T: 'static = ()> {
|
||||||
pub _marker: std::marker::PhantomData<fn(*const T)>,
|
_marker: std::marker::PhantomData<fn(*const T)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: 'static> ServerHandler for TestHandler<T> {}
|
impl<T: 'static> ServerHandler for TestHandler<T> {}
|
||||||
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Request {
|
struct Request {
|
||||||
pub fields: HashMap<String, String>,
|
fields: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, schemars::JsonSchema, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Sum {
|
struct Sum {
|
||||||
pub a: i32,
|
a: i32,
|
||||||
pub b: i32,
|
b: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rmcp::tool_router(router = test_router_1)]
|
#[rmcp::tool_router(router = test_router_1)]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue