feat!: align model types with MCP 2025-11-25 spec (#927)

* feat!: align model types with MCP 2025-11-25 spec

* fix: complete 2025-11-25 model spec conformance
This commit is contained in:
Dale Seo 2026-06-24 20:41:13 -04:00 • committed by GitHub
commit 77932141e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 4321 additions and 3886 deletions

View file

@ -171,10 +171,12 @@ impl ServerHandler for TestServer {
};
if let Err(e) = peer
.notify_logging_message(LoggingMessageNotificationParam {
level: request.level,
data,
logger,
.notify_logging_message({
let mut param = LoggingMessageNotificationParam::new(request.level, data);
if let Some(l) = logger {
param = param.with_logger(l);
}
param
})
.await
{

View file

@ -54,10 +54,7 @@ fn test_complete_request_param_serialization() {
let request = CompleteRequestParams::new(
Reference::for_prompt("weather_prompt"),
ArgumentInfo {
name: "location".to_string(),
value: "San".to_string(),
},
ArgumentInfo::new("location", "San"),
)
.with_context(CompletionContext::with_arguments(args));
@ -144,11 +141,8 @@ fn test_reference_convenience_methods() {
#[test]
fn test_completion_serialization_format() {
// Test that completion follows MCP 2025-06-18 specification format
let completion = CompletionInfo {
values: vec!["value1".to_string(), "value2".to_string()],
total: Some(2),
has_more: Some(false),
};
let completion =
CompletionInfo::with_all_values(vec!["value1".to_string(), "value2".to_string()]).unwrap();
let json = serde_json::to_value(&completion).unwrap();
@ -162,18 +156,18 @@ fn test_completion_serialization_format() {
#[test]
fn test_resource_reference() {
// Test that ResourceReference works correctly
let resource_ref = ResourceReference {
uri: "test://uri".to_string(),
};
// ResourceTemplateReference가 `ref/resource` 와이어 태그로 직렬화/역직렬화되는지 확인
let reference = Reference::for_resource("test://uri");
// Test that ResourceReference works correctly
let another_ref = ResourceReference {
uri: "test://uri".to_string(),
};
let json = serde_json::to_value(&reference).unwrap();
assert_eq!(json["type"], "ref/resource");
assert_eq!(json["uri"], "test://uri");
// They should be equivalent
assert_eq!(resource_ref.uri, another_ref.uri);
let back: Reference = serde_json::from_value(json).unwrap();
match back {
Reference::Resource(r) => assert_eq!(r.uri, "test://uri"),
other => panic!("expected Reference::Resource, got {other:?}"),
}
}
#[test]
@ -197,10 +191,7 @@ fn test_mcp_schema_compliance() {
// Test that our types serialize correctly according to MCP specification
let request = CompleteRequestParams::new(
Reference::for_resource("file://{path}"),
ArgumentInfo {
name: "path".to_string(),
value: "src/".to_string(),
},
ArgumentInfo::new("path", "src/"),
);
let json_str = serde_json::to_string(&request).unwrap();

View file

@ -40,7 +40,7 @@ impl Demo {
&self,
chat_request: Parameters<ChatRequest>,
) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request.0)?;
let content = ContentBlock::json(chat_request.0)?;
Ok(CallToolResult::success(vec![content]))
}
}

View file

@ -126,7 +126,8 @@ mod untagged_server_result {
#[test]
fn round_trip_call_tool_result_preserves_variant() {
let original = CallToolResult::success(vec![rmcp::model::Content::text("hello world")]);
let original =
CallToolResult::success(vec![rmcp::model::ContentBlock::text("hello world")]);
let json = serde_json::to_value(&original).unwrap();
let result = parse_result(wrap_response(json));
assert!(matches!(result, ServerResult::CallToolResult(_)));

View file

@ -36,15 +36,18 @@ async fn test_elicitation_serialization() {
);
}
/// Test CreateElicitationRequestParams structure serialization/deserialization
/// Test ElicitRequestParams structure serialization/deserialization
#[tokio::test]
async fn test_elicitation_request_param_serialization() {
let schema = ElicitationSchema::builder()
.required_property("email", PrimitiveSchema::String(StringSchema::email()))
.required_property(
"email",
PrimitiveSchemaDefinition::String(StringSchema::email()),
)
.build()
.unwrap();
let request_param = CreateElicitationRequestParams::FormElicitationParams {
let request_param = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Please provide your email address".to_string(),
requested_schema: schema,
@ -70,15 +73,15 @@ async fn test_elicitation_request_param_serialization() {
assert_eq!(json, expected);
// Test deserialization
let deserialized: CreateElicitationRequestParams = serde_json::from_value(expected).unwrap();
let deserialized: ElicitRequestParams = serde_json::from_value(expected).unwrap();
match (&deserialized, &request_param) {
(
CreateElicitationRequestParams::FormElicitationParams {
ElicitRequestParams::FormElicitationParams {
meta: None,
message: msg1,
requested_schema: schema1,
},
CreateElicitationRequestParams::FormElicitationParams {
ElicitRequestParams::FormElicitationParams {
meta: None,
message: msg2,
requested_schema: schema2,
@ -91,15 +94,12 @@ async fn test_elicitation_request_param_serialization() {
}
}
/// Test CreateElicitationResult structure with different action types
/// Test ElicitResult structure with different action types
#[tokio::test]
async fn test_elicitation_result_serialization() {
// Test Accept with content
let accept_result = CreateElicitationResult {
action: ElicitationAction::Accept,
content: Some(json!({"email": "user@example.com"})),
meta: None,
};
let accept_result = ElicitResult::new(ElicitationAction::Accept)
.with_content(json!({"email": "user@example.com"}));
let json = serde_json::to_value(&accept_result).unwrap();
let expected = json!({
@ -109,11 +109,7 @@ async fn test_elicitation_result_serialization() {
assert_eq!(json, expected);
// Test Decline without content
let decline_result = CreateElicitationResult {
action: ElicitationAction::Decline,
content: None,
meta: None,
};
let decline_result = ElicitResult::new(ElicitationAction::Decline);
let json = serde_json::to_value(&decline_result).unwrap();
let expected = json!({
@ -123,16 +119,15 @@ async fn test_elicitation_result_serialization() {
assert_eq!(json, expected);
// Test deserialization
let deserialized: CreateElicitationResult = serde_json::from_value(expected).unwrap();
let deserialized: ElicitResult = serde_json::from_value(expected).unwrap();
assert_eq!(deserialized.action, ElicitationAction::Decline);
assert_eq!(deserialized.content, None);
assert_eq!(deserialized.meta, None);
// Test protocol-level metadata round-trips as _meta.
let meta_result =
CreateElicitationResult::new(ElicitationAction::Accept).with_meta(Meta(object!({
"traceId": "elicitation-123"
})));
let meta_result = ElicitResult::new(ElicitationAction::Accept).with_meta(Meta(object!({
"traceId": "elicitation-123"
})));
let json = serde_json::to_value(&meta_result).unwrap();
let expected = json!({
@ -141,7 +136,7 @@ async fn test_elicitation_result_serialization() {
});
assert_eq!(json, expected);
let deserialized: CreateElicitationResult = serde_json::from_value(expected).unwrap();
let deserialized: ElicitResult = serde_json::from_value(expected).unwrap();
assert_eq!(
deserialized.meta,
Some(Meta(object!({ "traceId": "elicitation-123" })))
@ -154,7 +149,7 @@ async fn test_elicitation_json_rpc_protocol() {
let schema = ElicitationSchema::builder()
.required_property(
"confirmation",
PrimitiveSchema::Boolean(BooleanSchema::new()),
PrimitiveSchemaDefinition::Boolean(BooleanSchema::new()),
)
.build()
.unwrap();
@ -163,13 +158,11 @@ async fn test_elicitation_json_rpc_protocol() {
let request = JsonRpcRequest {
jsonrpc: JsonRpcVersion2_0,
id: RequestId::Number(1),
request: CreateElicitationRequest::new(
CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: "Do you want to continue?".to_string(),
requested_schema: schema,
},
),
request: ElicitRequest::new(ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Do you want to continue?".to_string(),
requested_schema: schema,
}),
};
// Test serialization of complete request
@ -180,11 +173,10 @@ async fn test_elicitation_json_rpc_protocol() {
assert_eq!(json["params"]["message"], "Do you want to continue?");
// Test deserialization
let deserialized: JsonRpcRequest<CreateElicitationRequest> =
serde_json::from_value(json).unwrap();
let deserialized: JsonRpcRequest<ElicitRequest> = serde_json::from_value(json).unwrap();
assert_eq!(deserialized.id, RequestId::Number(1));
match &deserialized.request.params {
CreateElicitationRequestParams::FormElicitationParams { message, .. } => {
ElicitRequestParams::FormElicitationParams { message, .. } => {
assert_eq!(message, "Do you want to continue?");
}
_ => panic!("Expected FormElicitationParam variant"),
@ -250,7 +242,7 @@ async fn test_elicitation_spec_compliance() {
#[tokio::test]
async fn test_elicitation_error_handling() {
// Test minimal schema handling (empty properties is technically valid)
let minimal_schema_request = CreateElicitationRequestParams::FormElicitationParams {
let minimal_schema_request = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Test message".to_string(),
requested_schema: ElicitationSchema::builder().build().unwrap(),
@ -260,11 +252,14 @@ async fn test_elicitation_error_handling() {
let _json = serde_json::to_value(&minimal_schema_request).unwrap();
// Test empty message
let empty_message_request = CreateElicitationRequestParams::FormElicitationParams {
let empty_message_request = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "".to_string(),
requested_schema: ElicitationSchema::builder()
.property("value", PrimitiveSchema::String(StringSchema::new()))
.property(
"value",
PrimitiveSchemaDefinition::String(StringSchema::new()),
)
.build()
.unwrap(),
};
@ -282,11 +277,14 @@ async fn test_elicitation_error_handling() {
#[tokio::test]
async fn test_elicitation_performance() {
let schema = ElicitationSchema::builder()
.property("data", PrimitiveSchema::String(StringSchema::new()))
.property(
"data",
PrimitiveSchemaDefinition::String(StringSchema::new()),
)
.build()
.unwrap();
let request = CreateElicitationRequestParams::FormElicitationParams {
let request = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Performance test message".to_string(),
requested_schema: schema,
@ -297,7 +295,7 @@ async fn test_elicitation_performance() {
// Serialize/deserialize 1000 times
for _ in 0..1000 {
let json = serde_json::to_value(&request).unwrap();
let _deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap();
let _deserialized: ElicitRequestParams = serde_json::from_value(json).unwrap();
}
let duration = start.elapsed();
@ -326,9 +324,7 @@ async fn test_elicitation_capabilities() {
assert_eq!(elicitation_cap.url, None);
// Test with schema validation enabled
elicitation_cap.form = Some(FormElicitationCapability {
schema_validation: Some(true),
});
elicitation_cap.form = Some(FormElicitationCapability::new().with_schema_validation(true));
// Test serialization
let json = serde_json::to_value(&elicitation_cap).unwrap();
@ -423,14 +419,14 @@ async fn test_elicitation_convenience_methods() {
.contains("Option A")
);
// Test that CreateElicitationRequestParam can be created with type-safe schemas
let confirmation_request = CreateElicitationRequestParams::FormElicitationParams {
// Test that ElicitRequestParams can be created with type-safe schemas
let confirmation_request = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Test confirmation".to_string(),
requested_schema: ElicitationSchema::builder()
.property(
"confirmed",
PrimitiveSchema::Boolean(
PrimitiveSchemaDefinition::Boolean(
BooleanSchema::new()
.description("User confirmation (true for yes, false for no)"),
),
@ -467,7 +463,7 @@ async fn test_elicitation_structured_schemas() {
.build()
.unwrap();
let request = CreateElicitationRequestParams::FormElicitationParams {
let request = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Please provide your user information".to_string(),
requested_schema: schema,
@ -475,10 +471,10 @@ async fn test_elicitation_structured_schemas() {
// Test that complex schemas serialize/deserialize correctly
let json = serde_json::to_value(&request).unwrap();
let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap();
let deserialized: ElicitRequestParams = serde_json::from_value(json).unwrap();
match deserialized {
CreateElicitationRequestParams::FormElicitationParams {
ElicitRequestParams::FormElicitationParams {
message,
requested_schema,
..
@ -699,7 +695,7 @@ async fn test_elicitation_multi_select_enum() {
.build()
.unwrap();
let request = CreateElicitationRequestParams::FormElicitationParams {
let request = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Please provide your user information".to_string(),
requested_schema: schema,
@ -707,10 +703,10 @@ async fn test_elicitation_multi_select_enum() {
// Test that complex schemas serialize/deserialize correctly
let json = serde_json::to_value(&request).unwrap();
let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap();
let deserialized: ElicitRequestParams = serde_json::from_value(json).unwrap();
match deserialized {
CreateElicitationRequestParams::FormElicitationParams {
ElicitRequestParams::FormElicitationParams {
message,
requested_schema,
..
@ -722,30 +718,20 @@ async fn test_elicitation_multi_select_enum() {
assert!(matches!(
requested_schema.properties.get("choices").unwrap(),
PrimitiveSchema::Enum(EnumSchema::Multi(_))
PrimitiveSchemaDefinition::Enum(EnumSchema::Multi(_))
));
if let Some(PrimitiveSchema::Enum(schema)) = requested_schema.properties.get("choices")
if let Some(PrimitiveSchemaDefinition::Enum(schema)) =
requested_schema.properties.get("choices")
{
assert_eq!(
schema,
&EnumSchema::Multi(MultiSelectEnumSchema::Titled(
TitledMultiSelectEnumSchema::new(TitledItems {
any_of: vec![
ConstTitle {
const_: "A".to_string(),
title: "A name".to_string()
},
ConstTitle {
const_: "B".to_string(),
title: "B name".to_string()
},
ConstTitle {
const_: "C".to_string(),
title: "C name".to_string()
},
],
})
TitledMultiSelectEnumSchema::new(TitledItems::new(vec![
ConstTitle::new("A", "A name"),
ConstTitle::new("B", "B name"),
ConstTitle::new("C", "C name"),
]))
.with_min_items(1)
.with_max_items(2)
))
@ -773,7 +759,7 @@ async fn test_elicitation_single_select_enum() {
.build()
.unwrap();
let request = CreateElicitationRequestParams::FormElicitationParams {
let request = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Please provide your user information".to_string(),
requested_schema: schema,
@ -781,10 +767,10 @@ async fn test_elicitation_single_select_enum() {
// Test that complex schemas serialize/deserialize correctly
let json = serde_json::to_value(&request).unwrap();
let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap();
let deserialized: ElicitRequestParams = serde_json::from_value(json).unwrap();
match deserialized {
CreateElicitationRequestParams::FormElicitationParams {
ElicitRequestParams::FormElicitationParams {
message,
requested_schema,
..
@ -795,27 +781,19 @@ async fn test_elicitation_single_select_enum() {
assert_eq!(requested_schema.required, Some(vec!["choices".to_string()]));
assert!(matches!(
requested_schema.properties.get("choices").unwrap(),
PrimitiveSchema::Enum(EnumSchema::Single(_))
PrimitiveSchemaDefinition::Enum(EnumSchema::Single(_))
));
if let Some(PrimitiveSchema::Enum(schema)) = requested_schema.properties.get("choices")
if let Some(PrimitiveSchemaDefinition::Enum(schema)) =
requested_schema.properties.get("choices")
{
assert_eq!(
schema,
&EnumSchema::Single(SingleSelectEnumSchema::Titled(
TitledSingleSelectEnumSchema::new(vec![
ConstTitle {
const_: "A".to_string(),
title: "A name".to_string()
},
ConstTitle {
const_: "B".to_string(),
title: "B name".to_string()
},
ConstTitle {
const_: "C".to_string(),
title: "C name".to_string()
}
ConstTitle::new("A", "A name"),
ConstTitle::new("B", "B name"),
ConstTitle::new("C", "C name"),
])
))
)
@ -841,12 +819,12 @@ async fn test_elicitation_direction_server_to_client() {
let schema = ElicitationSchema::builder()
.property(
"name",
PrimitiveSchema::String(StringSchema::new().description("Enter your name")),
PrimitiveSchemaDefinition::String(StringSchema::new().description("Enter your name")),
)
.build()
.unwrap();
let elicitation_request = CreateElicitationRequestParams::FormElicitationParams {
let elicitation_request = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Please enter your name".to_string(),
requested_schema: schema,
@ -858,23 +836,20 @@ async fn test_elicitation_direction_server_to_client() {
assert_eq!(serialized["requestedSchema"]["type"], "object");
// Test that elicitation requests are part of ServerRequest
let _server_request =
ServerRequest::CreateElicitationRequest(CreateElicitationRequest::new(elicitation_request));
let _server_request = ServerRequest::ElicitRequest(ElicitRequest::new(elicitation_request));
// Test that client can respond with elicitation results
let client_result = ClientResult::CreateElicitationResult(CreateElicitationResult {
action: ElicitationAction::Accept,
content: Some(json!("John Doe")),
meta: None,
});
let client_result = ClientResult::ElicitResult(
ElicitResult::new(ElicitationAction::Accept).with_content(json!("John Doe")),
);
// Verify client result can be serialized
match client_result {
ClientResult::CreateElicitationResult(result) => {
ClientResult::ElicitResult(result) => {
assert_eq!(result.action, ElicitationAction::Accept);
assert_eq!(result.content, Some(json!("John Doe")));
}
_ => panic!("CreateElicitationResult should be part of ClientResult"),
_ => panic!("ElicitResult should be part of ClientResult"),
}
}
@ -888,15 +863,17 @@ async fn test_elicitation_json_rpc_direction() {
let schema = ElicitationSchema::builder()
.property(
"continue",
PrimitiveSchema::Boolean(BooleanSchema::new().description("Do you want to continue?")),
PrimitiveSchemaDefinition::Boolean(
BooleanSchema::new().description("Do you want to continue?"),
),
)
.build()
.unwrap();
// 1. Server creates elicitation request
let server_request = ServerJsonRpcMessage::request(
ServerRequest::CreateElicitationRequest(CreateElicitationRequest::new(
CreateElicitationRequestParams::FormElicitationParams {
ServerRequest::ElicitRequest(ElicitRequest::new(
ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Do you want to continue?".to_string(),
requested_schema: schema,
@ -913,11 +890,9 @@ async fn test_elicitation_json_rpc_direction() {
// 2. Client responds with elicitation result
let client_response = ClientJsonRpcMessage::response(
ClientResult::CreateElicitationResult(CreateElicitationResult {
action: ElicitationAction::Accept,
content: Some(json!(true)),
meta: None,
}),
ClientResult::ElicitResult(
ElicitResult::new(ElicitationAction::Accept).with_content(json!(true)),
),
RequestId::Number(1),
);
@ -946,13 +921,12 @@ async fn test_elicitation_actions_compliance() {
];
for action in actions {
let result = CreateElicitationResult {
action: action.clone(),
content: match action {
ElicitationAction::Accept => Some(serde_json::json!("some data")),
_ => None,
},
meta: None,
let result = {
let r = ElicitResult::new(action.clone());
match action {
ElicitationAction::Accept => r.with_content(serde_json::json!("some data")),
_ => r,
}
};
let json = serde_json::to_value(&result).unwrap();
@ -970,28 +944,25 @@ async fn test_elicitation_actions_compliance() {
assert_eq!(json["action"], "cancel");
assert!(json.get("content").is_none() || json["content"].is_null());
}
_ => {}
}
}
}
/// Test that CreateElicitationResult IS in ClientResult (response compliance)
/// Test that ElicitResult IS in ClientResult (response compliance)
#[tokio::test]
async fn test_elicitation_result_in_client_result() {
use rmcp::model::*;
// Test that clients can return elicitation results
let result = ClientResult::CreateElicitationResult(CreateElicitationResult {
action: ElicitationAction::Decline,
content: None,
meta: None,
});
let result = ClientResult::ElicitResult(ElicitResult::new(ElicitationAction::Decline));
match result {
ClientResult::CreateElicitationResult(elicit_result) => {
ClientResult::ElicitResult(elicit_result) => {
assert_eq!(elicit_result.action, ElicitationAction::Decline);
assert_eq!(elicit_result.content, None);
}
_ => panic!("CreateElicitationResult should be part of ClientResult"),
_ => panic!("ElicitResult should be part of ClientResult"),
}
}
@ -1008,24 +979,16 @@ async fn test_elicitation_capability_structure() {
assert!(default_cap.url.is_none());
// Test ElicitationCapability with schema validation enabled
let cap_with_validation = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
};
let cap_with_validation = ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true));
assert_eq!(
cap_with_validation.form.as_ref().unwrap().schema_validation,
Some(true)
);
// Test ElicitationCapability with schema validation disabled
let cap_without_validation = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(false),
}),
url: None,
};
let cap_without_validation = ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(false));
assert_eq!(
cap_without_validation
.form
@ -1059,12 +1022,10 @@ async fn test_elicitation_capability_structure() {
async fn test_client_capabilities_with_elicitation() {
// Test ClientCapabilities with elicitation capability
let capabilities = ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
})
.enable_elicitation_with(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
)
.build();
// Verify elicitation capability is present
@ -1101,12 +1062,10 @@ async fn test_initialize_request_with_elicitation() {
// Test InitializeRequestParam with elicitation capability
let init_param = InitializeRequestParams::new(
ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
})
.enable_elicitation_with(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
)
.build(),
Implementation::new("test-client", "1.0.0"),
);
@ -1143,12 +1102,10 @@ async fn test_capability_checking_logic() {
// Case 1: Client with elicitation capability
let client_with_capability = InitializeRequestParams::new(
ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
})
.enable_elicitation_with(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
)
.build(),
Implementation::new("test-client", "1.0.0"),
);
@ -1257,12 +1214,8 @@ async fn test_elicitation_capability_serialization() {
assert_eq!(json, serde_json::json!({}));
// Test capability with schema validation enabled
let cap_with_validation = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
};
let cap_with_validation = ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true));
let json = serde_json::to_value(&cap_with_validation).unwrap();
assert_eq!(
@ -1275,12 +1228,8 @@ async fn test_elicitation_capability_serialization() {
);
// Test capability with schema validation disabled
let cap_without_validation = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(false),
}),
url: None,
};
let cap_without_validation = ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(false));
let json = serde_json::to_value(&cap_without_validation).unwrap();
assert_eq!(
@ -1332,12 +1281,8 @@ async fn test_client_capabilities_elicitation_builder() {
);
// Test enabling elicitation with custom capability
let custom_elicitation = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(false),
}),
url: None,
};
let custom_elicitation = ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(false));
let caps_custom = ClientCapabilities::builder()
.enable_elicitation_with(custom_elicitation.clone())
@ -1361,12 +1306,18 @@ async fn test_create_elicitation_with_timeout_basic() {
// This test verifies that the method accepts timeout parameter
let schema = ElicitationSchema::builder()
.required_property("name", PrimitiveSchema::String(StringSchema::new()))
.required_property("email", PrimitiveSchema::String(StringSchema::new()))
.required_property(
"name",
PrimitiveSchemaDefinition::String(StringSchema::new()),
)
.required_property(
"email",
PrimitiveSchemaDefinition::String(StringSchema::new()),
)
.build()
.unwrap();
let _params = CreateElicitationRequestParams::FormElicitationParams {
let _params = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Enter your details".to_string(),
requested_schema: schema,
@ -1547,6 +1498,7 @@ async fn test_elicitation_action_error_mapping() {
let error = ElicitationError::UserCancelled;
assert!(format!("{}", error).contains("cancelled/dismissed"));
}
_ => {}
}
}
}
@ -1690,7 +1642,10 @@ async fn test_elicitation_examples_compile() {
async fn test_build_validation_required_field_not_in_properties() {
// Try to mark a field as required that doesn't exist in properties
let result = ElicitationSchema::builder()
.property("email", PrimitiveSchema::String(StringSchema::email()))
.property(
"email",
PrimitiveSchemaDefinition::String(StringSchema::email()),
)
.mark_required("nonexistent_field")
.build();
@ -1706,8 +1661,14 @@ async fn test_build_validation_required_field_not_in_properties() {
#[tokio::test]
async fn test_build_validation_required_field_exists() {
let result = ElicitationSchema::builder()
.property("email", PrimitiveSchema::String(StringSchema::email()))
.property("name", PrimitiveSchema::String(StringSchema::new()))
.property(
"email",
PrimitiveSchemaDefinition::String(StringSchema::email()),
)
.property(
"name",
PrimitiveSchemaDefinition::String(StringSchema::new()),
)
.mark_required("email")
.mark_required("name")
.build();
@ -1728,7 +1689,10 @@ async fn test_build_validation_required_field_exists() {
async fn test_build_unchecked_panics_on_invalid() {
// build_unchecked validates but panics instead of returning Result
let _schema = ElicitationSchema::builder()
.property("email", PrimitiveSchema::String(StringSchema::email()))
.property(
"email",
PrimitiveSchemaDefinition::String(StringSchema::email()),
)
.mark_required("nonexistent_field")
.build_unchecked();
}
@ -1776,25 +1740,25 @@ async fn test_typed_property_methods() {
assert_eq!(schema.properties.len(), 4);
// Verify types are correct
if let Some(PrimitiveSchema::String(_)) = schema.properties.get("name") {
if let Some(PrimitiveSchemaDefinition::String(_)) = schema.properties.get("name") {
// Expected
} else {
panic!("name should be StringSchema");
}
if let Some(PrimitiveSchema::Number(_)) = schema.properties.get("price") {
if let Some(PrimitiveSchemaDefinition::Number(_)) = schema.properties.get("price") {
// Expected
} else {
panic!("price should be NumberSchema");
}
if let Some(PrimitiveSchema::Integer(_)) = schema.properties.get("quantity") {
if let Some(PrimitiveSchemaDefinition::Integer(_)) = schema.properties.get("quantity") {
// Expected
} else {
panic!("quantity should be IntegerSchema");
}
if let Some(PrimitiveSchema::Boolean(_)) = schema.properties.get("in_stock") {
if let Some(PrimitiveSchemaDefinition::Boolean(_)) = schema.properties.get("in_stock") {
// Expected
} else {
panic!("in_stock should be BooleanSchema");
@ -1831,7 +1795,7 @@ async fn test_required_typed_property_methods() {
/// Test URL elicitation request parameter serialization/deserialization
#[tokio::test]
async fn test_url_elicitation_request_param_serialization() {
let request_param = CreateElicitationRequestParams::UrlElicitationParams {
let request_param = ElicitRequestParams::UrlElicitationParams {
meta: None,
message: "Please visit the following URL to complete verification".to_string(),
url: "https://example.com/verify".to_string(),
@ -1850,9 +1814,9 @@ async fn test_url_elicitation_request_param_serialization() {
assert_eq!(json, expected);
// Test deserialization
let deserialized: CreateElicitationRequestParams = serde_json::from_value(expected).unwrap();
let deserialized: ElicitRequestParams = serde_json::from_value(expected).unwrap();
match deserialized {
CreateElicitationRequestParams::UrlElicitationParams {
ElicitRequestParams::UrlElicitationParams {
message,
url,
elicitation_id,
@ -1876,14 +1840,12 @@ async fn test_url_elicitation_json_rpc_protocol() {
let request = JsonRpcRequest {
jsonrpc: JsonRpcVersion2_0,
id: RequestId::Number(1),
request: CreateElicitationRequest::new(
CreateElicitationRequestParams::UrlElicitationParams {
meta: None,
message: "Please authorize this action at the following URL".to_string(),
url: "https://auth.example.com/authorize/abc123".to_string(),
elicitation_id: "auth-request-456".to_string(),
},
),
request: ElicitRequest::new(ElicitRequestParams::UrlElicitationParams {
meta: None,
message: "Please authorize this action at the following URL".to_string(),
url: "https://auth.example.com/authorize/abc123".to_string(),
elicitation_id: "auth-request-456".to_string(),
}),
};
// Test serialization of complete request
@ -1903,11 +1865,10 @@ async fn test_url_elicitation_json_rpc_protocol() {
assert_eq!(json["params"]["elicitationId"], "auth-request-456");
// Test deserialization
let deserialized: JsonRpcRequest<CreateElicitationRequest> =
serde_json::from_value(json).unwrap();
let deserialized: JsonRpcRequest<ElicitRequest> = serde_json::from_value(json).unwrap();
assert_eq!(deserialized.id, RequestId::Number(1));
match &deserialized.request.params {
CreateElicitationRequestParams::UrlElicitationParams {
ElicitRequestParams::UrlElicitationParams {
message,
url,
elicitation_id,
@ -1921,12 +1882,10 @@ async fn test_url_elicitation_json_rpc_protocol() {
}
}
/// Test ElicitationCompletionNotification serialization/deserialization
/// Test ElicitationCompleteNotification serialization/deserialization
#[tokio::test]
async fn test_elicitation_completion_notification() {
let notification_params = ElicitationResponseNotificationParam {
elicitation_id: "elicit-789".to_string(),
};
let notification_params = ElicitationResponseNotificationParam::new("elicit-789");
// Test serialization
let json = serde_json::to_value(&notification_params).unwrap();
@ -1941,7 +1900,7 @@ async fn test_elicitation_completion_notification() {
assert_eq!(deserialized.elicitation_id, "elicit-789");
// Test complete notification structure
let notification = ElicitationCompletionNotification::new(notification_params);
let notification = ElicitationCompleteNotification::new(notification_params);
let json = serde_json::to_value(&notification).unwrap();
assert_eq!(json["method"], "notifications/elicitation/complete");
@ -1963,10 +1922,7 @@ async fn test_url_elicitation_capability() {
assert_eq!(deserialized, url_cap);
// Test ElicitationCapability with URL mode enabled
let elicitation_cap = ElicitationCapability {
form: None,
url: Some(UrlElicitationCapability::default()),
};
let elicitation_cap = ElicitationCapability::new().with_url(UrlElicitationCapability::new());
let json = serde_json::to_value(&elicitation_cap).unwrap();
assert_eq!(
@ -1977,12 +1933,9 @@ async fn test_url_elicitation_capability() {
);
// Test ElicitationCapability with both form and URL modes
let both_cap = ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: Some(UrlElicitationCapability::default()),
};
let both_cap = ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true))
.with_url(UrlElicitationCapability::new());
let json = serde_json::to_value(&both_cap).unwrap();
assert_eq!(
@ -1996,7 +1949,7 @@ async fn test_url_elicitation_capability() {
);
}
/// Test backward compatibility: CreateElicitationRequestParam without mode tag
/// Test backward compatibility: ElicitRequestParams without mode tag
#[tokio::test]
async fn test_elicitation_backward_compatibility_no_mode() {
// JSON without "mode" field should deserialize as FormElicitationParam
@ -2013,11 +1966,10 @@ async fn test_elicitation_backward_compatibility_no_mode() {
}
});
let deserialized: CreateElicitationRequestParams =
serde_json::from_value(json_without_mode).unwrap();
let deserialized: ElicitRequestParams = serde_json::from_value(json_without_mode).unwrap();
match deserialized {
CreateElicitationRequestParams::FormElicitationParams {
ElicitRequestParams::FormElicitationParams {
message,
requested_schema,
..
@ -2035,11 +1987,14 @@ async fn test_elicitation_backward_compatibility_no_mode() {
async fn test_elicitation_both_modes() {
// Form mode
let form_schema = ElicitationSchema::builder()
.required_property("email", PrimitiveSchema::String(StringSchema::email()))
.required_property(
"email",
PrimitiveSchemaDefinition::String(StringSchema::email()),
)
.build()
.unwrap();
let form_request = CreateElicitationRequestParams::FormElicitationParams {
let form_request = ElicitRequestParams::FormElicitationParams {
meta: None,
message: "Enter email".to_string(),
requested_schema: form_schema,
@ -2051,7 +2006,7 @@ async fn test_elicitation_both_modes() {
assert!(form_json.get("url").is_none());
// URL mode
let url_request = CreateElicitationRequestParams::UrlElicitationParams {
let url_request = ElicitRequestParams::UrlElicitationParams {
meta: None,
message: "Visit URL".to_string(),
url: "https://example.com".to_string(),
@ -2103,12 +2058,10 @@ async fn test_url_elicitation_required_error_code() {
async fn test_client_capabilities_elicitation_modes() {
// Test with form-only capability
let form_only_caps = ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(true),
}),
url: None,
})
.enable_elicitation_with(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(true)),
)
.build();
let json = serde_json::to_value(&form_only_caps).unwrap();
@ -2120,10 +2073,9 @@ async fn test_client_capabilities_elicitation_modes() {
// Test with URL-only capability
let url_only_caps = ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: None,
url: Some(UrlElicitationCapability::default()),
})
.enable_elicitation_with(
ElicitationCapability::new().with_url(UrlElicitationCapability::new()),
)
.build();
let json = serde_json::to_value(&url_only_caps).unwrap();
@ -2138,12 +2090,11 @@ async fn test_client_capabilities_elicitation_modes() {
// Test with both capabilities
let both_caps = ClientCapabilities::builder()
.enable_elicitation_with(ElicitationCapability {
form: Some(FormElicitationCapability {
schema_validation: Some(false),
}),
url: Some(UrlElicitationCapability::default()),
})
.enable_elicitation_with(
ElicitationCapability::new()
.with_form(FormElicitationCapability::new().with_schema_validation(false))
.with_url(UrlElicitationCapability::new()),
)
.build();
let json = serde_json::to_value(&both_caps).unwrap();
@ -2151,19 +2102,16 @@ async fn test_client_capabilities_elicitation_modes() {
assert!(json["elicitation"]["url"].is_object());
}
/// Test ElicitationCompletionNotification in ServerNotification enum
/// Test ElicitationCompleteNotification in ServerNotification enum
#[tokio::test]
async fn test_elicitation_completion_in_server_notification() {
let notification_param = ElicitationResponseNotificationParam {
elicitation_id: "notify-123".to_string(),
};
let notification_param = ElicitationResponseNotificationParam::new("notify-123");
let completion_notification =
ElicitationCompletionNotification::new(notification_param.clone());
let completion_notification = ElicitationCompleteNotification::new(notification_param.clone());
// Test that it's part of ServerNotification
let server_notification =
ServerNotification::ElicitationCompletionNotification(completion_notification);
ServerNotification::ElicitationCompleteNotification(completion_notification);
// Test serialization
let json = serde_json::to_value(&server_notification).unwrap();
@ -2173,10 +2121,10 @@ async fn test_elicitation_completion_in_server_notification() {
// Test deserialization
let deserialized: ServerNotification = serde_json::from_value(json).unwrap();
match deserialized {
ServerNotification::ElicitationCompletionNotification(notif) => {
ServerNotification::ElicitationCompleteNotification(notif) => {
assert_eq!(notif.params.elicitation_id, "notify-123");
}
_ => panic!("Expected ElicitationCompletionNotification variant"),
_ => panic!("Expected ElicitationCompleteNotification variant"),
}
}
@ -2184,11 +2132,7 @@ async fn test_elicitation_completion_in_server_notification() {
#[tokio::test]
async fn test_url_elicitation_action_workflow() {
// Test Accept action for URL elicitation (user visited URL and confirmed)
let accept_result = CreateElicitationResult {
action: ElicitationAction::Accept,
content: None, // URL elicitation doesn't return content, just confirmation
meta: None,
};
let accept_result = ElicitResult::new(ElicitationAction::Accept);
let json = serde_json::to_value(&accept_result).unwrap();
assert_eq!(json["action"], "accept");
@ -2196,21 +2140,13 @@ async fn test_url_elicitation_action_workflow() {
assert!(json.get("content").is_none() || json["content"].is_null());
// Test Decline action for URL elicitation
let decline_result = CreateElicitationResult {
action: ElicitationAction::Decline,
content: None,
meta: None,
};
let decline_result = ElicitResult::new(ElicitationAction::Decline);
let json = serde_json::to_value(&decline_result).unwrap();
assert_eq!(json["action"], "decline");
// Test Cancel action for URL elicitation
let cancel_result = CreateElicitationResult {
action: ElicitationAction::Cancel,
content: None,
meta: None,
};
let cancel_result = ElicitResult::new(ElicitationAction::Cancel);
let json = serde_json::to_value(&cancel_result).unwrap();
assert_eq!(json["action"], "cancel");

View file

@ -1,26 +1,23 @@
use rmcp::model::{AnnotateAble, Content, Meta, RawContent, ResourceContents};
use rmcp::model::{ContentBlock, EmbeddedResource, Meta, ResourceContents};
use serde_json::json;
#[test]
fn serialize_embedded_text_resource_with_meta() {
// Inner contents meta
let mut resource_content_meta = Meta::new();
resource_content_meta.insert("inner".to_string(), json!(2));
// Top-level embedded resource meta
let mut resource_meta = Meta::new();
resource_meta.insert("top".to_string(), json!(1));
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
meta: Some(resource_meta),
resource: ResourceContents::TextResourceContents {
let content = ContentBlock::Resource(
EmbeddedResource::new(ResourceContents::TextResourceContents {
uri: "str://example".to_string(),
mime_type: Some("text/plain".to_string()),
text: "hello".to_string(),
meta: Some(resource_content_meta),
},
})
.no_annotation();
})
.with_meta(resource_meta),
);
let v = serde_json::to_value(&content).unwrap();
@ -40,16 +37,14 @@ fn serialize_embedded_text_resource_with_meta() {
#[test]
fn serialize_embedded_text_resource_without_meta_omits_fields() {
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
meta: None,
resource: ResourceContents::TextResourceContents {
let content = ContentBlock::Resource(EmbeddedResource::new(
ResourceContents::TextResourceContents {
uri: "str://no-meta".to_string(),
mime_type: Some("text/plain".to_string()),
text: "hi".to_string(),
meta: None,
},
})
.no_annotation();
));
let v = serde_json::to_value(&content).unwrap();
@ -70,19 +65,17 @@ fn deserialize_embedded_text_resource_with_meta() {
}
});
let content: Content = serde_json::from_value(raw).unwrap();
let content: ContentBlock = serde_json::from_value(raw).unwrap();
let raw = match &content.raw {
RawContent::Resource(er) => er,
let er = match &content {
ContentBlock::Resource(er) => er,
_ => panic!("expected resource"),
};
// top-level _meta
let top = raw.meta.as_ref().expect("top-level meta missing");
let top = er.meta.as_ref().expect("top-level meta missing");
assert_eq!(top.get("x").unwrap(), &json!(true));
// inner contents _meta
match &raw.resource {
match &er.resource {
ResourceContents::TextResourceContents {
meta, uri, text, ..
} => {
@ -103,16 +96,15 @@ fn serialize_embedded_blob_resource_with_meta() {
let mut resource_meta = Meta::new();
resource_meta.insert("blob_top".to_string(), json!("t"));
let content: Content = RawContent::Resource(rmcp::model::RawEmbeddedResource {
meta: Some(resource_meta),
resource: ResourceContents::BlobResourceContents {
let content = ContentBlock::Resource(
EmbeddedResource::new(ResourceContents::BlobResourceContents {
uri: "str://blob".to_string(),
mime_type: Some("application/octet-stream".to_string()),
blob: "Zm9v".to_string(),
meta: Some(resource_content_meta),
},
})
.no_annotation();
})
.with_meta(resource_meta),
);
let v = serde_json::to_value(&content).unwrap();

View file

@ -148,7 +148,7 @@ async fn test_inflight_response_drain_on_eof() -> anyhow::Result<()> {
let text = result
.content
.first()
.and_then(|c| c.raw.as_text())
.and_then(|c| c.as_text())
.map(|t| t.text.as_str())
.expect("expected text content in tool result");
assert_eq!(text, "done after 200ms");

View file

@ -26,14 +26,16 @@ async fn test_logging_spec_compliance() -> anyhow::Result<()> {
// Test server can send messages before level is set
server
.peer()
.notify_logging_message(LoggingMessageNotificationParam {
level: LoggingLevel::Info,
data: serde_json::json!({
"message": "Server initiated message",
"timestamp": chrono::Utc::now().to_rfc3339(),
}),
logger: Some("test_server".to_string()),
})
.notify_logging_message(
LoggingMessageNotificationParam::new(
LoggingLevel::Info,
serde_json::json!({
"message": "Server initiated message",
"timestamp": chrono::Utc::now().to_rfc3339(),
}),
)
.with_logger("test_server"),
)
.await?;
server.waiting().await?;
@ -277,14 +279,9 @@ async fn test_logging_optional_fields() -> anyhow::Result<()> {
// Test message with and without optional logger field
for (level, has_logger) in [(LoggingLevel::Info, true), (LoggingLevel::Debug, false)] {
server
.peer()
.notify_logging_message(LoggingMessageNotificationParam {
level,
data: json!({"test": "data"}),
logger: has_logger.then(|| "test_logger".to_string()),
})
.await?;
let mut param = LoggingMessageNotificationParam::new(level, json!({"test": "data"}));
param.logger = has_logger.then(|| "test_logger".to_string());
server.peer().notify_logging_message(param).await?;
}
server.waiting().await?;

View file

@ -38,7 +38,7 @@ impl ServerHandler for Server {
let _enter = span.enter();
if let Err(e) = peer
.notify_resource_updated(ResourceUpdatedNotificationParam { uri: uri.clone() })
.notify_resource_updated(ResourceUpdatedNotificationParam::new(uri.clone()))
.await
{
panic!("Failed to send notification: {}", e);

View file

@ -71,12 +71,11 @@ impl MyServer {
))?;
for step in 0..10 {
let _ = client
.notify_progress(ProgressNotificationParam {
progress_token: progress_token.clone(),
progress: (step as f64),
total: Some(10.0),
message: Some("Some message".into()),
})
.notify_progress(
ProgressNotificationParam::new(progress_token.clone(), step as f64)
.with_total(10.0)
.with_message("Some message"),
)
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}

View file

@ -4,7 +4,7 @@
use rmcp::{
ServerHandler,
handler::server::wrapper::Parameters,
model::{GetPromptResult, Prompt, PromptMessage, PromptMessageRole},
model::{GetPromptResult, Prompt, PromptMessage, Role},
prompt,
};
use schemars::JsonSchema;
@ -40,26 +40,20 @@ struct ComplexArgs {
// Test basic prompt attribute generation
#[prompt]
async fn basic_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Basic response",
)]
vec![PromptMessage::new_text(Role::Assistant, "Basic response")]
}
// Test prompt with custom name
#[prompt(name = "custom_name")]
async fn named_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Named response",
)]
vec![PromptMessage::new_text(Role::Assistant, "Named response")]
}
// Test prompt with custom description
#[prompt(description = "This is a custom description")]
async fn described_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Described response",
)]
}
@ -68,7 +62,7 @@ async fn described_prompt(_server: &TestServer) -> Vec<PromptMessage> {
#[prompt(name = "full_custom", description = "Fully customized prompt")]
async fn fully_custom_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Fully custom response",
)]
}
@ -79,7 +73,7 @@ async fn fully_custom_prompt(_server: &TestServer) -> Vec<PromptMessage> {
#[prompt]
async fn doc_comment_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Doc comment response",
)]
}
@ -89,7 +83,7 @@ async fn doc_comment_prompt(_server: &TestServer) -> Vec<PromptMessage> {
#[prompt(description = "This overrides the doc comment")]
async fn override_doc_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Override response",
)]
}
@ -97,10 +91,7 @@ async fn override_doc_prompt(_server: &TestServer) -> Vec<PromptMessage> {
// Test prompt with arguments
#[prompt]
async fn args_prompt(_server: &TestServer, _args: Parameters<TestArgs>) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Args response",
)]
vec![PromptMessage::new_text(Role::Assistant, "Args response")]
}
// Test prompt with complex arguments
@ -110,7 +101,7 @@ async fn complex_args_prompt(
_args: Parameters<ComplexArgs>,
) -> GetPromptResult {
GetPromptResult::new(vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Complex response",
)])
.with_description("Complex args result")
@ -119,10 +110,7 @@ async fn complex_args_prompt(
// Test sync prompt
#[prompt]
fn sync_prompt(_server: &TestServer) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Sync response",
)]
vec![PromptMessage::new_text(Role::Assistant, "Sync response")]
}
#[test]
@ -275,10 +263,7 @@ impl<T: Send + Sync + 'static> ServerHandler for GenericServer<T> {}
async fn generic_prompt<T: Send + Sync + 'static>(
_server: &GenericServer<T>,
) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
"Generic response",
)]
vec![PromptMessage::new_text(Role::Assistant, "Generic response")]
}
#[test]

View file

@ -7,8 +7,8 @@ use rmcp::{
ClientHandler, RoleServer, ServerHandler, ServiceExt,
handler::server::{router::prompt::PromptRouter, wrapper::Parameters},
model::{
ClientInfo, GetPromptRequestParams, GetPromptResult, ListPromptsResult,
PaginatedRequestParams, PromptMessage, PromptMessageRole,
ClientInfo, ContentBlock, GetPromptRequestParams, GetPromptResult, ListPromptsResult,
PaginatedRequestParams, PromptMessage, Role,
},
prompt, prompt_handler, prompt_router,
service::RequestContext,
@ -53,14 +53,14 @@ impl Server {
pub async fn code_review(&self, params: Parameters<CodeReviewRequest>) -> Vec<PromptMessage> {
vec![
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
format!(
"Please review the {} code in: {}",
params.0.language, params.0.file_path
),
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"I'll review this code for best practices and potential issues.".to_string(),
),
]
@ -69,7 +69,7 @@ impl Server {
#[prompt]
async fn empty_param(&self) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"This is a prompt with no parameters.".to_string(),
)]
}
@ -110,11 +110,11 @@ impl<DS: DataService> GenericServer<DS> {
let context = self.data_service.get_context();
GetPromptResult::new(vec![
PromptMessage::new_text(
PromptMessageRole::User,
Role::User,
"I need help with the current context.".to_string(),
),
PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
format!(
"Based on the context '{}', here's how I can help...",
context
@ -141,8 +141,8 @@ async fn test_prompt_macros() {
}))
.await;
assert_eq!(result.len(), 2);
assert_eq!(result[0].role, PromptMessageRole::User);
assert_eq!(result[1].role, PromptMessageRole::Assistant);
assert_eq!(result[0].role, Role::User);
assert_eq!(result[1].role, Role::Assistant);
}
#[tokio::test]
@ -166,8 +166,8 @@ async fn test_prompt_macros_with_generics() {
assert!(result.description.is_some());
assert_eq!(result.messages.len(), 2);
match &result.messages[1].content {
rmcp::model::PromptMessageContent::Text { text } => {
assert!(text.contains("mock context data"));
ContentBlock::Text(text_content) => {
assert!(text_content.text.contains("mock context data"));
}
_ => panic!("Expected text content"),
}
@ -233,7 +233,7 @@ impl OptionalSchemaTester {
#[prompt(description = "A prompt to test optional schema generation")]
async fn test_optional(&self, _req: Parameters<OptionalFieldTestSchema>) -> Vec<PromptMessage> {
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Testing optional fields".to_string(),
)]
}
@ -249,11 +249,8 @@ impl OptionalSchemaTester {
None => "Received null count".to_string(),
};
GetPromptResult::new(vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
message,
)])
.with_description("Test result for optional i64")
GetPromptResult::new(vec![PromptMessage::new_text(Role::Assistant, message)])
.with_description("Test result for optional i64")
}
}
@ -338,7 +335,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> {
.await?;
let result_text = match &result.messages.first().unwrap().content {
rmcp::model::PromptMessageContent::Text { text } => text.as_str(),
ContentBlock::Text(text_content) => text_content.text.as_str(),
_ => panic!("Expected text content"),
};
@ -363,7 +360,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> {
.await?;
let some_result_text = match &some_result.messages.first().unwrap().content {
rmcp::model::PromptMessageContent::Text { text } => text.as_str(),
ContentBlock::Text(text_content) => text_content.text.as_str(),
_ => panic!("Expected text content"),
};

View file

@ -5,7 +5,7 @@ use futures::future::BoxFuture;
use rmcp::{
ServerHandler,
handler::server::wrapper::Parameters,
model::{GetPromptResult, PromptMessage, PromptMessageRole},
model::{GetPromptResult, PromptMessage, Role},
};
#[derive(Debug, Default)]
@ -35,7 +35,7 @@ impl<T> TestHandler<T> {
) -> Vec<PromptMessage> {
drop(fields);
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Async method response",
)]
}
@ -47,7 +47,7 @@ impl<T> TestHandler<T> {
) -> Vec<PromptMessage> {
drop(fields);
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Sync method response",
)]
}
@ -57,7 +57,7 @@ impl<T> TestHandler<T> {
async fn async_function(Parameters(Request { fields }): Parameters<Request>) -> Vec<PromptMessage> {
drop(fields);
vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Async function response",
)]
}
@ -66,7 +66,7 @@ async fn async_function(Parameters(Request { fields }): Parameters<Request>) ->
fn async_function2<T>(_callee: &TestHandler<T>) -> BoxFuture<'_, GetPromptResult> {
Box::pin(async move {
GetPromptResult::new(vec![PromptMessage::new_text(
PromptMessageRole::Assistant,
Role::Assistant,
"Async function 2 response",
)])
.with_description("Async function 2")

View file

@ -59,12 +59,11 @@ impl ProgressTimeoutServer {
for step in 0..4 {
tokio::time::sleep(Duration::from_millis(50)).await;
let _ = client
.notify_progress(ProgressNotificationParam {
progress_token: progress_token.clone(),
progress: step as f64,
total: Some(4.0),
message: Some("working".into()),
})
.notify_progress(
ProgressNotificationParam::new(progress_token.clone(), step as f64)
.with_total(4.0)
.with_message("working"),
)
.await;
}
@ -79,12 +78,14 @@ impl ProgressTimeoutServer {
for step in 0..4 {
tokio::time::sleep(Duration::from_millis(50)).await;
let _ = client
.notify_progress(ProgressNotificationParam {
progress_token: ProgressToken(NumberOrString::Number(999_999)),
progress: step as f64,
total: Some(4.0),
message: Some("unrelated".into()),
})
.notify_progress(
ProgressNotificationParam::new(
ProgressToken(NumberOrString::Number(999_999)),
step as f64,
)
.with_total(4.0)
.with_message("unrelated"),
)
.await;
}

View file

@ -1,14 +1,14 @@
use rmcp::model::{CallToolResult, Content, RawResource};
use rmcp::model::{CallToolResult, ContentBlock, Resource};
#[test]
fn test_resource_link_in_tool_result() {
// Test creating a tool result with resource links
let resource = RawResource::new("file:///test/file.txt", "test.txt");
let resource = Resource::new("file:///test/file.txt", "test.txt");
// Create a tool result with a resource link
let result = CallToolResult::success(vec![
Content::text("Found a file"),
Content::resource_link(resource),
ContentBlock::text("Found a file"),
ContentBlock::resource_link(resource),
]);
// Serialize to JSON to verify format
@ -42,12 +42,12 @@ fn test_resource_link_in_tool_result() {
#[test]
fn test_resource_link_with_full_metadata() {
let mut resource = RawResource::new("https://example.com/data.json", "API Data");
resource.description = Some("JSON data from external API".to_string());
resource.mime_type = Some("application/json".to_string());
resource.size = Some(1024);
let resource = Resource::new("https://example.com/data.json", "API Data")
.with_description("JSON data from external API")
.with_mime_type("application/json")
.with_size(1024);
let result = CallToolResult::success(vec![Content::resource_link(resource)]);
let result = CallToolResult::success(vec![ContentBlock::resource_link(resource)]);
let json = serde_json::to_string(&result).unwrap();
let deserialized: CallToolResult = serde_json::from_str(&json).unwrap();
@ -71,12 +71,12 @@ fn test_resource_link_with_full_metadata() {
#[test]
fn test_mixed_content_types() {
// Test that resource links can be mixed with other content types
let resource = RawResource::new("file:///doc.pdf", "Document");
let resource = Resource::new("file:///doc.pdf", "Document");
let result = CallToolResult::success(vec![
Content::text("Processing complete"),
Content::resource_link(resource),
Content::embedded_text("memo://result", "Analysis results here"),
ContentBlock::text("Processing complete"),
ContentBlock::resource_link(resource),
ContentBlock::embedded_text("memo://result", "Analysis results here"),
]);
assert_eq!(result.content.len(), 3);

View file

@ -1,27 +1,21 @@
/// Integration tests for resource_link support in both tools and prompts
use rmcp::model::{
AnnotateAble, CallToolResult, Content, PromptMessage, PromptMessageContent, PromptMessageRole,
RawResource, Resource,
};
use rmcp::model::{CallToolResult, ContentBlock, PromptMessage, Resource, Role};
#[test]
fn test_tool_and_prompt_resource_link_compatibility() {
// Create a resource that can be used in both tools and prompts
let resource = RawResource::new("file:///shared/data.json", "Shared Data");
let resource_annotated: Resource = resource.clone().no_annotation();
let resource = Resource::new("file:///shared/data.json", "Shared Data");
// Test 1: Tool returning a resource link
let tool_result = CallToolResult::success(vec![
Content::text("Found shared data"),
Content::resource_link(resource.clone()),
ContentBlock::text("Found shared data"),
ContentBlock::resource_link(resource.clone()),
]);
let tool_json = serde_json::to_string(&tool_result).unwrap();
assert!(tool_json.contains("\"type\":\"resource_link\""));
// Test 2: Prompt returning a resource link
let prompt_message =
PromptMessage::new_resource_link(PromptMessageRole::Assistant, resource_annotated.clone());
let prompt_message = PromptMessage::new_resource_link(Role::Assistant, resource.clone());
let prompt_json = serde_json::to_string(&prompt_message).unwrap();
assert!(prompt_json.contains("\"type\":\"resource_link\""));
@ -30,11 +24,9 @@ fn test_tool_and_prompt_resource_link_compatibility() {
let tool_content = &tool_result.content[1];
let prompt_content = &prompt_message.content;
// Extract just the resource link parts
let tool_resource_json = serde_json::to_value(tool_content).unwrap();
let prompt_resource_json = serde_json::to_value(prompt_content).unwrap();
// Both should have the same structure
assert_eq!(
tool_resource_json.get("type").unwrap(),
prompt_resource_json.get("type").unwrap()
@ -51,16 +43,13 @@ fn test_tool_and_prompt_resource_link_compatibility() {
#[test]
fn test_resource_link_roundtrip() {
// Test that resource links can be serialized and deserialized correctly
// in both tool results and prompt messages
let mut resource = RawResource::new("https://api.example.com/resource", "API Resource");
resource.description = Some("External API resource".to_string());
resource.mime_type = Some("application/json".to_string());
resource.size = Some(2048);
let resource = Resource::new("https://api.example.com/resource", "API Resource")
.with_description("External API resource")
.with_mime_type("application/json")
.with_size(2048);
// Test with tool result
let tool_result = CallToolResult::success(vec![Content::resource_link(resource.clone())]);
let tool_result = CallToolResult::success(vec![ContentBlock::resource_link(resource.clone())]);
let tool_json = serde_json::to_string(&tool_result).unwrap();
let tool_deserialized: CallToolResult = serde_json::from_str(&tool_json).unwrap();
@ -82,15 +71,12 @@ fn test_resource_link_roundtrip() {
}
// Test with prompt message
let prompt_message = PromptMessage::new(
PromptMessageRole::User,
PromptMessageContent::resource_link(resource.no_annotation()),
);
let prompt_message = PromptMessage::new(Role::User, ContentBlock::resource_link(resource));
let prompt_json = serde_json::to_string(&prompt_message).unwrap();
let prompt_deserialized: PromptMessage = serde_json::from_str(&prompt_json).unwrap();
if let PromptMessageContent::ResourceLink { link } = prompt_deserialized.content {
if let ContentBlock::ResourceLink(link) = &prompt_deserialized.content {
assert_eq!(link.uri, "https://api.example.com/resource");
assert_eq!(link.name, "API Resource");
assert_eq!(link.description, Some("External API resource".to_string()));
@ -103,18 +89,15 @@ fn test_resource_link_roundtrip() {
#[test]
fn test_mixed_content_in_prompts_and_tools() {
// Test that resource links can be mixed with other content types
// in both prompts and tools
let resource1 = RawResource::new("file:///doc1.md", "Document 1");
let resource2 = RawResource::new("file:///doc2.md", "Document 2");
let resource1 = Resource::new("file:///doc1.md", "Document 1");
let resource2 = Resource::new("file:///doc2.md", "Document 2");
// Tool with mixed content
let tool_result = CallToolResult::success(vec![
Content::text("Processing complete. Found documents:"),
Content::resource_link(resource1.clone()),
Content::resource_link(resource2.clone()),
Content::embedded_text("summary://result", "Both documents processed successfully"),
ContentBlock::text("Processing complete. Found documents:"),
ContentBlock::resource_link(resource1),
ContentBlock::resource_link(resource2),
ContentBlock::embedded_text("summary://result", "Both documents processed successfully"),
]);
assert_eq!(tool_result.content.len(), 4);

Some files were not shown because too many files have changed in this diff Show more