mirror of https://github.com/kortix-ai/suna.git
update agent builder to use composio
This commit is contained in:
parent
d456817809
commit
aa37988106
|
@ -3190,20 +3190,16 @@ async def update_agent_custom_mcps(
|
|||
request: dict,
|
||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||
):
|
||||
"""Update agent's custom MCPs with the new format (for Composio integration)"""
|
||||
logger.info(f"Updating agent {agent_id} custom MCPs for user {user_id}")
|
||||
|
||||
try:
|
||||
client = await db.client
|
||||
|
||||
# Get agent and current version
|
||||
agent_result = await client.table('agents').select('current_version_id').eq('agent_id', agent_id).eq('account_id', user_id).execute()
|
||||
if not agent_result.data:
|
||||
raise HTTPException(status_code=404, detail="Agent not found")
|
||||
|
||||
agent = agent_result.data[0]
|
||||
|
||||
# Get current version config
|
||||
agent_config = {}
|
||||
if agent.get('current_version_id'):
|
||||
version_result = await client.table('agent_versions')\
|
||||
|
@ -3214,22 +3210,18 @@ async def update_agent_custom_mcps(
|
|||
if version_result.data and version_result.data.get('config'):
|
||||
agent_config = version_result.data['config']
|
||||
|
||||
# Get the new custom_mcps from request
|
||||
new_custom_mcps = request.get('custom_mcps', [])
|
||||
if not new_custom_mcps:
|
||||
raise HTTPException(status_code=400, detail="custom_mcps array is required")
|
||||
|
||||
# Get existing tools config
|
||||
tools = agent_config.get('tools', {})
|
||||
existing_custom_mcps = tools.get('custom_mcp', [])
|
||||
|
||||
# Update or add the new MCP
|
||||
updated = False
|
||||
for new_mcp in new_custom_mcps:
|
||||
mcp_type = new_mcp.get('type', '')
|
||||
|
||||
if mcp_type == 'composio':
|
||||
# For Composio, match by profile_id
|
||||
profile_id = new_mcp.get('config', {}).get('profile_id')
|
||||
if not profile_id:
|
||||
continue
|
||||
|
@ -3245,7 +3237,6 @@ async def update_agent_custom_mcps(
|
|||
existing_custom_mcps.append(new_mcp)
|
||||
updated = True
|
||||
else:
|
||||
# For other types, match by URL or name
|
||||
mcp_url = new_mcp.get('config', {}).get('url')
|
||||
mcp_name = new_mcp.get('name', '')
|
||||
|
||||
|
@ -3260,18 +3251,14 @@ async def update_agent_custom_mcps(
|
|||
existing_custom_mcps.append(new_mcp)
|
||||
updated = True
|
||||
|
||||
# Update the config
|
||||
tools['custom_mcp'] = existing_custom_mcps
|
||||
agent_config['tools'] = tools
|
||||
|
||||
# Create new version
|
||||
from agent.versioning.version_service import get_version_service
|
||||
import datetime
|
||||
|
||||
try:
|
||||
version_service = await get_version_service()
|
||||
|
||||
# Generate unique version name with timestamp
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
change_description = f"MCP tools update {timestamp}"
|
||||
|
||||
|
@ -3286,7 +3273,6 @@ async def update_agent_custom_mcps(
|
|||
)
|
||||
logger.info(f"Created version {new_version.version_id} for agent {agent_id}")
|
||||
|
||||
# Count total enabled tools across all MCPs
|
||||
total_enabled_tools = sum(len(mcp.get('enabledTools', [])) for mcp in new_custom_mcps)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create version for custom MCP tools update: {e}")
|
||||
|
@ -3305,6 +3291,3 @@ async def update_agent_custom_mcps(
|
|||
except Exception as e:
|
||||
logger.error(f"Error updating agent custom MCPs: {e}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
# Get MCP tools for an agent
|
||||
|
|
|
@ -3,7 +3,10 @@ from typing import Optional, List
|
|||
from agentpress.tool import ToolResult, openapi_schema, usage_example
|
||||
from agentpress.thread_manager import ThreadManager
|
||||
from .base_tool import AgentBuilderBaseTool
|
||||
from pipedream import profile_service, connection_service, app_service, mcp_service, connection_token_service
|
||||
from composio_integration.composio_service import get_integration_service
|
||||
from composio_integration.composio_profile_service import ComposioProfileService
|
||||
from composio_integration.toolkit_service import ToolkitService
|
||||
from mcp_module.mcp_service import mcp_service
|
||||
from .mcp_search_tool import MCPSearchTool
|
||||
from utils.logger import logger
|
||||
|
||||
|
@ -11,19 +14,19 @@ from utils.logger import logger
|
|||
class CredentialProfileTool(AgentBuilderBaseTool):
|
||||
def __init__(self, thread_manager: ThreadManager, db_connection, agent_id: str):
|
||||
super().__init__(thread_manager, db_connection, agent_id)
|
||||
self.pipedream_search = MCPSearchTool(thread_manager, db_connection, agent_id)
|
||||
self.composio_search = MCPSearchTool(thread_manager, db_connection, agent_id)
|
||||
|
||||
@openapi_schema({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_credential_profiles",
|
||||
"description": "Get all existing Pipedream credential profiles for the current user. Use this to show the user their available profiles.",
|
||||
"description": "Get all existing Composio credential profiles for the current user. Use this to show the user their available profiles.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_slug": {
|
||||
"toolkit_slug": {
|
||||
"type": "string",
|
||||
"description": "Optional filter to show only profiles for a specific app"
|
||||
"description": "Optional filter to show only profiles for a specific toolkit"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
@ -33,31 +36,29 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
@usage_example('''
|
||||
<function_calls>
|
||||
<invoke name="get_credential_profiles">
|
||||
<parameter name="app_slug">github</parameter>
|
||||
<parameter name="toolkit_slug">github</parameter>
|
||||
</invoke>
|
||||
</function_calls>
|
||||
''')
|
||||
async def get_credential_profiles(self, app_slug: Optional[str] = None) -> ToolResult:
|
||||
async def get_credential_profiles(self, toolkit_slug: Optional[str] = None) -> ToolResult:
|
||||
try:
|
||||
from uuid import UUID
|
||||
account_id = await self._get_current_account_id()
|
||||
profiles = await profile_service.get_profiles(UUID(account_id), app_slug)
|
||||
profile_service = ComposioProfileService(self.db)
|
||||
profiles = await profile_service.get_profiles(account_id, toolkit_slug)
|
||||
|
||||
formatted_profiles = []
|
||||
for profile in profiles:
|
||||
formatted_profiles.append({
|
||||
"profile_id": str(profile.profile_id),
|
||||
"profile_name": profile.profile_name.value if hasattr(profile.profile_name, 'value') else str(profile.profile_name),
|
||||
"profile_id": profile.profile_id,
|
||||
"profile_name": profile.profile_name,
|
||||
"display_name": profile.display_name,
|
||||
"app_slug": profile.app_slug.value if hasattr(profile.app_slug, 'value') else str(profile.app_slug),
|
||||
"app_name": profile.app_name,
|
||||
"external_user_id": profile.external_user_id.value if hasattr(profile.external_user_id, 'value') else str(profile.external_user_id),
|
||||
"toolkit_slug": profile.toolkit_slug,
|
||||
"toolkit_name": profile.toolkit_name,
|
||||
"mcp_url": profile.mcp_url,
|
||||
"is_connected": profile.is_connected,
|
||||
"is_active": profile.is_active,
|
||||
"is_default": profile.is_default,
|
||||
"enabled_tools": profile.enabled_tools,
|
||||
"created_at": profile.created_at.isoformat() if profile.created_at else None,
|
||||
"last_used_at": profile.last_used_at.isoformat() if profile.last_used_at else None
|
||||
"updated_at": profile.updated_at.isoformat() if profile.updated_at else None
|
||||
})
|
||||
|
||||
return self.success_response({
|
||||
|
@ -73,13 +74,13 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_credential_profile",
|
||||
"description": "Create a new Pipedream credential profile for a specific app. This will generate a unique external user ID for the profile.",
|
||||
"description": "Create a new Composio credential profile for a specific toolkit. This will create the integration and return an authentication link.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_slug": {
|
||||
"toolkit_slug": {
|
||||
"type": "string",
|
||||
"description": "The app slug to create the profile for (e.g., 'github', 'linear', 'slack')"
|
||||
"description": "The toolkit slug to create the profile for (e.g., 'github', 'linear', 'slack')"
|
||||
},
|
||||
"profile_name": {
|
||||
"type": "string",
|
||||
|
@ -90,14 +91,14 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
"description": "Display name for the profile (defaults to profile_name if not provided)"
|
||||
}
|
||||
},
|
||||
"required": ["app_slug", "profile_name"]
|
||||
"required": ["toolkit_slug", "profile_name"]
|
||||
}
|
||||
}
|
||||
})
|
||||
@usage_example('''
|
||||
<function_calls>
|
||||
<invoke name="create_credential_profile">
|
||||
<parameter name="app_slug">github</parameter>
|
||||
<parameter name="toolkit_slug">github</parameter>
|
||||
<parameter name="profile_name">Personal GitHub</parameter>
|
||||
<parameter name="display_name">My Personal GitHub Account</parameter>
|
||||
</invoke>
|
||||
|
@ -105,38 +106,37 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
''')
|
||||
async def create_credential_profile(
|
||||
self,
|
||||
app_slug: str,
|
||||
toolkit_slug: str,
|
||||
profile_name: str,
|
||||
display_name: Optional[str] = None
|
||||
) -> ToolResult:
|
||||
try:
|
||||
from uuid import UUID
|
||||
account_id = await self._get_current_account_id()
|
||||
# fetch app domain object directly
|
||||
app_obj = await app_service.get_app_by_slug(app_slug)
|
||||
if not app_obj:
|
||||
return self.fail_response(f"Could not find app for slug '{app_slug}'")
|
||||
# create credential profile using the app name
|
||||
profile = await profile_service.create_profile(
|
||||
account_id=UUID(account_id),
|
||||
|
||||
integration_service = get_integration_service(db_connection=self.db)
|
||||
result = await integration_service.integrate_toolkit(
|
||||
toolkit_slug=toolkit_slug,
|
||||
account_id=account_id,
|
||||
profile_name=profile_name,
|
||||
app_slug=app_slug,
|
||||
app_name=app_obj.name,
|
||||
description=display_name or profile_name,
|
||||
enabled_tools=[]
|
||||
display_name=display_name or profile_name,
|
||||
user_id=account_id,
|
||||
save_as_profile=True
|
||||
)
|
||||
|
||||
print("[DEBUG] create_credential_profile result:", result)
|
||||
|
||||
return self.success_response({
|
||||
"message": f"Successfully created credential profile '{profile_name}' for {app_obj.name}",
|
||||
"message": f"Successfully created credential profile '{profile_name}' for {result.toolkit.name}",
|
||||
"profile": {
|
||||
"profile_id": str(profile.profile_id),
|
||||
"profile_name": profile.profile_name.value if hasattr(profile.profile_name, 'value') else str(profile.profile_name),
|
||||
"display_name": profile.display_name,
|
||||
"app_slug": profile.app_slug.value if hasattr(profile.app_slug, 'value') else str(profile.app_slug),
|
||||
"app_name": profile.app_name,
|
||||
"external_user_id": profile.external_user_id.value if hasattr(profile.external_user_id, 'value') else str(profile.external_user_id),
|
||||
"is_connected": profile.is_connected,
|
||||
"created_at": profile.created_at.isoformat()
|
||||
"profile_id": result.profile_id,
|
||||
"profile_name": profile_name,
|
||||
"display_name": display_name or profile_name,
|
||||
"toolkit_slug": toolkit_slug,
|
||||
"toolkit_name": result.toolkit.name,
|
||||
"mcp_url": result.final_mcp_url,
|
||||
"redirect_url": result.connected_account.redirect_url,
|
||||
"is_connected": False,
|
||||
"auth_required": bool(result.connected_account.redirect_url)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -147,7 +147,7 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
"type": "function",
|
||||
"function": {
|
||||
"name": "connect_credential_profile",
|
||||
"description": "Generate a connection link for a credential profile. The user needs to visit this link to connect their app account to the profile.",
|
||||
"description": "Get the connection link for a credential profile. The user needs to visit this link to authenticate their account with the profile.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -169,27 +169,41 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
''')
|
||||
async def connect_credential_profile(self, profile_id: str) -> ToolResult:
|
||||
try:
|
||||
from uuid import UUID
|
||||
from pipedream.connection_token_service import ExternalUserId, AppSlug
|
||||
account_id = await self._get_current_account_id()
|
||||
profile_service = ComposioProfileService(self.db)
|
||||
profiles = await profile_service.get_profiles(account_id)
|
||||
|
||||
profile = None
|
||||
for p in profiles:
|
||||
if p.profile_id == profile_id:
|
||||
profile = p
|
||||
break
|
||||
|
||||
profile = await profile_service.get_profile(UUID(account_id), UUID(profile_id))
|
||||
if not profile:
|
||||
return self.fail_response("Credential profile not found")
|
||||
|
||||
# generate connection token using primitive values
|
||||
external_user_id = ExternalUserId(profile.external_user_id.value if hasattr(profile.external_user_id, 'value') else str(profile.external_user_id))
|
||||
app_slug = AppSlug(profile.app_slug.value if hasattr(profile.app_slug, 'value') else str(profile.app_slug))
|
||||
connection_result = await connection_token_service.create(external_user_id, app_slug)
|
||||
if profile.is_connected:
|
||||
return self.success_response({
|
||||
"message": f"Profile '{profile.display_name}' is already connected",
|
||||
"profile_name": profile.display_name,
|
||||
"toolkit_name": profile.toolkit_name,
|
||||
"is_connected": True,
|
||||
"instructions": f"This {profile.toolkit_name} profile is already connected and ready to use."
|
||||
})
|
||||
|
||||
config = await profile_service.get_profile_config(profile_id)
|
||||
redirect_url = config.get('redirect_url')
|
||||
|
||||
if not redirect_url:
|
||||
return self.fail_response("No authentication URL available for this profile")
|
||||
|
||||
return self.success_response({
|
||||
"message": f"Generated connection link for '{profile.display_name}'",
|
||||
"message": f"Connection link ready for '{profile.display_name}'",
|
||||
"profile_name": profile.display_name,
|
||||
"app_name": profile.app_name,
|
||||
"connection_link": connection_result.get("connect_link_url"),
|
||||
"external_user_id": profile.external_user_id.value if hasattr(profile.external_user_id, 'value') else str(profile.external_user_id),
|
||||
"expires_at": connection_result.get("expires_at"),
|
||||
"instructions": f"Please visit the connection link to connect your {profile.app_name} account to this profile. After connecting, you'll be able to use {profile.app_name} tools in your agent."
|
||||
"toolkit_name": profile.toolkit_name,
|
||||
"connection_link": redirect_url,
|
||||
"is_connected": profile.is_connected,
|
||||
"instructions": f"Please visit the connection link to authenticate your {profile.toolkit_name} account with this profile. After connecting, you'll be able to use {profile.toolkit_name} tools in your agent."
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
|
@ -320,11 +334,18 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
display_name: Optional[str] = None
|
||||
) -> ToolResult:
|
||||
try:
|
||||
from uuid import UUID
|
||||
account_id = await self._get_current_account_id()
|
||||
client = await self.db.client
|
||||
|
||||
profile = await profile_service.get_profile(UUID(account_id), UUID(profile_id))
|
||||
profile_service = ComposioProfileService(self.db)
|
||||
profiles = await profile_service.get_profiles(account_id)
|
||||
|
||||
profile = None
|
||||
for p in profiles:
|
||||
if p.profile_id == profile_id:
|
||||
profile = p
|
||||
break
|
||||
|
||||
if not profile:
|
||||
return self.fail_response("Credential profile not found")
|
||||
if not profile.is_connected:
|
||||
|
@ -346,17 +367,11 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
current_config = version_result.data['config']
|
||||
current_tools = current_config.get('tools', {})
|
||||
current_custom_mcps = current_tools.get('custom_mcp', [])
|
||||
|
||||
app_slug = profile.app_slug.value if hasattr(profile.app_slug, 'value') else str(profile.app_slug)
|
||||
|
||||
new_mcp_config = {
|
||||
'name': display_name or profile.display_name,
|
||||
'type': 'pipedream',
|
||||
'name': profile.toolkit_name,
|
||||
'type': 'composio',
|
||||
'config': {
|
||||
'url': 'https://remote.mcp.pipedream.net',
|
||||
'headers': {
|
||||
'x-pd-app-slug': app_slug
|
||||
},
|
||||
'profile_id': profile_id
|
||||
},
|
||||
'enabledTools': enabled_tools
|
||||
|
@ -382,9 +397,8 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
change_description=f"Configured {display_name or profile.display_name} with {len(enabled_tools)} tools"
|
||||
)
|
||||
|
||||
profile_name = profile.profile_name.value if hasattr(profile.profile_name, 'value') else str(profile.profile_name)
|
||||
return self.success_response({
|
||||
"message": f"Profile '{profile_name}' updated with {len(enabled_tools)} tools",
|
||||
"message": f"Profile '{profile.profile_name}' updated with {len(enabled_tools)} tools",
|
||||
"enabled_tools": enabled_tools,
|
||||
"total_tools": len(enabled_tools),
|
||||
"version_id": new_version.version_id,
|
||||
|
@ -421,14 +435,22 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
''')
|
||||
async def delete_credential_profile(self, profile_id: str) -> ToolResult:
|
||||
try:
|
||||
from uuid import UUID
|
||||
account_id = await self._get_current_account_id()
|
||||
client = await self.db.client
|
||||
|
||||
profile = await profile_service.get_profile(UUID(account_id), UUID(profile_id))
|
||||
profile_service = ComposioProfileService(self.db)
|
||||
profiles = await profile_service.get_profiles(account_id)
|
||||
|
||||
profile = None
|
||||
for p in profiles:
|
||||
if p.profile_id == profile_id:
|
||||
profile = p
|
||||
break
|
||||
|
||||
if not profile:
|
||||
return self.fail_response("Credential profile not found")
|
||||
|
||||
# Remove from agent configuration if it exists
|
||||
agent_result = await client.table('agents').select('current_version_id').eq('agent_id', self.agent_id).execute()
|
||||
if agent_result.data and agent_result.data[0].get('current_version_id'):
|
||||
version_result = await client.table('agent_versions')\
|
||||
|
@ -442,7 +464,7 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
current_tools = current_config.get('tools', {})
|
||||
current_custom_mcps = current_tools.get('custom_mcp', [])
|
||||
|
||||
updated_mcps = [mcp for mcp in current_custom_mcps if mcp.get('config', {}).get('profile_id') != str(profile.profile_id)]
|
||||
updated_mcps = [mcp for mcp in current_custom_mcps if mcp.get('config', {}).get('profile_id') != profile_id]
|
||||
|
||||
if len(updated_mcps) != len(current_custom_mcps):
|
||||
from agent.versioning.version_service import get_version_service
|
||||
|
@ -463,14 +485,15 @@ class CredentialProfileTool(AgentBuilderBaseTool):
|
|||
except Exception as e:
|
||||
return self.fail_response(f"Failed to update agent config: {str(e)}")
|
||||
|
||||
await profile_service.delete_profile(UUID(account_id), UUID(profile_id))
|
||||
# Delete the profile
|
||||
await profile_service.delete_profile(profile_id)
|
||||
|
||||
return self.success_response({
|
||||
"message": f"Successfully deleted credential profile '{profile.display_name}' for {profile.app_name}",
|
||||
"message": f"Successfully deleted credential profile '{profile.display_name}' for {profile.toolkit_name}",
|
||||
"deleted_profile": {
|
||||
"profile_id": str(profile.profile_id),
|
||||
"profile_id": profile.profile_id,
|
||||
"profile_name": profile.profile_name,
|
||||
"app_name": profile.app_name
|
||||
"toolkit_name": profile.toolkit_name
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@ from typing import Optional
|
|||
from agentpress.tool import ToolResult, openapi_schema, usage_example
|
||||
from agentpress.thread_manager import ThreadManager
|
||||
from .base_tool import AgentBuilderBaseTool
|
||||
from pipedream import app_service, mcp_service
|
||||
from composio_integration.toolkit_service import ToolkitService
|
||||
from composio_integration.composio_service import get_integration_service
|
||||
from utils.logger import logger
|
||||
|
||||
|
||||
|
@ -15,17 +16,17 @@ class MCPSearchTool(AgentBuilderBaseTool):
|
|||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_mcp_servers",
|
||||
"description": "Search for Pipedream MCP servers based on user requirements. Use this when the user wants to add MCP tools to their agent.",
|
||||
"description": "Search for Composio toolkits based on user requirements. Use this when the user wants to add MCP tools to their agent.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query for finding relevant Pipedream apps (e.g., 'linear', 'github', 'database', 'search')"
|
||||
"description": "Search query for finding relevant Composio toolkits (e.g., 'linear', 'github', 'database', 'search')"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of apps to return (default: 10)",
|
||||
"description": "Maximum number of toolkits to return (default: 10)",
|
||||
"default": 10
|
||||
}
|
||||
},
|
||||
|
@ -48,29 +49,30 @@ class MCPSearchTool(AgentBuilderBaseTool):
|
|||
limit: int = 10
|
||||
) -> ToolResult:
|
||||
try:
|
||||
search_result = await app_service.search_apps(
|
||||
query=query,
|
||||
category=category,
|
||||
page=1,
|
||||
limit=limit
|
||||
)
|
||||
toolkit_service = ToolkitService()
|
||||
integration_service = get_integration_service()
|
||||
|
||||
apps = search_result.get("apps", [])
|
||||
if query:
|
||||
toolkits = await integration_service.search_toolkits(query, category=category)
|
||||
else:
|
||||
toolkits = await toolkit_service.list_toolkits(limit=limit, category=category)
|
||||
|
||||
formatted_apps = []
|
||||
for app in apps:
|
||||
formatted_apps.append({
|
||||
"name": app.name,
|
||||
"app_slug": app.slug.value if hasattr(app.slug, 'value') else str(app.slug),
|
||||
"description": app.description,
|
||||
"logo_url": getattr(app, 'logo_url', ''),
|
||||
"auth_type": app.auth_type.value if app.auth_type else '',
|
||||
"is_verified": getattr(app, 'is_verified', False),
|
||||
"url": getattr(app, 'url', ''),
|
||||
"tags": getattr(app, 'tags', [])
|
||||
if len(toolkits) > limit:
|
||||
toolkits = toolkits[:limit]
|
||||
|
||||
formatted_toolkits = []
|
||||
for toolkit in toolkits:
|
||||
formatted_toolkits.append({
|
||||
"name": toolkit.name,
|
||||
"toolkit_slug": toolkit.slug,
|
||||
"description": toolkit.description or f"Toolkit for {toolkit.name}",
|
||||
"logo_url": toolkit.logo or '',
|
||||
"auth_schemes": toolkit.auth_schemes,
|
||||
"tags": toolkit.tags,
|
||||
"categories": toolkit.categories
|
||||
})
|
||||
|
||||
if not formatted_apps:
|
||||
if not formatted_toolkits:
|
||||
return ToolResult(
|
||||
success=False,
|
||||
output=json.dumps([], ensure_ascii=False)
|
||||
|
@ -78,167 +80,136 @@ class MCPSearchTool(AgentBuilderBaseTool):
|
|||
|
||||
return ToolResult(
|
||||
success=True,
|
||||
output=json.dumps(formatted_apps, ensure_ascii=False)
|
||||
output=json.dumps(formatted_toolkits, ensure_ascii=False)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return self.fail_response(f"Error searching Pipedream apps: {str(e)}")
|
||||
return self.fail_response(f"Error searching Composio toolkits: {str(e)}")
|
||||
|
||||
@openapi_schema({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_app_details",
|
||||
"description": "Get detailed information about a specific Pipedream app, including available tools and authentication requirements.",
|
||||
"description": "Get detailed information about a specific Composio toolkit, including available tools and authentication requirements.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app_slug": {
|
||||
"toolkit_slug": {
|
||||
"type": "string",
|
||||
"description": "The app slug to get details for (e.g., 'github', 'linear', 'slack')"
|
||||
"description": "The toolkit slug to get details for (e.g., 'github', 'linear', 'slack')"
|
||||
}
|
||||
},
|
||||
"required": ["app_slug"]
|
||||
"required": ["toolkit_slug"]
|
||||
}
|
||||
}
|
||||
})
|
||||
@usage_example('''
|
||||
<function_calls>
|
||||
<invoke name="get_app_details">
|
||||
<parameter name="app_slug">github</parameter>
|
||||
<parameter name="toolkit_slug">github</parameter>
|
||||
</invoke>
|
||||
</function_calls>
|
||||
''')
|
||||
async def get_app_details(self, app_slug: str) -> ToolResult:
|
||||
async def get_app_details(self, toolkit_slug: str) -> ToolResult:
|
||||
try:
|
||||
app_data = await app_service.get_app_by_slug(app_slug)
|
||||
toolkit_service = ToolkitService()
|
||||
toolkit_data = await toolkit_service.get_toolkit_by_slug(toolkit_slug)
|
||||
|
||||
if not app_data:
|
||||
return self.fail_response(f"Could not find app details for '{app_slug}'")
|
||||
if not toolkit_data:
|
||||
return self.fail_response(f"Could not find toolkit details for '{toolkit_slug}'")
|
||||
|
||||
formatted_app = {
|
||||
"name": app_data.name,
|
||||
"app_slug": app_data.slug.value if hasattr(app_data.slug, 'value') else str(app_data.slug),
|
||||
"description": app_data.description,
|
||||
"logo_url": getattr(app_data, 'logo_url', ''),
|
||||
"auth_type": app_data.auth_type.value if app_data.auth_type else '',
|
||||
"is_verified": getattr(app_data, 'is_verified', False),
|
||||
"url": getattr(app_data, 'url', ''),
|
||||
"tags": getattr(app_data, 'tags', []),
|
||||
"pricing": getattr(app_data, 'pricing', ''),
|
||||
"setup_instructions": getattr(app_data, 'setup_instructions', ''),
|
||||
"available_actions": getattr(app_data, 'available_actions', []),
|
||||
"available_triggers": getattr(app_data, 'available_triggers', [])
|
||||
formatted_toolkit = {
|
||||
"name": toolkit_data.name,
|
||||
"toolkit_slug": toolkit_data.slug,
|
||||
"description": toolkit_data.description or f"Toolkit for {toolkit_data.name}",
|
||||
"logo_url": toolkit_data.logo or '',
|
||||
"auth_schemes": toolkit_data.auth_schemes,
|
||||
"tags": toolkit_data.tags,
|
||||
"categories": toolkit_data.categories
|
||||
}
|
||||
|
||||
available_tools = []
|
||||
try:
|
||||
import httpx
|
||||
import json
|
||||
|
||||
url = f"https://remote.mcp.pipedream.net/?app={app_slug}&externalUserId=tools_preview"
|
||||
payload = {"jsonrpc": "2.0", "method": "tools/list", "params": {}, "id": 1}
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
async with client.stream("POST", url, json=payload, headers=headers) as resp:
|
||||
resp.raise_for_status()
|
||||
async for line in resp.aiter_lines():
|
||||
if not line or not line.startswith("data:"):
|
||||
continue
|
||||
data_str = line[len("data:"):].strip()
|
||||
try:
|
||||
data_obj = json.loads(data_str)
|
||||
tools = data_obj.get("result", {}).get("tools", [])
|
||||
for tool in tools:
|
||||
desc = tool.get("description", "") or ""
|
||||
idx = desc.find("[")
|
||||
if idx != -1:
|
||||
desc = desc[:idx].strip()
|
||||
|
||||
available_tools.append({
|
||||
"name": tool.get("name", ""),
|
||||
"description": desc
|
||||
})
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(f"Failed to parse JSON data: {data_str}")
|
||||
continue
|
||||
|
||||
except Exception as tools_error:
|
||||
logger.warning(f"Could not fetch MCP tools for {app_slug}: {tools_error}")
|
||||
|
||||
result = {
|
||||
"message": f"Retrieved details for {formatted_app['name']}",
|
||||
"app": formatted_app,
|
||||
"available_mcp_tools": available_tools,
|
||||
"total_mcp_tools": len(available_tools)
|
||||
"message": f"Retrieved details for {formatted_toolkit['name']}",
|
||||
"toolkit": formatted_toolkit,
|
||||
"supports_oauth": "OAUTH2" in toolkit_data.auth_schemes,
|
||||
"auth_schemes": toolkit_data.auth_schemes
|
||||
}
|
||||
|
||||
if available_tools:
|
||||
result["message"] += f" - {len(available_tools)} MCP tools available"
|
||||
|
||||
return self.success_response(result)
|
||||
|
||||
except Exception as e:
|
||||
return self.fail_response(f"Error getting app details: {str(e)}")
|
||||
return self.fail_response(f"Error getting toolkit details: {str(e)}")
|
||||
|
||||
@openapi_schema({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "discover_user_mcp_servers",
|
||||
"description": "Discover available MCP servers for a specific user and app combination. Use this to see what MCP tools are available for a connected profile.",
|
||||
"description": "Discover available MCP tools for a specific Composio profile. Use this to see what MCP tools are available for a connected profile.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"profile_id": {
|
||||
"type": "string",
|
||||
"description": "The external user ID from the credential profile"
|
||||
},
|
||||
"app_slug": {
|
||||
"type": "string",
|
||||
"description": "The app slug to discover MCP servers for"
|
||||
"description": "The profile ID from the Composio credential profile"
|
||||
}
|
||||
},
|
||||
"required": ["user_id", "app_slug"]
|
||||
"required": ["profile_id"]
|
||||
}
|
||||
}
|
||||
})
|
||||
@usage_example('''
|
||||
<function_calls>
|
||||
<invoke name="discover_user_mcp_servers">
|
||||
<parameter name="user_id">user_123456</parameter>
|
||||
<parameter name="app_slug">github</parameter>
|
||||
<parameter name="profile_id">profile-uuid-123</parameter>
|
||||
</invoke>
|
||||
</function_calls>
|
||||
''')
|
||||
async def discover_user_mcp_servers(self, user_id: str, app_slug: str) -> ToolResult:
|
||||
async def discover_user_mcp_servers(self, profile_id: str) -> ToolResult:
|
||||
try:
|
||||
from pipedream.mcp_service import ExternalUserId, AppSlug
|
||||
external_user_id = ExternalUserId(user_id)
|
||||
app_slug_obj = AppSlug(app_slug)
|
||||
servers = await mcp_service.discover_servers_for_user(external_user_id, app_slug_obj)
|
||||
account_id = await self._get_current_account_id()
|
||||
from composio_integration.composio_profile_service import ComposioProfileService
|
||||
from mcp_module.mcp_service import mcp_service
|
||||
|
||||
formatted_servers = []
|
||||
for server in servers:
|
||||
formatted_servers.append({
|
||||
"server_id": getattr(server, 'server_id', ''),
|
||||
"name": getattr(server, 'name', 'Unknown'),
|
||||
"app_slug": getattr(server, 'app_slug', app_slug),
|
||||
"status": getattr(server, 'status', 'unknown'),
|
||||
"available_tools": getattr(server, 'available_tools', []),
|
||||
"last_ping": getattr(server, 'last_ping', ''),
|
||||
"created_at": getattr(server, 'created_at', '')
|
||||
})
|
||||
profile_service = ComposioProfileService(self.db)
|
||||
profiles = await profile_service.get_profiles(account_id)
|
||||
|
||||
connected_servers = [s for s in formatted_servers if s["status"] == "connected"]
|
||||
total_tools = sum(len(s["available_tools"]) for s in connected_servers)
|
||||
profile = None
|
||||
for p in profiles:
|
||||
if p.profile_id == profile_id:
|
||||
profile = p
|
||||
break
|
||||
|
||||
if not profile:
|
||||
return self.fail_response(f"Composio profile {profile_id} not found")
|
||||
|
||||
if not profile.is_connected:
|
||||
return self.fail_response("Profile is not connected yet. Please connect the profile first.")
|
||||
|
||||
if not profile.mcp_url:
|
||||
return self.fail_response("Profile has no MCP URL")
|
||||
|
||||
result = await mcp_service.discover_custom_tools(
|
||||
request_type="http",
|
||||
config={"url": profile.mcp_url}
|
||||
)
|
||||
|
||||
if not result.success:
|
||||
return self.fail_response(f"Failed to discover tools: {result.message}")
|
||||
|
||||
available_tools = result.tools or []
|
||||
|
||||
return self.success_response({
|
||||
"message": f"Found {len(formatted_servers)} MCP servers for {app_slug} (user: {user_id}), {len(connected_servers)} connected with {total_tools} total tools available",
|
||||
"servers": formatted_servers,
|
||||
"connected_count": len(connected_servers),
|
||||
"total_tools": total_tools
|
||||
"message": f"Found {len(available_tools)} MCP tools available for {profile.toolkit_name} profile '{profile.profile_name}'",
|
||||
"profile_info": {
|
||||
"profile_id": profile.profile_id,
|
||||
"profile_name": profile.profile_name,
|
||||
"toolkit_name": profile.toolkit_name,
|
||||
"toolkit_slug": profile.toolkit_slug,
|
||||
"is_connected": profile.is_connected
|
||||
},
|
||||
"tools": available_tools,
|
||||
"total_tools": len(available_tools)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return self.fail_response(f"Error discovering MCP servers: {str(e)}")
|
||||
return self.fail_response(f"Error discovering MCP tools: {str(e)}")
|
|
@ -15,7 +15,7 @@ import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
|||
import { AgentLoader } from './loader';
|
||||
import { parseXmlToolCalls, isNewXmlFormat } from '@/components/thread/tool-views/xml-parser';
|
||||
import { ShowToolStream } from './ShowToolStream';
|
||||
import { PipedreamUrlDetector } from './pipedream-url-detector';
|
||||
import { ComposioUrlDetector } from './composio-url-detector';
|
||||
|
||||
const HIDE_STREAMING_XML_TAGS = new Set([
|
||||
'execute-command',
|
||||
|
@ -100,7 +100,7 @@ export function renderMarkdownContent(
|
|||
const textBeforeBlock = content.substring(lastIndex, match.index);
|
||||
if (textBeforeBlock.trim()) {
|
||||
contentParts.push(
|
||||
<PipedreamUrlDetector key={`md-${lastIndex}`} content={textBeforeBlock} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words" />
|
||||
<ComposioUrlDetector key={`md-${lastIndex}`} content={textBeforeBlock} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words" />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ export function renderMarkdownContent(
|
|||
// Render ask tool content with attachment UI
|
||||
contentParts.push(
|
||||
<div key={`ask-${match.index}-${index}`} className="space-y-3">
|
||||
<PipedreamUrlDetector content={askText} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words [&>:first-child]:mt-0 prose-headings:mt-3" />
|
||||
<ComposioUrlDetector content={askText} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words [&>:first-child]:mt-0 prose-headings:mt-3" />
|
||||
{renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)}
|
||||
</div>
|
||||
);
|
||||
|
@ -139,7 +139,7 @@ export function renderMarkdownContent(
|
|||
// Render complete tool content with attachment UI
|
||||
contentParts.push(
|
||||
<div key={`complete-${match.index}-${index}`} className="space-y-3">
|
||||
<PipedreamUrlDetector content={completeText} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words [&>:first-child]:mt-0 prose-headings:mt-3" />
|
||||
<ComposioUrlDetector content={completeText} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words [&>:first-child]:mt-0 prose-headings:mt-3" />
|
||||
{renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)}
|
||||
</div>
|
||||
);
|
||||
|
@ -186,12 +186,12 @@ export function renderMarkdownContent(
|
|||
const remainingText = content.substring(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
contentParts.push(
|
||||
<PipedreamUrlDetector key={`md-${lastIndex}`} content={remainingText} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words" />
|
||||
<ComposioUrlDetector key={`md-${lastIndex}`} content={remainingText} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words" />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return contentParts.length > 0 ? contentParts : <PipedreamUrlDetector content={content} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words" />;
|
||||
return contentParts.length > 0 ? contentParts : <ComposioUrlDetector content={content} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words" />;
|
||||
}
|
||||
|
||||
// Fall back to old XML format handling
|
||||
|
@ -202,7 +202,7 @@ export function renderMarkdownContent(
|
|||
|
||||
// If no XML tags found, just return the full content as markdown
|
||||
if (!content.match(xmlRegex)) {
|
||||
return <PipedreamUrlDetector content={content} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words" />;
|
||||
return <ComposioUrlDetector content={content} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words" />;
|
||||
}
|
||||
|
||||
while ((match = xmlRegex.exec(content)) !== null) {
|
||||
|
@ -210,7 +210,7 @@ export function renderMarkdownContent(
|
|||
if (match.index > lastIndex) {
|
||||
const textBeforeTag = content.substring(lastIndex, match.index);
|
||||
contentParts.push(
|
||||
<PipedreamUrlDetector key={`md-${lastIndex}`} content={textBeforeTag} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none inline-block mr-1 break-words" />
|
||||
<ComposioUrlDetector key={`md-${lastIndex}`} content={textBeforeTag} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none inline-block mr-1 break-words" />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -232,7 +232,7 @@ export function renderMarkdownContent(
|
|||
// Render <ask> tag content with attachment UI (using the helper)
|
||||
contentParts.push(
|
||||
<div key={`ask-${match.index}`} className="space-y-3">
|
||||
<PipedreamUrlDetector content={askContent} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words [&>:first-child]:mt-0 prose-headings:mt-3" />
|
||||
<ComposioUrlDetector content={askContent} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words [&>:first-child]:mt-0 prose-headings:mt-3" />
|
||||
{renderAttachments(attachments, fileViewerHandler, sandboxId, project)}
|
||||
</div>
|
||||
);
|
||||
|
@ -250,7 +250,7 @@ export function renderMarkdownContent(
|
|||
// Render <complete> tag content with attachment UI (using the helper)
|
||||
contentParts.push(
|
||||
<div key={`complete-${match.index}`} className="space-y-3">
|
||||
<PipedreamUrlDetector content={completeContent} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words [&>:first-child]:mt-0 prose-headings:mt-3" />
|
||||
<ComposioUrlDetector content={completeContent} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words [&>:first-child]:mt-0 prose-headings:mt-3" />
|
||||
{renderAttachments(attachments, fileViewerHandler, sandboxId, project)}
|
||||
</div>
|
||||
);
|
||||
|
@ -283,7 +283,7 @@ export function renderMarkdownContent(
|
|||
// Add text after the last tag
|
||||
if (lastIndex < content.length) {
|
||||
contentParts.push(
|
||||
<PipedreamUrlDetector key={`md-${lastIndex}`} content={content.substring(lastIndex)} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words" />
|
||||
<ComposioUrlDetector key={`md-${lastIndex}`} content={content.substring(lastIndex)} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none break-words" />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -689,7 +689,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
<div className="flex max-w-[85%] rounded-3xl rounded-br-lg bg-card border px-4 py-3 break-words overflow-hidden">
|
||||
<div className="space-y-3 min-w-0 flex-1">
|
||||
{cleanContent && (
|
||||
<PipedreamUrlDetector content={cleanContent} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere" />
|
||||
<ComposioUrlDetector content={cleanContent} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere" />
|
||||
)}
|
||||
|
||||
{/* Use the helper function to render user attachments */}
|
||||
|
@ -831,7 +831,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
return (
|
||||
<>
|
||||
{textBeforeTag && (
|
||||
<PipedreamUrlDetector content={textBeforeTag} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere" />
|
||||
<ComposioUrlDetector content={textBeforeTag} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere" />
|
||||
)}
|
||||
{showCursor && (
|
||||
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" />
|
||||
|
@ -894,7 +894,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
) : (
|
||||
<>
|
||||
{textBeforeTag && (
|
||||
<PipedreamUrlDetector content={textBeforeTag} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere" />
|
||||
<ComposioUrlDetector content={textBeforeTag} className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3 break-words overflow-wrap-anywhere" />
|
||||
)}
|
||||
{showCursor && (
|
||||
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" />
|
||||
|
|
|
@ -0,0 +1,298 @@
|
|||
import React from 'react';
|
||||
import { ExternalLink, ShieldCheck, Server } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Markdown } from '@/components/ui/markdown';
|
||||
|
||||
interface ComposioUrlDetectorProps {
|
||||
content: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface ComposioUrl {
|
||||
url: string;
|
||||
toolkitName: string | null;
|
||||
toolkitSlug: string | null;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
// Common toolkit name mappings for better display
|
||||
const TOOLKIT_NAME_MAPPINGS: Record<string, string> = {
|
||||
'gmail': 'Gmail',
|
||||
'github': 'GitHub',
|
||||
'gitlab': 'GitLab',
|
||||
'google_sheets': 'Google Sheets',
|
||||
'google_drive': 'Google Drive',
|
||||
'google_calendar': 'Google Calendar',
|
||||
'notion': 'Notion',
|
||||
'slack': 'Slack',
|
||||
'discord': 'Discord',
|
||||
'twitter': 'Twitter',
|
||||
'linkedin': 'LinkedIn',
|
||||
'facebook': 'Facebook',
|
||||
'instagram': 'Instagram',
|
||||
'youtube': 'YouTube',
|
||||
'zoom': 'Zoom',
|
||||
'microsoft_teams': 'Microsoft Teams',
|
||||
'outlook': 'Outlook',
|
||||
'dropbox': 'Dropbox',
|
||||
'onedrive': 'OneDrive',
|
||||
'salesforce': 'Salesforce',
|
||||
'hubspot': 'HubSpot',
|
||||
'mailchimp': 'Mailchimp',
|
||||
'stripe': 'Stripe',
|
||||
'paypal': 'PayPal',
|
||||
'shopify': 'Shopify',
|
||||
'wordpress': 'WordPress',
|
||||
'airtable': 'Airtable',
|
||||
'monday': 'Monday.com',
|
||||
'asana': 'Asana',
|
||||
'trello': 'Trello',
|
||||
'jira': 'Jira',
|
||||
'figma': 'Figma',
|
||||
'twilio': 'Twilio',
|
||||
'aws': 'AWS',
|
||||
'google_cloud': 'Google Cloud',
|
||||
'azure': 'Azure',
|
||||
};
|
||||
|
||||
// Toolkit logos/icons mapping
|
||||
const TOOLKIT_LOGOS: Record<string, string> = {
|
||||
'gmail': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/gmail.svg',
|
||||
'github': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/github.svg',
|
||||
'slack': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/slack.svg',
|
||||
'notion': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/notion.svg',
|
||||
'google_sheets': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/google-sheets.svg',
|
||||
'google_drive': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/google-drive.svg',
|
||||
'linear': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/linear.svg',
|
||||
'airtable': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/airtable.svg',
|
||||
'asana': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/asana.svg',
|
||||
'trello': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/trello.svg',
|
||||
'salesforce': 'https://cdn.jsdelivr.net/gh/ComposioHQ/open-logos@master/salesforce.svg',
|
||||
};
|
||||
|
||||
function formatToolkitName(toolkitSlug: string): string {
|
||||
// Check if we have a custom mapping first
|
||||
if (TOOLKIT_NAME_MAPPINGS[toolkitSlug.toLowerCase()]) {
|
||||
return TOOLKIT_NAME_MAPPINGS[toolkitSlug.toLowerCase()];
|
||||
}
|
||||
|
||||
// Fall back to converting snake_case to Title Case
|
||||
return toolkitSlug
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function extractToolkitInfoFromContext(content: string, urlStartIndex: number): { toolkitName: string | null; toolkitSlug: string | null } {
|
||||
// Look for toolkit information in the surrounding context
|
||||
const contextBefore = content.substring(Math.max(0, urlStartIndex - 500), urlStartIndex);
|
||||
const contextAfter = content.substring(urlStartIndex, Math.min(content.length, urlStartIndex + 200));
|
||||
const fullContext = contextBefore + contextAfter;
|
||||
|
||||
// Try to extract toolkit name from various patterns
|
||||
|
||||
// Pattern 1: "Successfully created credential profile 'ProfileName' for ToolkitName"
|
||||
let match = fullContext.match(/Successfully created credential profile[^f]*for\s+([^.!?\n]+)/i);
|
||||
if (match) {
|
||||
return { toolkitName: match[1].trim(), toolkitSlug: match[1].toLowerCase().replace(/\s+/g, '_') };
|
||||
}
|
||||
|
||||
// Pattern 2: "connect your ToolkitName account"
|
||||
match = fullContext.match(/connect your\s+([^a]+)\s+account/i);
|
||||
if (match) {
|
||||
const name = match[1].trim();
|
||||
return { toolkitName: name, toolkitSlug: name.toLowerCase().replace(/\s+/g, '_') };
|
||||
}
|
||||
|
||||
// Pattern 3: "authorize access to your ToolkitName account"
|
||||
match = fullContext.match(/authorize access to your\s+([^a]+)\s+account/i);
|
||||
if (match) {
|
||||
const name = match[1].trim();
|
||||
return { toolkitName: name, toolkitSlug: name.toLowerCase().replace(/\s+/g, '_') };
|
||||
}
|
||||
|
||||
// Pattern 4: Look for common toolkit names in the context
|
||||
const commonToolkits = Object.keys(TOOLKIT_NAME_MAPPINGS);
|
||||
for (const toolkit of commonToolkits) {
|
||||
const toolkitName = TOOLKIT_NAME_MAPPINGS[toolkit];
|
||||
if (fullContext.toLowerCase().includes(toolkitName.toLowerCase())) {
|
||||
return { toolkitName, toolkitSlug: toolkit };
|
||||
}
|
||||
}
|
||||
|
||||
return { toolkitName: null, toolkitSlug: null };
|
||||
}
|
||||
|
||||
function detectComposioUrls(content: string): ComposioUrl[] {
|
||||
// Detect Composio authentication URLs (these are typically OAuth URLs from various providers)
|
||||
const authUrlPatterns = [
|
||||
// Google OAuth
|
||||
/https:\/\/accounts\.google\.com\/oauth\/authorize\?[^\s)]+/g,
|
||||
// GitHub OAuth
|
||||
/https:\/\/github\.com\/login\/oauth\/authorize\?[^\s)]+/g,
|
||||
// Generic OAuth pattern for other providers
|
||||
/https:\/\/[^\/\s]+\/oauth2?\/authorize\?[^\s)]+/g,
|
||||
// Composio backend URLs
|
||||
/https:\/\/backend\.composio\.dev\/[^\s)]+/g,
|
||||
// Any HTTPS URL that looks like an auth callback
|
||||
/https:\/\/[^\/\s]+\/auth\/[^\s)]+/g,
|
||||
];
|
||||
|
||||
const urls: ComposioUrl[] = [];
|
||||
|
||||
for (const pattern of authUrlPatterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
const url = match[0];
|
||||
const { toolkitName, toolkitSlug } = extractToolkitInfoFromContext(content, match.index);
|
||||
|
||||
urls.push({
|
||||
url,
|
||||
toolkitName,
|
||||
toolkitSlug,
|
||||
startIndex: match.index,
|
||||
endIndex: match.index + url.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return urls.sort((a, b) => a.startIndex - b.startIndex);
|
||||
}
|
||||
|
||||
function hasAuthUrlPattern(content: string, url: ComposioUrl): boolean {
|
||||
const beforeUrl = content.substring(Math.max(0, url.startIndex - 100), url.startIndex);
|
||||
return /(?:authentication|auth|connect|visit)\s+(?:url|link):\s*$/i.test(beforeUrl);
|
||||
}
|
||||
|
||||
interface ComposioConnectButtonProps {
|
||||
url: string;
|
||||
toolkitName?: string;
|
||||
toolkitSlug?: string;
|
||||
}
|
||||
|
||||
const ComposioConnectButton: React.FC<ComposioConnectButtonProps> = ({
|
||||
url,
|
||||
toolkitName,
|
||||
toolkitSlug
|
||||
}) => {
|
||||
const displayName = toolkitName || (toolkitSlug ? formatToolkitName(toolkitSlug) : 'Service');
|
||||
const logoUrl = toolkitSlug ? TOOLKIT_LOGOS[toolkitSlug.toLowerCase()] : null;
|
||||
|
||||
const handleConnect = () => {
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="my-4 p-0 border bg-muted/30">
|
||||
<CardContent className="px-4 py-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-lg overflow-hidden bg-muted border flex items-center justify-center">
|
||||
{logoUrl ? (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${displayName} logo`}
|
||||
className="w-8 h-8 object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const parent = target.parentElement;
|
||||
if (parent) {
|
||||
parent.innerHTML = `<div class="w-full h-full flex items-center justify-center"><svg class="w-6 h-6 text-zinc-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd" /></svg></div>`;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Server className="w-6 h-6 text-zinc-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 flex items-center gap-2 justify-between">
|
||||
<div className="flex flex-col mb-3">
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-semibold text-zinc-900 dark:text-zinc-100 text-sm">
|
||||
Sign in to {displayName}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-600 dark:text-zinc-400 -mt-1">
|
||||
Click to authorize access to your {displayName} account
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleConnect}
|
||||
size="sm"
|
||||
className="max-w-64"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
Sign in
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const ComposioUrlDetector: React.FC<ComposioUrlDetectorProps> = ({
|
||||
content,
|
||||
className
|
||||
}) => {
|
||||
const composioUrls = detectComposioUrls(content);
|
||||
|
||||
if (composioUrls.length === 0) {
|
||||
return (
|
||||
<Markdown className={className}>
|
||||
{content}
|
||||
</Markdown>
|
||||
);
|
||||
}
|
||||
|
||||
const contentParts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
composioUrls.forEach((composioUrl, index) => {
|
||||
if (composioUrl.startIndex > lastIndex) {
|
||||
const textBefore = content.substring(lastIndex, composioUrl.startIndex);
|
||||
|
||||
const cleanedTextBefore = hasAuthUrlPattern(content, composioUrl)
|
||||
? textBefore.replace(/(?:authentication|auth|connect|visit)\s+(?:url|link):\s*$/i, '').trim()
|
||||
: textBefore;
|
||||
|
||||
if (cleanedTextBefore.trim()) {
|
||||
contentParts.push(
|
||||
<Markdown key={`text-${index}`} className={className}>
|
||||
{cleanedTextBefore}
|
||||
</Markdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
contentParts.push(
|
||||
<ComposioConnectButton
|
||||
key={`composio-${index}`}
|
||||
url={composioUrl.url}
|
||||
toolkitName={composioUrl.toolkitName || undefined}
|
||||
toolkitSlug={composioUrl.toolkitSlug || undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
lastIndex = composioUrl.endIndex;
|
||||
});
|
||||
|
||||
if (lastIndex < content.length) {
|
||||
const remainingText = content.substring(lastIndex);
|
||||
if (remainingText.trim()) {
|
||||
contentParts.push(
|
||||
<Markdown key="text-final" className={className}>
|
||||
{remainingText}
|
||||
</Markdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <>{contentParts}</>;
|
||||
};
|
|
@ -4,15 +4,16 @@ export interface CredentialProfile {
|
|||
profile_id: string;
|
||||
profile_name: string;
|
||||
display_name: string;
|
||||
app_slug: string;
|
||||
app_name: string;
|
||||
external_user_id: string;
|
||||
toolkit_slug: string;
|
||||
toolkit_name: string;
|
||||
mcp_url: string;
|
||||
redirect_url?: string;
|
||||
is_connected: boolean;
|
||||
created_at: string;
|
||||
auth_required?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateCredentialProfileData {
|
||||
app_slug: string | null;
|
||||
toolkit_slug: string | null;
|
||||
profile_name: string | null;
|
||||
display_name: string | null;
|
||||
message: string | null;
|
||||
|
@ -36,7 +37,7 @@ const extractFromNewFormat = (content: any): CreateCredentialProfileData => {
|
|||
const parsedContent = parseContent(content);
|
||||
|
||||
if (!parsedContent || typeof parsedContent !== 'object') {
|
||||
return { app_slug: null, profile_name: null, display_name: null, message: null, profile: null, success: undefined, timestamp: undefined };
|
||||
return { toolkit_slug: null, profile_name: null, display_name: null, message: null, profile: null, success: undefined, timestamp: undefined };
|
||||
}
|
||||
|
||||
if ('tool_execution' in parsedContent && typeof parsedContent.tool_execution === 'object') {
|
||||
|
@ -53,7 +54,7 @@ const extractFromNewFormat = (content: any): CreateCredentialProfileData => {
|
|||
parsedOutput = parsedOutput || {};
|
||||
|
||||
const extractedData = {
|
||||
app_slug: args.app_slug || null,
|
||||
toolkit_slug: args.toolkit_slug || null,
|
||||
profile_name: args.profile_name || null,
|
||||
display_name: args.display_name || null,
|
||||
message: parsedOutput.message || null,
|
||||
|
@ -67,7 +68,7 @@ const extractFromNewFormat = (content: any): CreateCredentialProfileData => {
|
|||
|
||||
if ('parameters' in parsedContent && 'output' in parsedContent) {
|
||||
const extractedData = {
|
||||
app_slug: parsedContent.parameters?.app_slug || null,
|
||||
toolkit_slug: parsedContent.parameters?.toolkit_slug || null,
|
||||
profile_name: parsedContent.parameters?.profile_name || null,
|
||||
display_name: parsedContent.parameters?.display_name || null,
|
||||
message: parsedContent.output?.message || null,
|
||||
|
@ -83,7 +84,7 @@ const extractFromNewFormat = (content: any): CreateCredentialProfileData => {
|
|||
return extractFromNewFormat(parsedContent.content);
|
||||
}
|
||||
|
||||
return { app_slug: null, profile_name: null, display_name: null, message: null, profile: null, success: undefined, timestamp: undefined };
|
||||
return { toolkit_slug: null, profile_name: null, display_name: null, message: null, profile: null, success: undefined, timestamp: undefined };
|
||||
};
|
||||
|
||||
const extractFromLegacyFormat = (content: any): Omit<CreateCredentialProfileData, 'success' | 'timestamp'> => {
|
||||
|
@ -93,7 +94,7 @@ const extractFromLegacyFormat = (content: any): Omit<CreateCredentialProfileData
|
|||
const args = toolData.arguments || {};
|
||||
|
||||
return {
|
||||
app_slug: args.app_slug || null,
|
||||
toolkit_slug: args.toolkit_slug || null,
|
||||
profile_name: args.profile_name || null,
|
||||
display_name: args.display_name || null,
|
||||
message: null,
|
||||
|
@ -102,7 +103,7 @@ const extractFromLegacyFormat = (content: any): Omit<CreateCredentialProfileData
|
|||
}
|
||||
|
||||
return {
|
||||
app_slug: null,
|
||||
toolkit_slug: null,
|
||||
profile_name: null,
|
||||
display_name: null,
|
||||
message: null,
|
||||
|
@ -117,7 +118,7 @@ export function extractCreateCredentialProfileData(
|
|||
toolTimestamp?: string,
|
||||
assistantTimestamp?: string
|
||||
): {
|
||||
app_slug: string | null;
|
||||
toolkit_slug: string | null;
|
||||
profile_name: string | null;
|
||||
display_name: string | null;
|
||||
message: string | null;
|
||||
|
@ -156,7 +157,7 @@ export function extractCreateCredentialProfileData(
|
|||
const assistantLegacy = extractFromLegacyFormat(assistantContent);
|
||||
|
||||
const combinedData = {
|
||||
app_slug: toolLegacy.app_slug || assistantLegacy.app_slug,
|
||||
toolkit_slug: toolLegacy.toolkit_slug || assistantLegacy.toolkit_slug,
|
||||
profile_name: toolLegacy.profile_name || assistantLegacy.profile_name,
|
||||
display_name: toolLegacy.display_name || assistantLegacy.display_name,
|
||||
message: toolLegacy.message || assistantLegacy.message,
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
import { extractToolData } from '../utils';
|
||||
|
||||
export interface AppDetails {
|
||||
export interface ToolkitDetails {
|
||||
name: string;
|
||||
app_slug: string;
|
||||
toolkit_slug: string;
|
||||
description: string;
|
||||
logo_url: string;
|
||||
auth_type: string;
|
||||
is_verified: boolean;
|
||||
url?: string | null;
|
||||
auth_schemes: string[];
|
||||
tags?: string[];
|
||||
pricing?: string;
|
||||
setup_instructions?: string;
|
||||
available_actions?: any[];
|
||||
available_triggers?: any[];
|
||||
categories?: string[];
|
||||
}
|
||||
|
||||
export interface GetAppDetailsData {
|
||||
app_slug: string | null;
|
||||
toolkit_slug: string | null;
|
||||
message: string | null;
|
||||
app: AppDetails | null;
|
||||
toolkit: ToolkitDetails | null;
|
||||
supports_oauth: boolean;
|
||||
auth_schemes: string[];
|
||||
success?: boolean;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
@ -38,7 +35,7 @@ const extractFromNewFormat = (content: any): GetAppDetailsData => {
|
|||
const parsedContent = parseContent(content);
|
||||
|
||||
if (!parsedContent || typeof parsedContent !== 'object') {
|
||||
return { app_slug: null, message: null, app: null, success: undefined, timestamp: undefined };
|
||||
return { toolkit_slug: null, message: null, toolkit: null, supports_oauth: false, auth_schemes: [], success: undefined, timestamp: undefined };
|
||||
}
|
||||
|
||||
if ('tool_execution' in parsedContent && typeof parsedContent.tool_execution === 'object') {
|
||||
|
@ -55,9 +52,11 @@ const extractFromNewFormat = (content: any): GetAppDetailsData => {
|
|||
parsedOutput = parsedOutput || {};
|
||||
|
||||
const extractedData = {
|
||||
app_slug: args.app_slug || null,
|
||||
toolkit_slug: args.toolkit_slug || null,
|
||||
message: parsedOutput.message || null,
|
||||
app: parsedOutput.app || null,
|
||||
toolkit: parsedOutput.toolkit || null,
|
||||
supports_oauth: parsedOutput.supports_oauth || false,
|
||||
auth_schemes: parsedOutput.auth_schemes || [],
|
||||
success: toolExecution.result?.success,
|
||||
timestamp: toolExecution.execution_details?.timestamp
|
||||
};
|
||||
|
@ -67,9 +66,11 @@ const extractFromNewFormat = (content: any): GetAppDetailsData => {
|
|||
|
||||
if ('parameters' in parsedContent && 'output' in parsedContent) {
|
||||
const extractedData = {
|
||||
app_slug: parsedContent.parameters?.app_slug || null,
|
||||
toolkit_slug: parsedContent.parameters?.toolkit_slug || null,
|
||||
message: parsedContent.output?.message || null,
|
||||
app: parsedContent.output?.app || null,
|
||||
toolkit: parsedContent.output?.toolkit || null,
|
||||
supports_oauth: parsedContent.output?.supports_oauth || false,
|
||||
auth_schemes: parsedContent.output?.auth_schemes || [],
|
||||
success: parsedContent.success,
|
||||
timestamp: undefined
|
||||
};
|
||||
|
@ -81,7 +82,7 @@ const extractFromNewFormat = (content: any): GetAppDetailsData => {
|
|||
return extractFromNewFormat(parsedContent.content);
|
||||
}
|
||||
|
||||
return { app_slug: null, message: null, app: null, success: undefined, timestamp: undefined };
|
||||
return { toolkit_slug: null, message: null, toolkit: null, supports_oauth: false, auth_schemes: [], success: undefined, timestamp: undefined };
|
||||
};
|
||||
|
||||
const extractFromLegacyFormat = (content: any): Omit<GetAppDetailsData, 'success' | 'timestamp'> => {
|
||||
|
@ -91,16 +92,20 @@ const extractFromLegacyFormat = (content: any): Omit<GetAppDetailsData, 'success
|
|||
const args = toolData.arguments || {};
|
||||
|
||||
return {
|
||||
app_slug: args.app_slug || null,
|
||||
toolkit_slug: args.toolkit_slug || null,
|
||||
message: null,
|
||||
app: null
|
||||
toolkit: null,
|
||||
supports_oauth: false,
|
||||
auth_schemes: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
app_slug: null,
|
||||
toolkit_slug: null,
|
||||
message: null,
|
||||
app: null
|
||||
toolkit: null,
|
||||
supports_oauth: false,
|
||||
auth_schemes: []
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -111,9 +116,11 @@ export function extractGetAppDetailsData(
|
|||
toolTimestamp?: string,
|
||||
assistantTimestamp?: string
|
||||
): {
|
||||
app_slug: string | null;
|
||||
toolkit_slug: string | null;
|
||||
message: string | null;
|
||||
app: AppDetails | null;
|
||||
toolkit: ToolkitDetails | null;
|
||||
supports_oauth: boolean;
|
||||
auth_schemes: string[];
|
||||
actualIsSuccess: boolean;
|
||||
actualToolTimestamp?: string;
|
||||
actualAssistantTimestamp?: string;
|
||||
|
@ -122,7 +129,7 @@ export function extractGetAppDetailsData(
|
|||
|
||||
if (toolContent) {
|
||||
data = extractFromNewFormat(toolContent);
|
||||
if (data.success !== undefined || data.app) {
|
||||
if (data.success !== undefined || data.toolkit) {
|
||||
return {
|
||||
...data,
|
||||
actualIsSuccess: data.success !== undefined ? data.success : isSuccess,
|
||||
|
@ -134,7 +141,7 @@ export function extractGetAppDetailsData(
|
|||
|
||||
if (assistantContent) {
|
||||
data = extractFromNewFormat(assistantContent);
|
||||
if (data.success !== undefined || data.app) {
|
||||
if (data.success !== undefined || data.toolkit) {
|
||||
return {
|
||||
...data,
|
||||
actualIsSuccess: data.success !== undefined ? data.success : isSuccess,
|
||||
|
@ -148,9 +155,11 @@ export function extractGetAppDetailsData(
|
|||
const assistantLegacy = extractFromLegacyFormat(assistantContent);
|
||||
|
||||
const combinedData = {
|
||||
app_slug: toolLegacy.app_slug || assistantLegacy.app_slug,
|
||||
toolkit_slug: toolLegacy.toolkit_slug || assistantLegacy.toolkit_slug,
|
||||
message: toolLegacy.message || assistantLegacy.message,
|
||||
app: toolLegacy.app || assistantLegacy.app,
|
||||
toolkit: toolLegacy.toolkit || assistantLegacy.toolkit,
|
||||
supports_oauth: toolLegacy.supports_oauth || assistantLegacy.supports_oauth,
|
||||
auth_schemes: toolLegacy.auth_schemes.length > 0 ? toolLegacy.auth_schemes : assistantLegacy.auth_schemes,
|
||||
actualIsSuccess: isSuccess,
|
||||
actualToolTimestamp: toolTimestamp,
|
||||
actualAssistantTimestamp: assistantTimestamp
|
||||
|
|
|
@ -26,7 +26,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||
import { LoadingState } from '../shared/LoadingState';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { extractGetAppDetailsData, AppDetails } from './_utils';
|
||||
import { extractGetAppDetailsData, ToolkitDetails } from './_utils';
|
||||
|
||||
export function GetAppDetailsToolView({
|
||||
name = 'get-app-details',
|
||||
|
@ -39,9 +39,11 @@ export function GetAppDetailsToolView({
|
|||
}: ToolViewProps) {
|
||||
|
||||
const {
|
||||
app_slug,
|
||||
toolkit_slug,
|
||||
message,
|
||||
app,
|
||||
toolkit,
|
||||
supports_oauth,
|
||||
auth_schemes,
|
||||
actualIsSuccess,
|
||||
actualToolTimestamp,
|
||||
actualAssistantTimestamp
|
||||
|
@ -55,27 +57,23 @@ export function GetAppDetailsToolView({
|
|||
|
||||
const toolTitle = getToolTitle(name);
|
||||
|
||||
const getAuthTypeColor = (authType: string) => {
|
||||
switch (authType?.toLowerCase()) {
|
||||
case 'oauth':
|
||||
return 'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-300 dark:border-emerald-800';
|
||||
case 'api_key':
|
||||
return 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800';
|
||||
case 'none':
|
||||
return 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-900/20 dark:text-gray-300 dark:border-gray-800';
|
||||
default:
|
||||
return 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-800';
|
||||
const getAuthTypeColor = (authSchemes: string[]) => {
|
||||
if (authSchemes?.includes('OAUTH2')) {
|
||||
return 'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-300 dark:border-emerald-800';
|
||||
} else if (authSchemes?.includes('API_KEY')) {
|
||||
return 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800';
|
||||
} else if (authSchemes?.includes('BEARER_TOKEN')) {
|
||||
return 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-800';
|
||||
} else {
|
||||
return 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-900/20 dark:text-gray-300 dark:border-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getAuthTypeIcon = (authType: string) => {
|
||||
switch (authType?.toLowerCase()) {
|
||||
case 'oauth':
|
||||
return ShieldCheck;
|
||||
case 'api_key':
|
||||
return Shield;
|
||||
default:
|
||||
return Shield;
|
||||
const getAuthTypeIcon = (authSchemes: string[]) => {
|
||||
if (authSchemes?.includes('OAUTH2')) {
|
||||
return ShieldCheck;
|
||||
} else {
|
||||
return Shield;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -122,20 +120,20 @@ export function GetAppDetailsToolView({
|
|||
iconColor="text-blue-500 dark:text-blue-400"
|
||||
bgColor="bg-gradient-to-b from-blue-100 to-blue-50 shadow-inner dark:from-blue-800/40 dark:to-blue-900/60 dark:shadow-blue-950/20"
|
||||
title="Loading app details"
|
||||
filePath={app_slug ? `"${app_slug}"` : undefined}
|
||||
filePath={toolkit_slug ? `"${toolkit_slug}"` : undefined}
|
||||
showProgress={true}
|
||||
/>
|
||||
) : app ? (
|
||||
) : toolkit ? (
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="border rounded-xl p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-12 h-12 rounded-xl overflow-hidden bg-muted/50 border flex items-center justify-center">
|
||||
{app.logo_url ? (
|
||||
{toolkit.logo_url ? (
|
||||
<img
|
||||
src={app.logo_url}
|
||||
alt={`${app.name} logo`}
|
||||
src={toolkit.logo_url}
|
||||
alt={`${toolkit.name} logo`}
|
||||
className="w-8 h-8 object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
|
@ -150,7 +148,7 @@ export function GetAppDetailsToolView({
|
|||
<Server className="w-8 h-8 text-zinc-400" />
|
||||
)}
|
||||
</div>
|
||||
{app.is_verified && (
|
||||
{supports_oauth && (
|
||||
<div className="absolute -top-1 -right-1">
|
||||
<div className="bg-blue-500 rounded-full p-1">
|
||||
<Verified className="w-3 h-3 text-white" />
|
||||
|
@ -160,13 +158,13 @@ export function GetAppDetailsToolView({
|
|||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{app.name}
|
||||
{toolkit.name}
|
||||
</h2>
|
||||
{app.is_verified && (
|
||||
{supports_oauth && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
|
@ -182,20 +180,20 @@ export function GetAppDetailsToolView({
|
|||
)}
|
||||
</div>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400 font-mono mb-2">
|
||||
{app.app_slug}
|
||||
{toolkit.toolkit_slug}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{(() => {
|
||||
const AuthIcon = getAuthTypeIcon(app.auth_type);
|
||||
const AuthIcon = getAuthTypeIcon(auth_schemes);
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn("text-xs font-medium", getAuthTypeColor(app.auth_type))}
|
||||
className={cn("text-xs font-medium", getAuthTypeColor(auth_schemes))}
|
||||
>
|
||||
<AuthIcon className="w-3 h-3 " />
|
||||
{app.auth_type?.replace('_', ' ') || 'Unknown'}
|
||||
{auth_schemes?.includes('OAUTH2') ? 'OAuth2' : auth_schemes?.includes('BEARER_TOKEN') ? 'Bearer Token' : auth_schemes?.includes('API_KEY') ? 'API Key' : 'Unknown'}
|
||||
</Badge>
|
||||
);
|
||||
})()}
|
||||
|
@ -203,12 +201,12 @@ export function GetAppDetailsToolView({
|
|||
</div>
|
||||
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300 leading-relaxed mb-4">
|
||||
{app.description}
|
||||
{toolkit.description}
|
||||
</p>
|
||||
|
||||
{app.tags && app.tags.length > 0 && (
|
||||
{toolkit.tags && toolkit.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-4">
|
||||
{app.tags.map((tag, tagIndex) => (
|
||||
{toolkit.tags.map((tag, tagIndex) => (
|
||||
<Badge
|
||||
key={tagIndex}
|
||||
variant="secondary"
|
||||
|
@ -220,114 +218,27 @@ export function GetAppDetailsToolView({
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{app.url && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 px-3 text-xs hover:bg-blue-50 dark:hover:bg-blue-900/20"
|
||||
onClick={() => window.open(app.url!, '_blank')}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3 " />
|
||||
Visit Website
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(app.pricing || app.setup_instructions) && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{app.pricing && (
|
||||
<div className="bg-white dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<DollarSign className="w-4 h-4 text-green-500" />
|
||||
<h3 className="font-medium text-zinc-900 dark:text-zinc-100">Pricing</h3>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300">
|
||||
{app.pricing || 'No pricing information available'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{app.setup_instructions && (
|
||||
<div className="bg-white dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Settings className="w-4 h-4 text-blue-500" />
|
||||
<h3 className="font-medium text-zinc-900 dark:text-zinc-100">Setup</h3>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-300">
|
||||
{app.setup_instructions || 'No setup instructions available'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(app.available_actions?.length || app.available_triggers?.length) ? (
|
||||
<div className="space-y-4">
|
||||
{app.available_actions && app.available_actions.length > 0 && (
|
||||
<div className="bg-white dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Zap className="w-4 h-4 text-orange-500" />
|
||||
<h3 className="font-medium text-zinc-900 dark:text-zinc-100">Available Actions</h3>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{app.available_actions.length}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{app.available_actions.slice(0, 5).map((action: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2 text-sm">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-orange-500" />
|
||||
<span className="text-zinc-700 dark:text-zinc-300">
|
||||
{action.name || action.display_name || action}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{app.available_actions.length > 5 && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-2">
|
||||
+{app.available_actions.length - 5} more actions
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{app.available_triggers && app.available_triggers.length > 0 && (
|
||||
<div className="bg-white dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Play className="w-4 h-4 text-green-500" />
|
||||
<h3 className="font-medium text-zinc-900 dark:text-zinc-100">Available Triggers</h3>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{app.available_triggers.length}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{app.available_triggers.slice(0, 5).map((trigger: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2 text-sm">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-green-500" />
|
||||
<span className="text-zinc-700 dark:text-zinc-300">
|
||||
{trigger.name || trigger.display_name || trigger}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{app.available_triggers.length > 5 && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-2">
|
||||
+{app.available_triggers.length - 5} more triggers
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-zinc-50 dark:bg-zinc-900/30 border border-zinc-200 dark:border-zinc-800 rounded-xl p-6 text-center">
|
||||
<BookOpen className="w-8 h-8 text-zinc-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
No actions or triggers available for this integration
|
||||
</p>
|
||||
{toolkit.categories && toolkit.categories.length > 0 && (
|
||||
<div className="bg-white dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Tag className="w-4 h-4 text-purple-500" />
|
||||
<h3 className="font-medium text-zinc-900 dark:text-zinc-100">Categories</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{toolkit.categories.map((category, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="secondary"
|
||||
className="text-xs"
|
||||
>
|
||||
{category}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -342,7 +253,7 @@ export function GetAppDetailsToolView({
|
|||
No app details found
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{app_slug ? `Unable to load details for "${app_slug}"` : 'App information not available'}
|
||||
{toolkit_slug ? `Unable to load details for "${toolkit_slug}"` : 'App information not available'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,19 +4,17 @@ export interface CredentialProfileItem {
|
|||
profile_id: string;
|
||||
profile_name: string;
|
||||
display_name: string;
|
||||
app_slug: string;
|
||||
app_name: string;
|
||||
external_user_id: string;
|
||||
toolkit_slug: string;
|
||||
toolkit_name: string;
|
||||
mcp_url: string;
|
||||
is_connected: boolean;
|
||||
is_active: boolean;
|
||||
is_default: boolean;
|
||||
enabled_tools: string[];
|
||||
created_at: string;
|
||||
last_used_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface GetCredentialProfilesData {
|
||||
app_slug: string | null;
|
||||
toolkit_slug: string | null;
|
||||
message: string | null;
|
||||
profiles: CredentialProfileItem[];
|
||||
total_count: number;
|
||||
|
@ -40,7 +38,7 @@ const extractFromNewFormat = (content: any): GetCredentialProfilesData => {
|
|||
|
||||
if (!parsedContent || typeof parsedContent !== 'object') {
|
||||
return {
|
||||
app_slug: null,
|
||||
toolkit_slug: null,
|
||||
message: null,
|
||||
profiles: [],
|
||||
total_count: 0,
|
||||
|
@ -63,7 +61,7 @@ const extractFromNewFormat = (content: any): GetCredentialProfilesData => {
|
|||
parsedOutput = parsedOutput || {};
|
||||
|
||||
const extractedData = {
|
||||
app_slug: args.app_slug || null,
|
||||
toolkit_slug: args.toolkit_slug || null,
|
||||
message: parsedOutput.message || null,
|
||||
profiles: Array.isArray(parsedOutput.profiles) ? parsedOutput.profiles : [],
|
||||
total_count: parsedOutput.total_count || 0,
|
||||
|
@ -76,7 +74,7 @@ const extractFromNewFormat = (content: any): GetCredentialProfilesData => {
|
|||
|
||||
if ('parameters' in parsedContent && 'output' in parsedContent) {
|
||||
const extractedData = {
|
||||
app_slug: parsedContent.parameters?.app_slug || null,
|
||||
toolkit_slug: parsedContent.parameters?.toolkit_slug || null,
|
||||
message: parsedContent.output?.message || null,
|
||||
profiles: Array.isArray(parsedContent.output?.profiles) ? parsedContent.output.profiles : [],
|
||||
total_count: parsedContent.output?.total_count || 0,
|
||||
|
@ -92,7 +90,7 @@ const extractFromNewFormat = (content: any): GetCredentialProfilesData => {
|
|||
}
|
||||
|
||||
return {
|
||||
app_slug: null,
|
||||
toolkit_slug: null,
|
||||
message: null,
|
||||
profiles: [],
|
||||
total_count: 0,
|
||||
|
@ -108,7 +106,7 @@ const extractFromLegacyFormat = (content: any): Omit<GetCredentialProfilesData,
|
|||
const args = toolData.arguments || {};
|
||||
|
||||
return {
|
||||
app_slug: args.app_slug || null,
|
||||
toolkit_slug: args.toolkit_slug || null,
|
||||
message: null,
|
||||
profiles: [],
|
||||
total_count: 0
|
||||
|
@ -116,7 +114,7 @@ const extractFromLegacyFormat = (content: any): Omit<GetCredentialProfilesData,
|
|||
}
|
||||
|
||||
return {
|
||||
app_slug: null,
|
||||
toolkit_slug: null,
|
||||
message: null,
|
||||
profiles: [],
|
||||
total_count: 0
|
||||
|
@ -130,7 +128,7 @@ export function extractGetCredentialProfilesData(
|
|||
toolTimestamp?: string,
|
||||
assistantTimestamp?: string
|
||||
): {
|
||||
app_slug: string | null;
|
||||
toolkit_slug: string | null;
|
||||
message: string | null;
|
||||
profiles: CredentialProfileItem[];
|
||||
total_count: number;
|
||||
|
@ -168,7 +166,7 @@ export function extractGetCredentialProfilesData(
|
|||
const assistantLegacy = extractFromLegacyFormat(assistantContent);
|
||||
|
||||
const combinedData = {
|
||||
app_slug: toolLegacy.app_slug || assistantLegacy.app_slug,
|
||||
toolkit_slug: toolLegacy.toolkit_slug || assistantLegacy.toolkit_slug,
|
||||
message: toolLegacy.message || assistantLegacy.message,
|
||||
profiles: toolLegacy.profiles.length > 0 ? toolLegacy.profiles : assistantLegacy.profiles,
|
||||
total_count: toolLegacy.total_count || assistantLegacy.total_count,
|
||||
|
|
|
@ -32,7 +32,7 @@ export function GetCredentialProfilesToolView({
|
|||
}: ToolViewProps) {
|
||||
|
||||
const {
|
||||
app_slug,
|
||||
toolkit_slug,
|
||||
message,
|
||||
profiles,
|
||||
total_count,
|
||||
|
@ -59,14 +59,7 @@ export function GetCredentialProfilesToolView({
|
|||
};
|
||||
};
|
||||
|
||||
const getActiveStatus = (isActive: boolean) => {
|
||||
return {
|
||||
color: isActive
|
||||
? 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800'
|
||||
: 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-900/20 dark:text-gray-300 dark:border-gray-800',
|
||||
text: isActive ? 'Active' : 'Inactive'
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Card className="gap-0 flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-card">
|
||||
|
@ -80,9 +73,9 @@ export function GetCredentialProfilesToolView({
|
|||
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{toolTitle}
|
||||
</CardTitle>
|
||||
{app_slug && (
|
||||
{toolkit_slug && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
||||
App: {app_slug}
|
||||
Toolkit: {toolkit_slug}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
@ -116,7 +109,7 @@ export function GetCredentialProfilesToolView({
|
|||
iconColor="text-blue-500 dark:text-blue-400"
|
||||
bgColor="bg-gradient-to-b from-blue-100 to-blue-50 shadow-inner dark:from-blue-800/40 dark:to-blue-900/60 dark:shadow-blue-950/20"
|
||||
title="Loading credential profiles"
|
||||
filePath={app_slug ? `"${app_slug}"` : undefined}
|
||||
filePath={toolkit_slug ? `"${toolkit_slug}"` : undefined}
|
||||
showProgress={true}
|
||||
/>
|
||||
) : profiles.length > 0 ? (
|
||||
|
@ -156,7 +149,7 @@ export function GetCredentialProfilesToolView({
|
|||
)}
|
||||
</div>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{profile.app_name}
|
||||
{profile.toolkit_name}
|
||||
</p>
|
||||
{profile.profile_name !== profile.display_name && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 font-mono">
|
||||
|
@ -169,7 +162,6 @@ export function GetCredentialProfilesToolView({
|
|||
<div className="flex items-center gap-2">
|
||||
{(() => {
|
||||
const connectionStatus = getConnectionStatus(profile.is_connected);
|
||||
const activeStatus = getActiveStatus(profile.is_active);
|
||||
const ConnectionIcon = connectionStatus.icon;
|
||||
|
||||
return (
|
||||
|
@ -181,12 +173,6 @@ export function GetCredentialProfilesToolView({
|
|||
<ConnectionIcon className="w-3 h-3" />
|
||||
{connectionStatus.text}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn("text-xs font-medium", activeStatus.color)}
|
||||
>
|
||||
{activeStatus.text}
|
||||
</Badge>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
@ -207,7 +193,7 @@ export function GetCredentialProfilesToolView({
|
|||
No profiles found
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{app_slug ? `No credential profiles found for "${app_slug}"` : 'No credential profiles available'}
|
||||
{toolkit_slug ? `No credential profiles found for "${toolkit_slug}"` : 'No credential profiles available'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,13 +2,12 @@ import { extractToolData } from '../utils';
|
|||
|
||||
export interface McpServerResult {
|
||||
name: string;
|
||||
app_slug: string;
|
||||
toolkit_slug: string;
|
||||
description: string;
|
||||
logo_url: string;
|
||||
auth_type: string;
|
||||
is_verified: boolean;
|
||||
url?: string | null;
|
||||
auth_schemes: string[];
|
||||
tags?: string[];
|
||||
categories?: string[];
|
||||
}
|
||||
|
||||
export interface SearchMcpServersData {
|
||||
|
|
|
@ -53,35 +53,31 @@ export function SearchMcpServersToolView({
|
|||
|
||||
const toolTitle = getToolTitle(name);
|
||||
|
||||
const getAuthTypeColor = (authType: string) => {
|
||||
switch (authType?.toLowerCase()) {
|
||||
case 'oauth':
|
||||
return 'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-300 dark:border-emerald-800';
|
||||
case 'api_key':
|
||||
return 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800';
|
||||
case 'none':
|
||||
return 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-900/20 dark:text-gray-300 dark:border-gray-800';
|
||||
default:
|
||||
return 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-800';
|
||||
const getAuthSchemeColor = (authSchemes: string[]) => {
|
||||
if (authSchemes?.includes('OAUTH2')) {
|
||||
return 'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-300 dark:border-emerald-800';
|
||||
} else if (authSchemes?.includes('API_KEY')) {
|
||||
return 'bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800';
|
||||
} else if (authSchemes?.includes('BEARER_TOKEN')) {
|
||||
return 'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-800';
|
||||
} else {
|
||||
return 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-900/20 dark:text-gray-300 dark:border-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getAuthTypeIcon = (authType: string) => {
|
||||
switch (authType?.toLowerCase()) {
|
||||
case 'oauth':
|
||||
return ShieldCheck;
|
||||
case 'api_key':
|
||||
return Shield;
|
||||
default:
|
||||
return Shield;
|
||||
const getAuthSchemeIcon = (authSchemes: string[]) => {
|
||||
if (authSchemes?.includes('OAUTH2')) {
|
||||
return ShieldCheck;
|
||||
} else {
|
||||
return Shield;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleExpanded = (index: number) => {
|
||||
setExpandedResults(prev => ({
|
||||
...prev,
|
||||
[index]: !prev[index]
|
||||
}));
|
||||
const getPrimaryAuthScheme = (authSchemes: string[]) => {
|
||||
if (authSchemes?.includes('OAUTH2')) return 'OAuth2';
|
||||
if (authSchemes?.includes('API_KEY')) return 'API Key';
|
||||
if (authSchemes?.includes('BEARER_TOKEN')) return 'Bearer Token';
|
||||
return authSchemes?.[0] || 'Unknown';
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -134,8 +130,9 @@ export function SearchMcpServersToolView({
|
|||
<ScrollArea className="h-full w-full">
|
||||
<div className="p-4 space-y-3">
|
||||
{results.map((result: McpServerResult, index: number) => {
|
||||
const AuthIcon = getAuthTypeIcon(result.auth_type);
|
||||
const AuthIcon = getAuthSchemeIcon(result.auth_schemes);
|
||||
const isExpanded = expandedResults[index];
|
||||
const hasOAuth = result.auth_schemes?.includes('OAUTH2');
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -163,9 +160,9 @@ export function SearchMcpServersToolView({
|
|||
<Server className="w-6 h-6 text-zinc-400" />
|
||||
)}
|
||||
</div>
|
||||
{result.is_verified && (
|
||||
{hasOAuth && (
|
||||
<div className="absolute -top-1 -right-1">
|
||||
<div className="bg-blue-500 rounded-full p-1">
|
||||
<div className="bg-emerald-500 rounded-full p-1">
|
||||
<Verified className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -179,32 +176,32 @@ export function SearchMcpServersToolView({
|
|||
<h3 className="font-semibold text-zinc-900 dark:text-zinc-100 truncate">
|
||||
{result.name}
|
||||
</h3>
|
||||
{result.is_verified && (
|
||||
{hasOAuth && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex items-center">
|
||||
<Sparkles className="w-4 h-4 text-blue-500" />
|
||||
<Sparkles className="w-4 h-4 text-emerald-500" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Verified integration</p>
|
||||
<p>OAuth2 supported</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400 font-mono">
|
||||
{result.app_slug}
|
||||
{result.toolkit_slug}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn("text-xs font-medium", getAuthTypeColor(result.auth_type))}
|
||||
className={cn("text-xs font-medium", getAuthSchemeColor(result.auth_schemes))}
|
||||
>
|
||||
<AuthIcon className="w-3 h-3 " />
|
||||
{result.auth_type?.replace('_', ' ') || 'Unknown'}
|
||||
{getPrimaryAuthScheme(result.auth_schemes)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -215,17 +212,6 @@ export function SearchMcpServersToolView({
|
|||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{result.url && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs hover:bg-purple-50 dark:hover:bg-purple-900/20"
|
||||
onClick={() => window.open(result.url!, '_blank')}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3 " />
|
||||
View
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue