chore: consolidate repeated rmcp tests (#931)

This commit is contained in:
Dale Seo 2026-06-26 16:15:20 -04:00 • committed by GitHub
commit e1af378949
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 306 additions and 332 deletions

View file

@ -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"]

View file

@ -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));
} }
} }

View file

@ -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]

View file

@ -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();
} }

View file

@ -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());
}
} }

View file

@ -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>,
} }

View file

@ -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 {

View file

@ -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>,
} }

View file

@ -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

View file

@ -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")]

View file

@ -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>,
} }

View file

@ -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,

View file

@ -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 {

View file

@ -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)]