From 722cb622fd9e310b8db000bb8b62989da3430342 Mon Sep 17 00:00:00 2001 From: escott- Date: Sat, 12 Sep 2026 09:16:18 -0700 Subject: [PATCH 1/3] fix(setup): report account conflicts before sign-in success (#591) Signed-off-by: escott- --- crates/mcp-server/src/setup/profile.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/mcp-server/src/setup/profile.rs b/crates/mcp-server/src/setup/profile.rs index 31556a1..c1864af 100644 --- a/crates/mcp-server/src/setup/profile.rs +++ b/crates/mcp-server/src/setup/profile.rs @@ -104,18 +104,20 @@ pub async fn run_setup_with_profile( } }; - println!( - "{}Signed in as {}", - CHECK, - style(&payload.email).cyan().bold() - ); - // ------------------------------------------------------------------ // Credentials: keep a valid same-user key, never clobber another user's. // ------------------------------------------------------------------ let api_url = normalize_api_url(&payload.api_url); let (active_key, kept_existing) = resolve_credentials(&payload, &api_url).await?; + // A redeemed link identifies the requested account; only credential + // resolution establishes which account this machine can actually use. + println!( + "{}Signed in as {}", + CHECK, + style(&payload.email).cyan().bold() + ); + let config = Config { api_key: Some(active_key.clone()), ..Default::default() @@ -611,7 +613,13 @@ async fn resolve_credentials( } else { return Err(anyhow!( "This machine already has credentials for {} but the setup link belongs to {}. \ - Re-run in an interactive terminal to switch accounts.", + Non-interactive setup cannot switch accounts; existing credentials were left untouched. \ + Open an interactive terminal, generate a fresh signed-in command in the dashboard \ + for the intended account, then run it and confirm the account switch. \ + If you used a signed-in link, it has already been redeemed. For CI or shared machines, \ + use an isolated OS user configured for the intended account. \ + Also remove conflicting CONTEXTSTREAM_API_KEY / CONTEXTSTREAM_TOKEN values \ + from that shell or CI job before retrying.", user.email, payload.email )); From 370f7d1618ff1eb0ba893a3aa612c5e61991c099 Mon Sep 17 00:00:00 2001 From: escott- Date: Sat, 12 Sep 2026 12:27:23 -0700 Subject: [PATCH 2/3] test(setup): cover account conflict failures and prepare 1.0.6 Signed-off-by: escott- --- Cargo.lock | 14 +-- Cargo.toml | 14 +-- .../tests/setup_profile_conflict.rs | 103 ++++++++++++++++++ package.json | 2 +- server.json | 4 +- 5 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 crates/mcp-server/tests/setup_profile_conflict.rs diff --git a/Cargo.lock b/Cargo.lock index c1149b5..040b982 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1593,7 +1593,7 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "mcp-acceleration-products" -version = "1.0.5" +version = "1.0.6" dependencies = [ "async-trait", "chrono", @@ -1611,7 +1611,7 @@ dependencies = [ [[package]] name = "mcp-client" -version = "1.0.5" +version = "1.0.6" dependencies = [ "anyhow", "base64", @@ -1639,7 +1639,7 @@ dependencies = [ [[package]] name = "mcp-model-registry" -version = "1.0.5" +version = "1.0.6" dependencies = [ "mcp-types", "serde", @@ -1648,7 +1648,7 @@ dependencies = [ [[package]] name = "mcp-server" -version = "1.0.5" +version = "1.0.6" dependencies = [ "anyhow", "axum", @@ -1702,7 +1702,7 @@ dependencies = [ [[package]] name = "mcp-session" -version = "1.0.5" +version = "1.0.6" dependencies = [ "chrono", "dashmap", @@ -1721,7 +1721,7 @@ dependencies = [ [[package]] name = "mcp-tools" -version = "1.0.5" +version = "1.0.6" dependencies = [ "anyhow", "async-trait", @@ -1756,7 +1756,7 @@ dependencies = [ [[package]] name = "mcp-types" -version = "1.0.5" +version = "1.0.6" dependencies = [ "async-trait", "chrono", diff --git a/Cargo.toml b/Cargo.toml index b5a9a87..b999d25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ members = [ ] [workspace.package] -version = "1.0.5" +version = "1.0.6" edition = "2021" rust-version = "1.95" license = "MIT" @@ -90,12 +90,12 @@ fs2 = "0.4" notify = "6" # Workspace crates -mcp-types = { version = "=1.0.5", path = "crates/mcp-types" } -mcp-client = { version = "=1.0.5", path = "crates/mcp-client" } -mcp-session = { version = "=1.0.5", path = "crates/mcp-session" } -mcp-tools = { version = "=1.0.5", path = "crates/mcp-tools" } -mcp-model-registry = { version = "=1.0.5", path = "crates/mcp-model-registry" } -mcp-acceleration-products = { version = "=1.0.5", path = "crates/mcp-acceleration-products" } +mcp-types = { version = "=1.0.6", path = "crates/mcp-types" } +mcp-client = { version = "=1.0.6", path = "crates/mcp-client" } +mcp-session = { version = "=1.0.6", path = "crates/mcp-session" } +mcp-tools = { version = "=1.0.6", path = "crates/mcp-tools" } +mcp-model-registry = { version = "=1.0.6", path = "crates/mcp-model-registry" } +mcp-acceleration-products = { version = "=1.0.6", path = "crates/mcp-acceleration-products" } # Testing mockall = "0.13" diff --git a/crates/mcp-server/tests/setup_profile_conflict.rs b/crates/mcp-server/tests/setup_profile_conflict.rs new file mode 100644 index 0000000..4e25644 --- /dev/null +++ b/crates/mcp-server/tests/setup_profile_conflict.rs @@ -0,0 +1,103 @@ +//! Exercise the real CLI without reading or changing the operator's credentials. +#![cfg(unix)] + +use axum::{http::StatusCode, routing::get, Json, Router}; +use serde_json::json; +use std::process::{Command, Stdio}; + +async fn run_profile_failure(status: StatusCode) -> (String, String) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let api_url = format!("http://{}", listener.local_addr().unwrap()); + let app = Router::new().route( + "/api/v1/auth/me", + get(move || async move { + ( + status, + Json(json!({"data": { + "id": "11111111-1111-4111-8111-111111111111", + "email": "existing@example.test", + "created_at": "2026-01-01T00:00:00Z" + }})), + ) + }), + ); + let server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() }); + let home = tempfile::tempdir().unwrap(); + let config_dir = home.path().join(".contextstream"); + std::fs::create_dir(&config_dir).unwrap(); + let credentials_path = config_dir.join("credentials.json"); + let credentials = json!({"api_key": "existing-test-key", "api_url": api_url}).to_string(); + std::fs::write(&credentials_path, &credentials).unwrap(); + let profile_path = home.path().join("profile.json"); + std::fs::write( + &profile_path, + json!({ + "device_id": "test-device", + "user_id": "22222222-2222-4222-8222-222222222222", + "email": "intended@example.test", + "api_key": {"id": "test-key-id", "secret": "minted-test-key"}, + "api_url": api_url, + "profile": {"editors": []} + }) + .to_string(), + ) + .unwrap(); + let isolated_home = home.path().to_path_buf(); + let output = tokio::task::spawn_blocking(move || { + Command::new(env!("CARGO_BIN_EXE_contextstream-mcp")) + .args(["setup", "--account-only", "--profile-file"]) + .arg(profile_path) + .env_clear() + .env("HOME", &isolated_home) + .env("CONTEXTSTREAM_API_URL", api_url) + .env("NO_COLOR", "1") + .current_dir(isolated_home) + .stdin(Stdio::null()) + .output() + .unwrap() + }) + .await + .unwrap(); + server.abort(); + assert!(!output.status.success()); + assert_eq!( + std::fs::read_to_string(credentials_path).unwrap(), + credentials + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(!stdout.contains("Signed in as"), "{stdout}"); + assert!(!stdout.contains("Credentials installed"), "{stdout}"); + assert!(!stdout.contains("Credentials replaced"), "{stdout}"); + for secret in ["existing-test-key", "minted-test-key"] { + assert!(!stdout.contains(secret)); + assert!(!stderr.contains(secret)); + } + (stdout, stderr) +} + +#[tokio::test] +async fn noninteractive_account_conflict_preserves_credentials_and_explains_recovery() { + let (_, stderr) = run_profile_failure(StatusCode::OK).await; + for expected in [ + "existing@example.test", + "intended@example.test", + "existing credentials were left untouched", + "fresh signed-in command", + "confirm the account switch", + "already been redeemed", + "CONTEXTSTREAM_API_KEY / CONTEXTSTREAM_TOKEN", + ] { + assert!(stderr.contains(expected), "missing {expected}: {stderr}"); + } +} + +#[tokio::test] +async fn failed_credential_verification_does_not_claim_sign_in_success() { + let (_, stderr) = run_profile_failure(StatusCode::INTERNAL_SERVER_ERROR).await; + assert!( + stderr.contains("Could not verify the existing credentials"), + "{stderr}" + ); + assert!(stderr.contains("left untouched"), "{stderr}"); +} diff --git a/package.json b/package.json index 864367a..e27ef3c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@contextstream/mcp-server", "mcpName": "io.github.contextstream/mcp-server", - "version": "1.0.5", + "version": "1.0.6", "description": "Verified npm launcher for the open-source ContextStream Rust MCP server", "type": "module", "license": "MIT", diff --git a/server.json b/server.json index 73c21e2..c5f11c3 100644 --- a/server.json +++ b/server.json @@ -3,7 +3,7 @@ "name": "io.github.contextstream/mcp-server", "title": "ContextStream MCP Server", "description": "Project memory, semantic code search, and grounded agent context.", - "version": "1.0.5", + "version": "1.0.6", "repository": { "url": "https://github.com/contextstream/mcp-server", "source": "github" @@ -20,7 +20,7 @@ "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "@contextstream/mcp-server", - "version": "1.0.5", + "version": "1.0.6", "transport": { "type": "stdio" }, From 2b6a6ea13bb96c7309b3474d2f2ed13b8835512d Mon Sep 17 00:00:00 2001 From: escott- Date: Sat, 12 Sep 2026 12:28:49 -0700 Subject: [PATCH 3/3] test(release): expect the 1.0.6 package version Signed-off-by: escott- --- .github/scripts/test_public_boundary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/test_public_boundary.py b/.github/scripts/test_public_boundary.py index 03ffbe8..a0d6b71 100644 --- a/.github/scripts/test_public_boundary.py +++ b/.github/scripts/test_public_boundary.py @@ -18,7 +18,7 @@ class PublicBoundaryTest(unittest.TestCase): def test_repository_satisfies_public_boundary(self) -> None: - self.assertEqual(boundary.verify(REPOSITORY_ROOT), "1.0.5") + self.assertEqual(boundary.verify(REPOSITORY_ROOT), "1.0.6") def test_forbidden_private_source_is_detected(self) -> None: with tempfile.TemporaryDirectory(prefix="public-boundary-") as temporary: