diff --git a/backend/pipedream/api.py b/backend/pipedream/api.py index 153c12b8..0dd5a43e 100644 --- a/backend/pipedream/api.py +++ b/backend/pipedream/api.py @@ -13,6 +13,9 @@ from .domain.exceptions import ( ConnectionNotFoundError, AppNotFoundError, MCPConnectionError, ValidationException, PipedreamException ) +# Add HTTPX and JSON for tools listing endpoint +import httpx +import json router = APIRouter(prefix="/pipedream", tags=["pipedream"]) pipedream_manager: Optional[PipedreamManager] = None @@ -478,6 +481,40 @@ async def get_app_icon(app_slug: str): logger.error(f"Failed to fetch icon for app {app_slug}: {str(e)}") raise _handle_pipedream_exception(e) +@router.get("/apps/{app_slug}/tools") +async def get_app_tools(app_slug: str): + logger.info(f"Getting tools for app: {app_slug}") + 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"} + try: + 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() + tools = [] + 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: + tool["description"] = desc[:idx].strip() + break + except json.JSONDecodeError: + logger.warning(f"Failed to parse JSON data: {data_str}") + continue + return {"success": True, "tools": tools} + except httpx.HTTPError as e: + logger.error(f"HTTP error when fetching tools for app {app_slug}: {e}") + raise HTTPException(status_code=502, detail="Bad Gateway") + except Exception as e: + logger.error(f"Unexpected error when fetching tools for app {app_slug}: {e}") + raise HTTPException(status_code=500, detail="Internal server error") @router.post("/profiles", response_model=ProfileResponse) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bd5d3438..cd16e9d3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -63,6 +63,7 @@ "comment-json": "^4.2.5", "date-fns": "^3.6.0", "diff": "^7.0.0", + "extract-colors": "^4.2.0", "flags": "^3.2.0", "framer-motion": "^12.6.5", "geist": "^1.2.1", @@ -8660,6 +8661,12 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/extract-colors": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/extract-colors/-/extract-colors-4.2.0.tgz", + "integrity": "sha512-+0X1EMDSea/upbny8RMEoBZdB0in6yiWFVDNZy2d/ehXINRCly+PXC8gp7enSJomeB9dhD7Sud4jzROStuHOJA==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 883db918..c3d25db8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -66,6 +66,7 @@ "comment-json": "^4.2.5", "date-fns": "^3.6.0", "diff": "^7.0.0", + "extract-colors": "^4.2.0", "flags": "^3.2.0", "framer-motion": "^12.6.5", "geist": "^1.2.1", diff --git a/frontend/src/components/agents/pipedream/_components/app-card.tsx b/frontend/src/components/agents/pipedream/_components/app-card.tsx index 97a35016..9eee0843 100644 --- a/frontend/src/components/agents/pipedream/_components/app-card.tsx +++ b/frontend/src/components/agents/pipedream/_components/app-card.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo } from 'react'; import { Button } from '@/components/ui/button'; -import { Card, CardContent } from '@/components/ui/card'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { DropdownMenu, @@ -8,12 +8,16 @@ import { DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { Plus, Settings, Zap, Bot, ChevronDown, Star, CheckCircle, Eye } from 'lucide-react'; +import { Plus, Settings, Zap, Bot, ChevronDown, Star, CheckCircle, Eye, ExternalLink, Info } from 'lucide-react'; import { cn } from '@/lib/utils'; import { getCategoryEmoji } from '../utils'; import type { AppCardProps } from '../types'; import { usePipedreamProfiles } from '@/hooks/react-query/pipedream/use-pipedream-profiles'; import { usePipedreamAppIcon } from '@/hooks/react-query/pipedream/use-pipedream'; +import { usePipedreamAppTools } from '@/hooks/react-query/pipedream/use-pipedream'; +import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Skeleton } from '@/components/ui/skeleton'; export const AppCard: React.FC = ({ app, @@ -28,6 +32,10 @@ export const AppCard: React.FC = ({ handleCategorySelect, }) => { const [isHovered, setIsHovered] = useState(false); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const { data: appToolsData, isLoading: isToolsLoading } = usePipedreamAppTools(app.name_slug, { enabled: isDialogOpen }); + const tools = appToolsData?.tools ?? []; + const { data: profiles } = usePipedreamProfiles(); const { data: iconData } = usePipedreamAppIcon(app.name_slug, { enabled: !app.img_src @@ -108,12 +116,159 @@ export const AppCard: React.FC = ({ )} -

