Skip to content

Commit c4e688d

Browse files
committed
feat(extensions): add socket config field for Unix domain socket transport
Adds `socket: Option<String>` to `ExtensionConfig::StreamableHttp` to support HTTP-over-Unix-domain-socket routing (e.g. K8s Envoy sidecars). The transport wiring using rmcp's `UnixSocketHttpClient` will follow once rmcp 1.5.0 is released on crates.io.
1 parent 0ec73c8 commit c4e688d

9 files changed

Lines changed: 118 additions & 2 deletions

File tree

crates/goose-acp/src/server.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ fn mcp_server_to_extension_config(mcp_server: McpServer) -> Result<ExtensionConf
164164
.map(|h| (h.name, h.value))
165165
.collect(),
166166
timeout,
167+
socket: None,
167168
bundled: Some(false),
168169
available_tools: vec![],
169170
})
@@ -2962,6 +2963,7 @@ mod tests {
29622963
"Bearer ghp_xxxxxxxxxxxx".into()
29632964
)]),
29642965
timeout: None,
2966+
socket: None,
29652967
bundled: Some(false),
29662968
available_tools: vec![],
29672969
})

crates/goose-cli/src/commands/configure.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,7 @@ fn configure_streamable_http_extension() -> anyhow::Result<()> {
11741174
headers,
11751175
description,
11761176
timeout: Some(timeout),
1177+
socket: None,
11771178
bundled: None,
11781179
available_tools: Vec::new(),
11791180
},

