Skip to content

Commit f024d9e

Browse files
committed
feat(gateway): web admin clone parity + fix dropped display-name/update-policy on save
Un-stub clone_server, set_server_display_name, is_clone_id_available, suggest_clone_suffix, and list_clone_dependents in the admin command bridge — mechanical port of the desktop Tauri clone commands onto ServerAppService. While wiring set_server_display_name, found save_server_inputs was silently dropping display_name_override on both desktop and web admin (update_config has no such param), and dropping update_policy/ pinned_version on web admin specifically (hardcoded to None, None). Fixed both runtimes to forward all three fields. Adds unit tests for is_clone_id_available, suggest_clone_suffix, list_clone_dependents, and set_display_name_override. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 6cdba06 commit f024d9e

4 files changed

Lines changed: 223 additions & 28 deletions

File tree

apps/desktop/src-tauri/src/commands/server.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ pub async fn save_server_inputs(
137137
env_overrides: Option<HashMap<String, String>>,
138138
args_append: Option<Vec<String>>,
139139
extra_headers: Option<HashMap<String, String>>,
140+
display_name_override: Option<String>,
140141
update_policy: Option<String>,
141142
pinned_version: Option<String>,
142143
) -> Result<InstalledServer, String> {
@@ -147,7 +148,7 @@ pub async fn save_server_inputs(
147148

148149
let space_uuid = uuid::Uuid::parse_str(&space_id).map_err(|e| e.to_string())?;
149150

150-
service
151+
let updated = service
151152
.update_config(
152153
space_uuid,
153154
&id,
@@ -159,7 +160,16 @@ pub async fn save_server_inputs(
159160
pinned_version,
160161
)
161162
.await
162-
.map_err(|e| e.to_string())
163+
.map_err(|e| e.to_string())?;
164+
165+
if display_name_override.is_some() {
166+
service
167+
.set_display_name_override(space_uuid, &id, display_name_override)
168+
.await
169+
.map_err(|e| e.to_string())
170+
} else {
171+
Ok(updated)
172+
}
163173
}
164174

165175
/// Set (or clear) the display name override for an installed server.

crates/mcpmux-core/src/application/server.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,4 +934,150 @@ mod tests {
934934
.expect("cloned row should be persisted");
935935
assert_eq!(persisted.extra_headers, parent_headers);
936936
}
937+
938+
#[tokio::test]
939+
async fn is_clone_id_available_reflects_existing_rows() {
940+
let space_id = Uuid::new_v4();
941+
let repo = Arc::new(InMemoryInstalledServerRepository::new());
942+
let event_bus = EventBus::new();
943+
let definition = user_space_http_definition("posthog-personal");
944+
repo.seed(
945+
InstalledServer::new(space_id.to_string(), "posthog-personal")
946+
.with_definition(&definition),
947+
)
948+
.await;
949+
repo.seed(
950+
InstalledServer::new(space_id.to_string(), "posthog-personal-work")
951+
.with_definition(&definition),
952+
)
953+
.await;
954+
955+
let service = ServerAppService::new(repo, None, None, event_bus.sender());
956+
957+
assert!(
958+
!service
959+
.is_clone_id_available(space_id, "posthog-personal", "work")
960+
.await
961+
.unwrap(),
962+
"suffix already installed must not be available"
963+
);
964+
assert!(
965+
service
966+
.is_clone_id_available(space_id, "posthog-personal", "mesh")
967+
.await
968+
.unwrap(),
969+
"unused suffix must be available"
970+
);
971+
}
972+
973+
#[tokio::test]
974+
async fn suggest_clone_suffix_skips_taken_defaults() {
975+
let space_id = Uuid::new_v4();
976+
let repo = Arc::new(InMemoryInstalledServerRepository::new());
977+
let event_bus = EventBus::new();
978+
let definition = user_space_http_definition("posthog-personal");
979+
repo.seed(
980+
InstalledServer::new(space_id.to_string(), "posthog-personal")
981+
.with_definition(&definition),
982+
)
983+
.await;
984+
repo.seed(
985+
InstalledServer::new(space_id.to_string(), "posthog-personal-work")
986+
.with_definition(&definition),
987+
)
988+
.await;
989+
990+
let service = ServerAppService::new(repo, None, None, event_bus.sender());
991+
992+
let suggested = service
993+
.suggest_clone_suffix(space_id, "posthog-personal")
994+
.await
995+
.unwrap();
996+
assert_eq!(
997+
suggested, "personal",
998+
"first free default suffix after 'work'"
999+
);
1000+
}
1001+
1002+
#[tokio::test]
1003+
async fn list_clone_dependents_filters_by_cloned_from() {
1004+
let space_id = Uuid::new_v4();
1005+
let repo = Arc::new(InMemoryInstalledServerRepository::new());
1006+
let event_bus = EventBus::new();
1007+
let definition = user_space_http_definition("posthog-personal");
1008+
repo.seed(
1009+
InstalledServer::new(space_id.to_string(), "posthog-personal")
1010+
.with_definition(&definition),
1011+
)
1012+
.await;
1013+
repo.seed(
1014+
InstalledServer::new(space_id.to_string(), "posthog-personal-work")
1015+
.with_definition(&definition)
1016+
.with_cloned_from("posthog-personal"),
1017+
)
1018+
.await;
1019+
repo.seed(
1020+
InstalledServer::new(space_id.to_string(), "posthog-personal-mesh")
1021+
.with_definition(&definition)
1022+
.with_cloned_from("posthog-personal"),
1023+
)
1024+
.await;
1025+
repo.seed(
1026+
InstalledServer::new(space_id.to_string(), "unrelated-server")
1027+
.with_definition(&definition),
1028+
)
1029+
.await;
1030+
1031+
let service = ServerAppService::new(repo, None, None, event_bus.sender());
1032+
1033+
let mut dependents = service
1034+
.list_clone_dependents(&space_id.to_string(), "posthog-personal")
1035+
.await
1036+
.unwrap()
1037+
.into_iter()
1038+
.map(|s| s.server_id)
1039+
.collect::<Vec<_>>();
1040+
dependents.sort();
1041+
assert_eq!(
1042+
dependents,
1043+
vec!["posthog-personal-mesh", "posthog-personal-work"]
1044+
);
1045+
}
1046+
1047+
#[tokio::test]
1048+
async fn set_display_name_override_sets_and_clears() {
1049+
let space_id = Uuid::new_v4();
1050+
let repo = Arc::new(InMemoryInstalledServerRepository::new());
1051+
let event_bus = EventBus::new();
1052+
let definition = user_space_http_definition("posthog-personal");
1053+
repo.seed(
1054+
InstalledServer::new(space_id.to_string(), "posthog-personal")
1055+
.with_definition(&definition),
1056+
)
1057+
.await;
1058+
1059+
let service = ServerAppService::new(repo.clone(), None, None, event_bus.sender());
1060+
1061+
let updated = service
1062+
.set_display_name_override(
1063+
space_id,
1064+
"posthog-personal",
1065+
Some("Work PostHog".to_string()),
1066+
)
1067+
.await
1068+
.unwrap();
1069+
assert_eq!(
1070+
updated.display_name_override,
1071+
Some("Work PostHog".to_string())
1072+
);
1073+
1074+
let cleared = service
1075+
.set_display_name_override(space_id, "posthog-personal", Some(" ".to_string()))
1076+
.await
1077+
.unwrap();
1078+
assert_eq!(
1079+
cleared.display_name_override, None,
1080+
"whitespace-only value clears the override"
1081+
);
1082+
}
9371083
}

crates/mcpmux-gateway/src/admin/command_bridge/read.rs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -803,31 +803,45 @@ pub async fn get_server_feature(ctx: &AdminBridgeCtx, id: String) -> Result<Valu
803803
}
804804

