Coverage for mcpgateway/admin.py: 89%
335 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-22 12:53 +0100
« 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.
4Copyright 2025
5SPDX-License-Identifier: Apache-2.0
6Authors: Mihai Criveti
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.
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"""
20import json
21import logging
22from typing import Any, Dict, List, Union
24from fastapi import APIRouter, Depends, HTTPException, Request
25from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
26from sqlalchemy.orm import Session
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
64# Initialize services
65server_service = ServerService()
66tool_service = ToolService()
67prompt_service = PromptService()
68gateway_service = GatewayService()
69resource_service = ResourceService()
70root_service = RootService()
72# Set up basic authentication
73logger = logging.getLogger("mcpgateway")
75admin_router = APIRouter(prefix="/admin", tags=["Admin UI"])
77####################
78# Admin UI Routes #
79####################
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.
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.
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]
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.
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.
114 Returns:
115 ServerRead: The server details.
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))
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.
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.
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
145 Args:
146 request (Request): FastAPI request containing form data.
147 db (Session): Database session dependency
148 user (str): Authenticated user dependency
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)
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}")
171 root_path = request.scope.get("root_path", "")
172 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
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.
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.
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
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
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)
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}")
224 root_path = request.scope.get("root_path", "")
225 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
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.
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.
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.
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}")
261 root_path = request.scope.get("root_path", "")
262 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
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.
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.
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
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}")
289 root_path = request.scope.get("root_path", "")
290 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
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.
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.
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.
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]
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.
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.
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.
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]
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.
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.
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.
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]
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.
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.
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.
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}")
403 root_path = request.scope.get("root_path", "")
404 return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)
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.
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.
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.
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.
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 )
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
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.
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.
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.
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]
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.
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.
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.
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)
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.
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)
536 Logs the raw form data and assembled tool_data for debugging.
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.
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)}")
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)
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.
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)
610 Assembles the tool_data dictionary by remapping form keys into the
611 snake-case keys expected by the schemas.
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.
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)
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)
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.
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.
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.
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)
677 root_path = request.scope.get("root_path", "")
678 return RedirectResponse(f"{root_path}/admin#tools", status_code=303)
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.
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.
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.
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}")
714 root_path = request.scope.get("root_path", "")
715 return RedirectResponse(f"{root_path}/admin#tools", status_code=303)
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.
722 Args:
723 gateway_id: Gateway ID.
724 db: Database session.
725 user: Authenticated user.
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)
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.
739 Expects form fields:
740 - name
741 - url
742 - description (optional)
744 Args:
745 request: FastAPI request containing form data.
746 db: Database session.
747 user: Authenticated user.
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)
778 return RedirectResponse(f"{root_path}/admin#gateways", status_code=500)
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.
790 Expects form fields:
791 - name
792 - url
793 - description (optional)
795 Args:
796 gateway_id: Gateway ID.
797 request: FastAPI request containing form data.
798 db: Database session.
799 user: Authenticated user.
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)
820 root_path = request.scope.get("root_path", "")
821 return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)
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.
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.
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.
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)
846 root_path = request.scope.get("root_path", "")
847 return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)
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.
854 Args:
855 uri: Resource URI.
856 db: Database session.
857 user: Authenticated user.
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}
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.
872 Expects form fields:
873 - uri
874 - name
875 - description (optional)
876 - mime_type (optional)
877 - content
879 Args:
880 request: FastAPI request containing form data.
881 db: Database session.
882 user: Authenticated user.
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)
899 root_path = request.scope.get("root_path", "")
900 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)
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.
912 Expects form fields:
913 - name
914 - description (optional)
915 - mime_type (optional)
916 - content
918 Args:
919 uri: Resource URI.
920 request: FastAPI request containing form data.
921 db: Database session.
922 user: Authenticated user.
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)
937 root_path = request.scope.get("root_path", "")
938 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)
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.
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.
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.
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)
963 root_path = request.scope.get("root_path", "")
964 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)
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.
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.
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.
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}")
1000 root_path = request.scope.get("root_path", "")
1001 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)
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.
1008 Args:
1009 name: Prompt name.
1010 db: Database session.
1011 user: Authenticated user.
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)
1019 prompt = PromptRead.model_validate(prompt_details)
1020 return prompt.dict(by_alias=True)
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.
1027 Expects form fields:
1028 - name
1029 - description (optional)
1030 - template
1031 - arguments (as a JSON string representing a list)
1033 Args:
1034 request: FastAPI request containing form data.
1035 db: Database session.
1036 user: Authenticated user.
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)
1053 root_path = request.scope.get("root_path", "")
1054 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)
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.
1066 Expects form fields:
1067 - name
1068 - description (optional)
1069 - template
1070 - arguments (as a JSON string representing a list)
1072 Args:
1073 name: Prompt name.
1074 request: FastAPI request containing form data.
1075 db: Database session.
1076 user: Authenticated user.
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)
1093 root_path = request.scope.get("root_path", "")
1094 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)
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.
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.
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.
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)
1119 root_path = request.scope.get("root_path", "")
1120 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)
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.
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.
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.
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}")
1156 root_path = request.scope.get("root_path", "")
1157 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)
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.
1164 Expects form fields:
1165 - path
1166 - name (optional)
1168 Args:
1169 request: FastAPI request containing form data.
1170 user: Authenticated user.
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)
1181 root_path = request.scope.get("root_path", "")
1182 return RedirectResponse(f"{root_path}/admin#roots", status_code=303)
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.
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.
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.
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)
1206 root_path = request.scope.get("root_path", "")
1207 return RedirectResponse(f"{root_path}/admin#roots", status_code=303)
1210# Metrics
1211MetricsDict = Dict[str, Union[ToolMetrics, ResourceMetrics, ServerMetrics, PromptMetrics]]
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.
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.
1228 Args:
1229 db (Session): Database session dependency.
1230 user (str): Authenticated user dependency.
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)
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 }
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.
1258 Args:
1259 db (Session): Database session dependency.
1260 user (str): Authenticated user dependency.
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}