Skip to content

Commit 9e91cee

Browse files
committed
made changes to fix bug #476
Signed-off-by: Satya <[email protected]>
1 parent 1a8b6f8 commit 9e91cee

File tree

6 files changed

+133
-34
lines changed

6 files changed

+133
-34
lines changed

mcpgateway/admin.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
2929
import httpx
3030
from pydantic import ValidationError
31+
from pydantic_core import ValidationError as CoreValidationError
3132
from sqlalchemy.exc import IntegrityError
3233
from sqlalchemy.orm import Session
3334

@@ -61,7 +62,7 @@
6162
from mcpgateway.services.prompt_service import PromptNotFoundError, PromptService
6263
from mcpgateway.services.resource_service import ResourceNotFoundError, ResourceService
6364
from mcpgateway.services.root_service import RootService
64-
from mcpgateway.services.server_service import ServerNotFoundError, ServerService
65+
from mcpgateway.services.server_service import ServerError, ServerNotFoundError, ServerService
6566
from mcpgateway.services.tool_service import ToolError, ToolNameConflictError, ToolNotFoundError, ToolService
6667
from mcpgateway.utils.create_jwt_token import get_jwt_token
6768
from mcpgateway.utils.error_formatter import ErrorFormatter
@@ -405,10 +406,10 @@ async def admin_add_server(request: Request, db: Session = Depends(get_db), user
405406
>>> server_service.register_server = original_register_server
406407
"""
407408
form = await request.form()
408-
is_inactive_checked = form.get("is_inactive_checked", "false")
409+
# root_path = request.scope.get("root_path", "")
410+
# is_inactive_checked = form.get("is_inactive_checked", "false")
409411
try:
410412
logger.debug(f"User {user} is adding a new server with name: {form['name']}")
411-
412413
server = ServerCreate(
413414
name=form.get("name"),
414415
description=form.get("description"),
@@ -417,24 +418,46 @@ async def admin_add_server(request: Request, db: Session = Depends(get_db), user
417418
associated_resources=form.get("associatedResources"),
418419
associated_prompts=form.get("associatedPrompts"),
419420
)
421+
except KeyError as e:
422+
# Convert KeyError to ValidationError-like response
423+
return JSONResponse(content={"message": f"Missing required field: {e}", "success": False}, status_code=422)
424+
425+
try:
420426
await server_service.register_server(db, server)
427+
return JSONResponse(
428+
content={"message": "Server created successfully!", "success": True},
429+
status_code=200,
430+
)
421431

422-
root_path = request.scope.get("root_path", "")
423-
if is_inactive_checked.lower() == "true":
424-
return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)
425-
return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
432+
except CoreValidationError as ex:
433+
return JSONResponse(content={"message": str(ex)}, status_code=422)
434+
435+
except IntegrityError as ex:
436+
logger.error(f"Database error: {ex}")
437+
return JSONResponse(content={"success": False, "message": "Server name already exists."}, status_code=409)
426438
except Exception as ex:
439+
if isinstance(ex, ServerError):
440+
# Custom server logic error — 500 Internal Server Error makes sense
441+
return JSONResponse(content={"message": str(ex), "success": False}, status_code=500)
442+
443+
if isinstance(ex, ValueError):
444+
# Invalid input — 400 Bad Request is appropriate
445+
return JSONResponse(content={"message": str(ex), "success": False}, status_code=400)
446+
447+
if isinstance(ex, RuntimeError):
448+
# Unexpected error during runtime — 500 is suitable
449+
return JSONResponse(content={"message": str(ex), "success": False}, status_code=500)
450+
427451
if isinstance(ex, ValidationError):
428-
logger.info(f"ValidationError adding server: type{ex}")
452+
# Pydantic or input validation failure — 422 Unprocessable Entity is correct
429453
return JSONResponse(content=ErrorFormatter.format_validation_error(ex), status_code=422)
454+
430455
if isinstance(ex, IntegrityError):
431-
logger.info(f"IntegrityError adding server: type{ex}")
456+
# DB constraint violation — 409 Conflict is appropriate
432457
return JSONResponse(status_code=409, content=ErrorFormatter.format_database_error(ex))
433-
logger.info(f"Other Error adding server:{ex}")
434-
root_path = request.scope.get("root_path", "")
435-
if is_inactive_checked.lower() == "true":
436-
return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)
437-
return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
458+
459+
# For any other unhandled error, default to 500
460+
return JSONResponse(content={"message": str(ex), "success": False}, status_code=500)
438461

439462

440463
@admin_router.post("/servers/{server_id}/edit")

mcpgateway/static/admin.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4227,6 +4227,69 @@ function handleResourceFormSubmit(e) {
42274227
});
42284228
}
42294229

4230+
async function handleServerFormSubmit(e) {
4231+
e.preventDefault();
4232+
4233+
const form = e.target;
4234+
const formData = new FormData(form);
4235+
const status = safeGetElement("serverFormError");
4236+
const loading = safeGetElement("add-server-loading"); // Add a loading spinner if needed
4237+
4238+
try {
4239+
const name = formData.get("name");
4240+
4241+
// Basic validation
4242+
const nameValidation = validateInputName(name, "server");
4243+
if (!nameValidation.valid) {
4244+
throw new Error(nameValidation.error);
4245+
}
4246+
4247+
if (loading) {
4248+
loading.style.display = "block";
4249+
}
4250+
4251+
if (status) {
4252+
status.textContent = "";
4253+
status.classList.remove("error-status");
4254+
}
4255+
4256+
const isInactiveCheckedBool = isInactiveChecked("servers");
4257+
formData.append("is_inactive_checked", isInactiveCheckedBool);
4258+
4259+
const response = await fetchWithTimeout(
4260+
`${window.ROOT_PATH}/admin/servers`,
4261+
{
4262+
method: "POST",
4263+
body: formData,
4264+
redirect: "manual",
4265+
},
4266+
);
4267+
4268+
const result = await response.json();
4269+
if (!result.success) {
4270+
console.log(result.message);
4271+
throw new Error(result.message || "Failed to add server.");
4272+
} else {
4273+
// Success redirect
4274+
const redirectUrl = isInactiveCheckedBool
4275+
? `${window.ROOT_PATH}/admin?include_inactive=true#catalog`
4276+
: `${window.ROOT_PATH}/admin#catalog`;
4277+
window.location.href = redirectUrl;
4278+
}
4279+
} catch (error) {
4280+
console.error("Add Server Error:", error);
4281+
if (status) {
4282+
status.textContent = error.message || "An error occurred.";
4283+
status.classList.add("error-status");
4284+
}
4285+
showErrorMessage(error.message); // Optional if you use global popup/snackbar
4286+
} finally {
4287+
if (loading) {
4288+
loading.style.display = "none";
4289+
}
4290+
}
4291+
}
4292+
42304293
async function handleToolFormSubmit(event) {
42314294
event.preventDefault();
42324295

@@ -4821,6 +4884,11 @@ function setupFormHandlers() {
48214884
paramButton.addEventListener("click", handleAddParameter);
48224885
}
48234886

4887+
const serverForm = safeGetElement("add-server-form");
4888+
if (serverForm) {
4889+
serverForm.addEventListener("submit", handleServerFormSubmit);
4890+
}
4891+
48244892
const editResourceForm = safeGetElement("edit-resource-form");
48254893
if (editResourceForm) {
48264894
editResourceForm.addEventListener("submit", () => {

mcpgateway/templates/admin.html

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,7 @@ <h2 class="text-2xl font-bold dark:text-gray-200">MCP Servers Catalog</h2>
240240
</div>
241241
<div class="bg-white shadow rounded-lg p-6 dark:bg-gray-800">
242242
<h3 class="text-lg font-bold mb-4 dark:text-gray-200">Add New Server</h3>
243-
<form method="POST" action="{{ root_path }}/admin/servers" id="add-server-form"
244-
onsubmit="return handleToggleSubmit(event, 'servers')"
245-
onreset="document.getElementById('associatedTools').selectedIndex = -1;">
243+
<form method="POST" id="add-server-form" onreset="document.getElementById('associatedTools').selectedIndex = -1;">
246244
<div class="grid grid-cols-1 gap-6">
247245
<div>
248246
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400">Name</label>
@@ -263,15 +261,13 @@ <h3 class="text-lg font-bold mb-4 dark:text-gray-200">Add New Server</h3>
263261
<label for="associatedTools" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
264262
Associated Tools
265263
</label>
266-
<select name="associatedTools" {# same name as before (minus camelCase) #} id="associatedTools" multiple
267-
{# remove if you want single-select #}
264+
<select name="associatedTools" id="associatedTools" multiple
268265
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">
269266
{% for tool in tools %}
270-
{# value can be tool.id, tool.slug, or any key you use later #}
271267
<option value="{{ tool.id }}">{{ tool.name }}</option>
272268
{% endfor %}
273269
</select>
274-
<span id="selectedToolsPills" class="inline-flex flex-wrap gap-1 mt-2"></span> <!-- container -->
270+
<span id="selectedToolsPills" class="inline-flex flex-wrap gap-1 mt-2"></span>
275271
<span id="selectedToolsWarning" class="block text-sm font-semibold text-red-600 mt-1"></span>
276272
</div>
277273
<div>
@@ -289,8 +285,14 @@ <h3 class="text-lg font-bold mb-4 dark:text-gray-200">Add New Server</h3>
289285
placeholder="e.g., prompt1, prompt2" />
290286
</div>
291287
</div>
288+
<!-- 🔻 Error Display Span -->
289+
<span id="serverFormError" class="block text-sm font-semibold text-red-600 mt-2"></span>
290+
291+
<!-- 🔄 Optional Loading Indicator -->
292+
<div id="add-server-loading" class="hidden text-sm text-gray-500 mt-2">Submitting...</div>
293+
292294
<div class="mt-6">
293-
<button type="submit" data-testid="add-server-btn"
295+
<button type="submit"
294296
x-tooltip="'💡Creates a new Virtual Server by combining Tools, Resources & Prompts from global catalogs.'"
295297
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
296298
Add Server

tests/e2e/test_admin_apis.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,8 @@ async def test_admin_server_lifecycle(self, client: AsyncClient, mock_settings):
202202

203203
# POST to /admin/servers should redirect
204204
response = await client.post("/admin/servers", data=form_data, headers=TEST_AUTH_HEADER, follow_redirects=False)
205-
assert response.status_code == 303
206-
assert "/admin#catalog" in response.headers["location"]
205+
assert response.status_code == 200
206+
# assert "/admin#catalog" in response.headers["location"]
207207

208208
# Get all servers and find our server
209209
response = await client.get("/admin/servers", headers=TEST_AUTH_HEADER)

tests/security/test_input_validation.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -480,16 +480,16 @@ def must_fail(schema: dict, label: str = "Invalid schema") -> None:
480480
try:
481481
tool = ToolCreate(name=self.VALID_TOOL_NAME, url=self.VALID_URL, input_schema=schema)
482482
assert tool.input_schema is not None
483-
print(f"✅ Valid schema #{i+1} accepted")
483+
print(f"✅ Valid schema #{i + 1} accepted")
484484
except ValidationError as err:
485-
print(f"❌ Valid schema #{i+1} rejected: {str(schema)[:50]}... -> {err}")
485+
print(f"❌ Valid schema #{i + 1} rejected: {str(schema)[:50]}... -> {err}")
486486
raise
487487

488488
# Invalid schemas - too deep
489489
for i, payload in enumerate(self.DEEP_NESTING_PAYLOADS):
490490
if isinstance(payload, dict):
491491
logger.debug(f"Testing deep nested schema")
492-
must_fail(payload, f"Deep nested schema #{i+1}")
492+
must_fail(payload, f"Deep nested schema #{i + 1}")
493493

494494
def test_tool_create_request_type_validation(self):
495495
"""Test request type validation based on integration type."""
@@ -638,7 +638,7 @@ def must_fail(content, label: str = "Invalid content") -> None:
638638
# Invalid content - HTML tags
639639
for i, payload in enumerate(self.XSS_PAYLOADS[:5]):
640640
logger.debug(f"Testing XSS payload in content: {payload[:50]}...")
641-
must_fail(payload, f"XSS content #{i+1}")
641+
must_fail(payload, f"XSS content #{i + 1}")
642642

643643
def test_resource_create_mime_type_validation(self):
644644
"""Test MIME type validation."""
@@ -911,7 +911,7 @@ def must_fail(payload: str, label: str = "Polyglot payload") -> None:
911911

912912
for i, payload in enumerate(polyglot_payloads):
913913
logger.debug(f"Testing polyglot payload: {payload[:50]}...")
914-
must_fail(payload, f"Polyglot #{i+1}")
914+
must_fail(payload, f"Polyglot #{i + 1}")
915915

916916
def test_timing_attack_prevention(self):
917917
"""Test that validation doesn't reveal timing information."""
@@ -1117,7 +1117,7 @@ def must_fail(template: str, label: str = "SSTI payload") -> None:
11171117

11181118
for i, payload in enumerate(ssti_payloads):
11191119
logger.debug(f"Testing SSTI payload: {payload[:50]}...")
1120-
must_fail(payload, f"SSTI #{i+1} ({payload[:20]}...)")
1120+
must_fail(payload, f"SSTI #{i + 1} ({payload[:20]}...)")
11211121

11221122
def test_regex_dos_prevention(self):
11231123
"""Test prevention of ReDoS attacks."""
@@ -1180,7 +1180,7 @@ def test_prototype_pollution_prevention(self):
11801180
]
11811181

