From b4cab316f760c72274193518ccf4a302fc7348d4 Mon Sep 17 00:00:00 2001 From: Timothy Clem Date: Wed, 6 May 2026 10:51:35 -0700 Subject: [PATCH] Remove disabled_mcp_servers + internalize env_value_mode (cross-SDK parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two Rust-only items on SessionConfig + ResumeSessionConfig that aren't present in the Node, Python, Go, or .NET SDKs and shouldn't be on the public Rust surface either. disabled_mcp_servers: removed entirely. The field was schema-undocumented (not in api.schema.json's SessionCreateRequest) and silently ignored by the runtime on session.create. Runtime MCP server disablement lives in the typed RPC namespace already (session.rpc().mcp().disable() and session.rpc().mcp().config().disable()). env_value_mode: internalized. Hardcode envValueMode: "direct" on every session.create / session.resume payload, matching Node and Go's wire behaviour. Subprocess MCP server env values pass through to the child literally; consumers don't have a meaningful choice at the SDK boundary. Also fix the README's Session cheat-sheet, which advertised several methods (get_model, get_mode/set_mode, list_workspace_files / read_workspace_file, read_plan / update_plan, start_fleet) as if they existed on Session directly. They don't — replace those snippets with the typed session.rpc().*() namespace they actually use. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/CHANGELOG.md | 35 +++++++++++++++---- rust/README.md | 28 ++++++++++----- rust/src/types.rs | 71 +++++++++++--------------------------- rust/tests/session_test.rs | 60 ++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 65 deletions(-) diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index e0dcadea7..54412930f 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -5,15 +5,38 @@ All notable changes to the `github-copilot-sdk` crate will be documented in this The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -After 0.1.0 ships, [release-plz](https://release-plz.dev/) will prepend new -entries from conventional-commit history. The Unreleased entry below is -hand-curated so that crates.io readers get a usable summary of the public -surface on first publish, not a flat list of merge commits — release-plz -will rename `[Unreleased]` to `[0.1.0] - ` and add a fresh empty -`[Unreleased]` above it when it cuts the first release PR. +[release-plz](https://release-plz.dev/) prepends new entries from +conventional-commit history. The 0.1.0 entry below was hand-curated so that +crates.io readers got a usable summary of the public surface on first publish, +not a flat list of merge commits. ## [Unreleased] +### Removed + +- `SessionConfig::disabled_mcp_servers` and the `with_disabled_mcp_servers` + builder. The field was schema-undocumented, silently ignored by the runtime + on `session.create`, and absent from the Node, Python, Go, and .NET SDKs. + Disable individual MCP servers at runtime through the typed RPC surface + instead — `session.rpc().mcp().disable(...)` and + `session.rpc().mcp().config().disable(...)`. +- `SessionConfig::env_value_mode` and `ResumeSessionConfig::env_value_mode` + (and their `with_env_value_mode` builders). The SDK now hardcodes + `envValueMode: "direct"` on every `session.create` and `session.resume` + payload, matching the wire behaviour of the Node, Python, Go, and .NET + SDKs. Subprocess MCP server `env` values are passed through to the child + process literally; callers no longer have a knob here. + +### README + +- The README's `Session` cheat-sheet showed several methods (`get_model`, + `get_mode` / `set_mode`, `list_workspace_files` / `read_workspace_file`, + `read_plan` / `update_plan`, `start_fleet`) as if they existed on + `Session` directly. They don't — the snippets now use the typed + `session.rpc().*` namespace. + +## [0.1.0] - 2026-05-06 + Initial public release. Programmatic Rust access to the GitHub Copilot CLI over JSON-RPC 2.0 (stdio or TCP), with handler-based event dispatch, typed tool/permission/elicitation helpers, and runtime session management. diff --git a/rust/README.md b/rust/README.md index 71c0bf26c..8c3c37aab 100644 --- a/rust/README.md +++ b/rust/README.md @@ -105,23 +105,35 @@ let messages = session.get_messages().await?; session.abort().await?; // Model management -let model = session.get_model().await?; +let model = session.rpc().model().get_current().await?; session.set_model("claude-sonnet-4.5", None).await?; // Mode management (interactive, plan, autopilot) -let mode = session.get_mode().await?; -session.set_mode("autopilot").await?; +let mode = session.rpc().mode().get().await?; +session.rpc().mode().set(ModeSetRequest { mode: SessionMode::Autopilot }).await?; // Workspace files -let files = session.list_workspace_files().await?; -let content = session.read_workspace_file("plan.md").await?; +let files = session.rpc().workspaces().list_files().await?; +let content = session + .rpc() + .workspaces() + .read_file(WorkspacesReadFileRequest { path: "plan.md".into() }) + .await?; // Plan management -let (exists, content) = session.read_plan().await?; -session.update_plan("Updated plan content").await?; +let plan = session.rpc().plan().read().await?; +session + .rpc() + .plan() + .update(PlanUpdateRequest { content: "Updated plan content".into() }) + .await?; // Fleet (sub-agents) -session.start_fleet(Some("Implement the auth module")).await?; +session + .rpc() + .fleet() + .start(FleetStartRequest { prompt: Some("Implement the auth module".into()) }) + .await?; // Cleanup (preserves on-disk session state for later resume) session.disconnect().await?; diff --git a/rust/src/types.rs b/rust/src/types.rs index cd24f4321..1839bd4ff 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -720,11 +720,8 @@ pub struct McpStdioServerConfig { /// Arguments to pass to the subprocess. #[serde(default)] pub args: Vec, - /// Environment variables to set on the subprocess. - /// - /// Interpretation depends on the parent session's - /// `env_value_mode`: `"direct"` (default) treats values as literals; - /// `"indirect"` treats them as env-var names to look up at start time. + /// Environment variables to set on the subprocess. Values are passed + /// through literally to the child process. #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub env: HashMap, /// Working directory for the subprocess. @@ -897,6 +894,14 @@ pub struct AzureProviderOptions { pub api_version: Option, } +/// Wire default for [`SessionConfig::env_value_mode`] / +/// [`ResumeSessionConfig::env_value_mode`]. The runtime understands +/// `"direct"` (literal values) or `"indirect"` (env-var lookup); the SDK +/// only ever sends `"direct"`. +fn default_env_value_mode() -> String { + "direct".into() +} + /// Configuration for creating a new session via the `session.create` RPC. /// /// All fields are optional — the CLI applies sensible defaults. @@ -977,10 +982,11 @@ pub struct SessionConfig { /// MCP server configurations passed through to the CLI. #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option>, - /// How the CLI interprets env values in MCP server configs. - /// `"direct"` = literal values; `"indirect"` = env var names to look up. - #[serde(skip_serializing_if = "Option::is_none")] - pub env_value_mode: Option, + /// Wire-format hint for MCP `env` map values. The runtime understands + /// `"direct"` (literal values) and `"indirect"` (env-var lookup); the + /// SDK only ever sends `"direct"` and consumers don't have a knob. + #[serde(default = "default_env_value_mode", skip_deserializing)] + pub(crate) env_value_mode: String, /// When true, the CLI runs config discovery (MCP config files, skills, plugins). #[serde(skip_serializing_if = "Option::is_none")] pub enable_config_discovery: Option, @@ -1027,10 +1033,6 @@ pub struct SessionConfig { /// even if found in skill directories. #[serde(skip_serializing_if = "Option::is_none")] pub disabled_skills: Option>, - /// MCP server names to disable. Servers in this set will not be - /// started or connected. - #[serde(skip_serializing_if = "Option::is_none")] - pub disabled_mcp_servers: Option>, /// Enable session hooks. When `true`, the CLI sends `hooks.invoke` /// RPC requests at key lifecycle points (pre/post tool use, prompt /// submission, session start/end, errors). @@ -1124,7 +1126,6 @@ impl std::fmt::Debug for SessionConfig { .field("available_tools", &self.available_tools) .field("excluded_tools", &self.excluded_tools) .field("mcp_servers", &self.mcp_servers) - .field("env_value_mode", &self.env_value_mode) .field("enable_config_discovery", &self.enable_config_discovery) .field("request_user_input", &self.request_user_input) .field("request_permission", &self.request_permission) @@ -1134,7 +1135,6 @@ impl std::fmt::Debug for SessionConfig { .field("skill_directories", &self.skill_directories) .field("instruction_directories", &self.instruction_directories) .field("disabled_skills", &self.disabled_skills) - .field("disabled_mcp_servers", &self.disabled_mcp_servers) .field("hooks", &self.hooks) .field("custom_agents", &self.custom_agents) .field("default_agent", &self.default_agent) @@ -1186,7 +1186,7 @@ impl Default for SessionConfig { available_tools: None, excluded_tools: None, mcp_servers: None, - env_value_mode: None, + env_value_mode: default_env_value_mode(), enable_config_discovery: None, request_user_input: Some(true), request_permission: Some(true), @@ -1196,7 +1196,6 @@ impl Default for SessionConfig { skill_directories: None, instruction_directories: None, disabled_skills: None, - disabled_mcp_servers: None, hooks: None, custom_agents: None, default_agent: None, @@ -1376,13 +1375,6 @@ impl SessionConfig { self } - /// Set how the CLI interprets env values in MCP server configs - /// (`"direct"` literal vs `"indirect"` env var name lookup). - pub fn with_env_value_mode(mut self, mode: impl Into) -> Self { - self.env_value_mode = Some(mode.into()); - self - } - /// Enable or disable CLI config discovery (MCP config files, skills, plugins). pub fn with_enable_config_discovery(mut self, enable: bool) -> Self { self.enable_config_discovery = Some(enable); @@ -1451,16 +1443,6 @@ impl SessionConfig { self } - /// Set the names of MCP servers to disable. - pub fn with_disabled_mcp_servers(mut self, names: I) -> Self - where - I: IntoIterator, - S: Into, - { - self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect()); - self - } - /// Set the custom agents (sub-agents) configured for this session. pub fn with_custom_agents>( mut self, @@ -1565,9 +1547,9 @@ pub struct ResumeSessionConfig { /// Re-supply MCP servers so they remain available after app restart. #[serde(skip_serializing_if = "Option::is_none")] pub mcp_servers: Option>, - /// How the CLI interprets env values in MCP configs. - #[serde(skip_serializing_if = "Option::is_none")] - pub env_value_mode: Option, + /// See [`SessionConfig::env_value_mode`]. Always `"direct"` on the wire. + #[serde(default = "default_env_value_mode", skip_deserializing)] + pub(crate) env_value_mode: String, /// Enable config discovery on resume. #[serde(skip_serializing_if = "Option::is_none")] pub enable_config_discovery: Option, @@ -1674,7 +1656,6 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("tools", &self.tools) .field("excluded_tools", &self.excluded_tools) .field("mcp_servers", &self.mcp_servers) - .field("env_value_mode", &self.env_value_mode) .field("enable_config_discovery", &self.enable_config_discovery) .field("request_user_input", &self.request_user_input) .field("request_permission", &self.request_permission) @@ -1731,7 +1712,7 @@ impl ResumeSessionConfig { tools: None, excluded_tools: None, mcp_servers: None, - env_value_mode: None, + env_value_mode: default_env_value_mode(), enable_config_discovery: None, request_user_input: Some(true), request_permission: Some(true), @@ -1874,13 +1855,6 @@ impl ResumeSessionConfig { self } - /// Set how the CLI interprets env values in MCP configs (`"direct"` / - /// `"indirect"`). - pub fn with_env_value_mode(mut self, mode: impl Into) -> Self { - self.env_value_mode = Some(mode.into()); - self - } - /// Enable or disable CLI config discovery on resume. pub fn with_enable_config_discovery(mut self, enable: bool) -> Self { self.enable_config_discovery = Some(enable); @@ -3134,12 +3108,10 @@ mod tests { .with_available_tools(["bash", "view"]) .with_excluded_tools(["dangerous"]) .with_mcp_servers(HashMap::new()) - .with_env_value_mode("direct") .with_enable_config_discovery(true) .with_request_user_input(false) .with_skill_directories([PathBuf::from("/tmp/skills")]) .with_disabled_skills(["broken-skill"]) - .with_disabled_mcp_servers(["broken-server"]) .with_agent("researcher") .with_config_dir(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) @@ -3161,7 +3133,6 @@ mod tests { Some(&["dangerous".to_string()][..]) ); assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.env_value_mode.as_deref(), Some("direct")); assert_eq!(cfg.enable_config_discovery, Some(true)); assert_eq!(cfg.request_user_input, Some(false)); // overrode default assert_eq!(cfg.request_permission, Some(true)); // default preserved @@ -3190,7 +3161,6 @@ mod tests { .with_tools([Tool::new("greet")]) .with_excluded_tools(["dangerous"]) .with_mcp_servers(HashMap::new()) - .with_env_value_mode("indirect") .with_enable_config_discovery(true) .with_request_user_input(false) .with_skill_directories([PathBuf::from("/tmp/skills")]) @@ -3211,7 +3181,6 @@ mod tests { Some(&["dangerous".to_string()][..]) ); assert!(cfg.mcp_servers.is_some()); - assert_eq!(cfg.env_value_mode.as_deref(), Some("indirect")); assert_eq!(cfg.enable_config_discovery, Some(true)); assert_eq!(cfg.request_user_input, Some(false)); // overrode default assert_eq!(cfg.request_permission, Some(true)); // default preserved diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index e64af248e..f186ee40c 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -2075,6 +2075,66 @@ async fn request_elicitation_sent_in_create_params() { timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); } +#[tokio::test] +async fn env_value_mode_hardcoded_direct_on_create_and_resume() { + use github_copilot_sdk::types::ResumeSessionConfig; + + let (client, mut server_read, mut server_write) = make_client(); + + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session(SessionConfig::default().with_handler(Arc::new(NoopHandler))) + .await + .unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "session.create"); + assert_eq!(request["params"]["envValueMode"], "direct"); + + let id = request["id"].as_u64().unwrap(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { "sessionId": "s-env-create" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); + + let resume_handle = tokio::spawn({ + let client = client.clone(); + async move { + let cfg = ResumeSessionConfig::new(SessionId::from("s-env-create")) + .with_handler(Arc::new(NoopHandler)); + client.resume_session(cfg).await.unwrap() + } + }); + + let request = read_framed(&mut server_read).await; + assert_eq!(request["method"], "session.resume"); + assert_eq!(request["params"]["envValueMode"], "direct"); + + let id = request["id"].as_u64().unwrap(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { "sessionId": "s-env-create" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + // resume_session also fires `session.skills.reload`; respond so resume can return. + let reload = read_framed(&mut server_read).await; + assert_eq!(reload["method"], "session.skills.reload"); + let id = reload["id"].as_u64().unwrap(); + let response = serde_json::json!({ "jsonrpc": "2.0", "id": id, "result": {} }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); +} + #[tokio::test] async fn elicitation_methods_fail_without_capability() { let (session, _server) = create_session_pair(Arc::new(NoopHandler)).await;