From e321d8ee8eb1e67857a2b0c38ba8578511748b96 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 12 Nov 2025 18:23:24 +0530 Subject: [PATCH 01/33] html changes assocaited mcp server Signed-off-by: rakdutta --- mcpgateway/templates/admin.html | 78 +++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index 4e3fd64bc..6ec4b7286 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -2034,6 +2034,84 @@

class="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-900 dark:placeholder-gray-300 dark:text-gray-300" /> +
+ + +
+ {% for gateway in gateways %} + + {% endfor %} + +
+
+ + + +
+ + +
+ + +
+
+ +
From 09faa453dd02177bd0a4c916b6f0dbe017e75d07 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 12 Nov 2025 19:29:12 +0530 Subject: [PATCH 02/33] search gateway Signed-off-by: rakdutta --- mcpgateway/services/server_service.py | 2 + mcpgateway/static/admin.js | 133 ++++++++++++++++++++++++++ mcpgateway/templates/admin.html | 28 +++--- 3 files changed, 149 insertions(+), 14 deletions(-) diff --git a/mcpgateway/services/server_service.py b/mcpgateway/services/server_service.py index 18eafce25..cfcfe24d3 100644 --- a/mcpgateway/services/server_service.py +++ b/mcpgateway/services/server_service.py @@ -264,6 +264,7 @@ def _assemble_associated_items( resources: Optional[List[str]], prompts: Optional[List[str]], a2a_agents: Optional[List[str]] = None, + gateways: Optional[List[str]] = None, ) -> Dict[str, Any]: """ Assemble the associated items dictionary from the separate fields. @@ -304,6 +305,7 @@ def _assemble_associated_items( "resources": resources or [], "prompts": prompts or [], "a2a_agents": a2a_agents or [], + "gateways": gateways or [], } def _get_team_name(self, db: Session, team_id: Optional[str]) -> Optional[str]: diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 7516ec2d2..d4c8d4d44 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7286,6 +7286,136 @@ function initPromptSelect( } } +// =================================================================== +// GATEWAY SELECT (Associated MCP Servers) - search/select/clear +// =================================================================== +function initGatewaySelect() { + try { + const container = document.getElementById('associatedGateways'); + if (!container) return; + + const searchInput = document.getElementById('searchGateways'); + const selectAllBtn = document.getElementById('selectAllGatewayBtn'); + const clearAllBtn = document.getElementById('clearAllGatewayBtn'); + const pillsBox = document.getElementById('selectedGatewayPills'); + const warnBox = document.getElementById('selectedGatewayWarning'); + const noMsg = document.getElementById('noGatewayMessage'); + const searchQuerySpan = document.getElementById('searchQuery'); + + function updatePills() { + try { + const checkboxes = Array.from(container.querySelectorAll('input[type="checkbox"]')); + const checked = checkboxes.filter(cb => cb.checked); + pillsBox.innerHTML = ''; + const maxShow = 6; + checked.slice(0, maxShow).forEach(cb => { + const span = document.createElement('span'); + span.className = 'inline-block bg-indigo-100 text-indigo-800 text-xs px-2 py-1 rounded-full'; + span.textContent = cb.nextElementSibling?.textContent?.trim() || cb.value; + pillsBox.appendChild(span); + }); + if (checked.length > maxShow) { + const span = document.createElement('span'); + span.className = 'inline-block bg-indigo-100 text-indigo-800 text-xs px-2 py-1 rounded-full'; + span.textContent = `+${checked.length - maxShow} more`; + pillsBox.appendChild(span); + } + + // Warning if too many selected + if (checked.length > 12) { + warnBox.textContent = `Selected ${checked.length} MCP servers. Selecting many servers may impact performance.`; + } else { + warnBox.textContent = ''; + } + } catch (e) { + console.error('Error updating gateway pills', e); + } + } + + function applySearch() { + try { + const q = (searchInput && searchInput.value || '').toLowerCase().trim(); + const items = Array.from(container.querySelectorAll('.tool-item')); + let visible = 0; + items.forEach(item => { + const text = (item.textContent || '').toLowerCase(); + if (!q || text.includes(q)) { + item.style.display = ''; + visible += 1; + } else { + item.style.display = 'none'; + } + }); + + if (noMsg) { + if (q && visible === 0) { + noMsg.style.display = 'block'; + if (searchQuerySpan) searchQuerySpan.textContent = q; + } else { + noMsg.style.display = 'none'; + } + } + } catch (e) { + console.error('Error applying gateway search', e); + } + } + + // Bind search + if (searchInput && !searchInput._gateway_bound) { + searchInput.addEventListener('input', applySearch); + searchInput._gateway_bound = true; + } + + // Select All - only selects visible items + if (selectAllBtn && !selectAllBtn._gateway_bound) { + selectAllBtn.addEventListener('click', () => { + const checkboxes = container.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(cb => { + const parent = cb.closest('.tool-item'); + if (!parent) return; + if (getComputedStyle(parent).display !== 'none') cb.checked = true; + }); + updatePills(); + }); + selectAllBtn._gateway_bound = true; + } + + if (clearAllBtn && !clearAllBtn._gateway_bound) { + clearAllBtn.addEventListener('click', () => { + const checkboxes = container.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(cb => (cb.checked = false)); + updatePills(); + }); + clearAllBtn._gateway_bound = true; + } + + // Delegate change events + if (!container._gateway_change_bound) { + container.addEventListener('change', (e) => { + if (e.target && e.target.type === 'checkbox') { + updatePills(); + } + }); + container._gateway_change_bound = true; + } + + // Initial run + applySearch(); + updatePills(); + + } catch (err) { + console.debug('initGatewaySelect failed', err); + } +} + +// Run on DOM ready and also on a short timeout for late loads +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initGatewaySelect); +} else { + initGatewaySelect(); +} +setTimeout(initGatewaySelect, 800); + // =================================================================== // INACTIVE ITEMS HANDLING // =================================================================== @@ -10746,6 +10876,7 @@ function initializeCodeMirrorEditors() { function initializeToolSelects() { console.log("Initializing tool selects..."); + // Add Server form initToolSelect( "associatedTools", @@ -10754,8 +10885,10 @@ function initializeToolSelects() { 6, "selectAllToolsBtn", "clearAllToolsBtn", + ); + initResourceSelect( "associatedResources", "selectedResourcesPills", diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index 6ec4b7286..c1afa1e9b 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -2039,18 +2039,18 @@

>
{% for gateway in gateways %} @@ -2059,9 +2059,9 @@

> {{ gateway.slug or gateway.name @@ -2070,26 +2070,26 @@

{% endfor %}

-
- - +
- {% for gateway in gateways %} + class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-300 dark:border-gray-700" + > - {% endfor %} - -
-
- + {% for gateway in gateways %} + + {% endfor %} + +
+
+ - -
+ +
- -
+ +
- -
-
+ +
+

- -
- -
- -
- - - - - Loading tools... + + +
+ +
+ + + + + Loading tools... +
-
- -
- + No tool found containing "" +

+
+ - -
+ +
- -
+ +
- -
+ +
+
-
+
+
+
From 3a494adfb627d5f309eb56c3b7c45708b986febc Mon Sep 17 00:00:00 2001 From: rakdutta Date: Thu, 13 Nov 2025 10:27:28 +0530 Subject: [PATCH 06/33] docstring Signed-off-by: rakdutta --- mcpgateway/services/server_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mcpgateway/services/server_service.py b/mcpgateway/services/server_service.py index cfcfe24d3..d0496a260 100644 --- a/mcpgateway/services/server_service.py +++ b/mcpgateway/services/server_service.py @@ -274,9 +274,10 @@ def _assemble_associated_items( resources: List of resource IDs. prompts: List of prompt IDs. a2a_agents: List of A2A agent IDs. + gateways: List of gateway IDs. Returns: - A dictionary with keys "tools", "resources", "prompts", and "a2a_agents". + A dictionary with keys "tools", "resources", "prompts", "a2a_agents", and "gateways". Examples: >>> service = ServerService() From 66c1400016eff1ddeab464e6b0b4102bdc8043a4 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 14 Nov 2025 13:09:44 +0530 Subject: [PATCH 07/33] endpoint Signed-off-by: rakdutta --- mcpgateway/admin.py | 21 ++ mcpgateway/static/admin.js | 436 ++++++++++++++++++++++++------------- 2 files changed, 310 insertions(+), 147 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 603985159..a987c4b8e 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -1887,6 +1887,26 @@ async def admin_list_gateways( gateways = await gateway_service.list_gateways_for_user(db, user_email, include_inactive=include_inactive) return [gateway.model_dump(by_alias=True) for gateway in gateways] +@admin_router.get("/gateways/ids") +async def admin_list_gateway_ids( + include_inactive: bool = False, + db: Session = Depends(get_db), + user=Depends(get_current_user_with_permissions), +) -> Dict[str, Any]: + """ + Return a JSON object containing a list of all gateway IDs. + + This endpoint is used by the admin UI to support the "Select All" action + for gateways. It returns a simple JSON payload with a single key + `gateway_ids` containing an array of gateway identifiers. + """ + user_email = get_user_email(user) + LOGGER.debug(f"User {user_email} requested gateway ids list") + gateways = await gateway_service.list_gateways_for_user(db, user_email, include_inactive=include_inactive) + ids = [str(g.id) for g in gateways] + LOGGER.info(f"Gateway IDs retrieved: {ids}") + return {"gateway_ids": ids} + @admin_router.post("/gateways/{gateway_id}/toggle") async def admin_toggle_gateway( @@ -8840,6 +8860,7 @@ async def admin_test_gateway(request: GatewayTestRequest, team_id: Optional[str] return GatewayTestResponse(status_code=502, latency_ms=latency_ms, body={"error": "Request failed", "details": str(e)}) + #################### # Admin Tag Routes # #################### diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 99462fe96..f2e5ca87c 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7289,177 +7289,319 @@ function initPromptSelect( // =================================================================== // GATEWAY SELECT (Associated MCP Servers) - search/select/clear // =================================================================== -function initGatewaySelect() { - try { - const container = document.getElementById('associatedGateways'); - if (!container) return; - - const searchInput = document.getElementById('searchGateways'); - const selectAllBtn = document.getElementById('selectAllGatewayBtn'); - const clearAllBtn = document.getElementById('clearAllGatewayBtn'); - const pillsBox = document.getElementById('selectedGatewayPills'); - const warnBox = document.getElementById('selectedGatewayWarning'); - const noMsg = document.getElementById('noGatewayMessage'); - const searchQuerySpan = document.getElementById('searchQuery'); - - function updatePills() { - try { - const checkboxes = Array.from(container.querySelectorAll('input[type="checkbox"]')); - const checked = checkboxes.filter(cb => cb.checked); - pillsBox.innerHTML = ''; - const maxShow = 6; - checked.slice(0, maxShow).forEach(cb => { - const span = document.createElement('span'); - span.className = 'inline-block bg-indigo-100 text-indigo-800 text-xs px-2 py-1 rounded-full'; - span.textContent = cb.nextElementSibling?.textContent?.trim() || cb.value; - pillsBox.appendChild(span); - }); - if (checked.length > maxShow) { - const span = document.createElement('span'); - span.className = 'inline-block bg-indigo-100 text-indigo-800 text-xs px-2 py-1 rounded-full'; - span.textContent = `+${checked.length - maxShow} more`; - pillsBox.appendChild(span); - } +function initGatewaySelect( + selectId = 'associatedGateways', + pillsId = 'selectedGatewayPills', + warnId = 'selectedGatewayWarning', + max = 12, + selectBtnId = 'selectAllGatewayBtn', + clearBtnId = 'clearAllGatewayBtn', + searchInputId = 'searchGateways', +) { + const container = document.getElementById(selectId); + const pillsBox = document.getElementById(pillsId); + const warnBox = document.getElementById(warnId); + const clearBtn = clearBtnId ? document.getElementById(clearBtnId) : null; + const selectBtn = selectBtnId ? document.getElementById(selectBtnId) : null; + const searchInput = searchInputId ? document.getElementById(searchInputId) : null; - // Warning if too many selected - if (checked.length > 12) { - warnBox.textContent = `Selected ${checked.length} MCP servers. Selecting many servers may impact performance.`; + if (!container || !pillsBox || !warnBox) { + console.warn( + `Gateway select elements not found: ${selectId}, ${pillsId}, ${warnId}`, + ); + return; + } + + const pillClasses = + "inline-block bg-indigo-100 text-indigo-800 text-xs px-2 py-1 rounded-full dark:bg-indigo-900 dark:text-indigo-200"; + + // Search functionality + function applySearch() { + if (!searchInput) return; + + try { + const query = searchInput.value.toLowerCase().trim(); + const items = container.querySelectorAll('.tool-item'); + let visibleCount = 0; + + items.forEach(item => { + const text = item.textContent.toLowerCase(); + if (!query || text.includes(query)) { + item.style.display = ''; + visibleCount++; } else { - warnBox.textContent = ''; + item.style.display = 'none'; } - } catch (e) { - console.error('Error updating gateway pills', e); - } - } - - function applySearch() { - try { - const q = (searchInput && searchInput.value || '').toLowerCase().trim(); - const items = Array.from(container.querySelectorAll('.tool-item')); - let visible = 0; - items.forEach(item => { - const text = (item.textContent || '').toLowerCase(); - if (!q || text.includes(q)) { - item.style.display = ''; - visible += 1; - } else { - item.style.display = 'none'; - } - }); + }); - if (noMsg) { - if (q && visible === 0) { - noMsg.style.display = 'block'; - if (searchQuerySpan) searchQuerySpan.textContent = q; - } else { - noMsg.style.display = 'none'; + // Update "no results" message if it exists + const noMsg = document.getElementById('noGatewayMessage'); + const searchQuerySpan = document.getElementById('searchQuery'); + if (noMsg) { + if (query && visibleCount === 0) { + noMsg.style.display = 'block'; + if (searchQuerySpan) { + searchQuerySpan.textContent = query; } + } else { + noMsg.style.display = 'none'; } - } catch (e) { - console.error('Error applying gateway search', e); } + } catch (error) { + console.error("Error applying gateway search:", error); } + } - // Bind search - if (searchInput && !searchInput._gateway_bound) { - searchInput.addEventListener('input', applySearch); - searchInput._gateway_bound = true; - } + // Bind search input + if (searchInput && !searchInput.dataset.searchBound) { + searchInput.addEventListener('input', applySearch); + searchInput.dataset.searchBound = 'true'; + } - // Select All - only selects visible items - if (selectAllBtn && !selectAllBtn._gateway_bound) { - selectAllBtn.addEventListener('click', () => { + function update() { + try { + const checkboxes = container.querySelectorAll( + 'input[type="checkbox"]', + ); + const checked = Array.from(checkboxes).filter((cb) => cb.checked); + + // Check if "Select All" mode is active + const selectAllInput = container.querySelector( + 'input[name="selectAllGateways"]', + ); + const allIdsInput = container.querySelector( + 'input[name="allGatewayIds"]', + ); + + let count = checked.length; + + // If Select All mode is active, use the count from allGatewayIds + if ( + selectAllInput && + selectAllInput.value === "true" && + allIdsInput + ) { try { - console.debug('selectAllGatewayBtn clicked'); - // ensure current search filter is applied so visibility is accurate - applySearch(); - // run in next frame to ensure styles settled - requestAnimationFrame(() => { - const checkboxes = container.querySelectorAll('input[type="checkbox"]'); - console.debug('selectAll: found checkboxes', checkboxes.length); - let selected = 0; - checkboxes.forEach(cb => { - // prefer the .tool-item wrapper, fallback to the checkbox itself - const parent = cb.closest('.tool-item') || cb; - if (!parent) { - console.debug('selectAll: no parent for checkbox', cb); - return; - } - const disp = getComputedStyle(parent).display; - // consider visible if not 'none' - if (disp && disp !== 'none') { - cb.checked = true; - // dispatch change so other listeners react - try { - cb.dispatchEvent(new Event('change', { bubbles: true })); - } catch (e) { - console.debug('selectAll: dispatch change failed', e); - } - selected += 1; - } else { - console.debug('selectAll: skipping hidden item, display=', disp, parent); - } - }); - console.debug('selectAll: selected count', selected); - updatePills(); - console.debug('selectAllGatewayBtn completed'); - }); + const allIds = JSON.parse(allIdsInput.value); + count = allIds.length; } catch (e) { - console.error('Error in selectAllGatewayBtn handler', e); + console.error("Error parsing allGatewayIds:", e); } + } + + // Rebuild pills safely - show first 3, then summarize the rest + pillsBox.innerHTML = ""; + const maxPillsToShow = 3; + + checked.slice(0, maxPillsToShow).forEach((cb) => { + const span = document.createElement("span"); + span.className = pillClasses; + span.textContent = + cb.nextElementSibling?.textContent?.trim() || "Unnamed"; + pillsBox.appendChild(span); }); - selectAllBtn._gateway_bound = true; + + // If more than maxPillsToShow, show a summary pill + if (count > maxPillsToShow) { + const span = document.createElement("span"); + span.className = pillClasses + " cursor-pointer"; + span.title = "Click to see all selected gateways"; + const remaining = count - maxPillsToShow; + span.textContent = `+${remaining} more`; + pillsBox.appendChild(span); + } + + // Warning when > max + if (count > max) { + warnBox.textContent = `Selected ${count} MCP servers. Selecting more than ${max} servers may impact performance.`; + } else { + warnBox.textContent = ""; + } + } catch (error) { + console.error("Error updating gateway select:", error); } + } - if (clearAllBtn && !clearAllBtn._gateway_bound) { - clearAllBtn.addEventListener('click', () => { - try { - console.debug('clearAllGatewayBtn clicked'); - requestAnimationFrame(() => { - const checkboxes = container.querySelectorAll('input[type="checkbox"]'); - checkboxes.forEach(cb => { - cb.checked = false; - cb.dispatchEvent(new Event('change', { bubbles: true })); - }); - updatePills(); - console.debug('clearAllGatewayBtn completed'); - }); - } catch (e) { - console.error('Error in clearAllGatewayBtn handler', e); + // Remove old event listeners by cloning and replacing (preserving ID) + if (clearBtn && !clearBtn.dataset.listenerAttached) { + clearBtn.dataset.listenerAttached = "true"; + const newClearBtn = clearBtn.cloneNode(true); + newClearBtn.dataset.listenerAttached = "true"; + clearBtn.parentNode.replaceChild(newClearBtn, clearBtn); + + newClearBtn.addEventListener("click", () => { + const checkboxes = container.querySelectorAll( + 'input[type="checkbox"]', + ); + checkboxes.forEach((cb) => (cb.checked = false)); + + // Clear the "select all" flag + const selectAllInput = container.querySelector( + 'input[name="selectAllGateways"]', + ); + if (selectAllInput) { + selectAllInput.remove(); + } + const allIdsInput = container.querySelector( + 'input[name="allGatewayIds"]', + ); + if (allIdsInput) { + allIdsInput.remove(); + } + + update(); + }); + } + + if (selectBtn && !selectBtn.dataset.listenerAttached) { + selectBtn.dataset.listenerAttached = "true"; + const newSelectBtn = selectBtn.cloneNode(true); + newSelectBtn.dataset.listenerAttached = "true"; + selectBtn.parentNode.replaceChild(newSelectBtn, selectBtn); + + newSelectBtn.addEventListener("click", async () => { + // Disable button and show loading state + const originalText = newSelectBtn.textContent; + newSelectBtn.disabled = true; + newSelectBtn.textContent = "Selecting all gateways..."; + + try { + // Fetch all gateway IDs from the server + const response = await fetch( + `${window.ROOT_PATH}/admin/gateways/ids`, + ); + if (!response.ok) { + throw new Error("Failed to fetch gateway IDs"); } - }); - clearAllBtn._gateway_bound = true; - } - // Delegate change events - if (!container._gateway_change_bound) { - container.addEventListener('change', (e) => { - if (e.target && e.target.type === 'checkbox') { - updatePills(); + const data = await response.json(); + const allGatewayIds = data.gateway_ids || []; + + // Apply search filter first to determine which items are visible + applySearch(); + + // Check only currently visible checkboxes + const loadedCheckboxes = container.querySelectorAll( + 'input[type="checkbox"]', + ); + loadedCheckboxes.forEach((cb) => { + const parent = cb.closest('.tool-item') || cb.parentElement; + const isVisible = parent && getComputedStyle(parent).display !== 'none'; + if (isVisible) { + cb.checked = true; + } + }); + + // Add a hidden input to indicate "select all" mode + // Remove any existing one first + let selectAllInput = container.querySelector( + 'input[name="selectAllGateways"]', + ); + if (!selectAllInput) { + selectAllInput = document.createElement("input"); + selectAllInput.type = "hidden"; + selectAllInput.name = "selectAllGateways"; + container.appendChild(selectAllInput); } - }); - container._gateway_change_bound = true; - } + selectAllInput.value = "true"; - // Initial run - applySearch(); - updatePills(); + // Also store the IDs as a JSON array for the backend + let allIdsInput = container.querySelector( + 'input[name="allGatewayIds"]', + ); + if (!allIdsInput) { + allIdsInput = document.createElement("input"); + allIdsInput.type = "hidden"; + allIdsInput.name = "allGatewayIds"; + container.appendChild(allIdsInput); + } + allIdsInput.value = JSON.stringify(allGatewayIds); - } catch (err) { - console.debug('initGatewaySelect failed', err); + update(); + + newSelectBtn.textContent = `✓ All ${allGatewayIds.length} gateways selected`; + setTimeout(() => { + newSelectBtn.textContent = originalText; + }, 2000); + } catch (error) { + console.error("Error in Select All:", error); + alert("Failed to select all gateways. Please try again."); + newSelectBtn.disabled = false; + newSelectBtn.textContent = originalText; + } finally { + newSelectBtn.disabled = false; + } + }); } -} -// Run on DOM ready and also on a short timeout for late loads -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initGatewaySelect); -} else { - initGatewaySelect(); + update(); // Initial render + + // Attach change listeners to checkboxes (using delegation for dynamic content) + if (!container.dataset.changeListenerAttached) { + container.dataset.changeListenerAttached = "true"; + container.addEventListener("change", (e) => { + if (e.target.type === "checkbox") { + // Check if we're in "Select All" mode + const selectAllInput = container.querySelector( + 'input[name="selectAllGateways"]', + ); + const allIdsInput = container.querySelector( + 'input[name="allGatewayIds"]', + ); + + if ( + selectAllInput && + selectAllInput.value === "true" && + allIdsInput + ) { + // User is manually checking/unchecking after Select All + // Update the allGatewayIds array to reflect the change + try { + let allIds = JSON.parse(allIdsInput.value); + const gatewayId = e.target.value; + + if (e.target.checked) { + // Add the ID if it's not already there + if (!allIds.includes(gatewayId)) { + allIds.push(gatewayId); + } + } else { + // Remove the ID from the array + allIds = allIds.filter((id) => id !== gatewayId); + } + + // Update the hidden field + allIdsInput.value = JSON.stringify(allIds); + } catch (error) { + console.error("Error updating allGatewayIds:", error); + } + } + + update(); + } + }); + } + + // Initial render + applySearch(); + update(); } -setTimeout(initGatewaySelect, 800); -// Delegated safety: if the Select All / Clear All buttons are replaced by HTMX -// or other dynamic swaps, re-run initGatewaySelect on capture of clicks targeting them -document.addEventListener('click', (e) => { if (e.target && e.target.closest && (e.target.closest('#selectAllGatewayBtn') || e.target.closest('#clearAllGatewayBtn'))) { try { initGatewaySelect(); } catch (err) { console.debug('delegated gateway init failed', err); } } }, true); + +// Initialize gateway select on page load +document.addEventListener('DOMContentLoaded', function() { + // Initialize for the create server form + if (document.getElementById('associatedGateways')) { + initGatewaySelect( + 'associatedGateways', + 'selectedGatewayPills', + 'selectedGatewayWarning', + 12, + 'selectAllGatewayBtn', + 'clearAllGatewayBtn', + 'searchGateways' + ); + } +}); // =================================================================== // INACTIVE ITEMS HANDLING From 31404dd6eed1db95845e20a350b54743fc5f3222 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 14 Nov 2025 14:17:20 +0530 Subject: [PATCH 08/33] partial html endpoint Signed-off-by: rakdutta --- mcpgateway/admin.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index a987c4b8e..5ff1a74e4 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -4940,6 +4940,7 @@ async def admin_tools_partial_html( per_page: int = Query(50, ge=1, le=500, description="Items per page"), include_inactive: bool = False, render: Optional[str] = Query(None, description="Render mode: 'controls' for pagination controls only"), + gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"), db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): @@ -4961,7 +4962,7 @@ async def admin_tools_partial_html( Returns: HTMLResponse with tools table rows and pagination controls. """ - LOGGER.debug(f"User {get_user_email(user)} requested tools HTML partial (page={page}, per_page={per_page}, render={render})") + LOGGER.debug(f"User {get_user_email(user)} requested tools HTML partial (page={page}, per_page={per_page}, render={render}, gateway_id={gateway_id})") # Get paginated data from the JSON endpoint logic user_email = get_user_email(user) @@ -4978,6 +4979,13 @@ async def admin_tools_partial_html( # Build query query = select(DbTool) + # Apply gateway filter if provided + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(',') if gid.strip()] + if gateway_ids: + query = query.where(DbTool.gateway_id.in_(gateway_ids)) + LOGGER.debug(f"Filtering tools by gateway IDs: {gateway_ids}") + # Apply active/inactive filter if not include_inactive: query = query.where(DbTool.enabled.is_(True)) @@ -5220,6 +5228,7 @@ async def admin_prompts_partial_html( per_page: int = Query(50, ge=1), include_inactive: bool = False, render: Optional[str] = Query(None), + gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"), db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): @@ -5259,6 +5268,14 @@ async def admin_prompts_partial_html( # Build base query query = select(DbPrompt) + + # Apply gateway filter if provided + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(',') if gid.strip()] + if gateway_ids: + query = query.where(DbPrompt.gateway_id.in_(gateway_ids)) + LOGGER.debug(f"Filtering prompts by gateway IDs: {gateway_ids}") + if not include_inactive: query = query.where(DbPrompt.is_active.is_(True)) @@ -5361,6 +5378,7 @@ async def admin_resources_partial_html( per_page: int = Query(50, ge=1, le=500, description="Items per page"), include_inactive: bool = False, render: Optional[str] = Query(None, description="Render mode: 'controls' for pagination controls only"), + gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"), db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): @@ -5386,7 +5404,7 @@ async def admin_resources_partial_html( resources partial (rows + controls), pagination controls only, or selector items depending on the ``render`` parameter. """ - LOGGER.debug(f"User {get_user_email(user)} requested resources HTML partial (page={page}, per_page={per_page}, render={render})") + LOGGER.debug(f"User {get_user_email(user)} requested resources HTML partial (page={page}, per_page={per_page}, render={render}, gateway_id={gateway_id})") # Normalize per_page per_page = max(settings.pagination_min_page_size, min(per_page, settings.pagination_max_page_size)) @@ -5401,6 +5419,13 @@ async def admin_resources_partial_html( # Build base query query = select(DbResource) + # Apply gateway filter if provided + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(',') if gid.strip()] + if gateway_ids: + query = query.where(DbResource.gateway_id.in_(gateway_ids)) + LOGGER.debug(f"Filtering resources by gateway IDs: {gateway_ids}") + # Apply active/inactive filter if not include_inactive: query = query.where(DbResource.is_active.is_(True)) From 1fa784ab04731c145ed38eb5d2ac060a0bd73098 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 14 Nov 2025 14:26:45 +0530 Subject: [PATCH 09/33] gateway_id log in console Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index f2e5ca87c..8b7f4e158 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7541,6 +7541,13 @@ function initGatewaySelect( container.dataset.changeListenerAttached = "true"; container.addEventListener("change", (e) => { if (e.target.type === "checkbox") { + // Log gateway_id when checkbox is clicked + const gatewayId = e.target.value; + const gatewayName = e.target.nextElementSibling?.textContent?.trim() || "Unknown"; + const isChecked = e.target.checked; + + console.log(`[MCP Server Selection] Gateway ID: ${gatewayId}, Name: ${gatewayName}, Checked: ${isChecked}`); + // Check if we're in "Select All" mode const selectAllInput = container.querySelector( 'input[name="selectAllGateways"]', @@ -7558,7 +7565,6 @@ function initGatewaySelect( // Update the allGatewayIds array to reflect the change try { let allIds = JSON.parse(allIdsInput.value); - const gatewayId = e.target.value; if (e.target.checked) { // Add the ID if it's not already there From 7f5d446cd1af313da656824480887cccdb957f52 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 14 Nov 2025 14:40:03 +0530 Subject: [PATCH 10/33] filter tools/prompts Signed-off-by: rakdutta --- GATEWAY_FILTER_TESTING.md | 169 ++++++++++++++++++ mcpgateway/admin.py | 3 + mcpgateway/static/admin.js | 101 +++++++++++ .../templates/prompts_selector_items.html | 2 +- .../templates/resources_selector_items.html | 2 +- .../templates/tools_selector_items.html | 2 +- 6 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 GATEWAY_FILTER_TESTING.md diff --git a/GATEWAY_FILTER_TESTING.md b/GATEWAY_FILTER_TESTING.md new file mode 100644 index 000000000..78bf159b7 --- /dev/null +++ b/GATEWAY_FILTER_TESTING.md @@ -0,0 +1,169 @@ +# Gateway Filter Testing Guide + +## Overview +This document describes how to test the new gateway filtering functionality for associated tools, prompts, and resources. + +## What Was Changed + +### Backend (Already Existed) +The backend API endpoints already supported filtering by `gateway_id`: +- `/admin/tools/partial?gateway_id=,` +- `/admin/resources/partial?gateway_id=,` +- `/admin/prompts/partial?gateway_id=,` + +### Frontend (New Implementation) +Added JavaScript functionality to: +1. Log gateway_id when MCP server checkbox is clicked +2. Collect all selected gateway IDs +3. Automatically reload tools/resources/prompts filtered by selected gateways + +## Testing Steps + +### 1. Open Browser Console +Press `F12` or `Ctrl+Shift+I` to open Developer Tools and switch to the Console tab. + +### 2. Navigate to Create Virtual Server +Go to the admin interface and click "Create Virtual Server" or open the virtual server creation form. + +### 3. Test Individual MCP Server Selection + +**Action:** Click on an MCP server checkbox to select it. + +**Expected Console Output:** +``` +[MCP Server Selection] Gateway ID: df212f5e971a41e3917edcfae9a66711, Name: mcp-server, Checked: true +[Filter Update] Reloading associated items for gateway IDs: df212f5e971a41e3917edcfae9a66711 +``` + +**Expected Behavior:** +- The tools, resources, and prompts lists should reload +- Only items associated with the selected gateway should be displayed + +### 4. Test Multiple MCP Server Selection + +**Action:** Select a second MCP server checkbox. + +**Expected Console Output:** +``` +[MCP Server Selection] Gateway ID: 885756888a8d43f2a62791e50c388494, Name: mcp-http, Checked: true +[Filter Update] Reloading associated items for gateway IDs: df212f5e971a41e3917edcfae9a66711,885756888a8d43f2a62791e50c388494 +``` + +**Expected Behavior:** +- Tools/resources/prompts from BOTH selected gateways should now be visible + +### 5. Test Deselection + +**Action:** Uncheck one of the selected MCP servers. + +**Expected Console Output:** +``` +[MCP Server Selection] Gateway ID: df212f5e971a41e3917edcfae9a66711, Name: mcp-server, Checked: false +[Filter Update] Reloading associated items for gateway IDs: 885756888a8d43f2a62791e50c388494 +``` + +**Expected Behavior:** +- Only tools/resources/prompts from the remaining selected gateway should be visible + +### 6. Test Clear All Button + +**Action:** Click the "Clear All" button under the Associated MCP Servers section. + +**Expected Console Output:** +``` +[Filter Update] Reloading associated items for gateway IDs: none (showing all) +``` + +**Expected Behavior:** +- All MCP server checkboxes should be unchecked +- ALL tools/resources/prompts should be displayed (no filter applied) + +### 7. Test Select All Button + +**Action:** Click the "Select All" button under the Associated MCP Servers section. + +**Expected Console Output:** +``` +[Filter Update] Reloading associated items for gateway IDs: +``` + +**Expected Behavior:** +- All visible MCP server checkboxes should be checked +- Tools/resources/prompts from ALL selected gateways should be displayed + +### 8. Test Search + Filter + +**Action:** +1. Type a search term in the "Search for MCP servers..." box +2. Select one of the filtered MCP servers + +**Expected Behavior:** +- Search should filter the visible MCP servers +- Selecting a filtered server should still trigger the filter reload +- Tools/resources/prompts should update based on selected gateway + +## Validation Points + +### ✅ Gateway ID Logging +- [ ] Gateway ID is logged when checkbox is clicked +- [ ] Gateway name is shown in the log +- [ ] Checked status (true/false) is accurate + +### ✅ Filter Application +- [ ] Tools list reloads with gateway_id parameter +- [ ] Resources list reloads with gateway_id parameter +- [ ] Prompts list reloads with gateway_id parameter +- [ ] Multiple gateway IDs are comma-separated in the URL + +### ✅ UI Responsiveness +- [ ] Lists reload smoothly without page refresh +- [ ] Loading indicators appear during reload (if implemented) +- [ ] Previously selected tools/resources/prompts are cleared after filter change +- [ ] Pills/badges update correctly to show selected items + +### ✅ Edge Cases +- [ ] Works with no gateways selected (shows all items) +- [ ] Works with one gateway selected +- [ ] Works with multiple gateways selected +- [ ] Works after using "Select All" +- [ ] Works after using "Clear All" +- [ ] Works with search filter active + +## Network Inspection + +Open the Network tab in Developer Tools to verify the correct API calls: + +**Expected API Calls:** +``` +GET /admin/tools/partial?page=1&per_page=50&render=selector&gateway_id=df212f5e971a41e3917edcfae9a66711 +GET /admin/resources/partial?page=1&per_page=50&render=selector&gateway_id=df212f5e971a41e3917edcfae9a66711 +GET /admin/prompts/partial?page=1&per_page=50&render=selector&gateway_id=df212f5e971a41e3917edcfae9a66711 +``` + +## Troubleshooting + +### Issue: No console logs appear +**Solution:** Make sure browser console is open and not filtered. Check that JavaScript is enabled. + +### Issue: Lists don't reload +**Solution:** Check if HTMX is loaded (`window.htmx` should be defined in console). Verify network requests are succeeding. + +### Issue: Wrong items shown after filtering +**Solution:** Check the gateway_id parameter in the network tab. Verify the backend returns correct data for those IDs. + +### Issue: "initToolSelect is not defined" error +**Solution:** Ensure the HTMX response includes the callback to initialize the select functions, or verify the functions are called after content loads. + +## Code Locations + +### JavaScript Functions +- `getSelectedGatewayIds()` - Collects all selected gateway IDs +- `reloadAssociatedItems()` - Triggers reload of tools/resources/prompts +- `initGatewaySelect()` - Main initialization function for gateway selection + +### Backend Endpoints +- `mcpgateway/admin.py` - `admin_tools_partial_html()`, `admin_resources_partial_html()`, `admin_prompts_partial_html()` + +### Frontend Files +- `mcpgateway/static/admin.js` - Main JavaScript implementation +- `mcpgateway/templates/admin.html` - HTML template with HTMX attributes diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 5ff1a74e4..0405af12b 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -5079,6 +5079,7 @@ async def admin_tools_partial_html( "data": data, "pagination": pagination.model_dump(), "root_path": request.scope.get("root_path", ""), + "gateway_id": gateway_id, }, ) @@ -5355,6 +5356,7 @@ async def admin_prompts_partial_html( "data": data, "pagination": pagination.model_dump(), "root_path": request.scope.get("root_path", ""), + "gateway_id": gateway_id, }, ) @@ -5504,6 +5506,7 @@ async def admin_resources_partial_html( "data": data, "pagination": pagination.model_dump(), "root_path": request.scope.get("root_path", ""), + "gateway_id": gateway_id, }, ) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 8b7f4e158..eadb931bf 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7450,6 +7450,9 @@ function initGatewaySelect( } update(); + + // Reload associated items after clearing selection + reloadAssociatedItems(); }); } @@ -7523,6 +7526,9 @@ function initGatewaySelect( setTimeout(() => { newSelectBtn.textContent = originalText; }, 2000); + + // Reload associated items after selecting all + reloadAssociatedItems(); } catch (error) { console.error("Error in Select All:", error); alert("Failed to select all gateways. Please try again."); @@ -7584,6 +7590,9 @@ function initGatewaySelect( } update(); + + // Trigger reload of associated tools, resources, and prompts with selected gateway filter + reloadAssociatedItems(); } }); } @@ -7593,6 +7602,98 @@ function initGatewaySelect( update(); } +/** + * Get all selected gateway IDs from the gateway selection container + * @returns {string[]} Array of selected gateway IDs + */ +function getSelectedGatewayIds() { + const container = document.getElementById('associatedGateways'); + if (!container) { + return []; + } + + // Check if "Select All" mode is active + const selectAllInput = container.querySelector('input[name="selectAllGateways"]'); + const allIdsInput = container.querySelector('input[name="allGatewayIds"]'); + + if (selectAllInput && selectAllInput.value === "true" && allIdsInput) { + try { + return JSON.parse(allIdsInput.value); + } catch (error) { + console.error("Error parsing allGatewayIds:", error); + } + } + + // Otherwise, get all checked checkboxes + const checkboxes = container.querySelectorAll('input[type="checkbox"]:checked'); + return Array.from(checkboxes).map(cb => cb.value); +} + +/** + * Reload associated tools, resources, and prompts filtered by selected gateway IDs + */ +function reloadAssociatedItems() { + const selectedGatewayIds = getSelectedGatewayIds(); + const gatewayIdParam = selectedGatewayIds.length > 0 ? selectedGatewayIds.join(',') : ''; + + console.log(`[Filter Update] Reloading associated items for gateway IDs: ${gatewayIdParam || 'none (showing all)'}`); + + // Reload tools + const toolsContainer = document.getElementById('associatedTools'); + if (toolsContainer) { + const toolsUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector`; + + // Use HTMX to reload the content + if (window.htmx) { + htmx.ajax('GET', toolsUrl, { + target: '#associatedTools', + swap: 'innerHTML' + }).then(() => { + // Re-initialize the tool select after content is loaded + initToolSelect('associatedTools', 'selectedToolsPills', 'selectedToolsWarning', 6, 'selectAllToolsBtn', 'clearAllToolsBtn'); + }); + } + } + + // Reload resources + const resourcesContainer = document.getElementById('associatedResources'); + if (resourcesContainer) { + const resourcesUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector`; + + if (window.htmx) { + htmx.ajax('GET', resourcesUrl, { + target: '#associatedResources', + swap: 'innerHTML' + }).then(() => { + // Re-initialize the resource select after content is loaded + initResourceSelect('associatedResources', 'selectedResourcesPills', 'selectedResourcesWarning', 6, 'selectAllResourcesBtn', 'clearAllResourcesBtn'); + }); + } + } + + // Reload prompts + const promptsContainer = document.getElementById('associatedPrompts'); + if (promptsContainer) { + const promptsUrl = gatewayIdParam + ? `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` + : `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector`; + + if (window.htmx) { + htmx.ajax('GET', promptsUrl, { + target: '#associatedPrompts', + swap: 'innerHTML' + }).then(() => { + // Re-initialize the prompt select after content is loaded + initPromptSelect('associatedPrompts', 'selectedPromptsPills', 'selectedPromptsWarning', 6, 'selectAllPromptsBtn', 'clearAllPromptsBtn'); + }); + } + } +} + // Initialize gateway select on page load document.addEventListener('DOMContentLoaded', function() { // Initialize for the create server form diff --git a/mcpgateway/templates/prompts_selector_items.html b/mcpgateway/templates/prompts_selector_items.html index bfdbfe395..fc7b3fd94 100644 --- a/mcpgateway/templates/prompts_selector_items.html +++ b/mcpgateway/templates/prompts_selector_items.html @@ -21,7 +21,7 @@
Date: Fri, 14 Nov 2025 14:49:38 +0530 Subject: [PATCH 11/33] partial_html endpoint Signed-off-by: rakdutta --- mcpgateway/admin.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 0405af12b..bd952062e 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -5005,8 +5005,12 @@ async def admin_tools_partial_html( query = query.where(or_(*access_conditions)) - # Count total items + # Count total items - must include gateway filter for accurate count count_query = select(func.count()).select_from(DbTool).where(or_(*access_conditions)) # pylint: disable=not-callable + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(',') if gid.strip()] + if gateway_ids: + count_query = count_query.where(DbTool.gateway_id.in_(gateway_ids)) if not include_inactive: count_query = count_query.where(DbTool.enabled.is_(True)) @@ -5288,8 +5292,12 @@ async def admin_prompts_partial_html( query = query.where(or_(*access_conditions)) - # Count total items + # Count total items - must include gateway filter for accurate count count_query = select(func.count()).select_from(DbPrompt).where(or_(*access_conditions)) # pylint: disable=not-callable + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(',') if gid.strip()] + if gateway_ids: + count_query = count_query.where(DbPrompt.gateway_id.in_(gateway_ids)) if not include_inactive: count_query = count_query.where(DbPrompt.is_active.is_(True)) @@ -5440,8 +5448,12 @@ async def admin_resources_partial_html( query = query.where(or_(*access_conditions)) - # Count total items + # Count total items - must include gateway filter for accurate count count_query = select(func.count()).select_from(DbResource).where(or_(*access_conditions)) # pylint: disable=not-callable + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(',') if gid.strip()] + if gateway_ids: + count_query = count_query.where(DbResource.gateway_id.in_(gateway_ids)) if not include_inactive: count_query = count_query.where(DbResource.is_active.is_(True)) From 6b018b9f9aa410d980499715a321489f4eb3927b Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 14 Nov 2025 16:48:18 +0530 Subject: [PATCH 12/33] filter resources Signed-off-by: rakdutta --- mcpgateway/admin.py | 20 +++++++++--- mcpgateway/static/admin.js | 67 ++++++++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index bd952062e..1f7a04ed5 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -4962,8 +4962,8 @@ async def admin_tools_partial_html( Returns: HTMLResponse with tools table rows and pagination controls. """ - LOGGER.debug(f"User {get_user_email(user)} requested tools HTML partial (page={page}, per_page={per_page}, render={render}, gateway_id={gateway_id})") - + LOGGER.info(f"User {get_user_email(user)} requested tools HTML partial (page={page}, per_page={per_page}, render={render}, gateway_id={gateway_id})") + # Get paginated data from the JSON endpoint logic user_email = get_user_email(user) @@ -5261,6 +5261,7 @@ async def admin_prompts_partial_html( items depending on ``render``. The response contains JSON-serializable encoded prompt data when templates expect it. """ + LOGGER.info(f"User {get_user_email(user)} requested prompts HTML partial (page={page}, per_page={per_page}, include_inactive={include_inactive}, render={render}, gateway_id={gateway_id})") # Normalize per_page within configured bounds per_page = max(settings.pagination_min_page_size, min(per_page, settings.pagination_max_page_size)) @@ -5414,7 +5415,10 @@ async def admin_resources_partial_html( resources partial (rows + controls), pagination controls only, or selector items depending on the ``render`` parameter. """ - LOGGER.debug(f"User {get_user_email(user)} requested resources HTML partial (page={page}, per_page={per_page}, render={render}, gateway_id={gateway_id})") + # Log the full request URL and query parameters for debugging + LOGGER.info(f"[RESOURCES FILTER DEBUG] Full request URL: {request.url}") + LOGGER.info(f"[RESOURCES FILTER DEBUG] Query params: {dict(request.query_params)}") + LOGGER.info(f"[RESOURCES FILTER DEBUG] User {get_user_email(user)} requested resources HTML partial (page={page}, per_page={per_page}, render={render}, gateway_id={gateway_id})") # Normalize per_page per_page = max(settings.pagination_min_page_size, min(per_page, settings.pagination_max_page_size)) @@ -5434,7 +5438,9 @@ async def admin_resources_partial_html( gateway_ids = [gid.strip() for gid in gateway_id.split(',') if gid.strip()] if gateway_ids: query = query.where(DbResource.gateway_id.in_(gateway_ids)) - LOGGER.debug(f"Filtering resources by gateway IDs: {gateway_ids}") + LOGGER.info(f"[RESOURCES FILTER DEBUG] Filtering resources by gateway IDs: {gateway_ids}") + else: + LOGGER.info("[RESOURCES FILTER DEBUG] No gateway_id filter provided, showing all resources") # Apply active/inactive filter if not include_inactive: @@ -5464,6 +5470,11 @@ async def admin_resources_partial_html( query = query.order_by(DbResource.name, DbResource.id).offset(offset).limit(per_page) resources_db = list(db.scalars(query).all()) + + LOGGER.info(f"[RESOURCES FILTER DEBUG] Query returned {len(resources_db)} resources (total_items={total_items})") + if resources_db and gateway_id: + LOGGER.info(f"[RESOURCES FILTER DEBUG] First few resource names: {[r.name for r in resources_db[:3]]}") + LOGGER.info(f"[RESOURCES FILTER DEBUG] First few resource gateway_ids: {[r.gateway_id for r in resources_db[:3]]}") # Convert to schemas using ResourceService local_resource_service = ResourceService() @@ -5476,6 +5487,7 @@ async def admin_resources_partial_html( continue data = jsonable_encoder(resources_data) + LOGGER.info(f"[RESOURCES FILTER DEBUG] Converted {len(data)} resources to JSON") # Build pagination metadata pagination = PaginationMeta( diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index eadb931bf..2da31eea1 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7608,7 +7608,10 @@ function initGatewaySelect( */ function getSelectedGatewayIds() { const container = document.getElementById('associatedGateways'); + console.log(`[Gateway Selection DEBUG] Container found:`, !!container); + if (!container) { + console.warn(`[Gateway Selection DEBUG] associatedGateways container not found`); return []; } @@ -7616,17 +7619,26 @@ function getSelectedGatewayIds() { const selectAllInput = container.querySelector('input[name="selectAllGateways"]'); const allIdsInput = container.querySelector('input[name="allGatewayIds"]'); + console.log(`[Gateway Selection DEBUG] Select All mode:`, selectAllInput?.value === "true"); + if (selectAllInput && selectAllInput.value === "true" && allIdsInput) { try { - return JSON.parse(allIdsInput.value); + const allIds = JSON.parse(allIdsInput.value); + console.log(`[Gateway Selection DEBUG] Returning all gateway IDs (${allIds.length} total)`); + return allIds; } catch (error) { - console.error("Error parsing allGatewayIds:", error); + console.error("[Gateway Selection DEBUG] Error parsing allGatewayIds:", error); } } // Otherwise, get all checked checkboxes const checkboxes = container.querySelectorAll('input[type="checkbox"]:checked'); - return Array.from(checkboxes).map(cb => cb.value); + const selectedIds = Array.from(checkboxes).map(cb => cb.value); + + console.log(`[Gateway Selection DEBUG] Found ${checkboxes.length} checked gateway checkboxes`); + console.log(`[Gateway Selection DEBUG] Selected gateway IDs:`, selectedIds); + + return selectedIds; } /** @@ -7637,6 +7649,7 @@ function reloadAssociatedItems() { const gatewayIdParam = selectedGatewayIds.length > 0 ? selectedGatewayIds.join(',') : ''; console.log(`[Filter Update] Reloading associated items for gateway IDs: ${gatewayIdParam || 'none (showing all)'}`); + console.log(`[Filter Update DEBUG] Selected gateway IDs array:`, selectedGatewayIds); // Reload tools const toolsContainer = document.getElementById('associatedTools'); @@ -7645,34 +7658,62 @@ function reloadAssociatedItems() { ? `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` : `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector`; + console.log(`[Filter Update DEBUG] Tools URL:`, toolsUrl); + // Use HTMX to reload the content if (window.htmx) { htmx.ajax('GET', toolsUrl, { target: '#associatedTools', swap: 'innerHTML' }).then(() => { + console.log(`[Filter Update DEBUG] Tools reloaded successfully`); // Re-initialize the tool select after content is loaded initToolSelect('associatedTools', 'selectedToolsPills', 'selectedToolsWarning', 6, 'selectAllToolsBtn', 'clearAllToolsBtn'); + }).catch(err => { + console.error(`[Filter Update DEBUG] Tools reload failed:`, err); }); + } else { + console.error(`[Filter Update DEBUG] HTMX not available for tools reload`); } + } else { + console.warn(`[Filter Update DEBUG] Tools container not found`); } - // Reload resources + // Reload resources - use fetch directly to avoid HTMX race conditions const resourcesContainer = document.getElementById('associatedResources'); if (resourcesContainer) { const resourcesUrl = gatewayIdParam ? `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` : `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector`; - if (window.htmx) { - htmx.ajax('GET', resourcesUrl, { - target: '#associatedResources', - swap: 'innerHTML' - }).then(() => { - // Re-initialize the resource select after content is loaded - initResourceSelect('associatedResources', 'selectedResourcesPills', 'selectedResourcesWarning', 6, 'selectAllResourcesBtn', 'clearAllResourcesBtn'); - }); - } + console.log(`[Filter Update DEBUG] Resources URL:`, resourcesUrl); + + // Use fetch() directly instead of htmx.ajax() to avoid race conditions + fetch(resourcesUrl, { + method: 'GET', + headers: { + 'HX-Request': 'true', + 'HX-Current-URL': window.location.href + } + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response.text(); + }) + .then(html => { + console.log(`[Filter Update DEBUG] Resources fetch successful, HTML length:`, html.length); + resourcesContainer.innerHTML = html; + // Re-initialize the resource select after content is loaded + initResourceSelect('associatedResources', 'selectedResourcesPills', 'selectedResourcesWarning', 6, 'selectAllResourcesBtn', 'clearAllResourcesBtn'); + console.log(`[Filter Update DEBUG] Resources reloaded successfully via fetch`); + }) + .catch(err => { + console.error(`[Filter Update DEBUG] Resources reload failed:`, err); + }); + } else { + console.warn(`[Filter Update DEBUG] Resources container not found`); } // Reload prompts From 8adac11902c3d3e5792de0c3e506e41bff83868b Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 14 Nov 2025 16:49:29 +0530 Subject: [PATCH 13/33] remove testing md file Signed-off-by: rakdutta --- GATEWAY_FILTER_TESTING.md | 169 -------------------------------------- 1 file changed, 169 deletions(-) delete mode 100644 GATEWAY_FILTER_TESTING.md diff --git a/GATEWAY_FILTER_TESTING.md b/GATEWAY_FILTER_TESTING.md deleted file mode 100644 index 78bf159b7..000000000 --- a/GATEWAY_FILTER_TESTING.md +++ /dev/null @@ -1,169 +0,0 @@ -# Gateway Filter Testing Guide - -## Overview -This document describes how to test the new gateway filtering functionality for associated tools, prompts, and resources. - -## What Was Changed - -### Backend (Already Existed) -The backend API endpoints already supported filtering by `gateway_id`: -- `/admin/tools/partial?gateway_id=,` -- `/admin/resources/partial?gateway_id=,` -- `/admin/prompts/partial?gateway_id=,` - -### Frontend (New Implementation) -Added JavaScript functionality to: -1. Log gateway_id when MCP server checkbox is clicked -2. Collect all selected gateway IDs -3. Automatically reload tools/resources/prompts filtered by selected gateways - -## Testing Steps - -### 1. Open Browser Console -Press `F12` or `Ctrl+Shift+I` to open Developer Tools and switch to the Console tab. - -### 2. Navigate to Create Virtual Server -Go to the admin interface and click "Create Virtual Server" or open the virtual server creation form. - -### 3. Test Individual MCP Server Selection - -**Action:** Click on an MCP server checkbox to select it. - -**Expected Console Output:** -``` -[MCP Server Selection] Gateway ID: df212f5e971a41e3917edcfae9a66711, Name: mcp-server, Checked: true -[Filter Update] Reloading associated items for gateway IDs: df212f5e971a41e3917edcfae9a66711 -``` - -**Expected Behavior:** -- The tools, resources, and prompts lists should reload -- Only items associated with the selected gateway should be displayed - -### 4. Test Multiple MCP Server Selection - -**Action:** Select a second MCP server checkbox. - -**Expected Console Output:** -``` -[MCP Server Selection] Gateway ID: 885756888a8d43f2a62791e50c388494, Name: mcp-http, Checked: true -[Filter Update] Reloading associated items for gateway IDs: df212f5e971a41e3917edcfae9a66711,885756888a8d43f2a62791e50c388494 -``` - -**Expected Behavior:** -- Tools/resources/prompts from BOTH selected gateways should now be visible - -### 5. Test Deselection - -**Action:** Uncheck one of the selected MCP servers. - -**Expected Console Output:** -``` -[MCP Server Selection] Gateway ID: df212f5e971a41e3917edcfae9a66711, Name: mcp-server, Checked: false -[Filter Update] Reloading associated items for gateway IDs: 885756888a8d43f2a62791e50c388494 -``` - -**Expected Behavior:** -- Only tools/resources/prompts from the remaining selected gateway should be visible - -### 6. Test Clear All Button - -**Action:** Click the "Clear All" button under the Associated MCP Servers section. - -**Expected Console Output:** -``` -[Filter Update] Reloading associated items for gateway IDs: none (showing all) -``` - -**Expected Behavior:** -- All MCP server checkboxes should be unchecked -- ALL tools/resources/prompts should be displayed (no filter applied) - -### 7. Test Select All Button - -**Action:** Click the "Select All" button under the Associated MCP Servers section. - -**Expected Console Output:** -``` -[Filter Update] Reloading associated items for gateway IDs: -``` - -**Expected Behavior:** -- All visible MCP server checkboxes should be checked -- Tools/resources/prompts from ALL selected gateways should be displayed - -### 8. Test Search + Filter - -**Action:** -1. Type a search term in the "Search for MCP servers..." box -2. Select one of the filtered MCP servers - -**Expected Behavior:** -- Search should filter the visible MCP servers -- Selecting a filtered server should still trigger the filter reload -- Tools/resources/prompts should update based on selected gateway - -## Validation Points - -### ✅ Gateway ID Logging -- [ ] Gateway ID is logged when checkbox is clicked -- [ ] Gateway name is shown in the log -- [ ] Checked status (true/false) is accurate - -### ✅ Filter Application -- [ ] Tools list reloads with gateway_id parameter -- [ ] Resources list reloads with gateway_id parameter -- [ ] Prompts list reloads with gateway_id parameter -- [ ] Multiple gateway IDs are comma-separated in the URL - -### ✅ UI Responsiveness -- [ ] Lists reload smoothly without page refresh -- [ ] Loading indicators appear during reload (if implemented) -- [ ] Previously selected tools/resources/prompts are cleared after filter change -- [ ] Pills/badges update correctly to show selected items - -### ✅ Edge Cases -- [ ] Works with no gateways selected (shows all items) -- [ ] Works with one gateway selected -- [ ] Works with multiple gateways selected -- [ ] Works after using "Select All" -- [ ] Works after using "Clear All" -- [ ] Works with search filter active - -## Network Inspection - -Open the Network tab in Developer Tools to verify the correct API calls: - -**Expected API Calls:** -``` -GET /admin/tools/partial?page=1&per_page=50&render=selector&gateway_id=df212f5e971a41e3917edcfae9a66711 -GET /admin/resources/partial?page=1&per_page=50&render=selector&gateway_id=df212f5e971a41e3917edcfae9a66711 -GET /admin/prompts/partial?page=1&per_page=50&render=selector&gateway_id=df212f5e971a41e3917edcfae9a66711 -``` - -## Troubleshooting - -### Issue: No console logs appear -**Solution:** Make sure browser console is open and not filtered. Check that JavaScript is enabled. - -### Issue: Lists don't reload -**Solution:** Check if HTMX is loaded (`window.htmx` should be defined in console). Verify network requests are succeeding. - -### Issue: Wrong items shown after filtering -**Solution:** Check the gateway_id parameter in the network tab. Verify the backend returns correct data for those IDs. - -### Issue: "initToolSelect is not defined" error -**Solution:** Ensure the HTMX response includes the callback to initialize the select functions, or verify the functions are called after content loads. - -## Code Locations - -### JavaScript Functions -- `getSelectedGatewayIds()` - Collects all selected gateway IDs -- `reloadAssociatedItems()` - Triggers reload of tools/resources/prompts -- `initGatewaySelect()` - Main initialization function for gateway selection - -### Backend Endpoints -- `mcpgateway/admin.py` - `admin_tools_partial_html()`, `admin_resources_partial_html()`, `admin_prompts_partial_html()` - -### Frontend Files -- `mcpgateway/static/admin.js` - Main JavaScript implementation -- `mcpgateway/templates/admin.html` - HTML template with HTMX attributes From 8b81ab3a5b3202a044c155fdfebc94a1814e2d08 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 14 Nov 2025 16:57:09 +0530 Subject: [PATCH 14/33] docstring server Signed-off-by: rakdutta --- mcpgateway/services/server_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mcpgateway/services/server_service.py b/mcpgateway/services/server_service.py index d0496a260..f93c21df3 100644 --- a/mcpgateway/services/server_service.py +++ b/mcpgateway/services/server_service.py @@ -284,22 +284,22 @@ def _assemble_associated_items( >>> # Test with all None values >>> result = service._assemble_associated_items(None, None, None) >>> result - {'tools': [], 'resources': [], 'prompts': [], 'a2a_agents': []} + {'tools': [], 'resources': [], 'prompts': [], 'a2a_agents': [], 'gateways': []} >>> # Test with empty lists >>> result = service._assemble_associated_items([], [], []) >>> result - {'tools': [], 'resources': [], 'prompts': [], 'a2a_agents': []} + {'tools': [], 'resources': [], 'prompts': [], 'a2a_agents': [], 'gateways': []} >>> # Test with actual values >>> result = service._assemble_associated_items(['tool1', 'tool2'], ['res1'], ['prompt1']) >>> result - {'tools': ['tool1', 'tool2'], 'resources': ['res1'], 'prompts': ['prompt1'], 'a2a_agents': []} + {'tools': ['tool1', 'tool2'], 'resources': ['res1'], 'prompts': ['prompt1'], 'a2a_agents': [], 'gateways': []} >>> # Test with mixed None and values >>> result = service._assemble_associated_items(['tool1'], None, ['prompt1']) >>> result - {'tools': ['tool1'], 'resources': [], 'prompts': ['prompt1'], 'a2a_agents': []} + {'tools': ['tool1'], 'resources': [], 'prompts': ['prompt1'], 'a2a_agents': [], 'gateways': []} """ return { "tools": tools or [], From 00ffd7ff1a2ca1dd611038f29e4909bfa8cfc9f2 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 14 Nov 2025 17:33:29 +0530 Subject: [PATCH 15/33] lint fix admin.js Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 287 +++++++++++++++++++++++-------------- 1 file changed, 180 insertions(+), 107 deletions(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 2da31eea1..c0094dbd7 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7290,20 +7290,22 @@ function initPromptSelect( // GATEWAY SELECT (Associated MCP Servers) - search/select/clear // =================================================================== function initGatewaySelect( - selectId = 'associatedGateways', - pillsId = 'selectedGatewayPills', - warnId = 'selectedGatewayWarning', + selectId = "associatedGateways", + pillsId = "selectedGatewayPills", + warnId = "selectedGatewayWarning", max = 12, - selectBtnId = 'selectAllGatewayBtn', - clearBtnId = 'clearAllGatewayBtn', - searchInputId = 'searchGateways', + selectBtnId = "selectAllGatewayBtn", + clearBtnId = "clearAllGatewayBtn", + searchInputId = "searchGateways", ) { const container = document.getElementById(selectId); const pillsBox = document.getElementById(pillsId); const warnBox = document.getElementById(warnId); const clearBtn = clearBtnId ? document.getElementById(clearBtnId) : null; const selectBtn = selectBtnId ? document.getElementById(selectBtnId) : null; - const searchInput = searchInputId ? document.getElementById(searchInputId) : null; + const searchInput = searchInputId + ? document.getElementById(searchInputId) + : null; if (!container || !pillsBox || !warnBox) { console.warn( @@ -7317,34 +7319,36 @@ function initGatewaySelect( // Search functionality function applySearch() { - if (!searchInput) return; - + if (!searchInput) { + return; + } + try { const query = searchInput.value.toLowerCase().trim(); - const items = container.querySelectorAll('.tool-item'); + const items = container.querySelectorAll(".tool-item"); let visibleCount = 0; - items.forEach(item => { + items.forEach((item) => { const text = item.textContent.toLowerCase(); if (!query || text.includes(query)) { - item.style.display = ''; + item.style.display = ""; visibleCount++; } else { - item.style.display = 'none'; + item.style.display = "none"; } }); // Update "no results" message if it exists - const noMsg = document.getElementById('noGatewayMessage'); - const searchQuerySpan = document.getElementById('searchQuery'); + const noMsg = document.getElementById("noGatewayMessage"); + const searchQuerySpan = document.getElementById("searchQuery"); if (noMsg) { if (query && visibleCount === 0) { - noMsg.style.display = 'block'; + noMsg.style.display = "block"; if (searchQuerySpan) { searchQuerySpan.textContent = query; } } else { - noMsg.style.display = 'none'; + noMsg.style.display = "none"; } } } catch (error) { @@ -7354,8 +7358,8 @@ function initGatewaySelect( // Bind search input if (searchInput && !searchInput.dataset.searchBound) { - searchInput.addEventListener('input', applySearch); - searchInput.dataset.searchBound = 'true'; + searchInput.addEventListener("input", applySearch); + searchInput.dataset.searchBound = "true"; } function update() { @@ -7450,7 +7454,7 @@ function initGatewaySelect( } update(); - + // Reload associated items after clearing selection reloadAssociatedItems(); }); @@ -7488,8 +7492,9 @@ function initGatewaySelect( 'input[type="checkbox"]', ); loadedCheckboxes.forEach((cb) => { - const parent = cb.closest('.tool-item') || cb.parentElement; - const isVisible = parent && getComputedStyle(parent).display !== 'none'; + const parent = cb.closest(".tool-item") || cb.parentElement; + const isVisible = + parent && getComputedStyle(parent).display !== "none"; if (isVisible) { cb.checked = true; } @@ -7526,7 +7531,7 @@ function initGatewaySelect( setTimeout(() => { newSelectBtn.textContent = originalText; }, 2000); - + // Reload associated items after selecting all reloadAssociatedItems(); } catch (error) { @@ -7549,11 +7554,15 @@ function initGatewaySelect( if (e.target.type === "checkbox") { // Log gateway_id when checkbox is clicked const gatewayId = e.target.value; - const gatewayName = e.target.nextElementSibling?.textContent?.trim() || "Unknown"; + const gatewayName = + e.target.nextElementSibling?.textContent?.trim() || + "Unknown"; const isChecked = e.target.checked; - - console.log(`[MCP Server Selection] Gateway ID: ${gatewayId}, Name: ${gatewayName}, Checked: ${isChecked}`); - + + console.log( + `[MCP Server Selection] Gateway ID: ${gatewayId}, Name: ${gatewayName}, Checked: ${isChecked}`, + ); + // Check if we're in "Select All" mode const selectAllInput = container.querySelector( 'input[name="selectAllGateways"]', @@ -7590,7 +7599,7 @@ function initGatewaySelect( } update(); - + // Trigger reload of associated tools, resources, and prompts with selected gateway filter reloadAssociatedItems(); } @@ -7607,37 +7616,57 @@ function initGatewaySelect( * @returns {string[]} Array of selected gateway IDs */ function getSelectedGatewayIds() { - const container = document.getElementById('associatedGateways'); + const container = document.getElementById("associatedGateways"); console.log(`[Gateway Selection DEBUG] Container found:`, !!container); - + if (!container) { - console.warn(`[Gateway Selection DEBUG] associatedGateways container not found`); + console.warn( + `[Gateway Selection DEBUG] associatedGateways container not found`, + ); return []; } - + // Check if "Select All" mode is active - const selectAllInput = container.querySelector('input[name="selectAllGateways"]'); - const allIdsInput = container.querySelector('input[name="allGatewayIds"]'); - - console.log(`[Gateway Selection DEBUG] Select All mode:`, selectAllInput?.value === "true"); - + const selectAllInput = container.querySelector( + 'input[name="selectAllGateways"]', + ); + const allIdsInput = container.querySelector( + 'input[name="allGatewayIds"]', + ); + + console.log( + `[Gateway Selection DEBUG] Select All mode:`, + selectAllInput?.value === "true", + ); if (selectAllInput && selectAllInput.value === "true" && allIdsInput) { try { const allIds = JSON.parse(allIdsInput.value); - console.log(`[Gateway Selection DEBUG] Returning all gateway IDs (${allIds.length} total)`); + console.log( + `[Gateway Selection DEBUG] Returning all gateway IDs (${allIds.length} total)`, + ); return allIds; } catch (error) { - console.error("[Gateway Selection DEBUG] Error parsing allGatewayIds:", error); + console.error( + "[Gateway Selection DEBUG] Error parsing allGatewayIds:", + error, + ); } } - + // Otherwise, get all checked checkboxes - const checkboxes = container.querySelectorAll('input[type="checkbox"]:checked'); - const selectedIds = Array.from(checkboxes).map(cb => cb.value); - - console.log(`[Gateway Selection DEBUG] Found ${checkboxes.length} checked gateway checkboxes`); - console.log(`[Gateway Selection DEBUG] Selected gateway IDs:`, selectedIds); - + const checkboxes = container.querySelectorAll( + 'input[type="checkbox"]:checked', + ); + const selectedIds = Array.from(checkboxes).map((cb) => cb.value); + + console.log( + `[Gateway Selection DEBUG] Found ${checkboxes.length} checked gateway checkboxes`, + ); + console.log( + `[Gateway Selection DEBUG] Selected gateway IDs:`, + selectedIds, + ); + return selectedIds; } @@ -7646,107 +7675,153 @@ function getSelectedGatewayIds() { */ function reloadAssociatedItems() { const selectedGatewayIds = getSelectedGatewayIds(); - const gatewayIdParam = selectedGatewayIds.length > 0 ? selectedGatewayIds.join(',') : ''; - - console.log(`[Filter Update] Reloading associated items for gateway IDs: ${gatewayIdParam || 'none (showing all)'}`); - console.log(`[Filter Update DEBUG] Selected gateway IDs array:`, selectedGatewayIds); - + const gatewayIdParam = + selectedGatewayIds.length > 0 ? selectedGatewayIds.join(",") : ""; + + console.log( + `[Filter Update] Reloading associated items for gateway IDs: ${gatewayIdParam || "none (showing all)"}`, + ); + console.log( + `[Filter Update DEBUG] Selected gateway IDs array:`, + selectedGatewayIds, + ); + // Reload tools - const toolsContainer = document.getElementById('associatedTools'); + const toolsContainer = document.getElementById("associatedTools"); if (toolsContainer) { - const toolsUrl = gatewayIdParam + const toolsUrl = gatewayIdParam ? `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` : `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector`; - + console.log(`[Filter Update DEBUG] Tools URL:`, toolsUrl); - + // Use HTMX to reload the content if (window.htmx) { - htmx.ajax('GET', toolsUrl, { - target: '#associatedTools', - swap: 'innerHTML' - }).then(() => { - console.log(`[Filter Update DEBUG] Tools reloaded successfully`); - // Re-initialize the tool select after content is loaded - initToolSelect('associatedTools', 'selectedToolsPills', 'selectedToolsWarning', 6, 'selectAllToolsBtn', 'clearAllToolsBtn'); - }).catch(err => { - console.error(`[Filter Update DEBUG] Tools reload failed:`, err); - }); + htmx.ajax("GET", toolsUrl, { + target: "#associatedTools", + swap: "innerHTML", + }) + .then(() => { + console.log( + `[Filter Update DEBUG] Tools reloaded successfully`, + ); + // Re-initialize the tool select after content is loaded + initToolSelect( + "associatedTools", + "selectedToolsPills", + "selectedToolsWarning", + 6, + "selectAllToolsBtn", + "clearAllToolsBtn", + ); + }) + .catch((err) => { + console.error( + `[Filter Update DEBUG] Tools reload failed:`, + err, + ); + }); } else { - console.error(`[Filter Update DEBUG] HTMX not available for tools reload`); + console.error( + `[Filter Update DEBUG] HTMX not available for tools reload`, + ); } } else { console.warn(`[Filter Update DEBUG] Tools container not found`); } - + // Reload resources - use fetch directly to avoid HTMX race conditions - const resourcesContainer = document.getElementById('associatedResources'); + const resourcesContainer = document.getElementById("associatedResources"); if (resourcesContainer) { const resourcesUrl = gatewayIdParam ? `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` : `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector`; - + console.log(`[Filter Update DEBUG] Resources URL:`, resourcesUrl); - + // Use fetch() directly instead of htmx.ajax() to avoid race conditions fetch(resourcesUrl, { - method: 'GET', + method: "GET", headers: { - 'HX-Request': 'true', - 'HX-Current-URL': window.location.href - } - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response.text(); - }) - .then(html => { - console.log(`[Filter Update DEBUG] Resources fetch successful, HTML length:`, html.length); - resourcesContainer.innerHTML = html; - // Re-initialize the resource select after content is loaded - initResourceSelect('associatedResources', 'selectedResourcesPills', 'selectedResourcesWarning', 6, 'selectAllResourcesBtn', 'clearAllResourcesBtn'); - console.log(`[Filter Update DEBUG] Resources reloaded successfully via fetch`); + "HX-Request": "true", + "HX-Current-URL": window.location.href, + }, }) - .catch(err => { - console.error(`[Filter Update DEBUG] Resources reload failed:`, err); - }); + .then((response) => { + if (!response.ok) { + throw new Error( + `HTTP ${response.status}: ${response.statusText}`, + ); + } + return response.text(); + }) + .then((html) => { + console.log( + `[Filter Update DEBUG] Resources fetch successful, HTML length:`, + html.length, + ); + resourcesContainer.innerHTML = html; + // Re-initialize the resource select after content is loaded + initResourceSelect( + "associatedResources", + "selectedResourcesPills", + "selectedResourcesWarning", + 6, + "selectAllResourcesBtn", + "clearAllResourcesBtn", + ); + console.log( + `[Filter Update DEBUG] Resources reloaded successfully via fetch`, + ); + }) + .catch((err) => { + console.error( + `[Filter Update DEBUG] Resources reload failed:`, + err, + ); + }); } else { console.warn(`[Filter Update DEBUG] Resources container not found`); } - + // Reload prompts - const promptsContainer = document.getElementById('associatedPrompts'); + const promptsContainer = document.getElementById("associatedPrompts"); if (promptsContainer) { const promptsUrl = gatewayIdParam ? `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` : `${window.ROOT_PATH}/admin/prompts/partial?page=1&per_page=50&render=selector`; - + if (window.htmx) { - htmx.ajax('GET', promptsUrl, { - target: '#associatedPrompts', - swap: 'innerHTML' + htmx.ajax("GET", promptsUrl, { + target: "#associatedPrompts", + swap: "innerHTML", }).then(() => { // Re-initialize the prompt select after content is loaded - initPromptSelect('associatedPrompts', 'selectedPromptsPills', 'selectedPromptsWarning', 6, 'selectAllPromptsBtn', 'clearAllPromptsBtn'); + initPromptSelect( + "associatedPrompts", + "selectedPromptsPills", + "selectedPromptsWarning", + 6, + "selectAllPromptsBtn", + "clearAllPromptsBtn", + ); }); } } } // Initialize gateway select on page load -document.addEventListener('DOMContentLoaded', function() { +document.addEventListener("DOMContentLoaded", function () { // Initialize for the create server form - if (document.getElementById('associatedGateways')) { + if (document.getElementById("associatedGateways")) { initGatewaySelect( - 'associatedGateways', - 'selectedGatewayPills', - 'selectedGatewayWarning', + "associatedGateways", + "selectedGatewayPills", + "selectedGatewayWarning", 12, - 'selectAllGatewayBtn', - 'clearAllGatewayBtn', - 'searchGateways' + "selectAllGatewayBtn", + "clearAllGatewayBtn", + "searchGateways", ); } }); @@ -11211,7 +11286,6 @@ function initializeCodeMirrorEditors() { function initializeToolSelects() { console.log("Initializing tool selects..."); - // Add Server form initToolSelect( "associatedTools", @@ -11220,7 +11294,6 @@ function initializeToolSelects() { 6, "selectAllToolsBtn", "clearAllToolsBtn", - ); From 4e0f6363a6f245a030df587ffcf1e7ff47d9a835 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 14 Nov 2025 17:44:34 +0530 Subject: [PATCH 16/33] lint fix Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 44 ++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index c0094dbd7..aedf7b2e2 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7617,25 +7617,23 @@ function initGatewaySelect( */ function getSelectedGatewayIds() { const container = document.getElementById("associatedGateways"); - console.log(`[Gateway Selection DEBUG] Container found:`, !!container); + console.log("[Gateway Selection DEBUG] Container found:", !!container); if (!container) { console.warn( - `[Gateway Selection DEBUG] associatedGateways container not found`, + "[Gateway Selection DEBUG] associatedGateways container not found", ); return []; } // Check if "Select All" mode is active const selectAllInput = container.querySelector( - 'input[name="selectAllGateways"]', - ); - const allIdsInput = container.querySelector( - 'input[name="allGatewayIds"]', + "input[name='selectAllGateways']", ); + const allIdsInput = container.querySelector("input[name='allGatewayIds']"); console.log( - `[Gateway Selection DEBUG] Select All mode:`, + "[Gateway Selection DEBUG] Select All mode:", selectAllInput?.value === "true", ); if (selectAllInput && selectAllInput.value === "true" && allIdsInput) { @@ -7655,17 +7653,14 @@ function getSelectedGatewayIds() { // Otherwise, get all checked checkboxes const checkboxes = container.querySelectorAll( - 'input[type="checkbox"]:checked', + "input[type='checkbox']:checked", ); const selectedIds = Array.from(checkboxes).map((cb) => cb.value); console.log( `[Gateway Selection DEBUG] Found ${checkboxes.length} checked gateway checkboxes`, ); - console.log( - `[Gateway Selection DEBUG] Selected gateway IDs:`, - selectedIds, - ); + console.log("[Gateway Selection DEBUG] Selected gateway IDs:", selectedIds); return selectedIds; } @@ -7682,7 +7677,7 @@ function reloadAssociatedItems() { `[Filter Update] Reloading associated items for gateway IDs: ${gatewayIdParam || "none (showing all)"}`, ); console.log( - `[Filter Update DEBUG] Selected gateway IDs array:`, + "[Filter Update DEBUG] Selected gateway IDs array:", selectedGatewayIds, ); @@ -7693,7 +7688,7 @@ function reloadAssociatedItems() { ? `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` : `${window.ROOT_PATH}/admin/tools/partial?page=1&per_page=50&render=selector`; - console.log(`[Filter Update DEBUG] Tools URL:`, toolsUrl); + console.log("[Filter Update DEBUG] Tools URL:", toolsUrl); // Use HTMX to reload the content if (window.htmx) { @@ -7703,7 +7698,7 @@ function reloadAssociatedItems() { }) .then(() => { console.log( - `[Filter Update DEBUG] Tools reloaded successfully`, + "[Filter Update DEBUG] Tools reloaded successfully", ); // Re-initialize the tool select after content is loaded initToolSelect( @@ -7717,17 +7712,17 @@ function reloadAssociatedItems() { }) .catch((err) => { console.error( - `[Filter Update DEBUG] Tools reload failed:`, + "[Filter Update DEBUG] Tools reload failed:", err, ); }); } else { console.error( - `[Filter Update DEBUG] HTMX not available for tools reload`, + "[Filter Update DEBUG] HTMX not available for tools reload", ); } } else { - console.warn(`[Filter Update DEBUG] Tools container not found`); + console.warn("[Filter Update DEBUG] Tools container not found"); } // Reload resources - use fetch directly to avoid HTMX race conditions @@ -7737,7 +7732,7 @@ function reloadAssociatedItems() { ? `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector&gateway_id=${encodeURIComponent(gatewayIdParam)}` : `${window.ROOT_PATH}/admin/resources/partial?page=1&per_page=50&render=selector`; - console.log(`[Filter Update DEBUG] Resources URL:`, resourcesUrl); + console.log("[Filter Update DEBUG] Resources URL:", resourcesUrl); // Use fetch() directly instead of htmx.ajax() to avoid race conditions fetch(resourcesUrl, { @@ -7757,7 +7752,7 @@ function reloadAssociatedItems() { }) .then((html) => { console.log( - `[Filter Update DEBUG] Resources fetch successful, HTML length:`, + "[Filter Update DEBUG] Resources fetch successful, HTML length:", html.length, ); resourcesContainer.innerHTML = html; @@ -7771,17 +7766,17 @@ function reloadAssociatedItems() { "clearAllResourcesBtn", ); console.log( - `[Filter Update DEBUG] Resources reloaded successfully via fetch`, + "[Filter Update DEBUG] Resources reloaded successfully via fetch", ); }) .catch((err) => { console.error( - `[Filter Update DEBUG] Resources reload failed:`, + "[Filter Update DEBUG] Resources reload failed:", err, ); }); } else { - console.warn(`[Filter Update DEBUG] Resources container not found`); + console.warn("[Filter Update DEBUG] Resources container not found"); } // Reload prompts @@ -11001,7 +10996,7 @@ if (window.performance && window.performance.mark) { // Tool Tips for components with Alpine.js // =================================================================== -/* global Alpine */ +/* global Alpine, htmx */ function setupTooltipsWithAlpine() { document.addEventListener("alpine:init", () => { console.log("Initializing Alpine tooltip directive..."); @@ -11296,7 +11291,6 @@ function initializeToolSelects() { "clearAllToolsBtn", ); - initResourceSelect( "associatedResources", "selectedResourcesPills", From 70e40a7f1eee65326db7160a3e4bb6446e3f5d1b Mon Sep 17 00:00:00 2001 From: rakdutta Date: Fri, 14 Nov 2025 17:51:29 +0530 Subject: [PATCH 17/33] lint fix js and html Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 6 ++++-- mcpgateway/templates/admin.html | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index aedf7b2e2..b4e91adab 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7340,7 +7340,9 @@ function initGatewaySelect( // Update "no results" message if it exists const noMsg = document.getElementById("noGatewayMessage"); - const searchQuerySpan = document.getElementById("searchQuery"); + const searchQuerySpan = + document.getElementById("searchQueryServers"); + if (noMsg) { if (query && visibleCount === 0) { noMsg.style.display = "block"; @@ -18522,7 +18524,7 @@ function initializeChatInputResize() { async function serverSideToolSearch(searchTerm) { const container = document.getElementById("associatedTools"); const noResultsMessage = safeGetElement("noToolsMessage", true); - const searchQuerySpan = safeGetElement("searchQuery", true); + const searchQuerySpan = safeGetElement("searchQueryTools", true); if (!container) { console.error("associatedTools container not found"); diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index 373628471..ea4aef67a 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -2075,7 +2075,7 @@

class="text-gray-700 dark:text-gray-300" style="display: none" > - No MCP server found containing "" + No MCP server found containing ""

@@ -2149,7 +2149,7 @@

class="text-gray-700 dark:text-gray-300 mt-2" style="display: none" > - No tool found containing "" + No tool found containing ""

+

+ Choose an MCP server, then select the tools, resources, and prompts to configure your virtual server. +

+
Date: Mon, 17 Nov 2025 16:58:24 +0530 Subject: [PATCH 24/33] black Signed-off-by: rakdutta --- mcpgateway/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 3178285fd..10ef6ae5d 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -5501,7 +5501,7 @@ async def admin_resources_partial_html( continue data = jsonable_encoder(resources_data) - + # Build pagination metadata pagination = PaginationMeta( page=page, From 07d18dbef135399d4f716565c2acb2e99c58575f Mon Sep 17 00:00:00 2001 From: rakdutta Date: Mon, 17 Nov 2025 18:50:53 +0530 Subject: [PATCH 25/33] bandit Signed-off-by: rakdutta --- mcpgateway/admin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 10ef6ae5d..fe270141c 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -2339,8 +2339,13 @@ def _matches_selected_team(item, tid: str) -> bool: vis = item.get("visibility") if isinstance(vis, str) and vis.lower() == "public": return True - except Exception: - pass + except Exception as exc: # pragma: no cover - defensive logging for unexpected types + LOGGER.debug( + "Error checking visibility on item (type=%s): %s", + type(item), + exc, + exc_info=True, + ) # item may be a pydantic model or dict-like # check common fields for team membership candidates = [] From d5ac4df930b841d008ec57345d3626719e719d9b Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 18 Nov 2025 17:23:31 +0530 Subject: [PATCH 26/33] add REST mcp server Signed-off-by: rakdutta --- mcpgateway/admin.py | 70 ++++++++++++++++++++++++++++----- mcpgateway/static/admin.js | 29 +++++++++++--- mcpgateway/templates/admin.html | 14 +++++++ 3 files changed, 98 insertions(+), 15 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index fe270141c..8c207ff8f 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -5003,12 +5003,23 @@ async def admin_tools_partial_html( # Build query query = select(DbTool) - # Apply gateway filter if provided + # Apply gateway filter if provided. Support special sentinel 'null' to + # request tools with NULL gateway_id (e.g., RestTool/no gateway). if gateway_id: gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] if gateway_ids: - query = query.where(DbTool.gateway_id.in_(gateway_ids)) - LOGGER.debug(f"Filtering tools by gateway IDs: {gateway_ids}") + # Treat literal 'null' (case-insensitive) as a request for NULL gateway_id + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + query = query.where(or_(DbTool.gateway_id.in_(non_null_ids), DbTool.gateway_id.is_(None))) + LOGGER.debug(f"Filtering tools by gateway IDs (including NULL): {non_null_ids} + NULL") + elif null_requested: + query = query.where(DbTool.gateway_id.is_(None)) + LOGGER.debug("Filtering tools by NULL gateway_id (RestTool)") + else: + query = query.where(DbTool.gateway_id.in_(non_null_ids)) + LOGGER.debug(f"Filtering tools by gateway IDs: {non_null_ids}") # Apply active/inactive filter if not include_inactive: @@ -5034,7 +5045,14 @@ async def admin_tools_partial_html( if gateway_id: gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] if gateway_ids: - count_query = count_query.where(DbTool.gateway_id.in_(gateway_ids)) + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + count_query = count_query.where(or_(DbTool.gateway_id.in_(non_null_ids), DbTool.gateway_id.is_(None))) + elif null_requested: + count_query = count_query.where(DbTool.gateway_id.is_(None)) + else: + count_query = count_query.where(DbTool.gateway_id.in_(non_null_ids)) if not include_inactive: count_query = count_query.where(DbTool.enabled.is_(True)) @@ -5304,8 +5322,17 @@ async def admin_prompts_partial_html( if gateway_id: gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] if gateway_ids: - query = query.where(DbPrompt.gateway_id.in_(gateway_ids)) - LOGGER.debug(f"Filtering prompts by gateway IDs: {gateway_ids}") + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + query = query.where(or_(DbPrompt.gateway_id.in_(non_null_ids), DbPrompt.gateway_id.is_(None))) + LOGGER.debug(f"Filtering prompts by gateway IDs (including NULL): {non_null_ids} + NULL") + elif null_requested: + query = query.where(DbPrompt.gateway_id.is_(None)) + LOGGER.debug("Filtering prompts by NULL gateway_id (RestTool)") + else: + query = query.where(DbPrompt.gateway_id.in_(non_null_ids)) + LOGGER.debug(f"Filtering prompts by gateway IDs: {non_null_ids}") if not include_inactive: query = query.where(DbPrompt.is_active.is_(True)) @@ -5323,7 +5350,14 @@ async def admin_prompts_partial_html( if gateway_id: gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] if gateway_ids: - count_query = count_query.where(DbPrompt.gateway_id.in_(gateway_ids)) + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + count_query = count_query.where(or_(DbPrompt.gateway_id.in_(non_null_ids), DbPrompt.gateway_id.is_(None))) + elif null_requested: + count_query = count_query.where(DbPrompt.gateway_id.is_(None)) + else: + count_query = count_query.where(DbPrompt.gateway_id.in_(non_null_ids)) if not include_inactive: count_query = count_query.where(DbPrompt.is_active.is_(True)) @@ -5461,8 +5495,17 @@ async def admin_resources_partial_html( if gateway_id: gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] if gateway_ids: - query = query.where(DbResource.gateway_id.in_(gateway_ids)) - LOGGER.debug(f"[RESOURCES FILTER DEBUG] Filtering resources by gateway IDs: {gateway_ids}") + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + query = query.where(or_(DbResource.gateway_id.in_(non_null_ids), DbResource.gateway_id.is_(None))) + LOGGER.debug(f"[RESOURCES FILTER DEBUG] Filtering resources by gateway IDs (including NULL): {non_null_ids} + NULL") + elif null_requested: + query = query.where(DbResource.gateway_id.is_(None)) + LOGGER.debug("[RESOURCES FILTER DEBUG] Filtering resources by NULL gateway_id (RestTool)") + else: + query = query.where(DbResource.gateway_id.in_(non_null_ids)) + LOGGER.debug(f"[RESOURCES FILTER DEBUG] Filtering resources by gateway IDs: {non_null_ids}") else: LOGGER.debug("[RESOURCES FILTER DEBUG] No gateway_id filter provided, showing all resources") @@ -5483,7 +5526,14 @@ async def admin_resources_partial_html( if gateway_id: gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] if gateway_ids: - count_query = count_query.where(DbResource.gateway_id.in_(gateway_ids)) + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + count_query = count_query.where(or_(DbResource.gateway_id.in_(non_null_ids), DbResource.gateway_id.is_(None))) + elif null_requested: + count_query = count_query.where(DbResource.gateway_id.is_(None)) + else: + count_query = count_query.where(DbResource.gateway_id.in_(non_null_ids)) if not include_inactive: count_query = count_query.where(DbResource.is_active.is_(True)) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index b4e91adab..ec573189a 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7600,6 +7600,10 @@ function initGatewaySelect( } } + // No exclusivity: allow the special 'null' gateway (RestTool) to be + // selected together with real gateways. Server-side filtering already + // supports mixed lists like `gateway_id=abc,null`. + update(); // Trigger reload of associated tools, resources, and prompts with selected gateway filter @@ -7653,14 +7657,25 @@ function getSelectedGatewayIds() { } } - // Otherwise, get all checked checkboxes + // Otherwise, get all checked checkboxes. If the special 'null' gateway + // checkbox is selected, include the sentinel 'null' alongside any real + // gateway ids. This allows requests like `gateway_id=abc,null` which the + // server interprets as (gateway_id = abc) OR (gateway_id IS NULL). const checkboxes = container.querySelectorAll( "input[type='checkbox']:checked", ); - const selectedIds = Array.from(checkboxes).map((cb) => cb.value); + + const selectedIds = Array.from(checkboxes) + .map((cb) => { + // Convert the special null-gateway checkbox to the literal 'null' + if (cb.dataset?.gatewayNull === "true") return "null"; + return cb.value; + }) + // Filter out any empty values to avoid sending empty CSV entries + .filter((id) => id !== "" && id !== null && id !== undefined); console.log( - `[Gateway Selection DEBUG] Found ${checkboxes.length} checked gateway checkboxes`, + `[Gateway Selection DEBUG] Found ${selectedIds.length} checked gateway checkboxes`, ); console.log("[Gateway Selection DEBUG] Selected gateway IDs:", selectedIds); @@ -7672,8 +7687,12 @@ function getSelectedGatewayIds() { */ function reloadAssociatedItems() { const selectedGatewayIds = getSelectedGatewayIds(); - const gatewayIdParam = - selectedGatewayIds.length > 0 ? selectedGatewayIds.join(",") : ""; + // Join all selected IDs (including the special 'null' sentinel if present) + // so the server receives a combined filter like `gateway_id=abc,null`. + let gatewayIdParam = ""; + if (selectedGatewayIds.length > 0) { + gatewayIdParam = selectedGatewayIds.join(","); + } console.log( `[Filter Update] Reloading associated items for gateway IDs: ${gatewayIdParam || "none (showing all)"}`, diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index e6b0918cd..00b2ee752 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -2070,6 +2070,20 @@

> {% endfor %} + + +

Date: Tue, 18 Nov 2025 18:20:16 +0530 Subject: [PATCH 27/33] select all Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 111 ++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 37 deletions(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index ec573189a..3b1269f61 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -6685,25 +6685,36 @@ function initToolSelect( newSelectBtn.textContent = "Selecting all tools..."; try { - // Fetch all tool IDs from the server - const response = await fetch( - `${window.ROOT_PATH}/admin/tools/ids`, - ); - if (!response.ok) { - throw new Error("Failed to fetch tool IDs"); - } - - const data = await response.json(); - const allToolIds = data.tool_ids || []; - - // Check all currently loaded checkboxes + // Prefer using currently visible (filtered) checkboxes when available const loadedCheckboxes = container.querySelectorAll( 'input[type="checkbox"]', ); - loadedCheckboxes.forEach((cb) => (cb.checked = true)); + const visibleCheckboxes = Array.from(loadedCheckboxes).filter( + (cb) => cb.offsetParent !== null, + ); + + let allToolIds = []; + + if (visibleCheckboxes.length > 0) { + // Use IDs from visible (filtered) items + allToolIds = visibleCheckboxes.map((cb) => cb.value); + // Check the visible ones + visibleCheckboxes.forEach((cb) => (cb.checked = true)); + } else { + // Fallback to fetching all tool IDs from the server + const response = await fetch( + `${window.ROOT_PATH}/admin/tools/ids`, + ); + if (!response.ok) { + throw new Error("Failed to fetch tool IDs"); + } + const data = await response.json(); + allToolIds = data.tool_ids || []; + // If nothing is visible (e.g. paginated), check all loaded checkboxes + loadedCheckboxes.forEach((cb) => (cb.checked = true)); + } // Add a hidden input to indicate "select all" mode - // Remove any existing one first let selectAllInput = container.querySelector( 'input[name="selectAllTools"]', ); @@ -6966,20 +6977,33 @@ function initResourceSelect( newSelectBtn.textContent = "Selecting all resources..."; try { - const resp = await fetch( - `${window.ROOT_PATH}/admin/resources/ids`, - ); - if (!resp.ok) { - throw new Error("Failed to fetch resource IDs"); - } - const data = await resp.json(); - const allIds = data.resource_ids || []; - - // Check all currently loaded checkboxes + // Prefer using currently visible (filtered) checkboxes when available const loadedCheckboxes = container.querySelectorAll( 'input[type="checkbox"]', ); - loadedCheckboxes.forEach((cb) => (cb.checked = true)); + const visibleCheckboxes = Array.from(loadedCheckboxes).filter( + (cb) => cb.offsetParent !== null, + ); + + let allIds = []; + + if (visibleCheckboxes.length > 0) { + // Use IDs from visible (filtered) items + allIds = visibleCheckboxes.map((cb) => cb.value); + visibleCheckboxes.forEach((cb) => (cb.checked = true)); + } else { + // Fallback to fetching all resource IDs from the server + const resp = await fetch( + `${window.ROOT_PATH}/admin/resources/ids`, + ); + if (!resp.ok) { + throw new Error("Failed to fetch resource IDs"); + } + const data = await resp.json(); + allIds = data.resource_ids || []; + // If nothing visible (paginated), check loaded checkboxes + loadedCheckboxes.forEach((cb) => (cb.checked = true)); + } // Add hidden select-all flag let selectAllInput = container.querySelector( @@ -7190,20 +7214,33 @@ function initPromptSelect( newSelectBtn.textContent = "Selecting all prompts..."; try { - const resp = await fetch( - `${window.ROOT_PATH}/admin/prompts/ids`, - ); - if (!resp.ok) { - throw new Error("Failed to fetch prompt IDs"); - } - const data = await resp.json(); - const allIds = data.prompt_ids || []; - - // Check all currently loaded checkboxes + // Prefer using currently visible (filtered) checkboxes when available const loadedCheckboxes = container.querySelectorAll( 'input[type="checkbox"]', ); - loadedCheckboxes.forEach((cb) => (cb.checked = true)); + const visibleCheckboxes = Array.from(loadedCheckboxes).filter( + (cb) => cb.offsetParent !== null, + ); + + let allIds = []; + + if (visibleCheckboxes.length > 0) { + // Use IDs from visible (filtered) items + allIds = visibleCheckboxes.map((cb) => cb.value); + visibleCheckboxes.forEach((cb) => (cb.checked = true)); + } else { + // Fallback to fetching all prompt IDs from the server + const resp = await fetch( + `${window.ROOT_PATH}/admin/prompts/ids`, + ); + if (!resp.ok) { + throw new Error("Failed to fetch prompt IDs"); + } + const data = await resp.json(); + allIds = data.prompt_ids || []; + // If nothing visible (paginated), check loaded checkboxes + loadedCheckboxes.forEach((cb) => (cb.checked = true)); + } // Add hidden select-all flag let selectAllInput = container.querySelector( @@ -7600,7 +7637,7 @@ function initGatewaySelect( } } - // No exclusivity: allow the special 'null' gateway (RestTool) to be + // No exclusivity: allow the special 'null' gateway (RestTool/Prompts/Resources) to be // selected together with real gateways. Server-side filtering already // supports mixed lists like `gateway_id=abc,null`. From c0c313e3d3fb6bd7e2354b02521a890b77b142e0 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 18 Nov 2025 19:04:37 +0530 Subject: [PATCH 28/33] select all Signed-off-by: rakdutta --- mcpgateway/admin.py | 54 ++++++++++++++++++++++++++++++++++++++ mcpgateway/static/admin.js | 54 ++++++++++++++++++++++++++------------ 2 files changed, 91 insertions(+), 17 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 8c207ff8f..6b5517500 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -5146,6 +5146,7 @@ async def admin_tools_partial_html( @admin_router.get("/tools/ids", response_class=JSONResponse) async def admin_get_all_tool_ids( include_inactive: bool = False, + gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"), db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): @@ -5174,6 +5175,23 @@ async def admin_get_all_tool_ids( if not include_inactive: query = query.where(DbTool.enabled.is_(True)) + # Apply optional gateway/server scoping (comma-separated ids). Accepts the + # literal value 'null' to indicate NULL gateway_id (local tools). + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] + if gateway_ids: + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + query = query.where(or_(DbTool.gateway_id.in_(non_null_ids), DbTool.gateway_id.is_(None))) + LOGGER.debug(f"Filtering tools by gateway IDs (including NULL): {non_null_ids} + NULL") + elif null_requested: + query = query.where(DbTool.gateway_id.is_(None)) + LOGGER.debug("Filtering tools by NULL gateway_id (local tools)") + else: + query = query.where(DbTool.gateway_id.in_(non_null_ids)) + LOGGER.debug(f"Filtering tools by gateway IDs: {non_null_ids}") + # Build access conditions access_conditions = [DbTool.owner_email == user_email, DbTool.visibility == "public"] if team_ids: @@ -5618,6 +5636,7 @@ async def admin_resources_partial_html( @admin_router.get("/prompts/ids", response_class=JSONResponse) async def admin_get_all_prompt_ids( include_inactive: bool = False, + gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"), db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): @@ -5642,6 +5661,23 @@ async def admin_get_all_prompt_ids( team_ids = [t.id for t in user_teams] query = select(DbPrompt.id) + + # Apply optional gateway/server scoping + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] + if gateway_ids: + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + query = query.where(or_(DbPrompt.gateway_id.in_(non_null_ids), DbPrompt.gateway_id.is_(None))) + LOGGER.debug(f"Filtering prompts by gateway IDs (including NULL): {non_null_ids} + NULL") + elif null_requested: + query = query.where(DbPrompt.gateway_id.is_(None)) + LOGGER.debug("Filtering prompts by NULL gateway_id (RestTool)") + else: + query = query.where(DbPrompt.gateway_id.in_(non_null_ids)) + LOGGER.debug(f"Filtering prompts by gateway IDs: {non_null_ids}") + if not include_inactive: query = query.where(DbPrompt.is_active.is_(True)) @@ -5657,6 +5693,7 @@ async def admin_get_all_prompt_ids( @admin_router.get("/resources/ids", response_class=JSONResponse) async def admin_get_all_resource_ids( include_inactive: bool = False, + gateway_id: Optional[str] = Query(None, description="Filter by gateway ID(s), comma-separated"), db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ): @@ -5681,6 +5718,23 @@ async def admin_get_all_resource_ids( team_ids = [t.id for t in user_teams] query = select(DbResource.id) + + # Apply optional gateway/server scoping + if gateway_id: + gateway_ids = [gid.strip() for gid in gateway_id.split(",") if gid.strip()] + if gateway_ids: + null_requested = any(gid.lower() == "null" for gid in gateway_ids) + non_null_ids = [gid for gid in gateway_ids if gid.lower() != "null"] + if non_null_ids and null_requested: + query = query.where(or_(DbResource.gateway_id.in_(non_null_ids), DbResource.gateway_id.is_(None))) + LOGGER.debug(f"Filtering resources by gateway IDs (including NULL): {non_null_ids} + NULL") + elif null_requested: + query = query.where(DbResource.gateway_id.is_(None)) + LOGGER.debug("Filtering resources by NULL gateway_id (RestTool)") + else: + query = query.where(DbResource.gateway_id.in_(non_null_ids)) + LOGGER.debug(f"Filtering resources by gateway IDs: {non_null_ids}") + if not include_inactive: query = query.where(DbResource.is_active.is_(True)) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 3b1269f61..a250b24bd 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -6685,7 +6685,7 @@ function initToolSelect( newSelectBtn.textContent = "Selecting all tools..."; try { - // Prefer using currently visible (filtered) checkboxes when available + // Prefer full-set selection when pagination/infinite-scroll is present const loadedCheckboxes = container.querySelectorAll( 'input[type="checkbox"]', ); @@ -6693,24 +6693,30 @@ function initToolSelect( (cb) => cb.offsetParent !== null, ); + // Detect pagination/infinite-scroll controls for tools + const hasPaginationControls = !!document.getElementById("tools-pagination-controls"); + const hasScrollTrigger = !!document.querySelector("[id^='tools-scroll-trigger']"); + const isPaginated = hasPaginationControls || hasScrollTrigger; + let allToolIds = []; - if (visibleCheckboxes.length > 0) { - // Use IDs from visible (filtered) items + if (!isPaginated && visibleCheckboxes.length > 0) { + // No pagination and some visible items => select visible set allToolIds = visibleCheckboxes.map((cb) => cb.value); - // Check the visible ones visibleCheckboxes.forEach((cb) => (cb.checked = true)); } else { - // Fallback to fetching all tool IDs from the server + // Paginated (or no visible items) => fetch full set from server + const selectedGatewayIds = getSelectedGatewayIds ? getSelectedGatewayIds() : []; + const gatewayParam = selectedGatewayIds && selectedGatewayIds.length ? `?gateway_id=${encodeURIComponent(selectedGatewayIds.join(","))}` : ""; const response = await fetch( - `${window.ROOT_PATH}/admin/tools/ids`, + `${window.ROOT_PATH}/admin/tools/ids${gatewayParam}`, ); if (!response.ok) { throw new Error("Failed to fetch tool IDs"); } const data = await response.json(); allToolIds = data.tool_ids || []; - // If nothing is visible (e.g. paginated), check all loaded checkboxes + // Check loaded checkboxes so UI shows selection where possible loadedCheckboxes.forEach((cb) => (cb.checked = true)); } @@ -6977,7 +6983,7 @@ function initResourceSelect( newSelectBtn.textContent = "Selecting all resources..."; try { - // Prefer using currently visible (filtered) checkboxes when available + // Prefer full-set selection when pagination/infinite-scroll is present const loadedCheckboxes = container.querySelectorAll( 'input[type="checkbox"]', ); @@ -6985,16 +6991,23 @@ function initResourceSelect( (cb) => cb.offsetParent !== null, ); + // Detect pagination/infinite-scroll controls for resources + const hasPaginationControls = !!document.getElementById("resources-pagination-controls"); + const hasScrollTrigger = !!document.querySelector("[id^='resources-scroll-trigger']"); + const isPaginated = hasPaginationControls || hasScrollTrigger; + let allIds = []; - if (visibleCheckboxes.length > 0) { - // Use IDs from visible (filtered) items + if (!isPaginated && visibleCheckboxes.length > 0) { + // No pagination and some visible items => select visible set allIds = visibleCheckboxes.map((cb) => cb.value); visibleCheckboxes.forEach((cb) => (cb.checked = true)); } else { - // Fallback to fetching all resource IDs from the server + // Paginated (or no visible items) => fetch full set from server + const selectedGatewayIds = getSelectedGatewayIds ? getSelectedGatewayIds() : []; + const gatewayParam = selectedGatewayIds && selectedGatewayIds.length ? `?gateway_id=${encodeURIComponent(selectedGatewayIds.join(","))}` : ""; const resp = await fetch( - `${window.ROOT_PATH}/admin/resources/ids`, + `${window.ROOT_PATH}/admin/resources/ids${gatewayParam}`, ); if (!resp.ok) { throw new Error("Failed to fetch resource IDs"); @@ -7214,7 +7227,7 @@ function initPromptSelect( newSelectBtn.textContent = "Selecting all prompts..."; try { - // Prefer using currently visible (filtered) checkboxes when available + // Prefer full-set selection when pagination/infinite-scroll is present const loadedCheckboxes = container.querySelectorAll( 'input[type="checkbox"]', ); @@ -7222,16 +7235,23 @@ function initPromptSelect( (cb) => cb.offsetParent !== null, ); + // Detect pagination/infinite-scroll controls for prompts + const hasPaginationControls = !!document.getElementById("prompts-pagination-controls"); + const hasScrollTrigger = !!document.querySelector("[id^='prompts-scroll-trigger']"); + const isPaginated = hasPaginationControls || hasScrollTrigger; + let allIds = []; - if (visibleCheckboxes.length > 0) { - // Use IDs from visible (filtered) items + if (!isPaginated && visibleCheckboxes.length > 0) { + // No pagination and some visible items => select visible set allIds = visibleCheckboxes.map((cb) => cb.value); visibleCheckboxes.forEach((cb) => (cb.checked = true)); } else { - // Fallback to fetching all prompt IDs from the server + // Paginated (or no visible items) => fetch full set from server + const selectedGatewayIds = getSelectedGatewayIds ? getSelectedGatewayIds() : []; + const gatewayParam = selectedGatewayIds && selectedGatewayIds.length ? `?gateway_id=${encodeURIComponent(selectedGatewayIds.join(","))}` : ""; const resp = await fetch( - `${window.ROOT_PATH}/admin/prompts/ids`, + `${window.ROOT_PATH}/admin/prompts/ids${gatewayParam}`, ); if (!resp.ok) { throw new Error("Failed to fetch prompt IDs"); From 7fe7b10752fbb0061a62047148b3779110f19e44 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 18 Nov 2025 19:22:28 +0530 Subject: [PATCH 29/33] REST-select-all Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index a250b24bd..f5ed20df8 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7573,6 +7573,22 @@ function initGatewaySelect( selectAllInput.value = "true"; // Also store the IDs as a JSON array for the backend + // Ensure the special 'null' sentinel is included when selecting all + try { + const nullCheckbox = container.querySelector( + 'input[data-gateway-null="true"]', + ); + if (nullCheckbox) { + // Include the literal string "null" so server-side + // `any(gid.lower() == 'null' ...)` evaluates to true. + if (!allGatewayIds.includes("null")) { + allGatewayIds.push("null"); + } + } + } catch (err) { + console.error("Error ensuring null sentinel in gateway IDs:", err); + } + let allIdsInput = container.querySelector( 'input[name="allGatewayIds"]', ); From 43eb85ea77a710db7818dba07a6dd460781e8247 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 19 Nov 2025 10:47:45 +0530 Subject: [PATCH 30/33] lint flake8 Signed-off-by: rakdutta --- mcpgateway/admin.py | 3 ++ mcpgateway/static/admin.js | 60 +++++++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 6b5517500..18c53a7cf 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -5157,6 +5157,7 @@ async def admin_get_all_tool_ids( Args: include_inactive (bool): Whether to include inactive tools in the results + gateway_id (Optional[str]): Filter by gateway ID(s), comma-separated. Accepts the literal value 'null' to indicate NULL gateway_id (local tools). db (Session): Database session dependency user: Current user making the request @@ -5647,6 +5648,7 @@ async def admin_get_all_prompt_ids( Args: include_inactive (bool): When True include prompts that are inactive. + gateway_id (Optional[str]): Filter by gateway ID(s), comma-separated. Accepts the literal value 'null' to indicate NULL gateway_id (local prompts). db (Session): Database session (injected dependency). user: Authenticated user object from dependency injection. @@ -5704,6 +5706,7 @@ async def admin_get_all_resource_ids( Args: include_inactive (bool): Whether to include inactive resources in the results. + gateway_id (Optional[str]): Filter by gateway ID(s), comma-separated. Accepts the literal value 'null' to indicate NULL gateway_id (local resources). db (Session): Database session dependency. user: Authenticated user object from dependency injection. diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index f5ed20df8..40f3032de 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -6694,8 +6694,12 @@ function initToolSelect( ); // Detect pagination/infinite-scroll controls for tools - const hasPaginationControls = !!document.getElementById("tools-pagination-controls"); - const hasScrollTrigger = !!document.querySelector("[id^='tools-scroll-trigger']"); + const hasPaginationControls = !!document.getElementById( + "tools-pagination-controls", + ); + const hasScrollTrigger = !!document.querySelector( + "[id^='tools-scroll-trigger']", + ); const isPaginated = hasPaginationControls || hasScrollTrigger; let allToolIds = []; @@ -6706,8 +6710,13 @@ function initToolSelect( visibleCheckboxes.forEach((cb) => (cb.checked = true)); } else { // Paginated (or no visible items) => fetch full set from server - const selectedGatewayIds = getSelectedGatewayIds ? getSelectedGatewayIds() : []; - const gatewayParam = selectedGatewayIds && selectedGatewayIds.length ? `?gateway_id=${encodeURIComponent(selectedGatewayIds.join(","))}` : ""; + const selectedGatewayIds = getSelectedGatewayIds + ? getSelectedGatewayIds() + : []; + const gatewayParam = + selectedGatewayIds && selectedGatewayIds.length + ? `?gateway_id=${encodeURIComponent(selectedGatewayIds.join(","))}` + : ""; const response = await fetch( `${window.ROOT_PATH}/admin/tools/ids${gatewayParam}`, ); @@ -6992,8 +7001,12 @@ function initResourceSelect( ); // Detect pagination/infinite-scroll controls for resources - const hasPaginationControls = !!document.getElementById("resources-pagination-controls"); - const hasScrollTrigger = !!document.querySelector("[id^='resources-scroll-trigger']"); + const hasPaginationControls = !!document.getElementById( + "resources-pagination-controls", + ); + const hasScrollTrigger = !!document.querySelector( + "[id^='resources-scroll-trigger']", + ); const isPaginated = hasPaginationControls || hasScrollTrigger; let allIds = []; @@ -7004,8 +7017,13 @@ function initResourceSelect( visibleCheckboxes.forEach((cb) => (cb.checked = true)); } else { // Paginated (or no visible items) => fetch full set from server - const selectedGatewayIds = getSelectedGatewayIds ? getSelectedGatewayIds() : []; - const gatewayParam = selectedGatewayIds && selectedGatewayIds.length ? `?gateway_id=${encodeURIComponent(selectedGatewayIds.join(","))}` : ""; + const selectedGatewayIds = getSelectedGatewayIds + ? getSelectedGatewayIds() + : []; + const gatewayParam = + selectedGatewayIds && selectedGatewayIds.length + ? `?gateway_id=${encodeURIComponent(selectedGatewayIds.join(","))}` + : ""; const resp = await fetch( `${window.ROOT_PATH}/admin/resources/ids${gatewayParam}`, ); @@ -7236,8 +7254,12 @@ function initPromptSelect( ); // Detect pagination/infinite-scroll controls for prompts - const hasPaginationControls = !!document.getElementById("prompts-pagination-controls"); - const hasScrollTrigger = !!document.querySelector("[id^='prompts-scroll-trigger']"); + const hasPaginationControls = !!document.getElementById( + "prompts-pagination-controls", + ); + const hasScrollTrigger = !!document.querySelector( + "[id^='prompts-scroll-trigger']", + ); const isPaginated = hasPaginationControls || hasScrollTrigger; let allIds = []; @@ -7248,8 +7270,13 @@ function initPromptSelect( visibleCheckboxes.forEach((cb) => (cb.checked = true)); } else { // Paginated (or no visible items) => fetch full set from server - const selectedGatewayIds = getSelectedGatewayIds ? getSelectedGatewayIds() : []; - const gatewayParam = selectedGatewayIds && selectedGatewayIds.length ? `?gateway_id=${encodeURIComponent(selectedGatewayIds.join(","))}` : ""; + const selectedGatewayIds = getSelectedGatewayIds + ? getSelectedGatewayIds() + : []; + const gatewayParam = + selectedGatewayIds && selectedGatewayIds.length + ? `?gateway_id=${encodeURIComponent(selectedGatewayIds.join(","))}` + : ""; const resp = await fetch( `${window.ROOT_PATH}/admin/prompts/ids${gatewayParam}`, ); @@ -7586,7 +7613,10 @@ function initGatewaySelect( } } } catch (err) { - console.error("Error ensuring null sentinel in gateway IDs:", err); + console.error( + "Error ensuring null sentinel in gateway IDs:", + err, + ); } let allIdsInput = container.querySelector( @@ -7741,7 +7771,9 @@ function getSelectedGatewayIds() { const selectedIds = Array.from(checkboxes) .map((cb) => { // Convert the special null-gateway checkbox to the literal 'null' - if (cb.dataset?.gatewayNull === "true") return "null"; + if (cb.dataset?.gatewayNull === "true") { + return "null"; + } return cb.value; }) // Filter out any empty values to avoid sending empty CSV entries From 3f80c22b5dd35055fb497d5d10ebfedb390cbdfa Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 19 Nov 2025 10:56:17 +0530 Subject: [PATCH 31/33] pagination resource Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 40f3032de..ffa08fce5 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7882,6 +7882,25 @@ function reloadAssociatedItems() { html.length, ); resourcesContainer.innerHTML = html; + // If HTMX is available, process the newly-inserted HTML so hx-* + // triggers (like the infinite-scroll 'intersect' trigger) are + // initialized. We previously used fetch() to avoid HTMX race + // conditions but forgot to run htmx.process on the new nodes, + // which left the "Loading more resources" trigger inert. + if (window.htmx && typeof window.htmx.process === "function") { + try { + window.htmx.process(resourcesContainer); + console.log( + "[Filter Update DEBUG] htmx.process called on resources container", + ); + } catch (e) { + console.warn( + "[Filter Update DEBUG] htmx.process failed:", + e, + ); + } + } + // Re-initialize the resource select after content is loaded initResourceSelect( "associatedResources", From 421a336fa242ed889ee056184578ae606e166241 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 19 Nov 2025 11:41:36 +0530 Subject: [PATCH 32/33] lint Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 49 ++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index ffa08fce5..602cc6c4f 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7884,14 +7884,55 @@ function reloadAssociatedItems() { resourcesContainer.innerHTML = html; // If HTMX is available, process the newly-inserted HTML so hx-* // triggers (like the infinite-scroll 'intersect' trigger) are - // initialized. We previously used fetch() to avoid HTMX race - // conditions but forgot to run htmx.process on the new nodes, - // which left the "Loading more resources" trigger inert. + // initialized. To avoid HTMX re-triggering the container's + // own `hx-get`/`hx-trigger="load"` (which would issue a second + // request without the gateway filter), temporarily remove those + // attributes from the container while we call `htmx.process`. if (window.htmx && typeof window.htmx.process === "function") { try { + // Backup and remove attributes that could auto-fire + const hadHxGet = resourcesContainer.hasAttribute( + "hx-get", + ); + const hadHxTrigger = resourcesContainer.hasAttribute( + "hx-trigger", + ); + const oldHxGet = resourcesContainer.getAttribute( + "hx-get", + ); + const oldHxTrigger = resourcesContainer.getAttribute( + "hx-trigger", + ); + + if (hadHxGet) { + resourcesContainer.removeAttribute("hx-get"); + } + if (hadHxTrigger) { + resourcesContainer.removeAttribute("hx-trigger"); + } + + // Process only the newly-inserted inner nodes to initialize + // any hx-* behavior (infinite scroll, after-swap hooks, etc.) window.htmx.process(resourcesContainer); + + // Restore original attributes so the container retains its + // declarative behavior for future operations, but don't + // re-process (we already processed child nodes). + if (hadHxGet && oldHxGet !== null) { + resourcesContainer.setAttribute( + "hx-get", + oldHxGet, + ); + } + if (hadHxTrigger && oldHxTrigger !== null) { + resourcesContainer.setAttribute( + "hx-trigger", + oldHxTrigger, + ); + } + console.log( - "[Filter Update DEBUG] htmx.process called on resources container", + "[Filter Update DEBUG] htmx.process called on resources container (attributes temporarily removed)", ); } catch (e) { console.warn( From b3d745d9551c308bad705f67ad2b943437f965e7 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 19 Nov 2025 11:44:21 +0530 Subject: [PATCH 33/33] lint Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 602cc6c4f..e1794918f 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -7891,18 +7891,14 @@ function reloadAssociatedItems() { if (window.htmx && typeof window.htmx.process === "function") { try { // Backup and remove attributes that could auto-fire - const hadHxGet = resourcesContainer.hasAttribute( - "hx-get", - ); - const hadHxTrigger = resourcesContainer.hasAttribute( - "hx-trigger", - ); - const oldHxGet = resourcesContainer.getAttribute( - "hx-get", - ); - const oldHxTrigger = resourcesContainer.getAttribute( - "hx-trigger", - ); + const hadHxGet = + resourcesContainer.hasAttribute("hx-get"); + const hadHxTrigger = + resourcesContainer.hasAttribute("hx-trigger"); + const oldHxGet = + resourcesContainer.getAttribute("hx-get"); + const oldHxTrigger = + resourcesContainer.getAttribute("hx-trigger"); if (hadHxGet) { resourcesContainer.removeAttribute("hx-get"); @@ -7919,10 +7915,7 @@ function reloadAssociatedItems() { // declarative behavior for future operations, but don't // re-process (we already processed child nodes). if (hadHxGet && oldHxGet !== null) { - resourcesContainer.setAttribute( - "hx-get", - oldHxGet, - ); + resourcesContainer.setAttribute("hx-get", oldHxGet); } if (hadHxTrigger && oldHxTrigger !== null) { resourcesContainer.setAttribute(