805805
pub async fn is_clone_id_available(
806-
_ctx: &AdminBridgeCtx,
807-
_space_id: String,
808-
_source_server_id: String,
809-
_suffix: String,
806+
ctx: &AdminBridgeCtx,
807+
space_id: String,
808+
source_server_id: String,
809+
suffix: String,
810810
) -> Result<Value> {
811-
// ponytail: clone_server lands in Phase 6
812-
Err(anyhow!("Server cloning not yet available"))
811+
let space_uuid = Uuid::parse_str(&space_id)?;
812+
as_json(
813+
ctx.services
814+
.server()
815+
.is_clone_id_available(space_uuid, &source_server_id, &suffix)
816+
.await?,
817+
)
813818
}
814819

815820
pub async fn suggest_clone_suffix(
816-
_ctx: &AdminBridgeCtx,
817-
_space_id: String,
818-
_source_server_id: String,
821+
ctx: &AdminBridgeCtx,
822+
space_id: String,
823+
source_server_id: String,
819824
) -> Result<Value> {
820-
// ponytail: clone_server lands in Phase 6
821-
Err(anyhow!("Server cloning not yet available"))
825+
let space_uuid = Uuid::parse_str(&space_id)?;
826+
as_json(
827+
ctx.services
828+
.server()
829+
.suggest_clone_suffix(space_uuid, &source_server_id)
830+
.await?,
831+
)
822832
}
823833

