Skip to content

Commit 2254863

Browse files
committed
Merge remote-tracking branch 'origin/main' into handle_duplicate_server_name_catalog_ui
2 parents d96d2f0 + 4bac388 commit 2254863

19 files changed

+2873
-172
lines changed

.github/tools/update_dependencies.py

Lines changed: 1561 additions & 0 deletions
Large diffs are not rendered by default.

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ mcp.prof
44
mcp.pstats
55
.depsorter_cache.json
66
.depupdate.*
7-
update_dependencies.py
87
token.txt
98
*.db
109
*.bak

Containerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM registry.access.redhat.com/ubi9-minimal:9.6-1752069876
1+
FROM registry.access.redhat.com/ubi9-minimal:9.6-1752587672
22
LABEL maintainer="Mihai Criveti" \
33
name="mcp/mcpgateway" \
44
version="0.3.1" \

Containerfile.lite

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ ARG PYTHON_VERSION=3.11
2626
###########################
2727
# Base image for copying into scratch
2828
###########################
29-
FROM registry.access.redhat.com/ubi9/ubi-micro:9.6-1751962311 AS base
29+
FROM registry.access.redhat.com/ubi9/ubi-micro:9.6-1752751762 AS base
3030

3131
###########################
3232
# Builder stage
3333
###########################
34-
FROM registry.access.redhat.com/ubi9/ubi:9.6-1751897624 AS builder
34+
FROM registry.access.redhat.com/ubi9/ubi:9.6-1752625787 AS builder
3535
SHELL ["/bin/bash", "-c"]
3636

3737
ARG PYTHON_VERSION

Makefile

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ FILES_TO_CLEAN := .coverage coverage.xml mcp.prof mcp.pstats \
3838
$(DOCS_DIR)/docs/test/sbom.md \
3939
$(DOCS_DIR)/docs/test/{unittest,full,index,test}.md \
4040
$(DOCS_DIR)/docs/images/coverage.svg $(LICENSES_MD) $(METRICS_MD) \
41-
*.db *.sqlite *.sqlite3 mcp.db-journal *.py,cover
41+
*.db *.sqlite *.sqlite3 mcp.db-journal *.py,cover \
42+
.depsorter_cache.json .depupdate.*
4243

4344
COVERAGE_DIR ?= $(DOCS_DIR)/docs/coverage
4445
LICENSES_MD ?= $(DOCS_DIR)/docs/test/licenses.md
@@ -1002,9 +1003,10 @@ hadolint:
10021003
.PHONY: deps-update containerfile-update
10031004

10041005
deps-update:
1005-
@echo "⬆️ Updating project dependencies via update-deps.py..."
1006-
@test -f update-deps.py || { echo "❌ update-deps.py not found in root directory."; exit 1; }
1007-
@/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 update-deps.py"
1006+
@echo "⬆️ Updating project dependencies via update_dependencies.py..."
1007+
@test -f ./.github/tools/update_dependencies.py || { echo "❌ update_dependencies.py not found in ./.github/tools."; exit 1; }
1008+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 ./.github/tools/update_dependencies.py --ignore-dependency starlette --file pyproject.toml"
1009+
@/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 ./.github/tools/update_dependencies.py --file docs/requirements.txt"
10081010
@echo "✅ Dependencies updated in pyproject.toml and docs/requirements.txt"
10091011

10101012
containerfile-update:
@@ -1136,6 +1138,7 @@ endef
11361138
# help: 🐳 UNIFIED CONTAINER OPERATIONS (Auto-detects Docker/Podman)
11371139
# help: container-build - Build image using detected runtime
11381140
# help: container-run - Run container using detected runtime
1141+
# help: container-run-host - Run container using detected runtime with host networking
11391142
# help: container-run-ssl - Run container with TLS using detected runtime
11401143
# help: container-run-ssl-host - Run container with TLS and host networking
11411144
# help: container-push - Push image (handles localhost/ prefix)
@@ -1151,7 +1154,7 @@ endef
11511154
# help: use-podman - Switch to Podman runtime
11521155
# help: show-runtime - Show current container runtime
11531156

1154-
.PHONY: container-build container-run container-run-ssl container-run-ssl-host \
1157+
.PHONY: container-build container-run container-run-host container-run-ssl container-run-ssl-host \
11551158
container-push container-info container-stop container-logs container-shell \
11561159
container-health image-list image-clean image-retag container-check-image \
11571160
container-build-multi use-docker use-podman show-runtime
@@ -1201,6 +1204,26 @@ container-run: container-check-image
12011204
@echo "🔍 Health check status:"
12021205
@$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo "No health check configured"
12031206

