Coverage for mcpgateway/admin.py: 89%

335 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-22 12:53 +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 

20import json 

21import logging 

22from typing import Any, Dict, List, Union 

23 

24from fastapi import APIRouter, Depends, HTTPException, Request 

25from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse 

26from sqlalchemy.orm import Session 

27 

28from mcpgateway.config import settings 

29from mcpgateway.db import get_db 

30from mcpgateway.schemas import ( 

31 GatewayCreate, 

32 GatewayRead, 

33 GatewayUpdate, 

34 PromptCreate, 

35 PromptMetrics, 

36 PromptRead, 

37 PromptUpdate, 

38 ResourceCreate, 

39 ResourceMetrics, 

40 ResourceRead, 

41 ResourceUpdate, 

42 ServerCreate, 

43 ServerMetrics, 

44 ServerRead, 

45 ServerUpdate, 

46 ToolCreate, 

47 ToolMetrics, 

48 ToolRead, 

49 ToolUpdate, 

50) 

51from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayService 

52from mcpgateway.services.prompt_service import PromptService 

53from mcpgateway.services.resource_service import ResourceService 

54from mcpgateway.services.root_service import RootService 

55from mcpgateway.services.server_service import ServerNotFoundError, ServerService 

56from mcpgateway.services.tool_service import ( 

57 ToolError, 

58 ToolNameConflictError, 

59 ToolService, 

60) 

61from mcpgateway.utils.create_jwt_token import get_jwt_token 

62from mcpgateway.utils.verify_credentials import require_auth, require_basic_auth 

63 

64# Initialize services 

65server_service = ServerService() 

66tool_service = ToolService() 

67prompt_service = PromptService() 

68gateway_service = GatewayService() 

69resource_service = ResourceService() 

70root_service = RootService() 

71 

72# Set up basic authentication 

73logger = logging.getLogger("mcpgateway") 

74 

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

76 

77#################### 

78# Admin UI Routes # 

79#################### 

80 

81 

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

83async def admin_list_servers( 

84 include_inactive: bool = False, 

85 db: Session = Depends(get_db), 

86 user: str = Depends(require_auth), 

87) -> List[ServerRead]: 

88 """ 

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

90 

91 Args: 

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

93 db (Session): The database session dependency. 

94 user (str): The authenticated user dependency. 

95 

96 Returns: 

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

98 """ 

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

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

101 return [server.dict(by_alias=True) for server in servers] 

102 

103 

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

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

106 """ 

107 Retrieve server details for the admin UI. 

108 

109 Args: 

110 server_id (int): The ID of the server to retrieve. 

111 db (Session): The database session dependency. 

112 user (str): The authenticated user dependency. 

113 

114 Returns: 

115 ServerRead: The server details. 

116 

117 Raises: 

118 HTTPException: If the server is not found. 

119 """ 

120 try: 

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

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

123 return server.dict(by_alias=True) 

124 except ServerNotFoundError as e: 

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

126 

127 

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

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

130 """ 

131 Add a new server via the admin UI. 

132 

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

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

135 registration. 

136 

137 Expects form fields: 

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

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

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

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

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

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

144 

145 Args: 

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

147 db (Session): Database session dependency 

148 user (str): Authenticated user dependency 

149 

150 Returns: 

151 RedirectResponse: A redirect to the admin dashboard catalog section 

152 """ 

153 form = await request.form() 

154 try: 

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

156 server = ServerCreate( 

157 name=form["name"], 

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

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

160 associated_tools=form.get("associatedTools"), 

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

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

163 ) 

164 await server_service.register_server(db, server) 

165 

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

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

168 except Exception as e: 

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

170 

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

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

173 

174 

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

176async def admin_edit_server( 

177 server_id: int, 

178 request: Request, 

179 db: Session = Depends(get_db), 

180 user: str = Depends(require_auth), 

181) -> RedirectResponse: 

182 """ 

183 Edit an existing server via the admin UI. 

184 

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

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

187 update operation. 

188 

189 Expects form fields: 

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

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

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

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

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

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

196 

197 Args: 

198 server_id (int): The ID of the server to edit 

199 request (Request): FastAPI request containing form data 

200 db (Session): Database session dependency 

201 user (str): Authenticated user dependency 

202 

203 Returns: 

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

205 """ 

