use rmcp::model::{ ClientJsonRpcMessage, ClientRequest, GetMeta, Implementation, JsonRpcNotification, JsonRpcRequest, NotificationMetaObject, RequestId, RequestMetaObject, ServerJsonRpcMessage, ServerNotification, SubscriptionFilter, SubscriptionsAcknowledgedNotification, SubscriptionsAcknowledgedNotificationParams, SubscriptionsListenRequest, SubscriptionsListenRequestParams, SubscriptionsListenResult, SubscriptionsListenResultMeta, }; use serde_json::json; #[test] fn subscription_filter_serializes_only_opted_in_notifications() { let filter = SubscriptionFilter::builder() .tools_list_changed() .resource_subscription("file:///one") .resource_subscription("file:///two") .build(); assert_eq!( serde_json::to_value(filter).expect("serialize filter"), json!({ "toolsListChanged": true, "resourceSubscriptions": ["file:///one", "file:///two"], }) ); } #[test] fn subscription_filter_subset_is_order_independent_and_ignores_false_flags() { let requested = SubscriptionFilter::builder() .tools_list_changed() .resource_subscriptions(["file:///one", "file:///two"]) .build(); let mut accepted = SubscriptionFilter::builder() .resource_subscriptions(["file:///two", "file:///one"]) .build(); accepted.tools_list_changed = Some(false); assert!(accepted.is_subset_of(&requested)); } #[test] fn subscription_filter_omits_empty_resource_intersection() { let requested = SubscriptionFilter::builder() .resource_subscription("file:///requested") .build(); let accepted = SubscriptionFilter::builder() .resource_subscription("file:///different") .build(); assert_eq!( serde_json::to_value(requested.intersection(&accepted)).expect("serialize intersection"), json!({}) ); } #[test] fn listen_request_round_trips_required_fields_and_arbitrary_metadata() { let mut request = SubscriptionsListenRequest::new(SubscriptionsListenRequestParams::new( SubscriptionFilter::builder().prompts_list_changed().build(), )); let mut meta = RequestMetaObject::new(); meta.insert("com.example/request".into(), json!("value")); request.extensions.insert(meta); let message = ClientJsonRpcMessage::request( ClientRequest::SubscriptionsListenRequest(request), RequestId::String("subscription-1".into()), ); let value = serde_json::to_value(&message).expect("serialize listen request"); assert_eq!( value, json!({ "jsonrpc": "2.0", "id": "subscription-1", "method": "subscriptions/listen", "params": { "_meta": { "com.example/request": "value", }, "notifications": { "promptsListChanged": true, }, }, }) ); let round_trip: ClientJsonRpcMessage = serde_json::from_value(value).expect("deserialize listen request"); let ClientJsonRpcMessage::Request(JsonRpcRequest { request, .. }) = round_trip else { panic!("expected request"); }; let ClientRequest::SubscriptionsListenRequest(request) = request else { panic!("expected subscriptions/listen request"); }; assert_eq!( request .extensions .get::() .and_then(|meta| meta.get("com.example/request")), Some(&json!("value")) ); } #[test] fn acknowledged_notification_round_trips_numeric_subscription_id_and_metadata() { let mut notification = SubscriptionsAcknowledgedNotification::new( SubscriptionsAcknowledgedNotificationParams::new( SubscriptionFilter::builder() .resources_list_changed() .build(), ), ); let mut meta = NotificationMetaObject::new(); meta.set_subscription_id(RequestId::Number(7)); meta.insert("com.example/notification".into(), json!(true)); notification.extensions.insert(meta); let message = ServerJsonRpcMessage::notification( ServerNotification::SubscriptionsAcknowledgedNotification(notification), ); let value = serde_json::to_value(&message).expect("serialize acknowledgment"); assert_eq!( value, json!({ "jsonrpc": "2.0", "method": "notifications/subscriptions/acknowledged", "params": { "_meta": { "io.modelcontextprotocol/subscriptionId": 7, "com.example/notification": true, }, "notifications": { "resourcesListChanged": true, }, }, }) ); let round_trip: ServerJsonRpcMessage = serde_json::from_value(value).expect("deserialize acknowledgment"); let ServerJsonRpcMessage::Notification(JsonRpcNotification { notification, .. }) = round_trip else { panic!("expected notification"); }; assert_eq!( notification.get_meta().subscription_id(), Some(RequestId::Number(7)) ); assert_eq!( notification.get_meta().get("com.example/notification"), Some(&json!(true)) ); } #[test] fn listen_result_requires_matching_string_subscription_id_and_preserves_metadata() { let mut meta = SubscriptionsListenResultMeta::new(RequestId::String("subscription-2".into())); meta.set_server_info(Implementation::new("test-server", "1.0.0")); meta.insert("com.example/result".into(), json!({ "reason": "shutdown" })); let result = SubscriptionsListenResult::new(meta); let value = serde_json::to_value(&result).expect("serialize listen result"); assert_eq!( value, json!({ "resultType": "complete", "_meta": { "io.modelcontextprotocol/subscriptionId": "subscription-2", "io.modelcontextprotocol/serverInfo": { "name": "test-server", "version": "1.0.0", }, "com.example/result": { "reason": "shutdown", }, }, }) ); let round_trip: SubscriptionsListenResult = serde_json::from_value(value).expect("deserialize listen result"); assert_eq!( round_trip.meta.subscription_id(), Some(RequestId::String("subscription-2".into())) ); assert_eq!( round_trip.meta.server_info(), Some(Implementation::new("test-server", "1.0.0")) ); assert_eq!( round_trip.meta.get("com.example/result"), Some(&json!({ "reason": "shutdown" })) ); } #[test] fn listen_result_meta_returns_none_after_required_id_is_removed() { let mut meta = SubscriptionsListenResultMeta::new(RequestId::Number(1)); meta.remove("io.modelcontextprotocol/subscriptionId"); assert_eq!(meta.subscription_id(), None); } #[cfg(feature = "schemars")] #[test] fn subscription_schemas_mark_only_draft_required_fields_as_required() { let request_schema = serde_json::to_value(schemars::schema_for!(SubscriptionsListenRequestParams)) .expect("request schema"); let filter_schema = serde_json::to_value(schemars::schema_for!(SubscriptionFilter)).expect("filter schema"); let acknowledgment_schema = serde_json::to_value(schemars::schema_for!( SubscriptionsAcknowledgedNotificationParams )) .expect("acknowledgment schema"); let result_schema = serde_json::to_value(schemars::schema_for!(SubscriptionsListenResult)) .expect("result schema"); assert_eq!( request_schema["required"], json!(["_meta", "notifications"]) ); assert_eq!( request_schema["properties"]["_meta"]["required"], json!([ "io.modelcontextprotocol/protocolVersion", "io.modelcontextprotocol/clientCapabilities" ]) ); assert!(filter_schema.get("required").is_none()); assert_eq!( filter_schema["properties"]["toolsListChanged"]["type"], "boolean" ); assert_eq!( filter_schema["properties"]["resourceSubscriptions"]["type"], "array" ); assert_eq!(acknowledgment_schema["required"], json!(["notifications"])); assert_eq!( acknowledgment_schema["properties"]["_meta"]["$ref"], "#/$defs/NotificationMetaObject" ); let result_meta_schema = &result_schema["$defs"]["SubscriptionsListenResultMeta"]; assert_eq!( result_meta_schema["properties"]["io.modelcontextprotocol/serverInfo"]["allOf"][0]["$ref"], "#/$defs/Implementation" ); assert_eq!( result_meta_schema["required"], json!(["io.modelcontextprotocol/subscriptionId"]) ); assert_eq!(result_schema["required"], json!(["resultType", "_meta"])); }