1207+
container-run-host: container-check-image
1208+
@echo "🚀 Running with $(CONTAINER_RUNTIME)..."
1209+
-$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true
1210+
-$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true
1211+
$(CONTAINER_RUNTIME) run --name $(PROJECT_NAME) \
1212+
--env-file=.env \
1213+
--network=host \
1214+
-p 4444:4444 \
1215+
--restart=always \
1216+
--memory=$(CONTAINER_MEMORY) --cpus=$(CONTAINER_CPUS) \
1217+
--health-cmd="curl --fail http://localhost:4444/health || exit 1" \
1218+
--health-interval=1m --health-retries=3 \
1219+
--health-start-period=30s --health-timeout=10s \
1220+
-d $(call get_image_name)
1221+
@sleep 2
1222+
@echo "✅ Container started"
1223+
@echo "🔍 Health check status:"
1224+
@$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo "No health check configured"
1225+
1226+
12041227
container-run-ssl: certs container-check-image
12051228
@echo "🚀 Running with $(CONTAINER_RUNTIME) (TLS)..."
12061229
-$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true
@@ -1241,6 +1264,9 @@ container-run-ssl-host: certs container-check-image
12411264
@sleep 2
12421265
@echo "✅ Container started with TLS (host networking)"
12431266

1267+
1268+
1269+
12441270
container-push: container-check-image
12451271
@echo "📤 Preparing to push image..."
12461272
@# For Podman, we need to remove localhost/ prefix for push
@@ -1445,6 +1471,7 @@ container-run-ssl-safe: container-validate container-run-ssl
14451471
# help: podman - Build container image
14461472
# help: podman-prod - Build production container image (using ubi-micro → scratch). Not supported on macOS.
14471473
# help: podman-run - Run the container on HTTP (port 4444)
1474+
# help: podman-run-host - Run the container on HTTP (port 4444) with --network-host
14481475
# help: podman-run-shell - Run the container on HTTP (port 4444) and start a shell
14491476
# help: podman-run-ssl - Run the container on HTTPS (port 4444, self-signed)
14501477
# help: podman-run-ssl-host - Run the container on HTTPS with --network-host (port 4444, self-signed)
@@ -1455,8 +1482,8 @@ container-run-ssl-safe: container-validate container-run-ssl
14551482
# help: podman-top - Show live top-level process info in container
14561483

14571484
.PHONY: podman-dev podman podman-prod podman-build podman-run podman-run-shell \
1458-
podman-run-ssl podman-run-ssl-host podman-stop podman-test podman-logs \
1459-
podman-stats podman-top podman-shell
1485+
podman-run-host podman-run-ssl podman-run-ssl-host podman-stop podman-test \
1486+
podman-logs podman-stats podman-top podman-shell
14601487

14611488
podman-dev:
14621489
@$(MAKE) container-build CONTAINER_RUNTIME=podman CONTAINER_FILE=Containerfile
@@ -1473,6 +1500,9 @@ podman-build:
14731500
podman-run:
14741501
@$(MAKE) container-run CONTAINER_RUNTIME=podman
14751502

1503+
podman-run-host:
1504+
@$(MAKE) container-run-host CONTAINER_RUNTIME=podman
1505+
14761506
podman-run-shell:
14771507
@echo "🚀 Starting podman container shell..."
14781508
podman run --name $(PROJECT_NAME)-shell \
@@ -1522,13 +1552,14 @@ podman-top:
15221552
# help: docker - Build production Docker image
15231553
# help: docker-prod - Build production container image (using ubi-micro → scratch). Not supported on macOS.
15241554
# help: docker-run - Run the container on HTTP (port 4444)
1555+
# help: docker-run-host - Run the container on HTTP (port 4444) with --network-host
15251556
# help: docker-run-ssl - Run the container on HTTPS (port 4444, self-signed)
15261557
# help: docker-run-ssl-host - Run the container on HTTPS with --network-host (port 4444, self-signed)
15271558
# help: docker-stop - Stop & remove the container
15281559
# help: docker-test - Quick curl smoke-test against the container
15291560
# help: docker-logs - Follow container logs (⌃C to quit)
15301561

1531-
.PHONY: docker-dev docker docker-prod docker-build docker-run docker-run-ssl \
1562+
.PHONY: docker-dev docker docker-prod docker-build docker-run docker-run-host docker-run-ssl \
15321563
docker-run-ssl-host docker-stop docker-test docker-logs docker-stats \
15331564
docker-top docker-shell
15341565

@@ -1547,6 +1578,9 @@ docker-build:
15471578
docker-run:
15481579
@$(MAKE) container-run CONTAINER_RUNTIME=docker
15491580

1581+
docker-run-host:
1582+
@$(MAKE) container-run-host CONTAINER_RUNTIME=docker
1583+
15501584
docker-run-ssl:
15511585
@$(MAKE) container-run-ssl CONTAINER_RUNTIME=docker
15521586

