Coverage for mcpgateway/admin.py: 79%
414 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-09 11:03 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-09 11:03 +0100
1# -*- coding: utf-8 -*-
2"""Admin UI Routes for MCP Gateway.
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"""
20# Standard
21import json
22import logging
23import time
24from typing import Any, Dict, List, Union
26# Third-Party
27from fastapi import APIRouter, Depends, HTTPException, Request
28from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
29import httpx
30from sqlalchemy.orm import Session
32# First-Party
33from mcpgateway.config import settings
34from mcpgateway.db import get_db
35from mcpgateway.schemas import (
36 GatewayCreate,
37 GatewayRead,
38 GatewayTestRequest,
39 GatewayTestResponse,
40 GatewayUpdate,
41 PromptCreate,
42 PromptMetrics,
43 PromptRead,
44 PromptUpdate,
45 ResourceCreate,
46 ResourceMetrics,
47 ResourceRead,
48 ResourceUpdate,
49 ServerCreate,
50 ServerMetrics,
51 ServerRead,
52 ServerUpdate,
53 ToolCreate,
54 ToolMetrics,
55 ToolRead,
56 ToolUpdate,
57)
58from mcpgateway.services.gateway_service import GatewayConnectionError, GatewayService
59from mcpgateway.services.prompt_service import PromptService
60from mcpgateway.services.resource_service import ResourceService
61from mcpgateway.services.root_service import RootService
62from mcpgateway.services.server_service import ServerNotFoundError, ServerService
63from mcpgateway.services.tool_service import (
64 ToolError,
65 ToolNameConflictError,
66 ToolService,
67)
68from mcpgateway.utils.create_jwt_token import get_jwt_token
69from mcpgateway.utils.verify_credentials import require_auth, require_basic_auth
71# Initialize services
72server_service = ServerService()
73tool_service = ToolService()
74prompt_service = PromptService()
75gateway_service = GatewayService()
76resource_service = ResourceService()
77root_service = RootService()
79# Set up basic authentication
80logger = logging.getLogger("mcpgateway")
82admin_router = APIRouter(prefix="/admin", tags=["Admin UI"])
84####################
85# Admin UI Routes #
86####################
89@admin_router.get("/servers", response_model=List[ServerRead])
90async def admin_list_servers(
91 include_inactive: bool = False,
92 db: Session = Depends(get_db),
93 user: str = Depends(require_auth),
94) -> List[ServerRead]:
95 """
96 List servers for the admin UI with an option to include inactive servers.
98 Args:
99 include_inactive (bool): Whether to include inactive servers.
100 db (Session): The database session dependency.
101 user (str): The authenticated user dependency.
103 Returns:
104 List[ServerRead]: A list of server records.
105 """
106 logger.debug(f"User {user} requested server list")
107 servers = await server_service.list_servers(db, include_inactive=include_inactive)
108 return [server.model_dump(by_alias=True) for server in servers]
111@admin_router.get("/servers/{server_id}", response_model=ServerRead)
112async def admin_get_server(server_id: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> ServerRead:
113 """
114 Retrieve server details for the admin UI.
116 Args:
117 server_id (str): The ID of the server to retrieve.
118 db (Session): The database session dependency.
119 user (str): The authenticated user dependency.
121 Returns:
122 ServerRead: The server details.
124 Raises:
125 HTTPException: If the server is not found.
126 """
127 try:
128 logger.debug(f"User {user} requested details for server ID {server_id}")
129 server = await server_service.get_server(db, server_id)
130 return server.model_dump(by_alias=True)
131 except ServerNotFoundError as e:
132 raise HTTPException(status_code=404, detail=str(e))
135@admin_router.post("/servers", response_model=ServerRead)
136async def admin_add_server(request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:
137 """
138 Add a new server via the admin UI.
140 This endpoint processes form data to create a new server entry in the database.
141 It handles exceptions gracefully and logs any errors that occur during server
142 registration.
144 Expects form fields:
145 - name (required): The name of the server
146 - description (optional): A description of the server's purpose
147 - icon (optional): URL or path to the server's icon
148 - associatedTools (optional, comma-separated): Tools associated with this server
149 - associatedResources (optional, comma-separated): Resources associated with this server
150 - associatedPrompts (optional, comma-separated): Prompts associated with this server
152 Args:
153 request (Request): FastAPI request containing form data.
154 db (Session): Database session dependency
155 user (str): Authenticated user dependency
157 Returns:
158 RedirectResponse: A redirect to the admin dashboard catalog section
159 """
160 form = await request.form()
161 is_inactive_checked = form.get("is_inactive_checked", "false")
162 try:
163 logger.debug(f"User {user} is adding a new server with name: {form['name']}")
165 server = ServerCreate(
166 name=form.get("name"),
167 description=form.get("description"),
168 icon=form.get("icon"),
169 associated_tools=",".join(form.getlist("associatedTools")),
170 associated_resources=form.get("associatedResources"),
171 associated_prompts=form.get("associatedPrompts"),
172 )
173 await server_service.register_server(db, server)
175 root_path = request.scope.get("root_path", "")
176 if is_inactive_checked.lower() == "true": 176 ↛ 177line 176 didn't jump to line 177 because the condition on line 176 was never true
177 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)
178 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
179 except Exception as e:
180 logger.error(f"Error adding server: {e}")
182 root_path = request.scope.get("root_path", "")
183 if is_inactive_checked.lower() == "true":
184 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)
185 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
188@admin_router.post("/servers/{server_id}/edit")
189async def admin_edit_server(
190 server_id: str,
191 request: Request,
192 db: Session = Depends(get_db),
193 user: str = Depends(require_auth),
194) -> RedirectResponse:
195 """
196 Edit an existing server via the admin UI.
198 This endpoint processes form data to update an existing server's properties.
199 It handles exceptions gracefully and logs any errors that occur during the
200 update operation.
202 Expects form fields:
203 - name (optional): The updated name of the server
204 - description (optional): An updated description of the server's purpose
205 - icon (optional): Updated URL or path to the server's icon
206 - associatedTools (optional, comma-separated): Updated list of tools associated with this server
207 - associatedResources (optional, comma-separated): Updated list of resources associated with this server
208 - associatedPrompts (optional, comma-separated): Updated list of prompts associated with this server
210 Args:
211 server_id (str): The ID of the server to edit
212 request (Request): FastAPI request containing form data
213 db (Session): Database session dependency
214 user (str): Authenticated user dependency
216 Returns:
217 RedirectResponse: A redirect to the admin dashboard catalog section with a status code of 303
218 """
219 form = await request.form()
220 is_inactive_checked = form.get("is_inactive_checked", "false")
221 try:
222 logger.debug(f"User {user} is editing server ID {server_id} with name: {form.get('name')}")
223 server = ServerUpdate(
224 name=form.get("name"),
225 description=form.get("description"),
226 icon=form.get("icon"),
227 associated_tools=",".join(form.getlist("associatedTools")),
228 associated_resources=form.get("associatedResources"),
229 associated_prompts=form.get("associatedPrompts"),
230 )
231 await server_service.update_server(db, server_id, server)
233 root_path = request.scope.get("root_path", "")
235 if is_inactive_checked.lower() == "true": 235 ↛ 236line 235 didn't jump to line 236 because the condition on line 235 was never true
236 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)
237 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
238 except Exception as e:
239 logger.error(f"Error editing server: {e}")
241 root_path = request.scope.get("root_path", "")
242 if is_inactive_checked.lower() == "true":
243 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)
244 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
247@admin_router.post("/servers/{server_id}/toggle")
248async def admin_toggle_server(
249 server_id: str,
250 request: Request,
251 db: Session = Depends(get_db),
252 user: str = Depends(require_auth),
253) -> RedirectResponse:
254 """
255 Toggle a server's active status via the admin UI.
257 This endpoint processes a form request to activate or deactivate a server.
258 It expects a form field 'activate' with value "true" to activate the server
259 or "false" to deactivate it. The endpoint handles exceptions gracefully and
260 logs any errors that might occur during the status toggle operation.
262 Args:
263 server_id (str): The ID of the server whose status to toggle.
264 request (Request): FastAPI request containing form data with the 'activate' field.
265 db (Session): Database session dependency.
266 user (str): Authenticated user dependency.
268 Returns:
269 RedirectResponse: A redirect to the admin dashboard catalog section with a
270 status code of 303 (See Other).
271 """
272 form = await request.form()
273 logger.debug(f"User {user} is toggling server ID {server_id} with activate: {form.get('activate')}")
274 activate = form.get("activate", "true").lower() == "true"
275 is_inactive_checked = form.get("is_inactive_checked", "false")
276 try:
277 await server_service.toggle_server_status(db, server_id, activate)
278 except Exception as e:
279 logger.error(f"Error toggling server status: {e}")
281 root_path = request.scope.get("root_path", "")
282 if is_inactive_checked.lower() == "true": 282 ↛ 283line 282 didn't jump to line 283 because the condition on line 282 was never true
283 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)
284 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
287@admin_router.post("/servers/{server_id}/delete")
288async def admin_delete_server(server_id: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:
289 """
290 Delete a server via the admin UI.
292 This endpoint removes a server from the database by its ID. It handles exceptions
293 gracefully and logs any errors that occur during the deletion process.
295 Args:
296 server_id (str): The ID of the server to delete
297 request (Request): FastAPI request object (not used but required by route signature).
298 db (Session): Database session dependency
299 user (str): Authenticated user dependency
301 Returns:
302 RedirectResponse: A redirect to the admin dashboard catalog section with a
303 status code of 303 (See Other)
304 """
305 try:
306 logger.debug(f"User {user} is deleting server ID {server_id}")
307 await server_service.delete_server(db, server_id)
308 except Exception as e:
309 logger.error(f"Error deleting server: {e}")
311 form = await request.form()
312 is_inactive_checked = form.get("is_inactive_checked", "false")
313 root_path = request.scope.get("root_path", "")
315 if is_inactive_checked.lower() == "true": 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true
316 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#catalog", status_code=303)
317 return RedirectResponse(f"{root_path}/admin#catalog", status_code=303)
320@admin_router.get("/resources", response_model=List[ResourceRead])
321async def admin_list_resources(
322 include_inactive: bool = False,
323 db: Session = Depends(get_db),
324 user: str = Depends(require_auth),
325) -> List[ResourceRead]:
326 """
327 List resources for the admin UI with an option to include inactive resources.
329 This endpoint retrieves a list of resources from the database, optionally including
330 those that are inactive. The inactive filter is useful for administrators who need
331 to view or manage resources that have been deactivated but not deleted.
333 Args:
334 include_inactive (bool): Whether to include inactive resources in the results.
335 db (Session): Database session dependency.
336 user (str): Authenticated user dependency.
338 Returns:
339 List[ResourceRead]: A list of resource records formatted with by_alias=True.
340 """
341 logger.debug(f"User {user} requested resource list")
342 resources = await resource_service.list_resources(db, include_inactive=include_inactive)
343 return [resource.model_dump(by_alias=True) for resource in resources]
346@admin_router.get("/prompts", response_model=List[PromptRead])
347async def admin_list_prompts(
348 include_inactive: bool = False,
349 db: Session = Depends(get_db),
350 user: str = Depends(require_auth),
351) -> List[PromptRead]:
352 """
353 List prompts for the admin UI with an option to include inactive prompts.
355 This endpoint retrieves a list of prompts from the database, optionally including
356 those that are inactive. The inactive filter helps administrators see and manage
357 prompts that have been deactivated but not deleted from the system.
359 Args:
360 include_inactive (bool): Whether to include inactive prompts in the results.
361 db (Session): Database session dependency.
362 user (str): Authenticated user dependency.
364 Returns:
365 List[PromptRead]: A list of prompt records formatted with by_alias=True.
366 """
367 logger.debug(f"User {user} requested prompt list")
368 prompts = await prompt_service.list_prompts(db, include_inactive=include_inactive)
369 return [prompt.model_dump(by_alias=True) for prompt in prompts]
372@admin_router.get("/gateways", response_model=List[GatewayRead])
373async def admin_list_gateways(
374 include_inactive: bool = False,
375 db: Session = Depends(get_db),
376 user: str = Depends(require_auth),
377) -> List[GatewayRead]:
378 """
379 List gateways for the admin UI with an option to include inactive gateways.
381 This endpoint retrieves a list of gateways from the database, optionally
382 including those that are inactive. The inactive filter allows administrators
383 to view and manage gateways that have been deactivated but not deleted.
385 Args:
386 include_inactive (bool): Whether to include inactive gateways in the results.
387 db (Session): Database session dependency.
388 user (str): Authenticated user dependency.
390 Returns:
391 List[GatewayRead]: A list of gateway records formatted with by_alias=True.
392 """
393 logger.debug(f"User {user} requested gateway list")
394 gateways = await gateway_service.list_gateways(db, include_inactive=include_inactive)
395 return [gateway.model_dump(by_alias=True) for gateway in gateways]
398@admin_router.post("/gateways/{gateway_id}/toggle")
399async def admin_toggle_gateway(
400 gateway_id: str,
401 request: Request,
402 db: Session = Depends(get_db),
403 user: str = Depends(require_auth),
404) -> RedirectResponse:
405 """
406 Toggle the active status of a gateway via the admin UI.
408 This endpoint allows an admin to toggle the active status of a gateway.
409 It expects a form field 'activate' with a value of "true" or "false" to
410 determine the new status of the gateway.
412 Args:
413 gateway_id (str): The ID of the gateway to toggle.
414 request (Request): The FastAPI request object containing form data.
415 db (Session): The database session dependency.
416 user (str): The authenticated user dependency.
418 Returns:
419 RedirectResponse: A redirect response to the admin dashboard with a
420 status code of 303 (See Other).
421 """
422 logger.debug(f"User {user} is toggling gateway ID {gateway_id}")
423 form = await request.form()
424 activate = form.get("activate", "true").lower() == "true"
425 is_inactive_checked = form.get("is_inactive_checked", "false")
427 try:
428 await gateway_service.toggle_gateway_status(db, gateway_id, activate)
429 except Exception as e:
430 logger.error(f"Error toggling gateway status: {e}")
432 root_path = request.scope.get("root_path", "")
433 if is_inactive_checked.lower() == "true": 433 ↛ 434line 433 didn't jump to line 434 because the condition on line 433 was never true
434 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#gateways", status_code=303)
435 return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)
438@admin_router.get("/", name="admin_home", response_class=HTMLResponse)
439async def admin_ui(
440 request: Request,
441 include_inactive: bool = False,
442 db: Session = Depends(get_db),
443 user: str = Depends(require_basic_auth),
444 jwt_token: str = Depends(get_jwt_token),
445) -> HTMLResponse:
446 """
447 Render the admin dashboard HTML page.
449 This endpoint serves as the main entry point to the admin UI. It fetches data for
450 servers, tools, resources, prompts, gateways, and roots from their respective
451 services, then renders the admin dashboard template with this data.
453 The endpoint also sets a JWT token as a cookie for authentication in subsequent
454 requests. This token is HTTP-only for security reasons.
456 Args:
457 request (Request): FastAPI request object.
458 include_inactive (bool): Whether to include inactive items in all listings.
459 db (Session): Database session dependency.
460 user (str): Authenticated user from basic auth dependency.
461 jwt_token (str): JWT token for authentication.
463 Returns:
464 HTMLResponse: Rendered HTML template for the admin dashboard.
465 """
466 logger.debug(f"User {user} accessed the admin UI")
467 servers = [server.model_dump(by_alias=True) for server in await server_service.list_servers(db, include_inactive=include_inactive)]
468 tools = [tool.model_dump(by_alias=True) for tool in await tool_service.list_tools(db, include_inactive=include_inactive)]
469 resources = [resource.model_dump(by_alias=True) for resource in await resource_service.list_resources(db, include_inactive=include_inactive)]
470 prompts = [prompt.model_dump(by_alias=True) for prompt in await prompt_service.list_prompts(db, include_inactive=include_inactive)]
471 gateways = [gateway.model_dump(by_alias=True) for gateway in await gateway_service.list_gateways(db, include_inactive=include_inactive)]
472 roots = [root.model_dump(by_alias=True) for root in await root_service.list_roots()]
473 root_path = settings.app_root_path
474 response = request.app.state.templates.TemplateResponse(
475 request,
476 "admin.html",
477 {
478 "request": request,
479 "servers": servers,
480 "tools": tools,
481 "resources": resources,
482 "prompts": prompts,
483 "gateways": gateways,
484 "roots": roots,
485 "include_inactive": include_inactive,
486 "root_path": root_path,
487 "gateway_tool_name_separator": settings.gateway_tool_name_separator,
488 },
489 )
491 response.set_cookie(key="jwt_token", value=jwt_token, httponly=True, secure=False, samesite="Strict") # JavaScript CAN'T read it # only over HTTPS # or "Lax" per your needs
492 return response
495@admin_router.get("/tools", response_model=List[ToolRead])
496async def admin_list_tools(
497 include_inactive: bool = False,
498 db: Session = Depends(get_db),
499 user: str = Depends(require_auth),
500) -> List[ToolRead]:
501 """
502 List tools for the admin UI with an option to include inactive tools.
504 This endpoint retrieves a list of tools from the database, optionally including
505 those that are inactive. The inactive filter helps administrators manage tools
506 that have been deactivated but not deleted from the system.
508 Args:
509 include_inactive (bool): Whether to include inactive tools in the results.
510 db (Session): Database session dependency.
511 user (str): Authenticated user dependency.
513 Returns:
514 List[ToolRead]: A list of tool records formatted with by_alias=True.
515 """
516 logger.debug(f"User {user} requested tool list")
517 tools = await tool_service.list_tools(db, include_inactive=include_inactive)
518 return [tool.model_dump(by_alias=True) for tool in tools]
521@admin_router.get("/tools/{tool_id}", response_model=ToolRead)
522async def admin_get_tool(tool_id: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> ToolRead:
523 """
524 Retrieve specific tool details for the admin UI.
526 This endpoint fetches the details of a specific tool from the database
527 by its ID. It provides access to all information about the tool for
528 viewing and management purposes.
530 Args:
531 tool_id (str): The ID of the tool to retrieve.
532 db (Session): Database session dependency.
533 user (str): Authenticated user dependency.
535 Returns:
536 ToolRead: The tool details formatted with by_alias=True.
537 """
538 logger.debug(f"User {user} requested details for tool ID {tool_id}")
539 tool = await tool_service.get_tool(db, tool_id)
540 return tool.model_dump(by_alias=True)
543@admin_router.post("/tools/")
544@admin_router.post("/tools")
545async def admin_add_tool(
546 request: Request,
547 db: Session = Depends(get_db),
548 user: str = Depends(require_auth),
549) -> JSONResponse:
550 """
551 Add a tool via the admin UI with error handling.
553 Expects form fields:
554 - name
555 - url
556 - description (optional)
557 - requestType (mapped to request_type; defaults to "SSE")
558 - integrationType (mapped to integration_type; defaults to "MCP")
559 - headers (JSON string)
560 - input_schema (JSON string)
561 - jsonpath_filter (optional)
562 - auth_type (optional)
563 - auth_username (optional)
564 - auth_password (optional)
565 - auth_token (optional)
566 - auth_header_key (optional)
567 - auth_header_value (optional)
569 Logs the raw form data and assembled tool_data for debugging.
571 Args:
572 request (Request): the FastAPI request object containing the form data.
573 db (Session): the SQLAlchemy database session.
574 user (str): identifier of the authenticated user.
576 Returns:
577 JSONResponse: a JSON response with `{"message": ..., "success": ...}` and an appropriate HTTP status code.
578 """
579 logger.debug(f"User {user} is adding a new tool")
580 form = await request.form()
581 logger.debug(f"Received form data: {dict(form)}")
583 tool_data = {
584 "name": form["name"],
585 "url": form["url"],
586 "description": form.get("description"),
587 "request_type": form.get("requestType", "SSE"),
588 "integration_type": form.get("integrationType", "MCP"),
589 "headers": json.loads(form.get("headers") or "{}"),
590 "input_schema": json.loads(form.get("input_schema") or "{}"),
591 "jsonpath_filter": form.get("jsonpath_filter", ""),
592 "auth_type": form.get("auth_type", ""),
593 "auth_username": form.get("auth_username", ""),
594 "auth_password": form.get("auth_password", ""),
595 "auth_token": form.get("auth_token", ""),
596 "auth_header_key": form.get("auth_header_key", ""),
597 "auth_header_value": form.get("auth_header_value", ""),
598 }
599 logger.debug(f"Tool data built: {tool_data}")
600 try:
601 tool = ToolCreate(**tool_data)
602 logger.debug(f"Validated tool data: {tool.model_dump(by_alias=True)}")
603 await tool_service.register_tool(db, tool)
604 return JSONResponse(
605 content={"message": "Tool registered successfully!", "success": True},
606 status_code=200,
607 )
608 except ToolNameConflictError as e:
609 logger.error(f"ToolNameConflictError: {str(e)}")
610 return JSONResponse(content={"message": str(e), "success": False}, status_code=400)
611 except Exception as e:
612 logger.error(f"Error in admin_add_tool: {str(e)}")
613 return JSONResponse(content={"message": str(e), "success": False}, status_code=500)
616@admin_router.post("/tools/{tool_id}/edit/")
617@admin_router.post("/tools/{tool_id}/edit")
618async def admin_edit_tool(
619 tool_id: str,
620 request: Request,
621 db: Session = Depends(get_db),
622 user: str = Depends(require_auth),
623) -> RedirectResponse:
624 """
625 Edit a tool via the admin UI.
627 Expects form fields:
628 - name
629 - url
630 - description (optional)
631 - requestType (to be mapped to request_type)
632 - integrationType (to be mapped to integration_type)
633 - headers (as a JSON string)
634 - input_schema (as a JSON string)
635 - jsonpathFilter (optional)
636 - auth_type (optional, string: "basic", "bearer", or empty)
637 - auth_username (optional, for basic auth)
638 - auth_password (optional, for basic auth)
639 - auth_token (optional, for bearer auth)
640 - auth_header_key (optional, for headers auth)
641 - auth_header_value (optional, for headers auth)
643 Assembles the tool_data dictionary by remapping form keys into the
644 snake-case keys expected by the schemas.
646 Args:
647 tool_id (str): The ID of the tool to edit.
648 request (Request): FastAPI request containing form data.
649 db (Session): Database session dependency.
650 user (str): Authenticated user dependency.
652 Returns:
653 RedirectResponse: A redirect response to the tools section of the admin
654 dashboard with a status code of 303 (See Other), or a JSON response with
655 an error message if the update fails.
656 """
657 logger.debug(f"User {user} is editing tool ID {tool_id}")
658 form = await request.form()
659 tool_data = {
660 "name": form["name"],
661 "url": form["url"],
662 "description": form.get("description"),
663 "request_type": form.get("requestType", "SSE"),
664 "integration_type": form.get("integrationType", "MCP"),
665 "headers": json.loads(form.get("headers") or "{}"),
666 "input_schema": json.loads(form.get("input_schema") or "{}"),
667 "jsonpath_filter": form.get("jsonpathFilter", ""),
668 "auth_type": form.get("auth_type", ""),
669 "auth_username": form.get("auth_username", ""),
670 "auth_password": form.get("auth_password", ""),
671 "auth_token": form.get("auth_token", ""),
672 "auth_header_key": form.get("auth_header_key", ""),
673 "auth_header_value": form.get("auth_header_value", ""),
674 }
675 logger.debug(f"Tool update data built: {tool_data}")
676 tool = ToolUpdate(**tool_data)
677 try:
678 await tool_service.update_tool(db, tool_id, tool)
680 root_path = request.scope.get("root_path", "")
681 is_inactive_checked = form.get("is_inactive_checked", "false")
682 if is_inactive_checked.lower() == "true": 682 ↛ 683line 682 didn't jump to line 683 because the condition on line 682 was never true
683 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#tools", status_code=303)
684 return RedirectResponse(f"{root_path}/admin#tools", status_code=303)
685 except ToolNameConflictError as e:
686 return JSONResponse(content={"message": str(e), "success": False}, status_code=400)
687 except ToolError as e:
688 return JSONResponse(content={"message": str(e), "success": False}, status_code=500)
691@admin_router.post("/tools/{tool_id}/delete")
692async def admin_delete_tool(tool_id: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:
693 """
694 Delete a tool via the admin UI.
696 This endpoint permanently removes a tool from the database using its ID.
697 It is irreversible and should be used with caution. The operation is logged,
698 and the user must be authenticated to access this route.
700 Args:
701 tool_id (str): The ID of the tool to delete.
702 request (Request): FastAPI request object (not used directly, but required by route signature).
703 db (Session): Database session dependency.
704 user (str): Authenticated user dependency.
706 Returns:
707 RedirectResponse: A redirect response to the tools section of the admin
708 dashboard with a status code of 303 (See Other).
709 """
710 logger.debug(f"User {user} is deleting tool ID {tool_id}")
711 await tool_service.delete_tool(db, tool_id)
713 form = await request.form()
714 is_inactive_checked = form.get("is_inactive_checked", "false")
715 root_path = request.scope.get("root_path", "")
717 if is_inactive_checked.lower() == "true": 717 ↛ 718line 717 didn't jump to line 718 because the condition on line 717 was never true
718 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#tools", status_code=303)
719 return RedirectResponse(f"{root_path}/admin#tools", status_code=303)
722@admin_router.post("/tools/{tool_id}/toggle")
723async def admin_toggle_tool(
724 tool_id: str,
725 request: Request,
726 db: Session = Depends(get_db),
727 user: str = Depends(require_auth),
728) -> RedirectResponse:
729 """
730 Toggle a tool's active status via the admin UI.
732 This endpoint processes a form request to activate or deactivate a tool.
733 It expects a form field 'activate' with value "true" to activate the tool
734 or "false" to deactivate it. The endpoint handles exceptions gracefully and
735 logs any errors that might occur during the status toggle operation.
737 Args:
738 tool_id (str): The ID of the tool whose status to toggle.
739 request (Request): FastAPI request containing form data with the 'activate' field.
740 db (Session): Database session dependency.
741 user (str): Authenticated user dependency.
743 Returns:
744 RedirectResponse: A redirect to the admin dashboard tools section with a
745 status code of 303 (See Other).
746 """
747 logger.debug(f"User {user} is toggling tool ID {tool_id}")
748 form = await request.form()
749 activate = form.get("activate", "true").lower() == "true"
750 is_inactive_checked = form.get("is_inactive_checked", "false")
751 try:
752 await tool_service.toggle_tool_status(db, tool_id, activate, reachable=activate)
753 except Exception as e:
754 logger.error(f"Error toggling tool status: {e}")
756 root_path = request.scope.get("root_path", "")
757 if is_inactive_checked.lower() == "true": 757 ↛ 758line 757 didn't jump to line 758 because the condition on line 757 was never true
758 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#tools", status_code=303)
759 return RedirectResponse(f"{root_path}/admin#tools", status_code=303)
762@admin_router.get("/gateways/{gateway_id}", response_model=GatewayRead)
763async def admin_get_gateway(gateway_id: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> GatewayRead:
764 """Get gateway details for the admin UI.
766 Args:
767 gateway_id: Gateway ID.
768 db: Database session.
769 user: Authenticated user.
771 Returns:
772 Gateway details.
773 """
774 logger.debug(f"User {user} requested details for gateway ID {gateway_id}")
775 gateway = await gateway_service.get_gateway(db, gateway_id)
776 return gateway.model_dump(by_alias=True)
779@admin_router.post("/gateways")
780async def admin_add_gateway(request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> JSONResponse:
781 """Add a gateway via the admin UI.
783 Expects form fields:
784 - name
785 - url
786 - description (optional)
788 Args:
789 request: FastAPI request containing form data.
790 db: Database session.
791 user: Authenticated user.
793 Returns:
794 A redirect response to the admin dashboard.
795 """
796 logger.debug(f"User {user} is adding a new gateway")
797 form = await request.form()
798 gateway = GatewayCreate(
799 name=form["name"],
800 url=form["url"],
801 description=form.get("description"),
802 transport=form.get("transport", "SSE"),
803 auth_type=form.get("auth_type", ""),
804 auth_username=form.get("auth_username", ""),
805 auth_password=form.get("auth_password", ""),
806 auth_token=form.get("auth_token", ""),
807 auth_header_key=form.get("auth_header_key", ""),
808 auth_header_value=form.get("auth_header_value", ""),
809 )
810 try:
811 await gateway_service.register_gateway(db, gateway)
812 return JSONResponse(
813 content={"message": "Gateway registered successfully!", "success": True},
814 status_code=200,
815 )
817 except Exception as ex:
818 if isinstance(ex, GatewayConnectionError):
819 return JSONResponse(content={"message": str(ex), "success": False}, status_code=502)
820 if isinstance(ex, ValueError):
821 return JSONResponse(content={"message": str(ex), "success": False}, status_code=400)
822 if isinstance(ex, RuntimeError):
823 return JSONResponse(content={"message": str(ex), "success": False}, status_code=500)
824 return JSONResponse(content={"message": str(ex), "success": False}, status_code=500)
827@admin_router.post("/gateways/{gateway_id}/edit")
828async def admin_edit_gateway(
829 gateway_id: str,
830 request: Request,
831 db: Session = Depends(get_db),
832 user: str = Depends(require_auth),
833) -> RedirectResponse:
834 """Edit a gateway via the admin UI.
836 Expects form fields:
837 - name
838 - url
839 - description (optional)
841 Args:
842 gateway_id: Gateway ID.
843 request: FastAPI request containing form data.
844 db: Database session.
845 user: Authenticated user.
847 Returns:
848 A redirect response to the admin dashboard.
849 """
850 logger.debug(f"User {user} is editing gateway ID {gateway_id}")
851 form = await request.form()
852 gateway = GatewayUpdate(
853 name=form["name"],
854 url=form["url"],
855 description=form.get("description"),
856 transport=form.get("transport", "SSE"),
857 auth_type=form.get("auth_type", None),
858 auth_username=form.get("auth_username", None),
859 auth_password=form.get("auth_password", None),
860 auth_token=form.get("auth_token", None),
861 auth_header_key=form.get("auth_header_key", None),
862 auth_header_value=form.get("auth_header_value", None),
863 )
864 await gateway_service.update_gateway(db, gateway_id, gateway)
866 root_path = request.scope.get("root_path", "")
867 is_inactive_checked = form.get("is_inactive_checked", "false")
869 if is_inactive_checked.lower() == "true": 869 ↛ 870line 869 didn't jump to line 870 because the condition on line 869 was never true
870 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#gateways", status_code=303)
871 return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)
874@admin_router.post("/gateways/{gateway_id}/delete")
875async def admin_delete_gateway(gateway_id: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:
876 """
877 Delete a gateway via the admin UI.
879 This endpoint removes a gateway from the database by its ID. The deletion is
880 permanent and cannot be undone. It requires authentication and logs the
881 operation for auditing purposes.
883 Args:
884 gateway_id (str): The ID of the gateway to delete.
885 request (Request): FastAPI request object (not used directly but required by the route signature).
886 db (Session): Database session dependency.
887 user (str): Authenticated user dependency.
889 Returns:
890 RedirectResponse: A redirect response to the gateways section of the admin
891 dashboard with a status code of 303 (See Other).
892 """
893 logger.debug(f"User {user} is deleting gateway ID {gateway_id}")
894 await gateway_service.delete_gateway(db, gateway_id)
896 form = await request.form()
897 is_inactive_checked = form.get("is_inactive_checked", "false")
898 root_path = request.scope.get("root_path", "")
900 if is_inactive_checked.lower() == "true": 900 ↛ 901line 900 didn't jump to line 901 because the condition on line 900 was never true
901 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#gateways", status_code=303)
902 return RedirectResponse(f"{root_path}/admin#gateways", status_code=303)
905@admin_router.get("/resources/{uri:path}")
906async def admin_get_resource(uri: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> Dict[str, Any]:
907 """Get resource details for the admin UI.
909 Args:
910 uri: Resource URI.
911 db: Database session.
912 user: Authenticated user.
914 Returns:
915 A dictionary containing resource details and its content.
916 """
917 logger.debug(f"User {user} requested details for resource URI {uri}")
918 resource = await resource_service.get_resource_by_uri(db, uri)
919 content = await resource_service.read_resource(db, uri)
920 return {"resource": resource.model_dump(by_alias=True), "content": content}
923@admin_router.post("/resources")
924async def admin_add_resource(request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:
925 """Add a resource via the admin UI.
927 Expects form fields:
928 - uri
929 - name
930 - description (optional)
931 - mime_type (optional)
932 - content
934 Args:
935 request: FastAPI request containing form data.
936 db: Database session.
937 user: Authenticated user.
939 Returns:
940 A redirect response to the admin dashboard.
941 """
942 logger.debug(f"User {user} is adding a new resource")
943 form = await request.form()
944 resource = ResourceCreate(
945 uri=form["uri"],
946 name=form["name"],
947 description=form.get("description"),
948 mime_type=form.get("mimeType"),
949 template=form.get("template"), # defaults to None if not provided
950 content=form["content"],
951 )
952 await resource_service.register_resource(db, resource)
954 root_path = request.scope.get("root_path", "")
955 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)
958@admin_router.post("/resources/{uri:path}/edit")
959async def admin_edit_resource(
960 uri: str,
961 request: Request,
962 db: Session = Depends(get_db),
963 user: str = Depends(require_auth),
964) -> RedirectResponse:
965 """Edit a resource via the admin UI.
967 Expects form fields:
968 - name
969 - description (optional)
970 - mime_type (optional)
971 - content
973 Args:
974 uri: Resource URI.
975 request: FastAPI request containing form data.
976 db: Database session.
977 user: Authenticated user.
979 Returns:
980 A redirect response to the admin dashboard.
981 """
982 logger.debug(f"User {user} is editing resource URI {uri}")
983 form = await request.form()
984 resource = ResourceUpdate(
985 name=form["name"],
986 description=form.get("description"),
987 mime_type=form.get("mimeType"),
988 content=form["content"],
989 )
990 await resource_service.update_resource(db, uri, resource)
992 root_path = request.scope.get("root_path", "")
993 is_inactive_checked = form.get("is_inactive_checked", "false")
995 if is_inactive_checked.lower() == "true": 995 ↛ 996line 995 didn't jump to line 996 because the condition on line 995 was never true
996 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#resources", status_code=303)
997 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)
1000@admin_router.post("/resources/{uri:path}/delete")
1001async def admin_delete_resource(uri: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:
1002 """
1003 Delete a resource via the admin UI.
1005 This endpoint permanently removes a resource from the database using its URI.
1006 The operation is irreversible and should be used with caution. It requires
1007 user authentication and logs the deletion attempt.
1009 Args:
1010 uri (str): The URI of the resource to delete.
1011 request (Request): FastAPI request object (not used directly but required by the route signature).
1012 db (Session): Database session dependency.
1013 user (str): Authenticated user dependency.
1015 Returns:
1016 RedirectResponse: A redirect response to the resources section of the admin
1017 dashboard with a status code of 303 (See Other).
1018 """
1019 logger.debug(f"User {user} is deleting resource URI {uri}")
1020 await resource_service.delete_resource(db, uri)
1022 form = await request.form()
1023 is_inactive_checked = form.get("is_inactive_checked", "false")
1024 root_path = request.scope.get("root_path", "")
1026 if is_inactive_checked.lower() == "true": 1026 ↛ 1027line 1026 didn't jump to line 1027 because the condition on line 1026 was never true
1027 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#resources", status_code=303)
1028 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)
1031@admin_router.post("/resources/{resource_id}/toggle")
1032async def admin_toggle_resource(
1033 resource_id: int,
1034 request: Request,
1035 db: Session = Depends(get_db),
1036 user: str = Depends(require_auth),
1037) -> RedirectResponse:
1038 """
1039 Toggle a resource's active status via the admin UI.
1041 This endpoint processes a form request to activate or deactivate a resource.
1042 It expects a form field 'activate' with value "true" to activate the resource
1043 or "false" to deactivate it. The endpoint handles exceptions gracefully and
1044 logs any errors that might occur during the status toggle operation.
1046 Args:
1047 resource_id (int): The ID of the resource whose status to toggle.
1048 request (Request): FastAPI request containing form data with the 'activate' field.
1049 db (Session): Database session dependency.
1050 user (str): Authenticated user dependency.
1052 Returns:
1053 RedirectResponse: A redirect to the admin dashboard resources section with a
1054 status code of 303 (See Other).
1055 """
1056 logger.debug(f"User {user} is toggling resource ID {resource_id}")
1057 form = await request.form()
1058 activate = form.get("activate", "true").lower() == "true"
1059 is_inactive_checked = form.get("is_inactive_checked", "false")
1060 try:
1061 await resource_service.toggle_resource_status(db, resource_id, activate)
1062 except Exception as e:
1063 logger.error(f"Error toggling resource status: {e}")
1065 root_path = request.scope.get("root_path", "")
1066 if is_inactive_checked.lower() == "true": 1066 ↛ 1067line 1066 didn't jump to line 1067 because the condition on line 1066 was never true
1067 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#resources", status_code=303)
1068 return RedirectResponse(f"{root_path}/admin#resources", status_code=303)
1071@admin_router.get("/prompts/{name}")
1072async def admin_get_prompt(name: str, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> Dict[str, Any]:
1073 """Get prompt details for the admin UI.
1075 Args:
1076 name: Prompt name.
1077 db: Database session.
1078 user: Authenticated user.
1080 Returns:
1081 A dictionary with prompt details.
1082 """
1083 logger.debug(f"User {user} requested details for prompt name {name}")
1084 prompt_details = await prompt_service.get_prompt_details(db, name)
1086 prompt = PromptRead.model_validate(prompt_details)
1087 return prompt.model_dump(by_alias=True)
1090@admin_router.post("/prompts")
1091async def admin_add_prompt(request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:
1092 """Add a prompt via the admin UI.
1094 Expects form fields:
1095 - name
1096 - description (optional)
1097 - template
1098 - arguments (as a JSON string representing a list)
1100 Args:
1101 request: FastAPI request containing form data.
1102 db: Database session.
1103 user: Authenticated user.
1105 Returns:
1106 A redirect response to the admin dashboard.
1107 """
1108 logger.debug(f"User {user} is adding a new prompt")
1109 form = await request.form()
1110 args_json = form.get("arguments") or "[]"
1111 arguments = json.loads(args_json)
1112 prompt = PromptCreate(
1113 name=form["name"],
1114 description=form.get("description"),
1115 template=form["template"],
1116 arguments=arguments,
1117 )
1118 await prompt_service.register_prompt(db, prompt)
1120 root_path = request.scope.get("root_path", "")
1121 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)
1124@admin_router.post("/prompts/{name}/edit")
1125async def admin_edit_prompt(
1126 name: str,
1127 request: Request,
1128 db: Session = Depends(get_db),
1129 user: str = Depends(require_auth),
1130) -> RedirectResponse:
1131 """Edit a prompt via the admin UI.
1133 Expects form fields:
1134 - name
1135 - description (optional)
1136 - template
1137 - arguments (as a JSON string representing a list)
1139 Args:
1140 name: Prompt name.
1141 request: FastAPI request containing form data.
1142 db: Database session.
1143 user: Authenticated user.
1145 Returns:
1146 A redirect response to the admin dashboard.
1147 """
1148 logger.debug(f"User {user} is editing prompt name {name}")
1149 form = await request.form()
1150 args_json = form.get("arguments") or "[]"
1151 arguments = json.loads(args_json)
1152 prompt = PromptUpdate(
1153 name=form["name"],
1154 description=form.get("description"),
1155 template=form["template"],
1156 arguments=arguments,
1157 )
1158 await prompt_service.update_prompt(db, name, prompt)
1160 root_path = request.scope.get("root_path", "")
1161 is_inactive_checked = form.get("is_inactive_checked", "false")
1163 if is_inactive_checked.lower() == "true": 1163 ↛ 1164line 1163 didn't jump to line 1164 because the condition on line 1163 was never true
1164 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#prompts", status_code=303)
1165 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)
1168@admin_router.post("/prompts/{name}/delete")
1169async def admin_delete_prompt(name: str, request: Request, db: Session = Depends(get_db), user: str = Depends(require_auth)) -> RedirectResponse:
1170 """
1171 Delete a prompt via the admin UI.
1173 This endpoint permanently deletes a prompt from the database using its name.
1174 Deletion is irreversible and requires authentication. All actions are logged
1175 for administrative auditing.
1177 Args:
1178 name (str): The name of the prompt to delete.
1179 request (Request): FastAPI request object (not used directly but required by the route signature).
1180 db (Session): Database session dependency.
1181 user (str): Authenticated user dependency.
1183 Returns:
1184 RedirectResponse: A redirect response to the prompts section of the admin
1185 dashboard with a status code of 303 (See Other).
1186 """
1187 logger.debug(f"User {user} is deleting prompt name {name}")
1188 await prompt_service.delete_prompt(db, name)
1190 form = await request.form()
1191 is_inactive_checked = form.get("is_inactive_checked", "false")
1192 root_path = request.scope.get("root_path", "")
1194 if is_inactive_checked.lower() == "true": 1194 ↛ 1195line 1194 didn't jump to line 1195 because the condition on line 1194 was never true
1195 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#prompts", status_code=303)
1196 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)
1199@admin_router.post("/prompts/{prompt_id}/toggle")
1200async def admin_toggle_prompt(
1201 prompt_id: int,
1202 request: Request,
1203 db: Session = Depends(get_db),
1204 user: str = Depends(require_auth),
1205) -> RedirectResponse:
1206 """
1207 Toggle a prompt's active status via the admin UI.
1209 This endpoint processes a form request to activate or deactivate a prompt.
1210 It expects a form field 'activate' with value "true" to activate the prompt
1211 or "false" to deactivate it. The endpoint handles exceptions gracefully and
1212 logs any errors that might occur during the status toggle operation.
1214 Args:
1215 prompt_id (int): The ID of the prompt whose status to toggle.
1216 request (Request): FastAPI request containing form data with the 'activate' field.
1217 db (Session): Database session dependency.
1218 user (str): Authenticated user dependency.
1220 Returns:
1221 RedirectResponse: A redirect to the admin dashboard prompts section with a
1222 status code of 303 (See Other).
1223 """
1224 logger.debug(f"User {user} is toggling prompt ID {prompt_id}")
1225 form = await request.form()
1226 activate = form.get("activate", "true").lower() == "true"
1227 is_inactive_checked = form.get("is_inactive_checked", "false")
1228 try:
1229 await prompt_service.toggle_prompt_status(db, prompt_id, activate)
1230 except Exception as e:
1231 logger.error(f"Error toggling prompt status: {e}")
1233 root_path = request.scope.get("root_path", "")
1234 if is_inactive_checked.lower() == "true": 1234 ↛ 1235line 1234 didn't jump to line 1235 because the condition on line 1234 was never true
1235 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#prompts", status_code=303)
1236 return RedirectResponse(f"{root_path}/admin#prompts", status_code=303)
1239@admin_router.post("/roots")
1240async def admin_add_root(request: Request, user: str = Depends(require_auth)) -> RedirectResponse:
1241 """Add a new root via the admin UI.
1243 Expects form fields:
1244 - path
1245 - name (optional)
1247 Args:
1248 request: FastAPI request containing form data.
1249 user: Authenticated user.
1251 Returns:
1252 A redirect response to the admin dashboard.
1253 """
1254 logger.debug(f"User {user} is adding a new root")
1255 form = await request.form()
1256 uri = form["uri"]
1257 name = form.get("name")
1258 await root_service.add_root(uri, name)
1260 root_path = request.scope.get("root_path", "")
1261 return RedirectResponse(f"{root_path}/admin#roots", status_code=303)
1264@admin_router.post("/roots/{uri:path}/delete")
1265async def admin_delete_root(uri: str, request: Request, user: str = Depends(require_auth)) -> RedirectResponse:
1266 """
1267 Delete a root via the admin UI.
1269 This endpoint removes a registered root URI from the system. The deletion is
1270 permanent and cannot be undone. It requires authentication and logs the
1271 operation for audit purposes.
1273 Args:
1274 uri (str): The URI of the root to delete.
1275 request (Request): FastAPI request object (not used directly but required by the route signature).
1276 user (str): Authenticated user dependency.
1278 Returns:
1279 RedirectResponse: A redirect response to the roots section of the admin
1280 dashboard with a status code of 303 (See Other).
1281 """
1282 logger.debug(f"User {user} is deleting root URI {uri}")
1283 await root_service.remove_root(uri)
1285 form = await request.form()
1286 root_path = request.scope.get("root_path", "")
1287 is_inactive_checked = form.get("is_inactive_checked", "false")
1289 if is_inactive_checked.lower() == "true": 1289 ↛ 1290line 1289 didn't jump to line 1290 because the condition on line 1289 was never true
1290 return RedirectResponse(f"{root_path}/admin/?include_inactive=true#roots", status_code=303)
1291 return RedirectResponse(f"{root_path}/admin#roots", status_code=303)
1294# Metrics
1295MetricsDict = Dict[str, Union[ToolMetrics, ResourceMetrics, ServerMetrics, PromptMetrics]]
1298@admin_router.get("/metrics", response_model=MetricsDict)
1299async def admin_get_metrics(
1300 db: Session = Depends(get_db),
1301 user: str = Depends(require_auth),
1302) -> MetricsDict:
1303 """
1304 Retrieve aggregate metrics for all entity types via the admin UI.
1306 This endpoint collects and returns usage metrics for tools, resources, servers,
1307 and prompts. The metrics are retrieved by calling the aggregate_metrics method
1308 on each respective service, which compiles statistics about usage patterns,
1309 success rates, and other relevant metrics for administrative monitoring
1310 and analysis purposes.
1312 Args:
1313 db (Session): Database session dependency.
1314 user (str): Authenticated user dependency.
1316 Returns:
1317 MetricsDict: A dictionary containing the aggregated metrics for tools,
1318 resources, servers, and prompts. Each value is a Pydantic model instance
1319 specific to the entity type.
1320 """
1321 logger.debug(f"User {user} requested aggregate metrics")
1322 tool_metrics = await tool_service.aggregate_metrics(db)
1323 resource_metrics = await resource_service.aggregate_metrics(db)
1324 server_metrics = await server_service.aggregate_metrics(db)
1325 prompt_metrics = await prompt_service.aggregate_metrics(db)
1327 # Return actual Pydantic model instances
1328 return {
1329 "tools": tool_metrics,
1330 "resources": resource_metrics,
1331 "servers": server_metrics,
1332 "prompts": prompt_metrics,
1333 }
1336@admin_router.post("/metrics/reset", response_model=Dict[str, object])
1337async def admin_reset_metrics(db: Session = Depends(get_db), user: str = Depends(require_auth)) -> Dict[str, object]:
1338 """
1339 Reset all metrics for tools, resources, servers, and prompts.
1340 Each service must implement its own reset_metrics method.
1342 Args:
1343 db (Session): Database session dependency.
1344 user (str): Authenticated user dependency.
1346 Returns:
1347 Dict[str, object]: A dictionary containing a success message and status.
1348 """
1349 logger.debug(f"User {user} requested to reset all metrics")
1350 await tool_service.reset_metrics(db)
1351 await resource_service.reset_metrics(db)
1352 await server_service.reset_metrics(db)
1353 await prompt_service.reset_metrics(db)
1354 return {"message": "All metrics reset successfully", "success": True}
1357@admin_router.post("/gateways/test", response_model=GatewayTestResponse)
1358async def admin_test_gateway(request: GatewayTestRequest, user: str = Depends(require_auth)) -> GatewayTestResponse:
1359 """
1360 Test a gateway by sending a request to its URL.
1361 This endpoint allows administrators to test the connectivity and response
1363 Args:
1364 request (GatewayTestRequest): The request object containing the gateway URL and request details.
1365 user (str): Authenticated user dependency.
1367 Returns:
1368 GatewayTestResponse: The response from the gateway, including status code, latency, and body
1369 """
1370 full_url = str(request.base_url).rstrip("/") + "/" + request.path.lstrip("/")
1371 logger.debug(f"User {user} testing server at {request.base_url}.")
1372 try:
1373 async with httpx.AsyncClient(timeout=settings.federation_timeout, verify=not settings.skip_ssl_verify) as client:
1374 start_time = time.monotonic()
1375 response = await client.request(method=request.method.upper(), url=full_url, headers=request.headers, json=request.body)
1376 latency_ms = int((time.monotonic() - start_time) * 1000)
1377 try:
1378 response_body: Union[dict, str] = response.json()
1379 except json.JSONDecodeError:
1380 response_body = response.text
1382 return GatewayTestResponse(status_code=response.status_code, latency_ms=latency_ms, body=response_body)
1384 except httpx.RequestError as e:
1385 logger.warning(f"Gateway test failed: {e}")
1386 raise HTTPException(status_code=502, detail=f"Request failed: {str(e)}")