refactor(mcp): extract AddServerRequestBuilder and McpImportSummary
Extract request building and import summary formatting helpers. Wire into McpServersViewModel with unit test coverage.
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
package com.m57.hermescontrol.ui.mcp
|
||||||
|
|
||||||
|
import com.m57.hermescontrol.data.model.AddMcpServerRequest
|
||||||
|
|
||||||
|
object AddServerRequestBuilder {
|
||||||
|
/**
|
||||||
|
* Constructs a validated AddMcpServerRequest from the add-server form parameters.
|
||||||
|
*/
|
||||||
|
fun build(
|
||||||
|
name: String,
|
||||||
|
mode: AddServerMode,
|
||||||
|
url: String,
|
||||||
|
command: String,
|
||||||
|
args: String,
|
||||||
|
auth: String,
|
||||||
|
bearerToken: String,
|
||||||
|
): AddMcpServerRequest {
|
||||||
|
val isHttp = mode == AddServerMode.HTTP
|
||||||
|
val isStdio = mode == AddServerMode.Stdio
|
||||||
|
|
||||||
|
val resolvedUrl = if (isHttp) url.trim().ifBlank { null } else null
|
||||||
|
val resolvedCommand = if (isStdio) command.trim().ifBlank { null } else null
|
||||||
|
val resolvedArgs =
|
||||||
|
if (isStdio && args.isNotBlank()) {
|
||||||
|
args.trim().split("\\s+".toRegex()).filter { it.isNotEmpty() }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val resolvedAuth = if (isHttp && auth != "none") auth else null
|
||||||
|
val resolvedBearerToken =
|
||||||
|
if (isHttp && auth == "header") bearerToken.trim().ifBlank { null } else null
|
||||||
|
|
||||||
|
return AddMcpServerRequest(
|
||||||
|
name = name.trim(),
|
||||||
|
url = resolvedUrl,
|
||||||
|
command = resolvedCommand,
|
||||||
|
args = resolvedArgs,
|
||||||
|
auth = resolvedAuth,
|
||||||
|
bearerToken = resolvedBearerToken,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overload reading form parameters directly from McpServersUiState.
|
||||||
|
*/
|
||||||
|
fun build(state: McpServersUiState): AddMcpServerRequest =
|
||||||
|
build(
|
||||||
|
name = state.addServerName,
|
||||||
|
mode = state.addMode,
|
||||||
|
url = state.addServerUrl,
|
||||||
|
command = state.addServerCommand,
|
||||||
|
args = state.addServerArgs,
|
||||||
|
auth = state.addServerAuth,
|
||||||
|
bearerToken = state.addServerBearerToken,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.m57.hermescontrol.ui.mcp
|
||||||
|
|
||||||
|
object McpImportSummary {
|
||||||
|
/**
|
||||||
|
* Formats the human-readable summary message reporting the result of a batch MCP JSON import.
|
||||||
|
*/
|
||||||
|
fun formatSummary(
|
||||||
|
successCount: Int,
|
||||||
|
errors: List<String>,
|
||||||
|
): String {
|
||||||
|
val errSummary = errors.joinToString("; ")
|
||||||
|
return when {
|
||||||
|
errors.isEmpty() -> "Successfully imported $successCount server(s)"
|
||||||
|
successCount > 0 -> "Imported $successCount server(s), ${errors.size} failed: $errSummary"
|
||||||
|
else -> "Failed to import: $errSummary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -297,13 +297,7 @@ class McpServersViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
val errSummary = errors.joinToString("; ")
|
val msg = McpImportSummary.formatSummary(successCount, errors)
|
||||||
val msg =
|
|
||||||
when {
|
|
||||||
errors.isEmpty() -> "Successfully imported $successCount server(s)"
|
|
||||||
successCount > 0 -> "Imported $successCount server(s), ${errors.size} failed: $errSummary"
|
|
||||||
else -> "Failed to import: $errSummary"
|
|
||||||
}
|
|
||||||
state.copy(
|
state.copy(
|
||||||
isImportingJson = false,
|
isImportingJson = false,
|
||||||
showImportDialog = false,
|
showImportDialog = false,
|
||||||
@@ -357,38 +351,7 @@ class McpServersViewModel :
|
|||||||
}
|
}
|
||||||
_uiState.update { it.copy(addingServer = true) }
|
_uiState.update { it.copy(addingServer = true) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val request =
|
val request = AddServerRequestBuilder.build(state)
|
||||||
AddMcpServerRequest(
|
|
||||||
name = state.addServerName.trim(),
|
|
||||||
url = if (state.addMode == AddServerMode.HTTP) state.addServerUrl.trim().ifBlank { null } else null,
|
|
||||||
command =
|
|
||||||
if (state.addMode == AddServerMode.Stdio) {
|
|
||||||
state.addServerCommand.trim().ifBlank { null }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
args =
|
|
||||||
if (state.addMode == AddServerMode.Stdio && state.addServerArgs.isNotBlank()) {
|
|
||||||
state.addServerArgs
|
|
||||||
.trim()
|
|
||||||
.split("\\s+".toRegex())
|
|
||||||
.filter { it.isNotEmpty() }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
auth =
|
|
||||||
if (state.addMode == AddServerMode.HTTP && state.addServerAuth != "none") {
|
|
||||||
state.addServerAuth
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
bearerToken =
|
|
||||||
if (state.addMode == AddServerMode.HTTP && state.addServerAuth == "header") {
|
|
||||||
state.addServerBearerToken.trim().ifBlank { null }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
)
|
|
||||||
val result =
|
val result =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
safeApiCall { ApiClient.hermesApi.addMcpServer(request) }
|
safeApiCall { ApiClient.hermesApi.addMcpServer(request) }
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.m57.hermescontrol.ui.mcp
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AddServerRequestBuilderTest {
|
||||||
|
@Test
|
||||||
|
fun `build HTTP mode creates request with url and auth`() {
|
||||||
|
val request =
|
||||||
|
AddServerRequestBuilder.build(
|
||||||
|
name = " weather-server ",
|
||||||
|
mode = AddServerMode.HTTP,
|
||||||
|
url = " https://api.weather.com/mcp ",
|
||||||
|
command = "ignored-cmd",
|
||||||
|
args = "ignored-arg",
|
||||||
|
auth = "header",
|
||||||
|
bearerToken = " secret-token ",
|
||||||
|
)
|
||||||
|
assertEquals("weather-server", request.name)
|
||||||
|
assertEquals("https://api.weather.com/mcp", request.url)
|
||||||
|
assertNull(request.command)
|
||||||
|
assertNull(request.args)
|
||||||
|
assertEquals("header", request.auth)
|
||||||
|
assertEquals("secret-token", request.bearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `build HTTP mode with none auth nulls auth and bearerToken`() {
|
||||||
|
val request =
|
||||||
|
AddServerRequestBuilder.build(
|
||||||
|
name = "public-server",
|
||||||
|
mode = AddServerMode.HTTP,
|
||||||
|
url = "https://example.com/mcp",
|
||||||
|
command = "",
|
||||||
|
args = "",
|
||||||
|
auth = "none",
|
||||||
|
bearerToken = "should-be-ignored",
|
||||||
|
)
|
||||||
|
assertEquals("public-server", request.name)
|
||||||
|
assertEquals("https://example.com/mcp", request.url)
|
||||||
|
assertNull(request.auth)
|
||||||
|
assertNull(request.bearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `build Stdio mode creates request with command and parsed args`() {
|
||||||
|
val request =
|
||||||
|
AddServerRequestBuilder.build(
|
||||||
|
name = "local-tools",
|
||||||
|
mode = AddServerMode.Stdio,
|
||||||
|
url = "https://ignored.com",
|
||||||
|
command = " python3 ",
|
||||||
|
args = " -m mcp_server --debug ",
|
||||||
|
auth = "header",
|
||||||
|
bearerToken = "token",
|
||||||
|
)
|
||||||
|
assertEquals("local-tools", request.name)
|
||||||
|
assertNull(request.url)
|
||||||
|
assertEquals("python3", request.command)
|
||||||
|
assertEquals(listOf("-m", "mcp_server", "--debug"), request.args)
|
||||||
|
assertNull(request.auth)
|
||||||
|
assertNull(request.bearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `build Stdio mode with empty args nulls args`() {
|
||||||
|
val request =
|
||||||
|
AddServerRequestBuilder.build(
|
||||||
|
name = "npx-server",
|
||||||
|
mode = AddServerMode.Stdio,
|
||||||
|
url = "",
|
||||||
|
command = "npx server",
|
||||||
|
args = " ",
|
||||||
|
auth = "none",
|
||||||
|
bearerToken = "",
|
||||||
|
)
|
||||||
|
assertEquals("npx server", request.command)
|
||||||
|
assertNull(request.args)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `build from McpServersUiState populates fields correctly`() {
|
||||||
|
val state =
|
||||||
|
McpServersUiState(
|
||||||
|
addServerName = "state-server",
|
||||||
|
addMode = AddServerMode.HTTP,
|
||||||
|
addServerUrl = "https://state.com",
|
||||||
|
addServerAuth = "none",
|
||||||
|
)
|
||||||
|
val request = AddServerRequestBuilder.build(state)
|
||||||
|
assertEquals("state-server", request.name)
|
||||||
|
assertEquals("https://state.com", request.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.m57.hermescontrol.ui.mcp
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class McpImportSummaryTest {
|
||||||
|
@Test
|
||||||
|
fun `formatSummary returns success message when zero errors`() {
|
||||||
|
val result = McpImportSummary.formatSummary(3, emptyList())
|
||||||
|
assertEquals("Successfully imported 3 server(s)", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `formatSummary returns partial success message when some failed`() {
|
||||||
|
val result = McpImportSummary.formatSummary(2, listOf("serverA: timeout", "serverB: 404"))
|
||||||
|
assertEquals("Imported 2 server(s), 2 failed: serverA: timeout; serverB: 404", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `formatSummary returns failure message when all failed`() {
|
||||||
|
val result = McpImportSummary.formatSummary(0, listOf("serverA: connection refused"))
|
||||||
|
assertEquals("Failed to import: serverA: connection refused", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user