Coverage for mcpgateway/admin.py: 79%

414 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-09 11:03 +0100

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

2"""Admin UI Routes for MCP Gateway. 

3 

4Copyright 2025 

5SPDX-License-Identifier: Apache-2.0 

6Authors: Mihai Criveti 

7 

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

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

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

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

12reading, updating, deletion, and status toggling. 

13 

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

15the require_auth or require_basic_auth dependency. The module integrates with 

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

17underlying data. 

18""" 

19 

20# Standard 

21import json 

22import logging 

23import time 

24from typing import Any, Dict, List, Union 

25 

26# Third-Party 

27from fastapi import APIRouter, Depends, HTTPException, Request 

28from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse 

29import httpx 

30from sqlalchemy.orm import Session 

31 

32# First-Party 

33from mcpgateway.config import settings 

34from mcpgateway.db import get_db 

35from 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) 

58from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayService 

59from mcpgateway.services.prompt_service import PromptService 

60from mcpgateway.services.resource_service import ResourceService 

61from mcpgateway.services.root_service import RootService 

62from mcpgateway.services.server_service import ServerNotFoundError, ServerService 

63from mcpgateway.services.tool_service import ( 

64 ToolError, 

65 ToolNameConflictError, 

66 ToolService, 

67) 

68from mcpgateway.utils.create_jwt_token import get_jwt_token 

69from mcpgateway.utils.verify_credentials import require_auth, require_basic_auth 

70 

71# Initialize services 

72server_service = ServerService() 

73tool_service = ToolService() 

74prompt_service = PromptService() 

75gateway_service = GatewayService() 

76resource_service = ResourceService() 

77root_service = RootService() 

78 

79# Set up basic authentication 

80logger = logging.getLogger("mcpgateway") 

81 

82admin_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]) 

90async 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) 

112async 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) 

136async 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") 

189async 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") 

248async 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") 

288async 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]) 

321async 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]) 

347async 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]) 

373async 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") 

399async 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) 

439async 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]) 

496async 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) 

522async 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") 

545async 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") 

618async 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") 

692async 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") 

723async 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) 

763async 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") 

780async 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") 

828async 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") 

875async 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}") 

906async 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") 

924async 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") 

959async 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") 

1001async 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") 

1032async 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}") 

1072async 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") 

1091async 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") 

1125async 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") 

1169async 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") 

1200async 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") 

1240async 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") 

1265async 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 

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

1296 

1297 

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

1299async 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]) 

1337async 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) 

1358async 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)}")