From aa3798810602c40c78ba8afa7c39c0af67a53a61 Mon Sep 17 00:00:00 2001 From: Saumya Date: Mon, 4 Aug 2025 10:43:42 +0530 Subject: [PATCH] update agent builder to use composio --- backend/agent/api.py | 17 - .../credential_profile_tool.py | 177 ++++++----- .../agent_builder_tools/mcp_search_tool.py | 221 ++++++------- .../thread/content/ThreadContent.tsx | 28 +- .../thread/content/composio-url-detector.tsx | 298 ++++++++++++++++++ .../create-credential-profile/_utils.ts | 27 +- .../tool-views/get-app-details/_utils.ts | 63 ++-- .../get-app-details/get-app-details.tsx | 197 ++++-------- .../get-credential-profiles/_utils.ts | 28 +- .../get-credential-profiles.tsx | 28 +- .../tool-views/search-mcp-servers/_utils.ts | 7 +- .../search-mcp-servers/search-mcp-servers.tsx | 72 ++--- 12 files changed, 664 insertions(+), 499 deletions(-) create mode 100644 frontend/src/components/thread/content/composio-url-detector.tsx diff --git a/backend/agent/api.py b/backend/agent/api.py index 493400db..69654b17 100644 --- a/backend/agent/api.py +++ b/backend/agent/api.py @@ -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 diff --git a/backend/agent/tools/agent_builder_tools/credential_profile_tool.py b/backend/agent/tools/agent_builder_tools/credential_profile_tool.py index d6faf1e5..abc5520b 100644 --- a/backend/agent/tools/agent_builder_tools/credential_profile_tool.py +++ b/backend/agent/tools/agent_builder_tools/credential_profile_tool.py @@ -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(''' - github + github ''') - 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(''' - github + github Personal GitHub My Personal GitHub Account @@ -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 } }) diff --git a/backend/agent/tools/agent_builder_tools/mcp_search_tool.py b/backend/agent/tools/agent_builder_tools/mcp_search_tool.py index 97a01a1a..1338b99a 100644 --- a/backend/agent/tools/agent_builder_tools/mcp_search_tool.py +++ b/backend/agent/tools/agent_builder_tools/mcp_search_tool.py @@ -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(''' - github + github ''') - 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(''' - user_123456 - github + profile-uuid-123 ''') - 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)}") \ No newline at end of file + return self.fail_response(f"Error discovering MCP tools: {str(e)}") \ No newline at end of file diff --git a/frontend/src/components/thread/content/ThreadContent.tsx b/frontend/src/components/thread/content/ThreadContent.tsx index be1f7e67..9f7e5786 100644 --- a/frontend/src/components/thread/content/ThreadContent.tsx +++ b/frontend/src/components/thread/content/ThreadContent.tsx @@ -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( - + ); } } @@ -123,7 +123,7 @@ export function renderMarkdownContent( // Render ask tool content with attachment UI contentParts.push(
- + {renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)}
); @@ -139,7 +139,7 @@ export function renderMarkdownContent( // Render complete tool content with attachment UI contentParts.push(
- + {renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)}
); @@ -186,12 +186,12 @@ export function renderMarkdownContent( const remainingText = content.substring(lastIndex); if (remainingText.trim()) { contentParts.push( - + ); } } - return contentParts.length > 0 ? contentParts : ; + return contentParts.length > 0 ? contentParts : ; } // 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 ; + return ; } 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( - + ); } @@ -232,7 +232,7 @@ export function renderMarkdownContent( // Render tag content with attachment UI (using the helper) contentParts.push(
- + {renderAttachments(attachments, fileViewerHandler, sandboxId, project)}
); @@ -250,7 +250,7 @@ export function renderMarkdownContent( // Render tag content with attachment UI (using the helper) contentParts.push(
- + {renderAttachments(attachments, fileViewerHandler, sandboxId, project)}
); @@ -283,7 +283,7 @@ export function renderMarkdownContent( // Add text after the last tag if (lastIndex < content.length) { contentParts.push( - + ); } @@ -689,7 +689,7 @@ export const ThreadContent: React.FC = ({
{cleanContent && ( - + )} {/* Use the helper function to render user attachments */} @@ -831,7 +831,7 @@ export const ThreadContent: React.FC = ({ return ( <> {textBeforeTag && ( - + )} {showCursor && ( @@ -894,7 +894,7 @@ export const ThreadContent: React.FC = ({ ) : ( <> {textBeforeTag && ( - + )} {showCursor && ( diff --git a/frontend/src/components/thread/content/composio-url-detector.tsx b/frontend/src/components/thread/content/composio-url-detector.tsx new file mode 100644 index 00000000..b7a711ac --- /dev/null +++ b/frontend/src/components/thread/content/composio-url-detector.tsx @@ -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 = { + '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 = { + '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 = ({ + 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 ( + + +
+
+
+ {logoUrl ? ( + {`${displayName} { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + const parent = target.parentElement; + if (parent) { + parent.innerHTML = `
`; + } + }} + /> + ) : ( + + )} +
+
+ +
+
+
+

+ Sign in to {displayName} +

+
+

+ Click to authorize access to your {displayName} account +

+
+ +
+
+
+
+ ); +}; + +export const ComposioUrlDetector: React.FC = ({ + content, + className +}) => { + const composioUrls = detectComposioUrls(content); + + if (composioUrls.length === 0) { + return ( + + {content} + + ); + } + + 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( + + {cleanedTextBefore} + + ); + } + } + + contentParts.push( + + ); + + lastIndex = composioUrl.endIndex; + }); + + if (lastIndex < content.length) { + const remainingText = content.substring(lastIndex); + if (remainingText.trim()) { + contentParts.push( + + {remainingText} + + ); + } + } + + return <>{contentParts}; +}; \ No newline at end of file diff --git a/frontend/src/components/thread/tool-views/create-credential-profile/_utils.ts b/frontend/src/components/thread/tool-views/create-credential-profile/_utils.ts index 937d44d5..7d8aef7d 100644 --- a/frontend/src/components/thread/tool-views/create-credential-profile/_utils.ts +++ b/frontend/src/components/thread/tool-views/create-credential-profile/_utils.ts @@ -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 => { @@ -93,7 +94,7 @@ const extractFromLegacyFormat = (content: any): Omit { 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 => { @@ -91,16 +92,20 @@ const extractFromLegacyFormat = (content: any): Omit 0 ? toolLegacy.auth_schemes : assistantLegacy.auth_schemes, actualIsSuccess: isSuccess, actualToolTimestamp: toolTimestamp, actualAssistantTimestamp: assistantTimestamp diff --git a/frontend/src/components/thread/tool-views/get-app-details/get-app-details.tsx b/frontend/src/components/thread/tool-views/get-app-details/get-app-details.tsx index e02f09de..bf4549c2 100644 --- a/frontend/src/components/thread/tool-views/get-app-details/get-app-details.tsx +++ b/frontend/src/components/thread/tool-views/get-app-details/get-app-details.tsx @@ -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 ? (
- {app.logo_url ? ( + {toolkit.logo_url ? ( {`${app.name} { const target = e.target as HTMLImageElement; @@ -150,7 +148,7 @@ export function GetAppDetailsToolView({ )}
- {app.is_verified && ( + {supports_oauth && (
@@ -160,13 +158,13 @@ export function GetAppDetailsToolView({
-
+
-
+

- {app.name} + {toolkit.name}

- {app.is_verified && ( + {supports_oauth && ( @@ -182,20 +180,20 @@ export function GetAppDetailsToolView({ )}

- {app.app_slug} + {toolkit.toolkit_slug}

{(() => { - const AuthIcon = getAuthTypeIcon(app.auth_type); + const AuthIcon = getAuthTypeIcon(auth_schemes); return ( - {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'} ); })()} @@ -203,12 +201,12 @@ export function GetAppDetailsToolView({

- {app.description} + {toolkit.description}

- {app.tags && app.tags.length > 0 && ( + {toolkit.tags && toolkit.tags.length > 0 && (
- {app.tags.map((tag, tagIndex) => ( + {toolkit.tags.map((tag, tagIndex) => ( )} - -
- {app.url && ( - - )} -
- {(app.pricing || app.setup_instructions) && ( -
- {app.pricing && ( -
-
- -

Pricing

-
-

- {app.pricing || 'No pricing information available'} -

-
- )} - - {app.setup_instructions && ( -
-
- -

Setup

-
-

- {app.setup_instructions || 'No setup instructions available'} -

-
- )} -
- )} - - {(app.available_actions?.length || app.available_triggers?.length) ? ( -
- {app.available_actions && app.available_actions.length > 0 && ( -
-
- -

Available Actions

- - {app.available_actions.length} - -
-
- {app.available_actions.slice(0, 5).map((action: any, index: number) => ( -
-
- - {action.name || action.display_name || action} - -
- ))} - {app.available_actions.length > 5 && ( -

- +{app.available_actions.length - 5} more actions -

- )} -
-
- )} - - {app.available_triggers && app.available_triggers.length > 0 && ( -
-
- -

Available Triggers

- - {app.available_triggers.length} - -
-
- {app.available_triggers.slice(0, 5).map((trigger: any, index: number) => ( -
-
- - {trigger.name || trigger.display_name || trigger} - -
- ))} - {app.available_triggers.length > 5 && ( -

- +{app.available_triggers.length - 5} more triggers -

- )} -
-
- )} -
- ) : ( -
- -

- No actions or triggers available for this integration -

+ {toolkit.categories && toolkit.categories.length > 0 && ( +
+
+ +

Categories

+
+
+ {toolkit.categories.map((category, index) => ( + + {category} + + ))} +
)}
@@ -342,7 +253,7 @@ export function GetAppDetailsToolView({ No app details found

- {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'}

diff --git a/frontend/src/components/thread/tool-views/get-credential-profiles/_utils.ts b/frontend/src/components/thread/tool-views/get-credential-profiles/_utils.ts index b243f57c..28279ed5 100644 --- a/frontend/src/components/thread/tool-views/get-credential-profiles/_utils.ts +++ b/frontend/src/components/thread/tool-views/get-credential-profiles/_utils.ts @@ -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 0 ? toolLegacy.profiles : assistantLegacy.profiles, total_count: toolLegacy.total_count || assistantLegacy.total_count, diff --git a/frontend/src/components/thread/tool-views/get-credential-profiles/get-credential-profiles.tsx b/frontend/src/components/thread/tool-views/get-credential-profiles/get-credential-profiles.tsx index 716f518e..b39c9e5d 100644 --- a/frontend/src/components/thread/tool-views/get-credential-profiles/get-credential-profiles.tsx +++ b/frontend/src/components/thread/tool-views/get-credential-profiles/get-credential-profiles.tsx @@ -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 ( @@ -80,9 +73,9 @@ export function GetCredentialProfilesToolView({ {toolTitle} - {app_slug && ( + {toolkit_slug && (

- App: {app_slug} + Toolkit: {toolkit_slug}

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

- {profile.app_name} + {profile.toolkit_name}

{profile.profile_name !== profile.display_name && (

@@ -169,7 +162,6 @@ export function GetCredentialProfilesToolView({

{(() => { const connectionStatus = getConnectionStatus(profile.is_connected); - const activeStatus = getActiveStatus(profile.is_active); const ConnectionIcon = connectionStatus.icon; return ( @@ -181,12 +173,6 @@ export function GetCredentialProfilesToolView({ {connectionStatus.text} - - {activeStatus.text} - ); })()} @@ -207,7 +193,7 @@ export function GetCredentialProfilesToolView({ No profiles found

- {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'}

diff --git a/frontend/src/components/thread/tool-views/search-mcp-servers/_utils.ts b/frontend/src/components/thread/tool-views/search-mcp-servers/_utils.ts index d7a42a1b..a87077b4 100644 --- a/frontend/src/components/thread/tool-views/search-mcp-servers/_utils.ts +++ b/frontend/src/components/thread/tool-views/search-mcp-servers/_utils.ts @@ -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 { diff --git a/frontend/src/components/thread/tool-views/search-mcp-servers/search-mcp-servers.tsx b/frontend/src/components/thread/tool-views/search-mcp-servers/search-mcp-servers.tsx index ccf67088..c59dcc12 100644 --- a/frontend/src/components/thread/tool-views/search-mcp-servers/search-mcp-servers.tsx +++ b/frontend/src/components/thread/tool-views/search-mcp-servers/search-mcp-servers.tsx @@ -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({
{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 (
)}
- {result.is_verified && ( + {hasOAuth && (
-
+
@@ -179,32 +176,32 @@ export function SearchMcpServersToolView({

{result.name}

- {result.is_verified && ( + {hasOAuth && (
- +
-

Verified integration

+

OAuth2 supported

)}

- {result.app_slug} + {result.toolkit_slug}

- {result.auth_type?.replace('_', ' ') || 'Unknown'} + {getPrimaryAuthScheme(result.auth_schemes)}
@@ -215,17 +212,6 @@ export function SearchMcpServersToolView({

- {result.url && ( - - )}