from datetime import datetime from typing import Annotated, TypeVar from core_domain import ServiceHealth from core_shared.secrets import SecretCipher from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlalchemy import text from sqlalchemy.orm import Session from app.application.services import ToolApplicationService from app.bootstrap.settings import ToolServiceSettings from app.db.session import get_db from app.domain.repositories import ( ToolBindingRepository, ToolCredentialRepository, ToolDefinitionRepository, ToolVersionRepository, ) from app.schemas.tool import ( ApiResponse, DeleteData, McpConnectData, McpConnectRequestDto, PageRequest, PageResult, ToolBindingCreateRequest, ToolBindingCreateRequestDto, ToolBindingDeleteRequestDto, ToolBindingDetailRequestDto, ToolBindingDetailResponse, ToolBindingDto, ToolBindingListRequestDto, ToolBindingResponse, ToolBindingUpdateRequestDto, ToolCreateRequest, ToolCreateRequestDto, ToolCredentialCreateRequest, ToolCredentialCreateRequestDto, ToolCredentialDeleteRequestDto, ToolCredentialDetailRequestDto, ToolCredentialDto, ToolCredentialResponse, ToolCredentialRevealDto, ToolCredentialRevealRequestDto, ToolCredentialRevealResponse, ToolCredentialUpdateRequestDto, ToolDeleteRequestDto, ToolDetailRequestDto, ToolDto, ToolResponse, ToolUpdateRequestDto, ToolVersionCreateRequest, ToolVersionCreateRequestDto, ToolVersionDeleteRequestDto, ToolVersionDetailRequestDto, ToolVersionDto, ToolVersionListRequestDto, ToolVersionResponse, ToolVersionUpdateRequestDto, ) router = APIRouter() DbSession = Annotated[Session, Depends(get_db)] T = TypeVar("T") def get_tool_application_service( request: Request, db: DbSession) -> ToolApplicationService: settings: ToolServiceSettings = request.app.state.settings return ToolApplicationService( tool_definition_repository=ToolDefinitionRepository(db), tool_version_repository=ToolVersionRepository(db), tool_binding_repository=ToolBindingRepository(db), tool_credential_repository=ToolCredentialRepository(db), secret_cipher=SecretCipher(key=settings.credential_encryption_key)) ToolServiceDep = Annotated[ToolApplicationService, Depends(get_tool_application_service)] def ok(data: T) -> ApiResponse[T]: return ApiResponse[T]( data=data, requestId="", serverTime=datetime.utcnow()) @router.get("/health", response_model=ServiceHealth) def health_check(db: DbSession) -> ServiceHealth: db.execute(text("SELECT 1")) return ServiceHealth(service="tool-service", status="ok", database="ok") @router.post("", response_model=ToolResponse) def create_tool_definition( payload: ToolCreateRequest, service: ToolServiceDep) -> ToolResponse: entity = service.create_tool_definition(payload) return ToolResponse.from_entity(entity) @router.get("", response_model=list[ToolResponse]) def list_tool_definitions( service: ToolServiceDep) -> list[ToolResponse]: return [ToolResponse.from_entity(item) for item in service.list_tool_definitions()] @router.post("/versions", response_model=ToolVersionResponse) def create_tool_version( payload: ToolVersionCreateRequest, service: ToolServiceDep) -> ToolVersionResponse: entity = service.create_tool_version(payload) return ToolVersionResponse.from_entity(entity) @router.get("/versions", response_model=list[ToolVersionResponse]) def list_tool_versions( service: ToolServiceDep, tool_id: str | None = Query(default=None)) -> list[ToolVersionResponse]: return [ ToolVersionResponse.from_entity(item) for item in service.list_tool_versions(tool_id=tool_id) ] @router.post("/bindings", response_model=ToolBindingResponse) def create_tool_binding( payload: ToolBindingCreateRequest, service: ToolServiceDep) -> ToolBindingResponse: try: entity = service.create_tool_binding(payload) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc return ToolBindingResponse.from_entity(entity) @router.get("/bindings", response_model=list[ToolBindingResponse]) def list_tool_bindings( service: ToolServiceDep, app_id: str | None = Query(default=None)) -> list[ToolBindingResponse]: return [ ToolBindingResponse.from_entity(item) for item in service.list_tool_bindings(app_id=app_id) ] @router.get("/bindings/{binding_id}", response_model=ToolBindingDetailResponse) def get_tool_binding_detail( binding_id: str, service: ToolServiceDep) -> ToolBindingDetailResponse: result = service.get_tool_binding_detail(binding_id=binding_id) if result is None: raise HTTPException(status_code=404, detail=f"tool_binding not found: {binding_id}") binding, tool_version, tool_definition = result return ToolBindingDetailResponse( binding=ToolBindingResponse.from_entity(binding), tool_version=ToolVersionResponse.from_entity(tool_version), tool_definition=ToolResponse.from_entity(tool_definition)) @router.post("/credentials", response_model=ToolCredentialResponse) def create_tool_credential( payload: ToolCredentialCreateRequest, service: ToolServiceDep) -> ToolCredentialResponse: entity = service.create_tool_credential(payload) return ToolCredentialResponse.from_entity(entity) @router.get("/credentials", response_model=list[ToolCredentialResponse]) def list_tool_credentials( service: ToolServiceDep) -> list[ToolCredentialResponse]: return [ ToolCredentialResponse.from_entity(item) for item in service.list_tool_credentials() ] @router.post("/credentials/{credential_id}/reveal", response_model=ToolCredentialRevealResponse) def reveal_tool_credential( credential_id: str, service: ToolServiceDep) -> ToolCredentialRevealResponse: result = service.reveal_tool_credential( credential_id=credential_id) if result is None: raise HTTPException(status_code=404, detail=f"tool credential not found: {credential_id}") credential, secret_json = result return ToolCredentialRevealResponse( credential=ToolCredentialResponse.from_entity(credential), secret_json=secret_json) @router.post("/list", response_model=ApiResponse[PageResult[ToolDto]]) def list_tools_contract( payload: PageRequest, service: ToolServiceDep) -> ApiResponse[PageResult[ToolDto]]: keyword = (payload.keyword or "").lower().strip() items = [ item for item in service.list_tool_definitions() if not keyword or keyword in item.name.lower() or keyword in item.tool_type.lower() or keyword in (item.description or "").lower() ] page_items = items[payload.offset:payload.offset + payload.pageSize] return ok( PageResult[ToolDto].from_items( items=[ToolDto.from_entity(item) for item in page_items], total=len(items), page=payload.page, page_size=payload.pageSize)) @router.post("/create", response_model=ApiResponse[ToolDto]) def create_tool_contract( payload: ToolCreateRequestDto, service: ToolServiceDep) -> ApiResponse[ToolDto]: entity = service.create_tool_definition_from_contract(payload) return ok(ToolDto.from_entity(entity)) @router.post("/detail", response_model=ApiResponse[ToolDto]) def get_tool_contract( payload: ToolDetailRequestDto, service: ToolServiceDep) -> ApiResponse[ToolDto]: entity = service.get_tool_definition_from_contract(payload) if entity is None: raise HTTPException(status_code=404, detail=f"tool not found: {payload.toolId}") return ok(ToolDto.from_entity(entity)) @router.post("/update", response_model=ApiResponse[ToolDto]) def update_tool_contract( payload: ToolUpdateRequestDto, service: ToolServiceDep) -> ApiResponse[ToolDto]: entity = service.update_tool_definition_from_contract(payload) if entity is None: raise HTTPException(status_code=404, detail=f"tool not found: {payload.toolId}") return ok(ToolDto.from_entity(entity)) @router.post("/delete", response_model=ApiResponse[DeleteData]) def delete_tool_contract( payload: ToolDeleteRequestDto, service: ToolServiceDep) -> ApiResponse[DeleteData]: deleted = service.delete_tool_definition_from_contract(payload) return ok(DeleteData(deleted=deleted, toolId=payload.toolId)) @router.post("/connections/list", response_model=ApiResponse[PageResult[ToolVersionDto]]) def list_tool_connections_contract( payload: ToolVersionListRequestDto, service: ToolServiceDep) -> ApiResponse[PageResult[ToolVersionDto]]: items = service.list_tool_versions(tool_id=payload.toolId) page_items = items[payload.offset:payload.offset + payload.pageSize] return ok( PageResult[ToolVersionDto].from_items( items=[ToolVersionDto.from_entity(item) for item in page_items], total=len(items), page=payload.page, page_size=payload.pageSize)) @router.post("/connections/create", response_model=ApiResponse[ToolVersionDto]) def create_tool_connection_contract( payload: ToolVersionCreateRequestDto, service: ToolServiceDep) -> ApiResponse[ToolVersionDto]: entity = service.create_tool_version_from_contract(payload) return ok(ToolVersionDto.from_entity(entity)) @router.post("/connections/detail", response_model=ApiResponse[ToolVersionDto]) def get_tool_connection_contract( payload: ToolVersionDetailRequestDto, service: ToolServiceDep) -> ApiResponse[ToolVersionDto]: entity = service.get_tool_version_from_contract(payload) if entity is None: raise HTTPException( status_code=404, detail=f"tool connection not found: {payload.connectionId}") return ok(ToolVersionDto.from_entity(entity)) @router.post("/connections/update", response_model=ApiResponse[ToolVersionDto]) def update_tool_connection_contract( payload: ToolVersionUpdateRequestDto, service: ToolServiceDep) -> ApiResponse[ToolVersionDto]: entity = service.update_tool_version_from_contract(payload) if entity is None: raise HTTPException( status_code=404, detail=f"tool connection not found: {payload.connectionId}") return ok(ToolVersionDto.from_entity(entity)) @router.post("/connections/delete", response_model=ApiResponse[DeleteData]) def delete_tool_connection_contract( payload: ToolVersionDeleteRequestDto, service: ToolServiceDep) -> ApiResponse[DeleteData]: connection_id = payload.connectionId deleted = service.delete_tool_version(connection_id=connection_id) return ok(DeleteData(deleted=deleted, connectionId=connection_id)) @router.post("/mcp/connect", response_model=ApiResponse[McpConnectData]) def connect_mcp_server_contract( payload: McpConnectRequestDto, service: ToolServiceDep) -> ApiResponse[McpConnectData]: return ok(service.connect_mcp_server(payload)) @router.post("/bindings/list", response_model=ApiResponse[PageResult[ToolBindingDto]]) def list_tool_bindings_contract( payload: ToolBindingListRequestDto, service: ToolServiceDep) -> ApiResponse[PageResult[ToolBindingDto]]: items = service.list_tool_bindings(app_id=payload.appId) page_items = items[payload.offset:payload.offset + payload.pageSize] return ok( PageResult[ToolBindingDto].from_items( items=[ToolBindingDto.from_entity(item) for item in page_items], total=len(items), page=payload.page, page_size=payload.pageSize)) @router.post("/bindings/create", response_model=ApiResponse[ToolBindingDto]) def create_tool_binding_contract( payload: ToolBindingCreateRequestDto, service: ToolServiceDep) -> ApiResponse[ToolBindingDto]: try: entity = service.create_tool_binding_from_contract(payload) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc return ok(ToolBindingDto.from_entity(entity)) @router.post("/bindings/detail", response_model=ApiResponse[ToolBindingDto]) def get_tool_binding_contract( payload: ToolBindingDetailRequestDto, service: ToolServiceDep) -> ApiResponse[ToolBindingDto]: entity = service.get_tool_binding_from_contract(payload) if entity is None: raise HTTPException( status_code=404, detail=f"tool binding not found: {payload.bindingId}") return ok(ToolBindingDto.from_entity(entity)) @router.post("/bindings/update", response_model=ApiResponse[ToolBindingDto]) def update_tool_binding_contract( payload: ToolBindingUpdateRequestDto, service: ToolServiceDep) -> ApiResponse[ToolBindingDto]: entity = service.update_tool_binding_from_contract(payload) if entity is None: raise HTTPException( status_code=404, detail=f"tool binding not found: {payload.bindingId}") return ok(ToolBindingDto.from_entity(entity)) @router.post("/bindings/delete", response_model=ApiResponse[DeleteData]) def delete_tool_binding_contract( payload: ToolBindingDeleteRequestDto, service: ToolServiceDep) -> ApiResponse[DeleteData]: deleted = service.delete_tool_binding(payload) return ok(DeleteData(deleted=deleted, bindingId=payload.bindingId)) @router.post("/credentials/list", response_model=ApiResponse[PageResult[ToolCredentialDto]]) def list_tool_credentials_contract( payload: PageRequest, service: ToolServiceDep) -> ApiResponse[PageResult[ToolCredentialDto]]: keyword = (payload.keyword or "").lower().strip() items = [ item for item in service.list_tool_credentials() if not keyword or keyword in item.name.lower() or keyword in item.credential_type.lower() ] page_items = items[payload.offset:payload.offset + payload.pageSize] return ok( PageResult[ToolCredentialDto].from_items( items=[ToolCredentialDto.from_entity(item) for item in page_items], total=len(items), page=payload.page, page_size=payload.pageSize)) @router.post("/credentials/create", response_model=ApiResponse[ToolCredentialDto]) def create_tool_credential_contract( payload: ToolCredentialCreateRequestDto, service: ToolServiceDep) -> ApiResponse[ToolCredentialDto]: entity = service.create_tool_credential_from_contract(payload) return ok(ToolCredentialDto.from_entity(entity)) @router.post("/credentials/detail", response_model=ApiResponse[ToolCredentialDto]) def get_tool_credential_contract( payload: ToolCredentialDetailRequestDto, service: ToolServiceDep) -> ApiResponse[ToolCredentialDto]: entity = service.get_tool_credential_from_contract(payload) if entity is None: raise HTTPException( status_code=404, detail=f"tool credential not found: {payload.credentialId}") return ok(ToolCredentialDto.from_entity(entity)) @router.post("/credentials/update", response_model=ApiResponse[ToolCredentialDto]) def update_tool_credential_contract( payload: ToolCredentialUpdateRequestDto, service: ToolServiceDep) -> ApiResponse[ToolCredentialDto]: entity = service.update_tool_credential_from_contract(payload) if entity is None: raise HTTPException( status_code=404, detail=f"tool credential not found: {payload.credentialId}") return ok(ToolCredentialDto.from_entity(entity)) @router.post("/credentials/delete", response_model=ApiResponse[DeleteData]) def delete_tool_credential_contract( payload: ToolCredentialDeleteRequestDto, service: ToolServiceDep) -> ApiResponse[DeleteData]: deleted = service.delete_tool_credential(payload) return ok(DeleteData(deleted=deleted, credentialId=payload.credentialId)) @router.post("/credentials/reveal", response_model=ApiResponse[ToolCredentialRevealDto]) def reveal_tool_credential_contract( payload: ToolCredentialRevealRequestDto, service: ToolServiceDep) -> ApiResponse[ToolCredentialRevealDto]: result = service.reveal_tool_credential_from_contract( credential_id=payload.credentialId) if result is None: raise HTTPException( status_code=404, detail=f"tool credential not found: {payload.credentialId}") return ok(result)