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
2 changes: 1 addition & 1 deletion .github/scripts/test_public_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ members = [
]

[workspace.package]
version = "1.0.5"
version = "1.0.6"
edition = "2021"
rust-version = "1.95"
license = "MIT"
Expand Down Expand Up @@ -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"
Expand Down
22 changes: 15 additions & 7 deletions crates/mcp-server/src/setup/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
));
Expand Down
103 changes: 103 additions & 0 deletions crates/mcp-server/tests/setup_profile_conflict.rs
Original file line number Diff line number Diff line change
@@ -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}");
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
},
Expand Down