update agent builder to use composio

This commit is contained in:
Saumya 2025-08-04 10:43:42 +05:30
parent d456817809
commit aa37988106
12 changed files with 664 additions and 499 deletions

View File

@ -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

View File

@ -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
}
})

View File

@ -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)}")

View File

@ -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" />

View File

@ -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}</>;
};

View File

@ -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,

View File

@ -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

View File

@ -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>

View File

@ -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,

View File

@ -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>

View File

@ -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 {

View File

@ -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>