{app.name}

+ + + + + Info + + + + {app.name} Tools +
+
+
+
+
+
+ {(app.img_src || iconData?.icon_url) ? ( + {`${app.name} + ) : ( + + {app.name.charAt(0).toUpperCase()} + + )} +
+
+

{app.name}

+ + {tools.length} {tools.length === 1 ? 'Tool' : 'Tools'} Available + +
+
+

+ {app.description} +

+
+ {isConnected && ( +
+
+ + + Connected ({connectedProfiles.length} profile{connectedProfiles.length !== 1 ? 's' : ''}) + +
+
+ )} +
+ +
+
+
+
+
+

Available Tools

+

+ {tools.length} tool{tools.length !== 1 ? 's' : ''} ready to integrate +

+
+ +
+ {isToolsLoading ? ( +
+ + + + + + +
+ ) : tools.length > 0 ? ( +
+ {tools.map((tool, index) => ( + + + +
+
+ +
+
+

+ {tool.name} +

+

+ {tool.description} +

+
+
+
+
+
+ ))} +
+ ) : ( +
+
+
+ +
+

No tools available

+

Check back later for updates

+
+
+ )} +
+
+
+
+
+

{app.description} @@ -201,4 +356,4 @@ export const AppCard: React.FC = ({ ); -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 8c76a870..138daea4 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -28,7 +28,7 @@ const buttonVariants = cva( size: { default: 'h-9 px-4 py-2 has-[>svg]:px-3', sm: 'h-8 rounded-lg gap-1.5 px-3 has-[>svg]:px-2.5', - lg: 'h-10 rounded-lg px-6 has-[>svg]:px-4', + lg: 'h-10 rounded-xl px-6 has-[>svg]:px-4', icon: 'size-9', node_secondary: 'px-0', }, diff --git a/frontend/src/hooks/react-query/pipedream/use-pipedream.ts b/frontend/src/hooks/react-query/pipedream/use-pipedream.ts index 39759948..086ef948 100644 --- a/frontend/src/hooks/react-query/pipedream/use-pipedream.ts +++ b/frontend/src/hooks/react-query/pipedream/use-pipedream.ts @@ -11,6 +11,7 @@ import { type PipedreamAppResponse, type PipedreamToolsResponse, type AppIconResponse, + type PipedreamTool, } from './utils'; import { pipedreamKeys } from './keys'; @@ -129,3 +130,15 @@ export const usePipedreamAvailableTools = createQueryHook( retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), } ); + +export const usePipedreamAppTools = (appSlug: string, options?: { enabled?: boolean }) => { + return useQuery({ + queryKey: ['pipedream', 'app-tools', appSlug], + queryFn: async (): Promise<{ success: boolean; tools: PipedreamTool[] }> => { + return await pipedreamApi.getAppTools(appSlug); + }, + enabled: options?.enabled ?? !!appSlug, + staleTime: 5 * 60 * 1000, + retry: 2, + }); +}; diff --git a/frontend/src/hooks/react-query/pipedream/utils.ts b/frontend/src/hooks/react-query/pipedream/utils.ts index ff7f28e1..11fffb02 100644 --- a/frontend/src/hooks/react-query/pipedream/utils.ts +++ b/frontend/src/hooks/react-query/pipedream/utils.ts @@ -405,14 +405,26 @@ export const pipedreamApi = { const result = await backendApi.get( `/pipedream/apps/${appSlug}/icon`, { - errorContext: { operation: 'get app icon', resource: 'Pipedream app icon' }, + errorContext: { operation: 'load app icon', resource: 'Pipedream app icon' }, } ); if (!result.success) { throw new Error(result.error?.message || 'Failed to get app icon'); - } + } + return result.data!; }, - + async getAppTools(appSlug: string): Promise<{ success: boolean; tools: PipedreamTool[] }> { + const result = await backendApi.get<{ success: boolean; tools: PipedreamTool[] }>( + `/pipedream/apps/${appSlug}/tools`, + { + errorContext: { operation: 'load app tools', resource: 'Pipedream app tools' }, + } + ); + if (!result.success) { + throw new Error(result.error?.message || 'Failed to get app tools'); + } + return result.data!; + }, }; \ No newline at end of file