diff --git a/crates/mcp_backend/src/lib.rs b/crates/mcp_backend/src/lib.rs index f60c502..6f09849 100644 --- a/crates/mcp_backend/src/lib.rs +++ b/crates/mcp_backend/src/lib.rs @@ -116,6 +116,9 @@ pub struct SubmitRequest { pub config: ServerConfig, /// `true` = register (admin/owner, auto-approved); `false` = request review. pub register: bool, + /// The machine the server was discovered on. The backend uses this to scope + /// approval of a local (stdio) server to the specific host it lives on. + pub hostname: String, } /// Async client bound to a base URL + bearer key. @@ -390,10 +393,14 @@ fn submit_body(req: &SubmitRequest) -> serde_json::Value { let mut body = json!({ "name": req.name, "status": if req.register { "registered" } else { "requested" }, + "hostname": req.hostname, }); let obj = body.as_object_mut().expect("object literal"); match &req.config { ServerConfig::Stdio { command, args, env } => { + // Explicit transport type so the backend doesn't have to infer stdio + // from the presence of `command`. + obj.insert("type".into(), json!("stdio")); obj.insert("command".into(), json!(command)); obj.insert("args".into(), json!(args)); obj.insert("env".into(), json!(env)); @@ -473,11 +480,14 @@ mod tests { env: BTreeMap::new(), }, register: true, + hostname: "dev-box".into(), }; let b = submit_body(&stdio); assert_eq!(b["name"], "s"); assert_eq!(b["status"], "registered"); + assert_eq!(b["type"], "stdio"); assert_eq!(b["command"], "npx"); + assert_eq!(b["hostname"], "dev-box"); let http = SubmitRequest { name: "h".into(), @@ -487,11 +497,13 @@ mod tests { kind: HttpKind::Sse, }, register: false, + hostname: "dev-box".into(), }; let b = submit_body(&http); assert_eq!(b["status"], "requested"); assert_eq!(b["type"], "sse"); assert_eq!(b["url"], "https://x"); + assert_eq!(b["hostname"], "dev-box"); } #[test] diff --git a/crates/mcp_detector_daemon/src/ops.rs b/crates/mcp_detector_daemon/src/ops.rs index d489c9c..aa7af59 100644 --- a/crates/mcp_detector_daemon/src/ops.rs +++ b/crates/mcp_detector_daemon/src/ops.rs @@ -578,6 +578,7 @@ async fn submit_to_ew( name: name.to_string(), config, register, + hostname: crate::platform::hostname(), }) .await; match res { diff --git a/crates/mcp_detector_daemon/src/platform.rs b/crates/mcp_detector_daemon/src/platform.rs index 001a703..feccb14 100644 --- a/crates/mcp_detector_daemon/src/platform.rs +++ b/crates/mcp_detector_daemon/src/platform.rs @@ -96,3 +96,33 @@ mod imp { } pub use imp::*; + +/// Best-effort machine hostname, sent to the backend so a local (stdio) server +/// can be approved for the specific host it lives on. +/// +/// IMPORTANT: this must stay aligned with edison-stdiod's `config::hostname()` +/// (env `HOSTNAME`, then `COMPUTERNAME`, then the `hostname` command) so the +/// backend keys the *same* machine identity for both daemons. The command +/// fallback is what works on macOS, where `HOSTNAME` isn't exported to +/// launchd/user processes. On Windows `COMPUTERNAME` is always set and +/// short-circuits, so the GUI-subsystem daemon never spawns a console +/// `hostname`. Cross-platform, so it lives outside the cfg-split `imp`. +pub fn hostname() -> String { + for var in ["HOSTNAME", "COMPUTERNAME"] { + if let Ok(h) = std::env::var(var) { + let trimmed = h.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + } + if let Ok(out) = std::process::Command::new("hostname").output() + && let Ok(s) = String::from_utf8(out.stdout) + { + let trimmed = s.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + "unknown".to_string() +}