feat: add support for custom server notifications (#580)

https://github.com/modelcontextprotocol/rust-sdk/pull/556 introduced support for
custom client notifications, so this PR makes the complementary change, adding
support for custom server notifications.

MCP clients, particularly ones that offer "experimental" capabilities,
may wish to handle custom server notifications that are not part of the
standard MCP specification. This change introduces a new
`CustomServerNotification` type that allows a client to process
such custom notifications.

- introduces `CustomServerNotification` to carry arbitrary methods/params while
  still preserving meta/extensions; wires it into the `ServerNotification` union
  and `serde` so `params` can be decoded with `params_as`
- allows client handlers to receive custom notifications via a new
  `on_custom_notification` hook
- adds integration coverage that sends a custom server notification end-to-end
  and asserts the client sees the method and payload

Test:

```shell
cargo test -p rmcp --features client test_custom_server_notification_reaches_client
```
This commit is contained in:
Michael Bolin 2025-12-16 09:48:03 -08:00 • committed by GitHub
commit 2e3cc4a973
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -627,13 +627,13 @@ const_string!(CancelledNotificationMethod = "notifications/cancelled");
pub type CancelledNotification =
Notification<CancelledNotificationMethod, CancelledNotificationParam>;
/// A catch-all notification the client can use to send custom messages to a server.
/// A catch-all notification either side can use to send custom messages to its peer.
///
/// This preserves the raw `method` name and `params` payload so handlers can
/// deserialize them into domain-specific types.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CustomClientNotification {
pub struct CustomNotification {
pub method: String,
pub params: Option<Value>,
/// extensions will carry anything possible in the context, including [`Meta`]
@ -643,7 +643,7 @@ pub struct CustomClientNotification {
pub extensions: Extensions,
}
impl CustomClientNotification {
impl CustomNotification {
pub fn new(method: impl Into<String>, params: Option<Value>) -> Self {
Self {
method: method.into(),
@ -1786,7 +1786,7 @@ ts_union!(
| ProgressNotification
| InitializedNotification
| RootsListChangedNotification
| CustomClientNotification;
| CustomNotification;
);
ts_union!(
@ -1817,7 +1817,8 @@ ts_union!(
| ResourceUpdatedNotification
| ResourceListChangedNotification
| ToolListChangedNotification
| PromptListChangedNotification;
| PromptListChangedNotification
| CustomNotification;
);
ts_union!(
@ -1907,7 +1908,7 @@ mod tests {
serde_json::from_value(raw.clone()).expect("invalid notification");
match &message {
ClientJsonRpcMessage::Notification(JsonRpcNotification {
notification: ClientNotification::CustomClientNotification(notification),
notification: ClientNotification::CustomNotification(notification),
..
}) => {
assert_eq!(notification.method, "notifications/custom");
@ -1927,6 +1928,38 @@ mod tests {
assert_eq!(json, raw);
}
#[test]
fn test_custom_server_notification_roundtrip() {
let raw = json!( {
"jsonrpc": JsonRpcVersion2_0,
"method": "notifications/custom-server",
"params": {"hello": "world"},
});
let message: ServerJsonRpcMessage =
serde_json::from_value(raw.clone()).expect("invalid notification");
match &message {
ServerJsonRpcMessage::Notification(JsonRpcNotification {
notification: ServerNotification::CustomNotification(notification),
..
}) => {
assert_eq!(notification.method, "notifications/custom-server");
assert_eq!(
notification
.params
.as_ref()
.and_then(|p| p.get("hello"))
.expect("hello present"),
"world"
);
}
_ => panic!("Expected custom server notification"),
}
let json = serde_json::to_value(message).expect("valid json");
assert_eq!(json, raw);
}
#[test]
fn test_request_conversion() {
let raw = json!( {