* fix: stamp server info on graceful subscription results * fix: expose subscription server info metadata --------- Co-authored-by: mrcs64 <9069178+mrcs64@users.noreply.github.com>
254 lines
8.7 KiB
Rust
254 lines
8.7 KiB
Rust
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::<RequestMetaObject>()
|
|
.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"]));
|
|
}
|