mcp: handle list changed notifications

This commit is contained in:
2026-06-20 19:24:59 +09:00
parent d31b89072d
commit e33dee192c
4 changed files with 459 additions and 9 deletions
+31
View File
@@ -16,6 +16,7 @@ fn main() {
"tools-call-forbidden" => tools_call_forbidden(),
"fail-init" => fail_init(),
"sampling" => sampling_request(),
"list-changed-all" => list_changed_all(),
"shutdown-hang" => shutdown_hang(),
other => panic!("unknown mock mode: {other}"),
}
@@ -223,6 +224,36 @@ fn sampling_request() {
assert_eq!(response["error"]["code"], -32601);
}
fn list_changed_all() {
let init = read_json();
write_json(json!({
"jsonrpc": "2.0",
"id": init["id"],
"result": initialize_result(),
}));
let initialized = read_json();
assert_eq!(initialized["method"], "notifications/initialized");
for method in [
"notifications/tools/list_changed",
"notifications/resources/list_changed",
"notifications/prompts/list_changed",
] {
write_json(json!({
"jsonrpc": "2.0",
"method": method,
"params": {
"malicious_instruction": "INJECT_ME_FROM_LIST_CHANGED_PARAMS"
}
}));
}
let shutdown = read_json();
assert_eq!(shutdown["method"], "shutdown");
write_json(json!({"jsonrpc":"2.0", "id": shutdown["id"], "result": {}}));
let notification = read_json();
assert_eq!(notification["method"], "exit");
}
fn shutdown_hang() {
let init = read_json();
write_json(json!({
+32 -2
View File
@@ -1,8 +1,8 @@
use std::time::Duration;
use mcp::stdio::{
CallToolRequest, McpErrorKind, McpPhase, McpStdioClient, McpStdioLimits, McpStdioServerSpec,
McpToolListLimits,
CallToolRequest, McpErrorKind, McpListChangedKind, McpPhase, McpStdioClient, McpStdioLimits,
McpStdioServerSpec, McpToolListLimits,
};
fn mock_server(mode: &str) -> McpStdioServerSpec {
@@ -239,6 +239,36 @@ async fn shutdown_terminates_or_kills_uncooperative_server() {
assert!(shutdown.terminated || shutdown.killed);
}
#[tokio::test]
async fn list_changed_notifications_record_bounded_kind_only_state() {
let mut client = McpStdioClient::connect(mock_server("list-changed-all"), tight_limits())
.await
.expect("initialize succeeds");
tokio::time::sleep(Duration::from_millis(50)).await;
let snapshot = client.snapshot_list_changes().await;
assert_eq!(snapshot.server_name, "mock");
assert!(snapshot.contains(McpListChangedKind::Tools));
assert!(snapshot.contains(McpListChangedKind::Resources));
assert!(snapshot.contains(McpListChangedKind::Prompts));
let methods: Vec<&'static str> = snapshot
.kinds()
.map(McpListChangedKind::notification_method)
.collect();
assert_eq!(
methods,
vec![
"notifications/tools/list_changed",
"notifications/resources/list_changed",
"notifications/prompts/list_changed"
]
);
client.clear_list_changes().await;
assert!(client.snapshot_list_changes().await.is_empty());
client.shutdown().await.expect("shutdown succeeds");
}
#[tokio::test]
async fn sampling_requests_fail_closed_and_are_not_advertised() {
let mut client = McpStdioClient::connect(mock_server("sampling"), tight_limits())