diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eadcbcb4..7c191ec4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -584,7 +584,7 @@ This release focuses on **Advanced OAuth Integration, Plugin Ecosystem, MCP Regi - `GET /grpc` - List all gRPC services with team filtering - `GET /grpc/{id}` - Get service details - `PUT /grpc/{id}` - Update service configuration - - `POST /grpc/{id}/toggle` - Enable/disable service + - `POST /grpc/{id}/state` - Enable/disable service - `POST /grpc/{id}/delete` - Delete service - `POST /grpc/{id}/reflect` - Re-trigger service discovery - `GET /grpc/{id}/methods` - List discovered methods diff --git a/README.md b/README.md index a0d8dab20..bb342e1c3 100644 --- a/README.md +++ b/README.md @@ -2264,9 +2264,9 @@ curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ # Toggle active status curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/tools/1/toggle?activate=false + http://localhost:4444/tools/1/state?activate=false curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/tools/1/toggle?activate=true + http://localhost:4444/tools/1/state?activate=true # Delete tool curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/tools/1 @@ -2326,7 +2326,7 @@ curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ # Toggle agent status curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/a2a/agent-id/toggle?activate=false + http://localhost:4444/a2a/agent-id/state?activate=false # Delete agent curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ @@ -2376,7 +2376,7 @@ curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ # Toggle active status curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/gateways/1/toggle?activate=false + http://localhost:4444/gateways/1/state?activate=false # Delete gateway curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/gateways/1 @@ -2462,7 +2462,7 @@ curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ # Toggle active curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/prompts/5/toggle?activate=false + http://localhost:4444/prompts/5/state?activate=false # Delete prompt curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/prompts/greet @@ -2520,7 +2520,7 @@ curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ # Toggle active curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/servers/UUID_OF_SERVER_1/toggle?activate=false + http://localhost:4444/servers/UUID_OF_SERVER_1/state?activate=false ``` diff --git a/docs/docs/architecture/plugins/gateway-hooks.md b/docs/docs/architecture/plugins/gateway-hooks.md index f2c79e830..65b057105 100644 --- a/docs/docs/architecture/plugins/gateway-hooks.md +++ b/docs/docs/architecture/plugins/gateway-hooks.md @@ -884,7 +884,7 @@ async def server_post_delete(self, payload: ServerPostOperationPayload, | Attribute | Type | Description | |-----------|------|-------------| | **Hook Name** | `server_pre_status_change` | Hook identifier for configuration | -| **Execution Point** | Before server status toggle | When MCP server is about to be activated or deactivated | +| **Execution Point** | Before server status change | When MCP server is about to be activated or deactivated | | **Purpose** | Access control, dependency validation, impact assessment | Validate status change permissions and assess operational impact | **Payload Attributes (`ServerPreOperationPayload`)** - Same structure as other pre-hooks: diff --git a/docs/docs/design/images/code2flow.svg b/docs/docs/design/images/code2flow.svg index 512306397..07859fe10 100644 --- a/docs/docs/design/images/code2flow.svg +++ b/docs/docs/design/images/code2flow.svg @@ -1306,7 +1306,7 @@ node_7cd26d76 -419: toggle_agent_status() +419: set_a2a_agent_state() @@ -3093,7 +3093,7 @@ node_1b63b77c -8588: admin_toggle_a2a_agent() +8588: admin_set_a2a_agent_state() @@ -3105,7 +3105,7 @@ node_5de30b09 -1596: admin_toggle_gateway() +1596: admin_set_gateway_state() @@ -3117,7 +3117,7 @@ node_7a5e44f1 -6872: admin_toggle_prompt() +6872: admin_set_prompt_state() @@ -3129,7 +3129,7 @@ node_bc807ceb -6378: admin_toggle_resource() +6378: admin_set_resource_state() @@ -3141,7 +3141,7 @@ node_ddba35fe -1080: admin_toggle_server() +1080: admin_set_server_state() @@ -3153,7 +3153,7 @@ node_880f2772 -5257: admin_toggle_tool() +5257: admin_set_tool_state() @@ -6813,7 +6813,7 @@ node_d8cf5c91 -1291: toggle_gateway_status() +1291: set_gateway_state() @@ -10222,7 +10222,7 @@ node_713e7ad0 -1916: toggle_a2a_agent_status() +1916: set_a2a_agent_state() @@ -10234,7 +10234,7 @@ node_8d8912a8 -3044: toggle_gateway_status() +3044: set_gateway_state() @@ -10246,13 +10246,13 @@ node_64a5110c -2658: toggle_prompt_status() +2658: set_prompt_state() node_bb7af980 -773: toggle_prompt_status() +773: set_prompt_state() @@ -10264,13 +10264,13 @@ node_5e76168a -2338: toggle_resource_status() +2338: set_resource_state() node_b0cb6099 -717: toggle_resource_status() +717: set_resource_state() @@ -10282,13 +10282,13 @@ node_f1ffc7ad -1484: toggle_server_status() +1484: set_server_state() node_2aaac624 -833: toggle_server_status() +833: set_server_state() @@ -10300,13 +10300,13 @@ node_c67b693a -2277: toggle_tool_status() +2277: set_tool_state() node_0ddbe8ce -707: toggle_tool_status() +707: set_tool_state() diff --git a/docs/docs/development/developer-onboarding.md b/docs/docs/development/developer-onboarding.md index b28eeab03..63f2ff918 100644 --- a/docs/docs/development/developer-onboarding.md +++ b/docs/docs/development/developer-onboarding.md @@ -173,7 +173,7 @@ - Resources - Prompts - Gateways - - [ ] Toggle active/inactive switches + - [ ] Set active/inactive states - [ ] JWT stored in `HttpOnly` cookie, no errors in DevTools Console ???+ check "Metrics" diff --git a/docs/docs/index.md b/docs/docs/index.md index 43b68af99..684d8002e 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1714,11 +1714,11 @@ curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ -d '{ "description":"Updated desc" }' \ http://localhost:4444/tools/1 -# Toggle active status +# Set active status curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/tools/1/toggle?activate=false + http://localhost:4444/tools/1/state?activate=false curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/tools/1/toggle?activate=true + http://localhost:4444/tools/1/state?activate=true # Delete tool curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/tools/1 @@ -1776,9 +1776,9 @@ curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ }' \ http://localhost:4444/a2a/agent-name/invoke -# Toggle agent status +# Set agent state curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/a2a/agent-id/toggle?activate=false + http://localhost:4444/a2a/agent-id/state?activate=false # Delete agent curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ @@ -1826,9 +1826,9 @@ curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ -d '{"description":"New description"}' \ http://localhost:4444/gateways/1 -# Toggle active status +# Set active status curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/gateways/1/toggle?activate=false + http://localhost:4444/gateways/1/state?activate=false # Delete gateway curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/gateways/1 @@ -1912,9 +1912,9 @@ curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ -d '{"template":"Hi, {{ user }}!"}' \ http://localhost:4444/prompts/greet -# Toggle active +# Set active curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/prompts/5/toggle?activate=false + http://localhost:4444/prompts/5/state?activate=false # Delete prompt curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/prompts/greet @@ -1970,9 +1970,9 @@ curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ -d '{"description":"Updated"}' \ http://localhost:4444/servers/UUID_OF_SERVER_1 -# Toggle active +# Set active curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ - http://localhost:4444/servers/UUID_OF_SERVER_1/toggle?activate=false + http://localhost:4444/servers/UUID_OF_SERVER_1/state?activate=false ``` diff --git a/docs/docs/manage/api-usage.md b/docs/docs/manage/api-usage.md index 54e9f2498..e08dd883a 100644 --- a/docs/docs/manage/api-usage.md +++ b/docs/docs/manage/api-usage.md @@ -162,9 +162,9 @@ curl -s -X PUT -H "Authorization: Bearer $TOKEN" \ ### Enable/Disable Gateway ```bash -# Toggle gateway enabled status +# Set gateway state (enable/disable) curl -s -X POST -H "Authorization: Bearer $TOKEN" \ - $BASE_URL/gateways/$GATEWAY_ID/toggle?activate=false | jq '.' + $BASE_URL/gateways/$GATEWAY_ID/state?activate=false | jq '.' ``` ### Delete Gateway @@ -284,9 +284,9 @@ curl -s -X PUT -H "Authorization: Bearer $TOKEN" \ ### Enable/Disable Tool ```bash -# Toggle tool enabled status +# Set tool state (enable/disable) curl -s -X POST -H "Authorization: Bearer $TOKEN" \ - $BASE_URL/tools/$TOOL_ID/toggle?activate=false | jq '.' + $BASE_URL/tools/$TOOL_ID/state?activate=false | jq '.' ``` ### Delete Tool @@ -407,9 +407,9 @@ curl -s -X PUT -H "Authorization: Bearer $TOKEN" \ ### Enable/Disable Server ```bash -# Toggle server enabled status +# Set server state (enable/disable) curl -s -X POST -H "Authorization: Bearer $TOKEN" \ - $BASE_URL/servers/$SERVER_ID/toggle?activate=false | jq '.' + $BASE_URL/servers/$SERVER_ID/state?activate=false | jq '.' ``` ### Delete Server @@ -499,9 +499,9 @@ curl -s -X PUT -H "Authorization: Bearer $TOKEN" \ ### Enable/Disable Resource ```bash -# Toggle resource enabled status +# Set resource state (enable/disable) curl -s -X POST -H "Authorization: Bearer $TOKEN" \ - $BASE_URL/resources/$RESOURCE_ID/toggle?activate=false | jq '.' + $BASE_URL/resources/$RESOURCE_ID/state?activate=false | jq '.' ``` ### Delete Resource @@ -581,9 +581,9 @@ curl -s -X PUT -H "Authorization: Bearer $TOKEN" \ ### Enable/Disable Prompt ```bash -# Toggle prompt enabled status +# Set prompt state (enable/disable) curl -s -X POST -H "Authorization: Bearer $TOKEN" \ - $BASE_URL/prompts/$PROMPT_ID/toggle?activate=false | jq '.' + $BASE_URL/prompts/$PROMPT_ID/state?activate=false | jq '.' ``` ### Delete Prompt @@ -1038,7 +1038,7 @@ TOOLS=$(curl -s -H "Authorization: Bearer $TOKEN" $BASE_URL/tools | \ for TOOL_ID in $TOOLS; do echo "Enabling tool: $TOOL_ID" curl -s -X POST -H "Authorization: Bearer $TOKEN" \ - $BASE_URL/tools/$TOOL_ID/toggle > /dev/null + $BASE_URL/tools/$TOOL_ID/state > /dev/null done echo "Done!" diff --git a/docs/docs/overview/ui.md b/docs/docs/overview/ui.md index 724a3bd2e..32531e750 100644 --- a/docs/docs/overview/ui.md +++ b/docs/docs/overview/ui.md @@ -39,7 +39,7 @@ It provides tabbed access to: | Bulk import tools | Use API endpoint `/admin/tools/import` (see [Bulk Import](../manage/bulk-import.md)) | | View prompt output | Go to Prompts β†’ click View | | **View entity metadata** | Click "View" on any entity β†’ scroll to "Metadata" section | -| Toggle server activity | Use the "Activate/Deactivate" buttons in Servers tab | +| Set server activity | Use the "Activate/Deactivate" buttons in Servers tab | | Delete a resource | Navigate to Resources β†’ click Delete (after confirming) | All actions are reflected in the live API via `/tools`, `/prompts`, etc. diff --git a/docs/docs/tutorials/openwebui-tutorial.md b/docs/docs/tutorials/openwebui-tutorial.md index 4202e2b47..66af3c117 100644 --- a/docs/docs/tutorials/openwebui-tutorial.md +++ b/docs/docs/tutorials/openwebui-tutorial.md @@ -352,7 +352,7 @@ docker logs -f openwebui 1. Navigate to **Workspace** β†’ **Models** 2. Edit each model (granite, llama, mistral) 3. Scroll to **Tools** section -4. Toggle on the MCP tools you want available +4. Enable the MCP tools you want available 5. Click **Save** ### 6.4 Add MCP Servers to Gateway diff --git a/docs/docs/using/grpc-services.md b/docs/docs/using/grpc-services.md index af6d91cbf..4eb8c9c30 100644 --- a/docs/docs/using/grpc-services.md +++ b/docs/docs/using/grpc-services.md @@ -236,7 +236,7 @@ Click **View Methods** to see all discovered gRPC methods: - Output message type - Streaming flags (client/server streaming) -### Toggle Service +### Service State Use **Activate/Deactivate** to enable/disable a service: - Disabled services are not available for tool invocation @@ -304,10 +304,10 @@ Content-Type: application/json } ``` -### Toggle Service +### Service State ```bash -POST /grpc/{service_id}/toggle +POST /grpc/{service_id}/state ``` ### Delete Service diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index bff61268b..932be8e82 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -1337,23 +1337,23 @@ async def admin_edit_server( return JSONResponse(content={"message": str(ex), "success": False}, status_code=500) -@admin_router.post("/servers/{server_id}/toggle") -async def admin_toggle_server( +@admin_router.post("/servers/{server_id}/state") +async def admin_set_server_state( server_id: str, request: Request, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ) -> Response: """ - Toggle a server's active status via the admin UI. + Set a server's active state via the admin UI. This endpoint processes a form request to activate or deactivate a server. It expects a form field 'activate' with value "true" to activate the server or "false" to deactivate it. The endpoint handles exceptions gracefully and - logs any errors that might occur during the status toggle operation. + logs any errors that might occur during the state change operation. Args: - server_id (str): The ID of the server whose status to toggle. + server_id (str): The ID of the server whose state to set. request (Request): FastAPI request containing form data with the 'activate' field. db (Session): Database session dependency. user (str): Authenticated user dependency. @@ -1371,20 +1371,20 @@ async def admin_toggle_server( >>> >>> mock_db = MagicMock() >>> mock_user = {"email": "test_user", "db": mock_db} - >>> server_id = "server-to-toggle" + >>> server_id = "server-to-set" >>> >>> # Happy path: Activate server >>> form_data_activate = FormData([("activate", "true"), ("is_inactive_checked", "false")]) >>> mock_request_activate = MagicMock(spec=Request, scope={"root_path": ""}) >>> mock_request_activate.form = AsyncMock(return_value=form_data_activate) - >>> original_toggle_server_status = server_service.toggle_server_status - >>> server_service.toggle_server_status = AsyncMock() + >>> original_set_server_state = server_service.set_server_state + >>> server_service.set_server_state = AsyncMock() >>> - >>> async def test_admin_toggle_server_activate(): - ... result = await admin_toggle_server(server_id, mock_request_activate, mock_db, mock_user) + >>> async def test_admin_set_server_state_activate(): + ... result = await admin_set_server_state(server_id, mock_request_activate, mock_db, mock_user) ... return isinstance(result, RedirectResponse) and result.status_code == 303 and "/admin#catalog" in result.headers["location"] >>> - >>> asyncio.run(test_admin_toggle_server_activate()) + >>> asyncio.run(test_admin_set_server_state_activate()) True >>> >>> # Happy path: Deactivate server @@ -1392,33 +1392,33 @@ async def admin_toggle_server( >>> mock_request_deactivate = MagicMock(spec=Request, scope={"root_path": "/api"}) >>> mock_request_deactivate.form = AsyncMock(return_value=form_data_deactivate) >>> - >>> async def test_admin_toggle_server_deactivate(): - ... result = await admin_toggle_server(server_id, mock_request_deactivate, mock_db, mock_user) + >>> async def test_admin_set_server_state_deactivate(): + ... result = await admin_set_server_state(server_id, mock_request_deactivate, mock_db, mock_user) ... return isinstance(result, RedirectResponse) and result.status_code == 303 and "/api/admin#catalog" in result.headers["location"] >>> - >>> asyncio.run(test_admin_toggle_server_deactivate()) + >>> asyncio.run(test_admin_set_server_state_deactivate()) True >>> - >>> # Edge case: Toggle with inactive checkbox checked + >>> # Edge case: Set state with inactive checkbox checked >>> form_data_inactive = FormData([("activate", "true"), ("is_inactive_checked", "true")]) >>> mock_request_inactive = MagicMock(spec=Request, scope={"root_path": ""}) >>> mock_request_inactive.form = AsyncMock(return_value=form_data_inactive) >>> - >>> async def test_admin_toggle_server_inactive_checked(): - ... result = await admin_toggle_server(server_id, mock_request_inactive, mock_db, mock_user) + >>> async def test_admin_set_server_state_inactive_checked(): + ... result = await admin_set_server_state(server_id, mock_request_inactive, mock_db, mock_user) ... return isinstance(result, RedirectResponse) and result.status_code == 303 and "/admin/?include_inactive=true#catalog" in result.headers["location"] >>> - >>> asyncio.run(test_admin_toggle_server_inactive_checked()) + >>> asyncio.run(test_admin_set_server_state_inactive_checked()) True >>> - >>> # Error path: Simulate an exception during toggle + >>> # Error path: Simulate an exception during state change >>> form_data_error = FormData([("activate", "true")]) >>> mock_request_error = MagicMock(spec=Request, scope={"root_path": ""}) >>> mock_request_error.form = AsyncMock(return_value=form_data_error) - >>> server_service.toggle_server_status = AsyncMock(side_effect=Exception("Toggle failed")) + >>> server_service.set_server_state = AsyncMock(side_effect=Exception("Set failed")) >>> - >>> async def test_admin_toggle_server_exception(): - ... result = await admin_toggle_server(server_id, mock_request_error, mock_db, mock_user) + >>> async def test_admin_set_server_state_exception(): + ... result = await admin_set_server_state(server_id, mock_request_error, mock_db, mock_user) ... location_header = result.headers["location"] ... return ( ... isinstance(result, RedirectResponse) @@ -1428,20 +1428,20 @@ async def admin_toggle_server( ... and location_header.endswith("#catalog") # Ensure the fragment is correct ... ) >>> - >>> asyncio.run(test_admin_toggle_server_exception()) + >>> asyncio.run(test_admin_set_server_state_exception()) True >>> >>> # Restore original method - >>> server_service.toggle_server_status = original_toggle_server_status + >>> server_service.set_server_state = original_set_server_state """ form = await request.form() error_message = None user_email = get_user_email(user) - LOGGER.debug(f"User {user_email} is toggling server ID {server_id} with activate: {form.get('activate')}") + LOGGER.debug(f"User {user_email} is setting server ID {server_id} state with activate: {form.get('activate')}") activate = str(form.get("activate", "true")).lower() == "true" is_inactive_checked = str(form.get("is_inactive_checked", "false")) try: - await server_service.toggle_server_status(db, server_id, activate, user_email=user_email) + await server_service.set_server_state(db, server_id, activate, user_email=user_email) except PermissionError as e: LOGGER.warning(f"Permission denied for user {user_email} toggling servers {server_id}: {e}") error_message = str(e) @@ -1917,22 +1917,22 @@ async def admin_list_gateway_ids( return {"gateway_ids": ids} -@admin_router.post("/gateways/{gateway_id}/toggle") -async def admin_toggle_gateway( +@admin_router.post("/gateways/{gateway_id}/state") +async def admin_set_gateway_state( gateway_id: str, request: Request, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ) -> RedirectResponse: """ - Toggle the active status of a gateway via the admin UI. + Set the active state of a gateway via the admin UI. - This endpoint allows an admin to toggle the active status of a gateway. + This endpoint allows an admin to set the active state of a gateway. It expects a form field 'activate' with a value of "true" or "false" to - determine the new status of the gateway. + determine the new state of the gateway. Args: - gateway_id (str): The ID of the gateway to toggle. + gateway_id (str): The ID of the gateway whose state to set. request (Request): The FastAPI request object containing form data. db (Session): The database session dependency. user (str): The authenticated user dependency. @@ -1950,20 +1950,20 @@ async def admin_toggle_gateway( >>> >>> mock_db = MagicMock() >>> mock_user = {"email": "test_user", "db": mock_db} - >>> gateway_id = "gateway-to-toggle" + >>> gateway_id = "gateway-to-set" >>> >>> # Happy path: Activate gateway >>> form_data_activate = FormData([("activate", "true"), ("is_inactive_checked", "false")]) >>> mock_request_activate = MagicMock(spec=Request, scope={"root_path": ""}) >>> mock_request_activate.form = AsyncMock(return_value=form_data_activate) - >>> original_toggle_gateway_status = gateway_service.toggle_gateway_status - >>> gateway_service.toggle_gateway_status = AsyncMock() + >>> original_set_gateway_state = gateway_service.set_gateway_state + >>> gateway_service.set_gateway_state = AsyncMock() >>> - >>> async def test_admin_toggle_gateway_activate(): - ... result = await admin_toggle_gateway(gateway_id, mock_request_activate, mock_db, mock_user) + >>> async def test_admin_set_gateway_state_activate(): + ... result = await admin_set_gateway_state(gateway_id, mock_request_activate, mock_db, mock_user) ... return isinstance(result, RedirectResponse) and result.status_code == 303 and "/admin#gateways" in result.headers["location"] >>> - >>> asyncio.run(test_admin_toggle_gateway_activate()) + >>> asyncio.run(test_admin_set_gateway_state_activate()) True >>> >>> # Happy path: Deactivate gateway @@ -1971,21 +1971,21 @@ async def admin_toggle_gateway( >>> mock_request_deactivate = MagicMock(spec=Request, scope={"root_path": "/api"}) >>> mock_request_deactivate.form = AsyncMock(return_value=form_data_deactivate) >>> - >>> async def test_admin_toggle_gateway_deactivate(): - ... result = await admin_toggle_gateway(gateway_id, mock_request_deactivate, mock_db, mock_user) + >>> async def test_admin_set_gateway_state_deactivate(): + ... result = await admin_set_gateway_state(gateway_id, mock_request_deactivate, mock_db, mock_user) ... return isinstance(result, RedirectResponse) and result.status_code == 303 and "/api/admin#gateways" in result.headers["location"] >>> - >>> asyncio.run(test_admin_toggle_gateway_deactivate()) + >>> asyncio.run(test_admin_set_gateway_state_deactivate()) True >>> - >>> # Error path: Simulate an exception during toggle + >>> # Error path: Simulate an exception during state change >>> form_data_error = FormData([("activate", "true")]) >>> mock_request_error = MagicMock(spec=Request, scope={"root_path": ""}) >>> mock_request_error.form = AsyncMock(return_value=form_data_error) - >>> gateway_service.toggle_gateway_status = AsyncMock(side_effect=Exception("Toggle failed")) + >>> gateway_service.set_gateway_state = AsyncMock(side_effect=Exception("Set failed")) >>> - >>> async def test_admin_toggle_gateway_exception(): - ... result = await admin_toggle_gateway(gateway_id, mock_request_error, mock_db, mock_user) + >>> async def test_admin_set_gateway_state_exception(): + ... result = await admin_set_gateway_state(gateway_id, mock_request_error, mock_db, mock_user) ... location_header = result.headers["location"] ... return ( ... isinstance(result, RedirectResponse) @@ -1995,26 +1995,26 @@ async def admin_toggle_gateway( ... and location_header.endswith("#gateways") # Ensure the fragment is correct ... ) >>> - >>> asyncio.run(test_admin_toggle_gateway_exception()) + >>> asyncio.run(test_admin_set_gateway_state_exception()) True >>> # Restore original method - >>> gateway_service.toggle_gateway_status = original_toggle_gateway_status + >>> gateway_service.set_gateway_state = original_set_gateway_state """ error_message = None user_email = get_user_email(user) - LOGGER.debug(f"User {user_email} is toggling gateway ID {gateway_id}") + LOGGER.debug(f"User {user_email} is setting gateway ID {gateway_id} state") form = await request.form() activate = str(form.get("activate", "true")).lower() == "true" is_inactive_checked = str(form.get("is_inactive_checked", "false")) try: - await gateway_service.toggle_gateway_status(db, gateway_id, activate, user_email=user_email) + await gateway_service.set_gateway_state(db, gateway_id, activate, user_email=user_email) except PermissionError as e: - LOGGER.warning(f"Permission denied for user {user_email} toggling gateway {gateway_id}: {e}") + LOGGER.warning(f"Permission denied for user {user_email} setting gateway {gateway_id} state: {e}") error_message = str(e) except Exception as e: - LOGGER.error(f"Error toggling gateway status: {e}") - error_message = "Failed to toggle gateway status. Please try again." + LOGGER.error(f"Error setting gateway state: {e}") + error_message = "Failed to set gateway state. Please try again." root_path = request.scope.get("root_path", "") @@ -5567,18 +5567,28 @@ async def admin_resources_partial_html( resources_db = list(db.scalars(query).all()) - # Convert to schemas using ResourceService + # Convert DB rows to ResourceRead using ResourceService public API (async) + local_resource_service = ResourceService() resources_data = [] for r in resources_db: try: - resources_data.append(local_resource_service._convert_resource_to_read(r)) # pylint: disable=protected-access + # Use the public async getter which resolves team name and converts to schema + try: + resource_schema = await local_resource_service.get_resource_by_id(db, getattr(r, "id", None), include_inactive=include_inactive) + except Exception as inner_exc: + LOGGER.warning( + "Failed to load resource id=%s via ResourceService.get_resource_by_id: %s", + getattr(r, "id", ""), + inner_exc, + ) + continue + resources_data.append(resource_schema) except Exception as e: LOGGER.warning(f"Failed to convert resource {getattr(r, 'id', '')} to schema: {e}") continue data = jsonable_encoder(resources_data) - # Build pagination metadata pagination = PaginationMeta( page=page, @@ -6578,23 +6588,23 @@ async def admin_delete_tool(tool_id: str, request: Request, db: Session = Depend return RedirectResponse(f"{root_path}/admin#tools", status_code=303) -@admin_router.post("/tools/{tool_id}/toggle") -async def admin_toggle_tool( +@admin_router.post("/tools/{tool_id}/state") +async def admin_set_tool_state( tool_id: str, request: Request, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ) -> RedirectResponse: """ - Toggle a tool's active status via the admin UI. + Set a tool's active state via the admin UI. This endpoint processes a form request to activate or deactivate a tool. It expects a form field 'activate' with value "true" to activate the tool or "false" to deactivate it. The endpoint handles exceptions gracefully and - logs any errors that might occur during the status toggle operation. + logs any errors that might occur during the state change operation. Args: - tool_id (str): The ID of the tool whose status to toggle. + tool_id (str): The ID of the tool whose state to set. request (Request): FastAPI request containing form data with the 'activate' field. db (Session): Database session dependency. user (str): Authenticated user dependency. @@ -6612,20 +6622,20 @@ async def admin_toggle_tool( >>> >>> mock_db = MagicMock() >>> mock_user = {"email": "test_user", "db": mock_db} - >>> tool_id = "tool-to-toggle" + >>> tool_id = "tool-to-set" >>> >>> # Happy path: Activate tool >>> form_data_activate = FormData([("activate", "true"), ("is_inactive_checked", "false")]) >>> mock_request_activate = MagicMock(spec=Request, scope={"root_path": ""}) >>> mock_request_activate.form = AsyncMock(return_value=form_data_activate) - >>> original_toggle_tool_status = tool_service.toggle_tool_status - >>> tool_service.toggle_tool_status = AsyncMock() + >>> original_set_tool_state = tool_service.set_tool_state + >>> tool_service.set_tool_state = AsyncMock() >>> - >>> async def test_admin_toggle_tool_activate(): - ... result = await admin_toggle_tool(tool_id, mock_request_activate, mock_db, mock_user) + >>> async def test_admin_set_tool_state_activate(): + ... result = await admin_set_tool_state(tool_id, mock_request_activate, mock_db, mock_user) ... return isinstance(result, RedirectResponse) and result.status_code == 303 and "/admin#tools" in result.headers["location"] >>> - >>> asyncio.run(test_admin_toggle_tool_activate()) + >>> asyncio.run(test_admin_set_tool_state_activate()) True >>> >>> # Happy path: Deactivate tool @@ -6633,33 +6643,33 @@ async def admin_toggle_tool( >>> mock_request_deactivate = MagicMock(spec=Request, scope={"root_path": "/api"}) >>> mock_request_deactivate.form = AsyncMock(return_value=form_data_deactivate) >>> - >>> async def test_admin_toggle_tool_deactivate(): - ... result = await admin_toggle_tool(tool_id, mock_request_deactivate, mock_db, mock_user) + >>> async def test_admin_set_tool_state_deactivate(): + ... result = await admin_set_tool_state(tool_id, mock_request_deactivate, mock_db, mock_user) ... return isinstance(result, RedirectResponse) and result.status_code == 303 and "/api/admin#tools" in result.headers["location"] >>> - >>> asyncio.run(test_admin_toggle_tool_deactivate()) + >>> asyncio.run(test_admin_set_tool_state_deactivate()) True >>> - >>> # Edge case: Toggle with inactive checkbox checked + >>> # Edge case: Set state with inactive checkbox checked >>> form_data_inactive = FormData([("activate", "true"), ("is_inactive_checked", "true")]) >>> mock_request_inactive = MagicMock(spec=Request, scope={"root_path": ""}) >>> mock_request_inactive.form = AsyncMock(return_value=form_data_inactive) >>> - >>> async def test_admin_toggle_tool_inactive_checked(): - ... result = await admin_toggle_tool(tool_id, mock_request_inactive, mock_db, mock_user) + >>> async def test_admin_set_tool_state_inactive_checked(): + ... result = await admin_set_tool_state(tool_id, mock_request_inactive, mock_db, mock_user) ... return isinstance(result, RedirectResponse) and result.status_code == 303 and "/admin/?include_inactive=true#tools" in result.headers["location"] >>> - >>> asyncio.run(test_admin_toggle_tool_inactive_checked()) + >>> asyncio.run(test_admin_set_tool_state_inactive_checked()) True >>> - >>> # Error path: Simulate an exception during toggle + >>> # Error path: Simulate an exception during state change >>> form_data_error = FormData([("activate", "true")]) >>> mock_request_error = MagicMock(spec=Request, scope={"root_path": ""}) >>> mock_request_error.form = AsyncMock(return_value=form_data_error) - >>> tool_service.toggle_tool_status = AsyncMock(side_effect=Exception("Toggle failed")) + >>> tool_service.set_tool_state = AsyncMock(side_effect=Exception("Set failed")) >>> - >>> async def test_admin_toggle_tool_exception(): - ... result = await admin_toggle_tool(tool_id, mock_request_error, mock_db, mock_user) + >>> async def test_admin_set_tool_state_exception(): + ... result = await admin_set_tool_state(tool_id, mock_request_error, mock_db, mock_user) ... location_header = result.headers["location"] ... return ( ... isinstance(result, RedirectResponse) @@ -6669,26 +6679,26 @@ async def admin_toggle_tool( ... and location_header.endswith("#tools") # Ensure fragment is correct ... ) >>> - >>> asyncio.run(test_admin_toggle_tool_exception()) + >>> asyncio.run(test_admin_set_tool_state_exception()) True >>> >>> # Restore original method - >>> tool_service.toggle_tool_status = original_toggle_tool_status + >>> tool_service.set_tool_state = original_set_tool_state """ error_message = None user_email = get_user_email(user) - LOGGER.debug(f"User {user_email} is toggling tool ID {tool_id}") + LOGGER.debug(f"User {user_email} is setting tool ID {tool_id} state") form = await request.form() activate = str(form.get("activate", "true")).lower() == "true" is_inactive_checked = str(form.get("is_inactive_checked", "false")) try: - await tool_service.toggle_tool_status(db, tool_id, activate, reachable=activate, user_email=user_email) + await tool_service.set_tool_state(db, tool_id, activate, reachable=activate, user_email=user_email) except PermissionError as e: - LOGGER.warning(f"Permission denied for user {user_email} toggling tools {tool_id}: {e}") + LOGGER.warning(f"Permission denied for user {user_email} setting tool {tool_id} state: {e}") error_message = str(e) except Exception as e: - LOGGER.error(f"Error toggling tool status: {e}") - error_message = "Failed to toggle tool status. Please try again." + LOGGER.error(f"Error setting tool state: {e}") + error_message = "Failed to set tool state. Please try again." root_path = request.scope.get("root_path", "") @@ -7946,23 +7956,23 @@ async def admin_delete_resource(resource_id: str, request: Request, db: Session return RedirectResponse(f"{root_path}/admin#resources", status_code=303) -@admin_router.post("/resources/{resource_id}/toggle") -async def admin_toggle_resource( +@admin_router.post("/resources/{resource_id}/state") +async def admin_set_resource_state( resource_id: int, request: Request, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ) -> RedirectResponse: """ - Toggle a resource's active status via the admin UI. + Set a resource's active state via the admin UI. This endpoint processes a form request to activate or deactivate a resource. It expects a form field 'activate' with value "true" to activate the resource or "false" to deactivate it. The endpoint handles exceptions gracefully and - logs any errors that might occur during the status toggle operation. + logs any errors that might occur during the state change operation. Args: - resource_id (int): The ID of the resource whose status to toggle. + resource_id (int): The ID of the resource whose state to set. request (Request): FastAPI request containing form data with the 'activate' field. db (Session): Database session dependency. user (str): Authenticated user dependency. @@ -7988,14 +7998,14 @@ async def admin_toggle_resource( >>> mock_request.form = AsyncMock(return_value=form_data) >>> mock_request.scope = {"root_path": ""} >>> - >>> original_toggle_resource_status = resource_service.toggle_resource_status - >>> resource_service.toggle_resource_status = AsyncMock() + >>> original_set_resource_state = resource_service.set_resource_state + >>> resource_service.set_resource_state = AsyncMock() >>> - >>> async def test_admin_toggle_resource(): - ... response = await admin_toggle_resource(1, mock_request, mock_db, mock_user) + >>> async def test_admin_set_resource_state(): + ... response = await admin_set_resource_state(1, mock_request, mock_db, mock_user) ... return isinstance(response, RedirectResponse) and response.status_code == 303 >>> - >>> asyncio.run(test_admin_toggle_resource()) + >>> asyncio.run(test_admin_set_resource_state()) True >>> >>> # Test with activate=false @@ -8005,11 +8015,11 @@ async def admin_toggle_resource( ... ]) >>> mock_request.form = AsyncMock(return_value=form_data_deactivate) >>> - >>> async def test_admin_toggle_resource_deactivate(): - ... response = await admin_toggle_resource(1, mock_request, mock_db, mock_user) + >>> async def test_admin_set_resource_state_deactivate(): + ... response = await admin_set_resource_state(1, mock_request, mock_db, mock_user) ... return isinstance(response, RedirectResponse) and response.status_code == 303 >>> - >>> asyncio.run(test_admin_toggle_resource_deactivate()) + >>> asyncio.run(test_admin_set_resource_state_deactivate()) True >>> >>> # Test with inactive checkbox checked @@ -8019,43 +8029,43 @@ async def admin_toggle_resource( ... ]) >>> mock_request.form = AsyncMock(return_value=form_data_inactive) >>> - >>> async def test_admin_toggle_resource_inactive(): - ... response = await admin_toggle_resource(1, mock_request, mock_db, mock_user) + >>> async def test_admin_set_resource_state_inactive(): + ... response = await admin_set_resource_state(1, mock_request, mock_db, mock_user) ... return isinstance(response, RedirectResponse) and "include_inactive=true" in response.headers["location"] >>> - >>> asyncio.run(test_admin_toggle_resource_inactive()) + >>> asyncio.run(test_admin_set_resource_state_inactive()) True >>> >>> # Test exception handling - >>> resource_service.toggle_resource_status = AsyncMock(side_effect=Exception("Test error")) + >>> resource_service.set_resource_state = AsyncMock(side_effect=Exception("Test error")) >>> form_data_error = FormData([ ... ("activate", "true"), ... ("is_inactive_checked", "false") ... ]) >>> mock_request.form = AsyncMock(return_value=form_data_error) >>> - >>> async def test_admin_toggle_resource_exception(): - ... response = await admin_toggle_resource(1, mock_request, mock_db, mock_user) + >>> async def test_admin_set_resource_state_exception(): + ... response = await admin_set_resource_state(1, mock_request, mock_db, mock_user) ... return isinstance(response, RedirectResponse) and response.status_code == 303 >>> - >>> asyncio.run(test_admin_toggle_resource_exception()) + >>> asyncio.run(test_admin_set_resource_state_exception()) True - >>> resource_service.toggle_resource_status = original_toggle_resource_status + >>> resource_service.set_resource_state = original_set_resource_state """ user_email = get_user_email(user) - LOGGER.debug(f"User {user_email} is toggling resource ID {resource_id}") + LOGGER.debug(f"User {user_email} is setting resource ID {resource_id} state") form = await request.form() error_message = None activate = str(form.get("activate", "true")).lower() == "true" is_inactive_checked = str(form.get("is_inactive_checked", "false")) try: - await resource_service.toggle_resource_status(db, resource_id, activate, user_email=user_email) + await resource_service.set_resource_state(db, resource_id, activate, user_email=user_email) except PermissionError as e: - LOGGER.warning(f"Permission denied for user {user_email} toggling resource status {resource_id}: {e}") + LOGGER.warning(f"Permission denied for user {user_email} setting resource state {resource_id}: {e}") error_message = str(e) except Exception as e: - LOGGER.error(f"Error toggling resource status: {e}") - error_message = "Failed to toggle resource status. Please try again." + LOGGER.error(f"Error setting resource state: {e}") + error_message = "Failed to set resource state. Please try again." root_path = request.scope.get("root_path", "") @@ -8501,23 +8511,23 @@ async def admin_delete_prompt(prompt_id: str, request: Request, db: Session = De return RedirectResponse(f"{root_path}/admin#prompts", status_code=303) -@admin_router.post("/prompts/{prompt_id}/toggle") -async def admin_toggle_prompt( +@admin_router.post("/prompts/{prompt_id}/state") +async def admin_set_prompt_state( prompt_id: int, request: Request, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ) -> RedirectResponse: """ - Toggle a prompt's active status via the admin UI. + Set a prompt's active state via the admin UI. This endpoint processes a form request to activate or deactivate a prompt. It expects a form field 'activate' with value "true" to activate the prompt or "false" to deactivate it. The endpoint handles exceptions gracefully and - logs any errors that might occur during the status toggle operation. + logs any errors that might occur during the state change operation. Args: - prompt_id (int): The ID of the prompt whose status to toggle. + prompt_id (int): The ID of the prompt whose state to set. request (Request): FastAPI request containing form data with the 'activate' field. db (Session): Database session dependency. user (str): Authenticated user dependency. @@ -8543,14 +8553,14 @@ async def admin_toggle_prompt( >>> mock_request.form = AsyncMock(return_value=form_data) >>> mock_request.scope = {"root_path": ""} >>> - >>> original_toggle_prompt_status = prompt_service.toggle_prompt_status - >>> prompt_service.toggle_prompt_status = AsyncMock() + >>> original_set_prompt_state = prompt_service.set_prompt_state + >>> prompt_service.set_prompt_state = AsyncMock() >>> - >>> async def test_admin_toggle_prompt(): - ... response = await admin_toggle_prompt(1, mock_request, mock_db, mock_user) + >>> async def test_admin_set_prompt_state(): + ... response = await admin_set_prompt_state(1, mock_request, mock_db, mock_user) ... return isinstance(response, RedirectResponse) and response.status_code == 303 >>> - >>> asyncio.run(test_admin_toggle_prompt()) + >>> asyncio.run(test_admin_set_prompt_state()) True >>> >>> # Test with activate=false @@ -8560,11 +8570,11 @@ async def admin_toggle_prompt( ... ]) >>> mock_request.form = AsyncMock(return_value=form_data_deactivate) >>> - >>> async def test_admin_toggle_prompt_deactivate(): - ... response = await admin_toggle_prompt(1, mock_request, mock_db, mock_user) + >>> async def test_admin_set_prompt_state_deactivate(): + ... response = await admin_set_prompt_state(1, mock_request, mock_db, mock_user) ... return isinstance(response, RedirectResponse) and response.status_code == 303 >>> - >>> asyncio.run(test_admin_toggle_prompt_deactivate()) + >>> asyncio.run(test_admin_set_prompt_state_deactivate()) True >>> >>> # Test with inactive checkbox checked @@ -8574,43 +8584,43 @@ async def admin_toggle_prompt( ... ]) >>> mock_request.form = AsyncMock(return_value=form_data_inactive) >>> - >>> async def test_admin_toggle_prompt_inactive(): - ... response = await admin_toggle_prompt(1, mock_request, mock_db, mock_user) + >>> async def test_admin_set_prompt_state_inactive(): + ... response = await admin_set_prompt_state(1, mock_request, mock_db, mock_user) ... return isinstance(response, RedirectResponse) and "include_inactive=true" in response.headers["location"] >>> - >>> asyncio.run(test_admin_toggle_prompt_inactive()) + >>> asyncio.run(test_admin_set_prompt_state_inactive()) True >>> >>> # Test exception handling - >>> prompt_service.toggle_prompt_status = AsyncMock(side_effect=Exception("Test error")) + >>> prompt_service.set_prompt_state = AsyncMock(side_effect=Exception("Test error")) >>> form_data_error = FormData([ ... ("activate", "true"), ... ("is_inactive_checked", "false") ... ]) >>> mock_request.form = AsyncMock(return_value=form_data_error) >>> - >>> async def test_admin_toggle_prompt_exception(): - ... response = await admin_toggle_prompt(1, mock_request, mock_db, mock_user) + >>> async def test_admin_set_prompt_state_exception(): + ... response = await admin_set_prompt_state(1, mock_request, mock_db, mock_user) ... return isinstance(response, RedirectResponse) and response.status_code == 303 >>> - >>> asyncio.run(test_admin_toggle_prompt_exception()) + >>> asyncio.run(test_admin_set_prompt_state_exception()) True - >>> prompt_service.toggle_prompt_status = original_toggle_prompt_status + >>> prompt_service.set_prompt_state = original_set_prompt_state """ user_email = get_user_email(user) - LOGGER.debug(f"User {user_email} is toggling prompt ID {prompt_id}") + LOGGER.debug(f"User {user_email} is setting prompt ID {prompt_id} state") error_message = None form = await request.form() activate: bool = str(form.get("activate", "true")).lower() == "true" is_inactive_checked: str = str(form.get("is_inactive_checked", "false")) try: - await prompt_service.toggle_prompt_status(db, prompt_id, activate, user_email=user_email) + await prompt_service.set_prompt_state(db, prompt_id, activate, user_email=user_email) except PermissionError as e: - LOGGER.warning(f"Permission denied for user {user_email} toggling prompt {prompt_id}: {e}") + LOGGER.warning(f"Permission denied for user {user_email} setting prompt state {prompt_id}: {e}") error_message = str(e) except Exception as e: - LOGGER.error(f"Error toggling prompt status: {e}") - error_message = "Failed to toggle prompt status. Please try again." + LOGGER.error(f"Error setting prompt state: {e}") + error_message = "Failed to set prompt state. Please try again." root_path = request.scope.get("root_path", "") @@ -10862,8 +10872,8 @@ async def admin_edit_a2a_agent( return JSONResponse({"message": str(e), "success": False}, status_code=500) -@admin_router.post("/a2a/{agent_id}/toggle") -async def admin_toggle_a2a_agent( +@admin_router.post("/a2a/{agent_id}/state") +async def admin_set_a2a_agent_state( agent_id: str, request: Request, db: Session = Depends(get_db), @@ -10895,21 +10905,21 @@ async def admin_toggle_a2a_agent( user_email = get_user_email(user) - await a2a_service.toggle_agent_status(db, agent_id, activate, user_email=user_email) + await a2a_service.set_a2a_agent_state(db, agent_id, activate, user_email=user_email) root_path = request.scope.get("root_path", "") return RedirectResponse(f"{root_path}/admin#a2a-agents", status_code=303) except PermissionError as e: - LOGGER.warning(f"Permission denied for user {user_email} toggling A2A agent status{agent_id}: {e}") + LOGGER.warning(f"Permission denied for user {user_email} setting A2A agent state {agent_id}: {e}") error_message = str(e) except A2AAgentNotFoundError as e: - LOGGER.error(f"A2A agent toggle failed - not found: {e}") + LOGGER.error(f"A2A agent set state failed - not found: {e}") root_path = request.scope.get("root_path", "") error_message = "A2A agent not found." except Exception as e: - LOGGER.error(f"Error toggling A2A agent: {e}") + LOGGER.error(f"Error setting A2A agent state: {e}") root_path = request.scope.get("root_path", "") - error_message = "Failed to toggle status of A2A agent. Please try again." + error_message = "Failed to set status of A2A agent. Please try again." root_path = request.scope.get("root_path", "") @@ -11155,13 +11165,13 @@ async def admin_update_grpc_service( raise HTTPException(status_code=500, detail=str(e)) -@admin_router.post("/grpc/{service_id}/toggle") -async def admin_toggle_grpc_service( +@admin_router.post("/grpc/{service_id}/state") +async def admin_set_grpc_service_state( service_id: str, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), # pylint: disable=unused-argument ): - """Toggle a gRPC service's enabled status. + """Set a gRPC service's enabled status (via admin UI). Args: service_id: Service ID @@ -11179,7 +11189,7 @@ async def admin_toggle_grpc_service( try: service = await grpc_service_mgr.get_service(db, service_id) - result = await grpc_service_mgr.toggle_service(db, service_id, not service.enabled) + result = await grpc_service_mgr.set_grpc_service_state(db, service_id, not service.enabled) return JSONResponse(content=jsonable_encoder(result)) except GrpcServiceNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) diff --git a/mcpgateway/main.py b/mcpgateway/main.py index 7e2bd787f..e984371e3 100644 --- a/mcpgateway/main.py +++ b/mcpgateway/main.py @@ -1815,33 +1815,37 @@ async def update_server( raise HTTPException(status_code=409, detail=ErrorFormatter.format_database_error(e)) -@server_router.post("/{server_id}/toggle", response_model=ServerRead) +@server_router.post("/{server_id}/state", response_model=ServerRead) @require_permission("servers.update") -async def toggle_server_status( +async def set_server_state( server_id: str, activate: bool = True, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ) -> ServerRead: """ - Toggles the status of a server (activate or deactivate). + Set the state of a server (activate or deactivate). + + This endpoint updates the server's active/inactive state. Prefer the + language "set ... state" over "toggle" to avoid ambiguity; the + `activate` boolean explicitly controls the resulting state. Args: - server_id (str): The ID of the server to toggle. - activate (bool): Whether to activate or deactivate the server. + server_id (str): The ID of the server to modify. + activate (bool): Whether to activate (True) or deactivate (False) the server. db (Session): The database session used to interact with the data store. user (str): The authenticated user making the request. Returns: - ServerRead: The server object after the status change. + ServerRead: The server object after the state change. Raises: HTTPException: If the server is not found or there is an error. """ try: user_email = user.get("email") if isinstance(user, dict) else str(user) - logger.debug(f"User {user} is toggling server with ID {server_id} to {'active' if activate else 'inactive'}") - return await server_service.toggle_server_status(db, server_id, activate, user_email=user_email) + logger.debug(f"User {user} is setting server {server_id} to {'active' if activate else 'inactive'}") + return await server_service.set_server_state(db, server_id, activate, user_email=user_email) except PermissionError as e: raise HTTPException(status_code=403, detail=str(e)) except ServerNotFoundError as e: @@ -2290,35 +2294,39 @@ async def update_a2a_agent( raise HTTPException(status_code=409, detail=ErrorFormatter.format_database_error(e)) -@a2a_router.post("/{agent_id}/toggle", response_model=A2AAgentRead) +@a2a_router.post("/{agent_id}/state", response_model=A2AAgentRead) @require_permission("a2a.update") -async def toggle_a2a_agent_status( +async def set_a2a_agent_state( agent_id: str, activate: bool = True, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ) -> A2AAgentRead: """ - Toggles the status of an A2A agent (activate or deactivate). + Set the state of an A2A agent (activate or deactivate). + + This endpoint updates the agent's active/inactive state. Use "set ... state" + wording rather than "toggle"; the `activate` boolean explicitly controls the + resulting state. Args: - agent_id (str): The ID of the agent to toggle. - activate (bool): Whether to activate or deactivate the agent. + agent_id (str): The ID of the agent to modify. + activate (bool): Whether to activate (True) or deactivate (False) the agent. db (Session): The database session used to interact with the data store. user (str): The authenticated user making the request. Returns: - A2AAgentRead: The agent object after the status change. + A2AAgentRead: The agent object after the state change. Raises: HTTPException: If the agent is not found or there is an error. """ try: user_email = user.get("email") if isinstance(user, dict) else str(user) - logger.debug(f"User {user} is toggling A2A agent with ID {agent_id} to {'active' if activate else 'inactive'}") + logger.debug(f"User {user} is setting A2A agent with ID {agent_id} to {'active' if activate else 'inactive'}") if a2a_service is None: raise HTTPException(status_code=503, detail="A2A service not available") - return await a2a_service.toggle_agent_status(db, agent_id, activate, user_email=user_email) + return await a2a_service.set_a2a_agent_state(db, agent_id, activate, user_email=user_email) except PermissionError as e: raise HTTPException(status_code=403, detail=str(e)) except A2AAgentNotFoundError as e: @@ -2689,9 +2697,9 @@ async def delete_tool(tool_id: str, db: Session = Depends(get_db), user=Depends( raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) -@tool_router.post("/{tool_id}/toggle") +@tool_router.post("/{tool_id}/state") @require_permission("tools.update") -async def toggle_tool_status( +async def set_tool_state( tool_id: str, activate: bool = True, db: Session = Depends(get_db), @@ -2701,7 +2709,7 @@ async def toggle_tool_status( Activates or deactivates a tool. Args: - tool_id (str): The ID of the tool to toggle. + tool_id (str): The ID of the tool whose state to set. activate (bool): Whether to activate (`True`) or deactivate (`False`) the tool. db (Session): The database session dependency. user (str): The authenticated user making the request. @@ -2710,12 +2718,12 @@ async def toggle_tool_status( Dict[str, Any]: The status, message, and updated tool data. Raises: - HTTPException: If an error occurs during status toggling. + HTTPException: If an error occurs during the state change. """ try: - logger.debug(f"User {user} is toggling tool with ID {tool_id} to {'active' if activate else 'inactive'}") + logger.debug(f"User {user} is setting tool with ID {tool_id} to {'active' if activate else 'inactive'}") user_email = user.get("email") if isinstance(user, dict) else str(user) - tool = await tool_service.toggle_tool_status(db, tool_id, activate, reachable=activate, user_email=user_email) + tool = await tool_service.set_tool_state(db, tool_id, activate, reachable=activate, user_email=user_email) return { "status": "success", "message": f"Tool {tool_id} {'activated' if activate else 'deactivated'}", @@ -2753,9 +2761,9 @@ async def list_resource_templates( return ListResourceTemplatesResult(_meta={}, resource_templates=resource_templates, next_cursor=None) # No pagination for now -@resource_router.post("/{resource_id}/toggle") +@resource_router.post("/{resource_id}/state") @require_permission("resources.update") -async def toggle_resource_status( +async def set_resource_state( resource_id: int, activate: bool = True, db: Session = Depends(get_db), @@ -2776,10 +2784,10 @@ async def toggle_resource_status( Raises: HTTPException: If toggling fails. """ - logger.debug(f"User {user} is toggling resource with ID {resource_id} to {'active' if activate else 'inactive'}") + logger.debug(f"User {user} is setting resource with ID {resource_id} to {'active' if activate else 'inactive'}") try: user_email = user.get("email") if isinstance(user, dict) else str(user) - resource = await resource_service.toggle_resource_status(db, resource_id, activate, user_email=user_email) + resource = await resource_service.set_resource_state(db, resource_id, activate, user_email=user_email) return { "status": "success", "message": f"Resource {resource_id} {'activated' if activate else 'deactivated'}", @@ -3101,19 +3109,19 @@ async def subscribe_resource(resource_id: str, user=Depends(get_current_user_wit ############### # Prompt APIs # ############### -@prompt_router.post("/{prompt_id}/toggle") +@prompt_router.post("/{prompt_id}/state") @require_permission("prompts.update") -async def toggle_prompt_status( +async def set_prompt_state( prompt_id: int, activate: bool = True, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ) -> Dict[str, Any]: """ - Toggle the activation status of a prompt. + Set the activation state of a prompt. Args: - prompt_id: ID of the prompt to toggle. + prompt_id: ID of the prompt whose state to set. activate: True to activate, False to deactivate. db: Database session. user: Authenticated user. @@ -3122,12 +3130,12 @@ async def toggle_prompt_status( Status message and updated prompt details. Raises: - HTTPException: If the toggle fails (e.g., prompt not found or database error); emitted with *400 Bad Request* status and an error message. + HTTPException: If the state change fails (e.g., prompt not found or database error); emitted with *400 Bad Request* status and an error message. """ - logger.debug(f"User: {user} requested toggle for prompt {prompt_id}, activate={activate}") + logger.debug(f"User: {user} requested set state for prompt {prompt_id}, activate={activate}") try: user_email = user.get("email") if isinstance(user, dict) else str(user) - prompt = await prompt_service.toggle_prompt_status(db, prompt_id, activate, user_email=user_email) + prompt = await prompt_service.set_prompt_state(db, prompt_id, activate, user_email=user_email) return { "status": "success", "message": f"Prompt {prompt_id} {'activated' if activate else 'deactivated'}", @@ -3460,19 +3468,19 @@ async def delete_prompt(prompt_id: str, db: Session = Depends(get_db), user=Depe ################ # Gateway APIs # ################ -@gateway_router.post("/{gateway_id}/toggle") +@gateway_router.post("/{gateway_id}/state") @require_permission("gateways.update") -async def toggle_gateway_status( +async def set_gateway_state( gateway_id: str, activate: bool = True, db: Session = Depends(get_db), user=Depends(get_current_user_with_permissions), ) -> Dict[str, Any]: """ - Toggle the activation status of a gateway. + Set the activation state of a gateway. Args: - gateway_id (str): String ID of the gateway to toggle. + gateway_id (str): String ID of the gateway whose state to set. activate (bool): ``True`` to activate, ``False`` to deactivate. db (Session): Active SQLAlchemy session. user (str): Authenticated username. @@ -3481,12 +3489,12 @@ async def toggle_gateway_status( Dict[str, Any]: A dict containing the operation status, a message, and the updated gateway object. Raises: - HTTPException: Returned with **400 Bad Request** if the toggle operation fails (e.g., the gateway does not exist or the database raises an unexpected error). + HTTPException: Returned with **400 Bad Request** if the state change fails (e.g., the gateway does not exist or the database raises an unexpected error). """ - logger.debug(f"User '{user}' requested toggle for gateway {gateway_id}, activate={activate}") + logger.debug(f"User '{user}' requested set state for gateway {gateway_id}, activate={activate}") try: user_email = user.get("email") if isinstance(user, dict) else str(user) - gateway = await gateway_service.toggle_gateway_status( + gateway = await gateway_service.set_gateway_state( db, gateway_id, activate, diff --git a/mcpgateway/services/a2a_service.py b/mcpgateway/services/a2a_service.py index 33f3468d0..b697f448a 100644 --- a/mcpgateway/services/a2a_service.py +++ b/mcpgateway/services/a2a_service.py @@ -119,7 +119,7 @@ def __init__(self, name: str, is_active: bool = True, agent_id: Optional[str] = class A2AAgentService: """Service for managing A2A agents in the gateway. - Provides methods to create, list, retrieve, update, toggle status, and delete agent records. + Provides methods to create, list, retrieve, update, set state, and delete agent records. Also supports interactions with A2A-compatible agents. """ @@ -675,8 +675,8 @@ async def update_agent( db.rollback() raise A2AAgentError(f"Failed to update A2A agent: {str(e)}") - async def toggle_agent_status(self, db: Session, agent_id: str, activate: bool, reachable: Optional[bool] = None, user_email: Optional[str] = None) -> A2AAgentRead: - """Toggle the activation status of an A2A agent. + async def set_a2a_agent_state(self, db: Session, agent_id: str, activate: bool, reachable: Optional[bool] = None, user_email: Optional[str] = None) -> A2AAgentRead: + """Set the activation state of an A2A agent. Args: db: Database session. @@ -718,6 +718,9 @@ async def toggle_agent_status(self, db: Session, agent_id: str, activate: bool, return self._db_to_schema(db=db, db_agent=agent) + # Backwards-compatible alias + toggle_agent_status = set_a2a_agent_state + async def delete_agent(self, db: Session, agent_id: str, user_email: Optional[str] = None) -> None: """Delete an A2A agent. diff --git a/mcpgateway/services/gateway_service.py b/mcpgateway/services/gateway_service.py index 722b27799..c64b0c775 100644 --- a/mcpgateway/services/gateway_service.py +++ b/mcpgateway/services/gateway_service.py @@ -1546,9 +1546,9 @@ async def get_gateway(self, db: Session, gateway_id: str, include_inactive: bool raise GatewayNotFoundError(f"Gateway not found: {gateway_id}") - async def toggle_gateway_status(self, db: Session, gateway_id: str, activate: bool, reachable: bool = True, only_update_reachable: bool = False, user_email: Optional[str] = None) -> GatewayRead: + async def set_gateway_state(self, db: Session, gateway_id: str, activate: bool, reachable: bool = True, only_update_reachable: bool = False, user_email: Optional[str] = None) -> GatewayRead: """ - Toggle the activation status of a gateway. + Set the activation state of a gateway. Args: db: Database session @@ -1671,10 +1671,10 @@ async def toggle_gateway_status(self, db: Session, gateway_id: str, activate: bo if only_update_reachable: for tool in tools: - await self.tool_service.toggle_tool_status(db, tool.id, tool.enabled, reachable) + await self.tool_service.set_tool_state(db, tool.id, tool.enabled, reachable) else: for tool in tools: - await self.tool_service.toggle_tool_status(db, tool.id, activate, reachable) + await self.tool_service.set_tool_state(db, tool.id, activate, reachable) # Notify subscribers if activate: @@ -1691,7 +1691,10 @@ async def toggle_gateway_status(self, db: Session, gateway_id: str, activate: bo raise e except Exception as e: db.rollback() - raise GatewayError(f"Failed to toggle gateway status: {str(e)}") + raise GatewayError(f"Failed to set gateway state: {str(e)}") + + # Backwards-compatible alias + toggle_gateway_status = set_gateway_state async def _notify_gateway_updated(self, gateway: DbGateway) -> None: """ @@ -2073,7 +2076,7 @@ async def _handle_gateway_failure(self, gateway: DbGateway) -> None: if count >= GW_FAILURE_THRESHOLD: logger.error(f"Gateway {gateway.name} failed {GW_FAILURE_THRESHOLD} times. Deactivating...") with cast(Any, SessionLocal)() as db: - await self.toggle_gateway_status(db, gateway.id, activate=True, reachable=False, only_update_reachable=True) + await self.set_gateway_state(db, gateway.id, activate=True, reachable=False, only_update_reachable=True) self._gateway_failure_counts[gateway.id] = 0 # Reset after deactivation async def check_health_of_gateways(self, db: Session, gateways: List[DbGateway], user_email: Optional[str] = None) -> bool: @@ -2272,7 +2275,7 @@ def get_httpx_client_factory( # Reactivate gateway if it was previously inactive and health check passed now if gateway.enabled and not gateway.reachable: logger.info(f"Reactivating gateway: {gateway.name}, as it is healthy now") - await self.toggle_gateway_status(db, gateway.id, activate=True, reachable=True, only_update_reachable=True) + await self.set_gateway_state(db, gateway.id, activate=True, reachable=True, only_update_reachable=True) # Mark successful check gateway.last_seen = datetime.now(timezone.utc) diff --git a/mcpgateway/services/grpc_service.py b/mcpgateway/services/grpc_service.py index 221a5beac..4f042fc59 100644 --- a/mcpgateway/services/grpc_service.py +++ b/mcpgateway/services/grpc_service.py @@ -278,13 +278,13 @@ async def update_service( return GrpcServiceRead.model_validate(service) - async def toggle_service( + async def set_grpc_service_state( self, db: Session, service_id: str, activate: bool, ) -> GrpcServiceRead: - """Toggle a gRPC service's enabled status. + """Set a gRPC service's enabled state. Args: db: Database session @@ -313,6 +313,9 @@ async def toggle_service( return GrpcServiceRead.model_validate(service) + # Backwards-compatible alias + toggle_service = set_grpc_service_state + async def delete_service( self, db: Session, diff --git a/mcpgateway/services/prompt_service.py b/mcpgateway/services/prompt_service.py index 456ed2461..f549fca37 100644 --- a/mcpgateway/services/prompt_service.py +++ b/mcpgateway/services/prompt_service.py @@ -1014,9 +1014,9 @@ async def update_prompt( db.rollback() raise PromptError(f"Failed to update prompt: {str(e)}") - async def toggle_prompt_status(self, db: Session, prompt_id: int, activate: bool, user_email: Optional[str] = None) -> PromptRead: + async def set_prompt_state(self, db: Session, prompt_id: int, activate: bool, user_email: Optional[str] = None) -> PromptRead: """ - Toggle the activation status of a prompt. + Set the activation state of a prompt. Args: db: Database session @@ -1046,7 +1046,7 @@ async def toggle_prompt_status(self, db: Session, prompt_id: int, activate: bool >>> service._convert_db_prompt = MagicMock(return_value={}) >>> import asyncio >>> try: - ... asyncio.run(service.toggle_prompt_status(db, 1, True)) + ... asyncio.run(service.set_prompt_state(db, 1, True)) ... except Exception: ... pass """ @@ -1079,7 +1079,10 @@ async def toggle_prompt_status(self, db: Session, prompt_id: int, activate: bool raise e except Exception as e: db.rollback() - raise PromptError(f"Failed to toggle prompt status: {str(e)}") + raise PromptError(f"Failed to set prompt state: {str(e)}") + + # Backwards-compatible alias + toggle_prompt_status = set_prompt_state # Get prompt details for admin ui async def get_prompt_details(self, db: Session, prompt_id: Union[int, str], include_inactive: bool = False) -> Dict[str, Any]: # pylint: disable=unused-argument diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index 5bc52b450..a42d6edb1 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -940,9 +940,9 @@ async def read_resource( except Exception as e: logger.warning(f"Failed to end observability span for resource reading: {e}") - async def toggle_resource_status(self, db: Session, resource_id: int, activate: bool, user_email: Optional[str] = None) -> ResourceRead: + async def set_resource_state(self, db: Session, resource_id: int, activate: bool, user_email: Optional[str] = None) -> ResourceRead: """ - Toggle the activation status of a resource. + Set the activation state of a resource. Args: db: Database session @@ -973,7 +973,7 @@ async def toggle_resource_status(self, db: Session, resource_id: int, activate: >>> service._convert_resource_to_read = MagicMock(return_value='resource_read') >>> ResourceRead.model_validate = MagicMock(return_value='resource_read') >>> import asyncio - >>> asyncio.run(service.toggle_resource_status(db, 1, True)) + >>> asyncio.run(service.set_resource_state(db, 1, True)) 'resource_read' """ try: @@ -1010,7 +1010,10 @@ async def toggle_resource_status(self, db: Session, resource_id: int, activate: raise e except Exception as e: db.rollback() - raise ResourceError(f"Failed to toggle resource status: {str(e)}") + raise ResourceError(f"Failed to set resource state: {str(e)}") + + # Backwards-compatible alias + toggle_resource_status = set_resource_state async def subscribe_resource(self, db: Session, subscription: ResourceSubscription) -> None: """ @@ -1360,6 +1363,8 @@ async def get_resource_by_id(self, db: Session, resource_id: int, include_inacti raise ResourceNotFoundError(f"Resource not found: {resource_id}") + # Ensure team name is resolved before conversion (consistent with ToolService.get_tool) + resource.team = self._get_team_name(db, getattr(resource, "team_id", None)) return self._convert_resource_to_read(resource) async def _notify_resource_activated(self, resource: DbResource) -> None: diff --git a/mcpgateway/services/server_service.py b/mcpgateway/services/server_service.py index f93c21df3..59d035425 100644 --- a/mcpgateway/services/server_service.py +++ b/mcpgateway/services/server_service.py @@ -103,7 +103,7 @@ def __init__(self, name: str, is_active: bool = True, server_id: Optional[str] = class ServerService: """Service for managing MCP Servers in the catalog. - Provides methods to create, list, retrieve, update, toggle status, and delete server records. + Provides methods to create, list, retrieve, update, set state, and delete server records. Also supports event notifications for changes in server data. """ @@ -879,8 +879,8 @@ async def update_server( db.rollback() raise ServerError(f"Failed to update server: {str(e)}") - async def toggle_server_status(self, db: Session, server_id: str, activate: bool, user_email: Optional[str] = None) -> ServerRead: - """Toggle the activation status of a server. + async def set_server_state(self, db: Session, server_id: str, activate: bool, user_email: Optional[str] = None) -> ServerRead: + """Set the activation state of a server. Args: db: Database session. @@ -911,7 +911,7 @@ async def toggle_server_status(self, db: Session, server_id: str, activate: bool >>> service._convert_server_to_read = MagicMock(return_value='server_read') >>> ServerRead.model_validate = MagicMock(return_value='server_read') >>> import asyncio - >>> asyncio.run(service.toggle_server_status(db, 'server_id', True)) + >>> asyncio.run(service.set_server_state(db, 'server_id', True)) 'server_read' """ try: @@ -957,7 +957,10 @@ async def toggle_server_status(self, db: Session, server_id: str, activate: bool raise e except Exception as e: db.rollback() - raise ServerError(f"Failed to toggle server status: {str(e)}") + raise ServerError(f"Failed to set server state: {str(e)}") + + # Backwards-compatible alias + toggle_server_status = set_server_state async def delete_server(self, db: Session, server_id: str, user_email: Optional[str] = None) -> None: """Permanently delete a server. diff --git a/mcpgateway/services/tool_service.py b/mcpgateway/services/tool_service.py index 1baaf6e25..1f211675c 100644 --- a/mcpgateway/services/tool_service.py +++ b/mcpgateway/services/tool_service.py @@ -1000,9 +1000,9 @@ async def delete_tool(self, db: Session, tool_id: str, user_email: Optional[str] db.rollback() raise ToolError(f"Failed to delete tool: {str(e)}") - async def toggle_tool_status(self, db: Session, tool_id: str, activate: bool, reachable: bool, user_email: Optional[str] = None) -> ToolRead: + async def set_tool_state(self, db: Session, tool_id: str, activate: bool, reachable: bool, user_email: Optional[str] = None) -> ToolRead: """ - Toggle the activation status of a tool. + Set the activation state of a tool. Args: db (Session): The SQLAlchemy database session. @@ -1034,7 +1034,7 @@ async def toggle_tool_status(self, db: Session, tool_id: str, activate: bool, re >>> service._convert_tool_to_read = MagicMock(return_value='tool_read') >>> ToolRead.model_validate = MagicMock(return_value='tool_read') >>> import asyncio - >>> asyncio.run(service.toggle_tool_status(db, 'tool_id', True, True)) + >>> asyncio.run(service.set_tool_state(db, 'tool_id', True, True)) 'tool_read' """ try: @@ -1074,7 +1074,10 @@ async def toggle_tool_status(self, db: Session, tool_id: str, activate: bool, re raise e except Exception as e: db.rollback() - raise ToolError(f"Failed to toggle tool status: {str(e)}") + raise ToolError(f"Failed to set tool state: {str(e)}") + + # Backwards-compatible alias + toggle_tool_status = set_tool_state async def invoke_tool(self, db: Session, name: str, arguments: Dict[str, Any], request_headers: Optional[Dict[str, str]] = None, app_user_email: Optional[str] = None) -> ToolResult: """ @@ -1620,9 +1623,9 @@ async def update_tool( db.rollback() logger.error(f"Tool name conflict during update: {tnce}") raise tnce - except Exception as ex: + except Exception as e: db.rollback() - raise ToolError(f"Failed to update tool: {str(ex)}") + raise ToolError(f"Failed to update tool: {str(e)}") async def _notify_tool_updated(self, tool: DbTool) -> None: """ diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index 3fe94e430..a27833dff 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -1962,7 +1962,7 @@

