From 13c1b9ea2267b51be5f713b34f205381eadfe342 Mon Sep 17 00:00:00 2001 From: Alexa Agnew Date: Tue, 13 Oct 2026 10:41:54 -0700 Subject: [PATCH 1/2] name asset_ids in list_runs output and drop asset_name from list_channels Co-Authored-By: Claude Opus 5.5 --- rust/crates/sift_mcp/src/tool/channels/mod.rs | 5 +-- rust/crates/sift_mcp/src/tool/runs/mod.rs | 16 +++++---- rust/crates/sift_mcp/src/tool/runs/test.rs | 36 ++++++++++++++++++- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/rust/crates/sift_mcp/src/tool/channels/mod.rs b/rust/crates/sift_mcp/src/tool/channels/mod.rs index f23c608961..b7cf2962b3 100644 --- a/rust/crates/sift_mcp/src/tool/channels/mod.rs +++ b/rust/crates/sift_mcp/src/tool/channels/mod.rs @@ -18,8 +18,9 @@ impl SiftMcpServer { Output: - `{ \"channels\": [Channel, ...] }`. Each item is the full Sift `Channel` shape including - `channel_id`, `name`, `description`, `asset_id`/`asset_name`, data type, units, enum/bit-field - configuration, and timestamps. + `channel_id`, `name`, `description`, `asset_id`, data type, `unit_id`, enum/bit-field + configuration, and timestamps. Channels carry no asset name: to show it, call `list_assets` + with the `asset_id`. - `count`: how many items THIS response carries — read it instead of counting the array yourself. It is the size of the page you got back, not how many items match `filter`. diff --git a/rust/crates/sift_mcp/src/tool/runs/mod.rs b/rust/crates/sift_mcp/src/tool/runs/mod.rs index b1f230ecfc..6779ba386a 100644 --- a/rust/crates/sift_mcp/src/tool/runs/mod.rs +++ b/rust/crates/sift_mcp/src/tool/runs/mod.rs @@ -39,10 +39,12 @@ impl SiftMcpServer { Output: - `{ \"runs\": [Run, ...] }`. Each item is the full Sift `Run` shape including `run_id`, `name`, - `asset_id`/`asset_name`, `client_key`, `start_time`, `stop_time`, duration, annotation state, tags, - and metadata, plus an added `url` field with the run's Sift web link (`/run/`). `url` - is omitted when the host can't be derived. Present each run to the user as a Markdown link with - its name as the text and `url` as the target. + `description`, `asset_ids`, `client_key`, `start_time`, `stop_time`, `duration`, `is_pinned`, + `is_adhoc`, tags, metadata, timestamps, and archive state, plus an added `url` field with the + run's Sift web link (`/run/`). `url` is omitted when the host can't be derived. + Present each run to the user as a Markdown link with its name as the text and `url` as the target. + - `asset_ids` is an array of asset IDs. Runs carry no asset names: to show them, call + `list_assets` with `asset_id in [\"...\", ...]` built from the collected `asset_ids`. - `count`: how many items THIS response carries — read it instead of counting the array yourself. It is the size of the page you got back, not how many items match `filter`. @@ -69,8 +71,10 @@ impl SiftMcpServer { and you still need more. Values are clamped to `1..=200`; omitting it defaults to 50. - `fields`: optional array of field names to keep on each item, e.g. `[\"name\"]`. Omit it for the full object. Names match case-insensitively - and ignore underscores and hyphens, so `asset_id`, `assetId` and - `asset-id` all work. Any name that matched nothing on any returned item + and ignore underscores and hyphens, so `asset_ids`, `assetIds` and + `asset-ids` all work. Name output fields, not filter fields: `asset_id`, + `asset_name`, and `annotation_state` filter runs but are not on the item. + Any name that matched nothing on any returned item is listed in `unmatched_fields`; an empty page reports none, since it says nothing about whether a name was spelled right. Reach for this whenever you need only a few fields: full objects are wide, diff --git a/rust/crates/sift_mcp/src/tool/runs/test.rs b/rust/crates/sift_mcp/src/tool/runs/test.rs index 5fa0ccaf6a..cc33564d86 100644 --- a/rust/crates/sift_mcp/src/tool/runs/test.rs +++ b/rust/crates/sift_mcp/src/tool/runs/test.rs @@ -9,7 +9,9 @@ use tonic::{Response, Status, transport::Server}; use super::UpdateRunParams; use crate::{ server::SiftMcpServer, - tool::common::test_support::{list_params, list_params_with_fields, structured_field}, + tool::common::test_support::{ + list_params, list_params_with_fields, structured, structured_field, + }, }; fn update_run_params(run_id: &str) -> UpdateRunParams { @@ -282,3 +284,35 @@ async fn list_runs_can_project_the_injected_url() { "runId should be projected out" ); } + +#[tokio::test] +async fn list_runs_projects_asset_ids_but_not_filter_only_asset_fields() { + let mut run_mock = MockRunServiceImpl::new(); + run_mock.expect_list_runs().returning(|_| { + Ok(Response::new(ListRunsResponse { + runs: vec![Run { + run_id: "r1".into(), + asset_ids: vec!["a1".into(), "a2".into()], + ..Default::default() + }], + next_page_token: String::new(), + })) + }); + + let (server, _h) = server_with_mock(run_mock).await; + + let resp = server + .list_runs(list_params_with_fields( + "", + &["asset_ids", "asset_id", "asset_name"], + )) + .await + .expect("list_runs failed"); + + let body = structured(resp); + assert_eq!(body["runs"][0]["assetIds"], serde_json::json!(["a1", "a2"])); + assert_eq!( + body["unmatched_fields"], + serde_json::json!(["asset_id", "asset_name"]) + ); +} From e988e1476e0caf3a1c3900f7391321e97bcc9491 Mon Sep 17 00:00:00 2001 From: Alexa Agnew Date: Tue, 13 Oct 2026 12:43:58 -0700 Subject: [PATCH 2/2] test list_channels asset_id and unit_id projection Co-Authored-By: Claude Opus 5.5 --- .../crates/sift_mcp/src/tool/channels/test.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/rust/crates/sift_mcp/src/tool/channels/test.rs b/rust/crates/sift_mcp/src/tool/channels/test.rs index 4cfbb7c718..b9bcc75508 100644 --- a/rust/crates/sift_mcp/src/tool/channels/test.rs +++ b/rust/crates/sift_mcp/src/tool/channels/test.rs @@ -247,3 +247,36 @@ async fn list_channels_reports_a_field_that_matched_nothing() { ); assert_eq!(body["unmatched_fields"], serde_json::json!(["nmae"])); } + +#[tokio::test] +async fn list_channels_projects_asset_id_and_unit_id_but_not_asset_name() { + let mut channel_mock = MockChannelServiceImpl::new(); + channel_mock.expect_list_channels().returning(|_| { + Ok(Response::new(ListChannelsResponse { + channels: vec![Channel { + channel_id: "c1".into(), + asset_id: "a1".into(), + unit_id: "u1".into(), + ..Default::default() + }], + next_page_token: String::new(), + })) + }); + + let (server, _h) = server_with_mock(channel_mock).await; + + let resp = server + .list_channels(list_params_with_fields( + "", + &["asset_id", "unit_id", "asset_name"], + )) + .await + .expect("list_channels failed"); + + let body = structured(resp); + assert_eq!( + body["channels"], + serde_json::json!([{ "assetId": "a1", "unitId": "u1" }]) + ); + assert_eq!(body["unmatched_fields"], serde_json::json!(["asset_name"])); +}