Skip to content

Commit 889ee62

Browse files
fix: deterministic MCPG config key ordering (HashMap -> BTreeMap) (#328)
Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/925fe165-d29e-4a57-b34a-424a56bc7862 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
1 parent 0097428 commit 889ee62

4 files changed

Lines changed: 21 additions & 14 deletions

File tree

src/compile/common.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,13 +1142,20 @@ fn nonempty_vec<T: Clone>(v: &[T]) -> Option<Vec<T>> {
11421142
if v.is_empty() { None } else { Some(v.to_vec()) }
11431143
}
11441144

1145-
/// Returns `Some(m.clone())` when `m` is non-empty, otherwise `None`.
1146-
fn nonempty_map<K, V>(m: &HashMap<K, V>) -> Option<HashMap<K, V>>
1145+
/// Returns `Some(BTreeMap from m)` when `m` is non-empty, otherwise `None`.
1146+
///
1147+
/// Converts a `HashMap` source to a `BTreeMap` so JSON serialization is
1148+
/// deterministic (keys are emitted in sorted order).
1149+
fn nonempty_map<K, V>(m: &HashMap<K, V>) -> Option<std::collections::BTreeMap<K, V>>
11471150
where
1148-
K: Clone + Eq + std::hash::Hash,
1151+
K: Clone + Eq + std::hash::Hash + Ord,
11491152
V: Clone,
11501153
{
1151-
if m.is_empty() { None } else { Some(m.clone()) }
1154+
if m.is_empty() {
1155+
None
1156+
} else {
1157+
Some(m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
1158+
}
11521159
}
11531160

11541161
/// Validate a container-based MCP entry and emit any warnings.
@@ -1200,7 +1207,7 @@ fn build_http_mcpg_server(url: &str, opts: &crate::compile::types::McpOptions) -
12001207
fn try_add_user_mcp(
12011208
name: &str,
12021209
config: &McpConfig,
1203-
servers: &mut HashMap<String, McpgServerConfig>,
1210+
servers: &mut std::collections::BTreeMap<String, McpgServerConfig>,
12041211
) -> Result<()> {
12051212
// Prevent user-defined MCPs from overwriting the reserved safeoutputs backend
12061213
if name.eq_ignore_ascii_case("safeoutputs") {
@@ -1282,7 +1289,7 @@ pub fn generate_mcpg_config(
12821289
ctx: &CompileContext,
12831290
extensions: &[super::extensions::Extension],
12841291
) -> Result<McpgConfig> {
1285-
let mut mcp_servers = HashMap::new();
1292+
let mut mcp_servers = std::collections::BTreeMap::new();
12861293

12871294
// Add extension-contributed MCPG server entries (safeoutputs, azure-devops, etc.)
12881295
for ext in extensions {

src/compile/extensions/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
1717
use anyhow::Result;
1818
use serde::Serialize;
19-
use std::collections::HashMap;
19+
use std::collections::BTreeMap;
2020

2121
use super::types::FrontMatter;
2222

@@ -51,10 +51,10 @@ pub struct McpgServerConfig {
5151
pub url: Option<String>,
5252
/// HTTP headers (e.g., Authorization)
5353
#[serde(skip_serializing_if = "Option::is_none")]
54-
pub headers: Option<HashMap<String, String>>,
54+
pub headers: Option<BTreeMap<String, String>>,
5555
/// Environment variables for the server process
5656
#[serde(skip_serializing_if = "Option::is_none")]
57-
pub env: Option<HashMap<String, String>>,
57+
pub env: Option<BTreeMap<String, String>>,
5858
/// Tool allow-list (if empty or absent, all tools are allowed)
5959
#[serde(skip_serializing_if = "Option::is_none")]
6060
pub tools: Option<Vec<String>>,
@@ -74,7 +74,7 @@ pub struct McpgGatewayConfig {
7474
#[derive(Debug, Serialize, Clone)]
7575
#[serde(rename_all = "camelCase")]
7676
pub struct McpgConfig {
77-
pub mcp_servers: HashMap<String, McpgServerConfig>,
77+
pub mcp_servers: BTreeMap<String, McpgServerConfig>,
7878
pub gateway: McpgGatewayConfig,
7979
}
8080

src/compile/extensions/safe_outputs.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{CompileContext, CompilerExtension, ExtensionPhase, McpgServerConfig};
22
use anyhow::Result;
3-
use std::collections::HashMap;
3+
use std::collections::BTreeMap;
44

55
// ─── SafeOutputs (always-on, internal) ───────────────────────────────
66

@@ -34,7 +34,7 @@ impl CompilerExtension for SafeOutputsExtension {
3434
mounts: None,
3535
args: None,
3636
url: Some("http://localhost:${SAFE_OUTPUTS_PORT}/mcp".to_string()),
37-
headers: Some(HashMap::from([(
37+
headers: Some(BTreeMap::from([(
3838
"Authorization".to_string(),
3939
"Bearer ${SAFE_OUTPUTS_API_KEY}".to_string(),
4040
)])),

src/tools/azure_devops/extension.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::compile::{
99
};
1010
use crate::compile::types::AzureDevOpsToolConfig;
1111
use anyhow::Result;
12-
use std::collections::HashMap;
12+
use std::collections::BTreeMap;
1313

1414
/// Azure DevOps first-party tool extension.
1515
///
@@ -107,7 +107,7 @@ impl CompilerExtension for AzureDevOpsExtension {
107107
let (auth_flag, token_var) = ("envvar", "ADO_MCP_AUTH_TOKEN");
108108
entrypoint_args.extend(["-a".to_string(), auth_flag.to_string()]);
109109

110-
let env = Some(HashMap::from([(
110+
let env = Some(BTreeMap::from([(
111111
token_var.to_string(),
112112
String::new(), // Passthrough from MCPG process env
113113
)]));

0 commit comments

Comments
 (0)