{% if server.isActive %}
@@ -1978,7 +1978,7 @@

{% else %} @@ -4173,7 +4173,7 @@

{% if gateway.enabled %} @@ -4189,7 +4189,7 @@

{% else %} @@ -5438,7 +5438,7 @@

{% if agent.enabled %} - + {% else %} -
+
- + ` : - `
+ `
`; diff --git a/mcpgateway/templates/prompts_partial.html b/mcpgateway/templates/prompts_partial.html index ff3b2914e..b813794fc 100644 --- a/mcpgateway/templates/prompts_partial.html +++ b/mcpgateway/templates/prompts_partial.html @@ -41,7 +41,7 @@
-
+
diff --git a/mcpgateway/templates/resources_partial.html b/mcpgateway/templates/resources_partial.html index 5d013f0c1..a72c37e27 100644 --- a/mcpgateway/templates/resources_partial.html +++ b/mcpgateway/templates/resources_partial.html @@ -34,12 +34,12 @@
{% if resource.isActive %} -
+
{% else %} -
+
diff --git a/mcpgateway/templates/tools_partial.html b/mcpgateway/templates/tools_partial.html index 3436696a3..baeb58457 100644 --- a/mcpgateway/templates/tools_partial.html +++ b/mcpgateway/templates/tools_partial.html @@ -222,7 +222,7 @@ {% if tool.enabled %}
@@ -238,7 +238,7 @@ {% else %} diff --git a/mcpgateway/templates/tools_with_pagination.html b/mcpgateway/templates/tools_with_pagination.html index f7a628374..41cc7ffa2 100644 --- a/mcpgateway/templates/tools_with_pagination.html +++ b/mcpgateway/templates/tools_with_pagination.html @@ -128,14 +128,14 @@
{% if tool.enabled %} - + {% else %} -
+
{% else %} -
+