Skip to content

Commit 3a64feb

Browse files
authored
Merge pull request #103 from IBM/fix-basic-auth-gateways
Fix uv sync and return 502 status when gateway addition fails
2 parents 321714b + 768c87d commit 3a64feb

File tree

7 files changed

+283
-253
lines changed

7 files changed

+283
-253
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ pkill mcpgateway
177177
export MCP_AUTH_TOKEN=${MCPGATEWAY_BEARER_TOKEN}
178178
export MCP_SERVER_CATALOG_URLS=http://localhost:4444/servers/1
179179
python3 -m mcpgateway.wrapper
180+
# Alternatively with uv
181+
uv run --directory . -m mcpgateway.wrapper
180182
```
181183

182184
See [.env.example](.env.example) for full list of ENV variables you can use to override the configuration.
@@ -243,6 +245,8 @@ export MCP_WRAPPER_LOG_LEVEL=DEBUG # or OFF to disable logging
243245

244246
# Run the wrapper from the installed module
245247
python3 -m mcpgateway.wrapper
248+
# Alternatively with uv
249+
uv run --directory . -m mcpgateway.wrapper
246250
```
247251

248252
**Or using the container image:**
@@ -270,6 +274,8 @@ npx -y supergateway --stdio "uvenv run mcp_server_time -- --local-timezone=Europ
270274
export MCP_AUTH_TOKEN=${MCPGATEWAY_BEARER_TOKEN}
271275
export MCP_SERVER_CATALOG_URLS=http://localhost:4444/servers/1
272276
python3 -m mcpgateway.wrapper
277+
# Alternatively with uv
278+
uv run --directory . -m mcpgateway.wrapper
273279
```
274280

275281
<details>
@@ -352,6 +358,8 @@ pipx install --include-deps mcp-contextforge-gateway
352358
MCP_AUTH_TOKEN=${MCPGATEWAY_BEARER_TOKEN} \
353359
MCP_SERVER_CATALOG_URLS=http://localhost:4444/servers/1 \
354360
python3 -m mcpgateway.wrapper
361+
# Alternatively with uv
362+
uv run --directory . -m mcpgateway.wrapper
355363
```
356364

357365
**Claude Desktop JSON** (uses the host Python that pipx injected):
@@ -402,11 +410,9 @@ uv pip install mcp-contextforge-gateway
402410
# Launch wrapper
403411
MCP_AUTH_TOKEN=${MCPGATEWAY_BEARER_TOKEN} \
404412
MCP_SERVER_CATALOG_URLS=http://localhost:4444/servers/1 \
405-
uv python -m mcpgateway.wrapper # Use this just for testing, as the Client will run the uv command
413+
uv run --directory . -m mcpgateway.wrapper # Use this just for testing, as the Client will run the uv command
406414
```
407415

408-
*(You can swap `uv python` for plain `python` if the venv is active.)*
409-
410416
#### Claude Desktop JSON (runs through **uvenv run**)
411417

412418
```json

docs/docs/using/clients/cline.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,31 @@ To integrate Cline with your MCP Gateway:
3434
1. **Configure MCP Server**:
3535
- Open the Cline settings in VS Code.
3636
- Navigate to the MCP Servers section.
37-
- Add a new MCP server with the following configuration:
37+
- Add a new MCP server with the following configuration under mcpServers as shown below:
3838

3939
```json
40-
{
41-
"name": "MCP Gateway",
42-
"url": "http://localhost:4444",
43-
"auth": {
44-
"type": "basic",
45-
"username": "admin",
46-
"password": "changeme"
47-
}
48-
}
40+
"mcpServers": {
41+
"mcpgateway-wrapper": {
42+
"disabled": true,
43+
"timeout": 60,
44+
"type": "stdio",
45+
"command": "uv",
46+
"args": [
47+
"run",
48+
"--directory",
49+
"REPLACE_WITH_PATH_TO_REPO",
50+
"-m",
51+
"mcpgateway.wrapper"
52+
],
53+
"env": {
54+
"MCP_SERVER_CATALOG_URLS": "http://localhost:4444",
55+
"MCP_AUTH_TOKEN": "REPLACE_WITH_MCPGATEWAY_BEARER_TOKEN",
56+
"MCP_WRAPPER_LOG_LEVEL": "OFF"
57+
}
58+
}
59+
}
4960
```
5061

