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:
parent
42a1069833
commit
77932141e3
27 changed files with 4321 additions and 3886 deletions
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(_)));
|
||||
|
|
|
|||
|
|
@ -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(¬ification_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(¬ification).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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue