Skip to main content

prism_mcp_rs/protocol/
subscriptions.rs

1//! MCP 2026-07-28 notification subscription types.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8/// Notifications selected by a `subscriptions/listen` request.
9///
10/// Every field is opt-in. The `taskIds` field belongs to the official Tasks
11/// extension and is accepted only when that extension was declared by the
12/// request's client capabilities.
13#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
14pub struct SubscriptionFilter {
15    #[serde(rename = "toolsListChanged", skip_serializing_if = "Option::is_none")]
16    pub tools_list_changed: Option<bool>,
17    #[serde(rename = "promptsListChanged", skip_serializing_if = "Option::is_none")]
18    pub prompts_list_changed: Option<bool>,
19    #[serde(
20        rename = "resourcesListChanged",
21        skip_serializing_if = "Option::is_none"
22    )]
23    pub resources_list_changed: Option<bool>,
24    #[serde(
25        rename = "resourceSubscriptions",
26        default,
27        skip_serializing_if = "Vec::is_empty"
28    )]
29    pub resource_subscriptions: Vec<String>,
30    #[serde(rename = "taskIds", default, skip_serializing_if = "Vec::is_empty")]
31    pub task_ids: Vec<String>,
32}
33
34impl SubscriptionFilter {
35    /// Return true when this filter permits a notification.
36    pub fn matches(&self, method: &str, params: Option<&Value>) -> bool {
37        match method {
38            "notifications/tools/list_changed" => self.tools_list_changed == Some(true),
39            "notifications/prompts/list_changed" => self.prompts_list_changed == Some(true),
40            "notifications/resources/list_changed" => self.resources_list_changed == Some(true),
41            "notifications/resources/updated" => params
42                .and_then(|value| value.get("uri"))
43                .and_then(Value::as_str)
44                .is_some_and(|uri| self.resource_subscriptions.iter().any(|item| item == uri)),
45            "notifications/tasks" => params
46                .and_then(|value| value.get("taskId"))
47                .and_then(Value::as_str)
48                .is_some_and(|task_id| self.task_ids.iter().any(|item| item == task_id)),
49            _ => false,
50        }
51    }
52
53    /// Whether the Tasks extension is needed to honor this filter.
54    pub fn requests_tasks(&self) -> bool {
55        !self.task_ids.is_empty()
56    }
57}
58
59/// Parameters for `subscriptions/listen`.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct SubscriptionsListenParams {
62    pub notifications: SubscriptionFilter,
63    #[serde(rename = "_meta", default, skip_serializing_if = "HashMap::is_empty")]
64    pub meta: HashMap<String, Value>,
65}
66
67/// Parameters for the first notification on a subscription stream.
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct SubscriptionsAcknowledgedParams {
70    pub notifications: SubscriptionFilter,
71    #[serde(rename = "_meta")]
72    pub meta: HashMap<String, Value>,
73}
74
75/// Result sent only when a subscription is closed gracefully.
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct SubscriptionsListenResult {
78    #[serde(rename = "resultType")]
79    pub result_type: String,
80    #[serde(rename = "_meta")]
81    pub meta: HashMap<String, Value>,
82}
83
84/// Reserved metadata key that correlates stream messages with the listen request.
85pub const SUBSCRIPTION_ID_META_KEY: &str = "io.modelcontextprotocol/subscriptionId";
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn filters_are_strictly_opt_in() {
93        let filter = SubscriptionFilter {
94            tools_list_changed: Some(true),
95            resource_subscriptions: vec!["file:///a".to_string()],
96            task_ids: vec!["task-a".to_string()],
97            ..Default::default()
98        };
99        assert!(filter.matches("notifications/tools/list_changed", None));
100        assert!(!filter.matches("notifications/prompts/list_changed", None));
101        assert!(filter.matches(
102            "notifications/resources/updated",
103            Some(&serde_json::json!({"uri":"file:///a"}))
104        ));
105        assert!(!filter.matches(
106            "notifications/tasks",
107            Some(&serde_json::json!({"taskId":"task-b"}))
108        ));
109    }
110}