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
4 changes: 4 additions & 0 deletions rust/crates/sift_cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
`source` for the run or asset an artifact was built from, and `derived_from`
only for another artifact. Agents no longer send `derived_from` for a run and
get the call rejected.
- `create_report_template` and `update_report_template` now return a
`report_template_url` link to the template, and `list_report_templates` adds a
`url` to each template. Agents no longer link a new template to a report's
page.

## [v0.7.3] - October 7, 2026

Expand Down
8 changes: 8 additions & 0 deletions rust/crates/sift_mcp/src/service/url/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ impl UrlService {
Ok(format!("{host}/reports/{}", encode_value(report_id)))
}

pub fn build_report_template_url(&self, report_template_id: &str) -> Result<String, ErrorData> {
let host = self.app_host()?;
Ok(format!(
"{host}/report-templates/{}",
encode_value(report_template_id)
))
}

pub fn build_test_report_url(&self, test_report_id: &str) -> Result<String, ErrorData> {
let host = self.app_host()?;
Ok(format!(
Expand Down
6 changes: 6 additions & 0 deletions rust/crates/sift_mcp/src/service/url/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ fn report_url_uses_configured_app_uri() {
assert_eq!(url, "https://app.siftstack.com/reports/rep-123");
}

#[test]
fn report_template_url_uses_configured_app_uri() {
let url = service().build_report_template_url("tmpl-123").unwrap();
assert_eq!(url, "https://app.siftstack.com/report-templates/tmpl-123");
}

#[test]
fn rule_url_uses_configured_app_uri() {
let url = service().build_rule_url("rule-123").unwrap();
Expand Down
59 changes: 47 additions & 12 deletions rust/crates/sift_mcp/src/tool/report_templates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
error::{self, from_anyhow},
server::SiftMcpServer,
service::report_templates::{ReportTemplateUpdate, TemplateRuleIdentifier},
tool::common::MetadataEntry,
tool::common::{MetadataEntry, url_clause, with_urls},
};

#[cfg(test)]
Expand Down Expand Up @@ -65,7 +65,10 @@ impl SiftMcpServer {
`organization_id`, `client_key`, `name`, `description`, `created_date`, `modified_date`,
`created_by_user_id`, `modified_by_user_id`, `is_archived`, `archived_date`, `tags`, `metadata`,
and the ordered `rules` list. Each rule includes `rule_id`, `rule_version_id`,
`rule_version_number`, `client_key`, and `display_order`.
`rule_version_number`, `client_key`, and `display_order`. Each item also carries an added `url`
field with the template's Sift web link (`<host>/report-templates/<report_template_id>`), omitted
when the host can't be derived. Present each template to the user as a Markdown link with its
name as the text and `url` as the target.

Parameters:
- `filter`: CEL expression. Pass an empty string to list everything. Filterable fields:
Expand Down Expand Up @@ -113,6 +116,12 @@ impl SiftMcpServer {
.await
.map_err(from_anyhow)?;

let templates = with_urls(&templates, |t| {
self.url_service
.build_report_template_url(&t.report_template_id)
.ok()
})?;

Ok(CallToolResult::structured(
serde_json::json!({ "report_templates": templates }),
))
Expand All @@ -127,9 +136,11 @@ impl SiftMcpServer {
rejected by the server. Wraps `report_templates/v1 CreateReportTemplate`.

Output:
- `{ \"report_template\": ReportTemplate, \"next_step\": string }`. The returned template is the
server-assigned state including its new `report_template_id`, resolved `rules` (with
`display_order` set), and timestamps.
- `{ \"report_template\": ReportTemplate, \"report_template_url\": string|null, \"next_step\": string }`.
The returned template is the server-assigned state including its new `report_template_id`,
resolved `rules` (with `display_order` set), and timestamps. `report_template_url` is the
template's Sift web link, or null when the host can't be derived. Link the template only to this
URL, never to a report's page.

Parameters:
- `name`: required; the template name.
Expand Down Expand Up @@ -220,17 +231,27 @@ impl SiftMcpServer {
.await
.map_err(from_anyhow)?;

let report_template_url = self
.url_service
.build_report_template_url(&template.report_template_id)
.ok();
let next_step = format!(
"Created report template `{}` ({}) with {} rule(s). Surface it to the user. To run the template \
over a run, call `create_report` with `report_template_id = \"{}\"`.",
"Created report template `{}` ({}) with {} rule(s).{} Surface it to the user. To run the \
template over a run, call `create_report` with `report_template_id = \"{}\"`.",
template.name,
template.report_template_id,
template.rules.len(),
url_clause(
"report template",
Some(&template.name),
report_template_url.as_deref()
),
template.report_template_id,
);

let mut result = CallToolResult::structured(serde_json::json!({
"report_template": template,
"report_template_url": report_template_url,
"next_step": next_step,
}));
result.content = vec![ContentBlock::text(next_step)];
Expand All @@ -243,8 +264,10 @@ impl SiftMcpServer {
Update selected fields on an existing report template. Wraps `report_templates/v1 UpdateReportTemplate`.

Output:
- `{ \"report_template\": ReportTemplate, \"next_step\": string }`. The returned template reflects
the post-update server state.
- `{ \"report_template\": ReportTemplate, \"report_template_url\": string|null, \"next_step\": string }`.
The returned template reflects the post-update server state. `report_template_url` is the
template's Sift web link, or null when the host can't be derived. Link the template only to this
URL, never to a report's page.

Parameters:
- `report_template_id`: required; the id of the template to update.
Expand Down Expand Up @@ -357,14 +380,26 @@ impl SiftMcpServer {
} else {
""
};
let report_template_url = self
.url_service
.build_report_template_url(&template.report_template_id)
.ok();
let next_step = format!(
"Updated report template `{}` ({}).{} Surface the new state to the user and confirm nothing was \
unintentionally dropped — `tags`, `rules`, and `metadata` use REPLACE semantics.",
template.name, template.report_template_id, archive_clause,
"Updated report template `{}` ({}).{}{} Surface the new state to the user and confirm nothing \
was unintentionally dropped — `tags`, `rules`, and `metadata` use REPLACE semantics.",
template.name,
template.report_template_id,
archive_clause,
url_clause(
"report template",
Some(&template.name),
report_template_url.as_deref()
),
);

let mut result = CallToolResult::structured(serde_json::json!({
"report_template": template,
"report_template_url": report_template_url,
"next_step": next_step,
}));
result.content = vec![ContentBlock::text(next_step)];
Expand Down
24 changes: 24 additions & 0 deletions rust/crates/sift_mcp/src/tool/report_templates/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ async fn list_report_templates_returns_single_page() {
let arr = templates.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["reportTemplateId"], "tmpl-1");
assert_eq!(
arr[0]["url"],
"https://app.test.local/report-templates/tmpl-1"
);
}

#[tokio::test]
Expand Down Expand Up @@ -176,6 +180,16 @@ async fn create_report_template_happy_path() {
.await
.expect("create_report_template failed");

let url = structured_field(resp.clone(), "report_template_url");
assert_eq!(url, "https://app.test.local/report-templates/tmpl-new");
let next_step = structured_field(resp.clone(), "next_step");
assert!(
next_step
.as_str()
.unwrap()
.contains("[safety](https://app.test.local/report-templates/tmpl-new)"),
"got {next_step}"
);
let template = structured_field(resp, "report_template");
assert_eq!(template["reportTemplateId"], "tmpl-new");
}
Expand Down Expand Up @@ -247,6 +261,16 @@ async fn update_report_template_happy_path() {
.await
.expect("update_report_template failed");

let url = structured_field(resp.clone(), "report_template_url");
assert_eq!(url, "https://app.test.local/report-templates/tmpl-1");
let next_step = structured_field(resp.clone(), "next_step");
assert!(
next_step
.as_str()
.unwrap()
.contains("[renamed](https://app.test.local/report-templates/tmpl-1)"),
"got {next_step}"
);
let template = structured_field(resp, "report_template");
assert_eq!(template["name"], "renamed");
}
Expand Down
Loading