diff --git a/rust/crates/sift_mcp/src/tool/channels/mod.rs b/rust/crates/sift_mcp/src/tool/channels/mod.rs index b7cf2962b..985b4cc72 100644 --- a/rust/crates/sift_mcp/src/tool/channels/mod.rs +++ b/rust/crates/sift_mcp/src/tool/channels/mod.rs @@ -57,6 +57,9 @@ impl SiftMcpServer { Guidance: - Always scope with `asset_id == \"...\"` when the asset is known — channel namespaces are per-asset and unscoped queries return cross-asset results. + - Unscoped name searches scan every channel in the org and can time out in large orgs. Resolve + candidate assets with `list_assets` first, then search each with `asset_id == \"...\" && name.matches(...)`. + A timeout means the filter was too broad, not that the parameters were wrong; narrow it before retrying. - To enumerate channels recorded by a specific run, filter on `run_id` rather than joining client-side. ", annotations(title = "channels/list_channels", read_only_hint = true) diff --git a/rust/crates/sift_mcp/src/tool/channels/test.rs b/rust/crates/sift_mcp/src/tool/channels/test.rs index b9bcc7550..f748ed63c 100644 --- a/rust/crates/sift_mcp/src/tool/channels/test.rs +++ b/rust/crates/sift_mcp/src/tool/channels/test.rs @@ -248,6 +248,35 @@ async fn list_channels_reports_a_field_that_matched_nothing() { assert_eq!(body["unmatched_fields"], serde_json::json!(["nmae"])); } +#[test] +fn tool_description_warns_that_unscoped_name_searches_can_time_out() { + let tools = SiftMcpServer::channels_router().list_all(); + let tool = tools + .iter() + .find(|tool| tool.name == "list_channels") + .expect("list_channels is routed"); + let description = tool + .description + .as_deref() + .expect("list_channels has a description"); + + assert!( + description + .contains("Unscoped name searches scan every channel in the org and can time out"), + "the description must warn that unscoped name searches can time out" + ); + assert!( + description.contains("candidate assets with `list_assets` first"), + "the description must tell the agent to resolve assets before searching" + ); + assert!( + description.contains( + "A timeout means the filter was too broad, not that the parameters were wrong" + ), + "the description must say a timeout calls for a narrower filter" + ); +} + #[tokio::test] async fn list_channels_projects_asset_id_and_unit_id_but_not_asset_name() { let mut channel_mock = MockChannelServiceImpl::new();