11821182
for i, payload in enumerate(pollution_payloads):
1183-
logger.debug(f"Testing prototype pollution payload {i+1}")
1183+
logger.debug(f"Testing prototype pollution payload {i + 1}")
11841184
# Should handle these safely
11851185
try:
11861186
tool = ToolCreate(name=self.VALID_TOOL_NAME, url=self.VALID_URL, headers=payload)

tests/unit/mcpgateway/test_admin.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,9 @@ async def test_admin_add_server_with_empty_associations(self, mock_register_serv
287287
result = await admin_add_server(mock_request, mock_db, "test-user")
288288

289289
# Should still succeed
290-
assert isinstance(result, RedirectResponse)
291-
assert result.status_code == 303
290+
# assert isinstance(result, RedirectResponse)
291+
# changing the redirect status code (303) to success-status code (200)
292+
assert result.status_code == 200
292293

293294
@patch.object(ServerService, "update_server")
294295
async def test_admin_edit_server_with_root_path(self, mock_update_server, mock_request, mock_db):
@@ -1338,9 +1339,14 @@ async def test_exception_handling_consistency(self, exception_type, expected_sta
13381339

13391340
result = await admin_add_server(mock_request, mock_db, "test-user")
13401341

1342+
print(f"\nException: {exception_type.__name__ if hasattr(exception_type, '__name__') else exception_type}")
1343+
print(f"Result Type: {type(result)}")
1344+
print(f"Status Code: {getattr(result, 'status_code', 'N/A')}")
1345+
13411346
if expected_status in [422, 409]:
13421347
assert isinstance(result, JSONResponse)
13431348
assert result.status_code == expected_status
13441349
else:
13451350
# Generic exceptions return redirect
1346-
assert isinstance(result, RedirectResponse)
1351+
# assert isinstance(result, RedirectResponse)
1352+
assert isinstance(result, JSONResponse)

0 commit comments

Comments
 (0)