206 form = await request.form() 

207 try: 

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

209 server = ServerUpdate( 

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

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

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

213 associated_tools=form.get("associatedTools"), 

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

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

216 ) 

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

218 

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

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

221 except Exception as e: 

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

223 

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

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

226 

227 

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

229async def admin_toggle_server( 

230 server_id: int, 

231 request: Request, 

232 db: Session = Depends(get_db), 

233 user: str = Depends(require_auth), 

234) -> RedirectResponse: 

235 """ 

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

237 

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

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

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

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

242 

243 Args: 

244 server_id (int): The ID of the server whose status to toggle. 

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

246 db (Session): Database session dependency. 

247 user (str): Authenticated user dependency. 

248 

249 Returns: 

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

251 status code of 303 (See Other). 

252 """ 

253 form = await request.form() 

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

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

256 try: 

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

258 except Exception as e: 

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

260 

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

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

263 

264 

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

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

267 """ 

268 Delete a server via the admin UI. 

269 

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

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

272 

273 Args: 

274 server_id (int): The ID of the server to delete 

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

276 db (Session): Database session dependency 

277 user (str): Authenticated user dependency 

278 

279 Returns: 

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

281 status code of 303 (See Other) 

282 """ 

283 try: 

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

285 await server_service.delete_server(db, server_id) 

286 except Exception as e: 

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

288 

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

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

291 

292 

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

294async def admin_list_resources( 

295 include_inactive: bool = False, 

296 db: Session = Depends(get_db), 

297 user: str = Depends(require_auth), 

298) -> List[ResourceRead]: 

299 """ 

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

301 

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

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

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

305 

306 Args: 

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

308 db (Session): Database session dependency. 

309 user (str): Authenticated user dependency. 

310 

311 Returns: 

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

313 """ 

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

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

316 return [resource.dict(by_alias=True) for resource in resources] 

317 

318 

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

320async def admin_list_prompts( 

321 include_inactive: bool = False, 

322 db: Session = Depends(get_db), 

323 user: str = Depends(require_auth), 

324) -> List[PromptRead]: 

325 """ 

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

327 

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

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

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

331 

332 Args: 

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

334 db (Session): Database session dependency. 

335 user (str): Authenticated user dependency. 

336 

337 Returns: 

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

339 """ 

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

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

342 return [prompt.dict(by_alias=True) for prompt in prompts] 

343 

344 

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

346async def admin_list_gateways( 

347 include_inactive: bool = False, 

348 db: Session = Depends(get_db), 

349 user: str = Depends(require_auth), 

350) -> List[GatewayRead]: 

351 """ 

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

353 

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

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

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

357 

358 Args: 

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

360 db (Session): Database session dependency. 

361 user (str): Authenticated user dependency. 

362 

363 Returns: 

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

365 """ 

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

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

368 return [gateway.dict(by_alias=True) for gateway in gateways] 

369 

370 

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

372async def admin_toggle_gateway( 

373 gateway_id: int, 

374 request: Request, 

375 db: Session = Depends(get_db), 

376 user: str = Depends(require_auth), 

377) -> RedirectResponse: 

378 """ 

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

380 

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

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

383 determine the new status of the gateway. 

384 

385 Args: 

386 gateway_id (int): The ID of the gateway to toggle. 

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

388 db (Session): The database session dependency. 

389 user (str): The authenticated user dependency. 

390 

391 Returns: 

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

393 status code of 303 (See Other). 

394 """ 

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

396 form = await request.form() 

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

398 try: 

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

400 except Exception as e: 

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

402 

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

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

405 

406 

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

408async def admin_ui( 

409 request: Request, 

410 include_inactive: bool = False, 

411 db: Session = Depends(get_db), 

412 user: str = Depends(require_basic_auth), 

413 jwt_token: str = Depends(get_jwt_token), 

414) -> HTMLResponse: 

