Coverage for mcpgateway/admin.py: 79%

1# -*- coding: utf-8 -*-

2"""Admin UI Routes for MCP Gateway.

3

4 Copyright 2025

5 SPDX-License-Identifier: Apache-2.0

6 Authors: Mihai Criveti

7

8 This module contains all the administrative UI endpoints for the MCP Gateway.

9 It provides a comprehensive interface for managing servers, tools, resources,

10 prompts, gateways, and roots through RESTful API endpoints. The module handles

11 all aspects of CRUD operations for these entities, including creation,

12 reading, updating, deletion, and status toggling.

13

14 All endpoints in this module require authentication, which is enforced via

15 the require_auth or require_basic_auth dependency. The module integrates with

16 various services to perform the actual business logic operations on the

17 underlying data.

18"""

19

20# Standard

21 import json

22 import logging

23 import time

24 from typing import Any, Dict, List, Union

25

26# Third-Party

27 from fastapi import APIRouter, Depends, HTTPException, Request

28 from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse

29 import httpx

30 from sqlalchemy.orm import Session

31

32# First-Party

33 from mcpgateway.config import settings

34 from mcpgateway.db import get_db

35 from mcpgateway.schemas import (

36 GatewayCreate,

37 GatewayRead,

38 GatewayTestRequest,

39 GatewayTestResponse,

40 GatewayUpdate,

41 PromptCreate,

42 PromptMetrics,

43 PromptRead,

44 PromptUpdate,

45 ResourceCreate,

46 ResourceMetrics,

47 ResourceRead,

48 ResourceUpdate,

49 ServerCreate,

50 ServerMetrics,

51 ServerRead,

52 ServerUpdate,

53 ToolCreate,

54 ToolMetrics,

55 ToolRead,

56 ToolUpdate,

57)

58 from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayService

59 from mcpgateway.services.prompt_service import PromptService

60 from mcpgateway.services.resource_service import ResourceService

61 from mcpgateway.services.root_service import RootService

62 from mcpgateway.services.server_service import ServerNotFoundError, ServerService

63 from mcpgateway.services.tool_service import (

64 ToolError,

65 ToolNameConflictError,

66 ToolService,

67)

68 from mcpgateway.utils.create_jwt_token import get_jwt_token

69 from mcpgateway.utils.verify_credentials import require_auth, require_basic_auth

70

71# Initialize services

72 server_service = ServerService()

73 tool_service = ToolService()

74 prompt_service = PromptService()

75 gateway_service = GatewayService()

76 resource_service = ResourceService()

77 root_service = RootService()

78

79# Set up basic authentication

80 logger = logging.getLogger("mcpgateway")

81

82 admin_router = APIRouter(prefix="/admin", tags=["Admin UI"])

83

84####################

85# Admin UI Routes #

86####################

87

88

89@admin_router.get("/servers", response_model=List[ServerRead])

90 async def admin_list_servers(

91 include_inactive: bool = False,

92 db: Session = Depends(get_db),

93 user: str = Depends(require_auth),

94) -> List[ServerRead]:

95 """

96 List servers for the admin UI with an option to include inactive servers.

97

98 Args:

99 include_inactive (bool): Whether to include inactive servers.

100 db (Session): The database session dependency.

101 user (str): The authenticated user dependency.

102

103 Returns:

104 List[ServerRead]: A list of server records.

105 """

106 logger.debug(f"User {user} requested server list")

107 servers = await server_service.list_servers(db, include_inactive=include_inactive)

108 return [server.model_dump(by_alias=True) for server in servers]

109

110

111@admin_router.get("/servers/{server_id}", response_model=ServerRead)

112 async def admin_get_server(server_id: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> ServerRead:

113 """

114 Retrieve server details for the admin UI.

115

116 Args:

117 server_id (str): The ID of the server to retrieve.

118 db (Session): The database session dependency.

119 user (str): The authenticated user dependency.

120

121 Returns:

122 ServerRead: The server details.

123

124 Raises:

125 HTTPException: If the server is not found.

126 """

127 try:

128 logger.debug(f"User {user} requested details for server ID {server_id}")

129 server = await server_service.get_server(db, server_id)

130 return server.model_dump(by_alias=True)

131 except ServerNotFoundError as e:

132 raise HTTPException(status_code=404, detail=str(e))

133

134

135@admin_router.post("/servers", response_model=ServerRead)

136 async def admin_add_server(request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:

137 """

138 Add a new server via the admin UI.

139

140 This endpoint processes form data to create a new server entry in the database.

141 It handles exceptions gracefully and logs any errors that occur during server

142 registration.

143

144 Expects form fields:

145 - name (required): The name of the server

146 - description (optional): A description of the server's purpose

147 - icon (optional): URL or path to the server's icon

148 - associatedTools (optional, comma-separated): Tools associated with this server

149 - associatedResources (optional, comma-separated): Resources associated with this server

150 - associatedPrompts (optional, comma-separated): Prompts associated with this server

151

152 Args:

153 request (Request): FastAPI request containing form data.

154 db (Session): Database session dependency

155 user (str): Authenticated user dependency

156

157 Returns:

158 RedirectResponse: A redirect to the admin dashboard catalog section

159 """

160 form = await request.form()

161 is_inactive_checked = form.get("is_inactive_checked", "false")

162 try:

163 logger.debug(f"User {user} is adding a new server with name: {form['name']}")

164

165 server = ServerCreate(

166 name=form.get("name"),

167 description=form.get("description"),

168 icon=form.get("icon"),

169 associated_tools=",".join(form.getlist("associatedTools")),

170 associated_resources=form.get("associatedResources"),

171 associated_prompts=form.get("associatedPrompts"),

172 )

173 await server_service.register_server(db, server)

174

175 root_path = request.scope.get("root_path", "")

176 if is_inactive_checked.lower() == "true": 176 ↛ 177line 176 didn't jump to line 177 because the condition on line 176 was never true

177 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)

178 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)

179 except Exception as e:

180 logger.error(f"Error adding server: {e}")

181

182 root_path = request.scope.get("root_path", "")

183 if is_inactive_checked.lower() == "true":

184 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)

185 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)

186

187

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

189 async def admin_edit_server(

190 server_id: str,

191 request: Request,

192 db: Session = Depends(get_db),

193 user: str = Depends(require_auth),

194) -> RedirectResponse:

