Skip to content

Commit b9d3af4

Browse files
More visibility for gateway and tool status and auto healing capabilities (#215)
* changes implemented reachable and enabled as parameters for status of gateways and tools Signed-off-by: <Keval Mahajan> <[email protected]> * Updated test cases as per new changes for status in gateways and tools Signed-off-by: <Keval Mahajan> <[email protected]> * minor changes Signed-off-by: <Keval Mahajan> <[email protected]> * black and isort Signed-off-by: <Keval Mahajan> <[email protected]> * Alembic script for db migration Signed-off-by: <Keval Mahajan> <[email protected]> * offline tools rpc response Signed-off-by: <Keval Mahajan> <[email protected]> * offline tools rpc response message Signed-off-by: <Keval Mahajan> <[email protected]> * minor changes Signed-off-by: <Keval Mahajan> <[email protected]> * minor flake8 issues Signed-off-by: <Keval Mahajan> <[email protected]> * db migration for postgresql Signed-off-by: <Keval Mahajan> <[email protected]> * Check for existing tables in migration script Signed-off-by: Madhav Kandukuri <[email protected]> * db migration from status to enabled and reachable Signed-off-by: <Keval Mahajan> <[email protected]> * reachable value is updated as necessary Signed-off-by: <Keval Mahajan> <[email protected]> * Updated test cases and linting Signed-off-by: Keval Mahajan <[email protected]> * Use bootstrap script Signed-off-by: Madhav Kandukuri <[email protected]> * linting Signed-off-by: Keval Mahajan <[email protected]> * Use bootstrap script in main for table creation Signed-off-by: Madhav Kandukuri <[email protected]> * Revert "Use bootstrap script in main for table creation" This reverts commit 20fcceb. * Revert "linting" This reverts commit 2ecc431. * Revert "Use bootstrap script" This reverts commit d579e8e. --------- Signed-off-by: <Keval Mahajan> <[email protected]> Signed-off-by: Keval Mahajan <[email protected]> Co-authored-by: Madhav Kandukuri <[email protected]>
1 parent 738ca8a commit b9d3af4

File tree

16 files changed

+311
-113
lines changed

16 files changed

+311
-113
lines changed

alembic/versions/e4fc04d1a442_add_annotations_to_tables.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
# Third-Party
1616
import sqlalchemy as sa
17+
from sqlalchemy.orm import Session
1718

1819
# revision identifiers, used by Alembic.
1920
revision: str = 'e4fc04d1a442'
@@ -30,6 +31,14 @@ def upgrade() -> None:
3031
table. It includes a server-side default of an empty JSON object ('{}') to ensure
3132
that existing rows get a non-null default value.
3233
"""
34+
bind = op.get_bind()
35+
sess = Session(bind=bind)
36+
inspector = sa.inspect(bind)
37+
38+
if not inspector.has_table("gateways"):
39+
print("Fresh database detected. Skipping migration.")
40+
return
41+
3342
op.add_column('tools', sa.Column('annotations', sa.JSON(), server_default=sa.text("'{}'"), nullable=False))
3443

3544

@@ -40,4 +49,12 @@ def downgrade() -> None:
4049
This function provides a way to undo the migration, safely removing the
4150
'annotations' column from the 'tool' table.
4251
"""
52+
bind = op.get_bind()
53+
sess = Session(bind=bind)
54+
inspector = sa.inspect(bind)
55+
56+
if not inspector.has_table("gateways"):
57+
print("Fresh database detected. Skipping migration.")
58+
return
59+
4360
op.drop_column('tools', 'annotations')
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Add enabled and reachable columns in tools and gateways tables and migrate data (is_active ➜ enabled,reachable).
2+
3+
Revision ID: e75490e949b1
4+
Revises: e4fc04d1a442
5+
Create Date: 2025‑07‑02 17:12:40.678256
6+
"""
7+
8+
# Standard
9+
from typing import Sequence, Union
10+
11+
# First-Party
12+
# Alembic / SQLAlchemy
13+
from alembic import op
14+
15+
# Third-Party
16+
import sqlalchemy as sa
17+
18+
# Revision identifiers.
19+
revision: str = "e75490e949b1"
20+
down_revision: Union[str, Sequence[str], None] = "e4fc04d1a442"
21+
branch_labels: Union[str, Sequence[str], None] = None
22+
depends_on: Union[str, Sequence[str], None] = None
23+
24+
25+
def upgrade():
26+
"""
27+
Renames 'is_active' to 'enabled' and adds a new 'reachable' column (default True)
28+
in both 'tools' and 'gateways' tables.
29+
"""
30+
op.alter_column('tools', 'is_active', new_column_name='enabled')
31+
op.add_column('tools', sa.Column('reachable', sa.Boolean(), nullable=False, server_default=sa.true()))
32+
33+
op.alter_column('gateways', 'is_active', new_column_name='enabled')
34+
op.add_column('gateways', sa.Column('reachable', sa.Boolean(), nullable=False, server_default=sa.true()))
35+
36+
37+
def downgrade():
38+
"""
39+
Reverts the changes by renaming 'enabled' back to 'is_active'
40+
and dropping the 'reachable' column in both 'tools' and 'gateways' tables.
41+
"""
42+
op.alter_column('tools', 'enabled', new_column_name='is_active')
43+
op.drop_column('tools', 'reachable')
44+
45+
op.alter_column('gateways', 'enabled', new_column_name='is_active')
46+
op.drop_column('gateways', 'reachable')

mcpgateway/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ async def admin_toggle_tool(
712712
form = await request.form()
713713
activate = form.get("activate", "true").lower() == "true"
714714
try:
715-
await tool_service.toggle_tool_status(db, tool_id, activate)
715+
await tool_service.toggle_tool_status(db, tool_id, activate, reachable=activate)
716716
except Exception as e:
717717
logger.error(f"Error toggling tool status: {e}")
718718

mcpgateway/db.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,8 @@ class Tool(Base):
323323
annotations: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, default=lambda: {})
324324
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
325325
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
326-
is_active: Mapped[bool] = mapped_column(default=True)
326+
enabled: Mapped[bool] = mapped_column(default=True)
327+
reachable: Mapped[bool] = mapped_column(default=True)
327328
jsonpath_filter: Mapped[str] = mapped_column(default="")
328329

329330
# Request type and authentication fields
@@ -1025,7 +1026,8 @@ class Gateway(Base):
10251026
capabilities: Mapped[Dict[str, Any]] = mapped_column(JSON)
10261027
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
10271028
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)
1028-
is_active: Mapped[bool] = mapped_column(default=True)
1029+
enabled: Mapped[bool] = mapped_column(default=True)
1030+
reachable: Mapped[bool] = mapped_column(default=True)
10291031
last_seen: Mapped[Optional[datetime]]
10301032

10311033
# Relationship with local tools this gateway provides

mcpgateway/federation/forward.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ async def forward_tool_request(self, db: Session, tool_name: str, arguments: Dic
126126
"""
127127
try:
128128
# Find tool
129-
tool = db.execute(select(DbTool).where(DbTool.name == tool_name).where(DbTool.is_active)).scalar_one_or_none()
129+
tool = db.execute(select(DbTool).where(DbTool.name == tool_name).where(DbTool.enabled)).scalar_one_or_none()
130130

131131
if not tool:
132132
raise ForwardingError(f"Tool not found: {tool_name}")
@@ -208,7 +208,7 @@ async def _forward_to_gateway(
208208
"""
209209
# Get gateway
210210
gateway = db.get(DbGateway, gateway_id)
211-
if not gateway or not gateway.is_active:
211+
if not gateway or not gateway.enabled:
212212
raise ForwardingError(f"Gateway not found: {gateway_id}")
213213

214214
# Check rate limits
@@ -263,7 +263,7 @@ async def _forward_to_all(self, db: Session, method: str, params: Optional[Dict[
263263
ForwardingError: If all forwards fail
264264
"""
265265
# Get active gateways
266-
gateways = db.execute(select(DbGateway).where(DbGateway.is_active)).scalars().all()
266+
gateways = db.execute(select(DbGateway).where(DbGateway.enabled)).scalars().all()
267267

268268
# Forward to each gateway
269269
results = []
@@ -292,7 +292,7 @@ async def _find_resource_gateway(self, db: Session, uri: str) -> Optional[DbGate
292292
Gateway record or None
293293
"""
294294
# Get active gateways
295-
gateways = db.execute(select(DbGateway).where(DbGateway.is_active)).scalars().all()
295+
gateways = db.execute(select(DbGateway).where(DbGateway.enabled)).scalars().all()
296296

297297
# Check each gateway
298298
for gateway in gateways:

mcpgateway/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,7 @@ async def create_tool(tool: ToolCreate, db: Session = Depends(get_db), user: str
937937
logger.debug(f"User {user} is creating a new tool")
938938
return await tool_service.register_tool(db, tool)
939939
except ToolNameConflictError as e:
940-
if not e.is_active and e.tool_id:
940+
if not e.enabled and e.tool_id:
941941
raise HTTPException(
942942
status_code=status.HTTP_409_CONFLICT,
943943
detail=f"Tool name already exists but is inactive. Consider activating it with ID: {e.tool_id}",
@@ -1060,7 +1060,7 @@ async def toggle_tool_status(
10601060
"""
10611061
try:
10621062
logger.debug(f"User {user} is toggling tool with ID {tool_id} to {'active' if activate else 'inactive'}")
1063-
tool = await tool_service.toggle_tool_status(db, tool_id, activate)
1063+
tool = await tool_service.toggle_tool_status(db, tool_id, activate, reachable=activate)
10641064
return {
10651065
"status": "success",
10661066
"message": f"Tool {tool_id} {'activated' if activate else 'deactivated'}",

mcpgateway/schemas.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,8 @@ class ToolRead(BaseModelWithConfigDict):
382382
Includes all tool fields plus:
383383
- Database ID
384384
- Creation/update timestamps
385-
- Active status
385+
- enabled: If Tool is enabled or disabled.
386+
- reachable: If Tool is reachable or not.
386387
- Gateway ID for federation
387388
- Execution count indicating the number of times the tool has been executed.
388389
- Metrics: Aggregated metrics for the tool invocations.
@@ -402,7 +403,8 @@ class ToolRead(BaseModelWithConfigDict):
402403
auth: Optional[AuthenticationValues]
403404
created_at: datetime
404405
updated_at: datetime
405-
is_active: bool
406+
enabled: bool
407+
reachable: bool
406408
gateway_id: Optional[str]
407409
execution_count: int
408410
metrics: ToolMetrics
@@ -855,7 +857,8 @@ class GatewayRead(BaseModelWithConfigDict):
855857
- Database ID
856858
- Capabilities dictionary
857859
- Creation/update timestamps
858-
- Active status
860+
- enabled status
861+
- reachable status
859862
- Last seen timestamp
860863
- Authentication type: basic, bearer, headers
861864
- Authentication value: username/password or token or custom headers
@@ -876,7 +879,9 @@ class GatewayRead(BaseModelWithConfigDict):
876879
capabilities: Dict[str, Any] = Field(default_factory=dict, description="Gateway capabilities")
877880
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="Creation timestamp")
878881
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="Last update timestamp")
879-
is_active: bool = Field(default=True, description="Is the gateway active?")
882+
enabled: bool = Field(default=True, description="Is the gateway enabled?")
883+
reachable: bool = Field(default=True, description="Is the gateway reachable/online?")
884+
880885
last_seen: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc), description="Last seen timestamp")
881886

882887
# Authorizations

0 commit comments

Comments
 (0)