415 """ 

416 Render the admin dashboard HTML page. 

417 

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

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

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

421 

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

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

424 

425 Args: 

426 request (Request): FastAPI request object. 

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

428 db (Session): Database session dependency. 

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

430 jwt_token (str): JWT token for authentication. 

431 

432 Returns: 

433 HTMLResponse: Rendered HTML template for the admin dashboard. 

434 """ 

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

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

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

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

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

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

441 roots = [root.dict(by_alias=True) for root in await root_service.list_roots()] 

442 root_path = settings.app_root_path 

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

444 "admin.html", 

445 { 

446 "request": request, 

447 "servers": servers, 

448 "tools": tools, 

449 "resources": resources, 

450 "prompts": prompts, 

451 "gateways": gateways, 

452 "roots": roots, 

453 "include_inactive": include_inactive, 

454 "root_path": root_path, 

455 }, 

456 ) 

457 

458 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 

459 return response 

460 

461 

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

463async def admin_list_tools( 

464 include_inactive: bool = False, 

465 db: Session = Depends(get_db), 

466 user: str = Depends(require_auth), 

467) -> List[ToolRead]: 

468 """ 

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

470 

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

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

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

474 

475 Args: 

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

477 db (Session): Database session dependency. 

478 user (str): Authenticated user dependency. 

479 

480 Returns: 

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

482 """ 

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

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

485 return [tool.dict(by_alias=True) for tool in tools] 

486 

487 

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

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

490 """ 

491 Retrieve specific tool details for the admin UI. 

492 

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

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

495 viewing and management purposes. 

496 

497 Args: 

498 tool_id (int): The ID of the tool to retrieve. 

499 db (Session): Database session dependency. 

500 user (str): Authenticated user dependency. 

501 

502 Returns: 

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

504 """ 

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

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

507 return tool.dict(by_alias=True) 

508 

509 

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

511@admin_router.post("/tools") 

512async def admin_add_tool( 

513 request: Request, 

514 db: Session = Depends(get_db), 

515 user: str = Depends(require_auth), 

516) -> JSONResponse: 

517 """ 

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

519 

520 Expects form fields: 

521 - name 

522 - url 

523 - description (optional) 

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

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

526 - headers (JSON string) 

527 - input_schema (JSON string) 

528 - jsonpath_filter (optional) 

529 - auth_type (optional) 

530 - auth_username (optional) 

531 - auth_password (optional) 

532 - auth_token (optional) 

533 - auth_header_key (optional) 

534 - auth_header_value (optional) 

535 

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

537 

538 Args: 

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

540 db (Session): the SQLAlchemy database session. 

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

542 

543 Returns: 

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

545 """ 

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

547 form = await request.form() 

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

549 

550 tool_data = { 

551 "name": form["name"], 

552 "url": form["url"], 

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

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

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

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

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

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

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

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

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

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

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

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

565 } 

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

567 try: 

568 tool = ToolCreate(**tool_data) 

569 logger.debug(f"Validated tool data: {tool.dict()}") 

570 await tool_service.register_tool(db, tool) 

571 return JSONResponse( 

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

573 status_code=200, 

574 ) 

575 except ToolNameConflictError as e: 

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

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

578 except Exception as e: 

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

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

581 

582 

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

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

585async def admin_edit_tool( 

586 tool_id: int, 

587 request: Request, 

588 db: Session = Depends(get_db), 

589 user: str = Depends(require_auth), 

590) -> RedirectResponse: 

591 """ 

592 Edit a tool via the admin UI. 

593 

594 Expects form fields: 

595 - name 

596 - url 

597 - description (optional) 

598 - requestType (to be mapped to request_type) 

599 - integrationType (to be mapped to integration_type) 

600 - headers (as a JSON string) 

601 - input_schema (as a JSON string) 

602 - jsonpathFilter (optional) 

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

604 - auth_username (optional, for basic auth) 

605 - auth_password (optional, for basic auth) 

606 - auth_token (optional, for bearer auth) 

607 - auth_header_key (optional, for headers auth) 

608 - auth_header_value (optional, for headers auth) 

609 

610 Assembles the tool_data dictionary by remapping form keys into the 

611 snake-case keys expected by the schemas. 

612 

613 Args: 

614 tool_id (int): The ID of the tool to edit. 

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

616 db (Session): Database session dependency. 

617 user (str): Authenticated user dependency. 

618 

619 Returns: 

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

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

622 an error message if the update fails. 

623 """ 

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

