Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 2 additions & 1 deletion mcpgateway/cache/session_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import httpx

# First-Party
from mcpgateway import __version__
from mcpgateway.config import settings
from mcpgateway.db import get_db, SessionMessageRecord, SessionRecord
from mcpgateway.models import Implementation, InitializeResult, ServerCapabilities
Expand Down Expand Up @@ -699,7 +700,7 @@ async def handle_initialize_logic(self, body: dict) -> InitializeResult:
roots={"listChanged": True},
sampling={},
),
serverInfo=Implementation(name=settings.app_name, version="1.0.0"),
serverInfo=Implementation(name=settings.app_name, version=__version__),
instructions=("MCP Gateway providing federated tools, resources and prompts. Use /admin interface for configuration."),
)

Expand Down
5 changes: 3 additions & 2 deletions mcpgateway/federation/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from zeroconf.asyncio import AsyncServiceBrowser, AsyncZeroconf

# First-Party
from mcpgateway import __version__
from mcpgateway.config import settings
from mcpgateway.models import ServerCapabilities

Expand Down Expand Up @@ -64,7 +65,7 @@ def __init__(self):
port=settings.port,
properties={
"name": settings.app_name,
"version": "1.0.0",
"version": __version__,
"protocol": PROTOCOL_VERSION,
},
)
Expand Down Expand Up @@ -350,7 +351,7 @@ async def _get_gateway_info(self, url: str) -> ServerCapabilities:
"params": {
"protocol_version": PROTOCOL_VERSION,
"capabilities": {"roots": {"listChanged": True}, "sampling": {}},
"client_info": {"name": settings.app_name, "version": "1.0.0"},
"client_info": {"name": settings.app_name, "version": __version__},
},
}

Expand Down
2 changes: 1 addition & 1 deletion mcpgateway/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2155,7 +2155,7 @@ async def root_info():
dict: API info with app name, version, and UI/admin API status.
"""
logger.info("UI disabled, serving API info at root path")
return {"name": settings.app_name, "version": "1.0.0", "description": f"{settings.app_name} API - UI is disabled", "ui_enabled": False, "admin_api_enabled": ADMIN_API_ENABLED}
return {"name": settings.app_name, "version": __version__, "description": f"{settings.app_name} API - UI is disabled", "ui_enabled": False, "admin_api_enabled": ADMIN_API_ENABLED}


# Expose some endpoints at the root level as well
Expand Down
151 changes: 75 additions & 76 deletions mcpgateway/services/gateway_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,13 +280,14 @@ async def list_gateways(self, db: Session, include_inactive: bool = False) -> Li
gateways = db.execute(query).scalars().all()
return [GatewayRead.model_validate(g) for g in gateways]

async def update_gateway(self, db: Session, gateway_id: str, gateway_update: GatewayUpdate) -> GatewayRead:
async def update_gateway(self, db: Session, gateway_id: str, gateway_update: GatewayUpdate, include_inactive: bool = True) -> GatewayRead:
"""Update a gateway.

Args:
db: Database session
gateway_id: Gateway ID to update
gateway_update: Updated gateway data
include_inactive: Whether to include inactive gateways

