fix(mcp): show Installed state for catalog entries already installed (#1405)
This commit is contained in:
6 files changed
+92
-22
No files matched your search
@@ -1,6 +1,9 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
@Serializable
|
||||
data class McpServersResponse(
|
||||
@@ -19,12 +22,21 @@ data class McpServer(
|
||||
val status: String? = null,
|
||||
val error: String? = null,
|
||||
val auth: String? = null,
|
||||
val tools: List<String>? = null,
|
||||
val tools: JsonElement? = null,
|
||||
val source: String = "config",
|
||||
val plugin: String? = null,
|
||||
) {
|
||||
// #1283: plugin-provided servers are managed by their owning plugin.
|
||||
val isPluginOwned: Boolean get() = source == "plugin"
|
||||
|
||||
// Backend sends `tools` as null, a name list, or a `{include, exclude}` filter object.
|
||||
val toolCount: Int?
|
||||
get() =
|
||||
when (val t = tools) {
|
||||
is JsonArray -> t.size
|
||||
is JsonObject -> (t["include"] as? JsonArray)?.size
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -71,13 +83,15 @@ data class McpCatalogEntry(
|
||||
val url: String? = null,
|
||||
val command: String? = null,
|
||||
val args: List<String>? = null,
|
||||
val env: List<McpCatalogEnvVar>? = null,
|
||||
@SerialName("required_env") val env: List<McpCatalogEnvVar>? = null,
|
||||
val installed: Boolean = false,
|
||||
val enabled: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class McpCatalogEnvVar(
|
||||
val key: String,
|
||||
val label: String? = null,
|
||||
@SerialName("name") val key: String,
|
||||
@SerialName("prompt") val label: String? = null,
|
||||
val description: String? = null,
|
||||
val required: Boolean = false,
|
||||
)
|
||||
|
||||
@@ -585,6 +585,7 @@ class McpServersViewModel(
|
||||
)
|
||||
}
|
||||
loadServers(forceRefresh = true)
|
||||
loadCatalog()
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
|
||||
@@ -9,11 +9,17 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
@@ -47,6 +53,7 @@ fun CatalogView(
|
||||
filteredCatalog: List<McpCatalogEntry>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val installedNames = remember(state.servers) { state.servers.map { it.name }.toSet() }
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
SearchBar(
|
||||
query = state.catalogQuery,
|
||||
@@ -88,6 +95,7 @@ fun CatalogView(
|
||||
state = state,
|
||||
viewModel = viewModel,
|
||||
spacing = spacing,
|
||||
isInstalled = entry.installed || entry.name in installedNames,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -103,6 +111,7 @@ fun CatalogEntryCard(
|
||||
viewModel: McpServersViewModel,
|
||||
spacing: Spacing,
|
||||
modifier: Modifier = Modifier,
|
||||
isInstalled: Boolean = false,
|
||||
) {
|
||||
var showInstallForm by remember { mutableStateOf(false) }
|
||||
val isInstalling = state.installingCatalogEntry == entry.name
|
||||
@@ -131,23 +140,31 @@ fun CatalogEntryCard(
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
if (entry.env?.isNotEmpty() == true) {
|
||||
showInstallForm = !showInstallForm
|
||||
} else {
|
||||
viewModel.installCatalogEntry(entry)
|
||||
}
|
||||
},
|
||||
enabled = !isInstalling,
|
||||
) {
|
||||
Text(
|
||||
if (isInstalling) {
|
||||
stringResource(R.string.mcp_servers_catalog_installing)
|
||||
} else {
|
||||
stringResource(R.string.mcp_servers_catalog_install)
|
||||
if (isInstalled) {
|
||||
FilledTonalButton(onClick = {}, enabled = false) {
|
||||
Icon(Icons.Filled.Check, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(modifier = Modifier.width(spacing.xs))
|
||||
Text(stringResource(R.string.mcp_servers_catalog_installed))
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = {
|
||||
if (entry.env?.isNotEmpty() == true) {
|
||||
showInstallForm = !showInstallForm
|
||||
} else {
|
||||
viewModel.installCatalogEntry(entry)
|
||||
}
|
||||
},
|
||||
)
|
||||
enabled = !isInstalling,
|
||||
) {
|
||||
Text(
|
||||
if (isInstalling) {
|
||||
stringResource(R.string.mcp_servers_catalog_installing)
|
||||
} else {
|
||||
stringResource(R.string.mcp_servers_catalog_install)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +178,7 @@ fun CatalogEntryCard(
|
||||
}
|
||||
|
||||
// Install form with env vars
|
||||
AnimatedVisibility(visible = showInstallForm) {
|
||||
AnimatedVisibility(visible = showInstallForm && !isInstalled) {
|
||||
Column(modifier = Modifier.padding(top = spacing.sm)) {
|
||||
entry.env?.let { envVars ->
|
||||
if (envVars.isNotEmpty()) {
|
||||
|
||||
@@ -151,7 +151,7 @@ fun ServerCard(
|
||||
|
||||
// Tool count & schema token overhead badge (issue #1029)
|
||||
val toolInfos = testResult?.tools
|
||||
val toolCount = toolInfos?.size ?: server.tools?.size
|
||||
val toolCount = toolInfos?.size ?: server.toolCount
|
||||
if (toolCount != null && toolCount > 0) {
|
||||
val tokenEst = toolInfos?.let { McpTokenEstimator.estimateTokens(it) }
|
||||
val label = McpTokenEstimator.formatTokenOverhead(toolCount, tokenEst)
|
||||
|
||||
Reference in new issue
Block a user