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
58 changes: 58 additions & 0 deletions app-server/src/protocol_handler/e2e_tests.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1810,6 +1810,64 @@ async fn mcp_set_trust_then_verify_via_server_trust() {
}
}

#[tokio::test]
async fn mcp_set_trust_settings_error_redacts_protocol_response() {
const FILE_CONTENT_CANARY: &str = "protocol-settings-content-canary-p3-03";
const TOKEN_CANARY: &str = "protocol-settings-token-canary-p3-03";

let app = test_app("mcp-set-trust-settings-error").await;
let upsert = app
.handle_request(make_request(
"mcp/upsert_server",
json!({
"id": "trust-settings-error-server",
"transport": "stdio",
"endpoint": "echo",
"enabled": false,
"summary": "trust settings error test",
"auth": {"kind": "none"},
}),
))
.await;
assert!(matches!(upsert.result, ResponseResult::Success { .. }));

let home = &app.sessions.config().home_dir;
let malformed = format!(r#"{{"apiToken":"{TOKEN_CANARY}","content":"{FILE_CONTENT_CANARY}"#);
let parse_error =
serde_json::from_str::<serde_json::Value>(&malformed).expect_err("malformed JSON");
tokio::fs::write(home.join("settings.json"), malformed)
.await
.expect("write malformed settings");

let response = app
.handle_request(make_request(
"mcp/set_trust",
json!({
"server_id": "trust-settings-error-server",
"trust": "trusted",
}),
))
.await;
match &response.result {
ResponseResult::Error(error) => {
assert_eq!(error.code, ErrorCode::McpError);
assert_eq!(
error.message,
format!("io error: json error: {parse_error}")
);
}
other => panic!("malformed settings should produce an MCP error, got: {other:?}"),
}

let serialized = serde_json::to_string(&response).expect("serialize protocol response");
for canary in [FILE_CONTENT_CANARY, TOKEN_CANARY] {
assert!(
!serialized.contains(canary),
"protocol response leaked {canary}"
);
}
}

#[tokio::test]
async fn mcp_read_resource_unknown_server_returns_error() {
let app = test_app("mcp-read-res-err").await;
Expand Down
2 changes: 1 addition & 1 deletion mcp/src/registry/trust.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,7 @@ impl McpRegistry {
save_trust(&storage_dir, &trust_store).await?;
orbcode_config::set_mcp_server_trust_setting(&home_dir, server_id, trust_to_setting(trust))
.await
.map_err(|error| McpError::Io(std::io::Error::other(error.to_string())))?;
.map_err(|error| McpError::Io(std::io::Error::other(error)))?;
Ok(())
}

Expand Down
44 changes: 44 additions & 0 deletions mcp/src/tests/mod.rs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
mod runtime_fault_fixtures;

use std::collections::BTreeMap;
use std::error::Error as _;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
Expand DownExpand Up@@ -3138,6 +3139,49 @@ async fn set_server_trust_persists_to_settings_layer_without_trust_json() {
assert!(matches!(error, McpError::ServerUntrusted { .. }));
}

#[tokio::test]
async fn set_server_trust_retains_redacted_settings_error_source() {
const FILE_CONTENT_CANARY: &str = "settings-file-content-canary-p3-03";
const TOKEN_CANARY: &str = "settings-token-canary-p3-03";

let (home, cwd) = temp_paths("trust-settings-error-source");
std::fs::write(
cwd.join(".mcp.json"),
r#"{"mcpServers":{"docs":{"type":"ws","url":"wss://docs.example/mcp"}}}"#,
)
.expect("write mcp json");
let registry = McpRegistry::load(&home, &cwd).await.expect("load registry");
let malformed = format!(r#"{{"apiToken":"{TOKEN_CANARY}","content":"{FILE_CONTENT_CANARY}"#);
let parse_error = serde_json::from_str::<Value>(&malformed).expect_err("malformed JSON");
std::fs::write(home.join("settings.json"), malformed).expect("write malformed settings");
let error = registry
.set_server_trust("docs", McpServerTrust::Trusted)
.await
.expect_err("malformed settings must reject trust persistence");

let McpError::Io(io_error) = &error else {
panic!("settings persistence must preserve the McpError::Io boundary");
};
let stored_source = io_error
.get_ref()
.expect("io error stores the ConfigError source");
assert!(stored_source.is::<orbcode_config::ConfigError>());

let io_source = error.source().expect("McpError retains io source");
let json_source = io_source
.source()
.expect("io error chain reaches the serde_json source");
assert!(json_source.is::<serde_json::Error>());

let display = error.to_string();
let debug = format!("{error:?}");
assert_eq!(display, format!("io error: json error: {parse_error}"));
for canary in [FILE_CONTENT_CANARY, TOKEN_CANARY] {
assert!(!display.contains(canary), "Display leaked {canary}");
assert!(!debug.contains(canary), "Debug leaked {canary}");
}
}

#[test]
fn bearer_env_missing_returns_auth_required() {
let env_var = "ORBCODE_MCP_TEST_TOKEN_MISSING_DEFINITELY";
Expand Down
Loading