Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions rust/crates/sift_cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### 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
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