625 form = await request.form() 

626 tool_data = { 

627 "name": form["name"], 

628 "url": form["url"], 

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

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

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

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

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

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

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

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

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

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

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

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

641 } 

642 logger.info(f"Tool update data built: {tool_data}") 

643 tool = ToolUpdate(**tool_data) 

644 try: 

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

646 

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

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

649 except ToolNameConflictError as e: 

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

651 except ToolError as e: 

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

653 

654 

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

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

657 """ 

658 Delete a tool via the admin UI. 

659 

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

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

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

663 

664 Args: 

665 tool_id (int): The ID of the tool to delete. 

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

667 db (Session): Database session dependency. 

668 user (str): Authenticated user dependency. 

669 

670 Returns: 

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

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

673 """ 

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

675 await tool_service.delete_tool(db, tool_id) 

676 

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

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

679 

680 

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

682async def admin_toggle_tool( 

683 tool_id: int, 

684 request: Request, 

685 db: Session = Depends(get_db), 

686 user: str = Depends(require_auth), 

687) -> RedirectResponse: 

688 """ 

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

690 

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

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

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

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

695 

696 Args: 

697 tool_id (int): The ID of the tool whose status to toggle. 

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

699 db (Session): Database session dependency. 

700 user (str): Authenticated user dependency. 

701 

702 Returns: 

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

704 status code of 303 (See Other). 

705 """ 

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

707 form = await request.form() 

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

709 try: 

710 await tool_service.toggle_tool_status(db, tool_id, activate) 

711 except Exception as e: 

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

713 

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

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

716 

717 

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

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

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

721 

722 Args: 

723 gateway_id: Gateway ID. 

724 db: Database session. 

725 user: Authenticated user. 

726 

727 Returns: 

728 Gateway details. 

729 """ 

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

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

732 return gateway.dict(by_alias=True) 

733 

734 

735@admin_router.post("/gateways") 

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

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

738 

739 Expects form fields: 

740 - name 

741 - url 

742 - description (optional) 

743 

744 Args: 

745 request: FastAPI request containing form data. 

746 db: Database session. 

747 user: Authenticated user. 

748 

749 Returns: 

750 A redirect response to the admin dashboard. 

751 """ 

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

753 form = await request.form() 

754 gateway = GatewayCreate( 

755 name=form["name"], 

756 url=form["url"], 

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

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

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

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

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

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

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

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

765 ) 

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

767 try: 

768 await gateway_service.register_gateway(db, gateway) 

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

770 except Exception as ex: 

771 if isinstance(ex, GatewayConnectionError): 

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

773 if isinstance(ex, ValueError): 

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

775 if isinstance(ex, RuntimeError): 

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

777 

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

779 

780 

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

782async def admin_edit_gateway( 

783 gateway_id: int, 

784 request: Request, 

785 db: Session = Depends(get_db), 

786 user: str = Depends(require_auth), 

787) -> RedirectResponse: 

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

789 

790 Expects form fields: 

791 - name 

792 - url 

793 - description (optional) 

794 

795 Args: 

796 gateway_id: Gateway ID. 

797 request: FastAPI request containing form data. 

798 db: Database session. 

799 user: Authenticated user. 

800 

801 Returns: 

802 A redirect response to the admin dashboard. 

803 """ 

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

805 form = await request.form() 

806 gateway = GatewayUpdate( 

807 name=form["name"], 

808 url=form["url"], 

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

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

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

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

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

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

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

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

817 ) 

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

819 

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

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

822 

823 

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

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

826 """ 

827 Delete a gateway via the admin UI. 

828 

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

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

831 operation for auditing purposes. 

832 

833 Args: 

834 gateway_id (int): The ID of the gateway to delete. 

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

836 db (Session): Database session dependency. 

837 user (str): Authenticated user dependency. 

838 

839 Returns: 

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

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

842 """ 

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

844 await gateway_service.delete_gateway(db, gateway_id) 

845 

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

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

848 

849 

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

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

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

853 

854 Args: 

855 uri: Resource URI. 

856 db: Database session. 

857 user: Authenticated user. 

858 

859 Returns: 

860 A dictionary containing resource details and its content. 

861 """ 

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

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

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

