Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@

> **Migration**: Switch to PostgreSQL for production deployments. Update `DATABASE_URL` to a `postgresql+psycopg://` connection string. SQLite (`sqlite:///./mcp.db`) remains available for local development and testing.

### Added

#### **Refresh Tools Button for All Gateway Types** ([#3765](https://github.com/IBM/mcp-context-forge/issues/3765))

Users can now pull the latest tools from any registered MCP without re-registering the gateway.

* A **Fetch Tools** / **Refresh Tools** action is available for all gateway auth types (previously limited to OAuth only) via the new overflow menu in the gateways table
* The label reads **Fetch Tools** on first use (no tools registered yet) and automatically switches to **Refresh Tools** once tools exist, driven by the new `toolCount` field on `GatewayRead`
* On success, a toast notification shows the delta: tools added / updated / removed
* The table reloads via HTMX to reflect updated tool counts and button labels without a full page refresh
* A **Refresh from MCPs** button is also available in the virtual server edit form, allowing users to pull the latest tools from all currently selected gateways and reload the tools selector without leaving the modal
* Backed by the existing `POST /gateways/{id}/tools/refresh` API endpoint

#### **Overflow Menu for Gateways Table** ([#3519](https://github.com/IBM/mcp-context-forge/issues/3519))

* Replaced the stacked action buttons in the gateways table with a single overflow menu (three-dot button), reducing visual clutter and aligning with the Carbon Design System overflow menu pattern
* All existing actions (Test, OAuth Authorize, Fetch/Refresh Tools, View, Edit, Activate/Deactivate, Delete) remain fully accessible inside the menu

## [1.0.0-RC2] - 2026-03-09 - Hardening, Admin UI Polish, Plugin Framework & Quality

