From e9eef541c135e62c6ec8ffee0344337f4116fc4f Mon Sep 17 00:00:00 2001 From: rakdutta Date: Mon, 1 Dec 2025 10:36:02 +0530 Subject: [PATCH 01/23] UUID res and prompt Signed-off-by: rakdutta --- mcpgateway/admin.py | 11 +++++------ mcpgateway/db.py | 6 ++++-- mcpgateway/main.py | 4 ++-- mcpgateway/schemas.py | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 31ee5629d..993fe5e61 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -8083,7 +8083,7 @@ async def admin_delete_gateway(gateway_id: str, request: Request, db: Session = @admin_router.get("/resources/{resource_id}") -async def admin_get_resource(resource_id: int, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions)) -> Dict[str, Any]: +async def admin_get_resource(resource_id: str, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions)) -> Dict[str, Any]: """Get resource details for the admin UI. Args: @@ -8532,7 +8532,7 @@ async def admin_delete_resource(resource_id: str, request: Request, db: Session @admin_router.post("/resources/{resource_id}/toggle") async def admin_toggle_resource( - resource_id: int, + resource_id: str, request: Request, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), @@ -8656,7 +8656,7 @@ async def admin_toggle_resource( @admin_router.get("/prompts/{prompt_id}") -async def admin_get_prompt(prompt_id: int, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions)) -> Dict[str, Any]: +async def admin_get_prompt(prompt_id: str, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions)) -> Dict[str, Any]: """Get prompt details for the admin UI. Args: @@ -8943,8 +8943,7 @@ async def admin_edit_prompt( """ LOGGER.debug(f"User {get_user_email(user)} is editing prompt {prompt_id}") form = await request.form() - LOGGER.info(f"form data: {form}") - + visibility = str(form.get("visibility", "private")) user_email = get_user_email(user) # Determine personal team for default assignment @@ -9087,7 +9086,7 @@ async def admin_delete_prompt(prompt_id: str, request: Request, db: Session = De @admin_router.post("/prompts/{prompt_id}/toggle") async def admin_toggle_prompt( - prompt_id: int, + prompt_id: str, request: Request, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), diff --git a/mcpgateway/db.py b/mcpgateway/db.py index fd9da825a..900f45cf0 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -2210,7 +2210,8 @@ class Resource(Base): __tablename__ = "resources" - id: Mapped[int] = mapped_column(primary_key=True) + #id: Mapped[int] = mapped_column(primary_key=True) + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex) uri: Mapped[str] = mapped_column(String(767), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) @@ -2469,7 +2470,8 @@ class Prompt(Base): __tablename__ = "prompts" - id: Mapped[int] = mapped_column(primary_key=True) + #id: Mapped[int] = mapped_column(primary_key=True) + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) template: Mapped[str] = mapped_column(Text) diff --git a/mcpgateway/main.py b/mcpgateway/main.py index 96c5da508..ce558573b 100644 --- a/mcpgateway/main.py +++ b/mcpgateway/main.py @@ -2767,7 +2767,7 @@ async def list_resource_templates( @resource_router.post("/{resource_id}/toggle") @require_permission("resources.update") async def toggle_resource_status( - resource_id: int, + resource_id: str, activate: bool = True, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), @@ -3126,7 +3126,7 @@ async def subscribe_resource(user=Depends(get_current_user_with_permissions)) -> @prompt_router.post("/{prompt_id}/toggle") @require_permission("prompts.update") async def toggle_prompt_status( - prompt_id: int, + prompt_id: str, activate: bool = True, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), diff --git a/mcpgateway/schemas.py b/mcpgateway/schemas.py index 475dc7cf4..3cd42176c 100644 --- a/mcpgateway/schemas.py +++ b/mcpgateway/schemas.py @@ -1771,7 +1771,7 @@ class ResourceRead(BaseModelWithConfigDict): - Metrics: Aggregated metrics for the resource invocations. """ - id: int + id: str = Field(description="Unique ID of the resource") uri: str name: str description: Optional[str] @@ -2280,7 +2280,7 @@ class PromptRead(BaseModelWithConfigDict): - Metrics: Aggregated metrics for the prompt invocations. """ - id: int + id: str = Field(description="Unique ID of the prompt") name: str description: Optional[str] template: str From af0ab072ce7d654a1092fb8824c74ec103df583e Mon Sep 17 00:00:00 2001 From: rakdutta Date: Mon, 1 Dec 2025 12:11:54 +0530 Subject: [PATCH 02/23] get_prompt main Signed-off-by: rakdutta --- mcpgateway/services/prompt_service.py | 37 +++++---------------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/mcpgateway/services/prompt_service.py b/mcpgateway/services/prompt_service.py index ac7bbdcaf..1877e384a 100644 --- a/mcpgateway/services/prompt_service.py +++ b/mcpgateway/services/prompt_service.py @@ -736,21 +736,8 @@ async def get_prompt( ) as span: try: # Determine how to look up the prompt - prompt_id_int = None prompt_name = None - if isinstance(prompt_id, int): - prompt_id_int = prompt_id - elif isinstance(prompt_id, str): - # Try to convert to int first (for backward compatibility with numeric string IDs) - try: - prompt_id_int = int(prompt_id) - except ValueError: - # Not a numeric string, treat as prompt name - prompt_name = prompt_id - else: - prompt_id_int = prompt_id - if self._plugin_manager: # Use existing context_table from previous hooks if available context_table = plugin_context_table @@ -773,7 +760,7 @@ async def get_prompt( pre_result, context_table = await self._plugin_manager.invoke_hook( PromptHookType.PROMPT_PRE_FETCH, - payload=PromptPrehookPayload(prompt_id=str(prompt_id), args=arguments), + payload=PromptPrehookPayload(prompt_id=prompt_id, args=arguments), global_context=global_context, local_contexts=context_table, # Pass context from previous hooks violations_as_exceptions=True, @@ -782,23 +769,12 @@ async def get_prompt( # Use modified payload if provided if pre_result.modified_payload: payload = pre_result.modified_payload - # Re-parse the modified prompt_id - if isinstance(payload.prompt_id, int): - prompt_id_int = payload.prompt_id - prompt_name = None - elif isinstance(payload.prompt_id, str): - try: - prompt_id_int = int(payload.prompt_id) - prompt_name = None - except ValueError: - prompt_name = payload.prompt_id - prompt_id_int = None arguments = payload.args # Find prompt by ID or name - if prompt_id_int is not None: - prompt = db.execute(select(DbPrompt).where(DbPrompt.id == prompt_id_int).where(DbPrompt.is_active)).scalar_one_or_none() - search_key = prompt_id_int + if prompt_id is not None: + prompt = db.execute(select(DbPrompt).where(DbPrompt.id == prompt_id).where(DbPrompt.is_active)).scalar_one_or_none() + search_key = prompt_id else: # Look up by name (active prompts only) # Note: Team/owner scoping could be added here when user context is available @@ -807,8 +783,8 @@ async def get_prompt( if not prompt: # Check if an inactive prompt exists - if prompt_id_int is not None: - inactive_prompt = db.execute(select(DbPrompt).where(DbPrompt.id == prompt_id_int).where(not_(DbPrompt.is_active))).scalar_one_or_none() + if prompt_id is not None: + inactive_prompt = db.execute(select(DbPrompt).where(DbPrompt.id == prompt_id).where(not_(DbPrompt.is_active))).scalar_one_or_none() else: inactive_prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt_name).where(not_(DbPrompt.is_active))).scalar_one_or_none() @@ -858,6 +834,7 @@ async def get_prompt( span.set_attribute("messages.count", len(result.messages)) success = True + logger.info(f"Retrieved prompt: {prompt.id} successfully") return result except Exception as e: From 8d7ce29e7cef5bc03f08b2d3b9aeae4f7bcf091a Mon Sep 17 00:00:00 2001 From: rakdutta Date: Mon, 1 Dec 2025 12:40:38 +0530 Subject: [PATCH 03/23] flake8 Signed-off-by: rakdutta --- mcpgateway/admin.py | 6 +++--- mcpgateway/db.py | 2 -- mcpgateway/main.py | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 993fe5e61..d1b65ecd0 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -8546,7 +8546,7 @@ async def admin_toggle_resource( logs any errors that might occur during the status toggle operation. Args: - resource_id (int): The ID of the resource whose status to toggle. + resource_id (str): The ID of the resource whose status to toggle. request (Request): FastAPI request containing form data with the 'activate' field. db (Session): Database session dependency. user (str): Authenticated user dependency. @@ -8943,7 +8943,7 @@ async def admin_edit_prompt( """ LOGGER.debug(f"User {get_user_email(user)} is editing prompt {prompt_id}") form = await request.form() - + visibility = str(form.get("visibility", "private")) user_email = get_user_email(user) # Determine personal team for default assignment @@ -9100,7 +9100,7 @@ async def admin_toggle_prompt( logs any errors that might occur during the status toggle operation. Args: - prompt_id (int): The ID of the prompt whose status to toggle. + prompt_id (str): The ID of the prompt whose status to toggle. request (Request): FastAPI request containing form data with the 'activate' field. db (Session): Database session dependency. user (str): Authenticated user dependency. diff --git a/mcpgateway/db.py b/mcpgateway/db.py index 900f45cf0..2bdf06a2b 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -2210,7 +2210,6 @@ class Resource(Base): __tablename__ = "resources" - #id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex) uri: Mapped[str] = mapped_column(String(767), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) @@ -2470,7 +2469,6 @@ class Prompt(Base): __tablename__ = "prompts" - #id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) diff --git a/mcpgateway/main.py b/mcpgateway/main.py index ce558573b..6eb6e0f2d 100644 --- a/mcpgateway/main.py +++ b/mcpgateway/main.py @@ -2758,7 +2758,7 @@ async def list_resource_templates( Returns: ListResourceTemplatesResult: A paginated list of resource templates. """ - logger.debug(f"User {user} requested resource templates") + logger.info(f"User {user} requested resource templates") resource_templates = await resource_service.list_resource_templates(db) # For simplicity, we're not implementing real pagination here return ListResourceTemplatesResult(_meta={}, resource_templates=resource_templates, next_cursor=None) # No pagination for now @@ -2776,7 +2776,7 @@ async def toggle_resource_status( Activate or deactivate a resource by its ID. Args: - resource_id (int): The ID of the resource. + resource_id (str): The ID of the resource. activate (bool): True to activate, False to deactivate. db (Session): Database session. user (str): Authenticated user. From 35d385654ef108a0286b7aac39ec76511e6101fe Mon Sep 17 00:00:00 2001 From: rakdutta Date: Mon, 1 Dec 2025 12:58:07 +0530 Subject: [PATCH 04/23] UUID doctest Signed-off-by: rakdutta --- mcpgateway/admin.py | 14 +++++++------- mcpgateway/services/resource_service.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index d1b65ecd0..31aee7830 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -1713,7 +1713,7 @@ async def admin_list_resources( >>> >>> # Mock resource data >>> mock_resource = ResourceRead( - ... id=1, + ... id="39334ce0ed2644d79ede8913a66930c9", ... uri="test://resource/1", ... name="Test Resource", ... description="A test resource", @@ -1744,7 +1744,7 @@ async def admin_list_resources( >>> >>> # Test listing with inactive resources (if mock includes them) >>> mock_inactive_resource = ResourceRead( - ... id=2, uri="test://resource/2", name="Inactive Resource", + ... id="39334ce0ed2644d79ede8913a66930c9", uri="test://resource/2", name="Inactive Resource", ... description="Another test", mime_type="application/json", size=50, ... created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), ... is_active=False, metrics=ResourceMetrics( @@ -1823,7 +1823,7 @@ async def admin_list_prompts( >>> >>> # Mock prompt data >>> mock_prompt = PromptRead( - ... id=1, + ... id="ca627760127d409080fdefc309147e08", ... name="Test Prompt", ... description="A test prompt", ... template="Hello {{name}}!", @@ -1853,7 +1853,7 @@ async def admin_list_prompts( >>> >>> # Test listing with inactive prompts (if mock includes them) >>> mock_inactive_prompt = PromptRead( - ... id=2, name="Inactive Prompt", description="Another test", template="Bye!", + ... id="39334ce0ed2644d79ede8913a66930c9", name="Inactive Prompt", description="Another test", template="Bye!", ... arguments=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), ... is_active=False, metrics=PromptMetrics( ... total_executions=0, successful_executions=0, failed_executions=0, @@ -8109,7 +8109,7 @@ async def admin_get_resource(resource_id: str, db: Session = Depends(get_db), us >>> mock_db = MagicMock() >>> mock_user = {"email": "test_user", "db": mock_db} >>> resource_uri = "test://resource/get" - >>> resource_id = 1 + >>> resource_id = "ca627760127d409080fdefc309147e08" >>> >>> # Mock resource data >>> mock_resource = ResourceRead( @@ -8142,7 +8142,7 @@ async def admin_get_resource(resource_id: str, db: Session = Depends(get_db), us >>> resource_service.get_resource_by_id = AsyncMock(side_effect=ResourceNotFoundError("Resource not found")) >>> async def test_admin_get_resource_not_found(): ... try: - ... await admin_get_resource(999, mock_db, mock_user) + ... await admin_get_resource("39334ce0ed2644d79ede8913a66930c9", mock_db, mock_user) ... return False ... except HTTPException as e: ... return e.status_code == 404 and "Resource not found" in e.detail @@ -8695,7 +8695,7 @@ async def admin_get_prompt(prompt_id: str, db: Session = Depends(get_db), user=D ... last_execution_time=datetime.now(timezone.utc) ... ) >>> mock_prompt_details = { - ... "id": 1, + ... "id": "ca627760127d409080fdefc309147e08", ... "name": prompt_name, ... "description": "A test prompt", ... "template": "Hello {{name}}!", diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index 264af6583..4d72f694d 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -226,7 +226,7 @@ def _convert_resource_to_read(self, resource: DbResource) -> ResourceRead: >>> m1 = SimpleNamespace(is_success=True, response_time=0.1, timestamp=now) >>> m2 = SimpleNamespace(is_success=False, response_time=0.3, timestamp=now) >>> r = SimpleNamespace( - ... id=1, uri='res://x', name='R', description=None, mime_type='text/plain', size=123, + ... id="ca627760127d409080fdefc309147e08", uri='res://x', name='R', description=None, mime_type='text/plain', size=123, ... created_at=now, updated_at=now, is_active=True, tags=[{"id": "t", "label": "T"}], metrics=[m1, m2] ... ) >>> out = svc._convert_resource_to_read(r) From df4de19697a8bcc7e89986d18663c05787bee5ac Mon Sep 17 00:00:00 2001 From: rakdutta Date: Mon, 1 Dec 2025 14:48:57 +0530 Subject: [PATCH 05/23] UUID pytest fix Signed-off-by: rakdutta --- mcpgateway/db.py | 2 +- mcpgateway/services/resource_service.py | 4 +- tests/integration/test_integration.py | 2 +- .../services/test_export_service.py | 8 ++-- .../services/test_resource_service.py | 38 +++++++++---------- tests/unit/mcpgateway/test_admin.py | 2 +- tests/unit/mcpgateway/test_main.py | 4 +- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/mcpgateway/db.py b/mcpgateway/db.py index 2bdf06a2b..3f6e9f9e7 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -1506,7 +1506,7 @@ class ResourceMetric(Base): __tablename__ = "resource_metrics" id: Mapped[int] = mapped_column(primary_key=True) - resource_id: Mapped[int] = mapped_column(Integer, ForeignKey("resources.id"), nullable=False) + resource_id: Mapped[str] = mapped_column(String(36), ForeignKey("resources.id"), nullable=False) timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) response_time: Mapped[float] = mapped_column(Float, nullable=False) is_success: Mapped[bool] = mapped_column(Boolean, nullable=False) diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index 4d72f694d..b2913d11a 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -1775,7 +1775,7 @@ async def delete_resource(self, db: Session, resource_id: Union[int, str], user_ db.rollback() raise ResourceError(f"Failed to delete resource: {str(e)}") - async def get_resource_by_id(self, db: Session, resource_id: int, include_inactive: bool = False) -> ResourceRead: + async def get_resource_by_id(self, db: Session, resource_id: str, include_inactive: bool = False) -> ResourceRead: """ Get a resource by ID. @@ -1799,7 +1799,7 @@ async def get_resource_by_id(self, db: Session, resource_id: int, include_inacti >>> db.execute.return_value.scalar_one_or_none.return_value = resource >>> service._convert_resource_to_read = MagicMock(return_value='resource_read') >>> import asyncio - >>> asyncio.run(service.get_resource_by_id(db, 999)) + >>> asyncio.run(service.get_resource_by_id(db, "39334ce0ed2644d79ede8913a66930c9")) 'resource_read' """ query = select(DbResource).where(DbResource.id == resource_id) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index b9b3ec299..016d4df30 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -210,7 +210,7 @@ def auth_headers() -> dict[str, str]: ) MOCK_RESOURCE = ResourceRead( - id=1, + id="39334ce0ed2644d79ede8913a66930c9", uri="file:///tmp/hello.txt", name="Hello", description="demo text", diff --git a/tests/unit/mcpgateway/services/test_export_service.py b/tests/unit/mcpgateway/services/test_export_service.py index af5d2d58f..4577525d5 100644 --- a/tests/unit/mcpgateway/services/test_export_service.py +++ b/tests/unit/mcpgateway/services/test_export_service.py @@ -933,7 +933,7 @@ async def test_export_selective_all_entity_types(export_service, mock_db): ) sample_prompt = PromptRead( - id=1, + id="ca627760127d409080fdefc309147e08", name="test_prompt", template="Test template", description="Test prompt", @@ -946,7 +946,7 @@ async def test_export_selective_all_entity_types(export_service, mock_db): ) sample_resource = ResourceRead( - id=1, + id="ca627760127d409080fdefc309147e08", name="test_resource", uri="file:///test.txt", description="Test resource", @@ -1103,7 +1103,7 @@ async def test_export_selected_prompts(export_service, mock_db): # First-Party sample_prompt = PromptRead( - id=1, + id="ca627760127d409080fdefc309147e08", name="test_prompt", template="Test template", description="Test prompt", @@ -1141,7 +1141,7 @@ async def test_export_selected_resources(export_service, mock_db): # First-Party sample_resource = ResourceRead( - id=1, + id="ca627760127d409080fdefc309147e08", name="test_resource", uri="file:///test.txt", description="Test resource", diff --git a/tests/unit/mcpgateway/services/test_resource_service.py b/tests/unit/mcpgateway/services/test_resource_service.py index 11b1a8fea..bb5ccfd2d 100644 --- a/tests/unit/mcpgateway/services/test_resource_service.py +++ b/tests/unit/mcpgateway/services/test_resource_service.py @@ -64,7 +64,7 @@ def mock_resource(): resource = MagicMock() # core attributes - resource.id = 1 + resource.id = "39334ce0ed2644d79ede8913a66930c9" resource.uri = "http://example.com/resource" resource.name = "Test Resource" resource.description = "A test resource" @@ -101,7 +101,7 @@ def mock_resource_template(): resource = MagicMock() # core attributes - resource.id = 1 + resource.id = "39334ce0ed2644d79ede8913a66930c9" resource.uri = "http://example.com/resource/{name}" resource.name = "Test Resource" resource.description = "A test resource" @@ -138,7 +138,7 @@ def mock_inactive_resource(): resource = MagicMock() # core attributes - resource.id = 2 + resource.id = "2" resource.uri = "http://example.com/inactive" resource.name = "Inactive Resource" resource.description = "An inactive resource" @@ -224,7 +224,7 @@ async def test_register_resource_success(self, resource_service, mock_db, sample patch.object(resource_service, "_convert_resource_to_read") as mock_convert, ): mock_convert.return_value = ResourceRead( - id=1, + id="39334ce0ed2644d79ede8913a66930c9", uri=sample_resource_create.uri, name=sample_resource_create.name, description=sample_resource_create.description or "", @@ -340,7 +340,7 @@ async def test_register_resource_binary_content(self, resource_service, mock_db) patch.object(resource_service, "_convert_resource_to_read") as mock_convert, ): mock_convert.return_value = ResourceRead( - id=1, + id="39334ce0ed2644d79ede8913a66930c9", uri=binary_resource.uri, name=binary_resource.name, description=binary_resource.description or "", @@ -546,7 +546,7 @@ async def test_toggle_resource_status_activate(self, resource_service, mock_db, with patch.object(resource_service, "_notify_resource_activated", new_callable=AsyncMock), patch.object(resource_service, "_convert_resource_to_read") as mock_convert: mock_convert.return_value = ResourceRead( - id=2, + id="39334ce0ed2644d79ede8913a66930c9", uri=mock_inactive_resource.uri, name=mock_inactive_resource.name, description=mock_inactive_resource.description or "", @@ -580,7 +580,7 @@ async def test_toggle_resource_status_deactivate(self, resource_service, mock_db with patch.object(resource_service, "_notify_resource_deactivated", new_callable=AsyncMock), patch.object(resource_service, "_convert_resource_to_read") as mock_convert: mock_convert.return_value = ResourceRead( - id=1, + id="39334ce0ed2644d79ede8913a66930c9", uri=mock_resource.uri, name=mock_resource.name, description=mock_resource.description, @@ -626,7 +626,7 @@ async def test_toggle_resource_status_no_change(self, resource_service, mock_db, with patch.object(resource_service, "_convert_resource_to_read") as mock_convert: mock_convert.return_value = ResourceRead( - id=1, + id="39334ce0ed2644d79ede8913a66930c9", uri=mock_resource.uri, name=mock_resource.name, description=mock_resource.description, @@ -667,7 +667,7 @@ async def test_update_resource_success(self, resource_service, mock_db, mock_res with patch.object(resource_service, "_notify_resource_updated", new_callable=AsyncMock), patch.object(resource_service, "_convert_resource_to_read") as mock_convert: mock_convert.return_value = ResourceRead( - id=1, + id="39334ce0ed2644d79ede8913a66930c9", uri=mock_resource.uri, name="Updated Name", description="Updated description", @@ -740,7 +740,7 @@ async def test_update_resource_binary_content(self, resource_service, mock_db, m with patch.object(resource_service, "_notify_resource_updated", new_callable=AsyncMock), patch.object(resource_service, "_convert_resource_to_read") as mock_convert: mock_convert.return_value = ResourceRead( - id=1, + id="", uri=mock_resource.uri, name=mock_resource.name, description=mock_resource.description, @@ -803,7 +803,7 @@ async def test_get_resource_by_id_inactive(self, resource_service, mock_db, mock mock_db.execute.side_effect = [mock_scalar1, mock_scalar2] with pytest.raises(ResourceNotFoundError) as exc_info: - await resource_service.get_resource_by_id(mock_db, "1") + await resource_service.get_resource_by_id(mock_db, "39334ce0ed2644d79ede8913a66930c9") assert "exists but is inactive" in str(exc_info.value) @@ -814,7 +814,7 @@ async def test_get_resource_by_uri_include_inactive(self, resource_service, mock mock_scalar.scalar_one_or_none.return_value = mock_inactive_resource mock_db.execute.return_value = mock_scalar - result = await resource_service.get_resource_by_id(mock_db, "1", include_inactive=True) + result = await resource_service.get_resource_by_id(mock_db, "39334ce0ed2644d79ede8913a66930c9", include_inactive=True) assert isinstance(result, ResourceRead) assert result.uri == mock_inactive_resource.uri @@ -1089,7 +1089,7 @@ async def test_read_template_resource_not_found(self): # Correct template object (NOT ResourceContent) template_obj = ResourceTemplate( - id=1, + id="1", uriTemplate="file://search/{query}", # alias is used in constructor name="search_template", description="Template for performing a file search", @@ -1128,7 +1128,7 @@ async def test_read_template_resource_error(self): # Create a valid ResourceTemplate object template_obj = ResourceTemplate( - id=1, + id="1", uriTemplate="test://template/{id}", # alias for uri_template name="template", description="Test template", @@ -1173,7 +1173,7 @@ async def test_read_template_resource_binary_not_supported(self): # Binary MIME template template = MagicMock() - template.id = 1 + template.id = "39334ce0ed2644d79ede8913a66930c9" template.uri_template = "test://template/{id}" template.name = "binary_template" template.mime_type = "application/octet-stream" @@ -1374,7 +1374,7 @@ async def test_notify_resource_deleted(self, resource_service): """Test resource deleted notification.""" resource_service._event_service.publish_event = AsyncMock() - resource_info = {"id": 1, "uri": "test://resource", "name": "Test"} + resource_info = {"id": "39334ce0ed2644d79ede8913a66930c9", "uri": "test://resource", "name": "Test"} await resource_service._notify_resource_deleted(resource_info) resource_service._event_service.publish_event.assert_called_once() @@ -1439,7 +1439,7 @@ async def test_toggle_resource_status_error(self, resource_service, mock_db, moc mock_db.commit.side_effect = Exception("Database error") with pytest.raises(ResourceError): - await resource_service.toggle_resource_status(mock_db, 1, activate=False) + await resource_service.toggle_resource_status(mock_db, "39334ce0ed2644d79ede8913a66930c9", activate=False) mock_db.rollback.assert_called_once() @@ -1615,7 +1615,7 @@ async def test_get_top_resources(self, resource_service, mock_db): """Test getting top performing resources.""" # Mock query results mock_result1 = MagicMock() - mock_result1.id = 1 + mock_result1.id = "39334ce0ed2644d79ede8913a66930c9" mock_result1.name = "resource1" mock_result1.execution_count = 10 mock_result1.avg_response_time = 1.5 @@ -1623,7 +1623,7 @@ async def test_get_top_resources(self, resource_service, mock_db): mock_result1.last_execution = "2025-01-10T12:00:00" mock_result2 = MagicMock() - mock_result2.id = 2 + mock_result2.id = "2" mock_result2.name = "resource2" mock_result2.execution_count = 7 mock_result2.avg_response_time = 2.3 diff --git a/tests/unit/mcpgateway/test_admin.py b/tests/unit/mcpgateway/test_admin.py index 7f6e7ae2b..7658feb0c 100644 --- a/tests/unit/mcpgateway/test_admin.py +++ b/tests/unit/mcpgateway/test_admin.py @@ -913,7 +913,7 @@ async def test_admin_list_prompts_with_complex_arguments(self, mock_list_prompts async def test_admin_get_prompt_with_detailed_metrics(self, mock_get_prompt_details, mock_db): """Test getting prompt with detailed metrics.""" mock_get_prompt_details.return_value = { - "id": 1, + "id": "ca627760127d409080fdefc309147e08", "name": "test-prompt", "template": "Test {{var}}", "description": "Test prompt", diff --git a/tests/unit/mcpgateway/test_main.py b/tests/unit/mcpgateway/test_main.py index 0e7e77ea5..e551327f1 100644 --- a/tests/unit/mcpgateway/test_main.py +++ b/tests/unit/mcpgateway/test_main.py @@ -117,7 +117,7 @@ def camel_to_snake_tool(d: dict) -> dict: MOCK_RESOURCE_READ = { - "id": 1, + "id": "39334ce0ed2644d79ede8913a66930c9", "uri": "test/resource", "name": "Test Resource", "description": "A test resource", @@ -130,7 +130,7 @@ def camel_to_snake_tool(d: dict) -> dict: } MOCK_PROMPT_READ = { - "id": 1, + "id": "ca627760127d409080fdefc309147e08", "name": "test_prompt", "description": "A test prompt", "template": "Hello {name}", From aa7a45263c9174ebee1d8d8a464ceec6738a9967 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Mon, 1 Dec 2025 17:09:39 +0530 Subject: [PATCH 06/23] uuid alembic Signed-off-by: rakdutta --- ...6f_uuid_change_for_prompt_and_resources.py | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py diff --git a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py new file mode 100644 index 000000000..230647ec3 --- /dev/null +++ b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py @@ -0,0 +1,263 @@ +"""UUID Change for Prompt and Resources + +Revision ID: 356a2d4eed6f +Revises: z1a2b3c4d5e6 +Create Date: 2025-12-01 14:52:01.957105 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import uuid +import logging + +from sqlalchemy import text + +# logger for migration messages +logger = logging.getLogger(__name__) +# Exceptions we expect when dropping non-existent constraints/columns +EXPECTED_DB_EXCEPTIONS = ( + sa.exc.ProgrammingError, + sa.exc.OperationalError, + getattr(sa.exc, 'NoSuchTableError', Exception), + getattr(sa.exc, 'NoSuchColumnError', Exception), + NotImplementedError, +) + + + +# revision identifiers, used by Alembic. +revision: str = '356a2d4eed6f' +down_revision: Union[str, Sequence[str], None] = 'z1a2b3c4d5e6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # Add new UUID columns (temporary names) if they don't already exist + conn = op.get_bind() + inspector = sa.inspect(conn) + + def _column_exists(table_name: str, column_name: str) -> bool: + try: + return column_name in [c['name'] for c in inspector.get_columns(table_name)] + except Exception: + return False + + if not _column_exists('resources', 'new_id'): + op.add_column('resources', sa.Column('new_id', sa.String(length=36), nullable=True)) + else: + logger.debug("Column 'new_id' already exists on 'resources', skipping add_column") + + if not _column_exists('prompts', 'new_id'): + op.add_column('prompts', sa.Column('new_id', sa.String(length=36), nullable=True)) + else: + logger.debug("Column 'new_id' already exists on 'prompts', skipping add_column") + + if not _column_exists('resource_metrics', 'new_resource_id'): + op.add_column('resource_metrics', sa.Column('new_resource_id', sa.String(length=36), nullable=True)) + else: + logger.debug("Column 'new_resource_id' already exists on 'resource_metrics', skipping add_column") + + # Populate resources -> new_id and update resource_metrics to preserve relations + resources = conn.execute(sa.text('SELECT id FROM resources')).fetchall() + for (old_id,) in resources: + new_uuid = uuid.uuid4().hex + conn.execute(sa.text('UPDATE resources SET new_id = :new WHERE id = :old'), {'new': new_uuid, 'old': old_id}) + conn.execute(sa.text('UPDATE resource_metrics SET new_resource_id = :new WHERE resource_id = :old'), {'new': new_uuid, 'old': old_id}) + + # Populate prompts -> new_id + prompts = conn.execute(sa.text('SELECT id FROM prompts')).fetchall() + for (old_id,) in prompts: + new_uuid = uuid.uuid4().hex + conn.execute(sa.text('UPDATE prompts SET new_id = :new WHERE id = :old'), {'new': new_uuid, 'old': old_id}) + + # Make the new id columns non-nullable and perform renames in a SQLite-safe way + # Use batch_alter_table so Alembic will recreate the table for SQLite. + with op.batch_alter_table('resources') as batch_op: + batch_op.alter_column('new_id', existing_type=sa.String(length=36), nullable=False) + + with op.batch_alter_table('prompts') as batch_op: + batch_op.alter_column('new_id', existing_type=sa.String(length=36), nullable=False) + + with op.batch_alter_table('resource_metrics') as batch_op: + batch_op.alter_column('new_resource_id', existing_type=sa.String(length=36), nullable=False) + + # Try to drop existing primary key constraints (common default names are '_pkey') + try: + op.drop_constraint('resources_pkey', 'resources', type_='primary') + except EXPECTED_DB_EXCEPTIONS as e: + logger.debug("Ignoring missing/failed drop_constraint resources_pkey: %s", e) + try: + op.drop_constraint('prompts_pkey', 'prompts', type_='primary') + except EXPECTED_DB_EXCEPTIONS as e: + logger.debug("Ignoring missing/failed drop_constraint prompts_pkey: %s", e) + + # Rename old integer id columns to keep them temporarily and rename new_id -> id + # Use batch_alter_table so renames are portable across SQLite and other DBs. + with op.batch_alter_table('resources') as batch_op: + batch_op.alter_column('id', new_column_name='old_id', existing_type=sa.Integer()) + batch_op.alter_column('new_id', new_column_name='id', existing_type=sa.String(length=36)) + # Create primary key on the new UUID id column in a batch-safe way + batch_op.create_primary_key('resources_pkey', ['id']) + # Drop the old integer PK column as part of the same batch table rebuild + try: + batch_op.drop_column('old_id') + except Exception: + logger.debug("Failed to drop 'old_id' in resources batch; will attempt later") + + with op.batch_alter_table('prompts') as batch_op: + batch_op.alter_column('id', new_column_name='old_id', existing_type=sa.Integer()) + batch_op.alter_column('new_id', new_column_name='id', existing_type=sa.String(length=36)) + # Create primary key on the new UUID id column in a batch-safe way + batch_op.create_primary_key('prompts_pkey', ['id']) + # Drop the old integer PK column as part of the same batch table rebuild + try: + batch_op.drop_column('old_id') + except Exception: + logger.debug("Failed to drop 'old_id' in prompts batch; will attempt later") + + # Primary keys created inside batch_alter_table blocks for SQLite compatibility + + # Replace resource_metrics.resource_id with the UUID-based column and recreate FK + try: + op.drop_constraint('resource_metrics_resource_id_fkey', 'resource_metrics', type_='foreignkey') + except EXPECTED_DB_EXCEPTIONS as e: + logger.debug("Ignoring missing/failed drop_constraint resource_metrics_resource_id_fkey: %s", e) + + with op.batch_alter_table('resource_metrics') as batch_op: + batch_op.alter_column('resource_id', new_column_name='old_resource_id', existing_type=sa.Integer()) + batch_op.alter_column('new_resource_id', new_column_name='resource_id', existing_type=sa.String(length=36)) + # Create FK in batch mode so SQLite will rebuild the table with constraint + batch_op.create_foreign_key('resource_metrics_resource_id_fkey', 'resources', ['resource_id'], ['id']) + # Drop the old integer FK column as part of the same batch table rebuild + try: + batch_op.drop_column('old_resource_id') + except Exception: + logger.debug("Failed to drop 'old_resource_id' in resource_metrics batch; will attempt later") + + # Foreign key created inside batch_alter_table block for SQLite compatibility + + # Drop the old integer id columns now that UUIDs are in place + # Use batch_alter_table so SQLite can rebuild tables when dropping columns + if _column_exists('resources', 'old_id'): + with op.batch_alter_table('resources') as batch_op: + batch_op.drop_column('old_id') + else: + logger.debug("Column 'old_id' not present on 'resources', skipping drop_column") + + if _column_exists('prompts', 'old_id'): + with op.batch_alter_table('prompts') as batch_op: + batch_op.drop_column('old_id') + else: + logger.debug("Column 'old_id' not present on 'prompts', skipping drop_column") + + if _column_exists('resource_metrics', 'old_resource_id'): + with op.batch_alter_table('resource_metrics') as batch_op: + batch_op.drop_column('old_resource_id') + else: + logger.debug("Column 'old_resource_id' not present on 'resource_metrics', skipping drop_column") + + +def downgrade() -> None: + """Downgrade schema.""" + # NOTE: The original integer primary key values were dropped during the upgrade. + # This downgrade cannot restore the original integer values; instead it + # recreates integer `id` columns populated with new sequential integers and + # remaps foreign keys accordingly so the schema returns to integer-based PKs. + + conn = op.get_bind() + + # 1) Add integer columns to resources and prompts + op.add_column('resources', sa.Column('old_id', sa.Integer(), nullable=True)) + op.add_column('prompts', sa.Column('old_id', sa.Integer(), nullable=True)) + + # Populate sequential integers for resources + resources = conn.execute(sa.text('SELECT id FROM resources')).fetchall() + for idx, (uuid_val,) in enumerate(resources, start=1): + conn.execute(sa.text('UPDATE resources SET old_id = :num WHERE id = :uuid'), {'num': idx, 'uuid': uuid_val}) + + # Populate sequential integers for prompts + prompts = conn.execute(sa.text('SELECT id FROM prompts')).fetchall() + for idx, (uuid_val,) in enumerate(prompts, start=1): + conn.execute(sa.text('UPDATE prompts SET old_id = :num WHERE id = :uuid'), {'num': idx, 'uuid': uuid_val}) + + # 2) Drop primary key constraints on UUID id columns + try: + op.drop_constraint('resources_pkey', 'resources', type_='primary') + except EXPECTED_DB_EXCEPTIONS as e: + logger.debug("Ignoring missing/failed drop_constraint resources_pkey (downgrade): %s", e) + try: + op.drop_constraint('prompts_pkey', 'prompts', type_='primary') + except EXPECTED_DB_EXCEPTIONS as e: + logger.debug("Ignoring missing/failed drop_constraint prompts_pkey (downgrade): %s", e) + + # 3) Rename UUID id columns to keep them (uuid_id) and rename old_id -> id + with op.batch_alter_table('resources') as batch_op: + batch_op.alter_column('id', new_column_name='uuid_id', existing_type=sa.String(length=36)) + batch_op.alter_column('old_id', new_column_name='id', existing_type=sa.Integer()) + # Recreate primary key on integer id column in batch mode for SQLite + batch_op.create_primary_key('resources_pkey', ['id']) + + with op.batch_alter_table('prompts') as batch_op: + batch_op.alter_column('id', new_column_name='uuid_id', existing_type=sa.String(length=36)) + batch_op.alter_column('old_id', new_column_name='id', existing_type=sa.Integer()) + # Recreate primary key on integer id column in batch mode for SQLite + batch_op.create_primary_key('prompts_pkey', ['id']) + + # Primary keys recreated inside batch_alter_table blocks for SQLite compatibility + + # 5) For resource_metrics, add integer column and populate using mapping from resources + op.add_column('resource_metrics', sa.Column('old_resource_id', sa.Integer(), nullable=True)) + # Fetch mapping of resource uuid -> new integer id + mapping = conn.execute(sa.text('SELECT uuid_id, id FROM resources')).fetchall() + for uuid_val, int_id in mapping: + conn.execute(sa.text('UPDATE resource_metrics SET old_resource_id = :num WHERE resource_id = :uuid'), {'num': int_id, 'uuid': uuid_val}) + + # 6) Replace FK: drop existing FK, swap columns, recreate FK to integer PK + try: + op.drop_constraint('resource_metrics_resource_id_fkey', 'resource_metrics', type_='foreignkey') + except EXPECTED_DB_EXCEPTIONS as e: + logger.debug("Ignoring missing/failed drop_constraint resource_metrics_resource_id_fkey (downgrade): %s", e) + + # Rename current uuid resource_id to uuid_resource_id, and old_resource_id -> resource_id + with op.batch_alter_table('resource_metrics') as batch_op: + batch_op.alter_column('resource_id', new_column_name='uuid_resource_id', existing_type=sa.String(length=36)) + batch_op.alter_column('old_resource_id', new_column_name='resource_id', existing_type=sa.Integer()) + # Recreate FK to resources.id (integer PK) in batch mode for SQLite + batch_op.create_foreign_key('resource_metrics_resource_id_fkey', 'resources', ['resource_id'], ['id']) + + # Foreign key recreated inside batch_alter_table block for SQLite compatibility + + # 7) Drop UUID columns from resources, prompts, and resource_metrics + inspector = sa.inspect(conn) + + def _col_exists_down(table_name: str, column_name: str) -> bool: + try: + return column_name in [c['name'] for c in inspector.get_columns(table_name)] + except Exception: + return False + + if _col_exists_down('resources', 'uuid_id'): + with op.batch_alter_table('resources') as batch_op: + batch_op.drop_column('uuid_id') + else: + logger.debug("Column 'uuid_id' not present on 'resources', skipping drop_column") + + if _col_exists_down('prompts', 'uuid_id'): + with op.batch_alter_table('prompts') as batch_op: + batch_op.drop_column('uuid_id') + else: + logger.debug("Column 'uuid_id' not present on 'prompts', skipping drop_column") + + # resource_metrics uuid column was renamed to uuid_resource_id earlier; drop it if present + if _col_exists_down('resource_metrics', 'uuid_resource_id'): + with op.batch_alter_table('resource_metrics') as batch_op: + batch_op.drop_column('uuid_resource_id') + else: + logger.debug("Column 'uuid_resource_id' not present on 'resource_metrics', skipping drop_column") + + # NOTE: This downgrade generates new integer IDs and therefore will not + # match the original IDs that existed prior to the upgrade. Use with care. From 7a83406d47c57333f97270f7f35538a26ca3ad77 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 10:47:40 +0530 Subject: [PATCH 07/23] alembic Signed-off-by: rakdutta --- ...6f_uuid_change_for_prompt_and_resources.py | 406 ++++++++---------- mcpgateway/db.py | 6 +- 2 files changed, 178 insertions(+), 234 deletions(-) diff --git a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py index 230647ec3..e86dfbe1b 100644 --- a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py +++ b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py @@ -14,250 +14,194 @@ from sqlalchemy import text -# logger for migration messages -logger = logging.getLogger(__name__) -# Exceptions we expect when dropping non-existent constraints/columns -EXPECTED_DB_EXCEPTIONS = ( - sa.exc.ProgrammingError, - sa.exc.OperationalError, - getattr(sa.exc, 'NoSuchTableError', Exception), - getattr(sa.exc, 'NoSuchColumnError', Exception), - NotImplementedError, -) - - # revision identifiers, used by Alembic. revision: str = '356a2d4eed6f' -down_revision: Union[str, Sequence[str], None] = 'z1a2b3c4d5e6' +down_revision: Union[str, Sequence[str], None] = '9e028ecf59c4' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """Upgrade schema.""" - # Add new UUID columns (temporary names) if they don't already exist conn = op.get_bind() - inspector = sa.inspect(conn) - - def _column_exists(table_name: str, column_name: str) -> bool: - try: - return column_name in [c['name'] for c in inspector.get_columns(table_name)] - except Exception: - return False - - if not _column_exists('resources', 'new_id'): - op.add_column('resources', sa.Column('new_id', sa.String(length=36), nullable=True)) - else: - logger.debug("Column 'new_id' already exists on 'resources', skipping add_column") - - if not _column_exists('prompts', 'new_id'): - op.add_column('prompts', sa.Column('new_id', sa.String(length=36), nullable=True)) - else: - logger.debug("Column 'new_id' already exists on 'prompts', skipping add_column") - - if not _column_exists('resource_metrics', 'new_resource_id'): - op.add_column('resource_metrics', sa.Column('new_resource_id', sa.String(length=36), nullable=True)) - else: - logger.debug("Column 'new_resource_id' already exists on 'resource_metrics', skipping add_column") - - # Populate resources -> new_id and update resource_metrics to preserve relations - resources = conn.execute(sa.text('SELECT id FROM resources')).fetchall() - for (old_id,) in resources: - new_uuid = uuid.uuid4().hex - conn.execute(sa.text('UPDATE resources SET new_id = :new WHERE id = :old'), {'new': new_uuid, 'old': old_id}) - conn.execute(sa.text('UPDATE resource_metrics SET new_resource_id = :new WHERE resource_id = :old'), {'new': new_uuid, 'old': old_id}) - - # Populate prompts -> new_id - prompts = conn.execute(sa.text('SELECT id FROM prompts')).fetchall() - for (old_id,) in prompts: - new_uuid = uuid.uuid4().hex - conn.execute(sa.text('UPDATE prompts SET new_id = :new WHERE id = :old'), {'new': new_uuid, 'old': old_id}) - - # Make the new id columns non-nullable and perform renames in a SQLite-safe way - # Use batch_alter_table so Alembic will recreate the table for SQLite. - with op.batch_alter_table('resources') as batch_op: - batch_op.alter_column('new_id', existing_type=sa.String(length=36), nullable=False) - - with op.batch_alter_table('prompts') as batch_op: - batch_op.alter_column('new_id', existing_type=sa.String(length=36), nullable=False) - - with op.batch_alter_table('resource_metrics') as batch_op: - batch_op.alter_column('new_resource_id', existing_type=sa.String(length=36), nullable=False) - - # Try to drop existing primary key constraints (common default names are '
_pkey') - try: - op.drop_constraint('resources_pkey', 'resources', type_='primary') - except EXPECTED_DB_EXCEPTIONS as e: - logger.debug("Ignoring missing/failed drop_constraint resources_pkey: %s", e) - try: - op.drop_constraint('prompts_pkey', 'prompts', type_='primary') - except EXPECTED_DB_EXCEPTIONS as e: - logger.debug("Ignoring missing/failed drop_constraint prompts_pkey: %s", e) - - # Rename old integer id columns to keep them temporarily and rename new_id -> id - # Use batch_alter_table so renames are portable across SQLite and other DBs. - with op.batch_alter_table('resources') as batch_op: - batch_op.alter_column('id', new_column_name='old_id', existing_type=sa.Integer()) - batch_op.alter_column('new_id', new_column_name='id', existing_type=sa.String(length=36)) - # Create primary key on the new UUID id column in a batch-safe way - batch_op.create_primary_key('resources_pkey', ['id']) - # Drop the old integer PK column as part of the same batch table rebuild - try: - batch_op.drop_column('old_id') - except Exception: - logger.debug("Failed to drop 'old_id' in resources batch; will attempt later") - - with op.batch_alter_table('prompts') as batch_op: - batch_op.alter_column('id', new_column_name='old_id', existing_type=sa.Integer()) - batch_op.alter_column('new_id', new_column_name='id', existing_type=sa.String(length=36)) - # Create primary key on the new UUID id column in a batch-safe way - batch_op.create_primary_key('prompts_pkey', ['id']) - # Drop the old integer PK column as part of the same batch table rebuild - try: - batch_op.drop_column('old_id') - except Exception: - logger.debug("Failed to drop 'old_id' in prompts batch; will attempt later") - - # Primary keys created inside batch_alter_table blocks for SQLite compatibility - - # Replace resource_metrics.resource_id with the UUID-based column and recreate FK - try: - op.drop_constraint('resource_metrics_resource_id_fkey', 'resource_metrics', type_='foreignkey') - except EXPECTED_DB_EXCEPTIONS as e: - logger.debug("Ignoring missing/failed drop_constraint resource_metrics_resource_id_fkey: %s", e) - - with op.batch_alter_table('resource_metrics') as batch_op: - batch_op.alter_column('resource_id', new_column_name='old_resource_id', existing_type=sa.Integer()) - batch_op.alter_column('new_resource_id', new_column_name='resource_id', existing_type=sa.String(length=36)) - # Create FK in batch mode so SQLite will rebuild the table with constraint - batch_op.create_foreign_key('resource_metrics_resource_id_fkey', 'resources', ['resource_id'], ['id']) - # Drop the old integer FK column as part of the same batch table rebuild - try: - batch_op.drop_column('old_resource_id') - except Exception: - logger.debug("Failed to drop 'old_resource_id' in resource_metrics batch; will attempt later") - - # Foreign key created inside batch_alter_table block for SQLite compatibility - - # Drop the old integer id columns now that UUIDs are in place - # Use batch_alter_table so SQLite can rebuild tables when dropping columns - if _column_exists('resources', 'old_id'): - with op.batch_alter_table('resources') as batch_op: - batch_op.drop_column('old_id') - else: - logger.debug("Column 'old_id' not present on 'resources', skipping drop_column") - - if _column_exists('prompts', 'old_id'): - with op.batch_alter_table('prompts') as batch_op: - batch_op.drop_column('old_id') - else: - logger.debug("Column 'old_id' not present on 'prompts', skipping drop_column") - - if _column_exists('resource_metrics', 'old_resource_id'): - with op.batch_alter_table('resource_metrics') as batch_op: - batch_op.drop_column('old_resource_id') - else: - logger.debug("Column 'old_resource_id' not present on 'resource_metrics', skipping drop_column") + # 1) Add temporary id_new column to prompts and populate with uuid.hex + op.add_column("prompts", sa.Column("id_new", sa.String(36), nullable=True)) + + rows = conn.execute(text("SELECT id FROM prompts")).fetchall() + for (old_id,) in rows: + new_id = uuid.uuid4().hex + conn.execute(text("UPDATE prompts SET id_new = :new WHERE id = :old"), {"new": new_id, "old": old_id}) + + # 2) Create new prompts table (temporary) with varchar(36) id + op.create_table( + "prompts_tmp", + sa.Column("id", sa.String(36), primary_key=True, nullable=False), + sa.Column("name", sa.String(255), nullable=False), + sa.Column("description", sa.Text, nullable=True), + sa.Column("template", sa.Text, nullable=True), + sa.Column("argument_schema", sa.JSON, nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("is_active", sa.Boolean, nullable=True), + sa.Column("tags", sa.JSON, nullable=False), + sa.Column("created_by", sa.String(255), nullable=True), + sa.Column("created_from_ip", sa.String(45), nullable=True), + sa.Column("created_via", sa.String(100), nullable=True), + sa.Column("created_user_agent", sa.Text, nullable=True), + sa.Column("modified_by", sa.String(255), nullable=True), + sa.Column("modified_from_ip", sa.String(45), nullable=True), + sa.Column("modified_via", sa.String(100), nullable=True), + sa.Column("modified_user_agent", sa.Text, nullable=True), + sa.Column("import_batch_id", sa.String(36), nullable=True), + sa.Column("federation_source", sa.String(255), nullable=True), + sa.Column("version", sa.Integer, nullable=False, server_default="1"), + sa.Column("gateway_id", sa.String(36), nullable=True), + sa.Column("team_id", sa.String(36), nullable=True), + sa.Column("owner_email", sa.String(255), nullable=True), + sa.Column("visibility", sa.String(20), nullable=False, server_default="public"), + sa.UniqueConstraint("team_id", "owner_email", "name", name="uq_team_owner_name_prompt"), + sa.PrimaryKeyConstraint("id", name="pk_prompts"), + ) + + # 3) Copy data from prompts into prompts_tmp using id_new as id + copy_cols = ( + "id, name, description, template, argument_schema, created_at, updated_at, is_active, tags," + " created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip," + " modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility" + ) + conn.execute(text(f"INSERT INTO prompts_tmp ({copy_cols}) SELECT id_new, name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM prompts")) + + # 4) Create new prompt_metrics table with prompt_id varchar(36) + op.create_table( + "prompt_metrics_tmp", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("prompt_id", sa.String(36), nullable=False), + sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), + sa.Column("response_time", sa.Float, nullable=False), + sa.Column("is_success", sa.Boolean, nullable=False), + sa.Column("error_message", sa.Text, nullable=True), + sa.ForeignKeyConstraint(["prompt_id"], ["prompts_tmp.id"], name="fk_prompt_metrics_prompt_id"), + sa.PrimaryKeyConstraint("id", name="pk_prompt_metrics"), + ) + + # 5) Copy prompt_metrics mapping old integer prompt_id -> new uuid via join + conn.execute(text("INSERT INTO prompt_metrics_tmp (id, prompt_id, timestamp, response_time, is_success, error_message) SELECT pm.id, p.id_new, pm.timestamp, pm.response_time, pm.is_success, pm.error_message FROM prompt_metrics pm JOIN prompts p ON pm.prompt_id = p.id")) + + # 6) Create new server_prompt_association table with prompt_id varchar(36) + op.create_table( + "server_prompt_association_tmp", + sa.Column("server_id", sa.String(36), nullable=False), + sa.Column("prompt_id", sa.String(36), nullable=False), + sa.PrimaryKeyConstraint("server_id", "prompt_id", name="pk_server_prompt_assoc"), + sa.ForeignKeyConstraint(["server_id"], ["servers.id"], name="fk_server_prompt_server_id"), + sa.ForeignKeyConstraint(["prompt_id"], ["prompts_tmp.id"], name="fk_server_prompt_prompt_id"), + ) + + conn.execute(text("INSERT INTO server_prompt_association_tmp (server_id, prompt_id) SELECT spa.server_id, p.id_new FROM server_prompt_association spa JOIN prompts p ON spa.prompt_id = p.id")) + + # 7) Drop old tables and rename tmp tables into place + op.drop_table("prompt_metrics") + op.drop_table("server_prompt_association") + op.drop_table("prompts") + + op.rename_table("prompts_tmp", "prompts") + op.rename_table("prompt_metrics_tmp", "prompt_metrics") + op.rename_table("server_prompt_association_tmp", "server_prompt_association") + def downgrade() -> None: """Downgrade schema.""" - # NOTE: The original integer primary key values were dropped during the upgrade. - # This downgrade cannot restore the original integer values; instead it - # recreates integer `id` columns populated with new sequential integers and - # remaps foreign keys accordingly so the schema returns to integer-based PKs. - conn = op.get_bind() - # 1) Add integer columns to resources and prompts - op.add_column('resources', sa.Column('old_id', sa.Integer(), nullable=True)) - op.add_column('prompts', sa.Column('old_id', sa.Integer(), nullable=True)) - - # Populate sequential integers for resources - resources = conn.execute(sa.text('SELECT id FROM resources')).fetchall() - for idx, (uuid_val,) in enumerate(resources, start=1): - conn.execute(sa.text('UPDATE resources SET old_id = :num WHERE id = :uuid'), {'num': idx, 'uuid': uuid_val}) - - # Populate sequential integers for prompts - prompts = conn.execute(sa.text('SELECT id FROM prompts')).fetchall() - for idx, (uuid_val,) in enumerate(prompts, start=1): - conn.execute(sa.text('UPDATE prompts SET old_id = :num WHERE id = :uuid'), {'num': idx, 'uuid': uuid_val}) - - # 2) Drop primary key constraints on UUID id columns - try: - op.drop_constraint('resources_pkey', 'resources', type_='primary') - except EXPECTED_DB_EXCEPTIONS as e: - logger.debug("Ignoring missing/failed drop_constraint resources_pkey (downgrade): %s", e) - try: - op.drop_constraint('prompts_pkey', 'prompts', type_='primary') - except EXPECTED_DB_EXCEPTIONS as e: - logger.debug("Ignoring missing/failed drop_constraint prompts_pkey (downgrade): %s", e) - - # 3) Rename UUID id columns to keep them (uuid_id) and rename old_id -> id - with op.batch_alter_table('resources') as batch_op: - batch_op.alter_column('id', new_column_name='uuid_id', existing_type=sa.String(length=36)) - batch_op.alter_column('old_id', new_column_name='id', existing_type=sa.Integer()) - # Recreate primary key on integer id column in batch mode for SQLite - batch_op.create_primary_key('resources_pkey', ['id']) - - with op.batch_alter_table('prompts') as batch_op: - batch_op.alter_column('id', new_column_name='uuid_id', existing_type=sa.String(length=36)) - batch_op.alter_column('old_id', new_column_name='id', existing_type=sa.Integer()) - # Recreate primary key on integer id column in batch mode for SQLite - batch_op.create_primary_key('prompts_pkey', ['id']) - - # Primary keys recreated inside batch_alter_table blocks for SQLite compatibility - - # 5) For resource_metrics, add integer column and populate using mapping from resources - op.add_column('resource_metrics', sa.Column('old_resource_id', sa.Integer(), nullable=True)) - # Fetch mapping of resource uuid -> new integer id - mapping = conn.execute(sa.text('SELECT uuid_id, id FROM resources')).fetchall() - for uuid_val, int_id in mapping: - conn.execute(sa.text('UPDATE resource_metrics SET old_resource_id = :num WHERE resource_id = :uuid'), {'num': int_id, 'uuid': uuid_val}) - - # 6) Replace FK: drop existing FK, swap columns, recreate FK to integer PK - try: - op.drop_constraint('resource_metrics_resource_id_fkey', 'resource_metrics', type_='foreignkey') - except EXPECTED_DB_EXCEPTIONS as e: - logger.debug("Ignoring missing/failed drop_constraint resource_metrics_resource_id_fkey (downgrade): %s", e) - - # Rename current uuid resource_id to uuid_resource_id, and old_resource_id -> resource_id - with op.batch_alter_table('resource_metrics') as batch_op: - batch_op.alter_column('resource_id', new_column_name='uuid_resource_id', existing_type=sa.String(length=36)) - batch_op.alter_column('old_resource_id', new_column_name='resource_id', existing_type=sa.Integer()) - # Recreate FK to resources.id (integer PK) in batch mode for SQLite - batch_op.create_foreign_key('resource_metrics_resource_id_fkey', 'resources', ['resource_id'], ['id']) - - # Foreign key recreated inside batch_alter_table block for SQLite compatibility - - # 7) Drop UUID columns from resources, prompts, and resource_metrics - inspector = sa.inspect(conn) - - def _col_exists_down(table_name: str, column_name: str) -> bool: - try: - return column_name in [c['name'] for c in inspector.get_columns(table_name)] - except Exception: - return False - - if _col_exists_down('resources', 'uuid_id'): - with op.batch_alter_table('resources') as batch_op: - batch_op.drop_column('uuid_id') - else: - logger.debug("Column 'uuid_id' not present on 'resources', skipping drop_column") - - if _col_exists_down('prompts', 'uuid_id'): - with op.batch_alter_table('prompts') as batch_op: - batch_op.drop_column('uuid_id') - else: - logger.debug("Column 'uuid_id' not present on 'prompts', skipping drop_column") - - # resource_metrics uuid column was renamed to uuid_resource_id earlier; drop it if present - if _col_exists_down('resource_metrics', 'uuid_resource_id'): - with op.batch_alter_table('resource_metrics') as batch_op: - batch_op.drop_column('uuid_resource_id') - else: - logger.debug("Column 'uuid_resource_id' not present on 'resource_metrics', skipping drop_column") - - # NOTE: This downgrade generates new integer IDs and therefore will not - # match the original IDs that existed prior to the upgrade. Use with care. + # Best-effort: rebuild integer prompt ids and remap dependent FK columns. + # 1) Create old-style prompts table with integer id (autoincrement) + op.create_table( + "prompts_old", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True, nullable=False), + sa.Column("name", sa.String(255), nullable=False), + sa.Column("description", sa.Text, nullable=True), + sa.Column("template", sa.Text, nullable=True), + sa.Column("argument_schema", sa.JSON, nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("is_active", sa.Boolean, nullable=True), + sa.Column("tags", sa.JSON, nullable=False), + sa.Column("created_by", sa.String(255), nullable=True), + sa.Column("created_from_ip", sa.String(45), nullable=True), + sa.Column("created_via", sa.String(100), nullable=True), + sa.Column("created_user_agent", sa.Text, nullable=True), + sa.Column("modified_by", sa.String(255), nullable=True), + sa.Column("modified_from_ip", sa.String(45), nullable=True), + sa.Column("modified_via", sa.String(100), nullable=True), + sa.Column("modified_user_agent", sa.Text, nullable=True), + sa.Column("import_batch_id", sa.String(36), nullable=True), + sa.Column("federation_source", sa.String(255), nullable=True), + sa.Column("version", sa.Integer, nullable=False, server_default="1"), + sa.Column("gateway_id", sa.String(36), nullable=True), + sa.Column("team_id", sa.String(36), nullable=True), + sa.Column("owner_email", sa.String(255), nullable=True), + sa.Column("visibility", sa.String(20), nullable=False, server_default="public"), + sa.UniqueConstraint("team_id", "owner_email", "name", name="uq_team_owner_name_prompt"), + sa.PrimaryKeyConstraint("id", name="pk_prompts"), + ) + + # 2) Insert rows from current prompts into prompts_old letting id autoincrement. + # We'll preserve uniqueness by using the team_id/owner_email/name triple to later remap. + conn.execute(text("INSERT INTO prompts_old (name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM prompts")) + + # 3) Build mapping from new uuid -> new integer id using the unique key (team_id, owner_email, name) + mapping = {} + res = conn.execute(text("SELECT p.id as uuid_id, p.team_id, p.owner_email, p.name, old.id as int_id FROM prompts p JOIN prompts_old old ON COALESCE(p.team_id, '') = COALESCE(old.team_id, '') AND COALESCE(p.owner_email, '') = COALESCE(old.owner_email, '') AND p.name = old.name")) + for row in res: + mapping[row[0]] = row[4] + + # 4) Recreate prompt_metrics_old and remap prompt_id + op.create_table( + "prompt_metrics_old", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("prompt_id", sa.Integer, nullable=False), + sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), + sa.Column("response_time", sa.Float, nullable=False), + sa.Column("is_success", sa.Boolean, nullable=False), + sa.Column("error_message", sa.Text, nullable=True), + sa.ForeignKeyConstraint(["prompt_id"], ["prompts_old.id"], name="fk_prompt_metrics_prompt_id"), + sa.PrimaryKeyConstraint("id", name="pk_prompt_metric"), + ) + + # Copy metrics mapping prompt_id via Python mapping + rows = conn.execute(text("SELECT id, prompt_id, timestamp, response_time, is_success, error_message FROM prompt_metrics")).fetchall() + for r in rows: + old_uuid = r[1] + int_id = mapping.get(old_uuid) + if int_id is None: + # skip orphaned metric + continue + conn.execute(text("INSERT INTO prompt_metrics_old (id, prompt_id, timestamp, response_time, is_success, error_message) VALUES (:id, :pid, :ts, :rt, :is_s, :err)"), {"id": r[0], "pid": int_id, "ts": r[2], "rt": r[3], "is_s": r[4], "err": r[5]}) + + # 5) Recreate server_prompt_association_old and remap prompt_id + op.create_table( + "server_prompt_association_old", + sa.Column("server_id", sa.String(36), nullable=False), + sa.Column("prompt_id", sa.Integer, nullable=False), + sa.PrimaryKeyConstraint("server_id", "prompt_id", name="pk_server_prompt_assoc"), + sa.ForeignKeyConstraint(["server_id"], ["servers.id"], name="fk_server_prompt_server_id"), + sa.ForeignKeyConstraint(["prompt_id"], ["prompts_old.id"], name="fk_server_prompt_prompt_id"), + ) + + rows = conn.execute(text("SELECT server_id, prompt_id FROM server_prompt_association")).fetchall() + for server_id, prompt_uuid in rows: + int_id = mapping.get(prompt_uuid) + if int_id is None: + continue + conn.execute(text("INSERT INTO server_prompt_association_old (server_id, prompt_id) VALUES (:sid, :pid)"), {"sid": server_id, "pid": int_id}) + + # 6) Drop current tables and rename old ones back + op.drop_table("prompt_metrics") + op.drop_table("server_prompt_association") + op.drop_table("prompts") + + op.rename_table("prompts_old", "prompts") + op.rename_table("prompt_metrics_old", "prompt_metrics") + op.rename_table("server_prompt_association_old", "server_prompt_association") diff --git a/mcpgateway/db.py b/mcpgateway/db.py index 3f6e9f9e7..74764badd 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -1428,7 +1428,7 @@ def reject(self, admin_email: str, reason: str, notes: Optional[str] = None) -> "server_resource_association", Base.metadata, Column("server_id", String(36), ForeignKey("servers.id"), primary_key=True), - Column("resource_id", Integer, ForeignKey("resources.id"), primary_key=True), + Column("resource_id", String(36), ForeignKey("resources.id"), primary_key=True), ) # Association table for servers and prompts @@ -1436,7 +1436,7 @@ def reject(self, admin_email: str, reason: str, notes: Optional[str] = None) -> "server_prompt_association", Base.metadata, Column("server_id", String(36), ForeignKey("servers.id"), primary_key=True), - Column("prompt_id", Integer, ForeignKey("prompts.id"), primary_key=True), + Column("prompt_id", String(36), ForeignKey("prompts.id"), primary_key=True), ) # Association table for servers and A2A agents @@ -1558,7 +1558,7 @@ class PromptMetric(Base): __tablename__ = "prompt_metrics" id: Mapped[int] = mapped_column(primary_key=True) - prompt_id: Mapped[int] = mapped_column(Integer, ForeignKey("prompts.id"), nullable=False) + prompt_id: Mapped[str] = mapped_column(String(36), ForeignKey("prompts.id"), nullable=False) timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) response_time: Mapped[float] = mapped_column(Float, nullable=False) is_success: Mapped[bool] = mapped_column(Boolean, nullable=False) From a704d88e7c86aff42c7e58e26478309a2caaaded Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 10:57:33 +0530 Subject: [PATCH 08/23] alembic prompts and resources Signed-off-by: rakdutta --- ...6f_uuid_change_for_prompt_and_resources.py | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py index e86dfbe1b..f87f5b0bc 100644 --- a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py +++ b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py @@ -110,6 +110,109 @@ def upgrade() -> None: op.rename_table("prompt_metrics_tmp", "prompt_metrics") op.rename_table("server_prompt_association_tmp", "server_prompt_association") + # ----------------------------- + # Resources -> change id to VARCHAR(32) and remap FKs + # ----------------------------- + # Add temporary id_new to resources + op.add_column("resources", sa.Column("id_new", sa.String(32), nullable=True)) + + rows = conn.execute(text("SELECT id FROM resources")).fetchall() + for (old_id,) in rows: + new_id = uuid.uuid4().hex + conn.execute(text("UPDATE resources SET id_new = :new WHERE id = :old"), {"new": new_id, "old": old_id}) + + # Create resources_tmp with varchar(32) id + op.create_table( + "resources_tmp", + sa.Column("id", sa.String(32), primary_key=True, nullable=False), + sa.Column("uri", sa.String(767), nullable=False), + sa.Column("name", sa.String(255), nullable=False), + sa.Column("description", sa.Text, nullable=True), + sa.Column("mime_type", sa.String(255), nullable=True), + sa.Column("size", sa.Integer, nullable=True), + sa.Column("uri_template", sa.Text, nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("is_active", sa.Boolean, nullable=True), + sa.Column("tags", sa.JSON, nullable=False), + sa.Column("text_content", sa.Text, nullable=True), + sa.Column("binary_content", sa.LargeBinary, nullable=True), + sa.Column("created_by", sa.String(255), nullable=True), + sa.Column("created_from_ip", sa.String(45), nullable=True), + sa.Column("created_via", sa.String(100), nullable=True), + sa.Column("created_user_agent", sa.Text, nullable=True), + sa.Column("modified_by", sa.String(255), nullable=True), + sa.Column("modified_from_ip", sa.String(45), nullable=True), + sa.Column("modified_via", sa.String(100), nullable=True), + sa.Column("modified_user_agent", sa.Text, nullable=True), + sa.Column("import_batch_id", sa.String(36), nullable=True), + sa.Column("federation_source", sa.String(255), nullable=True), + sa.Column("version", sa.Integer, nullable=False, server_default="1"), + sa.Column("gateway_id", sa.String(36), nullable=True), + sa.Column("team_id", sa.String(36), nullable=True), + sa.Column("owner_email", sa.String(255), nullable=True), + sa.Column("visibility", sa.String(20), nullable=False, server_default="public"), + sa.UniqueConstraint("team_id", "owner_email", "uri", name="uq_team_owner_uri_resource"), + sa.PrimaryKeyConstraint("id", name="pk_resources"), + ) + + # Copy data into resources_tmp using id_new + res_copy_cols = ( + "id, uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility" + ) + conn.execute(text(f"INSERT INTO resources_tmp ({res_copy_cols}) SELECT id_new, uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM resources")) + + # resource_metrics_tmp with resource_id varchar(32) + op.create_table( + "resource_metrics_tmp", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("resource_id", sa.String(32), nullable=False), + sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), + sa.Column("response_time", sa.Float, nullable=False), + sa.Column("is_success", sa.Boolean, nullable=False), + sa.Column("error_message", sa.Text, nullable=True), + sa.ForeignKeyConstraint(["resource_id"], ["resources_tmp.id"], name="fk_resource_metrics_resource_id"), + sa.PrimaryKeyConstraint("id", name="pk_resource_metrics"), + ) + + # copy resource_metrics mapping old int->new uuid + conn.execute(text("INSERT INTO resource_metrics_tmp (id, resource_id, timestamp, response_time, is_success, error_message) SELECT rm.id, r.id_new, rm.timestamp, rm.response_time, rm.is_success, rm.error_message FROM resource_metrics rm JOIN resources r ON rm.resource_id = r.id")) + + # server_resource_association_tmp + op.create_table( + "server_resource_association_tmp", + sa.Column("server_id", sa.String(36), nullable=False), + sa.Column("resource_id", sa.String(32), nullable=False), + sa.PrimaryKeyConstraint("server_id", "resource_id", name="pk_server_resource_assoc"), + sa.ForeignKeyConstraint(["server_id"], ["servers.id"], name="fk_server_resource_server_id"), + sa.ForeignKeyConstraint(["resource_id"], ["resources_tmp.id"], name="fk_server_resource_resource_id"), + ) + + conn.execute(text("INSERT INTO server_resource_association_tmp (server_id, resource_id) SELECT sra.server_id, r.id_new FROM server_resource_association sra JOIN resources r ON sra.resource_id = r.id")) + + # resource_subscriptions_tmp + op.create_table( + "resource_subscriptions_tmp", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("resource_id", sa.String(32), nullable=False), + sa.Column("subscriber_id", sa.String(255), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("last_notification", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(["resource_id"], ["resources_tmp.id"], name="fk_resource_subscriptions_resource_id"), + ) + + conn.execute(text("INSERT INTO resource_subscriptions_tmp (id, resource_id, subscriber_id, created_at, last_notification) SELECT rs.id, r.id_new, rs.subscriber_id, rs.created_at, rs.last_notification FROM resource_subscriptions rs JOIN resources r ON rs.resource_id = r.id")) + + # Drop old resource-related tables and rename tmp tables + op.drop_table("resource_metrics") + op.drop_table("server_resource_association") + op.drop_table("resource_subscriptions") + op.drop_table("resources") + + op.rename_table("resources_tmp", "resources") + op.rename_table("resource_metrics_tmp", "resource_metrics") + op.rename_table("server_resource_association_tmp", "server_resource_association") + op.rename_table("resource_subscriptions_tmp", "resource_subscriptions") def downgrade() -> None: """Downgrade schema.""" @@ -205,3 +308,118 @@ def downgrade() -> None: op.rename_table("prompts_old", "prompts") op.rename_table("prompt_metrics_old", "prompt_metrics") op.rename_table("server_prompt_association_old", "server_prompt_association") + + # ============================= + # Resources downgrade: rebuild integer ids and remap FKs + # ============================= + # 1) Create old-style resources table with integer id (autoincrement) + op.create_table( + "resources_old", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True, nullable=False), + sa.Column("uri", sa.String(767), nullable=False), + sa.Column("name", sa.String(255), nullable=False), + sa.Column("description", sa.Text, nullable=True), + sa.Column("mime_type", sa.String(255), nullable=True), + sa.Column("size", sa.Integer, nullable=True), + sa.Column("uri_template", sa.Text, nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("is_active", sa.Boolean, nullable=True), + sa.Column("tags", sa.JSON, nullable=False), + sa.Column("text_content", sa.Text, nullable=True), + sa.Column("binary_content", sa.LargeBinary, nullable=True), + sa.Column("created_by", sa.String(255), nullable=True), + sa.Column("created_from_ip", sa.String(45), nullable=True), + sa.Column("created_via", sa.String(100), nullable=True), + sa.Column("created_user_agent", sa.Text, nullable=True), + sa.Column("modified_by", sa.String(255), nullable=True), + sa.Column("modified_from_ip", sa.String(45), nullable=True), + sa.Column("modified_via", sa.String(100), nullable=True), + sa.Column("modified_user_agent", sa.Text, nullable=True), + sa.Column("import_batch_id", sa.String(36), nullable=True), + sa.Column("federation_source", sa.String(255), nullable=True), + sa.Column("version", sa.Integer, nullable=False, server_default="1"), + sa.Column("gateway_id", sa.String(36), nullable=True), + sa.Column("team_id", sa.String(36), nullable=True), + sa.Column("owner_email", sa.String(255), nullable=True), + sa.Column("visibility", sa.String(20), nullable=False, server_default="public"), + sa.UniqueConstraint("team_id", "owner_email", "uri", name="uq_team_owner_uri_resource"), + sa.PrimaryKeyConstraint("id", name="pk_resources"), + ) + + # 2) Insert rows from current resources into resources_old letting id autoincrement. + conn.execute(text("INSERT INTO resources_old (uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM resources")) + + # 3) Build mapping from new uuid -> new integer id using unique key (team_id, owner_email, uri) + mapping_res = {} + res_map = conn.execute(text("SELECT r.id as uuid_id, r.team_id, r.owner_email, r.uri, old.id as int_id FROM resources r JOIN resources_old old ON COALESCE(r.team_id, '') = COALESCE(old.team_id, '') AND COALESCE(r.owner_email, '') = COALESCE(old.owner_email, '') AND r.uri = old.uri")) + for row in res_map: + mapping_res[row[0]] = row[4] + + # 4) Recreate resource_metrics_old and remap resource_id + op.create_table( + "resource_metrics_old", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("resource_id", sa.Integer, nullable=False), + sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), + sa.Column("response_time", sa.Float, nullable=False), + sa.Column("is_success", sa.Boolean, nullable=False), + sa.Column("error_message", sa.Text, nullable=True), + sa.ForeignKeyConstraint(["resource_id"], ["resources_old.id"], name="fk_resource_metrics_resource_id"), + sa.PrimaryKeyConstraint("id", name="pk_resource_metrics_old"), + ) + + # Copy resource metrics remapping ids + rows = conn.execute(text("SELECT id, resource_id, timestamp, response_time, is_success, error_message FROM resource_metrics")).fetchall() + for r in rows: + old_uuid = r[1] + int_id = mapping_res.get(old_uuid) + if int_id is None: + continue + conn.execute(text("INSERT INTO resource_metrics_old (id, resource_id, timestamp, response_time, is_success, error_message) VALUES (:id, :rid, :ts, :rt, :is_s, :err)"), {"id": r[0], "rid": int_id, "ts": r[2], "rt": r[3], "is_s": r[4], "err": r[5]}) + + # 5) Recreate server_resource_association_old and remap resource_id + op.create_table( + "server_resource_association_old", + sa.Column("server_id", sa.String(36), nullable=False), + sa.Column("resource_id", sa.Integer, nullable=False), + sa.PrimaryKeyConstraint("server_id", "resource_id", name="pk_server_resource_assoc"), + sa.ForeignKeyConstraint(["server_id"], ["servers.id"], name="fk_server_resource_server_id"), + sa.ForeignKeyConstraint(["resource_id"], ["resources_old.id"], name="fk_server_resource_resource_id"), + ) + + rows = conn.execute(text("SELECT server_id, resource_id FROM server_resource_association")).fetchall() + for server_id, resource_uuid in rows: + int_id = mapping_res.get(resource_uuid) + if int_id is None: + continue + conn.execute(text("INSERT INTO server_resource_association_old (server_id, resource_id) VALUES (:sid, :rid)"), {"sid": server_id, "rid": int_id}) + + # 6) Recreate resource_subscriptions_old and remap resource_id + op.create_table( + "resource_subscriptions_old", + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("resource_id", sa.Integer, nullable=False), + sa.Column("subscriber_id", sa.String(255), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("last_notification", sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(["resource_id"], ["resources_old.id"], name="fk_resource_subscriptions_resource_id"), + ) + + rows = conn.execute(text("SELECT id, resource_id, subscriber_id, created_at, last_notification FROM resource_subscriptions")).fetchall() + for r in rows: + int_id = mapping_res.get(r[1]) + if int_id is None: + continue + conn.execute(text("INSERT INTO resource_subscriptions_old (id, resource_id, subscriber_id, created_at, last_notification) VALUES (:id, :rid, :sub, :ts, :ln)"), {"id": r[0], "rid": int_id, "sub": r[2], "ts": r[3], "ln": r[4]}) + + # 7) Drop current resource tables and rename old ones back + op.drop_table("resource_metrics") + op.drop_table("server_resource_association") + op.drop_table("resource_subscriptions") + op.drop_table("resources") + + op.rename_table("resources_old", "resources") + op.rename_table("resource_metrics_old", "resource_metrics") + op.rename_table("server_resource_association_old", "server_resource_association") + op.rename_table("resource_subscriptions_old", "resource_subscriptions") From 90a875e44f6fc9c9c0dbaeb40cc490778623af09 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 11:14:19 +0530 Subject: [PATCH 09/23] alembic Signed-off-by: rakdutta --- ...6f_uuid_change_for_prompt_and_resources.py | 33 ++++++++++++++++--- mcpgateway/db.py | 6 ++-- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py index f87f5b0bc..28204282a 100644 --- a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py +++ b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py @@ -101,6 +101,9 @@ def upgrade() -> None: conn.execute(text("INSERT INTO server_prompt_association_tmp (server_id, prompt_id) SELECT spa.server_id, p.id_new FROM server_prompt_association spa JOIN prompts p ON spa.prompt_id = p.id")) + # Update observability spans that reference prompts: remap integer prompt IDs -> new uuid + conn.execute(text("UPDATE observability_spans SET resource_id = p.id_new FROM prompts p WHERE observability_spans.resource_type = 'prompts' AND observability_spans.resource_id = p.id")) + # 7) Drop old tables and rename tmp tables into place op.drop_table("prompt_metrics") op.drop_table("server_prompt_association") @@ -114,7 +117,7 @@ def upgrade() -> None: # Resources -> change id to VARCHAR(32) and remap FKs # ----------------------------- # Add temporary id_new to resources - op.add_column("resources", sa.Column("id_new", sa.String(32), nullable=True)) + op.add_column("resources", sa.Column("id_new", sa.String(36), nullable=True)) rows = conn.execute(text("SELECT id FROM resources")).fetchall() for (old_id,) in rows: @@ -124,7 +127,7 @@ def upgrade() -> None: # Create resources_tmp with varchar(32) id op.create_table( "resources_tmp", - sa.Column("id", sa.String(32), primary_key=True, nullable=False), + sa.Column("id", sa.String(36), primary_key=True, nullable=False), sa.Column("uri", sa.String(767), nullable=False), sa.Column("name", sa.String(255), nullable=False), sa.Column("description", sa.Text, nullable=True), @@ -166,7 +169,7 @@ def upgrade() -> None: op.create_table( "resource_metrics_tmp", sa.Column("id", sa.Integer, primary_key=True, nullable=False), - sa.Column("resource_id", sa.String(32), nullable=False), + sa.Column("resource_id", sa.String(36), nullable=False), sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False), sa.Column("response_time", sa.Float, nullable=False), sa.Column("is_success", sa.Boolean, nullable=False), @@ -182,7 +185,7 @@ def upgrade() -> None: op.create_table( "server_resource_association_tmp", sa.Column("server_id", sa.String(36), nullable=False), - sa.Column("resource_id", sa.String(32), nullable=False), + sa.Column("resource_id", sa.String(36), nullable=False), sa.PrimaryKeyConstraint("server_id", "resource_id", name="pk_server_resource_assoc"), sa.ForeignKeyConstraint(["server_id"], ["servers.id"], name="fk_server_resource_server_id"), sa.ForeignKeyConstraint(["resource_id"], ["resources_tmp.id"], name="fk_server_resource_resource_id"), @@ -190,11 +193,14 @@ def upgrade() -> None: conn.execute(text("INSERT INTO server_resource_association_tmp (server_id, resource_id) SELECT sra.server_id, r.id_new FROM server_resource_association sra JOIN resources r ON sra.resource_id = r.id")) + # Update observability spans that reference resources: remap integer resource IDs -> new uuid + conn.execute(text("UPDATE observability_spans SET resource_id = r.id_new FROM resources r WHERE observability_spans.resource_type = 'resources' AND observability_spans.resource_id = r.id")) + # resource_subscriptions_tmp op.create_table( "resource_subscriptions_tmp", sa.Column("id", sa.Integer, primary_key=True, nullable=False), - sa.Column("resource_id", sa.String(32), nullable=False), + sa.Column("resource_id", sa.String(36), nullable=False), sa.Column("subscriber_id", sa.String(255), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), sa.Column("last_notification", sa.DateTime(timezone=True), nullable=True), @@ -300,6 +306,15 @@ def downgrade() -> None: continue conn.execute(text("INSERT INTO server_prompt_association_old (server_id, prompt_id) VALUES (:sid, :pid)"), {"sid": server_id, "pid": int_id}) + # Remap observability_spans for prompts: uuid -> integer id using mapping built above + span_rows = conn.execute(text("SELECT span_id, resource_id FROM observability_spans WHERE resource_type = 'prompts'")).fetchall() + for span_id, res_uuid in span_rows: + int_id = mapping.get(res_uuid) + if int_id is None: + # skip orphaned span + continue + conn.execute(text("UPDATE observability_spans SET resource_id = :rid WHERE span_id = :sid"), {"rid": int_id, "sid": span_id}) + # 6) Drop current tables and rename old ones back op.drop_table("prompt_metrics") op.drop_table("server_prompt_association") @@ -413,6 +428,14 @@ def downgrade() -> None: continue conn.execute(text("INSERT INTO resource_subscriptions_old (id, resource_id, subscriber_id, created_at, last_notification) VALUES (:id, :rid, :sub, :ts, :ln)"), {"id": r[0], "rid": int_id, "sub": r[2], "ts": r[3], "ln": r[4]}) + # Remap observability_spans for resources: uuid -> integer id using mapping_res built above + span_rows = conn.execute(text("SELECT span_id, resource_id FROM observability_spans WHERE resource_type = 'resources'")).fetchall() + for span_id, res_uuid in span_rows: + int_id = mapping_res.get(res_uuid) + if int_id is None: + continue + conn.execute(text("UPDATE observability_spans SET resource_id = :rid WHERE span_id = :sid"), {"rid": int_id, "sid": span_id}) + # 7) Drop current resource tables and rename old ones back op.drop_table("resource_metrics") op.drop_table("server_resource_association") diff --git a/mcpgateway/db.py b/mcpgateway/db.py index 74764badd..b362691cd 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -1496,7 +1496,7 @@ class ResourceMetric(Base): Attributes: id (int): Primary key. - resource_id (int): Foreign key linking to the resource. + resource_id (str): Foreign key linking to the resource. timestamp (datetime): The time when the invocation occurred. response_time (float): The response time in seconds. is_success (bool): True if the invocation succeeded, False otherwise. @@ -1548,7 +1548,7 @@ class PromptMetric(Base): Attributes: id (int): Primary key. - prompt_id (int): Foreign key linking to the prompt. + prompt_id (str): Foreign key linking to the prompt. timestamp (datetime): The time when the invocation occurred. response_time (float): The response time in seconds. is_success (bool): True if the invocation succeeded, False otherwise. @@ -2423,7 +2423,7 @@ class ResourceSubscription(Base): __tablename__ = "resource_subscriptions" id: Mapped[int] = mapped_column(primary_key=True) - resource_id: Mapped[int] = mapped_column(ForeignKey("resources.id")) + resource_id: Mapped[str] = mapped_column(ForeignKey("resources.id")) subscriber_id: Mapped[str] = mapped_column(String(255), nullable=False) # Client identifier created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) last_notification: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) From 3e830a42949823a4746f33aa0caf6cc2b5de573b Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 11:33:41 +0530 Subject: [PATCH 10/23] js Signed-off-by: rakdutta --- mcpgateway/static/admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcpgateway/static/admin.js b/mcpgateway/static/admin.js index 5e128fe71..6474ff450 100644 --- a/mcpgateway/static/admin.js +++ b/mcpgateway/static/admin.js @@ -11562,7 +11562,7 @@ async function runPromptTest() { // Call the prompt API endpoint const response = await fetch( - `${window.ROOT_PATH}/prompts/${encodeURIComponent(promptTestState.currentTestPrompt.name)}`, + `${window.ROOT_PATH}/prompts/${encodeURIComponent(promptTestState.currentTestPrompt.id)}`, { method: "POST", headers: { From d066f99f28357ef7f1883c5cdfd9358ff4fb0a0e Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 12:35:02 +0530 Subject: [PATCH 11/23] enabled Signed-off-by: rakdutta --- mcpgateway/admin.py | 17 +-- ...6f_uuid_change_for_prompt_and_resources.py | 111 +++++++++++++++--- mcpgateway/db.py | 9 +- mcpgateway/schemas.py | 9 +- mcpgateway/services/prompt_service.py | 50 ++++---- 5 files changed, 138 insertions(+), 58 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 31aee7830..05189aa91 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -1830,7 +1830,7 @@ async def admin_list_prompts( ... arguments=[{"name": "name", "type": "string"}], ... created_at=datetime.now(timezone.utc), ... updated_at=datetime.now(timezone.utc), - ... is_active=True, + ... enabled=True, ... metrics=PromptMetrics( ... total_executions=10, successful_executions=10, failed_executions=0, ... failure_rate=0.0, min_response_time=0.01, max_response_time=0.1, @@ -1855,7 +1855,7 @@ async def admin_list_prompts( >>> mock_inactive_prompt = PromptRead( ... id="39334ce0ed2644d79ede8913a66930c9", name="Inactive Prompt", description="Another test", template="Bye!", ... arguments=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), - ... is_active=False, metrics=PromptMetrics( + ... enabled=False, metrics=PromptMetrics( ... total_executions=0, successful_executions=0, failed_executions=0, ... failure_rate=0.0, min_response_time=0.0, max_response_time=0.0, ... avg_response_time=0.0, last_execution_time=None @@ -5941,7 +5941,7 @@ async def admin_prompts_partial_html( LOGGER.debug(f"Filtering prompts by gateway IDs: {non_null_ids}") if not include_inactive: - query = query.where(DbPrompt.is_active.is_(True)) + query = query.where(DbPrompt.enabled.is_(True)) # Access conditions: owner, team, public access_conditions = [DbPrompt.owner_email == user_email] @@ -5965,7 +5965,7 @@ async def admin_prompts_partial_html( else: count_query = count_query.where(DbPrompt.gateway_id.in_(non_null_ids)) if not include_inactive: - count_query = count_query.where(DbPrompt.is_active.is_(True)) + count_query = count_query.where(DbPrompt.enabled.is_(True)) total_items = db.scalar(count_query) or 0 @@ -6268,7 +6268,7 @@ async def admin_get_all_prompt_ids( LOGGER.debug(f"Filtering prompts by gateway IDs: {non_null_ids}") if not include_inactive: - query = query.where(DbPrompt.is_active.is_(True)) + query = query.where(DbPrompt.enabled.is_(True)) access_conditions = [DbPrompt.owner_email == user_email, DbPrompt.visibility == "public"] if team_ids: @@ -6437,7 +6437,7 @@ async def admin_search_prompts( query = select(DbPrompt.id, DbPrompt.name, DbPrompt.description) if not include_inactive: - query = query.where(DbPrompt.is_active.is_(True)) + query = query.where(DbPrompt.enabled.is_(True)) access_conditions = [DbPrompt.owner_email == user_email, DbPrompt.visibility == "public"] if team_ids: @@ -8702,7 +8702,7 @@ async def admin_get_prompt(prompt_id: str, db: Session = Depends(get_db), user=D ... "arguments": [{"name": "name", "type": "string"}], ... "created_at": datetime.now(timezone.utc), ... "updated_at": datetime.now(timezone.utc), - ... "is_active": True, + ... "enabled": True, ... "metrics": mock_metrics, ... "tags": [] ... } @@ -12242,7 +12242,8 @@ async def get_prompts_section( "description": prompt.description, "arguments": prompt.arguments or [], "tags": prompt.tags or [], - "isActive": prompt.is_active, + # Prompt enabled/disabled state is stored on the prompt as `enabled`. + "isActive": getattr(prompt, "enabled", False), "team_id": getattr(prompt, "team_id", None), "visibility": getattr(prompt, "visibility", "private"), } diff --git a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py index 28204282a..e90d272c1 100644 --- a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py +++ b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py @@ -5,19 +5,19 @@ Create Date: 2025-12-01 14:52:01.957105 """ + from typing import Sequence, Union from alembic import op import sqlalchemy as sa import uuid -import logging from sqlalchemy import text # revision identifiers, used by Alembic. -revision: str = '356a2d4eed6f' -down_revision: Union[str, Sequence[str], None] = '9e028ecf59c4' +revision: str = "356a2d4eed6f" +down_revision: Union[str, Sequence[str], None] = "9e028ecf59c4" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -71,7 +71,18 @@ def upgrade() -> None: " created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip," " modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility" ) - conn.execute(text(f"INSERT INTO prompts_tmp ({copy_cols}) SELECT id_new, name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM prompts")) + conn.execute( + text( + ( + "INSERT INTO prompts_tmp (" + copy_cols + ") " + "SELECT id_new, name, description, template, argument_schema, created_at, " + "updated_at, is_active, tags, created_by, created_from_ip, created_via, " + "created_user_agent, modified_by, modified_from_ip, modified_via, " + "modified_user_agent, import_batch_id, federation_source, version, gateway_id, " + "team_id, owner_email, visibility FROM prompts" + ) + ) + ) # 4) Create new prompt_metrics table with prompt_id varchar(36) op.create_table( @@ -87,7 +98,11 @@ def upgrade() -> None: ) # 5) Copy prompt_metrics mapping old integer prompt_id -> new uuid via join - conn.execute(text("INSERT INTO prompt_metrics_tmp (id, prompt_id, timestamp, response_time, is_success, error_message) SELECT pm.id, p.id_new, pm.timestamp, pm.response_time, pm.is_success, pm.error_message FROM prompt_metrics pm JOIN prompts p ON pm.prompt_id = p.id")) + conn.execute( + text( + "INSERT INTO prompt_metrics_tmp (id, prompt_id, timestamp, response_time, is_success, error_message) SELECT pm.id, p.id_new, pm.timestamp, pm.response_time, pm.is_success, pm.error_message FROM prompt_metrics pm JOIN prompts p ON pm.prompt_id = p.id" + ) + ) # 6) Create new server_prompt_association table with prompt_id varchar(36) op.create_table( @@ -112,7 +127,7 @@ def upgrade() -> None: op.rename_table("prompts_tmp", "prompts") op.rename_table("prompt_metrics_tmp", "prompt_metrics") op.rename_table("server_prompt_association_tmp", "server_prompt_association") - + # ----------------------------- # Resources -> change id to VARCHAR(32) and remap FKs # ----------------------------- @@ -160,10 +175,20 @@ def upgrade() -> None: ) # Copy data into resources_tmp using id_new - res_copy_cols = ( - "id, uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility" + res_copy_cols = "id, uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility" + conn.execute( + text( + ( + "INSERT INTO resources_tmp (" + res_copy_cols + ") " + "SELECT id_new, uri, name, description, mime_type, size, uri_template, " + "created_at, updated_at, is_active, tags, text_content, binary_content, " + "created_by, created_from_ip, created_via, created_user_agent, modified_by, " + "modified_from_ip, modified_via, modified_user_agent, import_batch_id, " + "federation_source, version, gateway_id, team_id, owner_email, visibility " + "FROM resources" + ) + ) ) - conn.execute(text(f"INSERT INTO resources_tmp ({res_copy_cols}) SELECT id_new, uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM resources")) # resource_metrics_tmp with resource_id varchar(32) op.create_table( @@ -179,7 +204,11 @@ def upgrade() -> None: ) # copy resource_metrics mapping old int->new uuid - conn.execute(text("INSERT INTO resource_metrics_tmp (id, resource_id, timestamp, response_time, is_success, error_message) SELECT rm.id, r.id_new, rm.timestamp, rm.response_time, rm.is_success, rm.error_message FROM resource_metrics rm JOIN resources r ON rm.resource_id = r.id")) + conn.execute( + text( + "INSERT INTO resource_metrics_tmp (id, resource_id, timestamp, response_time, is_success, error_message) SELECT rm.id, r.id_new, rm.timestamp, rm.response_time, rm.is_success, rm.error_message FROM resource_metrics rm JOIN resources r ON rm.resource_id = r.id" + ) + ) # server_resource_association_tmp op.create_table( @@ -191,7 +220,9 @@ def upgrade() -> None: sa.ForeignKeyConstraint(["resource_id"], ["resources_tmp.id"], name="fk_server_resource_resource_id"), ) - conn.execute(text("INSERT INTO server_resource_association_tmp (server_id, resource_id) SELECT sra.server_id, r.id_new FROM server_resource_association sra JOIN resources r ON sra.resource_id = r.id")) + conn.execute( + text("INSERT INTO server_resource_association_tmp (server_id, resource_id) SELECT sra.server_id, r.id_new FROM server_resource_association sra JOIN resources r ON sra.resource_id = r.id") + ) # Update observability spans that reference resources: remap integer resource IDs -> new uuid conn.execute(text("UPDATE observability_spans SET resource_id = r.id_new FROM resources r WHERE observability_spans.resource_type = 'resources' AND observability_spans.resource_id = r.id")) @@ -207,7 +238,11 @@ def upgrade() -> None: sa.ForeignKeyConstraint(["resource_id"], ["resources_tmp.id"], name="fk_resource_subscriptions_resource_id"), ) - conn.execute(text("INSERT INTO resource_subscriptions_tmp (id, resource_id, subscriber_id, created_at, last_notification) SELECT rs.id, r.id_new, rs.subscriber_id, rs.created_at, rs.last_notification FROM resource_subscriptions rs JOIN resources r ON rs.resource_id = r.id")) + conn.execute( + text( + "INSERT INTO resource_subscriptions_tmp (id, resource_id, subscriber_id, created_at, last_notification) SELECT rs.id, r.id_new, rs.subscriber_id, rs.created_at, rs.last_notification FROM resource_subscriptions rs JOIN resources r ON rs.resource_id = r.id" + ) + ) # Drop old resource-related tables and rename tmp tables op.drop_table("resource_metrics") @@ -220,6 +255,7 @@ def upgrade() -> None: op.rename_table("server_resource_association_tmp", "server_resource_association") op.rename_table("resource_subscriptions_tmp", "resource_subscriptions") + def downgrade() -> None: """Downgrade schema.""" conn = op.get_bind() @@ -258,11 +294,25 @@ def downgrade() -> None: # 2) Insert rows from current prompts into prompts_old letting id autoincrement. # We'll preserve uniqueness by using the team_id/owner_email/name triple to later remap. - conn.execute(text("INSERT INTO prompts_old (name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM prompts")) + conn.execute( + text( + "INSERT INTO prompts_old (name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM prompts" + ) + ) # 3) Build mapping from new uuid -> new integer id using the unique key (team_id, owner_email, name) mapping = {} - res = conn.execute(text("SELECT p.id as uuid_id, p.team_id, p.owner_email, p.name, old.id as int_id FROM prompts p JOIN prompts_old old ON COALESCE(p.team_id, '') = COALESCE(old.team_id, '') AND COALESCE(p.owner_email, '') = COALESCE(old.owner_email, '') AND p.name = old.name")) + res = conn.execute( + text( + ( + "SELECT p.id as uuid_id, p.team_id, p.owner_email, p.name, old.id as int_id " + "FROM prompts p JOIN prompts_old old ON " + "COALESCE(p.team_id, '') = COALESCE(old.team_id, '') AND " + "COALESCE(p.owner_email, '') = COALESCE(old.owner_email, '') AND " + "p.name = old.name" + ) + ) + ) for row in res: mapping[row[0]] = row[4] @@ -287,7 +337,10 @@ def downgrade() -> None: if int_id is None: # skip orphaned metric continue - conn.execute(text("INSERT INTO prompt_metrics_old (id, prompt_id, timestamp, response_time, is_success, error_message) VALUES (:id, :pid, :ts, :rt, :is_s, :err)"), {"id": r[0], "pid": int_id, "ts": r[2], "rt": r[3], "is_s": r[4], "err": r[5]}) + conn.execute( + text("INSERT INTO prompt_metrics_old (id, prompt_id, timestamp, response_time, is_success, error_message) VALUES (:id, :pid, :ts, :rt, :is_s, :err)"), + {"id": r[0], "pid": int_id, "ts": r[2], "rt": r[3], "is_s": r[4], "err": r[5]}, + ) # 5) Recreate server_prompt_association_old and remap prompt_id op.create_table( @@ -363,11 +416,25 @@ def downgrade() -> None: ) # 2) Insert rows from current resources into resources_old letting id autoincrement. - conn.execute(text("INSERT INTO resources_old (uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM resources")) + conn.execute( + text( + "INSERT INTO resources_old (uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM resources" + ) + ) # 3) Build mapping from new uuid -> new integer id using unique key (team_id, owner_email, uri) mapping_res = {} - res_map = conn.execute(text("SELECT r.id as uuid_id, r.team_id, r.owner_email, r.uri, old.id as int_id FROM resources r JOIN resources_old old ON COALESCE(r.team_id, '') = COALESCE(old.team_id, '') AND COALESCE(r.owner_email, '') = COALESCE(old.owner_email, '') AND r.uri = old.uri")) + res_map = conn.execute( + text( + ( + "SELECT r.id as uuid_id, r.team_id, r.owner_email, r.uri, old.id as int_id " + "FROM resources r JOIN resources_old old ON " + "COALESCE(r.team_id, '') = COALESCE(old.team_id, '') AND " + "COALESCE(r.owner_email, '') = COALESCE(old.owner_email, '') AND " + "r.uri = old.uri" + ) + ) + ) for row in res_map: mapping_res[row[0]] = row[4] @@ -391,7 +458,10 @@ def downgrade() -> None: int_id = mapping_res.get(old_uuid) if int_id is None: continue - conn.execute(text("INSERT INTO resource_metrics_old (id, resource_id, timestamp, response_time, is_success, error_message) VALUES (:id, :rid, :ts, :rt, :is_s, :err)"), {"id": r[0], "rid": int_id, "ts": r[2], "rt": r[3], "is_s": r[4], "err": r[5]}) + conn.execute( + text("INSERT INTO resource_metrics_old (id, resource_id, timestamp, response_time, is_success, error_message) VALUES (:id, :rid, :ts, :rt, :is_s, :err)"), + {"id": r[0], "rid": int_id, "ts": r[2], "rt": r[3], "is_s": r[4], "err": r[5]}, + ) # 5) Recreate server_resource_association_old and remap resource_id op.create_table( @@ -426,7 +496,10 @@ def downgrade() -> None: int_id = mapping_res.get(r[1]) if int_id is None: continue - conn.execute(text("INSERT INTO resource_subscriptions_old (id, resource_id, subscriber_id, created_at, last_notification) VALUES (:id, :rid, :sub, :ts, :ln)"), {"id": r[0], "rid": int_id, "sub": r[2], "ts": r[3], "ln": r[4]}) + conn.execute( + text("INSERT INTO resource_subscriptions_old (id, resource_id, subscriber_id, created_at, last_notification) VALUES (:id, :rid, :sub, :ts, :ln)"), + {"id": r[0], "rid": int_id, "sub": r[2], "ts": r[3], "ln": r[4]}, + ) # Remap observability_spans for resources: uuid -> integer id using mapping_res built above span_rows = conn.execute(text("SELECT span_id, resource_id FROM observability_spans WHERE resource_type = 'resources'")).fetchall() diff --git a/mcpgateway/db.py b/mcpgateway/db.py index b362691cd..66316fadd 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -2219,7 +2219,8 @@ class Resource(Base): uri_template: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # URI template for parameterized resources created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now) - is_active: Mapped[bool] = mapped_column(default=True) + #is_active: Mapped[bool] = mapped_column(default=True) + enabled: Mapped[bool] = mapped_column(default=True) tags: Mapped[List[str]] = mapped_column(JSON, default=list, nullable=False) # Comprehensive metadata for audit tracking @@ -2476,7 +2477,8 @@ class Prompt(Base): argument_schema: Mapped[Dict[str, Any]] = mapped_column(JSON) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now) - is_active: Mapped[bool] = mapped_column(default=True) + #is_active: Mapped[bool] = mapped_column(default=True) + enabled: Mapped[bool] = mapped_column(default=True) tags: Mapped[List[str]] = mapped_column(JSON, default=list, nullable=False) # Comprehensive metadata for audit tracking @@ -2667,7 +2669,8 @@ class Server(Base): icon: Mapped[Optional[str]] = mapped_column(String(767), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now) - is_active: Mapped[bool] = mapped_column(default=True) + #is_active: Mapped[bool] = mapped_column(default=True) + enabled: Mapped[bool] = mapped_column(default=True) tags: Mapped[List[str]] = mapped_column(JSON, default=list, nullable=False) # Comprehensive metadata for audit tracking diff --git a/mcpgateway/schemas.py b/mcpgateway/schemas.py index 3cd42176c..1fb9a87c7 100644 --- a/mcpgateway/schemas.py +++ b/mcpgateway/schemas.py @@ -1780,7 +1780,8 @@ class ResourceRead(BaseModelWithConfigDict): size: Optional[int] created_at: datetime updated_at: datetime - is_active: bool + #is_active: bool + enabled: bool metrics: ResourceMetrics tags: List[Dict[str, str]] = Field(default_factory=list, description="Tags for categorizing the resource") @@ -2287,7 +2288,8 @@ class PromptRead(BaseModelWithConfigDict): arguments: List[PromptArgument] created_at: datetime updated_at: datetime - is_active: bool + #is_active: bool + enabled: bool tags: List[Dict[str, str]] = Field(default_factory=list, description="Tags for categorizing the prompt") metrics: PromptMetrics @@ -3710,7 +3712,8 @@ class ServerRead(BaseModelWithConfigDict): icon: Optional[str] created_at: datetime updated_at: datetime - is_active: bool + #is_active: bool + enabled: bool associated_tools: List[str] = [] associated_resources: List[int] = [] associated_prompts: List[int] = [] diff --git a/mcpgateway/services/prompt_service.py b/mcpgateway/services/prompt_service.py index 1877e384a..dac07f888 100644 --- a/mcpgateway/services/prompt_service.py +++ b/mcpgateway/services/prompt_service.py @@ -60,12 +60,12 @@ class PromptNotFoundError(PromptError): class PromptNameConflictError(PromptError): """Raised when a prompt name conflicts with existing (active or inactive) prompt.""" - def __init__(self, name: str, is_active: bool = True, prompt_id: Optional[int] = None, visibility: str = "public") -> None: + def __init__(self, name: str, enabled: bool = True, prompt_id: Optional[int] = None, visibility: str = "public") -> None: """Initialize the error with prompt information. Args: name: The conflicting prompt name - is_active: Whether the existing prompt is active + enabled: Whether the existing prompt is enabled prompt_id: ID of the existing prompt if available visibility: Prompt visibility level (private, team, public). @@ -74,21 +74,21 @@ def __init__(self, name: str, is_active: bool = True, prompt_id: Optional[int] = >>> error = PromptNameConflictError("test_prompt") >>> error.name 'test_prompt' - >>> error.is_active + >>> error.enabled True >>> error.prompt_id is None True >>> error = PromptNameConflictError("inactive_prompt", False, 123) - >>> error.is_active + >>> error.enabled False >>> error.prompt_id 123 """ self.name = name - self.is_active = is_active + self.enabled = enabled self.prompt_id = prompt_id message = f"{visibility.capitalize()} Prompt already exists with name: {name}" - if not is_active: + if not enabled: message += f" (currently inactive, ID: {prompt_id})" super().__init__(message) @@ -244,7 +244,7 @@ def _convert_db_prompt(self, db_prompt: DbPrompt) -> Dict[str, Any]: "arguments": arguments_list, "created_at": db_prompt.created_at, "updated_at": db_prompt.updated_at, - "is_active": db_prompt.is_active, + "enabled": db_prompt.enabled, "metrics": { "totalExecutions": total, "successfulExecutions": successful, @@ -386,12 +386,12 @@ async def register_prompt( # Check for existing public prompt with the same name existing_prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt.name, DbPrompt.visibility == "public")).scalar_one_or_none() if existing_prompt: - raise PromptNameConflictError(prompt.name, is_active=existing_prompt.is_active, prompt_id=existing_prompt.id, visibility=existing_prompt.visibility) + raise PromptNameConflictError(prompt.name, enabled=existing_prompt.enabled, prompt_id=existing_prompt.id, visibility=existing_prompt.visibility) elif visibility.lower() == "team": # Check for existing team prompt with the same name existing_prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt.name, DbPrompt.visibility == "team", DbPrompt.team_id == team_id)).scalar_one_or_none() if existing_prompt: - raise PromptNameConflictError(prompt.name, is_active=existing_prompt.is_active, prompt_id=existing_prompt.id, visibility=existing_prompt.visibility) + raise PromptNameConflictError(prompt.name, enabled=existing_prompt.enabled, prompt_id=existing_prompt.id, visibility=existing_prompt.visibility) # Add to DB db.add(db_prompt) @@ -469,7 +469,7 @@ async def list_prompts(self, db: Session, include_inactive: bool = False, cursor query = query.where(DbPrompt.id > last_id) if not include_inactive: - query = query.where(DbPrompt.is_active) + query = query.where(DbPrompt.enabled) # Add tag filtering if tags are provided if tags: @@ -531,7 +531,7 @@ async def list_prompts_for_user( # Apply active/inactive filter if not include_inactive: - query = query.where(DbPrompt.is_active) + query = query.where(DbPrompt.enabled) if team_id: if team_id not in team_ids: @@ -610,7 +610,7 @@ async def list_server_prompts(self, db: Session, server_id: str, include_inactiv """ query = select(DbPrompt).join(server_prompt_association, DbPrompt.id == server_prompt_association.c.prompt_id).where(server_prompt_association.c.server_id == server_id) if not include_inactive: - query = query.where(DbPrompt.is_active) + query = query.where(DbPrompt.enabled) # Cursor-based pagination logic can be implemented here in the future. logger.debug(cursor) prompts = db.execute(query).scalars().all() @@ -773,20 +773,20 @@ async def get_prompt( # Find prompt by ID or name if prompt_id is not None: - prompt = db.execute(select(DbPrompt).where(DbPrompt.id == prompt_id).where(DbPrompt.is_active)).scalar_one_or_none() + prompt = db.execute(select(DbPrompt).where(DbPrompt.id == prompt_id).where(DbPrompt.enabled)).scalar_one_or_none() search_key = prompt_id else: # Look up by name (active prompts only) # Note: Team/owner scoping could be added here when user context is available - prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt_name).where(DbPrompt.is_active)).scalar_one_or_none() + prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt_name).where(DbPrompt.enabled)).scalar_one_or_none() search_key = prompt_name if not prompt: # Check if an inactive prompt exists if prompt_id is not None: - inactive_prompt = db.execute(select(DbPrompt).where(DbPrompt.id == prompt_id).where(not_(DbPrompt.is_active))).scalar_one_or_none() + inactive_prompt = db.execute(select(DbPrompt).where(DbPrompt.id == prompt_id).where(not_(DbPrompt.enabled))).scalar_one_or_none() else: - inactive_prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt_name).where(not_(DbPrompt.is_active))).scalar_one_or_none() + inactive_prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt_name).where(not_(DbPrompt.enabled))).scalar_one_or_none() if inactive_prompt: raise PromptNotFoundError(f"Prompt '{search_key}' exists but is inactive") @@ -926,13 +926,13 @@ async def update_prompt( # Check for existing public prompts with the same name existing_prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt_update.name, DbPrompt.visibility == "public")).scalar_one_or_none() if existing_prompt: - raise PromptNameConflictError(prompt_update.name, is_active=existing_prompt.is_active, prompt_id=existing_prompt.id, visibility=existing_prompt.visibility) + raise PromptNameConflictError(prompt_update.name, enabled=existing_prompt.enabled, prompt_id=existing_prompt.id, visibility=existing_prompt.visibility) elif visibility.lower() == "team" and team_id: # Check for existing team prompt with the same name existing_prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt_update.name, DbPrompt.visibility == "team", DbPrompt.team_id == team_id)).scalar_one_or_none() logger.info(f"Existing prompt check result: {existing_prompt}") if existing_prompt: - raise PromptNameConflictError(prompt_update.name, is_active=existing_prompt.is_active, prompt_id=existing_prompt.id, visibility=existing_prompt.visibility) + raise PromptNameConflictError(prompt_update.name, enabled=existing_prompt.enabled, prompt_id=existing_prompt.id, visibility=existing_prompt.visibility) # Check ownership if user_email provided if user_email: @@ -1061,8 +1061,8 @@ async def toggle_prompt_status(self, db: Session, prompt_id: int, activate: bool if not await permission_service.check_resource_ownership(user_email, prompt): raise PermissionError("Only the owner can activate the Prompt" if activate else "Only the owner can deactivate the Prompt") - if prompt.is_active != activate: - prompt.is_active = activate + if prompt.enabled != activate: + prompt.enabled = activate prompt.updated_at = datetime.now(timezone.utc) db.commit() db.refresh(prompt) @@ -1331,7 +1331,7 @@ async def _notify_prompt_added(self, prompt: DbPrompt) -> None: "id": prompt.id, "name": prompt.name, "description": prompt.description, - "is_active": prompt.is_active, + "enabled": prompt.enabled, }, "timestamp": datetime.now(timezone.utc).isoformat(), } @@ -1350,7 +1350,7 @@ async def _notify_prompt_updated(self, prompt: DbPrompt) -> None: "id": prompt.id, "name": prompt.name, "description": prompt.description, - "is_active": prompt.is_active, + "enabled": prompt.enabled, }, "timestamp": datetime.now(timezone.utc).isoformat(), } @@ -1365,7 +1365,7 @@ async def _notify_prompt_activated(self, prompt: DbPrompt) -> None: """ event = { "type": "prompt_activated", - "data": {"id": prompt.id, "name": prompt.name, "is_active": True}, + "data": {"id": prompt.id, "name": prompt.name, "enabled": True}, "timestamp": datetime.now(timezone.utc).isoformat(), } await self._publish_event(event) @@ -1379,7 +1379,7 @@ async def _notify_prompt_deactivated(self, prompt: DbPrompt) -> None: """ event = { "type": "prompt_deactivated", - "data": {"id": prompt.id, "name": prompt.name, "is_active": False}, + "data": {"id": prompt.id, "name": prompt.name, "enabled": False}, "timestamp": datetime.now(timezone.utc).isoformat(), } await self._publish_event(event) @@ -1407,7 +1407,7 @@ async def _notify_prompt_removed(self, prompt: DbPrompt) -> None: """ event = { "type": "prompt_removed", - "data": {"id": prompt.id, "name": prompt.name, "is_active": False}, + "data": {"id": prompt.id, "name": prompt.name, "enabled": False}, "timestamp": datetime.now(timezone.utc).isoformat(), } await self._publish_event(event) From 0c65ccea2c8899730a490ea8ea63189fcca5d1bb Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 12:40:01 +0530 Subject: [PATCH 12/23] enable prompts Signed-off-by: rakdutta --- mcpgateway/services/completion_service.py | 3 ++- mcpgateway/services/export_service.py | 3 ++- mcpgateway/templates/prompts_partial.html | 6 +++--- tests/unit/mcpgateway/services/test_export_service.py | 2 +- tests/unit/mcpgateway/services/test_prompt_service.py | 7 +++++-- .../mcpgateway/services/test_prompt_service_extended.py | 2 +- tests/unit/mcpgateway/test_main.py | 4 ++-- 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/mcpgateway/services/completion_service.py b/mcpgateway/services/completion_service.py index 89b99c9d9..9e9f9c72a 100644 --- a/mcpgateway/services/completion_service.py +++ b/mcpgateway/services/completion_service.py @@ -176,7 +176,8 @@ async def _complete_prompt_argument(self, db: Session, ref: Dict[str, Any], arg_ if not prompt_name: raise CompletionError("Missing prompt name") - prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt_name).where(DbPrompt.is_active)).scalar_one_or_none() + # Only consider prompts that are enabled (renamed from `is_active` -> `enabled`) + prompt = db.execute(select(DbPrompt).where(DbPrompt.name == prompt_name).where(DbPrompt.enabled)).scalar_one_or_none() if not prompt: raise CompletionError(f"Prompt not found: {prompt_name}") diff --git a/mcpgateway/services/export_service.py b/mcpgateway/services/export_service.py index 0334160b5..d5806dd59 100644 --- a/mcpgateway/services/export_service.py +++ b/mcpgateway/services/export_service.py @@ -429,7 +429,8 @@ async def _export_prompts(self, db: Session, tags: Optional[List[str]], include_ "description": prompt.description, "input_schema": input_schema, "tags": prompt.tags or [], - "is_active": prompt.is_active, + # Use the new `enabled` attribute on prompt objects but keep export key `is_active` for compatibility + "is_active": getattr(prompt, "enabled", getattr(prompt, "is_active", False)), } # Convert arguments to input schema format diff --git a/mcpgateway/templates/prompts_partial.html b/mcpgateway/templates/prompts_partial.html index 16d1bbec6..56ce2e176 100644 --- a/mcpgateway/templates/prompts_partial.html +++ b/mcpgateway/templates/prompts_partial.html @@ -23,7 +23,7 @@ - + - +
{{ prompt.owner_email }} {% if prompt.team %}{{ prompt.team }}{% else %}None{% endif %} {% if prompt.visibility == 'public' %}🌍 Public{% elif prompt.visibility == 'team' %}👥 Team{% else %}🔒 Private{% endif %}
{% set enabled = prompt.is_active %}{% if enabled %}Active{% else %}Inactive{% endif %}
{% set enabled = prompt.enabled %}{% if enabled %}Active{% else %}Inactive{% endif %}
@@ -42,8 +42,8 @@
- - + +
diff --git a/tests/unit/mcpgateway/services/test_export_service.py b/tests/unit/mcpgateway/services/test_export_service.py index 4577525d5..6c2a839f9 100644 --- a/tests/unit/mcpgateway/services/test_export_service.py +++ b/tests/unit/mcpgateway/services/test_export_service.py @@ -767,7 +767,7 @@ async def test_export_prompts_with_arguments(export_service, mock_db): mock_prompt.template = "Process {{user_input}} with {{context}}" mock_prompt.description = "Test prompt" mock_prompt.arguments = [mock_arg1, mock_arg2] - mock_prompt.is_active = True + mock_prompt.enabled = True mock_prompt.tags = ["nlp", "processing"] export_service.prompt_service.list_prompts.return_value = ([mock_prompt], None) diff --git a/tests/unit/mcpgateway/services/test_prompt_service.py b/tests/unit/mcpgateway/services/test_prompt_service.py index 3ffa1cae4..8282f5c19 100644 --- a/tests/unit/mcpgateway/services/test_prompt_service.py +++ b/tests/unit/mcpgateway/services/test_prompt_service.py @@ -94,6 +94,8 @@ def _build_db_prompt( p.argument_schema = {"properties": {"name": {"type": "string"}}, "required": ["name"]} p.created_at = p.updated_at = datetime(2025, 1, 1, tzinfo=timezone.utc) p.is_active = is_active + # New model uses `enabled` — keep both attributes for backward compatibility in tests + p.enabled = is_active p.metrics = metrics or [] # validate_arguments: accept anything p.validate_arguments = Mock() @@ -366,6 +368,7 @@ async def test_toggle_prompt_status(self, prompt_service, test_db): p.team_id = 1 p.name = "hello" p.is_active = True + p.enabled = True test_db.get = Mock(return_value=p) test_db.commit = Mock() test_db.refresh = Mock() @@ -373,9 +376,9 @@ async def test_toggle_prompt_status(self, prompt_service, test_db): res = await prompt_service.toggle_prompt_status(test_db, 1, activate=False) - assert p.is_active is False + assert p.enabled is False prompt_service._notify_prompt_deactivated.assert_called_once() - assert res["is_active"] is False + assert res["enabled"] is False @pytest.mark.asyncio async def test_toggle_prompt_status_not_found(self, prompt_service, test_db): diff --git a/tests/unit/mcpgateway/services/test_prompt_service_extended.py b/tests/unit/mcpgateway/services/test_prompt_service_extended.py index 08b7aedd6..9cac19191 100644 --- a/tests/unit/mcpgateway/services/test_prompt_service_extended.py +++ b/tests/unit/mcpgateway/services/test_prompt_service_extended.py @@ -348,7 +348,7 @@ async def test_notify_prompt_methods(self): mock_prompt = MagicMock() mock_prompt.id = "test-id" mock_prompt.name = "test-prompt" - mock_prompt.is_active = True + mock_prompt.enabled = True # Test _notify_prompt_added await service._notify_prompt_added(mock_prompt) diff --git a/tests/unit/mcpgateway/test_main.py b/tests/unit/mcpgateway/test_main.py index e551327f1..1a27598b1 100644 --- a/tests/unit/mcpgateway/test_main.py +++ b/tests/unit/mcpgateway/test_main.py @@ -820,7 +820,7 @@ def test_delete_prompt_endpoint(self, mock_delete, test_client, auth_headers): def test_toggle_prompt_status(self, mock_toggle, test_client, auth_headers): """Test toggling prompt active/inactive status.""" mock_prompt = MagicMock() - mock_prompt.model_dump.return_value = {"id": 1, "is_active": False} + mock_prompt.model_dump.return_value = {"id": 1, "enabled": False} mock_toggle.return_value = mock_prompt response = test_client.post("/prompts/1/toggle?activate=false", headers=auth_headers) assert response.status_code == 200 @@ -897,7 +897,7 @@ def test_delete_prompt_endpoint(self, mock_delete, test_client, auth_headers): def test_toggle_prompt_status(self, mock_toggle, test_client, auth_headers): """Test toggling prompt active/inactive status.""" mock_prompt = MagicMock() - mock_prompt.model_dump.return_value = {"id": 1, "is_active": False} + mock_prompt.model_dump.return_value = {"id": 1, "enabled": False} mock_toggle.return_value = mock_prompt response = test_client.post("/prompts/1/toggle?activate=false", headers=auth_headers) assert response.status_code == 200 From 8586b8ee9411bec6c473e4bbe3ba953d9787f522 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 14:04:35 +0530 Subject: [PATCH 13/23] enabled Signed-off-by: rakdutta --- mcpgateway/admin.py | 24 ++++---- ...6f_uuid_change_for_prompt_and_resources.py | 28 +++++++-- mcpgateway/services/completion_service.py | 2 +- mcpgateway/services/resource_service.py | 58 +++++++++---------- mcpgateway/services/server_service.py | 54 ++++++++--------- 5 files changed, 91 insertions(+), 75 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 05189aa91..369bb4920 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -854,7 +854,7 @@ async def admin_list_servers( ... icon="test-icon.png", ... created_at=datetime.now(timezone.utc), ... updated_at=datetime.now(timezone.utc), - ... is_active=True, + ... enabled=True, ... associated_tools=["tool1", "tool2"], ... associated_resources=[1, 2], ... associated_prompts=[1], @@ -960,7 +960,7 @@ async def admin_get_server(server_id: str, db: Session = Depends(get_db), user=D ... icon="test-icon.png", ... created_at=datetime.now(timezone.utc), ... updated_at=datetime.now(timezone.utc), - ... is_active=True, + ... enabled=True, ... associated_tools=["tool1"], ... associated_resources=[1], ... associated_prompts=[1], @@ -1012,7 +1012,7 @@ async def admin_get_server(server_id: str, db: Session = Depends(get_db), user=D except ServerNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: - LOGGER.error(f"Error getting gateway {server_id}: {e}") + LOGGER.error(f"Error getting server {server_id}: {e}") raise e @@ -1721,7 +1721,7 @@ async def admin_list_resources( ... size=100, ... created_at=datetime.now(timezone.utc), ... updated_at=datetime.now(timezone.utc), - ... is_active=True, + ... enabled=True, ... metrics=ResourceMetrics( ... total_executions=5, successful_executions=5, failed_executions=0, ... failure_rate=0.0, min_response_time=0.1, max_response_time=0.5, @@ -1747,7 +1747,7 @@ async def admin_list_resources( ... id="39334ce0ed2644d79ede8913a66930c9", uri="test://resource/2", name="Inactive Resource", ... description="Another test", mime_type="application/json", size=50, ... created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), - ... is_active=False, metrics=ResourceMetrics( + ... enabled=False, metrics=ResourceMetrics( ... total_executions=0, successful_executions=0, failed_executions=0, ... failure_rate=0.0, min_response_time=0.0, max_response_time=0.0, ... avg_response_time=0.0, last_execution_time=None), @@ -2235,7 +2235,7 @@ async def admin_ui( True >>> >>> # Test with populated data (mocking a few items) - >>> mock_server = ServerRead(id="s1", name="S1", description="d", created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), is_active=True, associated_tools=[], associated_resources=[], associated_prompts=[], icon="i", metrics=ServerMetrics(total_executions=0, successful_executions=0, failed_executions=0, failure_rate=0.0, min_response_time=0.0, max_response_time=0.0, avg_response_time=0.0, last_execution_time=None)) + >>> mock_server = ServerRead(id="s1", name="S1", description="d", created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), enabled=True, associated_tools=[], associated_resources=[], associated_prompts=[], icon="i", metrics=ServerMetrics(total_executions=0, successful_executions=0, failed_executions=0, failure_rate=0.0, min_response_time=0.0, max_response_time=0.0, avg_response_time=0.0, last_execution_time=None)) >>> mock_tool = ToolRead( ... id="t1", name="T1", original_name="T1", url="http://t1.com", description="d", ... created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), @@ -6117,7 +6117,7 @@ async def admin_resources_partial_html( # Apply active/inactive filter if not include_inactive: - query = query.where(DbResource.is_active.is_(True)) + query = query.where(DbResource.enabled.is_(True)) # Access conditions: owner, team, public access_conditions = [DbResource.owner_email == user_email] @@ -6141,7 +6141,7 @@ async def admin_resources_partial_html( else: count_query = count_query.where(DbResource.gateway_id.in_(non_null_ids)) if not include_inactive: - count_query = count_query.where(DbResource.is_active.is_(True)) + count_query = count_query.where(DbResource.enabled.is_(True)) total_items = db.scalar(count_query) or 0 @@ -6326,7 +6326,7 @@ async def admin_get_all_resource_ids( LOGGER.debug(f"Filtering resources by gateway IDs: {non_null_ids}") if not include_inactive: - query = query.where(DbResource.is_active.is_(True)) + query = query.where(DbResource.enabled.is_(True)) access_conditions = [DbResource.owner_email == user_email, DbResource.visibility == "public"] if team_ids: @@ -6374,7 +6374,7 @@ async def admin_search_resources( query = select(DbResource.id, DbResource.name, DbResource.description) if not include_inactive: - query = query.where(DbResource.is_active.is_(True)) + query = query.where(DbResource.enabled.is_(True)) access_conditions = [DbResource.owner_email == user_email, DbResource.visibility == "public"] if team_ids: @@ -8115,7 +8115,7 @@ async def admin_get_resource(resource_id: str, db: Session = Depends(get_db), us >>> mock_resource = ResourceRead( ... id=resource_id, uri=resource_uri, name="Get Resource", description="Test", ... mime_type="text/plain", size=10, created_at=datetime.now(timezone.utc), - ... updated_at=datetime.now(timezone.utc), is_active=True, metrics=ResourceMetrics( + ... updated_at=datetime.now(timezone.utc), enabled=True, metrics=ResourceMetrics( ... total_executions=0, successful_executions=0, failed_executions=0, ... failure_rate=0.0, min_response_time=0.0, max_response_time=0.0, avg_response_time=0.0, ... last_execution_time=None @@ -12297,7 +12297,7 @@ async def get_servers_section( "name": server.name, "description": server.description, "tags": server.tags or [], - "isActive": server.is_active, + "isActive": server.enabled, "team_id": getattr(server, "team_id", None), "visibility": getattr(server, "visibility", "private"), } diff --git a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py index e90d272c1..c46962988 100644 --- a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py +++ b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py @@ -44,7 +44,7 @@ def upgrade() -> None: sa.Column("argument_schema", sa.JSON, nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("is_active", sa.Boolean, nullable=True), + sa.Column("enabled", sa.Boolean, nullable=True), sa.Column("tags", sa.JSON, nullable=False), sa.Column("created_by", sa.String(255), nullable=True), sa.Column("created_from_ip", sa.String(45), nullable=True), @@ -67,7 +67,7 @@ def upgrade() -> None: # 3) Copy data from prompts into prompts_tmp using id_new as id copy_cols = ( - "id, name, description, template, argument_schema, created_at, updated_at, is_active, tags," + "id, name, description, template, argument_schema, created_at, updated_at, enabled, tags," " created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip," " modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility" ) @@ -151,7 +151,7 @@ def upgrade() -> None: sa.Column("uri_template", sa.Text, nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("is_active", sa.Boolean, nullable=True), + sa.Column("enabled", sa.Boolean, nullable=True), sa.Column("tags", sa.JSON, nullable=False), sa.Column("text_content", sa.Text, nullable=True), sa.Column("binary_content", sa.LargeBinary, nullable=True), @@ -175,7 +175,7 @@ def upgrade() -> None: ) # Copy data into resources_tmp using id_new - res_copy_cols = "id, uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility" + res_copy_cols = "id, uri, name, description, mime_type, size, uri_template, created_at, updated_at, enabled, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility" conn.execute( text( ( @@ -255,6 +255,14 @@ def upgrade() -> None: op.rename_table("server_resource_association_tmp", "server_resource_association") op.rename_table("resource_subscriptions_tmp", "resource_subscriptions") + with op.batch_alter_table("servers") as batch_op: + batch_op.alter_column( + "is_active", + new_column_name="enabled", + existing_type=sa.Boolean(), + existing_server_default=sa.text("1"), + existing_nullable=False, + ) def downgrade() -> None: """Downgrade schema.""" @@ -296,7 +304,7 @@ def downgrade() -> None: # We'll preserve uniqueness by using the team_id/owner_email/name triple to later remap. conn.execute( text( - "INSERT INTO prompts_old (name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM prompts" + "INSERT INTO prompts_old (name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT name, description, template, argument_schema, created_at, updated_at, enabled, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM prompts" ) ) @@ -418,7 +426,7 @@ def downgrade() -> None: # 2) Insert rows from current resources into resources_old letting id autoincrement. conn.execute( text( - "INSERT INTO resources_old (uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM resources" + "INSERT INTO resources_old (uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT uri, name, description, mime_type, size, uri_template, created_at, updated_at, enabled, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM resources" ) ) @@ -519,3 +527,11 @@ def downgrade() -> None: op.rename_table("resource_metrics_old", "resource_metrics") op.rename_table("server_resource_association_old", "server_resource_association") op.rename_table("resource_subscriptions_old", "resource_subscriptions") + with op.batch_alter_table("servers") as batch_op: + batch_op.alter_column( + "enabled", + new_column_name="is_active", + existing_type=sa.Boolean(), + existing_server_default=sa.text("1"), + existing_nullable=False, + ) diff --git a/mcpgateway/services/completion_service.py b/mcpgateway/services/completion_service.py index 9e9f9c72a..4dfa74497 100644 --- a/mcpgateway/services/completion_service.py +++ b/mcpgateway/services/completion_service.py @@ -266,7 +266,7 @@ async def _complete_resource_uri(self, db: Session, ref: Dict[str, Any], arg_val raise CompletionError("Missing URI template") # List matching resources - resources = db.execute(select(DbResource).where(DbResource.is_active)).scalars().all() + resources = db.execute(select(DbResource).where(DbResource.enabled)).scalars().all() # Filter by URI pattern matches = [] diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index b2913d11a..222b53d11 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -86,21 +86,21 @@ class ResourceNotFoundError(ResourceError): class ResourceURIConflictError(ResourceError): """Raised when a resource URI conflicts with existing (active or inactive) resource.""" - def __init__(self, uri: str, is_active: bool = True, resource_id: Optional[int] = None, visibility: str = "public") -> None: + def __init__(self, uri: str, enabled: bool = True, resource_id: Optional[int] = None, visibility: str = "public") -> None: """Initialize the error with resource information. Args: uri: The conflicting resource URI - is_active: Whether the existing resource is active + enabled: Whether the existing resource is active resource_id: ID of the existing resource if available visibility: Visibility status of the resource """ self.uri = uri - self.is_active = is_active + self.enabled = enabled self.resource_id = resource_id message = f"{visibility.capitalize()} Resource already exists with URI: {uri}" logger.info(f"ResourceURIConflictError: {message}") - if not is_active: + if not enabled: message += f" (currently inactive, ID: {resource_id})" super().__init__(message) @@ -227,7 +227,7 @@ def _convert_resource_to_read(self, resource: DbResource) -> ResourceRead: >>> m2 = SimpleNamespace(is_success=False, response_time=0.3, timestamp=now) >>> r = SimpleNamespace( ... id="ca627760127d409080fdefc309147e08", uri='res://x', name='R', description=None, mime_type='text/plain', size=123, - ... created_at=now, updated_at=now, is_active=True, tags=[{"id": "t", "label": "T"}], metrics=[m1, m2] + ... created_at=now, updated_at=now, enabled=True, tags=[{"id": "t", "label": "T"}], metrics=[m1, m2] ... ) >>> out = svc._convert_resource_to_read(r) >>> out.metrics.total_executions @@ -349,12 +349,12 @@ async def register_resource( # Check for existing public resource with the same uri existing_resource = db.execute(select(DbResource).where(DbResource.uri == resource.uri, DbResource.visibility == "public")).scalar_one_or_none() if existing_resource: - raise ResourceURIConflictError(resource.uri, is_active=existing_resource.is_active, resource_id=existing_resource.id, visibility=existing_resource.visibility) + raise ResourceURIConflictError(resource.uri, enabled=existing_resource.enabled, resource_id=existing_resource.id, visibility=existing_resource.visibility) elif visibility.lower() == "team" and team_id: # Check for existing team resource with the same uri existing_resource = db.execute(select(DbResource).where(DbResource.uri == resource.uri, DbResource.visibility == "team", DbResource.team_id == team_id)).scalar_one_or_none() if existing_resource: - raise ResourceURIConflictError(resource.uri, is_active=existing_resource.is_active, resource_id=existing_resource.id, visibility=existing_resource.visibility) + raise ResourceURIConflictError(resource.uri, enabled=existing_resource.enabled, resource_id=existing_resource.id, visibility=existing_resource.visibility) # Detect mime type if not provided mime_type = resource.mime_type @@ -472,7 +472,7 @@ async def list_resources(self, db: Session, include_inactive: bool = False, curs query = query.where(DbResource.id > last_id) if not include_inactive: - query = query.where(DbResource.is_active) + query = query.where(DbResource.enabled) # Add tag filtering if tags are provided if tags: @@ -565,7 +565,7 @@ async def list_resources_for_user( # Apply active/inactive filter if not include_inactive: - query = query.where(DbResource.is_active) + query = query.where(DbResource.enabled) if team_id: if team_id not in team_ids: @@ -649,7 +649,7 @@ async def list_server_resources(self, db: Session, server_id: str, include_inact .where(server_resource_association.c.server_id == server_id) ) if not include_inactive: - query = query.where(DbResource.is_active) + query = query.where(DbResource.enabled) # Cursor-based pagination logic can be implemented here in the future. resources = db.execute(query).scalars().all() result = [] @@ -1274,7 +1274,7 @@ async def read_resource( # Matches uri (modified value from pluggins if applicable) # with uri from resource DB # if uri is of type resource template then resource is retreived from DB - query = select(DbResource).where(DbResource.uri == str(uri)).where(DbResource.is_active) + query = select(DbResource).where(DbResource.uri == str(uri)).where(DbResource.enabled) if include_inactive: query = select(DbResource).where(DbResource.uri == str(uri)) resource_db = db.execute(query).scalar_one_or_none() @@ -1283,7 +1283,7 @@ async def read_resource( content = resource_db.content else: # Check the inactivity first - check_inactivity = db.execute(select(DbResource).where(DbResource.uri == str(resource_uri)).where(not_(DbResource.is_active))).scalar_one_or_none() + check_inactivity = db.execute(select(DbResource).where(DbResource.uri == str(resource_uri)).where(not_(DbResource.enabled))).scalar_one_or_none() if check_inactivity: raise ResourceNotFoundError(f"Resource '{resource_uri}' exists but is inactive") @@ -1306,7 +1306,7 @@ async def read_resource( if resource_id: # if resource_id provided instead of resource_uri # retrieves resource based on resource_id - query = select(DbResource).where(DbResource.id == str(resource_id)).where(DbResource.is_active) + query = select(DbResource).where(DbResource.id == str(resource_id)).where(DbResource.enabled) if include_inactive: query = select(DbResource).where(DbResource.id == str(resource_id)) resource_db = db.execute(query).scalar_one_or_none() @@ -1314,7 +1314,7 @@ async def read_resource( original_uri = resource_db.uri or None content = resource_db.content else: - check_inactivity = db.execute(select(DbResource).where(DbResource.id == str(resource_id)).where(not_(DbResource.is_active))).scalar_one_or_none() + check_inactivity = db.execute(select(DbResource).where(DbResource.id == str(resource_id)).where(not_(DbResource.enabled))).scalar_one_or_none() if check_inactivity: raise ResourceNotFoundError(f"Resource '{resource_id}' exists but is inactive") raise ResourceNotFoundError(f"Resource not found for the resource id: {resource_id}") @@ -1449,8 +1449,8 @@ async def toggle_resource_status(self, db: Session, resource_id: int, activate: raise PermissionError("Only the owner can activate the Resource" if activate else "Only the owner can deactivate the Resource") # Update status if it's different - if resource.is_active != activate: - resource.is_active = activate + if resource.enabled != activate: + resource.enabled = activate resource.updated_at = datetime.now(timezone.utc) db.commit() db.refresh(resource) @@ -1494,11 +1494,11 @@ async def subscribe_resource(self, db: Session, subscription: ResourceSubscripti """ try: # Verify resource exists - resource = db.execute(select(DbResource).where(DbResource.uri == subscription.uri).where(DbResource.is_active)).scalar_one_or_none() + resource = db.execute(select(DbResource).where(DbResource.uri == subscription.uri).where(DbResource.enabled)).scalar_one_or_none() if not resource: # Check if inactive resource exists - inactive_resource = db.execute(select(DbResource).where(DbResource.uri == subscription.uri).where(not_(DbResource.is_active))).scalar_one_or_none() + inactive_resource = db.execute(select(DbResource).where(DbResource.uri == subscription.uri).where(not_(DbResource.enabled))).scalar_one_or_none() if inactive_resource: raise ResourceNotFoundError(f"Resource '{subscription.uri}' exists but is inactive") @@ -1618,12 +1618,12 @@ async def update_resource( # Check for existing public resources with the same uri existing_resource = db.execute(select(DbResource).where(DbResource.uri == resource_update.uri, DbResource.visibility == "public")).scalar_one_or_none() if existing_resource: - raise ResourceURIConflictError(resource_update.uri, is_active=existing_resource.is_active, resource_id=existing_resource.id, visibility=existing_resource.visibility) + raise ResourceURIConflictError(resource_update.uri, enabled=existing_resource.enabled, resource_id=existing_resource.id, visibility=existing_resource.visibility) elif visibility.lower() == "team" and team_id: # Check for existing team resource with the same uri existing_resource = db.execute(select(DbResource).where(DbResource.uri == resource_update.uri, DbResource.visibility == "team", DbResource.team_id == team_id)).scalar_one_or_none() if existing_resource: - raise ResourceURIConflictError(resource_update.uri, is_active=existing_resource.is_active, resource_id=existing_resource.id, visibility=existing_resource.visibility) + raise ResourceURIConflictError(resource_update.uri, enabled=existing_resource.enabled, resource_id=existing_resource.id, visibility=existing_resource.visibility) # Check ownership if user_email provided if user_email: @@ -1805,14 +1805,14 @@ async def get_resource_by_id(self, db: Session, resource_id: str, include_inacti query = select(DbResource).where(DbResource.id == resource_id) if not include_inactive: - query = query.where(DbResource.is_active) + query = query.where(DbResource.enabled) resource = db.execute(query).scalar_one_or_none() if not resource: if not include_inactive: # Check if inactive resource exists - inactive_resource = db.execute(select(DbResource).where(DbResource.id == resource_id).where(not_(DbResource.is_active))).scalar_one_or_none() + inactive_resource = db.execute(select(DbResource).where(DbResource.id == resource_id).where(not_(DbResource.enabled))).scalar_one_or_none() if inactive_resource: raise ResourceNotFoundError(f"Resource '{resource_id}' exists but is inactive") @@ -1834,7 +1834,7 @@ async def _notify_resource_activated(self, resource: DbResource) -> None: "id": resource.id, "uri": resource.uri, "name": resource.name, - "is_active": True, + "enabled": True, }, "timestamp": datetime.now(timezone.utc).isoformat(), } @@ -1853,7 +1853,7 @@ async def _notify_resource_deactivated(self, resource: DbResource) -> None: "id": resource.id, "uri": resource.uri, "name": resource.name, - "is_active": False, + "enabled": False, }, "timestamp": datetime.now(timezone.utc).isoformat(), } @@ -1886,7 +1886,7 @@ async def _notify_resource_removed(self, resource: DbResource) -> None: "id": resource.id, "uri": resource.uri, "name": resource.name, - "is_active": False, + "enabled": False, }, "timestamp": datetime.now(timezone.utc).isoformat(), } @@ -1952,7 +1952,7 @@ async def _read_template_resource(self, db: Session, uri: str, include_inactive: break if template: - check_inactivity = db.execute(select(DbResource).where(DbResource.id == str(template.id)).where(not_(DbResource.is_active))).scalar_one_or_none() + check_inactivity = db.execute(select(DbResource).where(DbResource.id == str(template.id)).where(not_(DbResource.enabled))).scalar_one_or_none() if check_inactivity: raise ResourceNotFoundError(f"Resource '{template.id}' exists but is inactive") else: @@ -2064,7 +2064,7 @@ async def _notify_resource_added(self, resource: DbResource) -> None: "uri": resource.uri, "name": resource.name, "description": resource.description, - "is_active": resource.is_active, + "enabled": resource.enabled, }, "timestamp": datetime.now(timezone.utc).isoformat(), } @@ -2083,7 +2083,7 @@ async def _notify_resource_updated(self, resource: DbResource) -> None: "id": resource.id, "uri": resource.uri, "content": resource.content, - "is_active": resource.is_active, + "enabled": resource.enabled, }, "timestamp": datetime.now(timezone.utc).isoformat(), } @@ -2126,7 +2126,7 @@ async def list_resource_templates(self, db: Session, include_inactive: bool = Fa """ query = select(DbResource).where(DbResource.uri_template.isnot(None)) if not include_inactive: - query = query.where(DbResource.is_active) + query = query.where(DbResource.enabled) # Cursor-based pagination logic can be implemented here in the future. templates = db.execute(query).scalars().all() result = [ResourceTemplate.model_validate(t) for t in templates] diff --git a/mcpgateway/services/server_service.py b/mcpgateway/services/server_service.py index 21686d774..9da0015a1 100644 --- a/mcpgateway/services/server_service.py +++ b/mcpgateway/services/server_service.py @@ -54,7 +54,7 @@ class ServerNotFoundError(ServerError): class ServerNameConflictError(ServerError): """Raised when a server name conflicts with an existing one.""" - def __init__(self, name: str, is_active: bool = True, server_id: Optional[str] = None, visibility: str = "public") -> None: + def __init__(self, name: str, enabled: bool = True, server_id: Optional[str] = None, visibility: str = "public") -> None: """ Initialize a ServerNameConflictError exception. @@ -68,7 +68,7 @@ def __init__(self, name: str, is_active: bool = True, server_id: Optional[str] = Args: name: The server name that caused the conflict. - is_active: Whether the conflicting server is currently active. Defaults to True. + enabled: Whether the conflicting server is currently active. Defaults to True. server_id: The ID of the conflicting server, if known. Only included in message for inactive servers. visibility: The visibility of the conflicting server (e.g., "public", "private", "team"). @@ -76,26 +76,26 @@ def __init__(self, name: str, is_active: bool = True, server_id: Optional[str] = >>> error = ServerNameConflictError("My Server") >>> str(error) 'Public Server already exists with name: My Server' - >>> error = ServerNameConflictError("My Server", is_active=False, server_id=123) + >>> error = ServerNameConflictError("My Server", enabled=False, server_id=123) >>> str(error) 'Public Server already exists with name: My Server (currently inactive, ID: 123)' - >>> error.is_active + >>> error.enabled False >>> error.server_id 123 - >>> error = ServerNameConflictError("My Server", is_active=False, visibility="team") + >>> error = ServerNameConflictError("My Server", enabled=False, visibility="team") >>> str(error) 'Team Server already exists with name: My Server (currently inactive, ID: None)' - >>> error.is_active + >>> error.enabled False >>> error.server_id is None True """ self.name = name - self.is_active = is_active + self.enabled = enabled self.server_id = server_id message = f"{visibility.capitalize()} Server already exists with name: {name}" - if not is_active: + if not enabled: message += f" (currently inactive, ID: {server_id})" super().__init__(message) @@ -208,7 +208,7 @@ def _convert_server_to_read(self, server: DbServer) -> ServerRead: >>> m2 = SimpleNamespace(is_success=False, response_time=0.4, timestamp=now) >>> server = SimpleNamespace( ... id='s1', name='S', description=None, icon=None, - ... created_at=now, updated_at=now, is_active=True, + ... created_at=now, updated_at=now, enabled=True, ... associated_tools=[], associated_resources=[], associated_prompts=[], associated_a2a_agents=[], ... tags=[], metrics=[m1, m2], ... tools=[], resources=[], prompts=[], a2a_agents=[] @@ -344,7 +344,7 @@ def _get_team_name(self, db: Session, team_id: Optional[str]) -> Optional[str]: """ if not team_id: return None - team = db.query(DbEmailTeam).filter(DbEmailTeam.id == team_id, DbEmailTeam.is_active.is_(True)).first() + team = db.query(DbEmailTeam).filter(DbEmailTeam.id == team_id, DbEmailTeam.enabled.is_(True)).first() return team.name if team else None async def register_server( @@ -417,7 +417,7 @@ async def register_server( name=server_in.name, description=server_in.description, icon=server_in.icon, - is_active=True, + enabled=True, tags=server_in.tags or [], # Team scoping fields - use schema values if provided, otherwise fallback to parameters team_id=getattr(server_in, "team_id", None) or team_id, @@ -435,12 +435,12 @@ async def register_server( # Check for existing public server with the same name existing_server = db.execute(select(DbServer).where(DbServer.name == server_in.name, DbServer.visibility == "public")).scalar_one_or_none() if existing_server: - raise ServerNameConflictError(server_in.name, is_active=existing_server.is_active, server_id=existing_server.id, visibility=existing_server.visibility) + raise ServerNameConflictError(server_in.name, enabled=existing_server.enabled, server_id=existing_server.id, visibility=existing_server.visibility) elif visibility.lower() == "team" and team_id: # Check for existing team server with the same name existing_server = db.execute(select(DbServer).where(DbServer.name == server_in.name, DbServer.visibility == "team", DbServer.team_id == team_id)).scalar_one_or_none() if existing_server: - raise ServerNameConflictError(server_in.name, is_active=existing_server.is_active, server_id=existing_server.id, visibility=existing_server.visibility) + raise ServerNameConflictError(server_in.name, enabled=existing_server.enabled, server_id=existing_server.id, visibility=existing_server.visibility) # Set custom UUID if provided if server_in.id: logger.info(f"Setting custom UUID for server: {server_in.id}") @@ -539,7 +539,7 @@ async def register_server( "icon": db_server.icon, "created_at": db_server.created_at, "updated_at": db_server.updated_at, - "is_active": db_server.is_active, + "enabled": db_server.enabled, "associated_tools": [str(tool.id) for tool in db_server.tools], "associated_resources": [str(resource.id) for resource in db_server.resources], "associated_prompts": [str(prompt.id) for prompt in db_server.prompts], @@ -586,7 +586,7 @@ async def list_servers(self, db: Session, include_inactive: bool = False, tags: """ query = select(DbServer) if not include_inactive: - query = query.where(DbServer.is_active) + query = query.where(DbServer.enabled) # Add tag filtering if tags are provided if tags: @@ -635,7 +635,7 @@ async def list_servers_for_user( # Apply active/inactive filter if not include_inactive: - query = query.where(DbServer.is_active) + query = query.where(DbServer.enabled) if team_id: if team_id not in team_ids: @@ -722,7 +722,7 @@ async def get_server(self, db: Session, server_id: str) -> ServerRead: "icon": server.icon, "created_at": server.created_at, "updated_at": server.updated_at, - "is_active": server.is_active, + "enabled": server.enabled, "associated_tools": [tool.name for tool in server.tools], "associated_resources": [res.id for res in server.resources], "associated_prompts": [prompt.id for prompt in server.prompts], @@ -810,12 +810,12 @@ async def update_server( # Check for existing public server with the same name existing_server = db.execute(select(DbServer).where(DbServer.name == server_update.name, DbServer.visibility == "public")).scalar_one_or_none() if existing_server: - raise ServerNameConflictError(server_update.name, is_active=existing_server.is_active, server_id=existing_server.id, visibility=existing_server.visibility) + raise ServerNameConflictError(server_update.name, enabled=existing_server.enabled, server_id=existing_server.id, visibility=existing_server.visibility) elif visibility.lower() == "team" and team_id: # Check for existing team server with the same name existing_server = db.execute(select(DbServer).where(DbServer.name == server_update.name, DbServer.visibility == "team", DbServer.team_id == team_id)).scalar_one_or_none() if existing_server: - raise ServerNameConflictError(server_update.name, is_active=existing_server.is_active, server_id=existing_server.id, visibility=existing_server.visibility) + raise ServerNameConflictError(server_update.name, enabled=existing_server.enabled, server_id=existing_server.id, visibility=existing_server.visibility) # Update simple fields if server_update.id is not None and server_update.id != server.id: @@ -934,7 +934,7 @@ async def update_server( "team": self._get_team_name(db, server.team_id), "created_at": server.created_at, "updated_at": server.updated_at, - "is_active": server.is_active, + "enabled": server.enabled, "associated_tools": [tool.id for tool in server.tools], "associated_resources": [res.id for res in server.resources], "associated_prompts": [prompt.id for prompt in server.prompts], @@ -1001,8 +1001,8 @@ async def toggle_server_status(self, db: Session, server_id: str, activate: bool if not await permission_service.check_resource_ownership(user_email, server): raise PermissionError("Only the owner can activate the Server" if activate else "Only the owner can deactivate the Server") - if server.is_active != activate: - server.is_active = activate + if server.enabled != activate: + server.enabled = activate server.updated_at = datetime.now(timezone.utc) db.commit() db.refresh(server) @@ -1020,7 +1020,7 @@ async def toggle_server_status(self, db: Session, server_id: str, activate: bool "team": self._get_team_name(db, server.team_id), "created_at": server.created_at, "updated_at": server.updated_at, - "is_active": server.is_active, + "enabled": server.enabled, "associated_tools": [tool.id for tool in server.tools], "associated_resources": [res.id for res in server.resources], "associated_prompts": [prompt.id for prompt in server.prompts], @@ -1131,7 +1131,7 @@ async def _notify_server_added(self, server: DbServer) -> None: "associated_tools": associated_tools, "associated_resources": associated_resources, "associated_prompts": associated_prompts, - "is_active": server.is_active, + "enabled": server.enabled, }, "timestamp": datetime.now(timezone.utc).isoformat(), } @@ -1157,7 +1157,7 @@ async def _notify_server_updated(self, server: DbServer) -> None: "associated_tools": associated_tools, "associated_resources": associated_resources, "associated_prompts": associated_prompts, - "is_active": server.is_active, + "enabled": server.enabled, }, "timestamp": datetime.now(timezone.utc).isoformat(), } @@ -1175,7 +1175,7 @@ async def _notify_server_activated(self, server: DbServer) -> None: "data": { "id": server.id, "name": server.name, - "is_active": True, + "enabled": True, }, "timestamp": datetime.now(timezone.utc).isoformat(), } @@ -1193,7 +1193,7 @@ async def _notify_server_deactivated(self, server: DbServer) -> None: "data": { "id": server.id, "name": server.name, - "is_active": False, + "enabled": False, }, "timestamp": datetime.now(timezone.utc).isoformat(), } From 6321141bab57f8f813ee6c4034a9c088c51c5082 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 14:39:51 +0530 Subject: [PATCH 14/23] resource enabled Signed-off-by: rakdutta --- mcpgateway/admin.py | 4 ++-- mcpgateway/schemas.py | 1 - mcpgateway/templates/resources_partial.html | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 369bb4920..07625a541 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -6160,7 +6160,7 @@ async def admin_resources_partial_html( except Exception as e: LOGGER.warning(f"Failed to convert resource {getattr(r, 'id', '')} to schema: {e}") continue - + LOGGER.info(f"resources_data partial::{resources_data}") data = jsonable_encoder(resources_data) # Build pagination metadata @@ -12187,7 +12187,7 @@ async def get_resources_section( "description": resource.description, "uri": resource.uri, "tags": resource.tags or [], - "isActive": resource.is_active, + "isActive": resource.enabled, "team_id": getattr(resource, "team_id", None), "visibility": getattr(resource, "visibility", "private"), } diff --git a/mcpgateway/schemas.py b/mcpgateway/schemas.py index 1fb9a87c7..8d771efa4 100644 --- a/mcpgateway/schemas.py +++ b/mcpgateway/schemas.py @@ -1780,7 +1780,6 @@ class ResourceRead(BaseModelWithConfigDict): size: Optional[int] created_at: datetime updated_at: datetime - #is_active: bool enabled: bool metrics: ResourceMetrics tags: List[Dict[str, str]] = Field(default_factory=list, description="Tags for categorizing the resource") diff --git a/mcpgateway/templates/resources_partial.html b/mcpgateway/templates/resources_partial.html index 0431c6b72..209df6880 100644 --- a/mcpgateway/templates/resources_partial.html +++ b/mcpgateway/templates/resources_partial.html @@ -27,13 +27,13 @@
{% if resource.ownerEmail %}{{ resource.ownerEmail }}{% else %}N/A{% endif %} {% if resource.team %}{{ resource.team.replace(' ', '
')|safe }}
{% else %}N/A{% endif %}
{% if resource.visibility == 'private' %}Private{% elif resource.visibility == 'team' %}Team{% elif resource.visibility == 'public' %}Public{% else %}N/A{% endif %}{{ 'Active' if resource.isActive else 'Inactive' }}{{ 'Active' if resource.enabled else 'Inactive' }}
- {% if resource.isActive %} + {% if resource.enabled %} From 822efa3b890b539dcc477009cf5830bf71bb347f Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 3 Dec 2025 11:44:16 +0530 Subject: [PATCH 15/23] change Signed-off-by: rakdutta --- mcpgateway/admin.py | 4 +--- mcpgateway/schemas.py | 4 ++-- mcpgateway/services/server_service.py | 17 ++++++++++++----- mcpgateway/templates/admin.html | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 07625a541..55e63b460 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -1145,8 +1145,7 @@ async def admin_add_server(request: Request, db: Session = Depends(get_db), user LOGGER.debug(f"User {get_user_email(user)} is adding a new server with name: {form['name']}") server_id = form.get("id") visibility = str(form.get("visibility", "private")) - LOGGER.info(f" user input id::{server_id}") - + # Handle "Select All" for tools associated_tools_list = form.getlist("associatedTools") if form.get("selectAllTools") == "true": @@ -6160,7 +6159,6 @@ async def admin_resources_partial_html( except Exception as e: LOGGER.warning(f"Failed to convert resource {getattr(r, 'id', '')} to schema: {e}") continue - LOGGER.info(f"resources_data partial::{resources_data}") data = jsonable_encoder(resources_data) # Build pagination metadata diff --git a/mcpgateway/schemas.py b/mcpgateway/schemas.py index 8d771efa4..ae0484a96 100644 --- a/mcpgateway/schemas.py +++ b/mcpgateway/schemas.py @@ -3714,8 +3714,8 @@ class ServerRead(BaseModelWithConfigDict): #is_active: bool enabled: bool associated_tools: List[str] = [] - associated_resources: List[int] = [] - associated_prompts: List[int] = [] + associated_resources: List[str] = [] + associated_prompts: List[str] = [] associated_a2a_agents: List[str] = [] metrics: ServerMetrics tags: List[Dict[str, str]] = Field(default_factory=list, description="Tags for categorizing the server") diff --git a/mcpgateway/services/server_service.py b/mcpgateway/services/server_service.py index 9da0015a1..d3c18af36 100644 --- a/mcpgateway/services/server_service.py +++ b/mcpgateway/services/server_service.py @@ -344,7 +344,7 @@ def _get_team_name(self, db: Session, team_id: Optional[str]) -> Optional[str]: """ if not team_id: return None - team = db.query(DbEmailTeam).filter(DbEmailTeam.id == team_id, DbEmailTeam.enabled.is_(True)).first() + team = db.query(DbEmailTeam).filter(DbEmailTeam.id == team_id, DbEmailTeam.is_active.is_(True)).first() return team.name if team else None async def register_server( @@ -412,6 +412,7 @@ async def register_server( 'server_read' """ try: + logger.info(f"Registering server: {server_in.name}") # # Create the new server record. db_server = DbServer( name=server_in.name, @@ -445,6 +446,7 @@ async def register_server( if server_in.id: logger.info(f"Setting custom UUID for server: {server_in.id}") db_server.id = server_in.id + logger.info(f"Adding server to DB session: {db_server.name}") db.add(db_server) # Associate tools, verifying each exists using bulk query when multiple items @@ -467,7 +469,7 @@ async def register_server( # Associate resources, verifying each exists using bulk query when multiple items if server_in.associated_resources: - resource_ids = [int(resource_id.strip()) for resource_id in server_in.associated_resources if resource_id.strip()] + resource_ids = [resource_id.strip() for resource_id in server_in.associated_resources if resource_id.strip()] if len(resource_ids) > 1: # Use bulk query for multiple items resources = db.execute(select(DbResource).where(DbResource.id.in_(resource_ids))).scalars().all() @@ -479,6 +481,11 @@ async def register_server( elif resource_ids: # Use single query for single item (maintains test compatibility) resource_obj = db.get(DbResource, resource_ids[0]) + # for resource_id in server_in.associated_resources: + # if resource_id.strip() == "": + # continue + # # Resource IDs are stored as string UUID hex values, not integers. + # resource_obj = db.get(DbResource, resource_id) if not resource_obj: raise ServerError(f"Resource with id {resource_ids[0]} does not exist.") db_server.resources.append(resource_obj) @@ -884,7 +891,7 @@ async def update_server( if server_update.associated_resources is not None: server.resources = [] if server_update.associated_resources: - resource_ids = [int(resource_id) for resource_id in server_update.associated_resources if resource_id] + resource_ids = [resource_id for resource_id in server_update.associated_resources if resource_id] if resource_ids: resources = db.execute(select(DbResource).where(DbResource.id.in_(resource_ids))).scalars().all() server.resources = list(resources) @@ -893,7 +900,7 @@ async def update_server( if server_update.associated_prompts is not None: server.prompts = [] if server_update.associated_prompts: - prompt_ids = [int(prompt_id) for prompt_id in server_update.associated_prompts if prompt_id] + prompt_ids = [prompt_id for prompt_id in server_update.associated_prompts if prompt_id] if prompt_ids: prompts = db.execute(select(DbPrompt).where(DbPrompt.id.in_(prompt_ids))).scalars().all() server.prompts = list(prompts) @@ -1025,7 +1032,7 @@ async def toggle_server_status(self, db: Session, server_id: str, activate: bool "associated_resources": [res.id for res in server.resources], "associated_prompts": [prompt.id for prompt in server.prompts], } - logger.debug(f"Server Data: {server_data}") + logger.info(f"Server Data: {server_data}") return self._convert_server_to_read(server) except PermissionError as e: raise e diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index e9a13d0b1..270883234 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -1975,7 +1975,7 @@

- {% if server.isActive %} + {% if server.enabled %} Date: Tue, 2 Dec 2025 18:53:43 +0530 Subject: [PATCH 16/23] list server res Signed-off-by: rakdutta --- mcpgateway/services/resource_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index 222b53d11..23840d615 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -642,10 +642,11 @@ async def list_server_resources(self, db: Session, server_id: str, include_inact >>> isinstance(result, list) True """ + logger.debug(f"Listing resources for server_id: {server_id}, include_inactive: {include_inactive}") query = ( select(DbResource) .join(server_resource_association, DbResource.id == server_resource_association.c.resource_id) - .where(DbResource.uri_template.is_(None)) + #.where(DbResource.uri_template.is_(None)) .where(server_resource_association.c.server_id == server_id) ) if not include_inactive: From c58ca1e646961661cf62888280fa66829626c724 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 19:16:43 +0530 Subject: [PATCH 17/23] doctest Signed-off-by: rakdutta --- mcpgateway/admin.py | 15 +++++++-------- mcpgateway/db.py | 6 +++--- mcpgateway/schemas.py | 4 ++-- mcpgateway/services/resource_service.py | 5 ++--- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index 55e63b460..cf8eda6fe 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -856,8 +856,8 @@ async def admin_list_servers( ... updated_at=datetime.now(timezone.utc), ... enabled=True, ... associated_tools=["tool1", "tool2"], - ... associated_resources=[1, 2], - ... associated_prompts=[1], + ... associated_resources=["1", "2"], + ... associated_prompts=["1"], ... metrics=mock_metrics ... ) >>> @@ -962,8 +962,8 @@ async def admin_get_server(server_id: str, db: Session = Depends(get_db), user=D ... updated_at=datetime.now(timezone.utc), ... enabled=True, ... associated_tools=["tool1"], - ... associated_resources=[1], - ... associated_prompts=[1], + ... associated_resources=["1"], + ... associated_prompts=["1"], ... metrics=mock_metrics ... ) >>> @@ -1143,9 +1143,8 @@ async def admin_add_server(request: Request, db: Session = Depends(get_db), user try: LOGGER.debug(f"User {get_user_email(user)} is adding a new server with name: {form['name']}") - server_id = form.get("id") visibility = str(form.get("visibility", "private")) - + # Handle "Select All" for tools associated_tools_list = form.getlist("associatedTools") if form.get("selectAllTools") == "true": @@ -1755,7 +1754,7 @@ async def admin_list_resources( >>> resource_service.list_resources_for_user = AsyncMock(return_value=[mock_resource, mock_inactive_resource]) >>> async def test_admin_list_resources_all(): ... result = await admin_list_resources(include_inactive=True, db=mock_db, user=mock_user) - ... return len(result) == 2 and not result[1]['isActive'] + ... return len(result) == 2 and not result[1]['enabled'] >>> >>> asyncio.run(test_admin_list_resources_all()) True @@ -1864,7 +1863,7 @@ async def admin_list_prompts( >>> prompt_service.list_prompts_for_user = AsyncMock(return_value=[mock_prompt, mock_inactive_prompt]) >>> async def test_admin_list_prompts_all(): ... result = await admin_list_prompts(include_inactive=True, db=mock_db, user=mock_user) - ... return len(result) == 2 and not result[1]['isActive'] + ... return len(result) == 2 and not result[1]['enabled'] >>> >>> asyncio.run(test_admin_list_prompts_all()) True diff --git a/mcpgateway/db.py b/mcpgateway/db.py index 66316fadd..f5289951d 100644 --- a/mcpgateway/db.py +++ b/mcpgateway/db.py @@ -2219,7 +2219,7 @@ class Resource(Base): uri_template: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # URI template for parameterized resources created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now) - #is_active: Mapped[bool] = mapped_column(default=True) + # is_active: Mapped[bool] = mapped_column(default=True) enabled: Mapped[bool] = mapped_column(default=True) tags: Mapped[List[str]] = mapped_column(JSON, default=list, nullable=False) @@ -2477,7 +2477,7 @@ class Prompt(Base): argument_schema: Mapped[Dict[str, Any]] = mapped_column(JSON) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now) - #is_active: Mapped[bool] = mapped_column(default=True) + # is_active: Mapped[bool] = mapped_column(default=True) enabled: Mapped[bool] = mapped_column(default=True) tags: Mapped[List[str]] = mapped_column(JSON, default=list, nullable=False) @@ -2669,7 +2669,7 @@ class Server(Base): icon: Mapped[Optional[str]] = mapped_column(String(767), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now) - #is_active: Mapped[bool] = mapped_column(default=True) + # is_active: Mapped[bool] = mapped_column(default=True) enabled: Mapped[bool] = mapped_column(default=True) tags: Mapped[List[str]] = mapped_column(JSON, default=list, nullable=False) diff --git a/mcpgateway/schemas.py b/mcpgateway/schemas.py index ae0484a96..33046f778 100644 --- a/mcpgateway/schemas.py +++ b/mcpgateway/schemas.py @@ -2287,7 +2287,7 @@ class PromptRead(BaseModelWithConfigDict): arguments: List[PromptArgument] created_at: datetime updated_at: datetime - #is_active: bool + # is_active: bool enabled: bool tags: List[Dict[str, str]] = Field(default_factory=list, description="Tags for categorizing the prompt") metrics: PromptMetrics @@ -3711,7 +3711,7 @@ class ServerRead(BaseModelWithConfigDict): icon: Optional[str] created_at: datetime updated_at: datetime - #is_active: bool + # is_active: bool enabled: bool associated_tools: List[str] = [] associated_resources: List[str] = [] diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index 23840d615..9381cf688 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -644,9 +644,8 @@ async def list_server_resources(self, db: Session, server_id: str, include_inact """ logger.debug(f"Listing resources for server_id: {server_id}, include_inactive: {include_inactive}") query = ( - select(DbResource) - .join(server_resource_association, DbResource.id == server_resource_association.c.resource_id) - #.where(DbResource.uri_template.is_(None)) + select(DbResource).join(server_resource_association, DbResource.id == server_resource_association.c.resource_id) + # .where(DbResource.uri_template.is_(None)) .where(server_resource_association.c.server_id == server_id) ) if not include_inactive: From ede035b8e606fec290cd42ea46b9fba32231eff0 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 19:42:15 +0530 Subject: [PATCH 18/23] alembic Signed-off-by: rakdutta --- ...6f_uuid_change_for_prompt_and_resources.py | 295 ++++++++++++++++-- 1 file changed, 264 insertions(+), 31 deletions(-) diff --git a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py index c46962988..f77d7062f 100644 --- a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py +++ b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py @@ -66,24 +66,123 @@ def upgrade() -> None: ) # 3) Copy data from prompts into prompts_tmp using id_new as id - copy_cols = ( - "id, name, description, template, argument_schema, created_at, updated_at, enabled, tags," - " created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip," - " modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility" + # Use SQLAlchemy Core to move rows from `prompts` -> `prompts_tmp` without + # composing SQL text. This avoids dynamic string concat while keeping the + # same column mapping (id_new -> id, is_active -> enabled). + prompts_src = sa.table( + "prompts", + sa.column("id_new"), + sa.column("name"), + sa.column("description"), + sa.column("template"), + sa.column("argument_schema"), + sa.column("created_at"), + sa.column("updated_at"), + sa.column("is_active"), + sa.column("tags"), + sa.column("created_by"), + sa.column("created_from_ip"), + sa.column("created_via"), + sa.column("created_user_agent"), + sa.column("modified_by"), + sa.column("modified_from_ip"), + sa.column("modified_via"), + sa.column("modified_user_agent"), + sa.column("import_batch_id"), + sa.column("federation_source"), + sa.column("version"), + sa.column("gateway_id"), + sa.column("team_id"), + sa.column("owner_email"), + sa.column("visibility"), ) - conn.execute( - text( - ( - "INSERT INTO prompts_tmp (" + copy_cols + ") " - "SELECT id_new, name, description, template, argument_schema, created_at, " - "updated_at, is_active, tags, created_by, created_from_ip, created_via, " - "created_user_agent, modified_by, modified_from_ip, modified_via, " - "modified_user_agent, import_batch_id, federation_source, version, gateway_id, " - "team_id, owner_email, visibility FROM prompts" - ) - ) + + prompts_tgt = sa.table( + "prompts_tmp", + sa.column("id"), + sa.column("name"), + sa.column("description"), + sa.column("template"), + sa.column("argument_schema"), + sa.column("created_at"), + sa.column("updated_at"), + sa.column("enabled"), + sa.column("tags"), + sa.column("created_by"), + sa.column("created_from_ip"), + sa.column("created_via"), + sa.column("created_user_agent"), + sa.column("modified_by"), + sa.column("modified_from_ip"), + sa.column("modified_via"), + sa.column("modified_user_agent"), + sa.column("import_batch_id"), + sa.column("federation_source"), + sa.column("version"), + sa.column("gateway_id"), + sa.column("team_id"), + sa.column("owner_email"), + sa.column("visibility"), ) + target_cols = [ + "id", + "name", + "description", + "template", + "argument_schema", + "created_at", + "updated_at", + "enabled", + "tags", + "created_by", + "created_from_ip", + "created_via", + "created_user_agent", + "modified_by", + "modified_from_ip", + "modified_via", + "modified_user_agent", + "import_batch_id", + "federation_source", + "version", + "gateway_id", + "team_id", + "owner_email", + "visibility", + ] + + select_exprs = [ + prompts_src.c.id_new, + prompts_src.c.name, + prompts_src.c.description, + prompts_src.c.template, + prompts_src.c.argument_schema, + prompts_src.c.created_at, + prompts_src.c.updated_at, + prompts_src.c.is_active, + prompts_src.c.tags, + prompts_src.c.created_by, + prompts_src.c.created_from_ip, + prompts_src.c.created_via, + prompts_src.c.created_user_agent, + prompts_src.c.modified_by, + prompts_src.c.modified_from_ip, + prompts_src.c.modified_via, + prompts_src.c.modified_user_agent, + prompts_src.c.import_batch_id, + prompts_src.c.federation_source, + prompts_src.c.version, + prompts_src.c.gateway_id, + prompts_src.c.team_id, + prompts_src.c.owner_email, + prompts_src.c.visibility, + ] + + stmt = sa.select(*select_exprs) + ins = sa.insert(prompts_tgt).from_select(target_cols, stmt) + conn.execute(ins) + # 4) Create new prompt_metrics table with prompt_id varchar(36) op.create_table( "prompt_metrics_tmp", @@ -174,22 +273,137 @@ def upgrade() -> None: sa.PrimaryKeyConstraint("id", name="pk_resources"), ) - # Copy data into resources_tmp using id_new - res_copy_cols = "id, uri, name, description, mime_type, size, uri_template, created_at, updated_at, enabled, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility" - conn.execute( - text( - ( - "INSERT INTO resources_tmp (" + res_copy_cols + ") " - "SELECT id_new, uri, name, description, mime_type, size, uri_template, " - "created_at, updated_at, is_active, tags, text_content, binary_content, " - "created_by, created_from_ip, created_via, created_user_agent, modified_by, " - "modified_from_ip, modified_via, modified_user_agent, import_batch_id, " - "federation_source, version, gateway_id, team_id, owner_email, visibility " - "FROM resources" - ) - ) + # Copy data into resources_tmp using id_new via SQLAlchemy Core + resources_src = sa.table( + "resources", + sa.column("id_new"), + sa.column("uri"), + sa.column("name"), + sa.column("description"), + sa.column("mime_type"), + sa.column("size"), + sa.column("uri_template"), + sa.column("created_at"), + sa.column("updated_at"), + sa.column("is_active"), + sa.column("tags"), + sa.column("text_content"), + sa.column("binary_content"), + sa.column("created_by"), + sa.column("created_from_ip"), + sa.column("created_via"), + sa.column("created_user_agent"), + sa.column("modified_by"), + sa.column("modified_from_ip"), + sa.column("modified_via"), + sa.column("modified_user_agent"), + sa.column("import_batch_id"), + sa.column("federation_source"), + sa.column("version"), + sa.column("gateway_id"), + sa.column("team_id"), + sa.column("owner_email"), + sa.column("visibility"), + ) + + resources_tgt = sa.table( + "resources_tmp", + sa.column("id"), + sa.column("uri"), + sa.column("name"), + sa.column("description"), + sa.column("mime_type"), + sa.column("size"), + sa.column("uri_template"), + sa.column("created_at"), + sa.column("updated_at"), + sa.column("enabled"), + sa.column("tags"), + sa.column("text_content"), + sa.column("binary_content"), + sa.column("created_by"), + sa.column("created_from_ip"), + sa.column("created_via"), + sa.column("created_user_agent"), + sa.column("modified_by"), + sa.column("modified_from_ip"), + sa.column("modified_via"), + sa.column("modified_user_agent"), + sa.column("import_batch_id"), + sa.column("federation_source"), + sa.column("version"), + sa.column("gateway_id"), + sa.column("team_id"), + sa.column("owner_email"), + sa.column("visibility"), ) + target_cols_res = [ + "id", + "uri", + "name", + "description", + "mime_type", + "size", + "uri_template", + "created_at", + "updated_at", + "enabled", + "tags", + "text_content", + "binary_content", + "created_by", + "created_from_ip", + "created_via", + "created_user_agent", + "modified_by", + "modified_from_ip", + "modified_via", + "modified_user_agent", + "import_batch_id", + "federation_source", + "version", + "gateway_id", + "team_id", + "owner_email", + "visibility", + ] + + select_exprs_res = [ + resources_src.c.id_new, + resources_src.c.uri, + resources_src.c.name, + resources_src.c.description, + resources_src.c.mime_type, + resources_src.c.size, + resources_src.c.uri_template, + resources_src.c.created_at, + resources_src.c.updated_at, + resources_src.c.is_active, + resources_src.c.tags, + resources_src.c.text_content, + resources_src.c.binary_content, + resources_src.c.created_by, + resources_src.c.created_from_ip, + resources_src.c.created_via, + resources_src.c.created_user_agent, + resources_src.c.modified_by, + resources_src.c.modified_from_ip, + resources_src.c.modified_via, + resources_src.c.modified_user_agent, + resources_src.c.import_batch_id, + resources_src.c.federation_source, + resources_src.c.version, + resources_src.c.gateway_id, + resources_src.c.team_id, + resources_src.c.owner_email, + resources_src.c.visibility, + ] + + stmt_res = sa.select(*select_exprs_res) + ins_res = sa.insert(resources_tgt).from_select(target_cols_res, stmt_res) + conn.execute(ins_res) + # resource_metrics_tmp with resource_id varchar(32) op.create_table( "resource_metrics_tmp", @@ -264,6 +478,7 @@ def upgrade() -> None: existing_nullable=False, ) + def downgrade() -> None: """Downgrade schema.""" conn = op.get_bind() @@ -304,7 +519,16 @@ def downgrade() -> None: # We'll preserve uniqueness by using the team_id/owner_email/name triple to later remap. conn.execute( text( - "INSERT INTO prompts_old (name, description, template, argument_schema, created_at, updated_at, is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT name, description, template, argument_schema, created_at, updated_at, enabled, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM prompts" + ( + "INSERT INTO prompts_old (name, description, template, argument_schema, created_at, updated_at, " + "is_active, tags, created_by, created_from_ip, created_via, created_user_agent, modified_by, " + "modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, " + "gateway_id, team_id, owner_email, visibility) " + "SELECT name, description, template, argument_schema, created_at, updated_at, enabled, tags, " + "created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, " + "modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, " + "team_id, owner_email, visibility FROM prompts" + ) ) ) @@ -426,7 +650,16 @@ def downgrade() -> None: # 2) Insert rows from current resources into resources_old letting id autoincrement. conn.execute( text( - "INSERT INTO resources_old (uri, name, description, mime_type, size, uri_template, created_at, updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) SELECT uri, name, description, mime_type, size, uri_template, created_at, updated_at, enabled, tags, text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility FROM resources" + ( + "INSERT INTO resources_old (uri, name, description, mime_type, size, uri_template, created_at, " + "updated_at, is_active, tags, text_content, binary_content, created_by, created_from_ip, " + "created_via, created_user_agent, modified_by, modified_from_ip, modified_via, modified_user_agent, " + "import_batch_id, federation_source, version, gateway_id, team_id, owner_email, visibility) " + "SELECT uri, name, description, mime_type, size, uri_template, created_at, updated_at, enabled, tags, " + "text_content, binary_content, created_by, created_from_ip, created_via, created_user_agent, modified_by, " + "modified_from_ip, modified_via, modified_user_agent, import_batch_id, federation_source, version, gateway_id, " + "team_id, owner_email, visibility FROM resources" + ) ) ) From ed82f7f54140443176db5ba48b28268b5e224278 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Tue, 2 Dec 2025 19:58:46 +0530 Subject: [PATCH 19/23] pytest Signed-off-by: rakdutta --- tests/e2e/test_main_apis.py | 6 +++--- tests/integration/test_integration.py | 4 ++-- .../unit/mcpgateway/services/test_export_service.py | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/e2e/test_main_apis.py b/tests/e2e/test_main_apis.py index e94ddaf88..bb8c6e29c 100644 --- a/tests/e2e/test_main_apis.py +++ b/tests/e2e/test_main_apis.py @@ -548,7 +548,7 @@ async def test_create_virtual_server(self, client: AsyncClient, mock_auth): assert result["description"] == server_data["server"]["description"] assert "id" in result # Check for the actual field name used in the response - assert result.get("is_active", True) is True # or whatever field indicates active status + assert result.get("enabled", True) is True # or whatever field indicates active status async def test_get_server(self, client: AsyncClient, mock_auth): """Test GET /servers/{server_id}.""" @@ -600,14 +600,14 @@ async def test_toggle_server_status(self, client: AsyncClient, mock_auth): assert "id" in result assert "name" in result # Check if server was deactivated - assert result.get("isActive") is False or result.get("is_active") is False + assert result.get("enabled") is False or result.get("enabled") is False # Reactivate the server response = await client.post(f"/servers/{server_id}/toggle?activate=true", headers=TEST_AUTH_HEADER) assert response.status_code == 200 result = response.json() - assert result.get("isActive") is True or result.get("is_active") is True + assert result.get("enabled") is True or result.get("enabled") is True async def test_delete_server(self, client: AsyncClient, mock_auth): """Test DELETE /servers/{server_id}.""" diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 016d4df30..a2471b572 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -201,7 +201,7 @@ def auth_headers() -> dict[str, str]: icon=None, created_at=datetime(2025, 1, 1), updated_at=datetime(2025, 1, 1), - is_active=True, + enabled=True, associated_tools=[MOCK_TOOL.id], associated_resources=[], associated_prompts=[], @@ -218,7 +218,7 @@ def auth_headers() -> dict[str, str]: size=5, created_at=datetime(2025, 1, 1), updated_at=datetime(2025, 1, 1), - is_active=True, + enabled=True, metrics=MOCK_METRICS, tags=[], ) diff --git a/tests/unit/mcpgateway/services/test_export_service.py b/tests/unit/mcpgateway/services/test_export_service.py index 6c2a839f9..0c60f803b 100644 --- a/tests/unit/mcpgateway/services/test_export_service.py +++ b/tests/unit/mcpgateway/services/test_export_service.py @@ -925,7 +925,7 @@ async def test_export_selective_all_entity_types(export_service, mock_db): icon=None, associated_tools=[], associated_a2a_agents=[], - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), metrics=create_default_server_metrics(), @@ -938,7 +938,7 @@ async def test_export_selective_all_entity_types(export_service, mock_db): template="Test template", description="Test prompt", arguments=[], - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), metrics=create_default_prompt_metrics(), @@ -952,7 +952,7 @@ async def test_export_selective_all_entity_types(export_service, mock_db): description="Test resource", mime_type="text/plain", size=None, - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), metrics=create_default_resource_metrics(), @@ -1070,7 +1070,7 @@ async def test_export_selected_servers(export_service, mock_db): icon=None, associated_tools=[], associated_a2a_agents=[], - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), metrics=create_default_server_metrics(), @@ -1108,7 +1108,7 @@ async def test_export_selected_prompts(export_service, mock_db): template="Test template", description="Test prompt", arguments=[], - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), metrics=create_default_prompt_metrics(), @@ -1147,7 +1147,7 @@ async def test_export_selected_resources(export_service, mock_db): description="Test resource", mime_type="text/plain", size=None, - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), metrics=create_default_resource_metrics(), From 5fcefb62407b74a13af53a3a9794c812329ea79b Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 3 Dec 2025 11:17:56 +0530 Subject: [PATCH 20/23] pytest Signed-off-by: rakdutta --- .../services/test_prompt_service_extended.py | 4 +- .../services/test_server_service.py | 60 +++++++++---------- .../test_display_name_uuid_features.py | 20 +++---- tests/unit/mcpgateway/test_main.py | 14 ++--- tests/unit/mcpgateway/test_main_extended.py | 4 +- tests/unit/mcpgateway/test_schemas.py | 22 +++---- 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/tests/unit/mcpgateway/services/test_prompt_service_extended.py b/tests/unit/mcpgateway/services/test_prompt_service_extended.py index 9cac19191..7ffd701e3 100644 --- a/tests/unit/mcpgateway/services/test_prompt_service_extended.py +++ b/tests/unit/mcpgateway/services/test_prompt_service_extended.py @@ -49,14 +49,14 @@ async def test_prompt_name_conflict_error_init(self): # Test active prompt conflict error = PromptNameConflictError("test_prompt") assert error.name == "test_prompt" - assert error.is_active is True + assert error.enabled is True assert error.prompt_id is None assert "test_prompt" in str(error) # Test inactive prompt conflict error_inactive = PromptNameConflictError("inactive_prompt", False, 123) assert error_inactive.name == "inactive_prompt" - assert error_inactive.is_active is False + assert error_inactive.enabled is False assert error_inactive.prompt_id == 123 assert "inactive_prompt" in str(error_inactive) assert "currently inactive, ID: 123" in str(error_inactive) diff --git a/tests/unit/mcpgateway/services/test_server_service.py b/tests/unit/mcpgateway/services/test_server_service.py index 90805893a..333cae283 100644 --- a/tests/unit/mcpgateway/services/test_server_service.py +++ b/tests/unit/mcpgateway/services/test_server_service.py @@ -80,7 +80,7 @@ def mock_server(mock_tool, mock_resource, mock_prompt): server.modified_by = "test_user" server.created_at = "2023-01-01T00:00:00" server.updated_at = "2023-01-01T00:00:00" - server.is_active = True + server.enabled = True # Ownership fields for RBAC server.owner_email = "user@example.com" # Match default test user @@ -215,7 +215,7 @@ def query_side_effect(model): icon="http://example.com/image.jpg", created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=[], associated_resources=[], associated_prompts=[], @@ -260,7 +260,7 @@ async def test_register_server(self, server_service, test_db, mock_tool, mock_re mock_db_server.modified_by = "test_user" mock_db_server.created_at = "2023-01-01T00:00:00" mock_db_server.updated_at = "2023-01-01T00:00:00" - mock_db_server.is_active = True + mock_db_server.enabled = True mock_db_server.metrics = [] # Create mock lists with append methods @@ -307,8 +307,8 @@ def capture_add(server): test_db.get = Mock( side_effect=lambda cls, _id: { (DbTool, "101"): mock_tool, - (DbResource, 201): mock_resource, - (DbPrompt, 301): mock_prompt, + (DbResource, "201"): mock_resource, + (DbPrompt, "301"): mock_prompt, }.get((cls, _id)) ) @@ -322,10 +322,10 @@ def capture_add(server): icon="server-icon", created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=["101"], - associated_resources=[201], - associated_prompts=[301], + associated_resources=["201"], + associated_prompts=["301"], metrics={ "total_executions": 0, "successful_executions": 0, @@ -359,8 +359,8 @@ def capture_add(server): assert result.name == "test_server" assert "101" in result.associated_tools - assert 201 in result.associated_resources - assert 301 in result.associated_prompts + assert "201" in result.associated_resources + assert "301" in result.associated_prompts @pytest.mark.asyncio async def test_register_server_name_conflict(self, server_service, mock_server, test_db): @@ -428,10 +428,10 @@ async def test_list_servers(self, server_service, mock_server, test_db): icon="http://example.com/image.jgp", created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=["101"], - associated_resources=[201], - associated_prompts=[301], + associated_resources=["201"], + associated_prompts=["301"], metrics={ "total_executions": 0, "successful_executions": 0, @@ -464,10 +464,10 @@ async def test_get_server(self, server_service, mock_server, test_db): icon="http://example.com/image.jpg", created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=["101"], - associated_resources=[201], - associated_prompts=[301], + associated_resources=["201"], + associated_prompts=["301"], metrics={ "total_executions": 0, "successful_executions": 0, @@ -568,10 +568,10 @@ async def test_update_server(self, server_service, mock_server, test_db, mock_to icon="http://example.com/image.jpg", created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=["102"], - associated_resources=[202], - associated_prompts=[302], + associated_resources=["202"], + associated_prompts=["302"], metrics={ "total_executions": 0, "successful_executions": 0, @@ -660,7 +660,7 @@ async def test_update_server_name_conflict(self, server_service, mock_server, te server_team.visibility = "team" server_team.team_id = "teamA" - conflict_team_server = types.SimpleNamespace(id="3", name="existing_server", is_active=True, visibility="team", team_id="teamA") + conflict_team_server = types.SimpleNamespace(id="3", name="existing_server", enabled=True, visibility="team", team_id="teamA") test_db.get = Mock(return_value=server_team) mock_scalar = Mock() @@ -688,7 +688,7 @@ async def test_update_server_name_conflict(self, server_service, mock_server, te server_public.visibility = "public" server_public.team_id = None - conflict_public_server = types.SimpleNamespace(id="5", name="existing_server", is_active=True, visibility="public", team_id=None) + conflict_public_server = types.SimpleNamespace(id="5", name="existing_server", enabled=True, visibility="public", team_id=None) test_db.get = Mock(return_value=server_public) mock_scalar = Mock() @@ -727,10 +727,10 @@ async def test_toggle_server_status(self, server_service, mock_server, test_db): icon="server-icon", created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=False, + enabled=False, associated_tools=["101"], - associated_resources=[201], - associated_prompts=[301], + associated_resources=["201"], + associated_prompts=["301"], metrics={ "total_executions": 0, "successful_executions": 0, @@ -750,7 +750,7 @@ async def test_toggle_server_status(self, server_service, mock_server, test_db): test_db.commit.assert_called_once() test_db.refresh.assert_called_once() server_service._notify_server_deactivated.assert_called_once() - assert result.is_active is False + assert result.enabled is False # --------------------------- delete -------------------------------- # @pytest.mark.asyncio @@ -821,7 +821,7 @@ def capture_add(server): icon=None, created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=[], associated_resources=[], associated_prompts=[], @@ -892,7 +892,7 @@ def capture_add(server): icon=None, created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=[], associated_resources=[], associated_prompts=[], @@ -956,7 +956,7 @@ def capture_add(server): icon=None, created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=[], associated_resources=[], associated_prompts=[], @@ -1018,7 +1018,7 @@ async def test_update_server_uuid_normalization(self, server_service, test_db): existing_server = MagicMock(spec=DbServer) existing_server.id = "oldserverid" existing_server.name = "Old Name" - existing_server.is_active = True + existing_server.enabled = True existing_server.tools = [] existing_server.resources = [] existing_server.prompts = [] @@ -1053,7 +1053,7 @@ async def test_update_server_uuid_normalization(self, server_service, test_db): icon=None, created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=[], associated_resources=[], associated_prompts=[], diff --git a/tests/unit/mcpgateway/test_display_name_uuid_features.py b/tests/unit/mcpgateway/test_display_name_uuid_features.py index f6b9b40ce..906e304e4 100644 --- a/tests/unit/mcpgateway/test_display_name_uuid_features.py +++ b/tests/unit/mcpgateway/test_display_name_uuid_features.py @@ -160,7 +160,7 @@ def test_server_create_with_custom_uuid(self, db_session): custom_uuid = "12345678-1234-1234-1234-123456789abc" # Create server with custom UUID - db_server = DbServer(id=custom_uuid, name="Test Server", description="Test server with custom UUID", is_active=True) + db_server = DbServer(id=custom_uuid, name="Test Server", description="Test server with custom UUID", enabled=True) db_session.add(db_server) db_session.commit() @@ -173,7 +173,7 @@ def test_server_create_with_custom_uuid(self, db_session): def test_server_create_without_uuid(self, db_session): """Test creating a server without specifying UUID (auto-generated).""" # Create server without specifying UUID - db_server = DbServer(name="Auto UUID Server", description="Test server with auto UUID", is_active=True) + db_server = DbServer(name="Auto UUID Server", description="Test server with auto UUID", enabled=True) db_session.add(db_server) db_session.commit() @@ -190,7 +190,7 @@ def test_server_update_uuid(self, db_session): new_uuid = "new-uuid-5678" # Create server with original UUID - db_server = DbServer(id=original_uuid, name="UUID Update Server", description="Test server for UUID update", is_active=True) + db_server = DbServer(id=original_uuid, name="UUID Update Server", description="Test server for UUID update", enabled=True) db_session.add(db_server) db_session.commit() @@ -211,12 +211,12 @@ def test_server_uuid_uniqueness(self, db_session): duplicate_uuid = "duplicate-uuid-1234" # Create first server with UUID - db_server1 = DbServer(id=duplicate_uuid, name="First Server", description="First server", is_active=True) + db_server1 = DbServer(id=duplicate_uuid, name="First Server", description="First server", enabled=True) db_session.add(db_server1) db_session.commit() # Try to create second server with same UUID - db_server2 = DbServer(id=duplicate_uuid, name="Second Server", description="Second server", is_active=True) + db_server2 = DbServer(id=duplicate_uuid, name="Second Server", description="Second server", enabled=True) db_session.add(db_server2) @@ -333,7 +333,7 @@ def capture_add(server): icon=None, created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=[], associated_resources=[], associated_prompts=[], @@ -401,7 +401,7 @@ def capture_add(server): icon=None, created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=[], associated_resources=[], associated_prompts=[], @@ -465,7 +465,7 @@ def capture_add(server): icon=None, created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=[], associated_resources=[], associated_prompts=[], @@ -561,7 +561,7 @@ def test_database_storage_format_verification(self, db_session): id=expected_hex, # Simulate the normalized UUID name="Storage Test Server", description="Test UUID storage format", - is_active=True, + enabled=True, ) db_session.add(db_server) @@ -616,7 +616,7 @@ def capture_add(server): icon=None, created_at="2023-01-01T00:00:00", updated_at="2023-01-01T00:00:00", - is_active=True, + enabled=True, associated_tools=[], associated_resources=[], associated_prompts=[], diff --git a/tests/unit/mcpgateway/test_main.py b/tests/unit/mcpgateway/test_main.py index 1a27598b1..94e0beb80 100644 --- a/tests/unit/mcpgateway/test_main.py +++ b/tests/unit/mcpgateway/test_main.py @@ -55,10 +55,10 @@ "icon": "server-icon", "created_at": "2023-01-01T00:00:00+00:00", "updated_at": "2023-01-01T00:00:00+00:00", - "is_active": True, + "enabled": True, "associated_tools": ["101"], - "associated_resources": [201], - "associated_prompts": [301], + "associated_resources": ["201"], + "associated_prompts": ["301"], "metrics": MOCK_METRICS, } @@ -125,7 +125,7 @@ def camel_to_snake_tool(d: dict) -> dict: "size": 12, "created_at": "2023-01-01T00:00:00+00:00", "updated_at": "2023-01-01T00:00:00+00:00", - "is_active": True, + "enabled": True, "metrics": MOCK_METRICS, } @@ -137,7 +137,7 @@ def camel_to_snake_tool(d: dict) -> dict: "arguments": [], "created_at": "2023-01-01T00:00:00+00:00", "updated_at": "2023-01-01T00:00:00+00:00", - "is_active": True, + "enabled": True, "metrics": MOCK_METRICS, } @@ -505,7 +505,7 @@ def test_update_server_endpoint(self, mock_update, test_client, auth_headers): def test_toggle_server_status(self, mock_toggle, test_client, auth_headers): """Test toggling server active/inactive status.""" updated_server = MOCK_SERVER_READ.copy() - updated_server["is_active"] = False + updated_server["enabled"] = False mock_toggle.return_value = ServerRead(**updated_server) response = test_client.post("/servers/1/toggle?activate=false", headers=auth_headers) assert response.status_code == 200 @@ -740,7 +740,7 @@ def test_list_resource_templates(self, mock_list, test_client, auth_headers): def test_toggle_resource_status(self, mock_toggle, test_client, auth_headers): """Test toggling resource active/inactive status.""" mock_resource = MagicMock() - mock_resource.model_dump.return_value = {"id": 1, "is_active": False} + mock_resource.model_dump.return_value = {"id": "1", "enabled": False} mock_toggle.return_value = mock_resource response = test_client.post("/resources/1/toggle?activate=false", headers=auth_headers) assert response.status_code == 200 diff --git a/tests/unit/mcpgateway/test_main_extended.py b/tests/unit/mcpgateway/test_main_extended.py index 5dfa2d5c7..ceb40f763 100644 --- a/tests/unit/mcpgateway/test_main_extended.py +++ b/tests/unit/mcpgateway/test_main_extended.py @@ -290,7 +290,7 @@ def test_server_toggle_edge_cases(self, test_client, auth_headers): "icon": None, "created_at": "2023-01-01T00:00:00+00:00", "updated_at": "2023-01-01T00:00:00+00:00", - "is_active": True, + "enabled": True, "associated_tools": [], "associated_resources": [], "associated_prompts": [], @@ -313,7 +313,7 @@ def test_server_toggle_edge_cases(self, test_client, auth_headers): assert response.status_code == 200 # Test activate=false - mock_server_data["is_active"] = False + mock_server_data["enabled"] = False mock_toggle.return_value = ServerRead(**mock_server_data) response = test_client.post("/servers/1/toggle?activate=false", headers=auth_headers) assert response.status_code == 200 diff --git a/tests/unit/mcpgateway/test_schemas.py b/tests/unit/mcpgateway/test_schemas.py index 1d782d44e..fac95123f 100644 --- a/tests/unit/mcpgateway/test_schemas.py +++ b/tests/unit/mcpgateway/test_schemas.py @@ -770,10 +770,10 @@ def test_server_read(self): icon="http://example.com/server.png", created_at=one_hour_ago, updated_at=now, - is_active=True, + enabled=True, associated_tools=["1", "2", "3"], - associated_resources=[4, 5], - associated_prompts=[6], + associated_resources=["4", "5"], + associated_prompts=["6"], metrics=ServerMetrics( total_executions=100, successful_executions=95, @@ -792,10 +792,10 @@ def test_server_read(self): assert server.icon == "http://example.com/server.png" assert server.created_at == one_hour_ago assert server.updated_at == now - assert server.is_active is True + assert server.enabled is True assert server.associated_tools == ["1", "2", "3"] - assert server.associated_resources == [4, 5] - assert server.associated_prompts == [6] + assert server.associated_resources == ["4", "5"] + assert server.associated_prompts == ["6"] assert server.metrics.total_executions == 100 assert server.metrics.successful_executions == 95 @@ -807,10 +807,10 @@ def test_server_read(self): icon="http://example.com/object_server.png", created_at=one_hour_ago, updated_at=now, - is_active=True, + enabled=True, associated_tools=[Mock(id="10"), Mock(id="11")], - associated_resources=[Mock(id=12)], - associated_prompts=[Mock(id=13)], + associated_resources=[Mock(id="12")], + associated_prompts=[Mock(id="13")], metrics=ServerMetrics( total_executions=10, successful_executions=10, @@ -820,8 +820,8 @@ def test_server_read(self): ) assert server_with_objects.associated_tools == ["10", "11"] - assert server_with_objects.associated_resources == [12] - assert server_with_objects.associated_prompts == [13] + assert server_with_objects.associated_resources == ["12"] + assert server_with_objects.associated_prompts == ["13"] class TestToggleAndListSchemas: From cef8abb5eea24c873559a18bfa52131933ecbefe Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 3 Dec 2025 11:59:28 +0530 Subject: [PATCH 21/23] conflict resolve Signed-off-by: rakdutta --- mcpgateway/services/server_service.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mcpgateway/services/server_service.py b/mcpgateway/services/server_service.py index d3c18af36..e7f8aae4d 100644 --- a/mcpgateway/services/server_service.py +++ b/mcpgateway/services/server_service.py @@ -481,18 +481,13 @@ async def register_server( elif resource_ids: # Use single query for single item (maintains test compatibility) resource_obj = db.get(DbResource, resource_ids[0]) - # for resource_id in server_in.associated_resources: - # if resource_id.strip() == "": - # continue - # # Resource IDs are stored as string UUID hex values, not integers. - # resource_obj = db.get(DbResource, resource_id) if not resource_obj: raise ServerError(f"Resource with id {resource_ids[0]} does not exist.") db_server.resources.append(resource_obj) # Associate prompts, verifying each exists using bulk query when multiple items if server_in.associated_prompts: - prompt_ids = [int(prompt_id.strip()) for prompt_id in server_in.associated_prompts if prompt_id.strip()] + prompt_ids = [prompt_id.strip() for prompt_id in server_in.associated_prompts if prompt_id.strip()] if len(prompt_ids) > 1: # Use bulk query for multiple items prompts = db.execute(select(DbPrompt).where(DbPrompt.id.in_(prompt_ids))).scalars().all() From 13210c1c2c2d5632455635accc861d6de708dfe1 Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 3 Dec 2025 12:40:11 +0530 Subject: [PATCH 22/23] isort Signed-off-by: rakdutta --- .../356a2d4eed6f_uuid_change_for_prompt_and_resources.py | 6 +++--- mcpgateway/services/resource_service.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py index f77d7062f..325096243 100644 --- a/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py +++ b/mcpgateway/alembic/versions/356a2d4eed6f_uuid_change_for_prompt_and_resources.py @@ -6,15 +6,15 @@ """ +# Standard from typing import Sequence, Union +import uuid +# Third-Party from alembic import op import sqlalchemy as sa -import uuid - from sqlalchemy import text - # revision identifiers, used by Alembic. revision: str = "356a2d4eed6f" down_revision: Union[str, Sequence[str], None] = "9e028ecf59c4" diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index 9381cf688..ced8901ba 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -644,8 +644,9 @@ async def list_server_resources(self, db: Session, server_id: str, include_inact """ logger.debug(f"Listing resources for server_id: {server_id}, include_inactive: {include_inactive}") query = ( - select(DbResource).join(server_resource_association, DbResource.id == server_resource_association.c.resource_id) - # .where(DbResource.uri_template.is_(None)) + select(DbResource) + .join(server_resource_association, DbResource.id == server_resource_association.c.resource_id) + .where(DbResource.uri_template.is_(None)) .where(server_resource_association.c.server_id == server_id) ) if not include_inactive: From 6ddf903ba2b69946a23406f56d66d66176d7960f Mon Sep 17 00:00:00 2001 From: rakdutta Date: Wed, 3 Dec 2025 13:25:21 +0530 Subject: [PATCH 23/23] pytest Signed-off-by: rakdutta --- .../services/test_resource_service.py | 30 +++++++++---------- tests/unit/mcpgateway/test_admin.py | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/unit/mcpgateway/services/test_resource_service.py b/tests/unit/mcpgateway/services/test_resource_service.py index bb5ccfd2d..632b35cc1 100644 --- a/tests/unit/mcpgateway/services/test_resource_service.py +++ b/tests/unit/mcpgateway/services/test_resource_service.py @@ -73,7 +73,7 @@ def mock_resource(): resource.text_content = "Test content" resource.binary_content = None resource.size = 12 - resource.is_active = True + resource.enabled = True resource.created_by = "test_user" resource.modified_by = "test_user" resource.created_at = datetime.now(timezone.utc) @@ -110,7 +110,7 @@ def mock_resource_template(): resource.text_content = "Test content" resource.binary_content = None resource.size = 12 - resource.is_active = True + resource.enabled = True resource.created_by = "test_user" resource.modified_by = "test_user" resource.created_at = datetime.now(timezone.utc) @@ -147,7 +147,7 @@ def mock_inactive_resource(): resource.text_content = None resource.binary_content = None resource.size = 0 - resource.is_active = False + resource.enabled = False resource.created_by = "test_user" resource.modified_by = "test_user" resource.created_at = datetime.now(timezone.utc) @@ -230,7 +230,7 @@ async def test_register_resource_success(self, resource_service, mock_db, sample description=sample_resource_create.description or "", mime_type="text/plain", size=len(sample_resource_create.content), - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), template=None, @@ -346,7 +346,7 @@ async def test_register_resource_binary_content(self, resource_service, mock_db) description=binary_resource.description or "", mime_type="application/octet-stream", size=len(binary_resource.content), - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), template=None, @@ -552,7 +552,7 @@ async def test_toggle_resource_status_activate(self, resource_service, mock_db, description=mock_inactive_resource.description or "", mime_type=mock_inactive_resource.mime_type or "text/plain", size=mock_inactive_resource.size or 0, - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), template=None, @@ -570,7 +570,7 @@ async def test_toggle_resource_status_activate(self, resource_service, mock_db, result = await resource_service.toggle_resource_status(mock_db, 2, activate=True) - assert mock_inactive_resource.is_active is True + assert mock_inactive_resource.enabled is True mock_db.commit.assert_called_once() @pytest.mark.asyncio @@ -586,7 +586,7 @@ async def test_toggle_resource_status_deactivate(self, resource_service, mock_db description=mock_resource.description, mime_type=mock_resource.mime_type, size=mock_resource.size, - is_active=False, + enabled=False, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), template=None, @@ -604,7 +604,7 @@ async def test_toggle_resource_status_deactivate(self, resource_service, mock_db result = await resource_service.toggle_resource_status(mock_db, 1, activate=False) - assert mock_resource.is_active is False + assert mock_resource.enabled is False mock_db.commit.assert_called_once() @pytest.mark.asyncio @@ -622,7 +622,7 @@ async def test_toggle_resource_status_not_found(self, resource_service, mock_db) async def test_toggle_resource_status_no_change(self, resource_service, mock_db, mock_resource): """Test toggling status when no change needed.""" mock_db.get.return_value = mock_resource - mock_resource.is_active = True + mock_resource.enabled = True with patch.object(resource_service, "_convert_resource_to_read") as mock_convert: mock_convert.return_value = ResourceRead( @@ -632,7 +632,7 @@ async def test_toggle_resource_status_no_change(self, resource_service, mock_db, description=mock_resource.description, mime_type=mock_resource.mime_type, size=mock_resource.size, - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), template=None, @@ -673,7 +673,7 @@ async def test_update_resource_success(self, resource_service, mock_db, mock_res description="Updated description", mime_type="text/plain", size=15, # length of "Updated content" - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), template=None, @@ -746,7 +746,7 @@ async def test_update_resource_binary_content(self, resource_service, mock_db, m description=mock_resource.description, mime_type="application/octet-stream", size=len(b"new binary content"), - is_active=True, + enabled=True, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), template=None, @@ -1355,7 +1355,7 @@ async def test_notify_resource_activated(self, resource_service, mock_resource): resource_service._event_service.publish_event.assert_called_once() call_args = resource_service._event_service.publish_event.call_args[0][0] assert call_args["type"] == "resource_activated" - assert call_args["data"]["is_active"] is True + assert call_args["data"]["enabled"] is True @pytest.mark.asyncio async def test_notify_resource_deactivated(self, resource_service, mock_resource): @@ -1367,7 +1367,7 @@ async def test_notify_resource_deactivated(self, resource_service, mock_resource resource_service._event_service.publish_event.assert_called_once() call_args = resource_service._event_service.publish_event.call_args[0][0] assert call_args["type"] == "resource_deactivated" - assert call_args["data"]["is_active"] is False + assert call_args["data"]["enabled"] is False @pytest.mark.asyncio async def test_notify_resource_deleted(self, resource_service): diff --git a/tests/unit/mcpgateway/test_admin.py b/tests/unit/mcpgateway/test_admin.py index 7658feb0c..21b5791f1 100644 --- a/tests/unit/mcpgateway/test_admin.py +++ b/tests/unit/mcpgateway/test_admin.py @@ -920,7 +920,7 @@ async def test_admin_get_prompt_with_detailed_metrics(self, mock_get_prompt_deta "arguments": [{"name": "var", "type": "string"}], "created_at": datetime.now(timezone.utc), "updated_at": datetime.now(timezone.utc), - "is_active": True, + "enabled": True, "metrics": { "total_executions": 1000, "successful_executions": 950,