Skip to content

Commit b66cc54

Browse files
committed
fix(workspace-binding): expand ~ home shorthand in reported/pinned roots
Cursor's roots/list (and \${workspaceFolder} substitution through the mcp-remote global bridge) can report a workspace folder using shell-style \`~\` shorthand instead of a real absolute path. normalize_workspace_root never expanded it, so detect_style saw a relative path and validation/ routing failed for any folder under the user's home directory. Gateway and client share a filesystem in this scenario (local Cursor), so expand \`~\` against the current machine's home dir before absoluteness detection. Fixes the "Path must be absolute" error surfaced in the create-from-live Workspace binding panel for home-relative client roots. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent bda7e04 commit b66cc54

1 file changed

Lines changed: 51 additions & 1 deletion

File tree

crates/mcpmux-core/src/domain/workspace_binding.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,14 @@ pub fn normalize_workspace_root(input: &str) -> String {
283283
// drive-letter detector can fire on the following byte.
284284
let cleaned = strip_leading_slash_before_drive(&decoded);
285285

286+
// Some clients report roots using shell-style `~` shorthand instead of
287+
// the real absolute path (observed from Cursor's `roots/list` and from
288+
// `${workspaceFolder}` substitution through the mcp-remote bridge — see
289+
// docs/manual/cursor-workspace-bridge.md). Gateway and client share a
290+
// filesystem in that scenario, so expand against *this* machine's home
291+
// directory before absoluteness detection runs.
292+
let cleaned = expand_home_tilde(&cleaned);
293+
286294
match detect_style(&cleaned) {
287295
Some(PathStyle::Posix) => normalize_posix(&cleaned),
288296
Some(PathStyle::WindowsDrive) => normalize_windows_drive(&cleaned),
@@ -358,6 +366,26 @@ fn reconstruct_uri_path(rest: &str) -> String {
358366
format!("\\\\{host}{path}")
359367
}
360368

369+
/// Expand a leading `~` (or `~/...`) to the current user's home directory.
370+
///
371+
/// Only the bare `~` prefix is handled — `~otheruser/...` is left untouched
372+
/// (POSIX shells resolve that via the password database, which we have no
373+
/// business doing here, and detect_style will simply reject it as relative).
374+
fn expand_home_tilde(path: &str) -> String {
375+
let Some(rest) = path.strip_prefix('~') else {
376+
return path.to_string();
377+
};
378+
if !rest.is_empty() && !rest.starts_with('/') && !rest.starts_with('\\') {
379+
return path.to_string();
380+
}
381+
let Some(home) = dirs::home_dir() else {
382+
return path.to_string();
383+
};
384+
let home = home.to_string_lossy();
385+
let home = home.trim_end_matches(['/', '\\']);
386+
format!("{home}{rest}")
387+
}
388+
361389
fn strip_leading_slash_before_drive(path: &str) -> String {
362390
// Strip ALL leading separators before a drive letter, not just one: a
363391
// `file://` URI for a Windows path can arrive with a doubled slash
@@ -887,12 +915,34 @@ mod tests {
887915
validate_workspace_root("./proj"),
888916
WorkspaceRootValidation::Invalid { .. }
889917
));
918+
// `~otheruser/proj` isn't a bare-home shorthand — still relative.
890919
assert!(matches!(
891-
validate_workspace_root("~/proj"),
920+
validate_workspace_root("~otheruser/proj"),
892921
WorkspaceRootValidation::Invalid { .. }
893922
));
894923
}
895924

925+
#[test]
926+
fn normalize_expands_home_tilde() {
927+
let home = dirs::home_dir().expect("test environment has a home dir");
928+
let home_str = home.to_string_lossy().trim_end_matches(['/', '\\']).to_string();
929+
930+
assert_eq!(
931+
normalize_workspace_root("~/Desktop/proj"),
932+
format!("{home_str}/Desktop/proj")
933+
);
934+
// Bare `~` alone expands to the home dir itself.
935+
assert_eq!(normalize_workspace_root("~"), home_str);
936+
}
937+
938+
#[test]
939+
fn validate_accepts_home_tilde() {
940+
assert!(matches!(
941+
validate_workspace_root("~/Desktop/proj"),
942+
WorkspaceRootValidation::Ok { .. }
943+
));
944+
}
945+
896946
#[test]
897947
fn validate_rejects_filesystem_root() {
898948
for bad in &["/", "D:\\", "d:\\", "\\\\"] {

0 commit comments

Comments
 (0)