865 return {"resource": resource.dict(by_alias=True), "content": content} 

866 

867 

868@admin_router.post("/resources") 

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

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

871 

872 Expects form fields: 

873 - uri 

874 - name 

875 - description (optional) 

876 - mime_type (optional) 

877 - content 

878 

879 Args: 

880 request: FastAPI request containing form data. 

881 db: Database session. 

882 user: Authenticated user. 

883 

884 Returns: 

885 A redirect response to the admin dashboard. 

886 """ 

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

888 form = await request.form() 

889 resource = ResourceCreate( 

890 uri=form["uri"], 

891 name=form["name"], 

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

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

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

895 content=form["content"], 

896 ) 

897 await resource_service.register_resource(db, resource) 

898 

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

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

901 

902 

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

904async def admin_edit_resource( 

905 uri: str, 

906 request: Request, 

907 db: Session = Depends(get_db), 

908 user: str = Depends(require_auth), 

909) -> RedirectResponse: 

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

911 

912 Expects form fields: 

913 - name 

914 - description (optional) 

915 - mime_type (optional) 

916 - content 

917 

918 Args: 

919 uri: Resource URI. 

920 request: FastAPI request containing form data. 

921 db: Database session. 

922 user: Authenticated user. 

923 

924 Returns: 

925 A redirect response to the admin dashboard. 

926 """ 

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

928 form = await request.form() 

929 resource = ResourceUpdate( 

930 name=form["name"], 

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

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

933 content=form["content"], 

934 ) 

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

936 

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

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

939 

940 

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

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

943 """ 

944 Delete a resource via the admin UI. 

945 

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

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

948 user authentication and logs the deletion attempt. 

949 

950 Args: 

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

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

953 db (Session): Database session dependency. 

954 user (str): Authenticated user dependency. 

955 

956 Returns: 

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

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

959 """ 

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

961 await resource_service.delete_resource(db, uri) 

962 

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

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

965 

966 

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

968async def admin_toggle_resource( 

969 resource_id: int, 

970 request: Request, 

971 db: Session = Depends(get_db), 

972 user: str = Depends(require_auth), 

973) -> RedirectResponse: 

974 """ 

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

976 

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

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

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

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

981 

982 Args: 

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

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

985 db (Session): Database session dependency. 

986 user (str): Authenticated user dependency. 

987 

988 Returns: 

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

990 status code of 303 (See Other). 

991 """ 

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

993 form = await request.form() 

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

995 try: 

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

997 except Exception as e: 

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

999 

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

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

1002 

1003 

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

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

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

1007 

1008 Args: 

1009 name: Prompt name. 

1010 db: Database session. 

1011 user: Authenticated user. 

1012 

1013 Returns: 

1014 A dictionary with prompt details. 

1015 """ 

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

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

1018 

1019 prompt = PromptRead.model_validate(prompt_details) 

1020 return prompt.dict(by_alias=True) 

1021 

1022 

1023@admin_router.post("/prompts") 

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

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

1026 

1027 Expects form fields: 

1028 - name 

1029 - description (optional) 

1030 - template 

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

1032 

1033 Args: 

1034 request: FastAPI request containing form data. 

1035 db: Database session. 

1036 user: Authenticated user. 

1037 

1038 Returns: 

1039 A redirect response to the admin dashboard. 

1040 """ 

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

1042 form = await request.form() 

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

1044 arguments = json.loads(args_json) 

1045 prompt = PromptCreate( 

1046 name=form["name"], 

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

1048 template=form["template"], 

1049 arguments=arguments, 

1050 ) 

1051 await prompt_service.register_prompt(db, prompt) 

1052 

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

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

1055 

1056 

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

1058async def admin_edit_prompt( 

1059 name: str, 

1060 request: Request, 

1061 db: Session = Depends(get_db), 

1062 user: str = Depends(require_auth), 

1063) -> RedirectResponse: 

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

1065 

1066 Expects form fields: 

1067 - name 

1068 - description (optional) 

1069 - template 

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

