From c799141fdc22a2c8ce6f31096a4d037de463881b Mon Sep 17 00:00:00 2001 From: "Daniel E." Date: Thu, 26 Feb 2026 20:41:39 -0500 Subject: [PATCH] feat(admin): add separate admin terminal toggle - Add ENABLE_ADMIN_TERMINAL gate for /admin/terminal mount - Disable Terminal nav button unless terminal toggle is enabled - Expose terminal availability from /admin/environment - Document new env var and set Docker default to false --- Dockerfile | 4 +++- README.md | 7 ++++++- public/script.js | 25 +++++++++++++++++++++++-- src/sse.ts | 15 ++++++++++++--- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index d8c0cdd..071c3e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -122,6 +122,8 @@ ENV PORT=3663 # ENV MCP_PROXY_SSE_ALLOWED_KEYS="" # Optional: Enable Admin Web UI (set to "true" to enable) ENV ENABLE_ADMIN_UI=false +# Optional: Enable Admin terminal routes (/admin/terminal). Keep disabled unless needed. +ENV ENABLE_ADMIN_TERMINAL=false # Optional: Admin UI Credentials (required if ENABLE_ADMIN_UI=true) # It's recommended to set these via `docker run -e` instead of hardcoding here @@ -146,4 +148,4 @@ EXPOSE 3663 # By not specifying ENTRYPOINT or CMD here, we rely on the base image's defaults when built as an addon. # For standalone builds, users will need to specify the command when running the container, # e.g., docker run tini -- node build/sse.js -# Or, a multi-stage build could define a specific entrypoint/cmd for the standalone target. \ No newline at end of file +# Or, a multi-stage build could define a specific entrypoint/cmd for the standalone target. diff --git a/README.md b/README.md index 548cf96..24f7b6d 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,11 @@ Example `config/tool_config.json`: ```bash export ENABLE_ADMIN_UI=true ``` +- **`ENABLE_ADMIN_TERMINAL`**: (Optional, recommended) Set to `true` to enable `/admin/terminal` routes in the Admin UI. Default: `false`. + - Keep this disabled unless you explicitly need web terminal access. + ```bash + export ENABLE_ADMIN_TERMINAL=false + ``` - **`ADMIN_USERNAME`**: (Required if Admin UI enabled) Username for Admin UI login. Default: `admin`. - **`ADMIN_PASSWORD`**: (Required if Admin UI enabled) Password for Admin UI login. Default: `password` (**Change this!**). ```bash @@ -580,4 +585,4 @@ This script wraps the execution of the built server (`build/index.js`) with the ## Reference -This project was originally inspired by and refactored from [adamwattis/mcp-proxy-server](https://github.com/adamwattis/mcp-proxy-server). \ No newline at end of file +This project was originally inspired by and refactored from [adamwattis/mcp-proxy-server](https://github.com/adamwattis/mcp-proxy-server). diff --git a/public/script.js b/public/script.js index 7184215..3670c07 100644 --- a/public/script.js +++ b/public/script.js @@ -5,6 +5,7 @@ var discoveredTools = []; var toolDataLoaded = false; var adminEventSource = null; // This is the local variable for the current EventSource instance var effectiveToolsFolder = 'tools'; // Default value if not fetched or empty +var enableAdminTerminal = false; window.effectiveToolsFolder = effectiveToolsFolder; // Expose globally window.adminEventSource = null; // Expose adminEventSource globally from the start and keep it as a data property window.isServerConfigDirty = false; // Initialize and expose globally @@ -152,6 +153,17 @@ window.connectAdminSSE = connectAdminSSE; window.appendToInstallOutput = appendToInstallOutput; window.getInstallOutputElement = getInstallOutputElement; +function applyTerminalNavState() { + if (!navTerminalButton) return; + if (enableAdminTerminal) { + navTerminalButton.disabled = false; + navTerminalButton.title = ''; + } else { + navTerminalButton.disabled = true; + navTerminalButton.title = 'Terminal is disabled (set ENABLE_ADMIN_TERMINAL=true).'; + } +} + const showSection = (sectionId) => { document.querySelectorAll('.admin-section').forEach(section => { section.style.display = 'none'; @@ -200,15 +212,19 @@ const handleLoginSuccess = async () => { if (envResponse.ok) { const envData = await envResponse.json(); window.effectiveToolsFolder = (envData.toolsFolder && envData.toolsFolder.trim() !== '') ? envData.toolsFolder.trim() : 'tools'; + enableAdminTerminal = envData.enableAdminTerminal === true; console.log("Effective TOOLS_FOLDER set to:", window.effectiveToolsFolder); } else { console.warn("Failed to fetch environment info, defaulting effectiveToolsFolder to 'tools'."); window.effectiveToolsFolder = 'tools'; + enableAdminTerminal = false; } } catch (err) { console.error("Error fetching environment info (TOOLS_FOLDER):", err); window.effectiveToolsFolder = 'tools'; + enableAdminTerminal = false; } + applyTerminalNavState(); showSection('servers-section'); if (typeof loadServerConfig === 'function') { @@ -238,6 +254,8 @@ const handleLogoutSuccess = () => { const serverList = document.getElementById('server-list'); if (serverList) serverList.innerHTML = ''; const toolList = document.getElementById('tool-list'); if (toolList) toolList.innerHTML = ''; currentServerConfig = {}; currentToolConfig = { tools: {} }; discoveredTools = []; toolDataLoaded = false; + enableAdminTerminal = false; + applyTerminalNavState(); loginError.textContent = ''; if (adminEventSource) { adminEventSource.close(); @@ -338,7 +356,10 @@ document.addEventListener('DOMContentLoaded', () => { else if (typeof loadToolData !== 'function') console.error("loadToolData not found."); }); } - if (navTerminalButton) navTerminalButton.addEventListener('click', () => window.location.href = 'terminal.html'); + if (navTerminalButton) navTerminalButton.addEventListener('click', () => { + if (!enableAdminTerminal) return; + window.location.href = 'terminal.html'; + }); if (logoutButton) { logoutButton.addEventListener('click', async () => { try { @@ -434,4 +455,4 @@ document.addEventListener('DOMContentLoaded', () => { }); -console.log("script.js loaded and initialized."); \ No newline at end of file +console.log("script.js loaded and initialized."); diff --git a/src/sse.ts b/src/sse.ts index 22b7307..9584a29 100644 --- a/src/sse.ts +++ b/src/sse.ts @@ -71,6 +71,9 @@ const rawEnableAdminUI = process.env.ENABLE_ADMIN_UI; // Enable Admin UI if ENABLE_ADMIN_UI is 'true' (case-insensitive), '1', or 'yes' (case-insensitive). // Defaults to false if not set, empty, or any other value. const enableAdminUI = typeof rawEnableAdminUI === 'string' && (rawEnableAdminUI.toLowerCase() === 'true' || rawEnableAdminUI === '1' || rawEnableAdminUI.toLowerCase() === 'yes'); +// Separately control terminal exposure inside Admin UI. +const rawEnableAdminTerminal = process.env.ENABLE_ADMIN_TERMINAL; +const enableAdminTerminal = typeof rawEnableAdminTerminal === 'string' && (rawEnableAdminTerminal.toLowerCase() === 'true' || rawEnableAdminTerminal === '1' || rawEnableAdminTerminal.toLowerCase() === 'yes'); async function getSessionSecret(): Promise { if (SESSION_SECRET_ENV && SESSION_SECRET_ENV !== 'unsafe-default-secret' && SESSION_SECRET_ENV.trim() !== '') { @@ -283,7 +286,8 @@ if (enableAdminUI) { const config = await loadConfig(); res.json({ toolsFolder: process.env.TOOLS_FOLDER || "", - serverToolnameSeparator: config.serverToolnameSeparator // Expose the separator + serverToolnameSeparator: config.serverToolnameSeparator, // Expose the separator + enableAdminTerminal: enableAdminTerminal }); } catch (error: any) { logger.error("Error fetching environment info for admin UI:", error); @@ -511,8 +515,13 @@ if (enableAdminUI) { }); }); - // Mount the terminal router under /admin/terminal, protected by authentication - app.use('/admin/terminal', isAuthenticated, terminalRouter); + if (enableAdminTerminal) { + logger.log("Admin terminal is ENABLED."); + // Mount the terminal router under /admin/terminal, protected by authentication + app.use('/admin/terminal', isAuthenticated, terminalRouter); + } else { + logger.log("Admin terminal is DISABLED. Set ENABLE_ADMIN_TERMINAL=true to enable."); + } // Static file serving for admin UI should also be inside the if block