Returns:
Updated gateway information
Expand All @@ -302,88 +303,86 @@ async def update_gateway(self, db: Session, gateway_id: str, gateway_update: Gat
if not gateway:
raise GatewayNotFoundError(f"Gateway not found: {gateway_id}")

if not gateway.enabled:
raise GatewayNotFoundError(f"Gateway '{gateway.name}' exists but is inactive")

# Check for name conflicts if name is being changed
if gateway_update.name is not None and gateway_update.name != gateway.name:
existing_gateway = db.execute(select(DbGateway).where(DbGateway.name == gateway_update.name).where(DbGateway.id != gateway_id)).scalar_one_or_none()

if existing_gateway:
raise GatewayNameConflictError(
gateway_update.name,
enabled=existing_gateway.enabled,
gateway_id=existing_gateway.id,
)

# Update fields if provided
if gateway_update.name is not None:
gateway.name = gateway_update.name
gateway.slug = slugify(gateway_update.name)
if gateway_update.url is not None:
gateway.url = gateway_update.url
if gateway_update.description is not None:
gateway.description = gateway_update.description
if gateway_update.transport is not None:
gateway.transport = gateway_update.transport

if getattr(gateway, "auth_type", None) is not None:
gateway.auth_type = gateway_update.auth_type

# if auth_type is not None and only then check auth_value
if getattr(gateway, "auth_value", {}) != {}:
gateway.auth_value = gateway_update.auth_value

# Try to reinitialize connection if URL changed
if gateway_update.url is not None:
try:
capabilities, tools = await self._initialize_gateway(gateway.url, gateway.auth_value, gateway.transport)
new_tool_names = [tool.name for tool in tools]
if gateway.enabled or include_inactive:
# Check for name conflicts if name is being changed
if gateway_update.name is not None and gateway_update.name != gateway.name:
existing_gateway = db.execute(select(DbGateway).where(DbGateway.name == gateway_update.name).where(DbGateway.id != gateway_id)).scalar_one_or_none()

if existing_gateway:
raise GatewayNameConflictError(
gateway_update.name,
enabled=existing_gateway.enabled,
gateway_id=existing_gateway.id,
)

# Update fields if provided
if gateway_update.name is not None:
gateway.name = gateway_update.name
gateway.slug = slugify(gateway_update.name)
if gateway_update.url is not None:
gateway.url = gateway_update.url
if gateway_update.description is not None:
gateway.description = gateway_update.description
if gateway_update.transport is not None:
gateway.transport = gateway_update.transport

if getattr(gateway, "auth_type", None) is not None:
gateway.auth_type = gateway_update.auth_type

# if auth_type is not None and only then check auth_value
if getattr(gateway, "auth_value", {}) != {}:
gateway.auth_value = gateway_update.auth_value

# Try to reinitialize connection if URL changed
if gateway_update.url is not None:
try:
capabilities, tools = await self._initialize_gateway(gateway.url, gateway.auth_value, gateway.transport)
new_tool_names = [tool.name for tool in tools]

for tool in tools:
existing_tool = db.execute(select(DbTool).where(DbTool.original_name == tool.name).where(DbTool.gateway_id == gateway_id)).scalar_one_or_none()
if not existing_tool:
gateway.tools.append(
DbTool(
original_name=tool.name,
original_name_slug=slugify(tool.name),
url=gateway.url,
description=tool.description,
integration_type=tool.integration_type,
request_type=tool.request_type,
headers=tool.headers,
input_schema=tool.input_schema,
jsonpath_filter=tool.jsonpath_filter,
auth_type=gateway.auth_type,
auth_value=gateway.auth_value,
for tool in tools:
existing_tool = db.execute(select(DbTool).where(DbTool.original_name == tool.name).where(DbTool.gateway_id == gateway_id)).scalar_one_or_none()
if not existing_tool:
gateway.tools.append(
DbTool(
original_name=tool.name,
original_name_slug=slugify(tool.name),
url=gateway.url,
description=tool.description,
integration_type=tool.integration_type,
request_type=tool.request_type,
headers=tool.headers,
input_schema=tool.input_schema,
jsonpath_filter=tool.jsonpath_filter,
auth_type=gateway.auth_type,
auth_value=gateway.auth_value,
)
)
)

gateway.capabilities = capabilities
gateway.tools = [tool for tool in gateway.tools if tool.original_name in new_tool_names] # keep only still-valid rows
gateway.last_seen = datetime.now(timezone.utc)
gateway.capabilities = capabilities
gateway.tools = [tool for tool in gateway.tools if tool.original_name in new_tool_names] # keep only still-valid rows
gateway.last_seen = datetime.now(timezone.utc)

# Update tracking with new URL
self._active_gateways.discard(gateway.url)
self._active_gateways.add(gateway.url)
except Exception as e:
logger.warning(f"Failed to initialize updated gateway: {e}")
# Update tracking with new URL
self._active_gateways.discard(gateway.url)
self._active_gateways.add(gateway.url)
except Exception as e:
logger.warning(f"Failed to initialize updated gateway: {e}")

gateway.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(gateway)
gateway.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(gateway)

# Notify subscribers
await self._notify_gateway_updated(gateway)
# Notify subscribers
await self._notify_gateway_updated(gateway)

logger.info(f"Updated gateway: {gateway.name}")
return GatewayRead.model_validate(gateway)
logger.info(f"Updated gateway: {gateway.name}")
return GatewayRead.model_validate(gateway)

except Exception as e:
db.rollback()
raise GatewayError(f"Failed to update gateway: {str(e)}")

async def get_gateway(self, db: Session, gateway_id: str, include_inactive: bool = False) -> GatewayRead:
async def get_gateway(self, db: Session, gateway_id: str, include_inactive: bool = True) -> GatewayRead:
"""Get a specific gateway by ID.

Args:
Expand All @@ -400,11 +399,11 @@ async def get_gateway(self, db: Session, gateway_id: str, include_inactive: bool
gateway = db.get(DbGateway, gateway_id)
if not gateway:
raise GatewayNotFoundError(f"Gateway not found: {gateway_id}")

if not gateway.enabled and not include_inactive:
raise GatewayNotFoundError(f"Gateway '{gateway.name}' exists but is inactive")

return GatewayRead.model_validate(gateway)
if gateway.enabled or include_inactive:
return GatewayRead.model_validate(gateway)
raise GatewayNotFoundError(f"Gateway not found: {gateway_id}")

async def toggle_gateway_status(self, db: Session, gateway_id: str, activate: bool, reachable: bool = True, only_update_reachable: bool = False) -> GatewayRead:
"""Toggle gateway active status.
Expand Down
35 changes: 29 additions & 6 deletions mcpgateway/static/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -1983,12 +1983,35 @@ async function viewGateway(gatewayId) {
statusP.appendChild(statusStrong);

const statusSpan = document.createElement("span");
statusSpan.className = `px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
gateway.isActive
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`;
statusSpan.textContent = gateway.isActive ? "Active" : "Inactive";
let statusText = "";
let statusClass = "";
let statusIcon = "";
if (!gateway.enabled) {
statusText = "Inactive";
statusClass = "bg-red-100 text-red-800";
statusIcon = `
<svg class="ml-1 h-4 w-4 text-red-600 self-center" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M6.293 6.293a1 1 0 011.414 0L10 8.586l2.293-2.293a1 1 0 111.414 1.414L11.414 10l2.293 2.293a1 1 0 11-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 11-1.414-1.414L8.586 10 6.293 7.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>`;
} else if (gateway.enabled && gateway.reachable) {
statusText = "Active";
statusClass = "bg-green-100 text-green-800";
statusIcon = `
<svg class="ml-1 h-4 w-4 text-green-600 self-center" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm-1-4.586l5.293-5.293-1.414-1.414L9 11.586 7.121 9.707 5.707 11.121 9 14.414z" clip-rule="evenodd"></path>
</svg>`;
} else if (gateway.enabled && !gateway.reachable) {
statusText = "Offline";
statusClass = "bg-yellow-100 text-yellow-800";
statusIcon = `
<svg class="ml-1 h-4 w-4 text-yellow-600 self-center" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm-1-10h2v4h-2V8zm0 6h2v2h-2v-2z" clip-rule="evenodd"></path>
</svg>`;
}

statusSpan.className = `px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusClass}`;
statusSpan.innerHTML = `${statusText} ${statusIcon}`;

statusP.appendChild(statusSpan);
container.appendChild(statusP);

Expand Down
2 changes: 1 addition & 1 deletion mcpgateway/templates/version_info_partial.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ <h3 class="text-lg font-medium mb-4 dark:text-gray-200 flex items-center">
<svg class="h-6 w-6 mr-2 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"></path>
</svg>
Platform & Runtime
Platform &amp; Runtime
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-gray-50 rounded p-4 dark:bg-gray-700">
Expand Down
7 changes: 6 additions & 1 deletion tests/unit/mcpgateway/services/test_gateway_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,13 @@ async def test_get_gateway_inactive(self, gateway_service, mock_gateway, test_db
"""Inactive gateway is not returned unless explicitly asked for."""
mock_gateway.enabled = False
test_db.get = Mock(return_value=mock_gateway)
result = await gateway_service.get_gateway(test_db, 1, include_inactive=True)
assert result.id == 1
assert result.enabled == False
test_db.get.reset_mock()
test_db.get = Mock(return_value=mock_gateway)
with pytest.raises(GatewayNotFoundError):
await gateway_service.get_gateway(test_db, 1)
result = await gateway_service.get_gateway(test_db, 1, include_inactive=False)

# ────────────────────────────────────────────────────────────────────
# UPDATE
Expand Down
Loading