1071 

1072 Args: 

1073 name: Prompt name. 

1074 request: FastAPI request containing form data. 

1075 db: Database session. 

1076 user: Authenticated user. 

1077 

1078 Returns: 

1079 A redirect response to the admin dashboard. 

1080 """ 

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

1082 form = await request.form() 

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

1084 arguments = json.loads(args_json) 

1085 prompt = PromptUpdate( 

1086 name=form["name"], 

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

1088 template=form["template"], 

1089 arguments=arguments, 

1090 ) 

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

1092 

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

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

1095 

1096 

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

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

1099 """ 

1100 Delete a prompt via the admin UI. 

1101 

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

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

1104 for administrative auditing. 

1105 

1106 Args: 

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

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

1109 db (Session): Database session dependency. 

1110 user (str): Authenticated user dependency. 

1111 

1112 Returns: 

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

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

1115 """ 

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

1117 await prompt_service.delete_prompt(db, name) 

1118 

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

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

1121 

1122 

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

1124async def admin_toggle_prompt( 

1125 prompt_id: int, 

1126 request: Request, 

1127 db: Session = Depends(get_db), 

1128 user: str = Depends(require_auth), 

1129) -> RedirectResponse: 

1130 """ 

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

1132 

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

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

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

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

1137 

1138 Args: 

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

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

1141 db (Session): Database session dependency. 

1142 user (str): Authenticated user dependency. 

1143 

1144 Returns: 

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

1146 status code of 303 (See Other). 

1147 """ 

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

1149 form = await request.form() 

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

1151 try: 

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

1153 except Exception as e: 

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

1155 

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

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

1158 

1159 

1160@admin_router.post("/roots") 

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

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

1163 

1164 Expects form fields: 

1165 - path 

1166 - name (optional) 

1167 

1168 Args: 

1169 request: FastAPI request containing form data. 

1170 user: Authenticated user. 

1171 

1172 Returns: 

1173 A redirect response to the admin dashboard. 

1174 """ 

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

1176 form = await request.form() 

1177 uri = form["uri"] 

1178 name = form.get("name") 

1179 await root_service.add_root(uri, name) 

1180 

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

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

1183 

1184 

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

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

1187 """ 

1188 Delete a root via the admin UI. 

1189 

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

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

1192 operation for audit purposes. 

1193 

1194 Args: 

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

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

1197 user (str): Authenticated user dependency. 

1198 

1199 Returns: 

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

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

1202 """ 

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

1204 await root_service.remove_root(uri) 

1205 

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

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

1208 

1209 

1210# Metrics 

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

1212 

1213 

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

1215async def admin_get_metrics( 

1216 db: Session = Depends(get_db), 

1217 user: str = Depends(require_auth), 

1218) -> MetricsDict: 

1219 """ 

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

1221 

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

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

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

1225 success rates, and other relevant metrics for administrative monitoring 

1226 and analysis purposes. 

1227 

1228 Args: 

1229 db (Session): Database session dependency. 

1230 user (str): Authenticated user dependency. 

1231 

1232 Returns: 

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

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

1235 specific to the entity type. 

1236 """ 

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

1238 tool_metrics = await tool_service.aggregate_metrics(db) 

1239 resource_metrics = await resource_service.aggregate_metrics(db) 

1240 server_metrics = await server_service.aggregate_metrics(db) 

1241 prompt_metrics = await prompt_service.aggregate_metrics(db) 

1242 

1243 # Return actual Pydantic model instances 

1244 return { 

1245 "tools": tool_metrics, 

1246 "resources": resource_metrics, 

1247 "servers": server_metrics, 

1248 "prompts": prompt_metrics, 

1249 } 

1250 

1251 

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

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

1254 """ 

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

1256 Each service must implement its own reset_metrics method. 

1257 

1258 Args: 

1259 db (Session): Database session dependency. 

1260 user (str): Authenticated user dependency. 

1261 

1262 Returns: 

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

1264 """ 

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

1266 await tool_service.reset_metrics(db) 

1267 await resource_service.reset_metrics(db) 

1268 await server_service.reset_metrics(db) 

1269 await prompt_service.reset_metrics(db) 

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