195 """

196 Edit an existing server via the admin UI.

197

198 This endpoint processes form data to update an existing server's properties.

199 It handles exceptions gracefully and logs any errors that occur during the

200 update operation.

201

202 Expects form fields:

203 - name (optional): The updated name of the server

204 - description (optional): An updated description of the server's purpose

205 - icon (optional): Updated URL or path to the server's icon

206 - associatedTools (optional, comma-separated): Updated list of tools associated with this server

207 - associatedResources (optional, comma-separated): Updated list of resources associated with this server

208 - associatedPrompts (optional, comma-separated): Updated list of prompts associated with this server

209

210 Args:

211 server_id (str): The ID of the server to edit

212 request (Request): FastAPI request containing form data

213 db (Session): Database session dependency

214 user (str): Authenticated user dependency

215

216 Returns:

217 RedirectResponse: A redirect to the admin dashboard catalog section with a status code of 303

218 """

219 form = await request.form()

220 is_inactive_checked = form.get("is_inactive_checked", "false")

221 try:

222 logger.debug(f"User {user} is editing server ID {server_id} with name: {form.get('name')}")

223 server = ServerUpdate(

224 name=form.get("name"),

225 description=form.get("description"),

226 icon=form.get("icon"),

227 associated_tools=",".join(form.getlist("associatedTools")),

228 associated_resources=form.get("associatedResources"),

229 associated_prompts=form.get("associatedPrompts"),

230 )

231 await server_service.update_server(db, server_id, server)

232

233 root_path = request.scope.get("root_path", "")

234

235 if is_inactive_checked.lower() == "true": 235 ↛ 236line 235 didn't jump to line 236 because the condition on line 235 was never true

236 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)

237 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)

238 except Exception as e:

239 logger.error(f"Error editing server: {e}")

240

241 root_path = request.scope.get("root_path", "")

242 if is_inactive_checked.lower() == "true":

243 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)

244 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)

245

246

247@admin_router.post("/servers/{server_id}/toggle")

248 async def admin_toggle_server(

249 server_id: str,

250 request: Request,

251 db: Session = Depends(get_db),

252 user: str = Depends(require_auth),

253) -> RedirectResponse:

254 """

255 Toggle a server's active status via the admin UI.

256

257 This endpoint processes a form request to activate or deactivate a server.

258 It expects a form field 'activate' with value "true" to activate the server

259 or "false" to deactivate it. The endpoint handles exceptions gracefully and

260 logs any errors that might occur during the status toggle operation.

261

262 Args:

263 server_id (str): The ID of the server whose status to toggle.

264 request (Request): FastAPI request containing form data with the 'activate' field.

265 db (Session): Database session dependency.

266 user (str): Authenticated user dependency.

267

268 Returns:

269 RedirectResponse: A redirect to the admin dashboard catalog section with a

270 status code of 303 (See Other).

271 """

272 form = await request.form()

273 logger.debug(f"User {user} is toggling server ID {server_id} with activate: {form.get('activate')}")

274 activate = form.get("activate", "true").lower() == "true"

275 is_inactive_checked = form.get("is_inactive_checked", "false")

276 try:

277 await server_service.toggle_server_status(db, server_id, activate)

278 except Exception as e:

279 logger.error(f"Error toggling server status: {e}")

280

281 root_path = request.scope.get("root_path", "")

282 if is_inactive_checked.lower() == "true": 282 ↛ 283line 282 didn't jump to line 283 because the condition on line 282 was never true

283 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)

284 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)

285

286

287@admin_router.post("/servers/{server_id}/delete")

288 async def admin_delete_server(server_id: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:

289 """

290 Delete a server via the admin UI.

291

292 This endpoint removes a server from the database by its ID. It handles exceptions

293 gracefully and logs any errors that occur during the deletion process.

294

295 Args:

296 server_id (str): The ID of the server to delete

297 request (Request): FastAPI request object (not used but required by route signature).

298 db (Session): Database session dependency

299 user (str): Authenticated user dependency

300

301 Returns:

302 RedirectResponse: A redirect to the admin dashboard catalog section with a

303 status code of 303 (See Other)

304 """

305 try:

306 logger.debug(f"User {user} is deleting server ID {server_id}")

307 await server_service.delete_server(db, server_id)

308 except Exception as e:

309 logger.error(f"Error deleting server: {e}")

310

311 form = await request.form()

312 is_inactive_checked = form.get("is_inactive_checked", "false")

313 root_path = request.scope.get("root_path", "")

314

315 if is_inactive_checked.lower() == "true": 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true

316 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)

317 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)

318

319

320@admin_router.get("/resources", response_model=List[ResourceRead])

321 async def admin_list_resources(

322 include_inactive: bool = False,

323 db: Session = Depends(get_db),

324 user: str = Depends(require_auth),

325) -> List[ResourceRead]:

326 """

327 List resources for the admin UI with an option to include inactive resources.

328

329 This endpoint retrieves a list of resources from the database, optionally including

330 those that are inactive. The inactive filter is useful for administrators who need

331 to view or manage resources that have been deactivated but not deleted.

332

333 Args:

334 include_inactive (bool): Whether to include inactive resources in the results.

335 db (Session): Database session dependency.

336 user (str): Authenticated user dependency.

337

338 Returns:

339 List[ResourceRead]: A list of resource records formatted with by_alias=True.

340 """

341 logger.debug(f"User {user} requested resource list")

342 resources = await resource_service.list_resources(db, include_inactive=include_inactive)

343 return [resource.model_dump(by_alias=True) for resource in resources]

344

345

346@admin_router.get("/prompts", response_model=List[PromptRead])

347 async def admin_list_prompts(

348 include_inactive: bool = False,

349 db: Session = Depends(get_db),

350 user: str = Depends(require_auth),

351) -> List[PromptRead]:

352 """

353 List prompts for the admin UI with an option to include inactive prompts.

354

355 This endpoint retrieves a list of prompts from the database, optionally including

356 those that are inactive. The inactive filter helps administrators see and manage

357 prompts that have been deactivated but not deleted from the system.

358

359 Args:

360 include_inactive (bool): Whether to include inactive prompts in the results.

361 db (Session): Database session dependency.

362 user (str): Authenticated user dependency.

363

364 Returns:

365 List[PromptRead]: A list of prompt records formatted with by_alias=True.

366 """

367 logger.debug(f"User {user} requested prompt list")

368 prompts = await prompt_service.list_prompts(db, include_inactive=include_inactive)

369 return [prompt.model_dump(by_alias=True) for prompt in prompts]

370

371

372@admin_router.get("/gateways", response_model=List[GatewayRead])

