Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions rust/crates/sift_mcp/src/tool/channels/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
33 changes: 33 additions & 0 deletions rust/crates/sift_mcp/src/tool/channels/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]));
}
16 changes: 10 additions & 6 deletions rust/crates/sift_mcp/src/tool/runs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<host>/run/<run_id>`). `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 (`<host>/run/<run_id>`). `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`.
Expand All @@ -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,
Expand Down
36 changes: 35 additions & 1 deletion rust/crates/sift_mcp/src/tool/runs/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"])
);
}
Loading