docs/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ cssselect2>=0.8.0
1616
defusedxml>=0.7.1
1717
EditorConfig>=0.17.1
1818
flatten-json>=0.1.14
19-
fonttools>=4.58.5
19+
fonttools>=4.59.0
2020
funcparserlib>=1.0.1
2121
ghp-import>=2.1.0
2222
gitdb>=4.0.12

mcpgateway/admin.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2667,7 +2667,7 @@ async def admin_add_resource(request: Request, db: Session = Depends(get_db), us
26672667
>>>
26682668
>>> async def test_admin_add_resource():
26692669
... response = await admin_add_resource(mock_request, mock_db, mock_user)
2670-
... return isinstance(response, RedirectResponse) and response.status_code == 303
2670+
... return isinstance(response, JSONResponse) and response.status_code == 200 and response.body.decode() == '{"message":"Add resource registered successfully!","success":true}'
26712671
>>>
26722672
>>> import asyncio; asyncio.run(test_admin_add_resource())
26732673
True
@@ -2685,9 +2685,10 @@ async def admin_add_resource(request: Request, db: Session = Depends(get_db), us
26852685
content=form["content"],
26862686
)
26872687
await resource_service.register_resource(db, resource)
2688-
2689-
root_path = request.scope.get("root_path", "")
2690-
return RedirectResponse(f"{root_path}/admin#resources", status_code=303)
2688+
return JSONResponse(
2689+
content={"message": "Add resource registered successfully!", "success": True},
2690+
status_code=200,
2691+
)
26912692
except Exception as ex:
26922693
if isinstance(ex, ValidationError):
26932694
logger.error(f"ValidationError in admin_add_resource: {ErrorFormatter.format_validation_error(ex)}")
@@ -2696,6 +2697,7 @@ async def admin_add_resource(request: Request, db: Session = Depends(get_db), us
26962697
error_message = ErrorFormatter.format_database_error(ex)
26972698
logger.error(f"IntegrityError in admin_add_resource: {error_message}")
26982699
return JSONResponse(status_code=409, content=error_message)
2700+
26992701
logger.error(f"Error in admin_add_resource: {ex}")
27002702
return JSONResponse(content={"message": str(ex), "success": False}, status_code=500)
27012703

mcpgateway/main.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import json
3232
import logging
3333
from typing import Any, AsyncIterator, Dict, List, Optional, Union
34+
from urllib.parse import urlparse, urlunparse
3435

3536
# Third-Party
3637
from fastapi import (
@@ -54,6 +55,7 @@
5455
from sqlalchemy.exc import IntegrityError
5556
from sqlalchemy.orm import Session
5657
from starlette.middleware.base import BaseHTTPMiddleware
58+
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
5759

5860
# First-Party
5961
from mcpgateway import __version__
@@ -476,6 +478,9 @@ async def __call__(self, scope, receive, send):
476478
# Add streamable HTTP middleware for /mcp routes
477479
app.add_middleware(MCPPathRewriteMiddleware)
478480

481+
# Trust all proxies (or lock down with a list of host patterns)
482+
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
483+
479484

480485
# Set up Jinja2 templates and store in app state for later use
481486
templates = Jinja2Templates(directory=str(settings.templates_dir))
@@ -597,6 +602,42 @@ async def invalidate_resource_cache(uri: Optional[str] = None) -> None:
597602
resource_cache.clear()
598603

599604

605+
def get_protocol_from_request(request: Request) -> str:
606+
"""
607+
Return "https" or "http" based on:
608+
1) X-Forwarded-Proto (if set by a proxy)
609+
2) request.url.scheme (e.g. when Gunicorn/Uvicorn is terminating TLS)
610+
611+
Args:
612+
request (Request): The FastAPI request object.
613+
614+
Returns:
615+
str: The protocol used for the request, either "http" or "https".
616+
"""
617+
forwarded = request.headers.get("x-forwarded-proto")
618+
if forwarded:
619+
# may be a comma-separated list; take the first
620+
return forwarded.split(",")[0].strip()
621+
return request.url.scheme
622+
623+
624+
def update_url_protocol(request: Request) -> str:
625+
"""
626+
Update the base URL protocol based on the request's scheme or forwarded headers.
627+
628+
Args:
629+
request (Request): The FastAPI request object.
630+
631+
Returns:
632+
str: The base URL with the correct protocol.
633+
"""
634+
parsed = urlparse(str(request.base_url))
635+
proto = get_protocol_from_request(request)
636+
new_parsed = parsed._replace(scheme=proto)
637+
# urlunparse keeps netloc and path intact
638+
return urlunparse(new_parsed).rstrip("/")
639+
640+
600641
# Protocol APIs #
601642
@protocol_router.post("/initialize")
602643
async def initialize(request: Request, user: str = Depends(require_auth)) -> InitializeResult:
@@ -919,8 +960,9 @@ async def sse_endpoint(request: Request, server_id: str, user: str = Depends(req
919960
"""
920961
try:
921962
logger.debug(f"User {user} is establishing SSE connection for server {server_id}")
922-
base_url = str(request.base_url).rstrip("/")
963+
base_url = update_url_protocol(request)
923964
server_sse_url = f"{base_url}/servers/{server_id}"
965+
924966
transport = SSETransport(base_url=server_sse_url)
925967
await transport.connect()
926968
await session_registry.add_session(transport.session_id, transport)
@@ -2055,7 +2097,8 @@ async def utility_sse_endpoint(request: Request, user: str = Depends(require_aut
20552097
"""
20562098
try:
20572099
logger.debug("User %s requested SSE connection", user)
2058-
base_url = str(request.base_url).rstrip("/")
2100+
base_url = update_url_protocol(request)
2101+
20592102
transport = SSETransport(base_url=base_url)
20602103
await transport.connect()
20612104
await session_registry.add_session(transport.session_id, transport)

mcpgateway/static/admin.js

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4183,48 +4183,70 @@ async function handleGatewayFormSubmit(e) {
41834183
}
41844184
}
41854185

4186-
function handleResourceFormSubmit(e) {
4186+
async function handleResourceFormSubmit(e) {
41874187
e.preventDefault();
41884188
const form = e.target;
41894189
const formData = new FormData(form);
4190+
const status = safeGetElement("status-resources");
4191+
const loading = safeGetElement("add-gateway-loading");
4192+
try {
4193+
// Validate inputs
4194+
const name = formData.get("name");
4195+
const uri = formData.get("uri");
4196+
const nameValidation = validateInputName(name, "resource");
4197+
const uriValidation = validateInputName(uri, "resource URI");
41904198

4191-
// Validate inputs
4192-
const name = formData.get("name");
4193-
const uri = formData.get("uri");
4199+
if (!nameValidation.valid) {
4200+
showErrorMessage(nameValidation.error);
4201+
return;
4202+
}
41944203

4195-
const nameValidation = validateInputName(name, "resource");
4196-
const uriValidation = validateInputName(uri, "resource URI");
4204+
if (!uriValidation.valid) {
4205+
showErrorMessage(uriValidation.error);
4206+
return;
4207+
}
41974208

4198-
if (!nameValidation.valid) {
4199-
showErrorMessage(nameValidation.error);
4200-
return;
4201-
}
4209+
if (loading) {
4210+
loading.style.display = "block";
4211+
}
4212+
if (status) {
4213+
status.textContent = "";
4214+
status.classList.remove("error-status");
4215+
}
4216+
4217+
const isInactiveCheckedBool = isInactiveChecked("resources");
4218+
formData.append("is_inactive_checked", isInactiveCheckedBool);
42024219

4203-
if (!uriValidation.valid) {
4204-
showErrorMessage(uriValidation.error);
4205-
return;
4206-
}
4220+
const response = await fetchWithTimeout(
4221+
`${window.ROOT_PATH}/admin/resources`,
4222+
{
4223+
method: "POST",
4224+
body: formData,
4225+
},
4226+
);
42074227

4208-
fetchWithTimeout(`${window.ROOT_PATH}/admin/resources`, {
4209-
method: "POST",
4210-
body: formData,
4211-
})
4212-
.then((response) => {
4213-
if (!response.ok) {
4214-
const status = safeGetElement("status-resources");
4215-
if (status) {
4216-
status.textContent = "Connection failed!";
4217-
status.classList.add("error-status");
4218-
}
4219-
throw new Error("Network response was not ok");
4220-
} else {
4221-
location.reload();
4228+
const result = await response.json();
4229+
if (!result.success) {
4230+
throw new Error(result.message || "An error occurred");
4231+
} else {
4232+
const redirectUrl = isInactiveCheckedBool
4233+
? `${window.ROOT_PATH}/admin?include_inactive=true#resources`
4234+
: `${window.ROOT_PATH}/admin#resources`;
4235+
window.location.href = redirectUrl;
42224236
}
4223-
})
4224-
.catch((error) => {
4237+
} catch (error) {
42254238
console.error("Error:", error);
4226-
showErrorMessage("Failed to create resource");
4227-
});
4239+
if (status) {
4240+
status.textContent = error.message || "An error occurred!";
4241+
status.classList.add("error-status");
4242+
}
4243+
showErrorMessage(error.message);
4244+
} finally {
4245+
// location.reload();
4246+
if (loading) {
4247+
loading.style.display = "none";
4248+
}
4249+
}
42284250
}
42294251

42304252
async function handleServerFormSubmit(e) {

0 commit comments

Comments
 (0)