2025-06-07 14:24:59 +08:00
from fastapi import APIRouter , HTTPException , Depends
from typing import List , Optional , Dict , Any
from pydantic import BaseModel , validator
2025-06-09 14:05:17 +08:00
import urllib . parse
2025-06-07 14:24:59 +08:00
from utils . logger import logger
from utils . auth_utils import get_current_user_id_from_jwt
2025-07-29 00:36:07 +08:00
from services . supabase import DBConnection
from . credential_service import (
2025-08-04 11:43:20 +08:00
get_credential_service
2025-07-29 00:36:07 +08:00
)
from . profile_service import (
get_profile_service ,
ProfileAccessDeniedError
2025-07-14 18:36:27 +08:00
)
2025-07-29 00:36:07 +08:00
from . utils import validate_config_not_empty , decode_mcp_qualified_name , extract_config_keys
2025-06-07 14:24:59 +08:00
router = APIRouter ( )
2025-07-29 00:36:07 +08:00
db : Optional [ DBConnection ] = None
2025-07-14 18:36:27 +08:00
2025-06-07 14:24:59 +08:00
class StoreCredentialRequest ( BaseModel ) :
mcp_qualified_name : str
display_name : str
config : Dict [ str , Any ]
@validator ( ' config ' )
2025-07-29 00:36:07 +08:00
def validate_config_not_empty_field ( cls , v ) :
return validate_config_not_empty ( v )
2025-06-07 14:24:59 +08:00
2025-07-14 18:36:27 +08:00
2025-06-18 22:50:50 +08:00
class StoreCredentialProfileRequest ( BaseModel ) :
mcp_qualified_name : str
profile_name : str
display_name : str
config : Dict [ str , Any ]
is_default : bool = False
@validator ( ' config ' )
2025-07-29 00:36:07 +08:00
def validate_config_not_empty_field ( cls , v ) :
return validate_config_not_empty ( v )
2025-06-18 22:50:50 +08:00
2025-07-14 18:36:27 +08:00
2025-08-10 05:12:16 +08:00
class BulkDeleteProfilesRequest ( BaseModel ) :
profile_ids : List [ str ]
2025-06-07 14:24:59 +08:00
class CredentialResponse ( BaseModel ) :
credential_id : str
mcp_qualified_name : str
display_name : str
2025-06-18 22:50:50 +08:00
config_keys : List [ str ]
2025-06-07 14:24:59 +08:00
is_active : bool
2025-07-29 00:36:07 +08:00
created_at : Optional [ str ] = None
updated_at : Optional [ str ] = None
2025-06-07 14:24:59 +08:00
2025-07-14 18:36:27 +08:00
2025-06-18 22:50:50 +08:00
class CredentialProfileResponse ( BaseModel ) :
profile_id : str
mcp_qualified_name : str
profile_name : str
display_name : str
config_keys : List [ str ]
is_active : bool
is_default : bool
2025-07-29 00:36:07 +08:00
created_at : Optional [ str ] = None
updated_at : Optional [ str ] = None
2025-06-18 22:50:50 +08:00
2025-07-14 18:36:27 +08:00
2025-08-10 05:12:16 +08:00
class BulkDeleteProfilesResponse ( BaseModel ) :
success : bool
deleted_count : int
failed_profiles : List [ str ] = [ ]
message : str
2025-08-04 11:43:20 +08:00
class ComposioProfileSummary ( BaseModel ) :
profile_id : str
profile_name : str
display_name : str
toolkit_slug : str
toolkit_name : str
is_connected : bool
is_default : bool
created_at : str
has_mcp_url : bool
class ComposioToolkitGroup ( BaseModel ) :
toolkit_slug : str
toolkit_name : str
icon_url : Optional [ str ] = None
profiles : List [ ComposioProfileSummary ]
class ComposioCredentialsResponse ( BaseModel ) :
success : bool
toolkits : List [ ComposioToolkitGroup ]
total_profiles : int
class ComposioMcpUrlResponse ( BaseModel ) :
success : bool
mcp_url : str
profile_name : str
toolkit_name : str
warning : str
2025-07-29 00:36:07 +08:00
def initialize ( database : DBConnection ) :
global db
db = database
2025-06-07 14:24:59 +08:00
@router.post ( " /credentials " , response_model = CredentialResponse )
2025-07-29 00:36:07 +08:00
async def store_credential (
2025-06-07 14:24:59 +08:00
request : StoreCredentialRequest ,
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
2025-07-29 00:36:07 +08:00
credential_service = get_credential_service ( db )
credential_id = await credential_service . store_credential (
account_id = user_id ,
mcp_qualified_name = request . mcp_qualified_name ,
display_name = request . display_name ,
config = request . config
2025-06-07 14:24:59 +08:00
)
2025-07-29 00:36:07 +08:00
credential = await credential_service . get_credential ( user_id , request . mcp_qualified_name )
2025-06-07 14:24:59 +08:00
if not credential :
raise HTTPException ( status_code = 500 , detail = " Failed to retrieve stored credential " )
return CredentialResponse (
credential_id = credential . credential_id ,
mcp_qualified_name = credential . mcp_qualified_name ,
display_name = credential . display_name ,
2025-07-29 00:36:07 +08:00
config_keys = extract_config_keys ( credential . config ) ,
2025-06-07 14:24:59 +08:00
is_active = credential . is_active ,
2025-07-29 00:36:07 +08:00
created_at = credential . created_at . isoformat ( ) if credential . created_at else None ,
updated_at = credential . updated_at . isoformat ( ) if credential . updated_at else None
2025-06-07 14:24:59 +08:00
)
2025-07-29 00:36:07 +08:00
except ValueError as e :
raise HTTPException ( status_code = 400 , detail = str ( e ) )
2025-06-07 14:24:59 +08:00
except Exception as e :
2025-07-29 00:36:07 +08:00
logger . error ( f " Error storing credential: { e } " )
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
2025-06-07 14:24:59 +08:00
@router.get ( " /credentials " , response_model = List [ CredentialResponse ] )
async def get_user_credentials (
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
2025-07-29 00:36:07 +08:00
credential_service = get_credential_service ( db )
credentials = await credential_service . get_user_credentials ( user_id )
2025-06-09 14:05:17 +08:00
2025-06-07 14:24:59 +08:00
return [
CredentialResponse (
credential_id = cred . credential_id ,
mcp_qualified_name = cred . mcp_qualified_name ,
display_name = cred . display_name ,
2025-07-29 00:36:07 +08:00
config_keys = extract_config_keys ( cred . config ) ,
2025-06-07 14:24:59 +08:00
is_active = cred . is_active ,
2025-07-29 00:36:07 +08:00
created_at = cred . created_at . isoformat ( ) if cred . created_at else None ,
updated_at = cred . updated_at . isoformat ( ) if cred . updated_at else None
2025-06-07 14:24:59 +08:00
)
for cred in credentials
]
2025-07-29 00:36:07 +08:00
2025-06-07 14:24:59 +08:00
except Exception as e :
2025-07-29 00:36:07 +08:00
logger . error ( f " Error getting user credentials: { e } " )
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
2025-06-07 14:24:59 +08:00
2025-06-09 14:05:17 +08:00
@router.delete ( " /credentials/ { mcp_qualified_name:path} " )
2025-07-29 00:36:07 +08:00
async def delete_credential (
2025-06-07 14:24:59 +08:00
mcp_qualified_name : str ,
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
2025-07-29 00:36:07 +08:00
decoded_name = decode_mcp_qualified_name ( mcp_qualified_name )
2025-07-14 18:36:27 +08:00
2025-07-29 00:36:07 +08:00
credential_service = get_credential_service ( db )
success = await credential_service . delete_credential ( user_id , decoded_name )
2025-06-09 14:05:17 +08:00
2025-06-07 14:24:59 +08:00
if not success :
raise HTTPException ( status_code = 404 , detail = " Credential not found " )
return { " message " : " Credential deleted successfully " }
except HTTPException :
raise
except Exception as e :
2025-07-29 00:36:07 +08:00
logger . error ( f " Error deleting credential: { e } " )
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
2025-06-07 14:24:59 +08:00
2025-06-18 22:50:50 +08:00
@router.post ( " /credential-profiles " , response_model = CredentialProfileResponse )
async def store_credential_profile (
request : StoreCredentialProfileRequest ,
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service ( db )
profile_id = await profile_service . store_profile (
account_id = user_id ,
mcp_qualified_name = request . mcp_qualified_name ,
profile_name = request . profile_name ,
display_name = request . display_name ,
config = request . config ,
is_default = request . is_default
2025-06-18 22:50:50 +08:00
)
2025-07-29 00:36:07 +08:00
profile = await profile_service . get_profile ( user_id , profile_id )
2025-06-18 22:50:50 +08:00
if not profile :
2025-07-14 18:36:27 +08:00
raise HTTPException ( status_code = 500 , detail = " Failed to retrieve stored profile " )
2025-06-18 22:50:50 +08:00
return CredentialProfileResponse (
profile_id = profile . profile_id ,
mcp_qualified_name = profile . mcp_qualified_name ,
profile_name = profile . profile_name ,
display_name = profile . display_name ,
2025-07-29 00:36:07 +08:00
config_keys = extract_config_keys ( profile . config ) ,
2025-06-18 22:50:50 +08:00
is_active = profile . is_active ,
is_default = profile . is_default ,
2025-07-29 00:36:07 +08:00
created_at = profile . created_at . isoformat ( ) if profile . created_at else None ,
updated_at = profile . updated_at . isoformat ( ) if profile . updated_at else None
2025-06-18 22:50:50 +08:00
)
2025-07-29 00:36:07 +08:00
except ValueError as e :
raise HTTPException ( status_code = 400 , detail = str ( e ) )
2025-06-18 22:50:50 +08:00
except Exception as e :
2025-07-29 00:36:07 +08:00
logger . error ( f " Error storing credential profile: { e } " )
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
2025-06-18 22:50:50 +08:00
@router.get ( " /credential-profiles " , response_model = List [ CredentialProfileResponse ] )
2025-07-29 00:36:07 +08:00
async def get_user_credential_profiles (
2025-06-18 22:50:50 +08:00
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service ( db )
profiles = await profile_service . get_all_user_profiles ( user_id )
2025-06-18 22:50:50 +08:00
return [
CredentialProfileResponse (
profile_id = profile . profile_id ,
mcp_qualified_name = profile . mcp_qualified_name ,
profile_name = profile . profile_name ,
display_name = profile . display_name ,
2025-07-29 00:36:07 +08:00
config_keys = extract_config_keys ( profile . config ) ,
2025-06-18 22:50:50 +08:00
is_active = profile . is_active ,
is_default = profile . is_default ,
2025-07-29 00:36:07 +08:00
created_at = profile . created_at . isoformat ( ) if profile . created_at else None ,
updated_at = profile . updated_at . isoformat ( ) if profile . updated_at else None
2025-06-18 22:50:50 +08:00
)
for profile in profiles
]
2025-07-29 00:36:07 +08:00
2025-06-18 22:50:50 +08:00
except Exception as e :
2025-07-29 00:36:07 +08:00
logger . error ( f " Error getting user credential profiles: { e } " )
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
2025-06-18 22:50:50 +08:00
@router.get ( " /credential-profiles/ { mcp_qualified_name:path} " , response_model = List [ CredentialProfileResponse ] )
async def get_credential_profiles_for_mcp (
mcp_qualified_name : str ,
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
2025-07-29 00:36:07 +08:00
decoded_name = decode_mcp_qualified_name ( mcp_qualified_name )
2025-07-14 18:36:27 +08:00
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service ( db )
profiles = await profile_service . get_profiles ( user_id , decoded_name )
2025-06-18 22:50:50 +08:00
return [
CredentialProfileResponse (
profile_id = profile . profile_id ,
mcp_qualified_name = profile . mcp_qualified_name ,
profile_name = profile . profile_name ,
display_name = profile . display_name ,
2025-07-29 00:36:07 +08:00
config_keys = extract_config_keys ( profile . config ) ,
2025-06-18 22:50:50 +08:00
is_active = profile . is_active ,
is_default = profile . is_default ,
2025-07-29 00:36:07 +08:00
created_at = profile . created_at . isoformat ( ) if profile . created_at else None ,
updated_at = profile . updated_at . isoformat ( ) if profile . updated_at else None
2025-06-18 22:50:50 +08:00
)
for profile in profiles
]
2025-07-29 00:36:07 +08:00
2025-06-18 22:50:50 +08:00
except Exception as e :
2025-07-29 00:36:07 +08:00
logger . error ( f " Error getting credential profiles for MCP: { e } " )
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
2025-06-18 22:50:50 +08:00
@router.get ( " /credential-profiles/profile/ {profile_id} " , response_model = CredentialProfileResponse )
2025-07-29 00:36:07 +08:00
async def get_credential_profile (
2025-06-18 22:50:50 +08:00
profile_id : str ,
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service ( db )
profile = await profile_service . get_profile ( user_id , profile_id )
2025-06-18 22:50:50 +08:00
if not profile :
2025-07-29 00:36:07 +08:00
raise HTTPException ( status_code = 404 , detail = " Profile not found " )
2025-06-18 22:50:50 +08:00
return CredentialProfileResponse (
profile_id = profile . profile_id ,
mcp_qualified_name = profile . mcp_qualified_name ,
profile_name = profile . profile_name ,
display_name = profile . display_name ,
2025-07-29 00:36:07 +08:00
config_keys = extract_config_keys ( profile . config ) ,
2025-06-18 22:50:50 +08:00
is_active = profile . is_active ,
is_default = profile . is_default ,
2025-07-29 00:36:07 +08:00
created_at = profile . created_at . isoformat ( ) if profile . created_at else None ,
updated_at = profile . updated_at . isoformat ( ) if profile . updated_at else None
2025-06-18 22:50:50 +08:00
)
2025-07-29 00:36:07 +08:00
except ProfileAccessDeniedError :
raise HTTPException ( status_code = 403 , detail = " Access denied to profile " )
2025-06-18 22:50:50 +08:00
except Exception as e :
2025-07-29 00:36:07 +08:00
logger . error ( f " Error getting credential profile: { e } " )
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
2025-06-18 22:50:50 +08:00
@router.put ( " /credential-profiles/ {profile_id} /set-default " )
async def set_default_credential_profile (
profile_id : str ,
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service ( db )
success = await profile_service . set_default_profile ( user_id , profile_id )
2025-06-18 22:50:50 +08:00
if not success :
2025-07-29 00:36:07 +08:00
raise HTTPException ( status_code = 404 , detail = " Profile not found " )
return { " message " : " Profile set as default successfully " }
2025-06-18 22:50:50 +08:00
except Exception as e :
2025-07-29 00:36:07 +08:00
logger . error ( f " Error setting default profile: { e } " )
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
2025-06-18 22:50:50 +08:00
@router.delete ( " /credential-profiles/ {profile_id} " )
async def delete_credential_profile (
profile_id : str ,
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
2025-07-29 00:36:07 +08:00
profile_service = get_profile_service ( db )
success = await profile_service . delete_profile ( user_id , profile_id )
2025-06-18 22:50:50 +08:00
if not success :
2025-07-29 00:36:07 +08:00
raise HTTPException ( status_code = 404 , detail = " Profile not found " )
return { " message " : " Profile deleted successfully " }
2025-06-18 22:50:50 +08:00
except Exception as e :
2025-07-29 00:36:07 +08:00
logger . error ( f " Error deleting profile: { e } " )
2025-08-04 11:43:20 +08:00
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
2025-08-10 05:12:16 +08:00
@router.post ( " /credential-profiles/bulk-delete " , response_model = BulkDeleteProfilesResponse )
async def bulk_delete_credential_profiles (
request : BulkDeleteProfilesRequest ,
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
profile_service = get_profile_service ( db )
deleted_count = 0
failed_profiles = [ ]
for profile_id in request . profile_ids :
try :
success = await profile_service . delete_profile ( user_id , profile_id )
if success :
deleted_count + = 1
else :
failed_profiles . append ( profile_id )
except Exception as e :
logger . error ( f " Error deleting profile { profile_id } : { e } " )
failed_profiles . append ( profile_id )
return BulkDeleteProfilesResponse (
success = True ,
deleted_count = deleted_count ,
failed_profiles = failed_profiles ,
message = " Bulk deletion completed "
)
except Exception as e :
logger . error ( f " Error performing bulk deletion: { e } " )
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
2025-08-04 11:43:20 +08:00
@router.get ( " /composio-profiles " , response_model = ComposioCredentialsResponse )
async def get_composio_profiles (
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
profile_service = get_profile_service ( db )
from composio_integration . composio_profile_service import ComposioProfileService
composio_service = ComposioProfileService ( db )
all_profiles = await profile_service . get_all_user_profiles ( user_id )
composio_profiles = [
profile for profile in all_profiles
if profile . mcp_qualified_name . startswith ( ' composio. ' )
]
from composio_integration . toolkit_service import ToolkitService
toolkit_service = ToolkitService ( )
toolkit_groups = { }
for profile in composio_profiles :
mcp_parts = profile . mcp_qualified_name . split ( ' . ' )
if len ( mcp_parts ) > = 2 :
toolkit_slug = mcp_parts [ 1 ]
toolkit_name = toolkit_slug . replace ( ' _ ' , ' ' ) . title ( )
else :
config = profile . config
toolkit_slug = config . get ( ' toolkit_slug ' , ' unknown ' )
toolkit_name = config . get ( ' toolkit_name ' , toolkit_slug . title ( ) )
if toolkit_slug not in toolkit_groups :
try :
icon_url = await toolkit_service . get_toolkit_icon ( toolkit_slug )
except :
icon_url = None
toolkit_groups [ toolkit_slug ] = {
' toolkit_slug ' : toolkit_slug ,
' toolkit_name ' : toolkit_name ,
' icon_url ' : icon_url ,
' profiles ' : [ ]
}
has_mcp_url = False
try :
mcp_url = await composio_service . get_mcp_url_for_runtime ( profile . profile_id )
has_mcp_url = bool ( mcp_url )
except :
has_mcp_url = False
profile_summary = ComposioProfileSummary (
profile_id = profile . profile_id ,
profile_name = profile . profile_name ,
display_name = profile . display_name ,
toolkit_slug = toolkit_slug ,
toolkit_name = toolkit_name ,
is_connected = has_mcp_url ,
is_default = profile . is_default ,
created_at = profile . created_at . isoformat ( ) if profile . created_at else " " ,
has_mcp_url = has_mcp_url
)
toolkit_groups [ toolkit_slug ] [ ' profiles ' ] . append ( profile_summary )
toolkits = [ ]
for group_data in toolkit_groups . values ( ) :
group_data [ ' profiles ' ] . sort ( key = lambda p : p . created_at , reverse = True )
toolkits . append ( ComposioToolkitGroup ( * * group_data ) )
toolkits . sort ( key = lambda t : t . toolkit_name )
return ComposioCredentialsResponse (
success = True ,
toolkits = toolkits ,
total_profiles = len ( composio_profiles )
)
except Exception as e :
logger . error ( f " Error getting Composio profiles: { e } " )
raise HTTPException ( status_code = 500 , detail = " Internal server error " )
@router.get ( " /composio-profiles/ {profile_id} /mcp-url " , response_model = ComposioMcpUrlResponse )
async def get_composio_mcp_url (
profile_id : str ,
user_id : str = Depends ( get_current_user_id_from_jwt )
) :
try :
from composio_integration . composio_profile_service import ComposioProfileService
composio_service = ComposioProfileService ( db )
profile_service = get_profile_service ( db )
profile = await profile_service . get_profile ( user_id , profile_id )
if not profile :
raise HTTPException ( status_code = 404 , detail = " Profile not found " )
if not profile . mcp_qualified_name . startswith ( ' composio. ' ) :
raise HTTPException ( status_code = 400 , detail = " Not a Composio profile " )
try :
mcp_url = await composio_service . get_mcp_url_for_runtime ( profile_id )
config = await composio_service . get_profile_config ( profile_id )
toolkit_name = config . get ( ' toolkit_name ' , ' Unknown ' )
except Exception as e :
logger . error ( f " Failed to decrypt Composio profile { profile_id } : { e } " )
raise HTTPException ( status_code = 404 , detail = " MCP URL not found or could not be decrypted " )
return ComposioMcpUrlResponse (
success = True ,
mcp_url = mcp_url ,
profile_name = profile . profile_name ,
toolkit_name = toolkit_name ,
warning = " This MCP URL contains sensitive authentication information. Never share it publicly or include it in code repositories. Anyone with access to this URL can perform actions on your behalf. "
)
except HTTPException :
raise
except Exception as e :
logger . error ( f " Error getting Composio MCP URL: { e } " )
2025-07-29 00:36:07 +08:00
raise HTTPException ( status_code = 500 , detail = " Internal server error " )