Skip to content

Commit 6f5ac55

Browse files
kevalmahajanNAYANAR0502
authored andcommitted
Improved gateway duplication check allowing same url with certain conditions (#1424)
* imporved duplicated gateway check Signed-off-by: Keval Mahajan <[email protected]> * error message changes Signed-off-by: Keval Mahajan <[email protected]> * check gateway uniqueness while updating too Signed-off-by: Keval Mahajan <[email protected]> * linting Signed-off-by: Keval Mahajan <[email protected]> * code linting Signed-off-by: Keval Mahajan <[email protected]> * added alembic migration script for removal of url uniquess constraint Signed-off-by: Keval Mahajan <[email protected]> * lints Signed-off-by: Keval Mahajan <[email protected]> * updated doctest Signed-off-by: Keval Mahajan <[email protected]> * updated test cases Signed-off-by: Keval Mahajan <[email protected]> * removed ununsed import Signed-off-by: Keval Mahajan <[email protected]> * updated docstring Signed-off-by: Keval Mahajan <[email protected]> --------- Signed-off-by: Keval Mahajan <[email protected]>
1 parent 478a825 commit 6f5ac55

File tree

6 files changed

+337
-119
lines changed

6 files changed

+337
-119
lines changed

mcpgateway/admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
from mcpgateway.services.catalog_service import catalog_service
104104
from mcpgateway.services.encryption_service import get_encryption_service
105105
from mcpgateway.services.export_service import ExportError, ExportService
106-
from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayNameConflictError, GatewayNotFoundError, GatewayService, GatewayUrlConflictError
106+
from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayDuplicateConflictError, GatewayNameConflictError, GatewayNotFoundError, GatewayService
107107
from mcpgateway.services.import_service import ConflictStrategy
108108
from mcpgateway.services.import_service import ImportError as ImportServiceError
109109
from mcpgateway.services.import_service import ImportService, ImportValidationError
@@ -7098,7 +7098,7 @@ async def admin_add_gateway(request: Request, db: Session = Depends(get_db), use
70987098

70997099
except GatewayConnectionError as ex:
71007100
return JSONResponse(content={"message": str(ex), "success": False}, status_code=502)
7101-
except GatewayUrlConflictError as ex:
7101+
except GatewayDuplicateConflictError as ex:
71027102
return JSONResponse(content={"message": str(ex), "success": False}, status_code=409)
71037103
except GatewayNameConflictError as ex:
71047104
return JSONResponse(content={"message": str(ex), "success": False}, status_code=409)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# -*- coding: utf-8 -*-
2+
"""Location: ./mcpgateway/alembic/versions/f3a3a3d901b8_remove_gateway_url_unique_constraint.py
3+
Copyright 2025
4+
SPDX-License-Identifier: Apache-2.0
5+
Authors: Keval Mahajan
6+
7+
Alembic migration to remove unique constraint on gateway URL.
8+
An improved alternative duplication check has been implemented for gateway duplication prevention.
9+
10+
Revision ID: f3a3a3d901b8
11+
Revises: aac21d6f9522
12+
Create Date: 2025-11-11 22:30:05.474282
13+
14+
"""
15+
16+
# Standard
17+
from typing import Sequence, Union
18+
19+
# Third-Party
20+
from alembic import op
21+
from sqlalchemy.engine import Inspector
22+
23+
# revision identifiers, used by Alembic.
24+
revision: str = "f3a3a3d901b8"
25+
down_revision: Union[str, Sequence[str], None] = "aac21d6f9522"
26+
branch_labels: Union[str, Sequence[str], None] = None
27+
depends_on: Union[str, Sequence[str], None] = None
28+
29+
30+
def constraint_exists(inspector, table_name, constraint_name):
31+
"""
32+
Check if a specific unique constraint exists on a given table.
33+
34+
This function queries the database using the provided SQLAlchemy
35+
inspector to determine if a constraint with the given name exists.
36+
If the check fails due to an exception (e.g., database connectivity issues),
37+
it conservatively assumes that the constraint exists.
38+
39+
Args:
40+
inspector (sqlalchemy.engine.reflection.Inspector): SQLAlchemy inspector
41+
instance for database introspection.
42+
table_name (str): Name of the table to inspect.
43+
constraint_name (str): Name of the unique constraint to check.
44+
45+
Returns:
46+
bool: True if the constraint exists or if the check could not be performed,
47+
False if the constraint does not exist.
48+
"""
49+
try:
50+
unique_constraints = inspector.get_unique_constraints(table_name)
51+
return any(uc["name"] == constraint_name for uc in unique_constraints)
52+
except Exception:
53+
# Fallback: assume constraint exists if we can't check
54+
return True
55+
56+
57+
def upgrade():
58+
"""Remove the unique constraint on (team_id, owner_email, url) from gateway table."""
59+
60+
conn = op.get_bind()
61+
inspector = Inspector.from_engine(conn)
62+
63+
# Check if constraint exists before attempting to drop
64+
if not constraint_exists(inspector, "gateways", "uq_team_owner_url_gateway"):
65+
print("Constraint 'uq_team_owner_url_gateway' does not exist, skipping drop.")
66+
return
67+
68+
if conn.dialect.name == "sqlite":
69+
# SQLite: Use batch mode to recreate table without the constraint
70+
with op.batch_alter_table("gateways", schema=None) as batch_op:
71+
batch_op.drop_constraint("uq_team_owner_url_gateway", type_="unique")
72+
else:
73+
# PostgreSQL, MySQL, etc.: Direct constraint drop
74+
op.drop_constraint("uq_team_owner_url_gateway", "gateways", type_="unique")
75+
76+
print("Successfully removed constraint 'uq_team_owner_url_gateway' from gateway table.")
77+
78+
79+
def downgrade():
80+
"""Re-add the unique constraint on (team_id, owner_email, url) to gateway table."""
81+
82+
conn = op.get_bind()
83+
inspector = Inspector.from_engine(conn)
84+
85+
# Check if constraint already exists before attempting to create
86+
if constraint_exists(inspector, "gateways", "uq_team_owner_url_gateway"):
87+
print("Constraint 'uq_team_owner_url_gateway' already exists, skipping creation.")
88+
return
89+
90+
if conn.dialect.name == "sqlite":
91+
# SQLite: Use batch mode to recreate table with the constraint
92+
with op.batch_alter_table("gateways", schema=None) as batch_op:
93+
batch_op.create_unique_constraint("uq_team_owner_url_gateway", ["team_id", "owner_email", "url"])
94+
else:
95+
# PostgreSQL, MySQL, etc.: Direct constraint creation
96+
op.create_unique_constraint("uq_team_owner_url_constraint", "gateways", ["team_id", "owner_email", "url"])
97+
98+
print("Successfully re-added constraint 'uq_team_owner_url_gateway' to gateways table.")

mcpgateway/db.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2885,10 +2885,7 @@ class Gateway(Base):
28852885

28862886
registered_oauth_clients: Mapped[List["RegisteredOAuthClient"]] = relationship("RegisteredOAuthClient", back_populates="gateway", cascade="all, delete-orphan")
28872887

2888-
__table_args__ = (
2889-
UniqueConstraint("team_id", "owner_email", "slug", name="uq_team_owner_slug_gateway"),
2890-
UniqueConstraint("team_id", "owner_email", "url", name="uq_team_owner_url_gateway"),
2891-
)
2888+
__table_args__ = (UniqueConstraint("team_id", "owner_email", "slug", name="uq_team_owner_slug_gateway"),)
28922889

28932890

28942891
@event.listens_for(Gateway, "after_update")

mcpgateway/main.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
from mcpgateway.services.a2a_service import A2AAgentError, A2AAgentNameConflictError, A2AAgentNotFoundError, A2AAgentService
109109
from mcpgateway.services.completion_service import CompletionService
110110
from mcpgateway.services.export_service import ExportError, ExportService
111-
from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayError, GatewayNameConflictError, GatewayNotFoundError, GatewayService, GatewayUrlConflictError
111+
from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayDuplicateConflictError, GatewayError, GatewayNameConflictError, GatewayNotFoundError, GatewayService
112112
from mcpgateway.services.import_service import ConflictStrategy, ImportConflictError
113113
from mcpgateway.services.import_service import ImportError as ImportServiceError
114114
from mcpgateway.services.import_service import ImportService, ImportValidationError
@@ -3421,8 +3421,8 @@ async def register_gateway(
34213421
return JSONResponse(content={"message": "Unable to process input"}, status_code=status.HTTP_400_BAD_REQUEST)
34223422
if isinstance(ex, GatewayNameConflictError):
34233423
return JSONResponse(content={"message": "Gateway name already exists"}, status_code=status.HTTP_409_CONFLICT)
3424-
if isinstance(ex, GatewayUrlConflictError):
3425-
return JSONResponse(content={"message": "Gateway URL already exists"}, status_code=status.HTTP_409_CONFLICT)
3424+
if isinstance(ex, GatewayDuplicateConflictError):
3425+
return JSONResponse(content={"message": "Gateway already exists"}, status_code=status.HTTP_409_CONFLICT)
34263426
if isinstance(ex, RuntimeError):
34273427
return JSONResponse(content={"message": "Error during execution"}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
34283428
if isinstance(ex, ValidationError):
@@ -3499,8 +3499,8 @@ async def update_gateway(
34993499
return JSONResponse(content={"message": "Unable to process input"}, status_code=status.HTTP_400_BAD_REQUEST)
35003500
if isinstance(ex, GatewayNameConflictError):
35013501
return JSONResponse(content={"message": "Gateway name already exists"}, status_code=status.HTTP_409_CONFLICT)
3502-
if isinstance(ex, GatewayUrlConflictError):
3503-
return JSONResponse(content={"message": "Gateway URL already exists"}, status_code=status.HTTP_409_CONFLICT)
3502+
if isinstance(ex, GatewayDuplicateConflictError):
3503+
return JSONResponse(content={"message": "Gateway already exists"}, status_code=status.HTTP_409_CONFLICT)
35043504
if isinstance(ex, RuntimeError):
35053505
return JSONResponse(content={"message": "Error during execution"}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
35063506
if isinstance(ex, ValidationError):

0 commit comments

Comments
 (0)