From cc5d0aefffbaa35be7c14011d34094b0144e292c Mon Sep 17 00:00:00 2001 From: Ian Luo Date: Wed, 26 Aug 2026 13:16:34 +0800 Subject: [PATCH] Preserve MCP trust settings error sources --- app-server/src/protocol_handler/e2e_tests.rs | 58 ++++++++++++++++++++ mcp/src/registry/trust.rs | 2 +- mcp/src/tests/mod.rs | 44 +++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/app-server/src/protocol_handler/e2e_tests.rs b/app-server/src/protocol_handler/e2e_tests.rs index 025e81d..75395ef 100644 --- a/app-server/src/protocol_handler/e2e_tests.rs +++ b/app-server/src/protocol_handler/e2e_tests.rs @@ -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::(&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; diff --git a/mcp/src/registry/trust.rs b/mcp/src/registry/trust.rs index c8d7841..a8bd3db 100644 --- a/mcp/src/registry/trust.rs +++ b/mcp/src/registry/trust.rs @@ -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(()) } diff --git a/mcp/src/tests/mod.rs b/mcp/src/tests/mod.rs index a74bbe3..cee12d0 100644 --- a/mcp/src/tests/mod.rs +++ b/mcp/src/tests/mod.rs @@ -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; @@ -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::(&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::()); + + 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::()); + + 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";