51-
- Replace the URL, username, and password with your MCP Gateway's details.
52-
5362
2. **Enable the MCP Server**:
5463
- Ensure the newly added MCP server is enabled in the Cline settings.
5564

mcpgateway/admin.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
ToolRead,
4949
ToolUpdate,
5050
)
51-
from mcpgateway.services.gateway_service import GatewayService
51+
from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayService
5252
from mcpgateway.services.prompt_service import PromptService
5353
from mcpgateway.services.resource_service import ResourceService
5454
from mcpgateway.services.root_service import RootService
@@ -762,10 +762,19 @@ async def admin_add_gateway(request: Request, db: Session = Depends(get_db), use
762762
auth_header_key=form.get("auth_header_key", ""),
763763
auth_header_value=form.get("auth_header_value", ""),
764764
)
765-
await gateway_service.register_gateway(db, gateway)
766-
767765
root_path = request.scope.get("root_path", "")
768-
return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)
766+
try:
767+
await gateway_service.register_gateway(db, gateway)
768+
return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)
769+
except Exception as ex:
770+
if isinstance(ex, GatewayConnectionError):
771+
return RedirectResponse(f"{root_path}/admin#gateways", status_code=502)
772+
elif isinstance(ex, ValueError):
773+
return RedirectResponse(f"{root_path}/admin#gateways", status_code=400)
774+
elif isinstance(ex, RuntimeError):
775+
return RedirectResponse(f"{root_path}/admin#gateways", status_code=500)
776+
else:
777+
return RedirectResponse(f"{root_path}/admin#gateways", status_code=500)
769778

770779

771780
@admin_router.post("/gateways/{gateway_id}/edit")

mcpgateway/main.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import asyncio
2929
import json
3030
import logging
31-
import os
3231
from contextlib import asynccontextmanager
3332
from typing import Any, AsyncIterator, Dict, List, Optional, Union
3433

