From ab62d4dbf20e78139dc69f473c355aa7b0b64a50 Mon Sep 17 00:00:00 2001 From: Keval Mahajan <65884586+kevalmahajan@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:25:30 +0530 Subject: [PATCH 01/21] Persisting checkbox state (#329) * persist show_inactive state after activate/inactive * make ruff,flake8,black Signed-off-by: Keval Mahajan * delete call persists the show_inactive state Signed-off-by: Keval Mahajan * delete, edit, add transactions support persistance of show_inactive checkbox Signed-off-by: Keval Mahajan * make black, ruff, flake8 Signed-off-by: Keval Mahajan --------- Signed-off-by: Keval Mahajan --- mcpgateway/admin.py | 73 ++++++++++++++++++++ mcpgateway/static/admin.js | 118 +++++++++++++++++++++++++++++++- mcpgateway/templates/admin.html | 32 ++++++--- 3 files changed, 213 insertions(+), 10 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 8f16c867f..5dafe7c53 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -154,8 +154,10 @@ async def admin_add_server(request: Request, db: Session = Depends(get_db), user RedirectResponse: A redirect to the admin dashboard catalog section """ form = await request.form() + is_inactive_checked = form.get("is_inactive_checked", "false") try: logger.debug(f"User {user} is adding a new server with name: {form['name']}") + server = ServerCreate( name=form.get("name"), description=form.get("description"), @@ -167,11 +169,15 @@ async def admin_add_server(request: Request, db: Session = Depends(get_db), user await server_service.register_server(db, server) root_path = request.scope.get("root_path", "") + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303) return RedirectResponse(f"{root_path}/admin#catalog", status_code=303) except Exception as e: logger.error(f"Error adding server: {e}") root_path = request.scope.get("root_path", "") + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303) return RedirectResponse(f"{root_path}/admin#catalog", status_code=303) @@ -207,6 +213,7 @@ async def admin_edit_server( RedirectResponse: A redirect to the admin dashboard catalog section with a status code of 303 """ form = await request.form() + is_inactive_checked = form.get("is_inactive_checked", "false") try: logger.debug(f"User {user} is editing server ID {server_id} with name: {form.get('name')}") server = ServerUpdate( @@ -220,11 +227,16 @@ async def admin_edit_server( await server_service.update_server(db, server_id, server) root_path = request.scope.get("root_path", "") + + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303) return RedirectResponse(f"{root_path}/admin#catalog", status_code=303) except Exception as e: logger.error(f"Error editing server: {e}") root_path = request.scope.get("root_path", "") + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303) return RedirectResponse(f"{root_path}/admin#catalog", status_code=303) @@ -256,12 +268,15 @@ async def admin_toggle_server( form = await request.form() logger.debug(f"User {user} is toggling server ID {server_id} with activate: {form.get('activate')}") activate = form.get("activate", "true").lower() == "true" + is_inactive_checked = form.get("is_inactive_checked", "false") try: await server_service.toggle_server_status(db, server_id, activate) except Exception as e: logger.error(f"Error toggling server status: {e}") root_path = request.scope.get("root_path", "") + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303) return RedirectResponse(f"{root_path}/admin#catalog", status_code=303) @@ -289,7 +304,12 @@ async def admin_delete_server(server_id: str, request: Request, db: Session = De except Exception as e: logger.error(f"Error deleting server: {e}") + form = await request.form() + is_inactive_checked = form.get("is_inactive_checked", "false") root_path = request.scope.get("root_path", "") + + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303) return RedirectResponse(f"{root_path}/admin#catalog", status_code=303) @@ -398,12 +418,16 @@ async def admin_toggle_gateway( logger.debug(f"User {user} is toggling gateway ID {gateway_id}") form = await request.form() activate = form.get("activate", "true").lower() == "true" + is_inactive_checked = form.get("is_inactive_checked", "false") + try: await gateway_service.toggle_gateway_status(db, gateway_id, activate) except Exception as e: logger.error(f"Error toggling gateway status: {e}") root_path = request.scope.get("root_path", "") + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#gateways", status_code=303) return RedirectResponse(f"{root_path}/admin#gateways", status_code=303) @@ -650,6 +674,9 @@ async def admin_edit_tool( await tool_service.update_tool(db, tool_id, tool) root_path = request.scope.get("root_path", "") + is_inactive_checked = form.get("is_inactive_checked", "false") + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#tools", status_code=303) return RedirectResponse(f"{root_path}/admin#tools", status_code=303) except ToolNameConflictError as e: return JSONResponse(content={"message": str(e), "success": False}, status_code=400) @@ -679,7 +706,12 @@ async def admin_delete_tool(tool_id: str, request: Request, db: Session = Depend logger.debug(f"User {user} is deleting tool ID {tool_id}") await tool_service.delete_tool(db, tool_id) + form = await request.form() + is_inactive_checked = form.get("is_inactive_checked", "false") root_path = request.scope.get("root_path", "") + + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#tools", status_code=303) return RedirectResponse(f"{root_path}/admin#tools", status_code=303) @@ -711,12 +743,15 @@ async def admin_toggle_tool( logger.debug(f"User {user} is toggling tool ID {tool_id}") form = await request.form() activate = form.get("activate", "true").lower() == "true" + is_inactive_checked = form.get("is_inactive_checked", "false") try: await tool_service.toggle_tool_status(db, tool_id, activate, reachable=activate) except Exception as e: logger.error(f"Error toggling tool status: {e}") root_path = request.scope.get("root_path", "") + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#tools", status_code=303) return RedirectResponse(f"{root_path}/admin#tools", status_code=303) @@ -825,6 +860,10 @@ async def admin_edit_gateway( await gateway_service.update_gateway(db, gateway_id, gateway) root_path = request.scope.get("root_path", "") + is_inactive_checked = form.get("is_inactive_checked", "false") + + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#gateways", status_code=303) return RedirectResponse(f"{root_path}/admin#gateways", status_code=303) @@ -850,7 +889,12 @@ async def admin_delete_gateway(gateway_id: str, request: Request, db: Session = logger.debug(f"User {user} is deleting gateway ID {gateway_id}") await gateway_service.delete_gateway(db, gateway_id) + form = await request.form() + is_inactive_checked = form.get("is_inactive_checked", "false") root_path = request.scope.get("root_path", "") + + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#gateways", status_code=303) return RedirectResponse(f"{root_path}/admin#gateways", status_code=303) @@ -942,6 +986,10 @@ async def admin_edit_resource( await resource_service.update_resource(db, uri, resource) root_path = request.scope.get("root_path", "") + is_inactive_checked = form.get("is_inactive_checked", "false") + + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#resources", status_code=303) return RedirectResponse(f"{root_path}/admin#resources", status_code=303) @@ -967,7 +1015,12 @@ async def admin_delete_resource(uri: str, request: Request, db: Session = Depend logger.debug(f"User {user} is deleting resource URI {uri}") await resource_service.delete_resource(db, uri) + form = await request.form() + is_inactive_checked = form.get("is_inactive_checked", "false") root_path = request.scope.get("root_path", "") + + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#resources", status_code=303) return RedirectResponse(f"{root_path}/admin#resources", status_code=303) @@ -999,12 +1052,15 @@ async def admin_toggle_resource( logger.debug(f"User {user} is toggling resource ID {resource_id}") form = await request.form() activate = form.get("activate", "true").lower() == "true" + is_inactive_checked = form.get("is_inactive_checked", "false") try: await resource_service.toggle_resource_status(db, resource_id, activate) except Exception as e: logger.error(f"Error toggling resource status: {e}") root_path = request.scope.get("root_path", "") + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#resources", status_code=303) return RedirectResponse(f"{root_path}/admin#resources", status_code=303) @@ -1098,6 +1154,10 @@ async def admin_edit_prompt( await prompt_service.update_prompt(db, name, prompt) root_path = request.scope.get("root_path", "") + is_inactive_checked = form.get("is_inactive_checked", "false") + + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#prompts", status_code=303) return RedirectResponse(f"{root_path}/admin#prompts", status_code=303) @@ -1123,7 +1183,12 @@ async def admin_delete_prompt(name: str, request: Request, db: Session = Depends logger.debug(f"User {user} is deleting prompt name {name}") await prompt_service.delete_prompt(db, name) + form = await request.form() + is_inactive_checked = form.get("is_inactive_checked", "false") root_path = request.scope.get("root_path", "") + + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#prompts", status_code=303) return RedirectResponse(f"{root_path}/admin#prompts", status_code=303) @@ -1155,12 +1220,15 @@ async def admin_toggle_prompt( logger.debug(f"User {user} is toggling prompt ID {prompt_id}") form = await request.form() activate = form.get("activate", "true").lower() == "true" + is_inactive_checked = form.get("is_inactive_checked", "false") try: await prompt_service.toggle_prompt_status(db, prompt_id, activate) except Exception as e: logger.error(f"Error toggling prompt status: {e}") root_path = request.scope.get("root_path", "") + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#prompts", status_code=303) return RedirectResponse(f"{root_path}/admin#prompts", status_code=303) @@ -1210,7 +1278,12 @@ async def admin_delete_root(uri: str, request: Request, user: str = Depends(requ logger.debug(f"User {user} is deleting root URI {uri}") await root_service.remove_root(uri) + form = await request.form() root_path = request.scope.get("root_path", "") + is_inactive_checked = form.get("is_inactive_checked", "false") + + if is_inactive_checked.lower() == "true": + return RedirectResponse(f"{root_path}/admin/?include_inactive=true#roots", status_code=303) return RedirectResponse(f"{root_path}/admin#roots", status_code=303) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 0ee9ffde5..57e68053e 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -162,6 +162,9 @@ document.addEventListener("DOMContentLoaded", function () { status.textContent = ""; status.classList.remove("error-status"); + const is_inactive_checked = isInactiveChecked('gateways'); + formData.append("is_inactive_checked", is_inactive_checked); + try { const response = await fetch(`${window.ROOT_PATH}/admin/gateways`, { method: "POST", @@ -172,7 +175,11 @@ document.addEventListener("DOMContentLoaded", function () { if (!result.success) { alert(result.message || "An error occurred"); } else { + if (is_inactive_checked) { + window.location.href = `${window.ROOT_PATH}/admin?include_inactive=true#gateways`; // Redirect on success + } else{ window.location.href = `${window.ROOT_PATH}/admin#gateways`; // Redirect on success + } } } catch (error) { @@ -294,6 +301,8 @@ document.addEventListener("DOMContentLoaded", function () { } let formData = new FormData(this); + const is_inactive_checked = isInactiveChecked('tools'); + formData.append("is_inactive_checked", is_inactive_checked); try { let response = await fetch(`${window.ROOT_PATH}/admin/tools`, { method: "POST", @@ -303,7 +312,11 @@ document.addEventListener("DOMContentLoaded", function () { if (!result.success) { alert(result.message || "An error occurred"); } else { - window.location.href = `${window.ROOT_PATH}/admin#tools`; // Redirect on success + if (is_inactive_checked) { + window.location.href = `${window.ROOT_PATH}/admin?include_inactive=true#tools`; // Redirect on success + } else{ + window.location.href = `${window.ROOT_PATH}/admin#tools`; // Redirect on success + } } } catch (error) { console.error("Fetch error:", error); @@ -532,6 +545,50 @@ function toggleInactiveItems(type) { window.location = url; } +// Function to check if the "Show Inactive" checkbox is checked +function isInactiveChecked(type) { + const checkbox = document.getElementById(`show-inactive-${type}`); + if (checkbox.checked) { + return true; + } else { + return false; + } +} + +function handleToggleSubmit(event, type) { + // Prevent form from submitting immediately + event.preventDefault(); + + // Get the value of 'is_inactive_checked' from the function + const is_inactive_checked = isInactiveChecked(type); + + // Dynamically add the 'is_inactive_checked' value to the form + const form = event.target; + const hiddenField = document.createElement('input'); + hiddenField.type = 'hidden'; + hiddenField.name = 'is_inactive_checked'; + hiddenField.value = is_inactive_checked; + + form.appendChild(hiddenField); + + // Now submit the form + form.submit(); +} + +function handleSubmitWithConfirmation(event, type) { + event.preventDefault(); + + const confirmationMessage = `Are you sure you want to permanently delete this ${type}? (Deactivation is reversible, deletion is permanent)`; + const confirmation = confirm(confirmationMessage); + if (!confirmation) { + return false; // Prevent form submission + } + + return handleToggleSubmit(event, type); // Proceed with your original function +} + + + // Tool CRUD operations /** * Fetches detailed tool information from the backend and renders all properties, @@ -714,6 +771,17 @@ async function editTool(toolId) { const response = await fetch(`${window.ROOT_PATH}/admin/tools/${toolId}`); const tool = await response.json(); + const isInActiveCheckedBool = isInactiveChecked('tools'); + let hiddenField = document.getElementById("edit-show-inactive"); + if (!hiddenField) { + hiddenField = document.createElement("input"); + hiddenField.type = "hidden"; + hiddenField.name = "is_inactive_checked"; + hiddenField.id = "edit-show-inactive"; + document.getElementById("edit-tool-form").appendChild(hiddenField); + } + hiddenField.value = isInActiveCheckedBool; + // Set form action and populate basic fields. document.getElementById("edit-tool-form").action = `${window.ROOT_PATH}/admin/tools/${toolId}/edit`; @@ -866,6 +934,17 @@ async function editResource(resourceUri) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); + const isInActiveCheckedBool = isInactiveChecked('resources'); + let hiddenField = document.getElementById("edit-show-inactive"); + if (!hiddenField) { + hiddenField = document.createElement("input"); + hiddenField.type = "hidden"; + hiddenField.name = "is_inactive_checked"; + hiddenField.id = "edit-show-inactive"; + document.getElementById("edit-resource-form").appendChild(hiddenField); + } + hiddenField.value = isInActiveCheckedBool; + const resource = data.resource; // Set the form action for editing document.getElementById("edit-resource-form").action = @@ -953,6 +1032,18 @@ async function editPrompt(promptName) { `${window.ROOT_PATH}/admin/prompts/${encodeURIComponent(promptName)}`, ); const prompt = await response.json(); + + const isInActiveCheckedBool = isInactiveChecked('resources'); + let hiddenField = document.getElementById("edit-show-inactive"); + if (!hiddenField) { + hiddenField = document.createElement("input"); + hiddenField.type = "hidden"; + hiddenField.name = "is_inactive_checked"; + hiddenField.id = "edit-show-inactive"; + document.getElementById("edit-prompt-form").appendChild(hiddenField); + } + hiddenField.value = isInActiveCheckedBool; + document.getElementById("edit-prompt-form").action = `${window.ROOT_PATH}/admin/prompts/${encodeURIComponent(promptName)}/edit`; document.getElementById("edit-prompt-name").value = prompt.name; @@ -1059,6 +1150,18 @@ async function editGateway(gatewayId) { try { const response = await fetch(`${window.ROOT_PATH}/admin/gateways/${gatewayId}`); const gateway = await response.json(); + + const isInActiveCheckedBool = isInactiveChecked('gateways'); + let hiddenField = document.getElementById("edit-show-inactive"); + if (!hiddenField) { + hiddenField = document.createElement("input"); + hiddenField.type = "hidden"; + hiddenField.name = "is_inactive_checked"; + hiddenField.id = "edit-show-inactive"; + document.getElementById("edit-gateway-form").appendChild(hiddenField); + } + hiddenField.value = isInActiveCheckedBool; + document.getElementById("edit-gateway-form").action = `${window.ROOT_PATH}/admin/gateways/${gatewayId}/edit`; document.getElementById("edit-gateway-name").value = gateway.name; @@ -1176,6 +1279,17 @@ async function editServer(serverId) { throw new Error(`HTTP error! status: ${response.status}`); } const server = await response.json(); + + const isInActiveCheckedBool = isInactiveChecked('servers'); + let hiddenField = document.getElementById("edit-show-inactive"); + if (!hiddenField) { + hiddenField = document.createElement("input"); + hiddenField.type = "hidden"; + hiddenField.name = "is_inactive_checked"; + hiddenField.id = "edit-show-inactive"; + document.getElementById("edit-server-form").appendChild(hiddenField); + } + hiddenField.value = isInActiveCheckedBool; // Set the form action for editing document.getElementById("edit-server-form").action = `${window.ROOT_PATH}/admin/servers/${serverId}/edit`; @@ -2007,6 +2121,8 @@ document.addEventListener("DOMContentLoaded", () => { }); window.toggleInactiveItems = toggleInactiveItems; +window.handleToggleSubmit = handleToggleSubmit; +window.handleSubmitWithConfirmation = handleSubmitWithConfirmation; window.viewTool = viewTool; window.editTool = editTool; window.testTool = testTool; diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index b2d0b8ffc..1710da029 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -270,6 +270,7 @@

MCP Servers Catalog

action="{{ root_path }}/admin/servers/{{ server.id }}/toggle" class="inline mr-2" title="Deactivate: This will temporarily disable the server without deleting it." + onsubmit="return handleToggleSubmit(event, 'servers')" > {% if gateway.enabled %} Gateway Details + + +
Date: Thu, 10 Jul 2025 04:45:58 +0100 Subject: [PATCH 07/21] Add proper HTML escaping for admin UI user data rendering, and eliminate all web lint issues closes #336 #338 (#337) * Initial validation and XSS protection for UI Signed-off-by: Mihai Criveti * Race condition UI fix Signed-off-by: Mihai Criveti * Full lint compliance for web stack Signed-off-by: Mihai Criveti * Full lint compliance for web stack and fixed metrics tab Signed-off-by: Mihai Criveti * Full lint compliance for web stack and fixed metrics tab Signed-off-by: Mihai Criveti * Full lint compliance for web stack and fixed metrics tab Signed-off-by: Mihai Criveti * Don't show full json Signed-off-by: Mihai Criveti * Cleanup escape issues Signed-off-by: Mihai Criveti --------- Signed-off-by: Mihai Criveti --- .bumpversion.cfg | 4 +- .env.example | 4 +- .eslintrc.json | 19 +- SECURITY.md | 67 +- docs/docs/architecture/roadmap.md | 2 +- docs/requirements.txt | 2 +- mcpgateway/main.py | 4 +- mcpgateway/static/admin.css | 24 +- mcpgateway/static/admin.js | 6389 +++++++++++------ mcpgateway/templates/admin.html | 3598 ++++------ .../templates/version_info_partial.html | 269 +- package-lock.json | 3706 +++++++++- package.json | 10 + .../mcpgateway/services/test_tool_service.py | 2 +- 14 files changed, 9426 insertions(+), 4674 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 205a7a5b2..e58d0c696 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -5,8 +5,8 @@ tag = False sign-tags = True tag_name = v{new_version} # tag format (only used if you flip tag=True later) parse = (?P\d+)\.(?P\d+)\.(?P\d+) -serialize = - {major}.{minor}.{patch} +serialize = + {major}.{minor}.{patch} [bumpversion:file:mcpgateway/__init__.py] search = __version__ = "{current_version}" diff --git a/.env.example b/.env.example index 85293bd42..11d3f2235 100644 --- a/.env.example +++ b/.env.example @@ -106,10 +106,10 @@ AUTH_ENCRYPTION_SECRET=my-test-salt ##################################### # Enable the visual Admin UI (true/false) -MCPGATEWAY_UI_ENABLED=true +MCPGATEWAY_UI_ENABLED=false # Enable the Admin API endpoints (true/false) -MCPGATEWAY_ADMIN_API_ENABLED=true +MCPGATEWAY_ADMIN_API_ENABLED=false ##################################### # Security and CORS diff --git a/.eslintrc.json b/.eslintrc.json index a80fce32a..5794a1ffb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,16 +1,27 @@ { "root": true, + "env": { "browser": true, + "node": true, "es2021": true }, - "extends": ["standard"], + "parserOptions": { - "ecmaVersion": 12, + "ecmaVersion": "latest", "sourceType": "module" }, + + "extends": [ + "standard", + "plugin:prettier/recommended" + ], + "rules": { - "semi": ["error", "always"], - "quotes": ["error", "double"] + "semi": ["error", "always"], + "quotes": ["error", "double", { "avoidEscape": true }], + + "curly": ["error", "all"], + "prefer-const": "warn" } } diff --git a/SECURITY.md b/SECURITY.md index 104e45112..60dc0911f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,7 +13,7 @@ Here's an expanded section for that part: **Tools are not enough**: While our automated security tooling provides comprehensive coverage, we recognize that true security requires human expertise and collaborative oversight. Our security posture extends beyond automated scanning to include: - **Manual Security Code Reviews**: Expert security engineers conduct thorough code reviews focusing on logic flaws, business logic vulnerabilities, and complex attack vectors that automated tools might miss -- **Threat Modeling & Risk Assessment**: Regular security assessments evaluate our attack surface, identify potential threat vectors, and validate our defense mechanisms against real-world attack scenarios +- **Threat Modeling & Risk Assessment**: Regular security assessments evaluate our attack surface, identify potential threat vectors, and validate our defense mechanisms against real-world attack scenarios - **Community-Driven Security**: We actively engage with the security research community, maintain responsible disclosure processes, and leverage collective intelligence to identify and address emerging threats - **Security Champion Program**: Developers across the project receive security training and act as security advocates within their teams, creating a culture of security awareness - **Penetration Testing**: Regular security assessments by internal and external security professionals validate our defenses against sophisticated attack techniques @@ -85,26 +85,26 @@ The following diagram illustrates our comprehensive security scanning pipeline: ```mermaid flowchart TD A[Code Changes] --> B{Pre-commit Hooks} - + B --> C[Ruff - Python Linter/Formatter] B --> D[Black - Code Formatter] B --> E[isort - Import Sorter] B --> F[mypy - Type Checking] B --> G[Bandit - Security Scanner] - + C --> H[Pre-commit Success?] D --> H E --> H F --> H G --> H - + H -->|No| I[Fix Issues & Retry] I --> B - + H -->|Yes| J[Push to GitHub] - + J --> K[GitHub Actions Triggers] - + K --> L[Python Package Build] K --> M[CodeQL Analysis] K --> N[Bandit Security Scan] @@ -113,29 +113,29 @@ flowchart TD K --> Q[Lint & Static Analysis] K --> R[Docker Image Build] K --> S[Container Security Scan] - + L --> L1[Python Build Test] L --> L2[Package Installation Test] - + M --> M1[Semantic Code Analysis] M --> M2[Security Vulnerability Detection] M --> M3[Data Flow Analysis] - + N --> N1[Security Issue Detection] N --> N2[Common Security Patterns] N --> N3[Hardcoded Secrets Check] - + O --> O1[Dependency Vulnerability Check] O --> O2[License Compliance] O --> O3[Supply Chain Security] - + P --> P1[pytest Unit Tests] P --> P2[Coverage Analysis] P --> P3[Integration Tests] - + Q --> Q1[Multiple Linters] Q --> Q2[Static Analysis Tools] - + Q1 --> Q1A[flake8 - PEP8 Compliance] Q1 --> Q1B[pylint - Code Quality] Q1 --> Q1C[pycodestyle - Style Guide] @@ -144,7 +144,7 @@ flowchart TD Q1 --> Q1F[yamllint - YAML Files] Q1 --> Q1G[jsonlint - JSON Files] Q1 --> Q1H[tomllint - TOML Files] - + Q2 --> Q2A[mypy - Type Checking] Q2 --> Q2B[pyright - Type Analysis] Q2 --> Q2C[pytype - Google Type Checker] @@ -153,77 +153,77 @@ flowchart TD Q2 --> Q2F[importchecker - Import Analysis] Q2 --> Q2G[fawltydeps - Dependency Analysis] Q2 --> Q2H[check-manifest - Package Completeness] - + R --> R1[Docker Build] R --> R2[Multi-stage Build Process] R --> R3[Security Hardening] - + S --> S1[Hadolint - Dockerfile Linting] S --> S2[Dockle - Container Security] S --> S3[Trivy - Vulnerability Scanner] S --> S4[OSV-Scanner - Open Source Vulns] - + T[Local Development] --> U[Make Targets] - + U --> V[make lint - Full Lint Suite] U --> W[Individual Security Tools] U --> X[make sbom - Software Bill of Materials] U --> Y[make lint-web - Frontend Security] - + V --> V1[All Python Linters] V --> V2[Code Quality Checks] V --> V3[Style Enforcement] - + W --> W1[make bandit - Security Scanner] W --> W2[make osv-scan - Vulnerability Check] W --> W3[make trivy - Container Security] W --> W4[make dockle - Image Analysis] W --> W5[make hadolint - Dockerfile Linting] W --> W6[make pip-audit - Dependency Scanning] - + X --> X1[CycloneDX SBOM Generation] X --> X2[Dependency Inventory] X --> X3[License Compliance Check] X --> X4[Vulnerability Assessment] - + Y --> Y1[htmlhint - HTML Validation] Y --> Y2[stylelint - CSS Security] Y --> Y3[eslint - JavaScript Security] Y --> Y4[retire.js - JS Library Vulnerabilities] Y --> Y5[npm audit - Package Vulnerabilities] - + Z[Additional Security Tools] --> Z1[SonarQube Analysis] Z --> Z2[WhiteSource Security Scanning] Z --> Z3[Spellcheck - Documentation] Z --> Z4[Pre-commit Hook Validation] - + AA[Container Security Pipeline] --> AA1[Multi-stage Build] AA --> AA2[Minimal Base Images] AA --> AA3[Security Hardening] AA --> AA4[Runtime Security] - + AA1 --> AA1A[Build Dependencies] AA1 --> AA1B[Runtime Dependencies] AA1 --> AA1C[Security Scanning] - + AA2 --> AA2A[UBI Micro Base] AA2 --> AA2B[Minimal Attack Surface] AA2 --> AA2C[No Shell Access] - + AA3 --> AA3A[Non-root User] AA3 --> AA3B[Read-only Filesystem] AA3 --> AA3C[Capability Dropping] - + AA4 --> AA4A[Runtime Monitoring] AA4 --> AA4B[Security Policies] AA4 --> AA4C[Vulnerability Patching] - + classDef security fill:#ff6b6b,stroke:#d63031,stroke-width:2px classDef linting fill:#74b9ff,stroke:#0984e3,stroke-width:2px classDef container fill:#00b894,stroke:#00a085,stroke-width:2px classDef process fill:#fdcb6e,stroke:#e17055,stroke-width:2px classDef success fill:#55a3ff,stroke:#2d3436,stroke-width:2px - + class G,M,N,O,W,W1,W2,W3,W4,Z1,Z2,AA security class C,D,E,F,Q,Q1,Q1A,Q1B,Q1C,Q1D,Q1E,Q1F,Q1G,Q1H,V linting class R,S,S1,S2,S3,S4,AA,AA1,AA2,AA3,AA4 container @@ -238,8 +238,9 @@ flowchart TD ## 📦 Supported Versions and Security Updates All Container Images and Python dependencies are updated with every release (major or minor) or on CRITICAL/HIGH security vulnerabilities (triggering a minor release). - -We currently support only the latest version of this project. Older versions are not maintained or patched. +We currently support only the latest version of this project, and only through the REST API. +Admin UI / APIs are provided for developer convenience and should be disabled in production using the provided feature flags. +Older versions are not maintained or patched. ### Security Patching Policy diff --git a/docs/docs/architecture/roadmap.md b/docs/docs/architecture/roadmap.md index 596131b0e..adbbe02aa 100644 --- a/docs/docs/architecture/roadmap.md +++ b/docs/docs/architecture/roadmap.md @@ -393,4 +393,4 @@ 7. **Chrome MCP Plugin Integration** - Browser extension for managing MCP configurations, servers, and connections ### 🔐 Secrets & Sensitive Data -8. **Secure Secrets Management & Masking** - External secrets store integration (Vault) \ No newline at end of file +8. **Secure Secrets Management & Masking** - External secrets store integration (Vault) diff --git a/docs/requirements.txt b/docs/requirements.txt index 4d787b7e4..d433d8847 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -87,4 +87,4 @@ weasyprint>=65.1 webcolors>=24.11.1 webencodings>=0.5.1 zipp>=3.23.0 -zopfli>=0.2.3.post1 \ No newline at end of file +zopfli>=0.2.3.post1 diff --git a/mcpgateway/main.py b/mcpgateway/main.py index 0a82c22f0..39331eb07 100644 --- a/mcpgateway/main.py +++ b/mcpgateway/main.py @@ -892,7 +892,7 @@ async def server_get_prompts( @tool_router.get("", response_model=Union[List[ToolRead], List[Dict], Dict, List]) @tool_router.get("/", response_model=Union[List[ToolRead], List[Dict], Dict, List]) async def list_tools( - cursor: Optional[str] = None, # Add this parameter + cursor: Optional[str] = None, include_inactive: bool = False, db: Session = Depends(get_db), apijsonpath: JsonPathModifier = Body(None), @@ -1138,7 +1138,7 @@ async def toggle_resource_status( @resource_router.get("", response_model=List[ResourceRead]) @resource_router.get("/", response_model=List[ResourceRead]) async def list_resources( - cursor: Optional[str] = None, # Add this parameter + cursor: Optional[str] = None, include_inactive: bool = False, db: Session = Depends(get_db), user: str = Depends(require_auth), diff --git a/mcpgateway/static/admin.css b/mcpgateway/static/admin.css index 873e3974b..e847241fc 100644 --- a/mcpgateway/static/admin.css +++ b/mcpgateway/static/admin.css @@ -9,7 +9,7 @@ } -/* Add this CSS for the spinner */ +/* CSS for the spinner */ .spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; @@ -17,16 +17,28 @@ width: 24px; height: 24px; animation: spin 1s linear infinite; + /* margin: 10px auto; */ /* Positioning to the left */ - margin: 10px 0 10px 10px; /* top, right, bottom, left */ - display: block; /* Ensures it behaves like a block-level element */ + margin: 10px 0 10px 10px; + + /* top, right, bottom, left */ + display: block; + + /* Ensures it behaves like a block-level element */ } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } -.feedback:blank { display:none; } +.feedback:blank { + display: none; +} diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 354980e49..d228b15fd 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -1,2216 +1,4460 @@ -document.addEventListener("DOMContentLoaded", function () { - const hash = window.location.hash; - if (hash) { - showTab(hash.slice(1)); - } - - document.getElementById("tab-catalog").addEventListener("click", () => { - showTab("catalog"); - }); - document.getElementById("tab-tools").addEventListener("click", () => { - showTab("tools"); - }); - document.getElementById("tab-resources").addEventListener("click", () => { - showTab("resources"); - }); - document.getElementById("tab-prompts").addEventListener("click", () => { - showTab("prompts"); - }); - document.getElementById("tab-gateways").addEventListener("click", () => { - showTab("gateways"); - }); - document.getElementById("tab-roots").addEventListener("click", () => { - showTab("roots"); - }); - document.getElementById("tab-metrics").addEventListener("click", () => { - showTab("metrics"); - }); - document.getElementById("tab-version-info").addEventListener("click", () => { - showTab("version-info"); - }); - - /* ------------------------------------------------------------------ - * Pre-load the "Version & Environment Info" partial once per page - * ------------------------------------------------------------------ */ - /* Pre-load version-info once */ - document.addEventListener("DOMContentLoaded", () => { - const panel = document.getElementById("version-info-panel"); - if (!panel || panel.innerHTML.trim() !== "") return; // already loaded - - fetch(`${window.ROOT_PATH}/version?partial=true`) - .then((response) => { - if (!response.ok) throw new Error("Network response was not ok"); - return response.text(); - }) - .then((html) => { - panel.innerHTML = html; - - // If the page was opened at #version-info, show that tab now - if (window.location.hash === "#version-info") { - showTab("version-info"); - } - }) - .catch((error) => { - console.error("Failed to preload version info:", error); - panel.innerHTML = - "

Failed to load version info.

"; - }); - }); - - /* ------------------------------------------------------------------ - * HTMX debug hooks - * ------------------------------------------------------------------ */ - document.body.addEventListener("htmx:afterSwap", (event) => { - if (event.detail.target.id === "version-info-panel") { - console.log("HTMX: Content swapped into version-info-panel"); - } - }); - - - // HTMX event listeners for debugging - document.body.addEventListener("htmx:beforeRequest", (event) => { - if (event.detail.elt.id === "tab-version-info") { - console.log("HTMX: Sending request for version info partial"); - } - }); - - document.body.addEventListener("htmx:afterSwap", (event) => { - if (event.detail.target.id === "version-info-panel") { - console.log("HTMX: Content swapped into version-info-panel"); - } - }); - - // Authentication toggle - document.getElementById("auth-type").addEventListener("change", function () { - const basicFields = document.getElementById("auth-basic-fields"); - const bearerFields = document.getElementById("auth-bearer-fields"); - const headersFields = document.getElementById("auth-headers-fields"); - handleAuthTypeSelection( - this.value, - basicFields, - bearerFields, - headersFields, - ); - }); - document - .getElementById("auth-type-gw") - .addEventListener("change", function () { - const basicFields = document.getElementById("auth-basic-fields-gw"); - const bearerFields = document.getElementById("auth-bearer-fields-gw"); - const headersFields = document.getElementById("auth-headers-fields-gw"); - handleAuthTypeSelection( - this.value, - basicFields, - bearerFields, - headersFields, - ); - }); - document - .getElementById("auth-type-gw-edit") - .addEventListener("change", function () { - const basicFields = document.getElementById("auth-basic-fields-gw-edit"); - const bearerFields = document.getElementById( - "auth-bearer-fields-gw-edit", - ); - const headersFields = document.getElementById( - "auth-headers-fields-gw-edit", - ); - handleAuthTypeSelection( - this.value, - basicFields, - bearerFields, - headersFields, - ); - }); - document - .getElementById("edit-auth-type") - .addEventListener("change", function () { - const basicFields = document.getElementById("edit-auth-basic-fields"); - const bearerFields = document.getElementById("edit-auth-bearer-fields"); - const headersFields = document.getElementById("edit-auth-headers-fields"); - if (this.value === "basic") { - basicFields.style.display = "block"; - bearerFields.style.display = "none"; - headersFields.style.display = "none"; - } else if (this.value === "bearer") { - basicFields.style.display = "none"; - bearerFields.style.display = "block"; - headersFields.style.display = "none"; - } else if (this.value === "authheaders") { - basicFields.style.display = "none"; - bearerFields.style.display = "none"; - headersFields.style.display = "block"; - } else { - basicFields.style.display = "none"; - bearerFields.style.display = "none"; - headersFields.style.display = "none"; - } - }); +/** + * ==================================================================== + * SECURE ADMIN.JS - COMPLETE VERSION WITH XSS PROTECTION + * ==================================================================== + * + * SECURITY FEATURES: + * - XSS prevention with comprehensive input sanitization + * - Input validation for all form fields + * - Safe DOM manipulation only + * - No innerHTML usage with user data + * - Comprehensive error handling and timeouts + * + * PERFORMANCE FEATURES: + * - Centralized state management + * - Memory leak prevention + * - Proper event listener cleanup + * - Race condition elimination + */ - document.getElementById("add-gateway-form") - .addEventListener("submit", async (e) => { - e.preventDefault(); +// =================================================================== +// SECURITY: HTML-escape function to prevent XSS attacks +// =================================================================== - const form = e.target; - const formData = new FormData(form); +function escapeHtml(unsafe) { + if (unsafe === null || unsafe === undefined) { + return ""; + } + return String(unsafe) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(/`/g, "`") + .replace(/\//g, "/"); // Extra protection against script injection +} + +/** + * SECURITY: Validate input names to prevent XSS and ensure clean data + */ +function validateInputName(name, type = "input") { + if (!name || typeof name !== "string") { + return { valid: false, error: `${type} name is required` }; + } - const status = document.getElementById("status-gateways"); - const loading = document.getElementById("add-gateway-loading"); + // Remove any HTML tags + const cleaned = name.replace(/<[^>]*>/g, ""); + + // Check for dangerous patterns + const dangerousPatterns = [ + / - - - - - - - - - + - -
+ + + + Codestin Search App + + + + + + + + + + + + + + +

MCP Context Forge - Gateway Administration @@ -46,1730 +34,1247 @@

Manage tools, resources, prompts, servers, and federated gateways (remote MCP servers) | - Docs + Docs | - ⭐ Star mcp-context-forge on GitHub + ⭐ Star mcp-context-forge on GitHub

- -
- + + - - + + - -
-
-

MCP Servers Catalog

-

Virtual Servers let you combine Tools, Resources, and Prompts from the global Tools catalog into a reusable configuration.

+ +
+
+

MCP Servers Catalog

+

Virtual Servers let you combine Tools, Resources, and + Prompts from the global Tools catalog into a reusable configuration.

-
- - -
+
+ +
-
-
- - - - - - - - - - - - - - - - {% for server in servers %} - - - - - - - - - - + + {% endfor %} + +
- Icon - - S. No. - - ID - - Name - - Description - - Tools - - Resources - - Prompts - - Actions -
- {% if server.icon %} - {{ server.name }} Icon - {% else %} - N/A - {% endif %} - - {{ loop.index }} - - {{ server.id }} - - {{ server.name }} - - {{ server.description }} - - {% if server.associatedTools %} {{ server.associatedTools | - map('string') | join(', ') }} {% else %} - N/A - {% endif %} - - {% if server.associatedResources %} {{ - server.associatedResources | join(', ') }} {% else %} - N/A - {% endif %} - - {% if server.associatedPrompts %} {{ - server.associatedPrompts | join(', ') }} {% else %} - N/A - {% endif %} - - {% if server.isActive %} -
- - -
- {% else %} -
- - -
- {% endif %} - + +
+ +
+
-
-

Add New Server

-
-
-
- - -
-
- - -
-
- - -
-
- - - - -
-
- - -
-
- - -
+
+
+

Add New Server

+ +
+
+ +
-
- +
+ +
- -
-
- - -