824834
pub async fn list_clone_dependents(
825-
_ctx: &AdminBridgeCtx,
826-
_space_id: String,
827-
_source_server_id: String,
835+
ctx: &AdminBridgeCtx,
836+
space_id: String,
837+
source_server_id: String,
828838
) -> Result<Value> {
829-
// ponytail: clone_server lands in Phase 6
830-
Err(anyhow!("Server cloning not yet available"))
839+
as_json(
840+
ctx.services
841+
.server()
842+
.list_clone_dependents(&space_id, &source_server_id)
843+
.await?,
844+
)
831845
}
832846

833847
pub async fn now_utc() -> Result<Value> {

crates/mcpmux-gateway/src/admin/command_bridge/write.rs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,20 +1020,34 @@ pub async fn save_server_inputs(
10201020
body.env_overrides,
10211021
body.args_append,
10221022
body.extra_headers,
1023-
None,
1024-
None,
1023+
body.update_policy.map(|p| UpdatePolicy::from_db_str(&p)),
1024+
body.pinned_version,
10251025
)
10261026
.await?;
1027+
1028+
let installed = if body.display_name_override.is_some() {
1029+
ctx.services
1030+
.server()
1031+
.set_display_name_override(space_uuid, &id, body.display_name_override)
1032+
.await?
1033+
} else {
1034+
installed
1035+
};
10271036
as_json(installed)
10281037
}
10291038

10301039
pub async fn set_server_display_name(
1031-
_ctx: &AdminBridgeCtx,
1032-
_id: String,
1033-
_body: SetServerDisplayNameBody,
1040+
ctx: &AdminBridgeCtx,
1041+
id: String,
1042+
body: SetServerDisplayNameBody,
10341043
) -> Result<Value> {
1035-
// ponytail: set_display_name_override lands in Phase 6
1036-
Err(anyhow!("Server display name override not yet available"))
1044+
let space_uuid = Uuid::parse_str(&body.space_id)?;
1045+
let installed = ctx
1046+
.services
1047+
.server()
1048+
.set_display_name_override(space_uuid, &id, body.display_name)
1049+
.await?;
1050+
as_json(installed)
10371051
}
10381052

10391053
pub async fn set_server_oauth_connected(
@@ -1049,9 +1063,20 @@ pub async fn set_server_oauth_connected(
10491063
Ok(json!({ "ok": true }))
10501064
}
10511065

1052-
pub async fn clone_server(_ctx: &AdminBridgeCtx, _body: CloneServerBody) -> Result<Value> {
1053-
// ponytail: clone_server lands in Phase 6
1054-
Err(anyhow!("Server cloning not yet available"))
1066+
pub async fn clone_server(ctx: &AdminBridgeCtx, body: CloneServerBody) -> Result<Value> {
1067+
let space_uuid = Uuid::parse_str(&body.space_id)?;
1068+
let installed = ctx
1069+
.services
1070+
.server()
1071+
.clone_server(
1072+
space_uuid,
1073+
&body.source_server_id,
1074+
&body.suffix,
1075+
body.alias.as_deref(),
1076+
body.display_name.as_deref(),
1077+
)
1078+
.await?;
1079+
as_json(installed)
10551080
}
10561081

10571082
// --- Gateway writes (delegated) ---

0 commit comments

Comments
 (0)