@@ -78,7 +77,7 @@
7877
ToolUpdate,
7978
)
8079
from mcpgateway.services.completion_service import CompletionService
81-
from mcpgateway.services.gateway_service import GatewayService
80+
from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayService
8281
from mcpgateway.services.logging_service import LoggingService
8382
from mcpgateway.services.prompt_service import (
8483
PromptError,
@@ -1506,7 +1505,17 @@ async def register_gateway(
15061505
Created gateway.
15071506
"""
15081507
logger.debug(f"User '{user}' requested to register gateway: {gateway}")
1509-
return await gateway_service.register_gateway(db, gateway)
1508+
try:
1509+
return await gateway_service.register_gateway(db, gateway)
1510+
except Exception as ex:
1511+
if isinstance(ex, GatewayConnectionError):
1512+
return JSONResponse(content={"message": "Unable to connect to gateway"}, status_code=502)
1513+
elif isinstance(ex, ValueError):
1514+
return JSONResponse(content={"message": "Unable to process input"}, status_code=400)
1515+
elif isinstance(ex, RuntimeError):
1516+
return JSONResponse(content={"message": "Error during execution"}, status_code=500)
1517+
else:
1518+
return JSONResponse(content={"message": "Unexpected error"}, status_code=500)
15101519

15111520

15121521
@gateway_router.get("/{gateway_id}", response_model=GatewayRead)

mcpgateway/services/gateway_service.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,11 @@ def __init__(self):
123123
self._redis_client = None
124124

125125
async def initialize(self) -> None:
126-
"""Initialize the service and start health check if this instance is the leader."""
126+
"""Initialize the service and start health check if this instance is the leader.
127+
128+
Raises:
129+
ConnectionError: When redis ping fails
130+
"""
127131
logger.info("Initializing gateway service")
128132

129133
if self._redis_client:
@@ -166,6 +170,7 @@ async def register_gateway(self, db: Session, gateway: GatewayCreate) -> Gateway
166170
167171
Raises:
168172
GatewayNameConflictError: If gateway name already exists
173+
[]: When ExceptionGroup found
169174
"""
170175
try:
171176
# Check for name conflicts (both active and inactive)
@@ -232,19 +237,18 @@ async def register_gateway(self, db: Session, gateway: GatewayCreate) -> Gateway
232237
await self._notify_gateway_added(db_gateway)
233238

234239
return GatewayRead.model_validate(gateway)
240+
except* GatewayConnectionError as ge:
241+
logger.error("GatewayConnectionError in group: %s", ge.exceptions)
242+
raise ge.exceptions[0]
235243
except* ValueError as ve:
236244
logger.error("ValueErrors in group: %s", ve.exceptions)
245+
raise ve.exceptions[0]
237246
except* RuntimeError as re:
238247
logger.error("RuntimeErrors in group: %s", re.exceptions)
248+
raise re.exceptions[0]
239249
except* BaseException as other: # catches every other sub-exception
240250
logger.error("Other grouped errors: %s", other.exceptions)
241-
# except IntegrityError as ex:
242-
# logger.error(f"Error adding gateway: {ex}")
243-
# db.rollback()
244-
# raise GatewayError(f"Gateway already exists: {gateway.name}")
245-
# except Exception as e:
246-
# db.rollback()
247-
# raise GatewayError(f"Failed to register gateway: {str(e)}")
251+
raise other.exceptions[0]
248252

249253
async def list_gateways(self, db: Session, include_inactive: bool = False) -> List[GatewayRead]:
250254
"""List all registered gateways.

mcpgateway/wrapper.py

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -73,32 +73,52 @@
7373
# Base URL Extraction
7474
# -----------------------------------------------------------------------------
7575
def _extract_base_url(url: str) -> str:
76-
"""
77-
Extract the base URL (scheme and network location) from a full URL.
76+
"""Return the gateway-level base URL.
77+
78+
The function keeps any application root path (`APP_ROOT_PATH`) that the
79+
remote gateway is mounted under (for example `/gateway`) while removing
80+
the `/servers/<id>` suffix that appears in catalog endpoints. It also
81+
discards any query string or fragment.
7882
7983
Args:
80-
url (str): The full URL to parse, e.g., "https://example.com/path?query=1".
84+
url (str): Full catalog URL, e.g.
85+
`https://host.com/gateway/servers/1`.
8186
8287
Returns:
83-
str: The base URL, including scheme and netloc, e.g., "https://example.com".
88+
str: Clean base URL suitable for building `/tools/`, `/prompts/`,
89+
or `/resources/` endpoints—for example
90+
`https://host.com/gateway`.
8491
8592
Raises:
86-
ValueError: If the URL does not contain a scheme or netloc.
87-
88-
Example:
89-
>>> _extract_base_url("https://www.example.com/path/to/resource")
90-
'https://www.example.com'
93+
ValueError: If *url* lacks a scheme or network location.
94+
95+
Examples:
96+
>>> _extract_base_url("https://host.com/servers/2")
97+
'https://host.com'
98+
>>> _extract_base_url("https://host.com/gateway/servers/2")
99+
'https://host.com/gateway'
100+
>>> _extract_base_url("https://host.com/gateway/servers")
101+
'https://host.com/gateway'
102+
>>> _extract_base_url("https://host.com/gateway")
103+
'https://host.com/gateway'
104+
105+
Note:
106+
If the target server was started with `APP_ROOT_PATH=/gateway`, the
107+
resulting catalog URLs include that prefix. This helper preserves the
108+
prefix so the wrapper's follow-up calls remain correctly scoped.
91109
"""
92110
parsed = urlparse(url)
93111
if not parsed.scheme or not parsed.netloc:
94112
raise ValueError(f"Invalid URL provided: {url}")
95113

96-
if "/servers/" in url:
97-
before_servers = parsed.path.split("/servers")[0]
98-
return f"{parsed.scheme}://{parsed.netloc}{before_servers}"
99-
100-
return f"{url}"
114+
path = parsed.path or ""
115+
if "/servers/" in path:
116+
path = path.split("/servers")[0] # ".../servers/123" -> "..."
117+
elif path.endswith("/servers"):
118+
path = path[:-len("/servers")] # ".../servers" -> "..."
119+
# otherwise keep the existing path (supports APP_ROOT_PATH)
101120

121+
return f"{parsed.scheme}://{parsed.netloc}{path}"
102122

103123
BASE_URL: str = _extract_base_url(SERVER_CATALOG_URLS[0]) if SERVER_CATALOG_URLS else ""
104124

@@ -162,10 +182,6 @@ async def get_tools_from_mcp_server(catalog_urls: List[str]) -> List[str]:
162182
163183
Returns:
164184
List[str]: A list of tool ID strings extracted from the server catalog.
165-
166-
Raises:
167-
httpx.RequestError: If a network problem occurs.
168-
httpx.HTTPStatusError: If the server returns a 4xx or 5xx response.
169185
"""
170186
server_ids = [url.split("/")[-1] for url in catalog_urls]
171187
url = f"{BASE_URL}/servers/"
@@ -187,10 +203,6 @@ async def tools_metadata(tool_ids: List[str]) -> List[Dict[str, Any]]:
187203
188204
Returns:
189205
List[Dict[str, Any]]: A list of metadata dictionaries for each tool.
190-
191-
Raises:
192-
httpx.RequestError: If a network problem occurs.
193-
httpx.HTTPStatusError: If the server returns a 4xx or 5xx response.
194206
"""
195207
if not tool_ids:
196208
return []
@@ -212,10 +224,6 @@ async def get_prompts_from_mcp_server(catalog_urls: List[str]) -> List[str]:
212224
213225
Returns:
214226
List[str]: A list of prompt ID strings.
215-
216-
Raises:
217-
httpx.RequestError: If a network problem occurs.
218-
httpx.HTTPStatusError: If the server returns a 4xx or 5xx response.
219227
"""
220228
server_ids = [url.split("/")[-1] for url in catalog_urls]
221229
url = f"{BASE_URL}/servers/"
@@ -237,10 +245,6 @@ async def prompts_metadata(prompt_ids: List[str]) -> List[Dict[str, Any]]:
237245
238246
Returns:
239247
List[Dict[str, Any]]: A list of metadata dictionaries for each prompt.
240-
241-
Raises:
242-
httpx.RequestError: If a network problem occurs.
243-
httpx.HTTPStatusError: If the server returns a 4xx or 5xx response.
244248
"""
245249
if not prompt_ids:
246250
return []
@@ -261,10 +265,6 @@ async def get_resources_from_mcp_server(catalog_urls: List[str]) -> List[str]:
261265
262266
Returns:
263267
List[str]: A list of resource ID strings.
264-
265-
Raises:
266-
httpx.RequestError: If a network problem occurs.
267-
httpx.HTTPStatusError: If the server returns a 4xx or 5xx response.
268268
"""
269269
server_ids = [url.split("/")[-1] for url in catalog_urls]
270270
url = f"{BASE_URL}/servers/"
@@ -286,10 +286,6 @@ async def resources_metadata(resource_ids: List[str]) -> List[Dict[str, Any]]:
286286
287287
Returns:
288288
List[Dict[str, Any]]: A list of metadata dictionaries for each resource.
289-
290-
Raises:
291-
httpx.RequestError: If a network problem occurs.
292-
httpx.HTTPStatusError: If the server returns a 4xx or 5xx response.
293289
"""
294290
if not resource_ids:
295291
return []

0 commit comments

Comments
 (0)