### Overview
Expand Down
2 changes: 1 addition & 1 deletion mcpgateway/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9191,7 +9191,7 @@ async def admin_gateways_partial_html(
team_ids = await _get_user_team_ids(user, db)

# Build base query
query = select(DbGateway).options(joinedload(DbGateway.email_team))
query = select(DbGateway).options(joinedload(DbGateway.email_team), selectinload(DbGateway.tools))

if not include_inactive:
query = query.where(DbGateway.enabled.is_(True))
Expand Down
4 changes: 2 additions & 2 deletions mcpgateway/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
# Import the admin routes from the new module
from mcpgateway import __version__
from mcpgateway import version as version_module
from mcpgateway.admin import admin_router, set_logging_service
from mcpgateway.admin import admin_router, enforce_admin_csrf, set_logging_service
from mcpgateway.auth import _check_token_revoked_sync, _lookup_api_token_sync, get_current_user, get_user_team_roles, normalize_token_teams, resolve_session_teams
from mcpgateway.bootstrap_db import main as bootstrap_db
from mcpgateway.cache import ResourceCache, SessionRegistry
Expand Down Expand Up @@ -6577,7 +6577,7 @@ async def delete_gateway(gateway_id: str, db: Session = Depends(get_db), user=De
raise HTTPException(status_code=400, detail=str(e))


@gateway_router.post("/{gateway_id}/tools/refresh", response_model=GatewayRefreshResponse)
@gateway_router.post("/{gateway_id}/tools/refresh", response_model=GatewayRefreshResponse, dependencies=[Depends(enforce_admin_csrf)])
@require_permission("gateways.update")
async def refresh_gateway_tools(
gateway_id: str,
Expand Down
3 changes: 3 additions & 0 deletions mcpgateway/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3402,6 +3402,9 @@ class GatewayRead(BaseModelWithConfigDict):

_normalize_visibility = field_validator("visibility", mode="before")(classmethod(lambda cls, v: _coerce_visibility(v)))

# Tool count (populated from the tools relationship; 0 when not loaded)
tool_count: int = Field(default=0, description="Number of tools registered for this gateway")

@model_validator(mode="before")
@classmethod
def _mask_query_param_auth(cls, data: Any) -> Any:
Expand Down
6 changes: 5 additions & 1 deletion mcpgateway/services/gateway_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamablehttp_client
from pydantic import ValidationError
from sqlalchemy import and_, delete, desc, or_, select, update
from sqlalchemy import and_, delete, desc, inspect as sa_inspect, or_, select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload, selectinload, Session

Expand Down Expand Up @@ -4176,6 +4176,10 @@ def convert_gateway_to_read(self, gateway: DbGateway) -> GatewayRead:
gateway_dict["version"] = getattr(gateway, "version", None)
gateway_dict["team"] = getattr(gateway, "team", None)

# Use official SQLAlchemy inspect API; returns 0 if tools relationship is not loaded
tools_loaded = sa_inspect(gateway).attrs["tools"].loaded_value
gateway_dict["tool_count"] = len(tools_loaded) if isinstance(tools_loaded, list) else 0

return GatewayRead.model_validate(gateway_dict).masked()

def _create_db_tool(
Expand Down
158 changes: 158 additions & 0 deletions mcpgateway/static/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -20829,6 +20829,164 @@ async function fetchToolsForGateway(gatewayId, gatewayName) {
// Expose fetch tools function to global scope
window.fetchToolsForGateway = fetchToolsForGateway;

/**
* Refresh (or first-time fetch) tools for a gateway via the unified refresh endpoint.
* Works for all auth types. Shows a toast with delta counts on success.
*
* @param {string} gatewayId - ID of the gateway
* @param {string} gatewayName - Display name for toast messages
* @param {HTMLElement|null} buttonEl - Optional button element for loading-state feedback
*/
async function refreshGatewayTools(gatewayId, gatewayName, buttonEl) {
const origText = buttonEl ? buttonEl.textContent : "";
if (buttonEl) {
buttonEl.disabled = true;
buttonEl.textContent = "⏳ Refreshing...";
}

try {
const csrfToken =
document.cookie
.split("; ")
.find((row) => row.startsWith("mcpgateway_csrf_token="))
?.split("=")[1] ?? "";
const response = await fetch(
`${window.ROOT_PATH}/gateways/${gatewayId}/tools/refresh`,
{
method: "POST",
credentials: "include",
headers: {
Accept: "application/json",
"x-csrf-token": csrfToken,
},
},
);

const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || data.message || "Refresh failed");
}

// Check if the refresh operation itself succeeded (even if HTTP 200)
if (data.success === false || data.error) {
throw new Error(data.error || "Refresh failed on the server");
}

const toolsAdded = data.toolsAdded ?? 0;
const toolsUpdated = data.toolsUpdated ?? 0;
const toolsRemoved = data.toolsRemoved ?? 0;
const deltaMsg =
toolsAdded || toolsUpdated || toolsRemoved
? `${toolsAdded} added, ${toolsUpdated} updated, ${toolsRemoved} removed`
: "No changes detected";
showSuccessMessage(`${gatewayName}: ${deltaMsg}`);

// Reload the gateways partial table via HTMX to reflect updated tool counts / button labels
htmx.ajax("GET", `${window.ROOT_PATH}/admin/gateways/partial`, {
target: "#gateways-table",
swap: "outerHTML",
});
} catch (err) {
console.error("refreshGatewayTools error:", err);
showErrorMessage(
`Failed to refresh tools for ${gatewayName}: ${err.message}`,
);
if (buttonEl) {
buttonEl.disabled = false;
buttonEl.textContent = origText;
}
}
}

window.refreshGatewayTools = refreshGatewayTools;

/**
* Refresh tools for all currently selected gateways in the virtual server edit form.
* After completion, triggers an HTMX reload of the tools selector list.
*
* @param {HTMLElement} buttonEl - The button element clicked
*/
async function refreshToolsForSelectedGateways(buttonEl) {
if (typeof getSelectedGatewayIds !== "function") {
console.warn(
"refreshToolsForSelectedGateways: getSelectedGatewayIds is not defined",
);
}
const gwIds =
typeof getSelectedGatewayIds === "function"
? getSelectedGatewayIds()
: [];
if (!gwIds.length) {
showErrorMessage("Select at least one MCP gateway first.");
return;
}

const origText = buttonEl.textContent;
buttonEl.disabled = true;
buttonEl.textContent = "⏳ Refreshing...";

let added = 0;
let updated = 0;
let removed = 0;
let failed = 0;

await Promise.allSettled(
gwIds.map(async (gid) => {
try {
const csrfToken =
document.cookie
.split("; ")
.find((row) => row.startsWith("mcpgateway_csrf_token="))
?.split("=")[1] ?? "";
const res = await fetch(
`${window.ROOT_PATH}/gateways/${gid}/tools/refresh`,
{
method: "POST",
credentials: "include",
headers: {
Accept: "application/json",
"x-csrf-token": csrfToken,
},
},
);
const data = await res.json();
if (res.ok && data.success !== false) {
added += data.toolsAdded ?? 0;
updated += data.toolsUpdated ?? 0;
removed += data.toolsRemoved ?? 0;
} else {
failed++;
}
} catch (_) {
failed++;
}
}),
);

buttonEl.disabled = false;
buttonEl.textContent = origText;

const deltaMsg =
added || updated || removed
? `${added} added, ${updated} updated, ${removed} removed`
: "No changes detected";
if (failed > 0) {
showErrorMessage(
`Refresh completed with ${failed} error(s). ${deltaMsg}`,
);
} else {
showSuccessMessage(`Tools refreshed: ${deltaMsg}`);
}

// Reload the tools selector via HTMX to pick up newly discovered tools
const container = document.getElementById("edit-server-tools");
if (container) {
htmx.trigger(container, "load");
}
}

window.refreshToolsForSelectedGateways = refreshToolsForSelectedGateways;

console.log("πŸ›‘οΈ ContextForge AI Gateway admin.js initialized");

// ===================================================================
Expand Down
20 changes: 14 additions & 6 deletions mcpgateway/templates/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -11134,12 +11134,20 @@ <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
<div
class="p-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-600"
>
<label
for="edit-server-tools"
class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2"
>
Associated Tools
</label>
<div class="flex items-center justify-between mb-2">
<label
for="edit-server-tools"
class="block text-sm font-semibold text-gray-700 dark:text-gray-300"
>
Associated Tools
</label>
<button
type="button"
onclick="refreshToolsForSelectedGateways(this)"
class="px-2 py-1 text-xs font-medium text-green-700 dark:text-green-400 border border-green-600 dark:border-green-500 rounded-md hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors"
title="Re-fetch tools from selected MCP gateways and reload this list"
>Refresh from MCPs</button>
</div>
<input
type="text"
id="searchEditTools"
Expand Down
Loading
Loading