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
7 changes: 7 additions & 0 deletions rust/crates/sift_cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### What's New

## [v0.7.2] - September 29, 2026

### What's New

- `sift-cli mcp` now sends a `User-Agent` when it fetches account feature flags.
Hosts that reject requests without one no longer disable flag-gated tools.

## [v0.7.1] - September 22, 2026

### What's New
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/sift_cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sift_cli"
version = "0.7.1"
version = "0.7.2"
authors.workspace = true
edition.workspace = true
categories.workspace = true
Expand Down
30 changes: 16 additions & 14 deletions rust/crates/sift_cli/src/cmd/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,21 @@ pub async fn run(ctx: Context, args: McpArgs, app_uri: String) -> Result<ExitCod
if client_event_config.is_none() {
tracing::info!("non-essential traffic is disabled");
}
let feature_flags = sift_mcp::FeatureFlags::fetch(&ctx.rest_uri, &ctx.api_key)
.await
.unwrap_or_else(|error| {
tracing::warn!(
error = format!("{error:#}"),
"failed to fetch feature flags; flag-gated tools are disabled"
);
sift_mcp::FeatureFlags::default()
});
let client_name = if args.chat {
sift_mcp::ClientName::Chat
} else {
sift_mcp::ClientName::SiftMcp
};
let feature_flags =
sift_mcp::FeatureFlags::fetch(&ctx.rest_uri, &ctx.api_key, client_name, &cli_version)
.await
.unwrap_or_else(|error| {
tracing::warn!(
error = format!("{error:#}"),
"failed to fetch feature flags; flag-gated tools are disabled"
);
sift_mcp::FeatureFlags::default()
});

let rest_config = sift_mcp::RestConfig::new(ctx.rest_uri.clone(), ctx.api_key.clone());
let credentials = Credentials::Config {
Expand All @@ -79,11 +85,7 @@ pub async fn run(ctx: Context, args: McpArgs, app_uri: String) -> Result<ExitCod
feature_flags,
Some(rest_config),
args.ignore_tool,
if args.chat {
sift_mcp::ClientName::Chat
} else {
sift_mcp::ClientName::SiftMcp
},
client_name,
)
.await
{
Expand Down
38 changes: 32 additions & 6 deletions rust/crates/sift_mcp/src/feature_flags.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use std::{collections::HashMap, time::Duration};

use anyhow::{Context, Result};
use reqwest::header::USER_AGENT;
use serde::Deserialize;

use crate::ClientName;

const FEATURE_FLAGS_PATH: &str = "/api/v1/feature-flags/variants";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);

Expand Down Expand Up @@ -41,12 +44,21 @@ impl FeatureFlags {
.is_some_and(|variant| !variant.value.is_empty() && variant.value != "off")
}

pub async fn fetch(rest_uri: &str, api_key: &str) -> Result<Self> {
pub async fn fetch(
rest_uri: &str,
api_key: &str,
client_name: ClientName,
cli_version: &str,
) -> Result<Self> {
let endpoint = format!("{}{FEATURE_FLAGS_PATH}", rest_uri.trim_end_matches('/'));
reqwest::Client::new()
.get(endpoint)
.timeout(REQUEST_TIMEOUT)
.bearer_auth(api_key)
.header(
USER_AGENT,
format!("{}/{cli_version}", client_name.as_str()),
)
.send()
.await
.context("feature flag request failed")?
Expand All @@ -63,7 +75,7 @@ mod tests {
use std::collections::HashMap;

use super::{FeatureFlagVariant, FeatureFlags};
use crate::client_event::start_http_server;
use crate::{ClientName, client_event::start_http_server};

fn response(status: &str, body: &str) -> Vec<u8> {
format!(
Expand Down Expand Up @@ -121,9 +133,14 @@ mod tests {
))
.await;

let flags = FeatureFlags::fetch(&format!("{rest_uri}/"), "test-key")
.await
.unwrap();
let flags = FeatureFlags::fetch(
&format!("{rest_uri}/"),
"test-key",
ClientName::SiftMcp,
"1.2.3",
)
.await
.unwrap();
assert!(flags.enabled("test-reports"));

let request = String::from_utf8(server.await.unwrap()).unwrap();
Expand All @@ -134,10 +151,19 @@ mod tests {
.lines()
.any(|line| line.eq_ignore_ascii_case("authorization: Bearer test-key"))
);
assert!(
headers
.lines()
.any(|line| line.eq_ignore_ascii_case("user-agent: sift_mcp/1.2.3"))
);

let (rest_uri, server) =
start_http_server(response("500 Internal Server Error", "{}")).await;
assert!(FeatureFlags::fetch(&rest_uri, "test-key").await.is_err());
assert!(
FeatureFlags::fetch(&rest_uri, "test-key", ClientName::SiftMcp, "1.2.3")
.await
.is_err()
);
server.await.unwrap();
}
}
Loading