crates/goose-cli/src/recipes/secret_discovery.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ mod tests {
166166
env_keys: vec!["GITHUB_TOKEN".to_string(), "GITHUB_API_URL".to_string()],
167167
description: "github-mcp".to_string(),
168168
timeout: None,
169+
socket: None,
169170
bundled: None,
170171
available_tools: Vec::new(),
171172
headers: HashMap::new(),
@@ -262,6 +263,7 @@ mod tests {
262263
env_keys: vec!["API_KEY".to_string()],
263264
description: "service-a".to_string(),
264265
timeout: None,
266+
socket: None,
265267
bundled: None,
266268
available_tools: Vec::new(),
267269
headers: HashMap::new(),
@@ -321,6 +323,7 @@ mod tests {
321323
env_keys: vec!["PARENT_TOKEN".to_string()],
322324
description: "parent-ext".to_string(),
323325
timeout: None,
326+
socket: None,
324327
bundled: None,
325328
available_tools: Vec::new(),
326329
headers: HashMap::new(),

crates/goose-cli/src/session/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ impl CliSession {
369369
headers: HashMap::new(),
370370
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
371371
timeout: Some(timeout),
372+
socket: None,
372373
bundled: None,
373374
available_tools: Vec::new(),
374375
}
@@ -2130,6 +2131,7 @@ mod tests {
21302131
headers: HashMap::new(),
21312132
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
21322133
timeout: Some(300),
2134+
socket: None,
21332135
bundled: None,
21342136
available_tools: vec![],
21352137
}
@@ -2145,6 +2147,7 @@ mod tests {
21452147
headers: HashMap::new(),
21462148
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
21472149
timeout: Some(300),
2150+
socket: None,
21482151
bundled: None,
21492152
available_tools: vec![],
21502153
}
@@ -2160,6 +2163,7 @@ mod tests {
21602163
headers: HashMap::new(),
21612164
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
21622165
timeout: Some(300),
2166+
socket: None,
21632167
bundled: None,
21642168
available_tools: vec![],
21652169
}

crates/goose/src/acp/provider.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,6 +1388,7 @@ mod tests {
13881388
env_keys: vec![],
13891389
headers: HashMap::from([("Authorization".into(), "Bearer ghp_xxxxxxxxxxxx".into())]),
13901390
timeout: None,
1391+
socket: None,
13911392
bundled: Some(false),
13921393
available_tools: vec![],
13931394
},
@@ -1441,6 +1442,7 @@ mod tests {
14411442
env_keys: vec![],
14421443
headers: HashMap::from([("Authorization".into(), "Bearer ghp_xxxxxxxxxxxx".into())]),
14431444
timeout: None,
1445+
socket: None,
14441446
bundled: Some(false),
14451447
available_tools: vec![],
14461448
};

crates/goose/src/agents/extension.rs

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ pub enum ExtensionConfig {
235235
// NOTE: set timeout to be optional for compatibility.
236236
// However, new configurations should include this field.
237237
timeout: Option<u64>,
238+
/// Optional Unix domain socket path for HTTP-over-UDS transport.
239+
/// When set, the HTTP connection is routed through this socket while
240+
/// `uri` is used for the Host header and path.
241+
/// Use `@name` for Linux abstract sockets.
242+
#[serde(default)]
243+
socket: Option<String>,
238244
#[serde(default)]
239245
bundled: Option<bool>,
240246
#[serde(default)]
@@ -307,6 +313,7 @@ impl ExtensionConfig {
307313
headers: HashMap::new(),
308314
description: description.into(),
309315
timeout: Some(timeout.into()),
316+
socket: None,
310317
bundled: None,
311318
available_tools: Vec::new(),
312319
}
@@ -460,6 +467,7 @@ impl ExtensionConfig {
460467
env_keys,
461468
headers,
462469
timeout,
470+
socket,
463471
bundled,
464472
available_tools,
465473
} => {
@@ -479,6 +487,7 @@ impl ExtensionConfig {
479487
env_keys: vec![],
480488
headers,
481489
timeout,
490+
socket,
482491
bundled,
483492
available_tools,
484493
})
@@ -494,8 +503,17 @@ impl std::fmt::Display for ExtensionConfig {
494503
ExtensionConfig::Sse { name, .. } => {
495504
write!(f, "SSE({}: unsupported)", name)
496505
}
497-
ExtensionConfig::StreamableHttp { name, uri, .. } => {
498-
write!(f, "StreamableHttp({}: {})", name, uri)
506+
ExtensionConfig::StreamableHttp {
507+
name,
508+
uri,
509+
socket,
510+
..
511+
} => {
512+
if let Some(socket) = socket {
513+
write!(f, "StreamableHttp({}: {} via {})", name, uri, socket)
514+
} else {
515+
write!(f, "StreamableHttp({}: {})", name, uri)
516+
}
499517
}
500518
ExtensionConfig::Stdio {
501519
name, cmd, args, ..
@@ -679,6 +697,7 @@ available_tools: []
679697
.into_iter()
680698
.collect(),
681699
timeout: None,
700+
socket: None,
682701
bundled: None,
683702
available_tools: vec![],
684703
},
@@ -699,6 +718,7 @@ available_tools: []
699718
.into_iter()
700719
.collect(),
701720
timeout: None,
721+
socket: None,
702722
bundled: None,
703723
available_tools: vec![],
704724
}
@@ -772,6 +792,7 @@ available_tools: []
772792
.into_iter()
773793
.collect(),
774794
timeout: None,
795+
socket: None,
775796
bundled: None,
776797
available_tools: vec![],
777798
},
@@ -789,6 +810,7 @@ available_tools: []
789810
.into_iter()
790811
.collect(),
791812
timeout: None,
813+
socket: None,
792814
bundled: None,
793815
available_tools: vec![],
794816
}
@@ -803,6 +825,7 @@ available_tools: []
803825
env_keys: vec!["MY_SECRET".into()],
804826
headers: std::collections::HashMap::new(),
805827
timeout: None,
828+
socket: None,
806829
bundled: None,
807830
available_tools: vec![],
808831
},
@@ -818,6 +841,7 @@ available_tools: []
818841
env_keys: vec![],
819842
headers: std::collections::HashMap::new(),
820843
timeout: None,
844+
socket: None,
821845
bundled: None,
822846
available_tools: vec![],
823847
}
@@ -867,4 +891,77 @@ available_tools: []
867891
cfg.set("MY_SECRET", &"secret_value", true).unwrap();
868892
assert_eq!(config.resolve(&cfg).await.unwrap(), expected);
869893
}
894+
895+
#[test]
896+
fn test_deserialize_streamable_http_with_socket() {
897+
let json = r#"{
898+
"type": "streamable_http",
899+
"name": "test",
900+
"uri": "http://localhost:8080/mcp",
901+
"socket": "@egress.sock"
902+
}"#;
903+
let config: ExtensionConfig = serde_json::from_str(json).unwrap();
904+
match config {
905+
ExtensionConfig::StreamableHttp { socket, .. } => {
906+
assert_eq!(socket, Some("@egress.sock".to_string()));
907+
}
908+
other => panic!("expected StreamableHttp, got {:?}", other),
909+
}
910+
}
911+
912+
#[test]
913+
fn test_deserialize_streamable_http_without_socket() {
914+
let json = r#"{
915+
"type": "streamable_http",
916+
"name": "test",
917+
"uri": "http://localhost:8080/mcp"
918+
}"#;
919+
let config: ExtensionConfig = serde_json::from_str(json).unwrap();
920+
match config {
921+
ExtensionConfig::StreamableHttp { socket, .. } => {
922+
assert_eq!(socket, None);
923+
}
924+
other => panic!("expected StreamableHttp, got {:?}", other),
925+
}
926+
}
927+
928+
#[test]
929+
fn test_display_streamable_http_with_socket() {
930+
let config = ExtensionConfig::StreamableHttp {
931+
name: "test".into(),
932+
description: String::new(),
933+
uri: "http://localhost:8080/mcp".into(),
934+
envs: extension::Envs::default(),
935+
env_keys: vec![],
936+
headers: std::collections::HashMap::new(),
937+
timeout: None,
938+
socket: Some("@egress.sock".to_string()),
939+
bundled: None,
940+
available_tools: vec![],
941+
};
942+
assert_eq!(
943+
config.to_string(),
944+
"StreamableHttp(test: http://localhost:8080/mcp via @egress.sock)"
945+
);
946+
}
947+
948+
#[test]
949+
fn test_display_streamable_http_without_socket() {
950+
let config = ExtensionConfig::StreamableHttp {
951+
name: "test".into(),
952+
description: String::new(),
953+
uri: "http://localhost:8080/mcp".into(),
954+
envs: extension::Envs::default(),
955+
env_keys: vec![],
956+
headers: std::collections::HashMap::new(),
957+
timeout: None,
958+
socket: None,
959+
bundled: None,
960+
available_tools: vec![],
961+
};
962+
assert_eq!(
963+
config.to_string(),
964+
"StreamableHttp(test: http://localhost:8080/mcp)"
965+
);
966+
}
870967
}