373 async def admin_list_gateways(

374 include_inactive: bool = False,

375 db: Session = Depends(get_db),

376 user: str = Depends(require_auth),

377) -> List[GatewayRead]:

378 """

379 List gateways for the admin UI with an option to include inactive gateways.

380

381 This endpoint retrieves a list of gateways from the database, optionally

382 including those that are inactive. The inactive filter allows administrators

383 to view and manage gateways that have been deactivated but not deleted.

384

385 Args:

386 include_inactive (bool): Whether to include inactive gateways in the results.

387 db (Session): Database session dependency.

388 user (str): Authenticated user dependency.

389

390 Returns:

391 List[GatewayRead]: A list of gateway records formatted with by_alias=True.

392 """

393 logger.debug(f"User {user} requested gateway list")

394 gateways = await gateway_service.list_gateways(db, include_inactive=include_inactive)

395 return [gateway.model_dump(by_alias=True) for gateway in gateways]

396

397

398@admin_router.post("/gateways/{gateway_id}/toggle")

399 async def admin_toggle_gateway(

400 gateway_id: str,

401 request: Request,

402 db: Session = Depends(get_db),

403 user: str = Depends(require_auth),

404) -> RedirectResponse:

405 """

406 Toggle the active status of a gateway via the admin UI.

407

408 This endpoint allows an admin to toggle the active status of a gateway.

409 It expects a form field 'activate' with a value of "true" or "false" to

410 determine the new status of the gateway.

411

412 Args:

413 gateway_id (str): The ID of the gateway to toggle.

414 request (Request): The FastAPI request object containing form data.

415 db (Session): The database session dependency.

416 user (str): The authenticated user dependency.

417

418 Returns:

419 RedirectResponse: A redirect response to the admin dashboard with a

420 status code of 303 (See Other).

421 """

422 logger.debug(f"User {user} is toggling gateway ID {gateway_id}")

423 form = await request.form()

424 activate = form.get("activate", "true").lower() == "true"

425 is_inactive_checked = form.get("is_inactive_checked", "false")

426

427 try:

428 await gateway_service.toggle_gateway_status(db, gateway_id, activate)

429 except Exception as e:

430 logger.error(f"Error toggling gateway status: {e}")

431

432 root_path = request.scope.get("root_path", "")

433 if is_inactive_checked.lower() == "true": 433 ↛ 434line 433 didn't jump to line 434 because the condition on line 433 was never true

434 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#gateways", status_code=303)

435 return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)

436

437

438@admin_router.get("/", name="admin_home", response_class=HTMLResponse)

439 async def admin_ui(

440 request: Request,

441 include_inactive: bool = False,

442 db: Session = Depends(get_db),

443 user: str = Depends(require_basic_auth),

444 jwt_token: str = Depends(get_jwt_token),

445) -> HTMLResponse:

446 """

447 Render the admin dashboard HTML page.

448

449 This endpoint serves as the main entry point to the admin UI. It fetches data for

450 servers, tools, resources, prompts, gateways, and roots from their respective

451 services, then renders the admin dashboard template with this data.

452

453 The endpoint also sets a JWT token as a cookie for authentication in subsequent

454 requests. This token is HTTP-only for security reasons.

455

456 Args:

457 request (Request): FastAPI request object.

458 include_inactive (bool): Whether to include inactive items in all listings.

459 db (Session): Database session dependency.

460 user (str): Authenticated user from basic auth dependency.

461 jwt_token (str): JWT token for authentication.

462

463 Returns:

464 HTMLResponse: Rendered HTML template for the admin dashboard.

465 """

466 logger.debug(f"User {user} accessed the admin UI")

467 servers = [server.model_dump(by_alias=True) for server in await server_service.list_servers(db, include_inactive=include_inactive)]

468 tools = [tool.model_dump(by_alias=True) for tool in await tool_service.list_tools(db, include_inactive=include_inactive)]

469 resources = [resource.model_dump(by_alias=True) for resource in await resource_service.list_resources(db, include_inactive=include_inactive)]

470 prompts = [prompt.model_dump(by_alias=True) for prompt in await prompt_service.list_prompts(db, include_inactive=include_inactive)]

471 gateways = [gateway.model_dump(by_alias=True) for gateway in await gateway_service.list_gateways(db, include_inactive=include_inactive)]

472 roots = [root.model_dump(by_alias=True) for root in await root_service.list_roots()]

473 root_path = settings.app_root_path

474 response = request.app.state.templates.TemplateResponse(

475 request,

476 "admin.html",

477 {

478 "request": request,

479 "servers": servers,

480 "tools": tools,

481 "resources": resources,

482 "prompts": prompts,

483 "gateways": gateways,

484 "roots": roots,

485 "include_inactive": include_inactive,

486 "root_path": root_path,

487 "gateway_tool_name_separator": settings.gateway_tool_name_separator,

488 },

489 )

490

491 response.set_cookie(key="jwt_token", value=jwt_token, httponly=True, secure=False, samesite="Strict") # JavaScript CAN'T read it # only over HTTPS # or "Lax" per your needs

492 return response

493

494

495@admin_router.get("/tools", response_model=List[ToolRead])

496 async def admin_list_tools(

497 include_inactive: bool = False,

498 db: Session = Depends(get_db),

499 user: str = Depends(require_auth),

500) -> List[ToolRead]:

501 """

502 List tools for the admin UI with an option to include inactive tools.

503

504 This endpoint retrieves a list of tools from the database, optionally including

505 those that are inactive. The inactive filter helps administrators manage tools

506 that have been deactivated but not deleted from the system.

507

508 Args:

509 include_inactive (bool): Whether to include inactive tools in the results.

510 db (Session): Database session dependency.

511 user (str): Authenticated user dependency.

512

513 Returns:

514 List[ToolRead]: A list of tool records formatted with by_alias=True.

515 """

516 logger.debug(f"User {user} requested tool list")

517 tools = await tool_service.list_tools(db, include_inactive=include_inactive)

518 return [tool.model_dump(by_alias=True) for tool in tools]

519

520

521@admin_router.get("/tools/{tool_id}", response_model=ToolRead)

