From 228431d57a0ddfaa0fa7694747cc896e61d5d342 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:58:20 +0300 Subject: [PATCH] [enhancement] feat(#606): support auth mode + bearer token when adding HTTP MCP servers (#607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(#606): support auth mode + bearer token when adding HTTP MCP servers - Extend AddMcpServerRequest with auth (none|header|oauth) and bearerToken - Add auth mode FilterChip selector in HTTP add-server dialog - Conditionally show masked bearer token field when auth == header - Wire new fields through McpServersViewModel request builder Backend contract: exactly one of url/command required; bearer_token requires auth == "header"; args/env only for stdio. SnakeCase naming strategy auto-serializes bearerToken -> bearer_token. Fixes #606 * fix(#606): address agy review — remember PasswordVisualTransformation + FlowRow for auth chips - Wrap PasswordVisualTransformation in remember {} to avoid per-recomposition allocation - Use FlowRow for the 3 auth-mode FilterChips so they wrap on narrow screens instead of overflowing --- .../m57/hermescontrol/data/model/McpServer.kt | 2 + .../hermescontrol/ui/mcp/McpServersScreen.kt | 42 +++++++++++++++++++ .../ui/mcp/McpServersViewModel.kt | 24 +++++++++++ app/src/main/res/values/strings.xml | 5 +++ 4 files changed, 73 insertions(+) diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/McpServer.kt b/app/src/main/java/com/m57/hermescontrol/data/model/McpServer.kt index 55d681e0..980bee4a 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/McpServer.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/McpServer.kt @@ -31,6 +31,8 @@ data class AddMcpServerRequest( val command: String? = null, val args: List? = null, val env: Map? = null, + val auth: String? = null, // "none" | "header" | "oauth" + val bearerToken: String? = null, // sent only when auth == "header" ) @Serializable diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt index b7a63b44..b398638f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -28,6 +29,7 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FilterChip import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -51,6 +53,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -256,6 +259,45 @@ private fun AddServerSection( modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), ) + Spacer(modifier = Modifier.height(spacing.sm)) + Text( + text = stringResource(R.string.mcp_servers_auth_mode), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(spacing.xs)) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(spacing.sm), + verticalArrangement = Arrangement.spacedBy(spacing.sm), + ) { + FilterChip( + selected = state.addServerAuth == "none", + onClick = { viewModel.updateAddServerAuth("none") }, + label = { Text(stringResource(R.string.mcp_servers_auth_none)) }, + ) + FilterChip( + selected = state.addServerAuth == "header", + onClick = { viewModel.updateAddServerAuth("header") }, + label = { Text(stringResource(R.string.mcp_servers_auth_header)) }, + ) + FilterChip( + selected = state.addServerAuth == "oauth", + onClick = { viewModel.updateAddServerAuth("oauth") }, + label = { Text(stringResource(R.string.mcp_servers_auth_oauth)) }, + ) + } + if (state.addServerAuth == "header") { + Spacer(modifier = Modifier.height(spacing.sm)) + OutlinedTextField( + value = state.addServerBearerToken, + onValueChange = viewModel::updateAddServerBearerToken, + label = { Text(stringResource(R.string.mcp_servers_field_bearer_token)) }, + singleLine = true, + visualTransformation = remember { PasswordVisualTransformation() }, + modifier = Modifier.fillMaxWidth(), + ) + } } else { OutlinedTextField( value = state.addServerCommand, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt index 888cb0b3..879fea57 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt @@ -34,6 +34,8 @@ data class McpServersUiState( val addServerUrl: String = "", val addServerCommand: String = "", val addServerArgs: String = "", + val addServerAuth: String = "none", // "none" | "header" | "oauth" + val addServerBearerToken: String = "", val addingServer: Boolean = false, // Env vars for editing val editingEnvFor: String? = null, @@ -204,6 +206,14 @@ class McpServersViewModel : _uiState.update { it.copy(addServerArgs = v) } } + fun updateAddServerAuth(v: String) { + _uiState.update { it.copy(addServerAuth = v) } + } + + fun updateAddServerBearerToken(v: String) { + _uiState.update { it.copy(addServerBearerToken = v) } + } + fun submitAddServer() { val state = _uiState.value if (state.addServerName.isBlank()) { @@ -231,6 +241,18 @@ class McpServersViewModel : } 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 = withContext(Dispatchers.IO) { @@ -246,6 +268,8 @@ class McpServersViewModel : addServerUrl = "", addServerCommand = "", addServerArgs = "", + addServerAuth = "none", + addServerBearerToken = "", toastMessage = "Server '${request.name}' added", ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 70b4cad0..edbe9fc6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -727,6 +727,11 @@ Installing… source: %1$s Required env vars + Authentication + None + Header + OAuth + Bearer token GATEWAY RUNNING