crates/goose/src/providers/claude_code.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,7 @@ mod tests {
11491149
env_keys: vec![],
11501150
headers: HashMap::from([("Authorization".into(), "Bearer token".into())]),
11511151
timeout: None,
1152+
socket: None,
11521153
bundled: Some(false),
11531154
available_tools: vec![],
11541155
}],
@@ -1170,6 +1171,7 @@ mod tests {
11701171
env_keys: vec![],
11711172
headers: HashMap::new(),
11721173
timeout: None,
1174+
socket: None,
11731175
bundled: None,
11741176
available_tools: vec![],
11751177
}],

crates/goose/src/providers/codex.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,7 @@ mod tests {
792792
env_keys: vec![],
793793
headers: HashMap::from([("Authorization".into(), "Bearer token".into())]),
794794
timeout: None,
795+
socket: None,
795796
bundled: Some(false),
796797
available_tools: vec![],
797798
},
@@ -810,6 +811,7 @@ mod tests {
810811
env_keys: vec![],
811812
headers: HashMap::new(),
812813
timeout: None,
814+
socket: None,
813815
bundled: None,
814816
available_tools: vec![],
815817
},

crates/goose/src/recipe/recipe_extension_adapter.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ enum RecipeExtensionConfigInternal {
6262
headers: HashMap<String, String>,
6363
timeout: Option<u64>,
6464
#[serde(default)]
65+
socket: Option<String>,
66+
#[serde(default)]
6567
bundled: Option<bool>,
6668
#[serde(default)]
6769
available_tools: Vec<String>,
@@ -140,6 +142,7 @@ impl From<RecipeExtensionConfigInternal> for ExtensionConfig {
140142
env_keys,
141143
headers,
142144
timeout,
145+
socket,
143146
bundled,
144147
available_tools
145148
},

0 commit comments

Comments
 (0)