522 async def admin_get_tool(tool_id: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> ToolRead:

523 """

524 Retrieve specific tool details for the admin UI.

525

526 This endpoint fetches the details of a specific tool from the database

527 by its ID. It provides access to all information about the tool for

528 viewing and management purposes.

529

530 Args:

531 tool_id (str): The ID of the tool to retrieve.

532 db (Session): Database session dependency.

533 user (str): Authenticated user dependency.

534

535 Returns:

536 ToolRead: The tool details formatted with by_alias=True.

537 """

538 logger.debug(f"User {user} requested details for tool ID {tool_id}")

539 tool = await tool_service.get_tool(db, tool_id)

540 return tool.model_dump(by_alias=True)

541

542

543@admin_router.post("/tools/")

544@admin_router.post("/tools")

545 async def admin_add_tool(

546 request: Request,

547 db: Session = Depends(get_db),

548 user: str = Depends(require_auth),

549) -> JSONResponse:

550 """

551 Add a tool via the admin UI with error handling.

552

553 Expects form fields:

554 - name

555 - url

556 - description (optional)

557 - requestType (mapped to request_type; defaults to "SSE")

558 - integrationType (mapped to integration_type; defaults to "MCP")

559 - headers (JSON string)

560 - input_schema (JSON string)

561 - jsonpath_filter (optional)

562 - auth_type (optional)

563 - auth_username (optional)

564 - auth_password (optional)

565 - auth_token (optional)

566 - auth_header_key (optional)

567 - auth_header_value (optional)

568

569 Logs the raw form data and assembled tool_data for debugging.

570

571 Args:

572 request (Request): the FastAPI request object containing the form data.

573 db (Session): the SQLAlchemy database session.

574 user (str): identifier of the authenticated user.

575

576 Returns:

577 JSONResponse: a JSON response with `{"message": ..., "success": ...}` and an appropriate HTTP status code.

578 """

579 logger.debug(f"User {user} is adding a new tool")

580 form = await request.form()

581 logger.debug(f"Received form data: {dict(form)}")

582

583 tool_data = {

584 "name": form["name"],

585 "url": form["url"],

586 "description": form.get("description"),

587 "request_type": form.get("requestType", "SSE"),

588 "integration_type": form.get("integrationType", "MCP"),

589 "headers": json.loads(form.get("headers") or "{}"),

590 "input_schema": json.loads(form.get("input_schema") or "{}"),

591 "jsonpath_filter": form.get("jsonpath_filter", ""),

592 "auth_type": form.get("auth_type", ""),

593 "auth_username": form.get("auth_username", ""),

594 "auth_password": form.get("auth_password", ""),

595 "auth_token": form.get("auth_token", ""),

596 "auth_header_key": form.get("auth_header_key", ""),

597 "auth_header_value": form.get("auth_header_value", ""),

598 }

599 logger.debug(f"Tool data built: {tool_data}")

600 try:

601 tool = ToolCreate(**tool_data)

602 logger.debug(f"Validated tool data: {tool.model_dump(by_alias=True)}")

603 await tool_service.register_tool(db, tool)

604 return JSONResponse(

605 content={"message": "Tool registered successfully!", "success": True},

606 status_code=200,

607 )

608 except ToolNameConflictError as e:

609 logger.error(f"ToolNameConflictError: {str(e)}")

610 return JSONResponse(content={"message": str(e), "success": False}, status_code=400)

611 except Exception as e:

612 logger.error(f"Error in admin_add_tool: {str(e)}")

613 return JSONResponse(content={"message": str(e), "success": False}, status_code=500)

614

615

616@admin_router.post("/tools/{tool_id}/edit/")

617@admin_router.post("/tools/{tool_id}/edit")

618 async def admin_edit_tool(

619 tool_id: str,

620 request: Request,

621 db: Session = Depends(get_db),

622 user: str = Depends(require_auth),

623) -> RedirectResponse:

624 """

625 Edit a tool via the admin UI.

626

627 Expects form fields:

628 - name

629 - url

630 - description (optional)

631 - requestType (to be mapped to request_type)

632 - integrationType (to be mapped to integration_type)

633 - headers (as a JSON string)

634 - input_schema (as a JSON string)

635 - jsonpathFilter (optional)

636 - auth_type (optional, string: "basic", "bearer", or empty)

637 - auth_username (optional, for basic auth)

638 - auth_password (optional, for basic auth)

639 - auth_token (optional, for bearer auth)

640 - auth_header_key (optional, for headers auth)

641 - auth_header_value (optional, for headers auth)

642

643 Assembles the tool_data dictionary by remapping form keys into the

644 snake-case keys expected by the schemas.

645

646 Args:

647 tool_id (str): The ID of the tool to edit.

648 request (Request): FastAPI request containing form data.

649 db (Session): Database session dependency.

650 user (str): Authenticated user dependency.

651

652 Returns:

653 RedirectResponse: A redirect response to the tools section of the admin

654 dashboard with a status code of 303 (See Other), or a JSON response with

655 an error message if the update fails.

656 """

657 logger.debug(f"User {user} is editing tool ID {tool_id}")

658 form = await request.form()

659 tool_data = {

660 "name": form["name"],

661 "url": form["url"],

662 "description": form.get("description"),

663 "request_type": form.get("requestType", "SSE"),

664 "integration_type": form.get("integrationType", "MCP"),

665 "headers": json.loads(form.get("headers") or "{}"),

666 "input_schema": json.loads(form.get("input_schema") or "{}"),

667 "jsonpath_filter": form.get("jsonpathFilter", ""),

668 "auth_type": form.get("auth_type", ""),

669 "auth_username": form.get("auth_username", ""),

670 "auth_password": form.get("auth_password", ""),

671 "auth_token": form.get("auth_token", ""),

672 "auth_header_key": form.get("auth_header_key", ""),

673 "auth_header_value": form.get("auth_header_value", ""),

674 }

675 logger.debug(f"Tool update data built: {tool_data}")

676 tool = ToolUpdate(**tool_data)

677 try:

678 await tool_service.update_tool(db, tool_id, tool)

679

680 root_path = request.scope.get("root_path", "")

681 is_inactive_checked = form.get("is_inactive_checked", "false")

682 if is_inactive_checked.lower() == "true": 682 ↛ 683line 682 didn't jump to line 683 because the condition on line 682 was never true

683 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#tools", status_code=303)

684 return RedirectResponse(f"{root_path}/admin#tools", status_code=303)

685 except ToolNameConflictError as e:

686 return JSONResponse(content={"message": str(e), "success": False}, status_code=400)

687 except ToolError as e:

688 return JSONResponse(content={"message": str(e), "success": False}, status_code=500)

689

690

691@admin_router.post("/tools/{tool_id}/delete")

692 async def admin_delete_tool(tool_id: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:

693 """

694 Delete a tool via the admin UI.

695

696 This endpoint permanently removes a tool from the database using its ID.

697 It is irreversible and should be used with caution. The operation is logged,

698 and the user must be authenticated to access this route.

699

700 Args:

701 tool_id (str): The ID of the tool to delete.

702 request (Request): FastAPI request object (not used directly, but required by route signature).

703 db (Session): Database session dependency.

704 user (str): Authenticated user dependency.

705

706 Returns:

707 RedirectResponse: A redirect response to the tools section of the admin

708 dashboard with a status code of 303 (See Other).

709 """

710 logger.debug(f"User {user} is deleting tool ID {tool_id}")

711 await tool_service.delete_tool(db, tool_id)

712

713 form = await request.form()

714 is_inactive_checked = form.get("is_inactive_checked", "false")

715 root_path = request.scope.get("root_path", "")

716

717 if is_inactive_checked.lower() == "true": 717 ↛ 718line 717 didn't jump to line 718 because the condition on line 717 was never true

718 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#tools", status_code=303)

719 return RedirectResponse(f"{root_path}/admin#tools", status_code=303)

720

721

722@admin_router.post("/tools/{tool_id}/toggle")

723 async def admin_toggle_tool(

724 tool_id: str,

725 request: Request,

726 db: Session = Depends(get_db),

727 user: str = Depends(require_auth),

728) -> RedirectResponse:

729 """

730 Toggle a tool's active status via the admin UI.

731

732 This endpoint processes a form request to activate or deactivate a tool.

733 It expects a form field 'activate' with value "true" to activate the tool

734 or "false" to deactivate it. The endpoint handles exceptions gracefully and

735 logs any errors that might occur during the status toggle operation.

736

737 Args:

738 tool_id (str): The ID of the tool whose status to toggle.

739 request (Request): FastAPI request containing form data with the 'activate' field.

740 db (Session): Database session dependency.

741 user (str): Authenticated user dependency.

742

743 Returns:

744 RedirectResponse: A redirect to the admin dashboard tools section with a

745 status code of 303 (See Other).

746 """

747 logger.debug(f"User {user} is toggling tool ID {tool_id}")

748 form = await request.form()

749 activate = form.get("activate", "true").lower() == "true"

750 is_inactive_checked = form.get("is_inactive_checked", "false")

751 try:

752 await tool_service.toggle_tool_status(db, tool_id, activate, reachable=activate)

753 except Exception as e:

754 logger.error(f"Error toggling tool status: {e}")

755

756 root_path = request.scope.get("root_path", "")

757 if is_inactive_checked.lower() == "true": 757 ↛ 758line 757 didn't jump to line 758 because the condition on line 757 was never true

758 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#tools", status_code=303)

759 return RedirectResponse(f"{root_path}/admin#tools", status_code=303)

760

761

762@admin_router.get("/gateways/{gateway_id}", response_model=GatewayRead)

763 async def admin_get_gateway(gateway_id: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> GatewayRead:

764 """Get gateway details for the admin UI.

765

766 Args:

767 gateway_id: Gateway ID.

768 db: Database session.

769 user: Authenticated user.

770

771 Returns:

772 Gateway details.

773 """

774 logger.debug(f"User {user} requested details for gateway ID {gateway_id}")

775 gateway = await gateway_service.get_gateway(db, gateway_id)

776 return gateway.model_dump(by_alias=True)

777

778

779@admin_router.post("/gateways")

780 async def admin_add_gateway(request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> JSONResponse:

781 """Add a gateway via the admin UI.

782

783 Expects form fields:

784 - name

785 - url

786 - description (optional)

787

788 Args:

789 request: FastAPI request containing form data.

790 db: Database session.

791 user: Authenticated user.

792

793 Returns:

794 A redirect response to the admin dashboard.

795 """

796 logger.debug(f"User {user} is adding a new gateway")

797 form = await request.form()

798 gateway = GatewayCreate(

799 name=form["name"],

800 url=form["url"],

801 description=form.get("description"),

802 transport=form.get("transport", "SSE"),

803 auth_type=form.get("auth_type", ""),

804 auth_username=form.get("auth_username", ""),

805 auth_password=form.get("auth_password", ""),

806 auth_token=form.get("auth_token", ""),

807 auth_header_key=form.get("auth_header_key", ""),

808 auth_header_value=form.get("auth_header_value", ""),

809 )

810 try:

811 await gateway_service.register_gateway(db, gateway)

812 return JSONResponse(

813 content={"message": "Gateway registered successfully!", "success": True},

814 status_code=200,

815 )

816

817 except Exception as ex:

818 if isinstance(ex, GatewayConnectionError):

819 return JSONResponse(content={"message": str(ex), "success": False}, status_code=502)

820 if isinstance(ex, ValueError):

821 return JSONResponse(content={"message": str(ex), "success": False}, status_code=400)

822 if isinstance(ex, RuntimeError):

823 return JSONResponse(content={"message": str(ex), "success": False}, status_code=500)

824 return JSONResponse(content={"message": str(ex), "success": False}, status_code=500)

825

826

827@admin_router.post("/gateways/{gateway_id}/edit")

828 async def admin_edit_gateway(

829 gateway_id: str,

830 request: Request,

831 db: Session = Depends(get_db),

832 user: str = Depends(require_auth),

833) -> RedirectResponse:

834 """Edit a gateway via the admin UI.

835

836 Expects form fields:

837 - name

838 - url

839 - description (optional)

840

841 Args:

842 gateway_id: Gateway ID.

843 request: FastAPI request containing form data.

844 db: Database session.

845 user: Authenticated user.

846

847 Returns:

848 A redirect response to the admin dashboard.

849 """

850 logger.debug(f"User {user} is editing gateway ID {gateway_id}")

851 form = await request.form()

852 gateway = GatewayUpdate(

853 name=form["name"],

854 url=form["url"],

855 description=form.get("description"),

856 transport=form.get("transport", "SSE"),

857 auth_type=form.get("auth_type", None),

858 auth_username=form.get("auth_username", None),

859 auth_password=form.get("auth_password", None),

860 auth_token=form.get("auth_token", None),

861 auth_header_key=form.get("auth_header_key", None),

862 auth_header_value=form.get("auth_header_value", None),

863 )

864 await gateway_service.update_gateway(db, gateway_id, gateway)

865

866 root_path = request.scope.get("root_path", "")

867 is_inactive_checked = form.get("is_inactive_checked", "false")

868

869 if is_inactive_checked.lower() == "true": 869 ↛ 870line 869 didn't jump to line 870 because the condition on line 869 was never true

870 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#gateways", status_code=303)

871 return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)

872

873

874@admin_router.post("/gateways/{gateway_id}/delete")

875 async def admin_delete_gateway(gateway_id: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:

876 """

877 Delete a gateway via the admin UI.

878

879 This endpoint removes a gateway from the database by its ID. The deletion is

880 permanent and cannot be undone. It requires authentication and logs the

881 operation for auditing purposes.

882

883 Args:

884 gateway_id (str): The ID of the gateway to delete.

885 request (Request): FastAPI request object (not used directly but required by the route signature).

886 db (Session): Database session dependency.

887 user (str): Authenticated user dependency.

888

889 Returns:

890 RedirectResponse: A redirect response to the gateways section of the admin

891 dashboard with a status code of 303 (See Other).

892 """

893 logger.debug(f"User {user} is deleting gateway ID {gateway_id}")

894 await gateway_service.delete_gateway(db, gateway_id)

895

896 form = await request.form()

897 is_inactive_checked = form.get("is_inactive_checked", "false")

898 root_path = request.scope.get("root_path", "")

899

900 if is_inactive_checked.lower() == "true": 900 ↛ 901line 900 didn't jump to line 901 because the condition on line 900 was never true

901 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#gateways", status_code=303)

902 return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)

903

904

905@admin_router.get("/resources/{uri:path}")

906 async def admin_get_resource(uri: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> Dict[str, Any]:

907 """Get resource details for the admin UI.

908

909 Args:

910 uri: Resource URI.

911 db: Database session.

912 user: Authenticated user.

913

914 Returns:

915 A dictionary containing resource details and its content.

916 """

917 logger.debug(f"User {user} requested details for resource URI {uri}")

918 resource = await resource_service.get_resource_by_uri(db, uri)

919 content = await resource_service.read_resource(db, uri)

920 return {"resource": resource.model_dump(by_alias=True), "content": content}

921

922

923@admin_router.post("/resources")

924 async def admin_add_resource(request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:

925 """Add a resource via the admin UI.

926

927 Expects form fields:

928 - uri

929 - name

930 - description (optional)

931 - mime_type (optional)

932 - content

933

934 Args:

935 request: FastAPI request containing form data.

936 db: Database session.

937 user: Authenticated user.

938

939 Returns:

940 A redirect response to the admin dashboard.

941 """

942 logger.debug(f"User {user} is adding a new resource")

943 form = await request.form()

944 resource = ResourceCreate(

945 uri=form["uri"],

946 name=form["name"],

947 description=form.get("description"),

948 mime_type=form.get("mimeType"),

949 template=form.get("template"), # defaults to None if not provided

950 content=form["content"],

951 )

952 await resource_service.register_resource(db, resource)

953

954 root_path = request.scope.get("root_path", "")

955 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)

956

957

958@admin_router.post("/resources/{uri:path}/edit")

959 async def admin_edit_resource(

960 uri: str,

961 request: Request,

962 db: Session = Depends(get_db),

963 user: str = Depends(require_auth),

964) -> RedirectResponse:

965 """Edit a resource via the admin UI.

966

967 Expects form fields:

968 - name

969 - description (optional)

970 - mime_type (optional)

971 - content

972

973 Args:

974 uri: Resource URI.

975 request: FastAPI request containing form data.

976 db: Database session.

977 user: Authenticated user.

978

979 Returns:

980 A redirect response to the admin dashboard.

981 """

982 logger.debug(f"User {user} is editing resource URI {uri}")

983 form = await request.form()

984 resource = ResourceUpdate(

985 name=form["name"],

986 description=form.get("description"),

987 mime_type=form.get("mimeType"),

988 content=form["content"],

989 )

990 await resource_service.update_resource(db, uri, resource)

991

992 root_path = request.scope.get("root_path", "")

993 is_inactive_checked = form.get("is_inactive_checked", "false")

994

995 if is_inactive_checked.lower() == "true": 995 ↛ 996line 995 didn't jump to line 996 because the condition on line 995 was never true

996 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#resources", status_code=303)

997 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)

998

999

1000@admin_router.post("/resources/{uri:path}/delete")

1001 async def admin_delete_resource(uri: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:

1002 """

1003 Delete a resource via the admin UI.

1004

1005 This endpoint permanently removes a resource from the database using its URI.

1006 The operation is irreversible and should be used with caution. It requires

1007 user authentication and logs the deletion attempt.

1008

1009 Args:

1010 uri (str): The URI of the resource to delete.

1011 request (Request): FastAPI request object (not used directly but required by the route signature).

1012 db (Session): Database session dependency.

1013 user (str): Authenticated user dependency.

1014

1015 Returns:

1016 RedirectResponse: A redirect response to the resources section of the admin

1017 dashboard with a status code of 303 (See Other).

1018 """

1019 logger.debug(f"User {user} is deleting resource URI {uri}")

1020 await resource_service.delete_resource(db, uri)

1021

1022 form = await request.form()

1023 is_inactive_checked = form.get("is_inactive_checked", "false")

1024 root_path = request.scope.get("root_path", "")

1025

1026 if is_inactive_checked.lower() == "true": 1026 ↛ 1027line 1026 didn't jump to line 1027 because the condition on line 1026 was never true

1027 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#resources", status_code=303)

1028 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)

1029

1030

1031@admin_router.post("/resources/{resource_id}/toggle")

1032 async def admin_toggle_resource(

1033 resource_id: int,

1034 request: Request,

1035 db: Session = Depends(get_db),

1036 user: str = Depends(require_auth),

1037) -> RedirectResponse:

1038 """

1039 Toggle a resource's active status via the admin UI.

1040

1041 This endpoint processes a form request to activate or deactivate a resource.

1042 It expects a form field 'activate' with value "true" to activate the resource

1043 or "false" to deactivate it. The endpoint handles exceptions gracefully and

1044 logs any errors that might occur during the status toggle operation.

1045

1046 Args:

1047 resource_id (int): The ID of the resource whose status to toggle.

1048 request (Request): FastAPI request containing form data with the 'activate' field.

1049 db (Session): Database session dependency.

1050 user (str): Authenticated user dependency.

1051

1052 Returns:

1053 RedirectResponse: A redirect to the admin dashboard resources section with a

1054 status code of 303 (See Other).

1055 """

1056 logger.debug(f"User {user} is toggling resource ID {resource_id}")

1057 form = await request.form()

1058 activate = form.get("activate", "true").lower() == "true"

1059 is_inactive_checked = form.get("is_inactive_checked", "false")

1060 try:

1061 await resource_service.toggle_resource_status(db, resource_id, activate)

1062 except Exception as e:

1063 logger.error(f"Error toggling resource status: {e}")

1064

1065 root_path = request.scope.get("root_path", "")

1066 if is_inactive_checked.lower() == "true": 1066 ↛ 1067line 1066 didn't jump to line 1067 because the condition on line 1066 was never true

1067 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#resources", status_code=303)

1068 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)

1069

1070

1071@admin_router.get("/prompts/{name}")

1072 async def admin_get_prompt(name: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> Dict[str, Any]:

1073 """Get prompt details for the admin UI.

1074

1075 Args:

1076 name: Prompt name.

1077 db: Database session.

1078 user: Authenticated user.

1079

1080 Returns:

1081 A dictionary with prompt details.

1082 """

1083 logger.debug(f"User {user} requested details for prompt name {name}")

1084 prompt_details = await prompt_service.get_prompt_details(db, name)

1085

1086 prompt = PromptRead.model_validate(prompt_details)

1087 return prompt.model_dump(by_alias=True)

1088

1089

1090@admin_router.post("/prompts")

1091 async def admin_add_prompt(request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:

1092 """Add a prompt via the admin UI.

1093

1094 Expects form fields:

1095 - name

1096 - description (optional)

1097 - template

1098 - arguments (as a JSON string representing a list)

1099

1100 Args:

1101 request: FastAPI request containing form data.

1102 db: Database session.

1103 user: Authenticated user.

1104

1105 Returns:

1106 A redirect response to the admin dashboard.

1107 """

1108 logger.debug(f"User {user} is adding a new prompt")

1109 form = await request.form()

1110 args_json = form.get("arguments") or "[]"

1111 arguments = json.loads(args_json)

1112 prompt = PromptCreate(

1113 name=form["name"],

1114 description=form.get("description"),

1115 template=form["template"],

1116 arguments=arguments,

1117 )

1118 await prompt_service.register_prompt(db, prompt)

1119

1120 root_path = request.scope.get("root_path", "")

1121 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)

1122

1123

1124@admin_router.post("/prompts/{name}/edit")

1125 async def admin_edit_prompt(

1126 name: str,

1127 request: Request,

1128 db: Session = Depends(get_db),

1129 user: str = Depends(require_auth),

1130) -> RedirectResponse:

1131 """Edit a prompt via the admin UI.

1132

1133 Expects form fields:

1134 - name

1135 - description (optional)

1136 - template

1137 - arguments (as a JSON string representing a list)

1138

1139 Args:

1140 name: Prompt name.

1141 request: FastAPI request containing form data.

1142 db: Database session.

1143 user: Authenticated user.

1144

1145 Returns:

1146 A redirect response to the admin dashboard.

1147 """

1148 logger.debug(f"User {user} is editing prompt name {name}")

1149 form = await request.form()

1150 args_json = form.get("arguments") or "[]"

1151 arguments = json.loads(args_json)

1152 prompt = PromptUpdate(

1153 name=form["name"],

1154 description=form.get("description"),

1155 template=form["template"],

1156 arguments=arguments,

1157 )

1158 await prompt_service.update_prompt(db, name, prompt)

1159

1160 root_path = request.scope.get("root_path", "")

1161 is_inactive_checked = form.get("is_inactive_checked", "false")

1162

1163 if is_inactive_checked.lower() == "true": 1163 ↛ 1164line 1163 didn't jump to line 1164 because the condition on line 1163 was never true

1164 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#prompts", status_code=303)

1165 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)

1166

1167

1168@admin_router.post("/prompts/{name}/delete")

1169 async def admin_delete_prompt(name: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:

1170 """

1171 Delete a prompt via the admin UI.

1172

1173 This endpoint permanently deletes a prompt from the database using its name.

1174 Deletion is irreversible and requires authentication. All actions are logged

1175 for administrative auditing.

1176

1177 Args:

1178 name (str): The name of the prompt to delete.

1179 request (Request): FastAPI request object (not used directly but required by the route signature).

1180 db (Session): Database session dependency.

1181 user (str): Authenticated user dependency.

1182

1183 Returns:

1184 RedirectResponse: A redirect response to the prompts section of the admin

1185 dashboard with a status code of 303 (See Other).

1186 """

1187 logger.debug(f"User {user} is deleting prompt name {name}")

1188 await prompt_service.delete_prompt(db, name)

1189

1190 form = await request.form()

1191 is_inactive_checked = form.get("is_inactive_checked", "false")

1192 root_path = request.scope.get("root_path", "")

1193

1194 if is_inactive_checked.lower() == "true": 1194 ↛ 1195line 1194 didn't jump to line 1195 because the condition on line 1194 was never true

1195 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#prompts", status_code=303)

1196 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)

1197

1198

1199@admin_router.post("/prompts/{prompt_id}/toggle")

1200 async def admin_toggle_prompt(

1201 prompt_id: int,

1202 request: Request,

1203 db: Session = Depends(get_db),

1204 user: str = Depends(require_auth),

1205) -> RedirectResponse:

1206 """

1207 Toggle a prompt's active status via the admin UI.

1208

1209 This endpoint processes a form request to activate or deactivate a prompt.

1210 It expects a form field 'activate' with value "true" to activate the prompt

1211 or "false" to deactivate it. The endpoint handles exceptions gracefully and

1212 logs any errors that might occur during the status toggle operation.

1213

1214 Args:

1215 prompt_id (int): The ID of the prompt whose status to toggle.

1216 request (Request): FastAPI request containing form data with the 'activate' field.

1217 db (Session): Database session dependency.

1218 user (str): Authenticated user dependency.

1219

1220 Returns:

1221 RedirectResponse: A redirect to the admin dashboard prompts section with a

1222 status code of 303 (See Other).

1223 """

1224 logger.debug(f"User {user} is toggling prompt ID {prompt_id}")

1225 form = await request.form()

1226 activate = form.get("activate", "true").lower() == "true"

1227 is_inactive_checked = form.get("is_inactive_checked", "false")

1228 try:

1229 await prompt_service.toggle_prompt_status(db, prompt_id, activate)

1230 except Exception as e:

1231 logger.error(f"Error toggling prompt status: {e}")

1232

1233 root_path = request.scope.get("root_path", "")

1234 if is_inactive_checked.lower() == "true": 1234 ↛ 1235line 1234 didn't jump to line 1235 because the condition on line 1234 was never true

1235 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#prompts", status_code=303)

1236 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)

1237

1238

1239@admin_router.post("/roots")

1240 async def admin_add_root(request: Request, user: str = Depends(require_auth)) -> RedirectResponse:

1241 """Add a new root via the admin UI.

1242

1243 Expects form fields:

1244 - path

1245 - name (optional)

1246

1247 Args:

1248 request: FastAPI request containing form data.

1249 user: Authenticated user.

1250

1251 Returns:

1252 A redirect response to the admin dashboard.

1253 """

1254 logger.debug(f"User {user} is adding a new root")

1255 form = await request.form()

1256 uri = form["uri"]

1257 name = form.get("name")

1258 await root_service.add_root(uri, name)

1259

1260 root_path = request.scope.get("root_path", "")

1261 return RedirectResponse(f"{root_path}/admin#roots", status_code=303)

1262

1263

1264@admin_router.post("/roots/{uri:path}/delete")

1265 async def admin_delete_root(uri: str, request: Request, user: str = Depends(require_auth)) -> RedirectResponse:

1266 """

1267 Delete a root via the admin UI.

1268

1269 This endpoint removes a registered root URI from the system. The deletion is

1270 permanent and cannot be undone. It requires authentication and logs the

1271 operation for audit purposes.

1272

1273 Args:

1274 uri (str): The URI of the root to delete.

1275 request (Request): FastAPI request object (not used directly but required by the route signature).

1276 user (str): Authenticated user dependency.

1277

1278 Returns:

1279 RedirectResponse: A redirect response to the roots section of the admin

1280 dashboard with a status code of 303 (See Other).

1281 """

1282 logger.debug(f"User {user} is deleting root URI {uri}")

1283 await root_service.remove_root(uri)

1284

1285 form = await request.form()

1286 root_path = request.scope.get("root_path", "")

1287 is_inactive_checked = form.get("is_inactive_checked", "false")

1288

1289 if is_inactive_checked.lower() == "true": 1289 ↛ 1290line 1289 didn't jump to line 1290 because the condition on line 1289 was never true

1290 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#roots", status_code=303)

1291 return RedirectResponse(f"{root_path}/admin#roots", status_code=303)

1292

1293

1294# Metrics

1295 MetricsDict = Dict[str, Union[ToolMetrics, ResourceMetrics, ServerMetrics, PromptMetrics]]

1296

1297

1298@admin_router.get("/metrics", response_model=MetricsDict)

1299 async def admin_get_metrics(

1300 db: Session = Depends(get_db),

1301 user: str = Depends(require_auth),

1302) -> MetricsDict:

1303 """

1304 Retrieve aggregate metrics for all entity types via the admin UI.

1305

1306 This endpoint collects and returns usage metrics for tools, resources, servers,

1307 and prompts. The metrics are retrieved by calling the aggregate_metrics method

1308 on each respective service, which compiles statistics about usage patterns,

1309 success rates, and other relevant metrics for administrative monitoring

1310 and analysis purposes.

1311

1312 Args:

1313 db (Session): Database session dependency.

1314 user (str): Authenticated user dependency.

1315

1316 Returns:

1317 MetricsDict: A dictionary containing the aggregated metrics for tools,

1318 resources, servers, and prompts. Each value is a Pydantic model instance

1319 specific to the entity type.

1320 """

1321 logger.debug(f"User {user} requested aggregate metrics")

1322 tool_metrics = await tool_service.aggregate_metrics(db)

1323 resource_metrics = await resource_service.aggregate_metrics(db)

1324 server_metrics = await server_service.aggregate_metrics(db)

1325 prompt_metrics = await prompt_service.aggregate_metrics(db)

1326

1327 # Return actual Pydantic model instances

1328 return {

1329 "tools": tool_metrics,

1330 "resources": resource_metrics,

1331 "servers": server_metrics,

1332 "prompts": prompt_metrics,

1333 }

1334

1335

1336@admin_router.post("/metrics/reset", response_model=Dict[str, object])

1337 async def admin_reset_metrics(db: Session = Depends(get_db), user: str = Depends(require_auth)) -> Dict[str, object]:

1338 """

1339 Reset all metrics for tools, resources, servers, and prompts.

1340 Each service must implement its own reset_metrics method.

1341

1342 Args:

1343 db (Session): Database session dependency.

1344 user (str): Authenticated user dependency.

1345

1346 Returns:

1347 Dict[str, object]: A dictionary containing a success message and status.

1348 """

1349 logger.debug(f"User {user} requested to reset all metrics")

1350 await tool_service.reset_metrics(db)

1351 await resource_service.reset_metrics(db)

1352 await server_service.reset_metrics(db)

1353 await prompt_service.reset_metrics(db)

1354 return {"message": "All metrics reset successfully", "success": True}

1355

1356

1357@admin_router.post("/gateways/test", response_model=GatewayTestResponse)

1358 async def admin_test_gateway(request: GatewayTestRequest, user: str = Depends(require_auth)) -> GatewayTestResponse:

1359 """

1360 Test a gateway by sending a request to its URL.

1361 This endpoint allows administrators to test the connectivity and response

1362

1363 Args:

1364 request (GatewayTestRequest): The request object containing the gateway URL and request details.

1365 user (str): Authenticated user dependency.

1366

1367 Returns:

1368 GatewayTestResponse: The response from the gateway, including status code, latency, and body

1369 """

1370 full_url = str(request.base_url).rstrip("/") + "/" + request.path.lstrip("/")

1371 logger.debug(f"User {user} testing server at {request.base_url}.")

1372 try:

1373 async with httpx.AsyncClient(timeout=settings.federation_timeout, verify=not settings.skip_ssl_verify) as client:

1374 start_time = time.monotonic()

1375 response = await client.request(method=request.method.upper(), url=full_url, headers=request.headers, json=request.body)

1376 latency_ms = int((time.monotonic() - start_time) * 1000)

1377 try:

1378 response_body: Union[dict, str] = response.json()

1379 except json.JSONDecodeError:

1380 response_body = response.text

1381

1382 return GatewayTestResponse(status_code=response.status_code, latency_ms=latency_ms, body=response_body)

1383

1384 except httpx.RequestError as e:

1385 logger.warning(f"Gateway test failed: {e}")

1386 raise HTTPException(status_code=502, detail=f"Request failed: {str(e)}")